一个工具的完整契约 · 两个 trait 一个 struct

进 grok 那近 15 万行的工具系统。这一章先立地基:在 grok 里「一个工具」到底是什么——一个类型上挂两个 trait(运行时契约 Tool + grok 元数据 ToolMetadata),它怎么声明 schema、怎么校验参数与配置、怎么以流的形式执行。

作者 David更新于 第 16 篇(共 60 篇)

一个工具的完整契约 · 两个 trait 一个 struct

Part 2 是全书最大的一部——工具系统 xai-grok-tools 如今已近 15 万行。但别慌,它有清晰的骨架。这一章先回答最基本的问题:在 grok 里,「一个工具」是什么?

一个工具 = 两个 trait 挂在一个 struct 上

grok 把「工具」拆成两个正交的 trait,一个类型同时实现它俩:

  • Tool(通用运行时契约,common/xai-tool-runtime/src/tool.rs):type Args(参数)、type Output(输出)、execute()(流式执行)/run()(阻塞执行)。这是「任何工具运行时都认」的契约。
  • ToolMetadata(grok 专属元数据,types/tool_metadata.rs):kind()、tool_namespace()、description_template()。这是「grok 这个 harness 额外要的」信息。

这个拆分很关键:Tool 是可跨 harness 复用的纯契约,ToolMetadata 才把它接进 grok 的世界。

ToolKind 与 ToolNamespace:工具的两个坐标

每个工具有两个分类坐标:

  • ToolKind(37 个变体,近来新增了 Workflow、Feedback 等):Read、Edit、Search、Execute、Plan、WebSearch……语义类别。system prompt里 ${{ tools.by_kind.read }} 引的就是它。ToolKind 的 JSON schema 故意是开放字符串(不是封闭枚举)+ #[serde(other)] Other——这样加新 kind 不会让老消费者崩。
  • ToolNamespace:GrokBuild / GrokBuildConcise / GrokBuildHashline / Codex / OpenCode / MCP——工具来自哪套。同一个 kind(比如 Edit)在不同 namespace 下有不同实现(见三种编辑格式与和别家 agent 互通)。

schema:从类型自动生成

工具怎么告诉模型「我要什么参数」?靠 Rust 的类型系统:T::Args 上 #[derive(JsonSchema)](schemars),注册时 generate_schema_cached::<T::Args>()(按类型缓存的 generate_schema)自动生成 JSON schema。参数结构即文档——你改 Args struct,模型看到的 schema 跟着变,不用手写两份。

模型是个「不太靠谱的调用方」,所以派发时先把 JSON 反序列化进类型化的 T::Args(ToolDyn::execute 里的 serde_json::from_value)——类型不对、缺字段,直接变成一个 invalid_arguments 错误回给模型。

还有一层贴心:宽容反序列化(schema.rs)——模型爱把数字写成字符串、把整数写成浮点,grok 的 deserialize_lenient_* 把这些都容错掉,而不是直接拒绝。

两段校验(types/params_validation.rs)管的则是另一类参数——客户端下发给每个工具的配置(存成 Params<P>):

  1. 结构校验:serde_path_to_error 把 JSON 反序列化进配置类型——类型不对、缺字段,这一步就报错(还能报出具体字段路径)。
  2. 语义校验:再调该类型的 validate_params_value 做业务规则校验。

执行:流是第一等公民

工具怎么跑?Tool::execute 返回一个 ToolStream<Output>,不变式是 [Progress*, Terminal]——零或多个进度事件,最后恰好一个终止结果(Result<Output, ToolError>)。阻塞式工具只要实现 run(),就自动得到一个「单终止项」的流。

为什么工具的原语是「流」而不是「函数」?因为跑一条 shell 命令、读一个大文件、调一次外部 API,都可能要边跑边报进度。把流当原语,进度上报就不是特例而是常态。

输出侧 ToolOutput 是个 #[serde(tag="type")] 的大枚举,每个内置工具一个变体,配 derive_more::From——编译期就保证「每种输出都被处理」。它有两个方法:is_error()(给遥测分类)和 to_prompt_format()(转成喂回模型的文本)。同一个结果,一份给机器(JSON)、一份给模型(文本),是贯穿工具系统的分野。

和《从 LLM 到 Coding Agent》对照

对照那本的 tool-abstraction:最小实现里,一个工具常是「一个函数 + 一份手写 JSON schema」。grok 的增量是把它做成类型驱动(schema 从 Args 自动生成)、双 trait(通用契约 + harness 元数据)、流式(进度是常态)、两段校验 + 宽容输入的完整契约。下一章看这些工具怎么被注册、排序、派发。


上一篇:打断与防失控 · 下一篇:工具注册与分发

本页目录