“我已经运行了测试”是一句话,测试进程真的启动并退出,是另一份证据。 分析 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 工具输出等不同形态。 来源:工具输出与响应转换。

对排查而言,需要同时检查两个结果:

  1. 工具实际返回了什么,包括失败、退出状态和必要元数据。
  2. 下一步模型实际获得了哪部分结果,以及调用关联是否保留。

两者之间如果存在截断、结构转换或过滤,模型的后续行为就不能仅凭原始终端输出解释。 关于历史进入模型前的处理,应进一步阅读 ContextManager 的 for_prompt 路径。 来源:history.rs 的 for_prompt。

并行工具调用需要两层判断

模型可以提出多个调用,但宿主是否并行执行,仍取决于工具能力和调度实现。 该基线的 tool_supports_parallel 会向注册表查询支持情况,而非仅凭模型输出数量决定。 来源:并行支持查询。

对自己的实验,可以先用两个独立的只读文件建立对照。 再加入一个“先写文件、后读取该文件”的依赖任务。 前者用于观察并行可能性,后者用于验证结果依赖。 不要用两个调用时间接近,就断言系统没有顺序约束。

建议记录:

call_id
工具名称与命名空间
参数摘要
执行前审批结果(若可观察)
开始时间与完成时间
返回状态
结果对应的调用身份

时间数据只能支持你测到的运行情况。 没有运行时证据时,源码中的“支持并行”应写成能力条件,不能写成已经并发执行。

一个能区分模型猜测与真实读取的小实验

在独立测试目录里生成随机 receipt,只写入文件,不写入任务提示。 让 Codex 读取文件后返回该 receipt,并检查结构化事件中是否真的出现读取动作。 这个设计同时检查结果正确性和工具使用证据。

如果 receipt 正确但没有可见调用,先检查是否有其他上下文来源。 如果调用发生但 receipt 错误,检查读取路径、输出内容和结果转换。 如果模型只声称读过,不能据此判定实验通过。

为控制变量,第一次只放一个小文本文件,不加插件,不引入网络服务。 这是一份待执行的实验设计,本文没有给出模型轮次、耗时或成功率数字。 它验证的是当前环境下的一次调用链,不是所有版本的整体可靠性。

对照现象定位失败位置

现象 优先核对
工具不存在或找不到 注册表、名称、命名空间
模型始终不调用 当轮规格、可发现性与任务要求
有调用但被拒绝 审批、策略与工具条件
执行失败 实际运行环境与错误输出
执行成功却继续误判 回传内容、关联和上下文处理
回复成功但产物错误 文件差异与业务验收

逆向工具调用最有价值的产物,是一条能说明“在哪一步失去预期”的证据链。 它需要同时覆盖模型输出、宿主判断和实际副作用。

公开实现提供了查找这些节点的入口。 具体到自己的工具或客户端,仍应以对应版本的运行记录核实;一个工具执行成功,也只完成了用户目标中的一个步骤。