Code Mode:一段代码顶多轮工具调用
官宣叫 PTC,源码叫 code mode:worker 沙箱的隔离、通信协议与双重记账
本页解决的问题
先给结论「Code Mode:一段代码顶多轮工具调用」要解决的关键问题是什么?
官宣叫 PTC,源码叫 code mode:worker 沙箱的隔离、通信协议与双重记账
让这个结论先证明自己值得留下。 把这一页当成决策工具,而不是需要背下来的定义。把概念连到一个真实任务、一个可观察结果,以及一个能改变你判断的失败上。
写下一个问题:试完这个方法后,你能用什么证据回答它?
结论听起来很完整,却没有检查最关键的假设。
先把名字说清楚。官方发布文叫它 PTC,程序化工具调用,preset 元数据也写着 name: PTC 模式(出处:apps/cli/config/agent-presets/code/preset.yml 第 1 行)。去源码里搜 PTC,搜不到。内部命名从头到尾是 code mode:配置项 mode: code、工具名 run_code。两个名字,同一个机制。
原生工具调用像给模型一个遥控器。按一下,读一个文件;结果回来,再按一下。每按一下都是一轮完整采样,模型要把滚大的上下文重新读一遍才能决定下一步。读 3 个日志文件写份报告就是 5 轮,换成 50 个文件,预算和耐心一起烧完。成本瓶颈不在工具本身,在一步一采样的节奏上。
一轮采样里,模型直接写一段 TypeScript 小程序,程序里用 await tools.name(args) 想调几次调几次,循环、分支都行。程序在沙箱里把活干完,中间结果全留在沙箱变量里,跑完只把 print 和 return 的内容送回模型。工具描述的原话是「Only what you print or return comes back — curate it.」(出处:packages/core/tools/src/code-mode.ts 第 52 行)。
这句话有出处,preset 文件头的设计意图注释原话就是「five round trips becomes one」,本课标题就从这来:
# The `code` agent preset: the standard coding agent, presented as Code Mode.
#
# Everything in `standard` is here unchanged. What is added is the `tool-presentation`
# row: instead of one tool call per action, the model writes a TypeScript
# program against a generated SDK and `run_code` executes it, so a sequence
# that would be five round trips becomes one.
deepseek-harness-master,核对文件 apps/cli/config/agent-presets/code/agent.cordis.yml,核对日期 2026-08-13。代码块保留源码原文。注释后半段还埋了个伏笔:这个 preset 改的只是工具的呈现方式,注册表本身留在宿主手里(出处:同文件第 8 至 11 行)。思路二讲的权限跟随,就从这个安排来。
这是网络编程几十年的老道理:往返贵,批处理便宜。数据库有批量写入,RPC 框架都在攒 batch。模型采样一轮比一次网络往返贵得多,把 N 次往返合成一次,收益只会更夸张。哪天 DSH 换个语言重写,这笔账照样成立。
思路一有个前提没解决:沙箱里跑的是模型现写的代码,没人审过一行。要是图省事直接在宿主进程里跑,翻车方式随便挑:环境变量里的 API key 随手读走;一个 while (true) 把内存吃光,宿主跟着一起死;程序还能伪造消息,冒充工具结果骗过上层。所以宿主必须从第一天就假设对面会使坏,这个假设立住之后,剩下的都是工程题。
DSH 的做法是三道防线,外加一条权限规矩。
第一道,资源焊死。每次运行新开一个全新 worker,用完即弃。启动参数把路堵死:环境变量清空,程序拿不到宿主的任何凭据;继承的加载器标志掐断;堆上限焊死,程序把堆吃爆,worker 直接退出(出处:packages/code-runtime/code-runtime-worker-thread/src/index.ts 第 378 至 387 行)。
第二道,通信只认结构化消息。宿主和 worker 之间只有一条消息端口,不共享内存。上行消息只有 call、log、output-limit、done 四种,每条入站消息先验形状,再逐字段重建一份干净的,垃圾静默丢弃(出处:同文件第 142 至 165 行)。worker 侧的 tools 命名空间用 null-prototype 构建,伪造 __proto__ 这种名字摸不到任何东西(出处:bootstrap.ts 第 324 至 326 行)。
第三道,时间和字节两头记账,worker 自己报的数字不作数。时间上两本账:computeMs 轮询 worker 实测的忙碌时间,热循环藏不住,干等慢工具又不冤枉计费;maxWallMs 管兜底,两个预算到点都直接强制终止(出处:index.ts 第 534 至 545 行)。字节上 worker 发送前自己预检,宿主收到后用 OutputLedger 再记一遍(出处:index.ts 第 169 至 229 行),谁也别想只报个好听的数。
然后是权限规矩:程序进了沙箱,审批一寸不松。每次 await tools.xxx 都被宿主包装成子调度,带着父调用的 token,走和原生模式同一条 pre-execute 审批瀑布,子调用 id 形如 callId:code:n,事件里全程留痕(出处:packages/core/tools/src/code-mode.ts 第 545、477、470 行)。程序能绑到的工具,正好是系统提示词里声明过的那些,受限工具在名单里直接消失(出处:同文件第 601 至 608 行)。反过来,mode: code 下想绕开 run_code 直发原生调用,进策略管线之前就被拒为 UNKNOWN_TOOL(出处:docs/subsystems/tools.zh.md)。入口收窄了,权限没换门。
DSH 自己给这套隔离的定位很清醒,README 开门见山:
「这是隔离措施,而非安全边界:其信任立场有意与 bash 等价…但提供 bash 没有的隔离:独立 isolate、空环境、堆上限与强制终止。」
出处:packages/code-runtime/code-runtime-worker-thread/README.zh.md,省略号处为原文引注不信任边界是安全设计的通用形状,和 worker_threads 这个具体技术没关系。换成容器、V8 isolate 或别家语言的子进程,该做的还是这四条:新开干净环境、资源封顶、通信走窄接口逐条验证、账本两头各记一份。浏览器对网页、操作系统对进程,走的都是同一套思路,模型代码只是名单上新来的一位,待遇照旧。
Claude Code
没有等价物。bash 工具是通用逃生舱,模型可以写脚本再执行,但 Read、Edit 这些工具 API 不作为可编程绑定暴露给脚本,脚本内部的动作也不走各工具自己的管线。Anthropic 官方博客《Code execution with MCP》提出了同思路,截至核对日期,未见 Claude Code 产品内置同类 run_code 机制。
Grok Build
无此机制,基于已公开证据。在本地 grok-build-main 仓库全库检索 run_code 与 code mode,只有遥测事件名是字面撞词。它的工具体系走原生调用加 toolset preset 组合,没有让模型写程序、由沙箱编排工具 API 的通道。
Codex CLI 与 Cloudflare
思路相通,但本课没核对这两家源码,只说事实:DSH 的设计笔记明确引用了 Cloudflare 的博客,核心观察是模型写代码的能力好于连发工具调用(出处:.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md)。Codex CLI 方向有类似公开讨论,本课没有核对它的源码。它的 exec 与 wait 后来另开了一章按 Rust 源码逐行拆解,超时和计费的取舍与 DSH 并不相同。
两段恶意程序,各撞哪个预算?
程序 A 是同步热循环 while (true) {},程序 B 是 await new Promise(() => {}),永远不会 resolve。对照思路二第三道防线推演:A 和 B 分别被 computeMs 还是 maxWallMs 终止?为什么 A 不能靠挂一个待完成的工具调用躲过计费?
「先玩一遍 · 一段代码 vs 多轮调用」的能力藏在每次交接里
「先把名字说清楚。官方发布文叫它 PTC ,程序化工具调用,preset 元数据也写着 name: PTC 模式 (出处: apps/cli/config/agent-presets/code/preset.yml 第 1 行)。去源码里搜 PTC,搜不到。内部命名从头到尾是 code mode :配置项 mode: code 、工具名 run_code 。两…」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。
先写清状态,再增加能力
从「原生工具调用像给模型一个遥控器。按一下,读一个文件;结果回来,再按一下。每按一下都是一轮完整采样,模型要把滚大的上下文重新读一遍才能决定下一步。读 3 个日志文件写份报告就是 5 轮,换成 50 个文件,预算和耐心一起烧完。成本瓶颈不在工具本身,在一步一采样的节奏上」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。
成功路径不能代表系统可靠
用「程序 A 是同步热循环 while (true) {} ,程序 B 是 await new Promise(() => {}) ,永远不会 resolve。对照思路二第三道防线推演:A 和 B 分别被 computeMs 还是 maxWallMs 终止?为什么 A 不能靠挂一个待完成的工具调用躲过计费」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。
从「先玩一遍 · 一段代码 vs 多轮调用」走到「思路一 · 让模型写程序,不要让模型当遥控器」
「先玩一遍 · 一段代码 vs 多轮调用」先把问题落在「同一个任务:读 3 个日志文件,汇总统计,写 1 份报告 播放 单步 重置 传统工具调用 0 轮采样 0 k token 模型 · 第 1 轮采样 先读第一个文件 logs/a.log。 read_file({ path: "logs/a.log" }) 412 行全文回传,整段进上下文 模型 · 第 2 轮采样 再读 logs/b.log。 read_file({ path: "logs/b.log" }) 398…」上;到了「思路一 · 让模型写程序,不要让模型当遥控器」,讨论继续推进到「先把名字说清楚。官方发布文叫它 PTC ,程序化工具调用,preset 元数据也写着 name: PTC 模式 (出处: apps/cli/config/agent-presets/code/preset.yml 第 1 行)。去源码里搜 PTC,搜不到。内部命名从头到尾是 code mode :配置项 mode: code 、工具名 run_code 。两个名字,同一个机制」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。
把这条判断带到下一个场景
分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。
- 「先玩一遍 · 一段代码 vs 多轮调用」:同一个任务:读 3 个日志文件,汇总统计,写 1 份报告 播放 单步 重置 传统工具调用 0 轮采样 0 k token 模型 · 第 1 轮采样 先读第一个文件 logs/a.log。 read_file({ path: "logs/a.log" }) 412 行全文回传,整段进上下文 模型 · 第 2 轮采样 再读 logs/b.log。 read_file({ path: "logs/b.log" }) 398…
- 「思路一 · 让模型写程序,不要让模型当遥控器」:先把名字说清楚。官方发布文叫它 PTC ,程序化工具调用,preset 元数据也写着 name: PTC 模式 (出处: apps/cli/config/agent-presets/code/preset.yml 第 1 行)。去源码里搜 PTC,搜不到。内部命名从头到尾是 code mode :配置项 mode: code 、工具名 run_code 。两个名字,同一个机制
- 「最后的要点」:第一道,资源焊死。每次运行新开一个全新 worker,用完即弃。启动参数把路堵死:环境变量清空,程序拿不到宿主的任何凭据;继承的加载器标志掐断;堆上限焊死,程序把堆吃爆,worker 直接退出(出处: packages/code-runtime/code-runtime-worker-thread/src/index.ts 第 378 至 387 行)
最后的「最后的要点」把讨论落到「第一道,资源焊死。每次运行新开一个全新 worker,用完即弃。启动参数把路堵死:环境变量清空,程序拿不到宿主的任何凭据;继承的加载器标志掐断;堆上限焊死,程序把堆吃爆,worker 直接退出(出处: packages/code-runtime/code-runtime-worker-thread/src/index.ts 第 378 至 387 行)」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。
我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。
读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。
如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。
还没有这篇文章的讨论。