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 輪往返:模型寫程式,沙箱替它跑,中間值永不進上下文。沙箱是隔離,安全邊界談不上,靠空環境、堆上限、雙預算和兩頭記帳自保。權限不因進沙箱而鬆動,每個子呼叫照走同一條審批瀑布。