大数跨境

Codemode 是什么:看 Pi 和 Codex 如何用代码调用工具

Codemode 是什么:看 Pi 和 Codex 如何用代码调用工具 独立开发
2026-10-08
3

Codemode 详解

最近由于 pi 通过 codemode 引入了 MCP 的调用,再次引发了关于 codemode 的讨论。其实在过去一段时间,随着基模能力的提升,codemode 已经成为了生产级 coding agent 的事实标配。

Codemode 本身也是一个 LLM 可以看到的 tool,但它是一个万能的元工具:可以让 LLM 写一段程序,通过程序调用工具、处理数据,再把需要的结果交回模型。

Codemode 把写代码这个动作本身做成了一个工具,它把过去 agent 常用的 Read, Write, Edit,甚至说是 MCP,都收敛进一个单一工具,收敛成 codemode 里的函数调用,通过代码编排实现工具的串并行,并对输入、输出进行修改。

这个模式在 Pi 和 Codex 中都能看到,今天详细讲一下他们 codemode 的实现,以及 codemode 的优缺点。大家可以重点关注 1-3 章 codemode 的通用概念。

1. 什么是 codemode

假设我们希望 Agent 完成这个任务:

查询所有活跃项目的任务,找出逾期超过 7 天、尚未完成的任务,再按负责人统计数量。

如果使用普通 Tool Calling,模型先调用“查询项目”工具,读到项目 ID,再调用“查询任务”工具。拿到结果后,它决定继续查询、筛选或汇总,直到获得足够的信息。

这是一条有效路径。但其中一些步骤只需要传递数据和执行明确规则:取出项目 ID,遍历项目,判断逾期天数,把计数加一。它们都可以提前写进程序。

Codemode 给模型一个执行代码的入口。模型可以提交下面这样的 JavaScript:

// 简化示意:假设工具返回完整数组,字段含义已知。
const projects = await tools.list_projects({ status: "active" });
const counts = new Map();

for (const project of projects) {
const tasks = await tools.list_tasks({ project_id: project.id });

for (const task of tasks) {
    if (!task.completed && task.overdue_days > 7) {
      const owner = task.owner ?? "未分配";
      counts.set(owner, (counts.get(owner) ?? 0) + 1);
    }
  }
}

text(Object.fromEntries(counts));

代码仍然调用了多次工具。查询项目和任务的网络请求也照常发生。不过,遍历、传递 ID 和统计都在程序里完成,模型最终只需要读到一个小对象:

{"张三": 4, "李四": 2, "未分配": 1}

关键在于:项目和任务列表先进入执行器,程序处理后,再选择输出什么给模型。


普通工具调用与 Codemode


2. codemode 的优势

代码对循环、分支和精确计算有现成的表达方式。工具只需要提供基础操作,模型就能用 for、if、await 把它们组合起来,不必为每种组合再设计一个专用工具。

这个变化带来三方面收益。

# 减少模型调用

在前面的任务中,“拿到项目 ID 后查询任务”可以预先表达。执行器获得项目结果以后,直接进入下一次调用,不必把结果交回模型,让模型再生成相同的决定。

对于调用耗时较短、步骤很多的任务,这可以减少模型往返。

但如果下一步需要理解意外情况、重新判断目标,仍然需要调用模型。

# 实现工具编排与串并行

数组中的计数、排序、过滤和连接,可以在代码里完成。模型负责确定处理目标和规则,执行器负责按规则计算。

例如“逾期超过 7 天”能够写成清楚的条件;“哪条任务最值得优先处理”可能还需要业务判断。把后者硬写成几个字段条件,并不会自动获得合理答案。

# 只让必要结果进入上下文

工具可能返回上千条记录,最终回答只需要几个统计数字。Codemode 可以让这些记录留在执行器内,通过 text 输出需要的内容。

codemode 的优点如上,但是同样引入了复杂度,对模型的能力提出了很高的要求,这仿佛引入了一个没有任何 schema 的 tool,非常依赖模型本身的输出能力。

3. 程序运行在哪里

理解 Codemode,需要先认识 Harness 和 Execution Environment。

Harness 是组织 Agent 运行的软件。

Execution Environment 则是具体操作发生的环境:本地进程、容器、远端沙箱、浏览器,或者某个外部服务。Harness 组织任务和决策,codemode的执行环境承担具体工作。

Codemode 的 JavaScript 执行器也有自己的边界。在本文检查的 Pi 和 Codex 实现中,裸 JavaScript 都没有直接的文件系统和网络入口。代码访问外部世界,需要调用宿主提供的接口。

如下图所示,codemode 引擎仅仅负责编排工具,工具的具体执行,比如 mcp client,bash,read/write/edit 等工具的实现,均在 harness 层,在 codemode 引擎之外。


代码执行器、宿主工具入口与具体执行环境


这可以回答一个很常见的问题:沙箱是否能看到当前目录的文件?

codemode 沙箱本身不具备文件权限,但代码可以调用 tools.read 或 tools.exec_command,让对应工具读取文件,再获得返回内容。

