模型: openai/gpt-5.4
生成日期: 2026-04-01
书名: Claude Code VS OpenCode:架构、设计与未来
章节: 第4章 — 工具系统设计
Token用量: 约 13,500 input + 2,200 output
4.1 工具定义范式
工具系统本质上是 Agent 的“外设总线”。模型本身只会生成 token,真正让它接触文件系统、Shell、网络与子任务的,是 tool abstraction。三套系统都承认这一点,但其定义范式并不相同。
OpenCode 采用 Tool.define() 工厂函数,核心位于 packages/opencode/src/tool/tool.ts。它要求每个工具显式声明 id、description、parameters 与 execute,其中 parameters 基于 Zod。Zod 可以理解为一类 TypeScript runtime schema validation library:TypeScript 只在编译期检查类型,运行时类型会被擦除,而 Zod 则把“类型描述”保留下来,在程序真正执行时继续验证参数是否合法。因此,OpenCode 的工具不是“相信模型会传对参数”,而是“先定义结构,再在运行时验收结构”。这是一种典型的 schema-first tool design。
Claude Code 的核心是 buildTool(),位于 src/Tool.ts。它的思路更偏产品化:先定义 ToolDef 接口,再由 buildTool() 补齐默认能力,例如 isReadOnly、isDestructive、checkPermissions、userFacingName。在 schema 层,它既支持 Zod,也支持 JSON Schema。JSON Schema 是 IETF 标准化的数据结构描述规范,可以把它理解为“面向跨语言交换的类型契约”。如果说 Zod 更适合 TypeScript 内部开发体验,那么 JSON Schema 更适合跨进程、跨服务、跨协议场景,尤其适合 Claude Code 这样要兼容 MCP、远程工具与更大生态的系统。
Oh-My-OpenCode(OMO)没有重新发明底层工具协议,而是包装 OpenCode SDK 的工具定义能力,在 src/plugin/tool-registry.ts、src/create-tools.ts、src/tools/* 中注册自己的工具集合。它的关键不是换掉 definition primitive,而是在 execute 前后插入一层 orchestration wrapper:工具创建时仍沿用 OpenCode 风格,但在执行链路上增加 hooks、metadata store、background task 管理、skill/MCP 注入等机制。换言之,OMO 的创新点不在“工具是什么”,而在“工具怎样被编排、拦截、约束和放大”。
这里可以引入 Anthropic 提出的 ACI(Agent-Computer Interface) 原则。传统 HCI(Human-Computer Interface)研究的是人机界面,而 ACI 研究的是“模型如何最小歧义地调用计算机能力”。它强调几个方向:第一,工具接口要清晰、强约束、低歧义;第二,参数名与描述要让模型容易推断;第三,返回结果要结构化,便于后续推理;第四,工具数量不宜无限膨胀,否则会提高模型的选择成本。OpenCode 的 Tool.define()、Claude Code 的 buildTool(),以及 OMO 的包装层,本质上都是 ACI 的工程化体现。
三者因此形成了三个层次。OpenCode 代表“开源基座式范式”:用 Tool.define() + Zod 建立简洁、统一、强验证的工具定义语言。Claude Code 代表“产品平台式范式”:用 buildTool() + JSON Schema/Zod + 默认权限语义,把工具定义扩展成一套可治理的产品接口。OMO 则代表“编排增强式范式”:在不破坏 OpenCode 基座的前提下,为工具定义附加多智能体、技能、会话与钩子系统。
因此,第4章讨论工具系统时,不能只把工具看作函数调用。更准确地说,工具定义范式决定了 Agent 能力边界的表达方式:它既是调用协议,也是安全边界,还是推理成本控制器。谁把这个层设计得更清晰,谁就更可能做出稳定、可扩展、可治理的编码智能体。
深度解析:Zod Schema 的全链路渗透
模型: openai/gpt-5.4
Token用量: 当前本地追加操作未暴露精确统计
如果上一节回答的是“为什么工具需要 schema”,那么这一节要回答的是另一个更深的问题:为什么 schema 在 Agent 系统里会变得如此关键,甚至像毛细血管一样渗透全链路?
先从一个高中生也能立刻理解的比喻开始:麦当劳点餐。
假设一家麦当劳完全没有结构化点单系统。顾客说:“我要那个鸡肉的,来个大一点的,再配个冷的,不要那个绿色的东西。” 收银员凭感觉理解,厨房再凭感觉理解,打包员再凭感觉理解。有人把“鸡肉的”理解成麦香鸡,有人理解成鸡腿堡;“大一点”到底是套餐变大,还是薯条变大;“冷的”到底是可乐、雪碧还是冰淇淋;“绿色的东西”到底是生菜、酸黄瓜,还是某种酱料。最后做出来的东西,可能每个人都觉得自己“理解得差不多”,但最终成品却和顾客真正想要的完全不同。
这就是没有 schema 的系统:每一层都在猜。
再看另一种情况。麦当劳点单界面要求必须填写这些字段:主食是什么、是否套餐、饮料是什么、规格大小、去掉哪些配料、是否加购。如果顾客表达模糊,收银员就不能直接下单,必须先把模糊之处问清楚。之后,厨房看到的是结构化订单,饮料台看到的是结构化饮料字段,打印小票也复用同一份结构化数据。注意,这里的关键不是“收银员更聪明”,而是整个链路不再靠猜测,而是靠同一份被验证过的结构化订单工作。
软件里的 schema,本质上就是这张“结构化订单”。
1. Zod 到底是什么?
Zod 是一个 TypeScript 生态里非常常见的 runtime validation 工具。这里有几个非标准术语需要单独解释,因为它们不是很多传统 CS 教科书中的常见条目。
- schema:可以理解成“数据形状说明书”或者“数据结构契约”。它描述一个数据对象应该有哪些字段、字段是什么类型、哪些字段必填、哪些值合法。
- runtime validation:运行时校验。TypeScript 的类型检查主要发生在编译期,代码一旦编译成 JavaScript,类型信息大多就消失了;而 runtime validation 指的是程序真正运行时,仍然去检查传进来的数据是否符合预期。
- discriminated union:可判别联合。它指“一个值可以是多种结构中的一种,但每种结构都带一个明确的标签字段”,比如
type: "success"或type: "error"。这样后续代码就不用猜它到底属于哪一类,而是看标签直接分流。
为什么 Zod 重要?因为它不只是“写一个类型定义”,而是把“类型约束”保留到运行时继续使用。也就是说,TypeScript 说“理论上这里应该是 number”,而 Zod 则负责在程序运行时问一句:“你现在传进来的,真的就是 number 吗?”
看一个非常简单的例子:
import { z } from "zod"
const ToolInput = z.object({
filePath: z.string().min(1),
lineNumber: z.number().int().positive(),
includeContext: z.boolean().default(false),
})
const rawInput = {
filePath: "/project/src/app.ts",
lineNumber: "18",
includeContext: true,
}
const result = ToolInput.safeParse(rawInput)
if (!result.success) {
console.error(result.error.issues)
} else {
const input = result.data
console.log(input.lineNumber)
}
上面这段代码里,ToolInput 就是 schema。它规定:
filePath必须是字符串;lineNumber必须是正整数;includeContext必须是布尔值,默认是false。
然后 safeParse 会在运行时检查 rawInput。因为例子里 lineNumber 传入的是字符串 "18",而不是数字 18,所以校验会失败,并返回结构化错误信息。
这里的重点是:Zod 不是“写给人看”的注释,而是“程序会真的执行”的边界检查器。
2. 为什么 AI Agent 特别需要它?
普通程序里,函数参数往往来自另一个程序员写的代码;而在 Agent 系统里,工具参数往往来自 LLM 的输出。问题在于:LLM 很强,但它天生是 non-deterministic 的。
所谓 non-deterministic,可以简单理解为:同一个输入、同一个目标,不同轮次的输出可能略有不同。对自然语言写作来说,这不是问题;对工具执行来说,这就很危险。因为自然语言可以模糊一点,系统边界却不能模糊。
常见问题包括:
- 本来要求 absolute path,模型却给了 relative path,比如
src/main.ts - 本来要求
lineNumber: 42,模型却给了lineNumber: "42" - 本来
session_id是必填字段,模型忘了填 - 本来字段允许值是
fast | safe,模型却自创了一个quick - 本来某个结果结构是 A 或 B 两种格式之一,模型却拼出一个 A、B 混杂的中间态
这些错误看起来都不大,但在工具系统里,小错会迅速放大。因为工具不是聊天回复,它是真实地操作文件、状态、数据库、消息历史、网络接口的。
所以 Agent 系统里必须坚持一个设计哲学:
LLMs can be non-deterministic, but system boundaries must be deterministic.
也就是:LLM 可以非确定,但系统边界必须确定。
3. 没有 schema 时,错误如何“静默传播”
最危险的不是“立刻报错”,而是看似没报错,但错误悄悄往后传播。
比如模型调用一个读取文件的工具,本来要求绝对路径,但它给了相对路径 src/index.ts。如果没有 schema 检查,工具实现可能会默认用当前工作目录去拼接。结果呢?
- 也许拼到的是错误的文件;
- 也许碰巧读到了另一个同名文件;
- 也许在不同 session、不同工作目录下,行为还不一致。
接下来,错误内容进入 tool result;tool result 又进入消息历史;后续推理再基于错误上下文继续操作;最后 SQLite 持久化保存的也是错的;UI、事件总线、HTTP API 看到的也是错的。系统可能一直没有抛出“致命异常”,但错误已经像污染物一样扩散开了。
这个流程可以表示成:
LLM 生成模糊参数
-> 工具没有严格校验,直接接受
-> 内部代码自行脑补默认含义
-> 读错文件 / 写错状态 / 产出错结果
-> 错误结果进入 message history
-> 后续步骤基于污染上下文继续推理
-> 错误静默扩散
这就是 silent failure propagation:静默失败传播。
相反,如果引入 schema,链路会变成:
LLM 生成模糊参数
-> Zod 在边界处校验
-> 立即发现具体错误
-> 返回精确反馈给模型
-> 模型修正参数后重试
-> 工具在可信数据上执行
-> 正确结果继续向后流动
这就是“精确反馈”的价值。它不是单纯地“更严格”,而是把错误拦在最小作用域里。
4. Zod 在 OpenCode 里的全链路渗透
很多初学者容易把 schema 理解成“工具执行前的一次参数检查”。这理解太浅了。更准确地说,像 OpenCode 这样的系统里,schema 会一路渗透整个链路,每经过一个边界,就像过一次海关。
graph LR
A["系统提示词"] -->|"Zod ✓"| B["LLM 推理"]
B -->|"Zod ✓"| C["工具参数"]
C -->|"Zod ✓"| D["工具执行"]
D -->|"Zod ✓"| E["工具结果"]
E -->|"Zod ✓"| F["消息历史"]
F -->|"Zod ✓"| G["SQLite 存储"]
G -->|"Zod ✓"| H["事件总线"]
H -->|"Zod ✓"| I["HTTP API"]
style A fill:#4a9eff,color:#fff
style I fill:#51cf66,color:#fff
可以把这条全链路拆开看:
-
system prompt assembly
工具名、字段名、参数说明会被拼进系统提示词。也就是说,schema 虽然还没执行,但它已经先影响了模型对工具的理解方式。 -
LLM inference
模型根据提示词决定是否调工具、调哪个工具、参数怎么填。这一步最不稳定,也最像“语言生成”。 -
tool call parameters
模型生成的参数对象真正到达运行时,此时 Zod 开始做 runtime validation,确认形状是否匹配。 -
tool execution
只有通过校验的数据才能进入真实execute()。这意味着工具实现可以在更可靠的前提下工作,而不是每个工具作者都要手写一遍if typeof ...。 -
tool result
返回结果往往也需要结构约束,比如成功、失败、是否截断、输出路径、metadata 等。这里也常常会用到 schema 或 discriminated union。 -
message history(MessageV2 parts)
工具结果会变成结构化消息片段,供下一轮推理继续读取。消息结构一旦不稳定,后续推理就会读歪。 -
SQLite storage
Session 持久化依赖稳定数据结构。没有 schema,旧会话恢复时就会陷入“这个字段当年到底是什么格式”的猜谜游戏。 -
event bus payload
UI 更新、通知、订阅器、observer 都要依赖事件对象。如果 payload 结构飘忽不定,事件消费端就会脆弱。 -
HTTP API response
对外 API 更需要确定契约。否则外部调用者会把你的系统当成“今天这样、明天那样”的不可靠黑盒。
所以可以把整个系统想成一连串边界:
Every boundary has a customs checkpoint.
也就是:每一个边界都应该像海关一样检查“你带来的数据形状是否合法”。这就是所谓的 全链路渗透。
5. 机场安检比喻:为什么要把“聪明”和“安全”分开
再换一个比喻:机场安检。
- LLM 像乘客:大多数时候没问题,但有时粗心、含糊、表达不标准,也可能出现不可预测行为。
- tools 像登机口:一旦让错误的人或错误的物品通过,后果会迅速放大。
- schema 像安检机与证件核验:它不去“猜”乘客大概是什么意思,而是按照明确规则检查当下这份输入是否合规。
很多工程事故,本质上都是把“模型很聪明”误当成“边界可以放松”。这和机场安检说“这个乘客看起来像好人,就不用过机器了”一样危险。“大概率没问题”不是系统设计原则。 系统设计原则必须是:规则明确、可重复执行、可检查、可追踪。
Zod 做的就是软件世界里的“安检机”。
6. 用图看设计哲学:非确定核心 + 确定性外壳
Agent 最理想的结构,不是要求 LLM 变成一个完全确定的逻辑机,而是承认它会有随机性,然后在外围包上一层严格的确定性壳层。
可以用一个 ASCII 图表示:
+--------------------------------------------------------------+
| 确定性的系统外壳 |
| |
| Prompt 组装 -> Schema 校验 -> Tool 执行 -> Storage 持久化 |
| | | | | |
| v v v v |
| [ 非确定性的 LLM Core ] |
| |
| Event Bus -> HTTP API -> Session 恢复 -> UI 渲染 |
| |
+--------------------------------------------------------------+
也可以压缩成一句话:
确定性边界 -> 概率性生成 -> 确定性校验 -> 确定性执行 -> 确定性持久化
这张图表达的就是本章最核心的设计思想:
LLM 可以是模糊的,但边界不可以。
7. 四个直接后果
如果一个 Agent 系统把 schema 做成真正的“全链路渗透”,会带来至少四个非常直接的后果。
第一,错误会被限制在最小范围内
参数不对,就在参数边界报错;结果结构不对,就在结果边界报错;事件 payload 不对,就在事件分发前报错。这样错误不会轻易越过边界去污染下一层。
第二,错误信息会非常精确
不是笼统地说“工具失败了”,而是明确说:
filePath必须是 absolute pathlineNumber必须是 integer- 缺少必填字段
session_id mode只能是fast或safe
这种精确反馈对人类工程师有用,对 LLM 也同样有用,因为模型更容易据此修正下一次调用。
第三,系统更容易 self-heal
所谓 self-heal,不是系统 magically 什么都能修,而是说:当错误暴露得足够早、足够清楚时,系统可以自动重试、自动修正、自动缩小问题范围。schema 失败通常是最适合重试的一类失败,因为它告诉模型“结构错了,按这个格式重来”。
第四,子系统可以安全协作
Prompt builder、tool runner、message history、SQLite、event bus、HTTP API、UI renderer,都是不同子系统。没有共享 schema 时,它们之间依赖大量“默认共识”和“口口相传”的隐性知识;有了 schema,它们共享的是明确契约。这样每个子系统都可以更独立地演进,同时仍然安全协作。
8. 为什么这不是“额外的严格”,而是 Agent 可用性的基础
很多人第一次接触 schema-first 设计,会觉得它只是“多了一道麻烦的校验”。但对 Agent 来说,这不是锦上添花,而是基础设施。
因为 Agent 最大的张力就在这里:
- 它的“脑”是概率性的;
- 它的“手脚”却连接着确定性的真实世界。
如果你不在两者之间放一个严格的契约层,模型的小偏差就会直接变成系统的大事故。相反,如果你把 schema 做成一道完整的边界层,那么 LLM 的创造性、模糊性、探索性就能被安全地约束在一个可治理的框架里。
所以,“Zod Schema 的全链路渗透”真正想表达的是:
- schema 不是局部工具,而是系统级控制机制;
- 它不只校验输入,还稳定全链路中的意义传递;
- 它让错误从“隐性污染”变成“显性反馈”;
- 它让一个非确定性的 LLM,能够被包裹进一个确定性的工程系统。
最后用一句最值得反复记住的话收束:
LLMs can be non-deterministic, but system boundaries must be deterministic.
而 Zod,正是把这句话从理念落成工程现实的关键工具之一。