Code Mode:一段代码顶多轮工具调用
模型写一段小程序,run_code 把它送进 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 方向有类似公开讨论,同样仅提思路。
两段恶意程序,各撞哪个预算?
程序 A 是同步热循环 while (true) {},程序 B 是 await new Promise(() => {}),永远不会 resolve。对照思路二第三道防线推演:A 和 B 分别被 computeMs 还是 maxWallMs 终止?为什么 A 不能靠挂一个待完成的工具调用躲过计费?