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 唔能夠靠掛一個待完成嘅工具調用躲過計費?