Skip to content

8.4 工具注册与分发机制:@tool 装饰器与 JSON Schema 自动生成 ​

“一个优秀的工匠不必死记硬背每个螺丝刀的尺寸,工具箱上的标签(Type Hints)与说明书(Docstring)会自动告诉大模型该如何正确使用它!”


🧰 大模型是如何调用工具的?Function Calling 底层协议揭秘 ​

在 8.2 节中,我们使用的是简单的正则表达式来解析 Action: calculate[...]。但在现代工业级 Agent 中,所有主流大模型(包括智谱 BigModel托管的 GLM-V3 / R1)都原生支持标准化的 Function Calling(函数调用)协议。

Function Calling 握手三步走: ​

  1. 开发者提供菜单(Tools Schema):在 API 请求中附带一个 JSON Schema 列表,告诉模型有哪些工具可用、需要什么参数;
  2. 模型点菜(Tool Calls):模型判断需要查数据时,不返回普通正文,而是返回 tool_calls 结构(包含函数名与 JSON 参数);
  3. 环境上菜并汇报(Tool Results):后端本地运行该函数,将真实结果封装为 role: "tool" 消息再发回给模型,模型整合后生成最终回答。

Function Calling 握手三步走:


🪄 手写 @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 工具注册与分发 标签页:

  1. 查看右侧自动生成的 Tools JSON Schema;
  2. 输入指令:帮我查询员工 101 的档案,并计算基本工资 30000 加上奖金 8000 的税后收入(税率 20%);
  3. 点击 🛠️ 触发 Function Calling 路由;
  4. 你可以在面板中清晰观察到大模型发出的底层 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 终端执行与代码编辑!