小节三:实现 @tool 自动注册——让“定义工具”和“注册工具”变成一件事

如果每次定义 Tool 都要:

def read_file(...):
    ...

registry.register(
    Tool(
        name="read_file",
        description="...",
        func=read_file,
    )
)

当然没问题。

但工具多了以后,会出现一个很常见的问题,函数写了,忘了 register。

于是模型看不到它。

或者别人只看到函数文件,不知道它到底是不是 Agent Tool。

更自然的写法是:

@tool
def read_file(path: str):
    ...

一看就知道,这是 Agent Tool。

而且定义完成自动注册。

装饰器本质上在做什么?

Python:

@tool
def read_file(...):
    ...

等价于:

read_file = tool(
    read_file
)

所以 tool() 函数可以:

  1. 读取函数名;
  2. 读取 docstring;
  3. 创建 Tool 对象;
  4. 注册到 Registry;
  5. 返回原函数。

这样函数仍然可以正常调用。

最小自动注册装饰器

from typing import Callable


registry = ToolRegistry()


def tool(
    func: Callable,
):
    registry.register(
        Tool(
            name=func.__name__,
            description=(
                func.__doc__
                or ""
            ),
            func=func,
        )
    )

    return func

然后:

@tool
def read_file(path: str):
    """读取文本文件。"""
    ...

完整可运行示例

运行后你会看到:

已注册工具:
- read_file: 读取指定文本文件。
- list_files: 列出当前项目文件。
- run_python: 模拟执行 Python 代码。

完全没有手工:

registry.register(read_file)

怎么支持自定义名字?

有时候 Python 函数名和对外 Tool Name 不想一样。

可以设计:

@tool(name="read_text_file")
def read_file(...):
    ...

这需要“带参数装饰器”。

from __future__ import annotations
def tool(
    name: str | None = None,
    description: str | None = None,
):
    def decorator(
        func: Callable[..., Any],
    ):
        tool_name = (
            name
            or func.__name__
        )

        tool_description = (
            description
            or func.__doc__
            or "暂无描述"
        ).strip()

        registry.register(
            Tool(
                name=tool_name,
                description=tool_description,
                func=func,
            )
        )

        return func

    return decorator

使用:

@tool(
    name="read_text_file",
    description="读取 UTF-8 文本文件",
)
def read_file(path: str):
    ...

完整升级版

自动注册这一步,最大的价值不是“写法更酷”,而是减少手工同步时的漏项。定义工具和写入 Registry 变成一个动作以后,工具系统会稳定很多。

图片[1]-小节三:实现 @tool 自动注册——让“定义工具”和“注册工具”变成一件事-大赛人网

你可以把这个过程理解成:写一次函数定义,同时完成“创建 Tool”与“写入 Registry”两件事。尤其当工具数量上来以后,这种自动化能明显减少低级错误。

from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable


@dataclass
class Tool:
    name: str
    description: str
    func: Callable[..., Any]


class ToolRegistry:

    def __init__(self):
        self._tools: dict[
            str,
            Tool
        ] = {}

    def register(
        self,
        tool: Tool,
    ) -> None:
        if tool.name in self._tools:
            raise ValueError(
                f"工具已存在:{tool.name}"
            )

        self._tools[
            tool.name
        ] = tool

    def list_tools(
        self,
    ) -> list[Tool]:
        return list(
            self._tools.values()
        )


registry = ToolRegistry()


def tool(
    name: str | None = None,
    description: str | None = None,
):
    def decorator(
        func: Callable[..., Any],
    ):
        tool_name = (
            name
            or func.__name__
        )

        tool_description = (
            description
            or func.__doc__
            or "暂无描述"
        ).strip()

        registry.register(
            Tool(
                name=tool_name,
                description=tool_description,
                func=func,
            )
        )

        return func

    return decorator


@tool()
def list_files() -> list[str]:
    """列出项目文件。"""

    return [
        "README.md",
        "app.py",
    ]


@tool(
    name="read_text_file",
    description=(
        "读取 UTF-8 文本文件"
    ),
)
def read_file(
    path: str,
) -> str:
    return (
        f"模拟读取:{path}"
    )


if __name__ == "__main__":
    for item in (
        registry.list_tools()
    ):
        print(
            item.name,
            item.description,
        )

这个模式后面会非常好用。

自动注册也不是越“魔法”越好

这里也提醒一下。

我们可以做:

自动扫描目录
自动 import 所有模块
自动发现函数
自动注册

但课程当前阶段不建议一上来这样做。

因为过度自动化会让“一个工具为什么出现在 Registry”变得不透明。

现在最清楚的方式就是:

@tool(...)

看到装饰器就知道它会注册。

这对教学、调试和安全都更友好。

装饰器注册还会带来一个很实际的 Python 行为:模块被 import 时,装饰器就会立即执行。若使用全局 Registry,测试导入顺序、重复加载和插件初始化都可能影响最终工具集合。更稳的工程做法通常是在应用启动阶段创建 Registry,再由明确的注册函数装配 Tool;@tool 可以继续负责生成 Tool 定义,但不一定要偷偷写入全局对象。

配套源码目前选择了更直接的方式:使用 ToolSpec 保存函数与元数据,通过 registry.register(name, func, ...) 显式注册,没有实现全局 @tool 自动注册。这不是遗漏,而是保留了一条更容易检查的装配路径。正文中的装饰器示例可以看作下一步扩展方案。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像-大赛人网
欢迎您留下宝贵的见解!
提交
头像-大赛人网

昵称

取消
昵称

    请登录后查看评论内容