Tool 是模型与现实之间的协议
模型输出的是“调用某个工具,参数如下”;它并没有任意 Rust 函数指针,也不直接拥有 Shell。Runtime 先把工具规格发给模型,再校验返回的名字和参数,最后选择实现。
flowchart LR
A["Model output item"] --> B["stream_events_utils<br/>识别 ToolCall"]
B --> C["ToolRouter<br/>解析 payload"]
C --> D["ToolRegistry<br/>选择 handler + hooks"]
D --> E["ToolOrchestrator<br/>approval + sandbox"]
E --> F{"Handler"}
F --> G["Shell / Unified exec"]
F --> H["Apply patch"]
F --> I["MCP / collaboration / other"]
G --> J["Tool output"]
H --> J
I --> J
J --> K["ContextManager"]
让概率模型操作确定性系统,同时保持参数可校验、行为可观察、权限可控制。
模型可见 schema → 结构化 ToolCall → 路由 → 策略编排 → handler → 结构化结果。
能力和策略解耦后,可增加工具而不复制一套安全与事件逻辑。
router.rs、registry.rs、orchestrator.rs 形成主干,handlers/ 提供具体能力。
schema 不能保证意图正确;工具过宽会放大风险,工具过窄又会限制任务。
Shell:通用但锋利
Shell 的优势是覆盖面:搜索、读取、测试、构建、Git、项目脚本都能复用现有命令。代价同样明显:
- 命令字符串存在注入和引用风险;
- 一个命令可能读写大量状态;
- 跨平台行为不同;
- 长输出会挤占 Context;
- 后台进程和交互式程序需要生命周期管理。
Codex 源码同时包含 Shell handler、统一执行工具以及工具规格。执行仍要经过审批和沙箱策略。
Patch:把修改表达为数据
与 sed -i 或任意脚本写文件相比,Patch 有几个工程优势:
- 明确列出增加、删除、修改的文件;
- 上下文不匹配时可以拒绝,而不是静默改错;
- 适合展示和审查;
- 工具 schema 可以约束格式;
- 更容易在写入前做路径检查。
Codex 的 apply_patch 是 freeform 工具:模型提交一段符合特定 grammar 的 patch 文本。实现会解析、校验,然后应用。
示意:模型看到的边界
*** Begin Patch
*** Update File: src/retry.ts
@@
-const limit = 2;
+const limit = 3;
*** End Patch
语法示意 这段只演示工具格式,并非官方仓库中的真实修改。
Orchestrator:策略真正生效的地方
工具 handler 不应自己随意决定“是否可以绕过沙箱”。ToolOrchestrator::run 把流程统一为:
- 根据工具与策略判断是否需要批准;
- 等待批准或得到拒绝;
- 在选定沙箱中进行第一次尝试;
- 若策略允许并且失败确实与沙箱有关,再走升级路径;
- 返回可供模型继续判断的结果。
工具结果怎样回到模型
结果不能只写终端。它需要被编码成模型协议中的 tool output,并与原始 call id 对应。这样模型才能知道:
- 哪个动作完成了;
- 标准输出与错误是什么;
- 是否超时或被拒绝;
- 文件改变了什么;
- 下一步应重试、修复还是结束。
设计一个好工具
最小检查表:
| 维度 | 问题 |
|---|---|
| 输入 | schema 是否足够窄,错误参数能否尽早拒绝? |
| 权限 | 读、写、网络、进程各需要什么边界? |
| 输出 | 是否结构化、限长,并保留恢复所需信息? |
| 幂等性 | 重试会不会重复创建或破坏状态? |
| 观察性 | 是否产生开始、完成、失败事件? |
| 取消 | 长任务能否中断和清理? |
接下来把单个工具放回更大的运行时对象模型:Runtime 与 Session。