第 4 章 · 01 SPEC §3.2 Tool interface 与 Registry


文档摘要

第 4 章 · 01 SPEC §3.2 Tool interface 与 Registry 本节摘要:本节精读 SPEC §3.2 的 Tool 抽象。Tool 是一个五方法 interface( / / / / ),与 Provider 同构但多了"参数 JSON Schema"与"只读标记"两个 LLM 工具特有概念。注册表分两层:进程全局 map(由 在 填充)+ 每次运行 实例(由 enabled 内置 + 插件工具组装,Agent 只看 )。

第 4 章 · 01 SPEC §3.2 Tool interface 与 Registry

本节摘要:本节精读 SPEC §3.2 的 Tool 抽象。Tool 是一个五方法 interface(Name/Description/Schema/Execute/ReadOnly),与 Provider 同构但多了"参数 JSON Schema"与"只读标记"两个 LLM 工具特有概念。注册表分两层:进程全局 builtins map(由 tool.RegisterBuiltin(t)init() 填充)+ 每次运行 *Registry 实例(由 enabled 内置 + 插件工具组装,Agent 只看 *Registry)。本节讲清这两层的关系、Add 时 schema 一次性规范化(保证每轮 Schemas() 字节稳定以保 prefix cache)、docs/TOOL_CONTRACT.md 工具契约文档与代码契约测试的双重锁定、工具参数用 santhosh-tekuri/jsonschema/v6 校验、以及"工具 schema 变化会破坏 prefix cache"这条关键约束。

