“我已经运行了测试”是一句话,测试进程真的启动并退出,是另一份证据。 分析 Codex 的工具调用时,如果只看助手文字,很容易把计划当成执行,把执行当成成功。 反过来,模型明明生成了一个工具调用,宿主却没有放行,也不能简单归因于模型不听话。
本文沿公开源码追踪工具调用的主干,回答一个问题:从模型想做某件事,到运行时真正执行,中间经过哪些判断? 重点是调用身份、工具路由、审批和结果形态。 具体 shell 命令、文件修改和 MCP 工具各有实现,不能用一条路径概括所有工具。
源码基线为 46d252f8bf8b09f4ef765bc72e40f02c229da3e6,核对日期 2026-10-20。
以下源码结论限定于这个提交,实验建议没有被写成已完成的模型调用结果。
一个工具至少有四种不同状态
看到源码里注册了某个工具,不代表它已经能被这一轮模型调用。
该基线的工具路由同时处理模型可见规格、代码模式下的名称与延迟暴露信息。
record_registration_metrics 附近还明确区分注册与可见、可达的关系。
来源:tools/router.rs。
可以用下面四个问题阅读实现:
| 状态 | 要问的问题 | 典型证据 |
|---|---|---|
| 已注册 | 运行时有没有这个工具的实现 | 注册表条目 |
| 可见或可发现 | 当前模型能否获得调用规格 | 工具计划与暴露方式 |
| 获准执行 | 当前调用是否通过适用权限判断 | 审批与策略结果 |
| 已完成 | 执行是否返回终态 | 工具输出或进程状态 |
这四个问题不宜合并成“工具开没开”。 它们分别对应不同的故障位置。 注册正确但当前不可见时,反复修改用户提示词未必解决问题。 模型已经发起调用却被策略拒绝时,换工具描述也不能替代执行权限。
第一步:把输出项识别成调用
在该基线里,ToolRouter::build_tool_call 接收 ResponseItem。
它会识别函数调用、自定义工具调用,以及符合条件的客户端工具搜索调用。
其他输出项不自动变成工具执行。
来源:build_tool_call。
这一层最值得注意的是调用身份。
构造出的 ToolCall 包含 tool_name、call_id 和 payload 等信息。
名字负责找到工具,call_id 用来关联这次调用,payload 承载对应类别的输入。
来源:ToolCall 定义。
下面是概念示例,字段组合用于说明,不是完整的线上报文:
{
"tool_name": "read_fixture",
"call_id": "call-fixture-01",
"payload": {
"path": "fixture.txt"
}
}
如果把调用 A 的结果关联给调用 B,模型看到的反馈就可能失去语义对应。 因此,做录制和回放时应验证调用与结果的关联,不能只按日志出现顺序猜配对。 这属于协议分析方法上的要求,不代表所有工具结果都恰好长成上面的 JSON。
第二步:从路由走到注册表
ToolRouter 的分发函数会构造执行所需的调用上下文,再交给注册表路径处理。
注册表侧包含运行时对象,也处理执行前后相关逻辑。
来源:路由分发、ToolRegistry。
研究时可以把调用链归纳成下面的框架:
模型输出项
↓ 识别种类、名称与调用身份
ToolCall
↓ 当前工具计划与注册表
调用上下文
↓ 对应工具运行时
执行前处理 / 适用权限判断
↓
工具执行与结果包装
↓
后续模型输入与客户端事件
这是一张分析图,不是所有工具共享的严格函数调用栈。 例如宿主工具、动态工具和本地进程工具,需要分别追踪它们的实际运行时。 否则很容易把某个 shell 工具的沙箱行为,误写成所有 MCP 调用的共同保证。
第三步:审批与沙箱各管一件事
tools/orchestrator.rs 的职责包括工具审批、沙箱选择,以及沙箱拒绝后适用的重试协调。
其 run 实现会读取审批策略、环境配置和权限信息,并区分跳过审批、禁止执行与需要审批的情况。
来源:工具执行协调器。
这两类机制应分开理解:
| 机制 | 解决的问题 | 观察重点 |
|---|---|---|
| 审批 | 这次动作是否被允许 | 动作、范围、决定及依据 |
| 沙箱 | 执行过程中能接触哪些资源 | 文件、网络与进程限制 |
| 工具自身校验 | 输入是否满足工具条件 | 参数、路径与状态检查 |
审批同意并不自动证明任意资源都能访问。 沙箱内失败也不自动证明模型没有发起调用。 这两句属于机制区分,具体失败仍要看工具错误、审批结果和操作系统返回值。
该提交还包含对严格自动审查和重试范围的处理。 源码不能支持“批准一次,所有后续升级都自动批准”这种概括。 如果权限范围改变,应继续沿实际重试分支阅读,而不是把首次决定当成永久通行证。 来源:orchestrator.rs 的审批和重试处理。
第四步:工具返回值还要变成模型能消费的结果
执行结束后,运行时结果并非简单地原封不动塞回聊天文本。
tools/context.rs 中的输出实现提供 to_response_item 等转换方法。
其中可以看到函数工具输出、MCP 工具输出等不同形态。
来源:工具输出与响应转换。
对排查而言,需要同时检查两个结果:
- 工具实际返回了什么,包括失败、退出状态和必要元数据。
- 下一步模型实际获得了哪部分结果,以及调用关联是否保留。
两者之间如果存在截断、结构转换或过滤,模型的后续行为就不能仅凭原始终端输出解释。
关于历史进入模型前的处理,应进一步阅读 ContextManager 的 for_prompt 路径。
来源:history.rs 的 for_prompt。
并行工具调用需要两层判断
模型可以提出多个调用,但宿主是否并行执行,仍取决于工具能力和调度实现。
该基线的 tool_supports_parallel 会向注册表查询支持情况,而非仅凭模型输出数量决定。
来源:并行支持查询。
对自己的实验,可以先用两个独立的只读文件建立对照。 再加入一个“先写文件、后读取该文件”的依赖任务。 前者用于观察并行可能性,后者用于验证结果依赖。 不要用两个调用时间接近,就断言系统没有顺序约束。
建议记录:
call_id
工具名称与命名空间
参数摘要
执行前审批结果(若可观察)
开始时间与完成时间
返回状态
结果对应的调用身份
时间数据只能支持你测到的运行情况。 没有运行时证据时,源码中的“支持并行”应写成能力条件,不能写成已经并发执行。
一个能区分模型猜测与真实读取的小实验
在独立测试目录里生成随机 receipt,只写入文件,不写入任务提示。 让 Codex 读取文件后返回该 receipt,并检查结构化事件中是否真的出现读取动作。 这个设计同时检查结果正确性和工具使用证据。
如果 receipt 正确但没有可见调用,先检查是否有其他上下文来源。 如果调用发生但 receipt 错误,检查读取路径、输出内容和结果转换。 如果模型只声称读过,不能据此判定实验通过。
为控制变量,第一次只放一个小文本文件,不加插件,不引入网络服务。 这是一份待执行的实验设计,本文没有给出模型轮次、耗时或成功率数字。 它验证的是当前环境下的一次调用链,不是所有版本的整体可靠性。
对照现象定位失败位置
| 现象 | 优先核对 |
|---|---|
| 工具不存在或找不到 | 注册表、名称、命名空间 |
| 模型始终不调用 | 当轮规格、可发现性与任务要求 |
| 有调用但被拒绝 | 审批、策略与工具条件 |
| 执行失败 | 实际运行环境与错误输出 |
| 执行成功却继续误判 | 回传内容、关联和上下文处理 |
| 回复成功但产物错误 | 文件差异与业务验收 |
逆向工具调用最有价值的产物,是一条能说明“在哪一步失去预期”的证据链。 它需要同时覆盖模型输出、宿主判断和实际副作用。
公开实现提供了查找这些节点的入口。 具体到自己的工具或客户端,仍应以对应版本的运行记录核实;一个工具执行成功,也只完成了用户目标中的一个步骤。