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 方向有類似公開討論,本課沒有核對它的原始碼。它的 exec 與 wait 後來另開了一章按 Rust 原始碼逐行拆解,超時和計費的取捨與 DSH 並不相同。
兩段惡意程式,各撞哪個預算?
程式 A 是同步熱迴圈 while (true) {},程式 B 是 await new Promise(() => {}),永遠不會 resolve。對照思路二第三道防線推演:A 和 B 分別被 computeMs 還是 maxWallMs 終止?為什麼 A 不能靠掛一個待完成的工具呼叫躲過計費?