主题
8.4 工具注册与分发机制:@tool 装饰器与 JSON Schema 自动生成
“一个优秀的工匠不必死记硬背每个螺丝刀的尺寸,工具箱上的标签(Type Hints)与说明书(Docstring)会自动告诉大模型该如何正确使用它!”
🧰 大模型是如何调用工具的?Function Calling 底层协议揭秘
在 8.2 节中,我们使用的是简单的正则表达式来解析 Action: calculate[...]。但在现代工业级 Agent 中,所有主流大模型(包括智谱 BigModel托管的 GLM-V3 / R1)都原生支持标准化的 Function Calling(函数调用)协议。
Function Calling 握手三步走:
- 开发者提供菜单(Tools Schema):在 API 请求中附带一个 JSON Schema 列表,告诉模型有哪些工具可用、需要什么参数;
- 模型点菜(Tool Calls):模型判断需要查数据时,不返回普通正文,而是返回
tool_calls结构(包含函数名与 JSON 参数); - 环境上菜并汇报(Tool Results):后端本地运行该函数,将真实结果封装为
role: "tool"消息再发回给模型,模型整合后生成最终回答。
🪄 手写 @tool 装饰器:告别手写冗长 JSON Schema
手写符合 OpenAI 规范的 JSON Schema 极其繁琐容易出错。我们利用 Python 内置的 inspect 模块,编写一个 @registry.register 装饰器,自动从普通 Python 函数中提取类型注解与注释!
python
# 核心装饰器实现原理 (code/s04_tool_registry.py)
def register(self, func: Callable) -> Callable:
name = func.__name__
doc = inspect.getdoc(func) or "没有提供函数说明"
sig = inspect.signature(func)
properties = {}
required = []
for param_name, param in sig.parameters.items():
param_type = param.annotation
properties[param_name] = {
"type": python_type_to_json_type(param_type), # 类型映射
"description": f"参数 {param_name}",
}
if param.default == inspect.Parameter.empty:
required.append(param_name)
schema = {
"type": "function",
"function": {
"name": name,
"description": doc,
"parameters": {
"type": "object",
"properties": properties,
"required": required,
},
},
}
self.tools[name] = func
self.schemas.append(schema)
return func开发者使用体验:就像写普通 Python 函数一样简单!
python
@registry.register
def calculate_salary(base: float, bonus: float, tax_rate: float = 0.1) -> str:
"""计算员工税后净收入"""
total = (base + bonus) * (1 - tax_rate)
return f"税后净收入: {total:.2f} 元"只要加上 @registry.register,系统就会自动生成如下标准 Schema 并提供给 GLM:
json
{
"type": "function",
"function": {
"name": "calculate_salary",
"description": "计算员工税后净收入",
"parameters": {
"type": "object",
"properties": {
"base": {"type": "number", "description": "参数 base"},
"bonus": {"type": "number", "description": "参数 bonus"},
"tax_rate": {"type": "number", "description": "参数 tax_rate"}
},
"required": ["base", "bonus"]
}
}
}🕹️ 在 Gradio 中动手体验
在 code/app.py 中切换到 8.4 工具注册与分发 标签页:
- 查看右侧自动生成的 Tools JSON Schema;
- 输入指令:
帮我查询员工 101 的档案,并计算基本工资 30000 加上奖金 8000 的税后收入(税率 20%); - 点击 🛠️ 触发 Function Calling 路由;
- 你可以在面板中清晰观察到大模型发出的底层
tool_calls参数与执行后的回填报文!
🧰 进阶深化:Pi 的原子工具哲学与"工具报错规范化"(参考 Pi Agent)
为什么顶尖 Agent 只保留 4 个原子工具?
Pi Agent 走了一条与"堆砌上百个场景化工具"完全相反的路:只封装 4 个原子工具 —— read(读文件)、write(写文件)、edit(字符串匹配精准修改)、bash(执行命令)。就像 Unix 管道一样,Agent 通过自由组合这些原子工具就能自主编排出任意的场景化能力,同时把工具生态的维护成本压到最低。这也印证了 learn-claude-code 的格言 "Bash is all you need":工具越少,调用边界越稳定,Agent 越可控。
工具必须"报错规范化"
模型只能读懂文本,不能读懂异常堆栈。因此生产级工具在发生错误时,绝不能抛裸异常打断主循环,而要把错误转成一段结构化文本返回给模型,让它能据此反思重试:
python
@registry.register
def safe_div(a: float, b: float) -> str:
"""安全除法:任何异常都转成文本,让模型自己看原因重试"""
try:
return f"✅ 结果: {a / b:.4f}"
except ZeroDivisionError:
return f"❌ [工具报错] 除数 b 不能为 0,请换一个非零参数再试" # 报错即观测好的工具结果格式 =
状态 emoji + 结论 + 可执行的补救建议,这能让模型的下一轮 Action 更聪明。
Schema 只负责把入口说清楚
参数 Schema 能拦住类型错误,却不能判断一次调用是否符合业务规则。例如,amount 是合法数字,不代表这笔退款已经获得授权。因此,工具内部仍要执行权限检查、范围校验和业务约束,不能把模型传来的参数当成可信输入。
工具结果建议采用稳定结构,至少包含 status、data、error_code 和可供下一步使用的提示。面向用户的说明可以友好,面向 Agent 的错误码应保持稳定。这样既方便模型修正参数,也便于测试与统计。
📝 本节小结
- 自动化 Schema:通过
@tool与反射机制,彻底消除了手工维护 JSON 格式的痛苦; - 标准化握手:完全复用 OpenAI / 智谱 BigModel标准协议,工具生态可无缝迁移;
- 下一步:掌握了工具注册后,我们要为 Agent 赋予最关键的两把“开发工具”——8.5 终端执行与代码编辑!