内容来源:原项目源码 internal/tool/tool.go(Tool interface + Registry + RegisterBuiltin)、internal/tool/builtin/*.go(各工具 init 注册)、docs/SPEC.md §3.2、docs/TOOL_CONTRACT.md

⚠️ 注意:工具 schema 进入系统提示前缀,因此schema 变化会破坏 prefix cache。这就是为什么 Registry.Add 时要一次性 CanonicalizeSchema,之后每轮 Schemas() 都复用规范化结果,绝不在热路径重新 marshal。

学习目标

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

  1. 默写 Tool interface 的五个方法。
  2. 区分进程全局 builtins map 与每次运行 *Registry 实例。
  3. 解释 Registry.Add 为何要 CanonicalizeSchema
  4. 说清 Schemas() 为什么按 name 排序输出。
  5. 描述 TOOL_CONTRACT.mdTestBuiltinToolContractDocumentation 测试如何双重锁定工具表面。
  6. 列举至少 8 个内置工具及其只读标记。

一、Tool interface:五个方法

SPEC §3.2 给出 Tool interface(docs/SPEC.md:110-116,对应 internal/tool/tool.go:19-33):

type Tool interface { Name() string Description() string Schema() json.RawMessage // JSON Schema for parameters Execute(ctx context.Context, args json.RawMessage) (string, error) ReadOnly() bool }

五个方法:

  • Name() string:工具名(如 "bash""read_file"),模型在 tool call 里引用。
  • Description() string:工具描述,进入系统提示,影响模型何时调用它。bash 工具的 Description 还会根据当前 shell(bash 还是 PowerShell)动态生成不同文本(internal/tool/builtin/bash.go:103-120),这是少数会变的描述。
  • Schema() json.RawMessage:参数的 JSON Schema,模型据此生成合法参数。
  • Execute(ctx, args) (string, error):执行工具,返回结果文本喂回模型。SPEC §3.2(docs/SPEC.md:126-127):Execute 自己解析 raw JSON args;错误返回而非致命——Agent 把错误喂回模型,让模型自我纠正。
  • ReadOnly() bool:工具是否有可观察的副作用。Agent 只在 batch 里所有调用都 ReadOnly 时才并行化;混合 batch 保持顺序,保 write/read 顺序。bash 与插件工具必须返回 false,因为副作用无法从参数静态推断。

与 Provider 比,Tool 多了 Schema(参数契约)、Description(模型可见说明)、ReadOnly(并行调度依据)——这是 LLM 工具调用的特有需求。Tool 没有 Factory——因为工具是无状态的(每次调用独立),不需要从 Config 构造;直接 RegisterBuiltin(t) 注册实例即可。

二、两层注册表:全局 builtins 与运行时 Registry

SPEC §3.2(docs/SPEC.md:118-122)描述了两层结构:

Built-in tools self-register into a process-global builtin set via init() (tool.RegisterBuiltin(t)); tool.Builtins() lists them. A runtime *Registry is assembled per run: enabled built-ins (filtered by config) plus plugin-provided tools. The agent only sees the *Registry.

两层:

第一层:进程全局 builtins(internal/tool/tool.go:237-267):

var builtins = map[string]Tool{} func RegisterBuiltin(t Tool) { name := t.Name() if _, dup := builtins[name]; dup { panic("tool: duplicate built-in " + name) } builtins[name] = t } func Builtins() []Tool { ... } // 按 name 排序返回 func LookupBuiltin(name string) (Tool, bool) { ... }

与 Provider 的 Register 同构:全局 map、init() 调用、重复注册 panic。区别是没有 Factory——直接注册 Tool 实例。

第二层:每次运行 *Registry(internal/tool/tool.go:271-303):

type Registry struct { mu sync.RWMutex tools map[string]Tool order []string canon map[string]json.RawMessage suspended map[string]bool } func (r *Registry) Add(t Tool) { r.mu.Lock() defer r.mu.Unlock() name := t.Name() // ... suspended 前缀检查 ... if _, ok := r.tools[name]; !ok { r.order = append(r.order, name) } r.tools[name] = t r.canon[name] = provider.CanonicalizeSchema(t.Schema()) // 关键:一次性规范化 }

*Registry每次运行新建的:把 config 里 enabled 的内置工具(过滤后的 Builtins())加上插件提供的工具(MCP 适配器)全部 Add 进去。Agent 只看 *Registry,看不到全局 builtins

为什么分两层?因为:

  • 全局层负责"有哪些可用工具"(编译期装配,init() 填充)。
  • 运行时层负责"本次运行启用哪些"(config 过滤 + 插件动态加入),且支持运行时 suspend/resume(MCP 服务器断连时 SuspendPrefix 移除其命名空间,重连时 ResumePrefix)。

三、Add 时一次性 CanonicalizeSchema:缓存友好的关键

Registry.Add 里这一行是 prefix cache 友好的核心(internal/tool/tool.go:302):

r.canon[name] = provider.CanonicalizeSchema(t.Schema())

工具 schema 注册时就一次性规范化(排序 key、统一格式),之后存进 canon map。每轮 Schemas() 直接复用 canon 里的字节,绝不在热路径重新 marshal。

为什么这么重要?SPEC §3.2(docs/SPEC.md:123-125):

Tool schemas are canonicalized on registry insertion. The built-in contract is documented in TOOL_CONTRACT.md and backed by tests that compare the documented surface against the same canonical schema path.

工具 schema 进入系统提示前缀(模型每轮都看到全部工具定义)。如果 schema 字节每轮抖动(比如 map key 顺序随机),prefix cache 就会反复 miss——这是 Reasonix 最不能容忍的事。规范化的本质是把"语义相同但字节不同"的 schema 归一化成唯一字节序列,保证 prefix 稳定。

Schemas() 输出按 name 排序(internal/tool/tool.go:523-544):

func (r *Registry) Schemas() []provider.ToolSchema { r.mu.RLock() defer r.mu.RUnlock() names := make([]string, len(r.order)) copy(names, r.order) sort.Strings(names) // 按 name 排序 out := make([]provider.ToolSchema, 0, len(names)) for _, name := range names { t := r.tools[name] if t == nil { continue } out = append(out, provider.ToolSchema{ Name: t.Name(), Description: t.Description(), Parameters: r.canon[name], // 复用规范化结果 }) } return out }

排序的目的也是 prefix 稳定——工具顺序固定,前缀字节才稳定。

💡 契约要点:工具 schema 是 prefix cache 敏感路径。三道保险:① AddCanonicalizeSchema;② 存进 canon 复用;③ Schemas() 按 name 排序。改工具 schema 是高影响动作,PR 必须带 Cache-impact: 元数据(见 REASONIX.md)。

四、builtin init() 自注册:工具清单

internal/tool/builtin/ 下每个工具文件都有一个 init() 调用 tool.RegisterBuiltin。完整清单(从源码 grep):

文件 init 注册 工具名 只读
bash.go:34 bash{} bash false
bgjobs.go:24-26 bashOutput{}/killShell{}/waitJob{} bash_output/kill_shell/wait true/false/true
codeindex.go:20 codeIndex{} code_index true
completestep.go:16 completeStep{} complete_step true
glob.go:21 globTool{} glob true
grep.go:64 grepTool{} grep true
ls.go:14 listDir{} ls true
readfile.go:27 readFile{} read_file true
editfile.go:11 editFile{} edit_file false
multiedit.go:11 multiEdit{} multi_edit false
writefile.go:14 writeFile{} write_file false
movefile.go:16 moveFile{} move_file false
delete_range.go:14 deleteRange{} delete_range false
delete_symbol.go:18 deleteSymbol{} delete_symbol false
notebookedit.go:14 notebookEdit{} notebook_edit false
todo.go:13 todoWrite{} todo_write true
webfetch.go:26 webFetch{} web_fetch true
updategoal.go:12 updateGoal{} update_goal true

规律:读类工具(read_file/grep/glob/ls/code_index/web_fetch/bash_output/wait)都是 true;写类工具(write_file/edit_file/multi_edit/move_file/delete_*/notebook_edit/bash/kill_shell)都是 false。bash 必须false,因为命令副作用无法从参数静态推断(一句 rm -rf 与一句 ls 参数结构类似)。

