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 方向有類似公開討論,本課沒有核對它的源碼。它的 exec 與 wait 後來另開了一章按 Rust 源碼逐行拆解,超時和計費的取捨與 DSH 並不相同。

本課未核對源碼 · 僅引公開敍述 · Codex · exec 與 wait
課堂練習
01

兩段惡意程式,各自撞邊個預算?

程式 A 是同步熱循環 while (true) {},程式 B 是 await new Promise(() => {}),永遠不會 resolve。對照思路二第三道防綫推演:A 和 B 分別被 computeMs 還是 maxWallMs 終止?點解 A 唔能夠靠掛一個待完成嘅工具調用躲過計費?

Takeaway:Code Mode(官宣名 PTC)用一輪採樣換掉 N 輪往返:模型寫程式,沙箱替它跑,中間值永不進上下文。沙箱是隔離,安全邊界談不上,靠空環境、堆上限、雙預算和兩頭記帳自保。權限不因進沙箱而鬆動,每個子調用照走同一條審批瀑布。