4.1 注册与 Schema:工具即 API 契约


4.1 注册与 Schema:工具即 API 契约

本节摘要:工具系统与模型之间只有一个接口:schema。模型看不见函数体、看不见异常类型、看不见你的目录结构——它每次决策时手里的全部信息,就是每个工具的名称、描述与参数 JSON Schema。因此工具定义不是代码注释,是运行时生效的 API 文档:描述含糊,模型就会错选、错参、错用时机。本节先立四要素契约(名称、描述、参数、返回),再给出装饰器注册器 registry.py(本书第二块积木,约 45 行):从 Python 函数签名与 docstring 自动生成 schema,注册即入册、入册即可被派发;然后是给模型写描述的清单与好坏对照;最后给出工具分类学——读、写、执行、检索、委派五类及其风险分级,为第 5 章权限设计备好输入。

学习目标

阅读完本节,你应当能够:

  1. 写出工具四要素契约,并说明每个要素分别影响模型的哪种行为。
  2. 实现装饰器注册器,自动完成"函数 → schema + 可执行项"的登记。
  3. 按清单改写一个含糊的工具描述,使其可被模型正确选用。
  4. 用五分类给工具集做风险分级。

一、四要素契约

要素 作用 影响模型的什么
名称(name) 唯一标识 选不选这个工具(与描述一起构成"索引词")
描述(description) 这个工具干什么、什么时候用、什么时候别用 选择准确率的最大变量
参数 Schema 每个参数的类型、含义、必填性、枚举值 调参正确率;枚举能显著降低自由发挥
返回约定 成功与失败各返回什么结构 模型对结果的解读与下一步决策(4.3 节)

一个原则贯穿四要素:契约面向模型而非面向程序员。参数叫 path 还是 file_path、描述写"读取文件"还是"读取 UTF-8 文本文件的完整内容,路径相对于工作区根目录"——对程序员等价,对模型天差地别。

二、装饰器注册器 registry.py

第 3 章 mini_loop.py 里手写了 TOOLS 清单与 TOOL_FUNCS 字典,两处维护必然漂移。正解是单一事实源:函数即工具,注册即登记。

# registry.py —— 装饰器工具注册器(写法示意,以官方文档为准) import inspect, json from typing import Callable _REGISTRY: dict[str, dict] = {} # name -> {"fn": fn, "schema": schema} def tool(fn: Callable) -> Callable: """装饰器:从函数签名 + docstring 自动生成 schema 并注册。""" sig = inspect.signature(fn) props, required = {}, [] for name, param in sig.parameters.items(): anno = param.annotation if param.annotation is not inspect.Parameter.empty else str props[name] = {"type": "string" if anno is str else "integer" if anno is int else "boolean" if anno is bool else "string", "description": ""} if param.default is inspect.Parameter.empty: required.append(name) doc = inspect.getdoc(fn) or "" _REGISTRY[fn.__name__] = { "fn": fn, "schema": {"type": "function", "function": { "name": fn.__name__, "description": doc.split("\n\n")[0], # 首段作描述 "parameters": {"type": "object", "properties": props, "required": required}}}, } return fn def schemas() -> list[dict]: """供主循环①感知拍使用:装入上下文 L3 层的契约清单。""" return [item["schema"] for item in _REGISTRY.values()] def dispatch(name: str, args: dict) -> str: """供主循环③行动拍使用:查册执行,错误也走 4.3 的结构化约定。""" item = _REGISTRY.get(name) if item is None: return json.dumps({"error": {"type": "unknown_tool", "message": f"未注册的工具: {name}"}}, ensure_ascii=False) try: return json.dumps({"ok": True, "result": item["fn"](**args)}, ensure_ascii=False, default=str) except Exception as exc: return json.dumps({"error": {"type": type(exc).__name__, "message": str(exc)}}, ensure_ascii=False)

用法与收益:

@tool def read_file(path: str) -> str: """读取 UTF-8 文本文件的完整内容。path 为相对于工作区根目录的路径。 适用于查看源码、配置与文档;不要用它读二进制文件。""" return open(path, encoding="utf-8").read()
  • 单一事实源:函数签名变了 schema 跟着变,消灭两处维护。
  • 接驳主循环schemas() 替换 mini_loop.py 的手写 TOOLSdispatch() 替换手写 TOOL_FUNCS——第 3 章积木与本章积木就此咬合。
  • 权限挂点dispatchitem["fn"](**args) 那一行之前,就是第 5 章权限门的插入位(与 3.1 节预告一致)。

三、给模型写描述的清单

描述是 schema 里杠杆最大的一项。一份自查清单:

  1. 干什么:一句话说清效果,不绕术语("读取……的完整内容")。
  2. 何时用 / 何时别用:与相邻工具划清边界(Grep 管"找内容在哪",Read 管"看完整文件")。
  3. 参数语义:路径相对什么、单位是什么、取值范围("相对于工作区根目录")。
  4. 副作用声明:有无写操作、是否幂等("重复调用结果一致")——这是模型评估风险与第 5 章分级的依据。
  5. 反例与边界:常见误用一句话点破("不要用它读二进制文件")。

好坏对照:

坏描述 好描述
"编辑文件" "对文件做精确字符串替换:给出旧串(必须与文件内容逐字一致)与新串。适合小改动;大范围重写请用 write_file"
"执行命令" "在项目根目录执行 shell 块。有副作用且不可自动撤销。只读检查类命令优先用 grep/glob 工具"

💡 一个省力技巧:把"何时别用"写成工具间的互相指引(Read 的描述里提 Grep,Grep 的描述里提 Read)——模型在边界场景里会顺着这些指引自我纠偏,这比堆十条例规都管用。

四、工具分类学:五类与风险分级

例子 副作用 风险级 权限默认(第 5 章用)
read_file、list_dir、glob 可 allow
检索 grep、web_search 无(外发查询除外) 可 allow(网络类单独看)
write_file、edit_file 改工作区 ask 或受限 allow
执行 bash、代码解释器 任意 ask + 沙箱
委派 task / 子智能体 间接全部 视子集而定 继承子集策略

两个设计结论:其一,"任意执行"是风险分水岭——第 2.3 节的规律(沙箱 ⟺ 允许任意执行)在这里落到工具分类上:提供 bash 类工具的那一刻,你就需要第 6 章的沙箱。其二,分类是给权限门的输入——第 5.1 节的 allowlist 将直接按这张表的"权限默认"列展开。

本节要点回顾

  1. 契约面向模型:模型手里只有 schema;名称影响选择、描述是最大变量、参数 Schema 管调参、返回约定管解读。
  2. registry.py:装饰器注册实现单一事实源,schemas() 喂感知拍、dispatch() 供行动拍,权限挂点已留好。
  3. 描述清单五条:干什么、何时用/别用、参数语义、副作用、反例;相邻工具互相指引效果好。
  4. 五分类:读 / 检索 / 写 / 执行 / 委派;"任意执行"是风险分水岭与沙箱开关。

工具定义好了,循环每次都把全部 schema 装进上下文 L3 层——工具少时没问题,多到几十个时呢?下一节讲工具选择与路由:什么时候让模型自己挑,什么时候在它前面加一道筛子。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U