如果每次定义 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() 函数可以:
- 读取函数名;
- 读取 docstring;
- 创建 Tool 对象;
- 注册到 Registry;
- 返回原函数。
这样函数仍然可以正常调用。
最小自动注册装饰器
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 自动注册——让“定义工具”和“注册工具”变成一件事-大赛人网](https://www.dsrw.com/wp-content/uploads/2026/11/image-1469.png)
你可以把这个过程理解成:写一次函数定义,同时完成“创建 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 自动注册。这不是遗漏,而是保留了一条更容易检查的装配路径。正文中的装饰器示例可以看作下一步扩展方案。
















请登录后查看评论内容