跳转到内容

Codex 的工具调用系统

Codex 的工具调用系统

学习目标

  • 理解 Codex 工具系统的四层架构实现
  • 掌握 Codex 的并行/串行工具执行模型
  • 分析 Codex 的工具审批和编排策略

前置知识

本章涉及工具调用的通用原理,建议先阅读:

下文假设你已理解上述概念,直接聚焦 Codex 的具体实现。


项目实践

工具系统四层架构

Codex 的工具系统分布在两个 crate:

  • codex-rs/tools/src/:规格定义层
  • codex-rs/core/src/tools/:路由、编排、执行层

ToolSpec 枚举

codex-rs/tools/src/lib.rs 中的 ToolSpec 定义了 Codex 支持的所有工具类型:

pub enum ToolSpec {
Function(ResponsesApiTool), // 标准函数调用工具
Namespace(ResponsesApiNamespace), // 命名空间工具
ToolSearch { ... }, // 工具搜索
ImageGeneration { ... }, // 图像生成
WebSearch { ... }, // 网络搜索
Freeform(FreeformTool), // 自定义自由格式工具
}

设计特点ToolSpec 直接序列化为 OpenAI Responses API 的 Tool JSON,无需额外的转换层。

核心 Handler 列表

core/src/tools/handlers/mod.rs 中注册的所有 Handler:

Handler功能风险等级
shellShell 命令执行
unified_exec统一执行命令
apply_patch应用代码补丁
mcpMCP 工具调用
mcp_resourceMCP 资源操作
plan生成执行计划
goal目标管理(创建/获取/更新)
request_permissions请求权限变更
request_user_input请求用户输入
request_plugin_install请求安装插件
tool_search工具搜索
multi_agents多 Agent 管理
agent_jobsAgent 作业管理
view_image查看图像
test_sync测试同步
dynamic动态工具

ToolOrchestrator 编排逻辑

ToolOrchestrator 是 Codex 工具执行的核心编排器,统一处理:

  1. 审批决策:根据 execpolicy 规则判断是否需要用户确认
  2. 沙箱选择:根据工具类型和当前配置选择合适的沙箱
  3. 重试策略:失败时按配置重试,带指数退避
// 伪代码示意
impl ToolOrchestrator {
async fn execute_tool(&self,
tool_call: ToolCall,
turn_context: &TurnContext,
) -> Result<ToolResult> {
// 1. 检查审批策略
let approval = self.check_policy(&tool_call).await?;
if approval == Decision::Prompt {
self.request_user_approval(&tool_call).await?;
}
// 2. 选择沙箱
let sandbox = self.select_sandbox(&tool_call, turn_context);
// 3. 执行工具
let result = self.run_with_retry(
|| handler.execute(tool_call, sandbox),
retry_policy,
).await?;
Ok(result)
}
}

并行工具执行

ToolCallRuntimecore/src/tools/parallel.rs)管理并行/串行执行:

pub(crate) struct ToolCallRuntime {
lock: RwLock<()>, // 读写锁控制并发
cancellation: CancellationToken, // 取消信号
}

执行模式

  • 并行:获取 RwLock 的 read guard,多个读 guard 可共存
  • 串行:获取 RwLock 的 write guard,独占执行
  • 取消CancellationToken 传播到所有子任务,中断时统一终止

调度决策:Codex 默认尽可能并行执行工具,除非:

  • 工具之间存在资源依赖(如都操作同一文件)
  • 工具需要用户审批(审批必须串行)
  • 工具配置了 serial: true

问题与规避

工具超时与取消

Codex 为每个工具调用设置超时。超时机制通过 tokio::time::timeout 实现:

  • 超时后发送 SIGTERM 给子进程
  • 宽限期(如 5 秒)后发送 SIGKILL
  • CancellationToken 确保所有相关任务同步终止

沙箱切换开销

不同工具可能需要不同的沙箱级别。Codex 的策略是:

  • 在 Turn 开始时确定沙箱配置,尽量保持不变
  • 仅在必要时(如权限提升请求)切换沙箱
  • 沙箱切换通过 shell-escalation 机制实现,而非完全重建沙箱

工具结果过大

Codex 对工具输出进行截断处理:

  • 命令输出超过阈值时截断,保留头部和尾部
  • 文件读取支持分页(offset + max_lines
  • 图像数据压缩或转为描述文本

设计取舍

为什么将工具规格和实现分离到不同 crate?

Codex 将 ToolSpec 定义在独立的 tools crate 中,而 Handler 实现放在 core crate 中。这种分离使得:

  • 其他 crate(如 protocoltui)可以引用工具规格而不依赖工具实现
  • 工具实现的变更不会影响依赖工具规格的上层代码
  • 便于为工具规格编写独立的测试

代价是增加了 crate 间的协调成本,新增工具时需要同时修改两个 crate。

为什么使用 RwLock 而非异步锁?

Codex 在 ToolCallRuntime 中使用 std::sync::RwLock 而非 tokio::sync::RwLock。这是因为工具执行本身就是异步的,获取锁的操作很短(只是检查是否可以执行),使用同步锁减少了异步状态机的复杂度。代价是在锁竞争时可能阻塞异步任务。


参考来源