DeepSeek Harness · 代码模式

Code Mode:一段代码顶多轮工具调用

模型写一段小程序,run_code 把它送进 worker 沙箱,五次工具调用一轮搞定。本课讲这个设计背后的两个思路。

课程目标读完能说清两件事:为什么让模型写一段程序,比逐个发工具调用又快又省;模型写的代码在沙箱里跑,宿主凭什么自保,权限为什么一寸不松。
先玩一遍 · 一段代码 vs 多轮调用
同一个任务:读 3 个日志文件,汇总统计,写 1 份报告
传统工具调用0轮采样0k token
模型 · 第 1 轮采样先读第一个文件 logs/a.log。
read_file({ path: "logs/a.log" })
412 行全文回传,整段进上下文
模型 · 第 2 轮采样再读 logs/b.log。
read_file({ path: "logs/b.log" })
398 行全文回传,整段进上下文
模型 · 第 3 轮采样还差 logs/c.log。
read_file({ path: "logs/c.log" })
441 行全文回传,整段进上下文
模型 · 第 4 轮采样对着上下文里的三份全文算统计,写报告。
write_file({ path: "report.md", ... })
写入成功
模型 · 第 5 轮采样汇总完成,输出最终回答。
任务完成 · 共 5 轮采样,每轮重发滚大的上下文
Code Mode(PTC)0轮采样0k token
模型 · 第 1 轮采样写一段程序,让沙箱去编排这五步。
示意程序(5 行,课程示例,非源码引用)
let total = 0, errors = 0 // 统计值,全程留在沙箱里for (const p of ['a.log', 'b.log', 'c.log']) { // 三个文件,循环里读 total += 统计(await tools.read_file({ path: p })) }await tools.write_file({ path: 'report.md', … }) // 写报告,照走审批return { total, errors } // 只有这一行回到模型
worker 沙箱 · 隔离环境里执行,中间结果不回模型
await tools.read_file("logs/a.log")经宿主转发,照走审批管线
await tools.read_file("logs/b.log")结果留在沙箱变量里
await tools.read_file("logs/c.log")结果留在沙箱变量里
循环内累加统计纯计算,零往返
await tools.write_file("report.md")仍走同一条权限管线
return { total, errors }只有这个值加日志会离开沙箱
一次性返回 { total: 1251, errors: 17 } · 还是第 1 轮采样
点播放,看同一个任务在两种模式下怎么跑。
采样轮数:5 vs 1左边每个动作换一轮采样;右边一轮采样写出程序,沙箱替它跑完全部动作。
吞吐示意:约 18.3k vs 约 2.6k左边每轮重发滚大的上下文;右边上下文里只有程序和一次性返回值。
中间结果去向左边三份文件全文都躺进上下文;右边全部留在沙箱变量里,只回传 print 或 return 的部分。
教学示意:轮数与 token 数为课程化估算,用于展示两种模式的结构差异;右侧程序为 5 行课程示例,非源码引用。
思路一 · 让模型写程序,不要让模型当遥控器

先把名字说清楚。官方发布文叫它 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 行)。

遥控器模式(原生工具调用) 模型 工具 5 次往返,每次往返都要一轮采样,上下文越滚越大 程序模式(Code Mode) 模型 只采样 1 轮 worker 沙箱 循环里连调 5 个工具,中间结果留在变量里 一段程序 只有 print 和 return 回来
教学化结构图:同一个五步任务,上面走 5 次往返,下面走 1 次。
五次往返,变成一次。

这句话有出处,preset 文件头的设计意图注释原话就是「five round trips becomes one」,本课标题就从这来:

apps/cli/config/agent-presets/code/agent.cordis.yml第 1 至 6 行节选
# 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 行),谁也别想只报个好听的数。

模型 只见到日志和返回值 宿主进程(验货、审批、记账) 验货:剥掉类型,核对绑定名单 不合格的程序直接拒,worker 不启动 审批:与原生模式同一条工具管线 每个子调用带父 token,先过 pre-execute 记账:时间与字节两头核对 computeMs、maxWallMs、OutputLedger worker 沙箱 程序(模型写的) 每次运行全新,用完即弃 tools 命名空间 只包含名单上的工具 资源上限 空环境、堆上限、强制终止 程序 reply call 日志 + 返回值 宿主与 worker 之间只有结构化消息,每条入站消息先验形状再逐字段重建
教学化结构图:节点与连线用于解释源码关系,内容经过课程化整理。

然后是权限规矩:程序进了沙箱,审批一寸不松。每次 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 机制。

依据本地 study 资料检索

Grok Build

无此机制,基于已公开证据。在本地 grok-build-main 仓库全库检索 run_code 与 code mode,只有遥测事件名是字面撞词。它的工具体系走原生调用加 toolset preset 组合,没有让模型写程序、由沙箱编排工具 API 的通道。

依据本地源码检索 · 2026-08-13

Codex CLI 与 Cloudflare

思路相通,但本课没核对这两家源码,只说事实:DSH 的设计笔记明确引用了 Cloudflare 的博客,核心观察是模型写代码的能力好于连发工具调用(出处:.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md)。Codex CLI 方向有类似公开讨论,同样仅提思路。

未核对源码 · 仅引公开叙述
课堂练习
01

两段恶意程序,各撞哪个预算?

程序 A 是同步热循环 while (true) {},程序 B 是 await new Promise(() => {}),永远不会 resolve。对照思路二第三道防线推演:A 和 B 分别被 computeMs 还是 maxWallMs 终止?为什么 A 不能靠挂一个待完成的工具调用躲过计费?

Takeaway:Code Mode(官宣名 PTC)用一轮采样换掉 N 轮往返:模型写程序,沙箱替它跑,中间值永不进上下文。沙箱是隔离,安全边界谈不上,靠空环境、堆上限、双预算和两头记账自保。权限不因进沙箱而松动,每个子调用照走同一条审批瀑布。