这些工具的具体实现是后续两节(bash 第 2 节、codeindex 第 3 节)的主题,以及第 5 章 Agent 会话用工具时再展开。

五、TOOL_CONTRACT.md:文档与测试双重锁定

docs/TOOL_CONTRACT.md 是工具表面契约文档,顶部有张表列出每个工具的名字、只读标记、描述。例如 bash 行:

| bash | false | Execute a command in the shell and return combined stdout/stderr. Use for builds, tests, git, package managers, etc. ... |

关键在文档末尾(docs/TOOL_CONTRACT.md Schema Snapshot 段):

The exact canonical schemas are intentionally tested in code rather than copied by hand here. Run:

go test ./internal/tool -run TestBuiltinToolContractDocumentation

The test checks that every registered built-in tool has a documented name, read-only flag, description row, and canonical schema generated by tool.BuiltinContractEntries.

这是双重锁定:文档表 + 代码测试。改工具表面(改名、改只读、改描述、改 schema)必须同步改文档,否则 TestBuiltinToolContractDocumentation 失败。这把"工具表面稳定"从口头约定变成 CI 强制——又是 SPEC "Change the contract first, then the code" 的具体兑现。

工具描述进系统提示,影响模型调用决策,也影响 prefix cache(描述字节稳定才 cache 命中)。所以改工具描述同样是高影响动作。

六、工具参数 JSON Schema 校验

工具参数用 santhosh-tekuri/jsonschema/v6 校验(满足 SPEC §1.3 纯 Go 轻量依赖)。校验发生在两处:

  • 注册时:CanonicalizeSchema 顺带做结构校验,保证 schema 本身合法。
  • 执行时:工具 Execute 自己解析 raw JSON args。SPEC §3.2(docs/SPEC.md:126-127):

Execute parses raw JSON args itself. Errors are returned, not fatal — the agent feeds them back so the model can self-correct.

工具自己解析参数,错误返回而非致命——Agent 把错误文本喂回模型,模型下一轮会修正参数重试。这种"错误可恢复"设计让工具调用更鲁棒。

jsonschema 校验的好处:模型生成的参数若违反 schema(类型错、缺必填、枚举越界),工具层能给出精确错误信息(哪个字段、什么问题),模型据此修正——比"参数错误"这种模糊提示有效得多。

本节要点回顾

  1. Tool interface:Name/Description/Schema/Execute/ReadOnly 五方法;无 Factory(工具无状态,直接注册实例)。
  2. 两层注册表:全局 builtins(init 填充,RegisterBuiltin 重复 panic)+ 每次运行 *Registry(enabled 内置 + 插件,Agent 只看这个)。
  3. Add 时 CanonicalizeSchema:Registry.Add 一次性规范化 schema 存进 canon,Schemas() 复用——保证 prefix cache 友好。
  4. Schemas 按 name 排序:工具顺序固定,前缀字节稳定。
  5. TOOL_CONTRACT.md 双重锁定:文档表 + TestBuiltinToolContractDocumentation 测试;改工具表面必须同步改文档。
  6. JSON Schema 校验:santhosh-tekuri/jsonschema/v6(纯 Go);工具 Execute 自己解析 args,错误返回喂回模型自我纠正。
  7. 内置工具清单:读类(read/grep/glob/ls/code_index/web_fetch)只读;写类(write/edit/multi_edit/move/delete/bash)非只读。

下一节,我们精读 bash 工具——它不是直接 system 调用,而是用 mvdan.cc/sh v3 做 POSIX shell 语法树分析,这是 Reasonix 安全与可控执行的关键设计。


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