文件访问发生在工具环境里,能读取哪些路径取决于那个环境的权限。

这是有人可能会有疑问,bash 也有管道,也能编排代码,能否替代 codemode?

答案是确定的,但是 bash 更像是一个执行层,具备很强的权限。codemode 更像是把编排的职责单独抽了出来,bash,mcp 都是 codemode 编排层的工具。这样分工会更加的清晰可控。

4. Pi:QuickJS、WASM 与工具桥接

Pi 提供一个名为 codemode 的工具。底层参数定义是 { code: string },支持对应采样形式的模型可以直接提交原始 JavaScript。

脚本作为 async function 的函数体运行,因此支持顶层 await 和 return。例如,工具查询完成后,可以 text(result),也可以直接 return result。

# QuickJS 和 WASM 各做什么

QuickJS 是一个用 C 实现的 JavaScript 引擎。Pi 使用编译后的 quickjs.wasm,由宿主加载并创建 QuickJS VM。

这条链分为构建和运行两个阶段:

构建时:
QuickJS 的 C 源码 → 编译工具链 → quickjs.wasm

运行时:
宿主运行 quickjs.wasm
→ QuickJS 解析、执行模型生成的 JavaScript

被编译成 WASM 的是引擎。模型每次生成的 JavaScript,由这个引擎执行,无须每次重新编译成 WASM。

WASM 是可以承载通用程序的低层指令格式。它不会自动提供 Node API、文件系统或网络;外部能力由宿主接口决定。在这里,Pi 使用 Node worker thread 承载 QuickJS WASM VM,减少脚本死循环对主事件循环的影响,并提供中断路径。

# tools.* 怎样调用真实工具

以 tools.mcp__server__method 为例,这个方法是宿主注入的包装函数。调用它以后,QuickJS 发送包含工具名字、参数和调用 ID 的消息。

宿主找到对应工具,执行 ctx.executeTool,然后把结果送回来。

QuickJS 中的 tools.*(args)
→ 桥接消息:名字、参数、调用 ID
→ Pi 宿主的 ctx.executeTool
→ 具体工具
→ 按调用 ID 返回结果
→ 继续执行 JavaScript

# 工具的发现

MCP 提供 tools/list 和 tools/call。Pi 连接服务器后,读取工具定义,将名字、schema 和执行入口注册到宿主目录。创建一次 Codemode 执行时,再把允许使用的工具注入 tools。

一个工具可以已注册、代码可调用,却没有在初始 prompt 中展开完整 schema。Pi 为此提供 ALL_TOOLS、searchTools、describeTool 和 describeNamespace。

模型可以先提交一段发现代码:

text(await searchTools("查询项目任务", { limit: 3 }));

读取返回的名字和声明后,再生成调用代码。

脚本还具有 text、image、console.*、exit 和 store/load 等接口。按配置也可以提供 models.*,访问 classifier 和图像模型。它们同样通过宿主调用,而不会让 QuickJS 获得任意网络权限。

5. Codex:V8 isolate 如何编排工具

Codex 的 Code Mode 同样通过宿主接口调用工具,但使用 V8 执行 JavaScript。

模型所能看到 tools 是 functions.exec 与 functions.wait。

# exec 运行原始 JavaScript

exec 是 Freeform Tool,输入是原始 JavaScript,可用首行 pragma 配置交回控制权的时间和输出预算:

// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}
const result = await tools.exec_command({
  cmd: "pwd",
  max_output_tokens: 100
});
text(result.output);

这里的 pwd 发生在 shell 工具的执行环境中。

每次 exec 建立新的 V8 isolate 和 Context。可以把 isolate 理解成一个独立的 JavaScript 运行实例,具有自己的 JS 堆与上下文。

# JavaScript 里有什么

除了普通数组、对象和 Promise,Codex 注入以下接口:

接口 作用
tools.* 调用当前允许的宿主工具
ALL_TOOLS 获取当前工具名字和描述
text 输出文本或 JSON 表示
image / audio / generatedImage 输出图片、音频等内容
store / load 跨 exec 保存可序列化值
notify 执行过程中发送额外输出
setTimeout / clearTimeout 管理宿主提供的定时回调
yield_control / exit 交回控制权,或结束脚本

console 没有作为输出入口提供,需要用 text 等接口。工具声明可能以 TypeScript 形式帮助模型理解参数,但实际执行的输入仍然是 JavaScript。

tools 里的成员也不等于 V8 内置功能。它们来自当前会话的工具注册表,例如:

• exec_command 启动 shell,write_stdin 继续读取输出或写入输入。
• apply_patch 提交文件修改补丁;它接收原始补丁字符串。
• view_image 读取本地图片。
• mcp__… 调用接入的 MCP 或应用工具。

安装连接器或改变配置,会改变这份目录。ALL_TOOLS 可以帮助查找名字和描述;具体参数以工具声明为准。源码还支持可选的排名搜索,不能假定每个环境都具有相同发现入口。

# 动态绑定怎样实现

Codex 宿主先筛选 Code Mode 可用工具,把名字、描述和调用种类组成 enabled_tools。运行时遍历目录,为每个工具创建一个 V8 函数,并放到 tools[global_name] 上。

这个函数附带工具索引。被调用时,Rust 回调读取索引、把参数转换成 JSON,创建 PromiseResolver,再向宿主发送 RuntimeEvent::ToolCall。

宿主将它构造成普通工具调用,交给 ToolCallRuntime 执行。结果回来后,运行时兑现对应 Promise,代码继续运行。

Pi 通过 worker 消息桥接,Codex 通过 V8 回调和运行时事件桥接。两者共同的设计是:引擎执行 JavaScript,宿主负责把工具请求送到真实执行入口。

在 Codex 源码中,shell 子调用仍进入原有工具管线,由相应执行机制处理审批和沙箱策略。所以,tools.exec_command 不会因为位于一个大脚本中,就自动获得更高权限。

文件工具、浏览器和远端 MCP 也有各自路径,不能把它们的授权一概归到 shell 沙箱上。

# wait 工具

codemode 执行可能会比较耗时。yield_time_ms 控制多久后先交回已有输出;如果脚本仍运行,exec 返回 cell ID,模型可以用 wait 获取后续事件:

{"cell_id": "…", "yield_time_ms": 10000, "max_tokens": 1000}

6. 安全、失败与恢复

把多个动作写入一个脚本,会让任务执行更紧凑,也让失败处理更值得提前设计。

# 沙箱需要配合工具授权

执行器没有直接文件和网络入口,可以缩小暴露面。但宿主仍应检查每个工具调用:参数是否合法,账户和资源是否在授权范围内,是否需要审批,调用次数和成本是否超过限额。

代码可能发起大量 Promise。实际并发还取决于工具管线和后端能力,需要有界并发与限流。Promise.all 中一个调用失败,不会自动撤销其他调用。

# 脚本失败可能留下部分成功

假设代码先创建任务,再写入说明,最后输出结果。第二步出错,并不会自动删除第一步创建的任务。

如果模型为修复错误而重新运行整个脚本,第一步可能再次执行。因此需要保留工具调用记录,让模型或工作流知道哪些步骤成功、哪些失败、哪些结果未知。

输出也应表达部分失败,避免程序只返回一个统计数字,让模型误以为全部查询都成功。

# 保存数据不等于恢复执行

Pi 成功脚本的 store 写入会保存为 codemode-store 会话条目。Codex 的 store/load 在检查的运行库中使用宿主会话内存保存值。这些能力适合留下对象 ID、游标或摘要。

它们没有保存任意 JS 调用栈和正在等待的 Promise。Codex 的 wait 读取存活 cell 的后续输出,也不能据此推导进程崩溃后的恢复能力。

Durability 需要解决:宿主停止后,重新启动应该从哪里继续,哪些操作可以重试?

最麻烦的窗口是:

发出创建请求
→ 外部系统创建成功
→ 宿主还没保存结果就崩溃
→ 重启后不知道操作是否完成

直接重跑可能产生重复对象,直接跳过又可能丢失后续处理。稳定的步骤 ID、外部 idempotency key、结果查询与对账,才能帮助消除这种不确定性。没有幂等接口的操作,还需要明确处理未知结果。

执行日志和补偿也不能保证任意外部副作用 exactly-once。时间、随机值及并发顺序会影响重放,工作流需要记录或限制这些非确定输入。

7. 设计自己的 Codemode

从 Pi 和 Codex 的调用链,可以提炼出几个职责。

• 工具目录:保存稳定的工具 ID、说明、输入输出 schema 和执行入口。工具较多时,再提供按需发现,避免把完整目录都交给模型。
• 代码执行器:创建受限运行实例,注入明确的 helpers,管理 Promise、输出预算和终止。引擎只是其中一层,内存、执行时间和调用配额仍需按实际实现配置。
• 宿主/harness:工具请求必须经过宿主执行,在这里完成参数验证、授权、审批、并发控制和记录。shell、远端 MCP 或浏览器分别执行具体工作,再把结果按约定格式返回。
• 持久化:需要跨重启继续的任务,还要增加工作流日志、稳定步骤和幂等协议。不要仅凭能够保存 JSON,就把可靠恢复视为已经完成。

对于只有几个工具、步骤简单的任务,直接 Tool Calling 可以足够。涉及多个工具的依赖、批量数据处理和大型目录时,Codemode 更有价值:模型表达处理目标与程序,执行器完成明确的控制流和计算,宿主守住工具的访问边界。

【声明】内容源于网络
0
0
独立开发
技术变现,实现被动收入,SEO优化技巧,流量获取与变现,独立开发指南,网站搭建,niche站点,独立站,affiliate,广告,订阅
内容 95
粉丝 1
独立开发 技术变现,实现被动收入,SEO优化技巧,流量获取与变现,独立开发指南,网站搭建,niche站点,独立站,affiliate,广告,订阅
总阅读15.1k
粉丝1
内容95