DeepSeek Harness · 工程方法論

KV Cache 是介面

prompt 前綴穩定性當成相容性承諾來維護。

課程目標讀完你能說清三件事:為什麼改 prompt 前綴裡的一個字會讓後面所有 token 的快取作廢、DSH 靠哪三件事把前綴穩定性當成相容性承諾來維護(文件紀律、工具順序中心列表、嚴格插值)、以及 Claude Code 用一次 10.2% 的真金白銀教訓驗證了同一個道理。
互動演示 · 前綴穩定性顯微鏡

先玩再講。下面的色帶是一條 system prompt 加工具 schema 的 token 序列(數值為教學化抽象)。點「播放」,演示會依次做幾個常見操作:原樣重發、改 persona 一個字、加一個工具、追加對話、外掛載入順序抖動。每一步色帶上會標出快取從第幾個 token 起失效,右邊的計價器累計你多付的重算 token。右上角可以切換 DSH 模式和對照模式,抖動那一步的結果完全不同。

發給模型的請求前綴(每格一段,寬度按 token 數)
harness 身份 persona 工具 schema 動態上下文 對話歷史
尚未發出請求。
失效成本計價器
0
本該命中快取、卻被迫重算的 token 累計。快取命中的輸入價通常比未命中便宜一個數量級,這個數字直接乘在帳單上。
本次請求命中–
請求次數0
點「播放」開始。KV Cache(鍵值快取):模型對相同的 token 前綴可以複用上一次的注意力計算,前提是前綴逐字相同。
邏輯拆解 · 為什麼一個字能廢掉一整條快取

先把 KV Cache 說成人話。模型處理請求時,會為每個 token 算出一堆中間結果(鍵和值)快取起來。下一條請求進來,只要開頭的 token 序列和上一條逐字相同,這段前綴的計算就能直接複用,provider 按命中給你打折。DeepSeek 的官方定價裡,命中快取的輸入 token 比未命中便宜一個數量級(具體倍率以官方價目頁為準)。

關鍵在逐字相同這四個字。快取按前綴匹配:從第一個不同的 token 起,後面全部作廢。而 agent 請求的開頭是什麼?system prompt 加工具 schema,動輒幾千 token,每條請求都帶。改 persona 裡一個詞、換一下工具順序、在開頭塞個當前時間,快取就從那個位置斷掉,之後的每條請求都全價重算。

所以 DSH 得出一個結論:prompt 前綴是模型這個 API 的介面,它的穩定性是一種相容性承諾,要像維護公開 API 一樣維護。落到工程上是三件事。

第一件,寫進文件紀律。本地快照裡 packages 下 268 個包 README,有 215 個帶一個固定的 #### KV Cache effect 小節。任何會出現在模型請求裡的東西,文件必須按三段式交代:模型看到什麼(What the model sees)、token 成本多少(Token effect)、對快取有什麼影響(KV Cache effect)。以 packages/core/tools/README.md 的工具 schema 一節為例,第 145 行原文:

Prefix-stable while visible definitions and their order are unchanged. Registration, disposal, or scoped restriction may invalidate reuse from the first changed schema token.
(只要可見的工具定義及其順序不變,前綴就穩定。註冊、解除安裝或按作用域限制工具,都可能從第一個變化的 schema token 起使快取複用失效。) 出處:deepseek-harness-master 倉庫 packages/core/tools/README.md 第 145 行,核對日期 2026-08-13

同一個檔案第 186 到 188 行還有一句反向陳述:工具呼叫的歷史和結果是 append-only 的,新內容跟在可複用前綴後面,不會打翻已有的快取。什麼傷快取、什麼不傷,全部寫成可查的文件條目。

第二件,工具順序由中心列表規範化。工具 schema 是前綴的大頭,它的順序原本跟著外掛註冊順序走。外掛是併發載入的,註冊順序隨環境抖動,DSH 在 CI 裡實際觀察到了不同的請求頭(Agent Note 2026-07-06-explicit-tool-order 的問題一節)。順序影響請求位元組,請求位元組影響快取,於是它成了必須顯式治理的對象:配置裡的 toolOrder 列表統一定序,列表裡必須恰好有一個 <unlisted-tools> 其餘項標記,沒配列表就按字典序兜底。規範化發生在 assemble() 內部、waterfall 之前,註冊順序在任何可觀測的位置都不再出現。

第三件,嚴格插值,寧可拋異常不交付壞 prompt。persona 是模板,{{model}} 這樣的變數群組嚴格按註冊表解釋:引用了未註冊的變數、變數本次沒有值、花括號組格式錯誤,一律拋異常,輪次直接失敗,一個請求都不會發出去。理由很直接:靜默容錯等於把一個悄悄變形的前綴發給模型,快取悄悄失效,壞 prompt 還可能悄悄改變行為。大聲失敗反而便宜。

順序也是介面

兩組內容完全相同、只是順序不同的工具 schema,對快取來說是兩個不同的前綴。所以工具順序不能交給載入時序這種環境噪聲決定。

失效從第一個變化 token 開始

快取按前綴匹配,越靠前的內容越碰不得。把易變的東西(時間、動態狀態)往後放,把萬年不變的身份和 schema 往前放。

追加不傷快取

對話歷史 append-only 地增長,舊前綴原樣保留,只為新增部分付全價。這也是會話日誌只追加設計在帳單上的紅利。

關鍵證據 · 中心列表與嚴格插值的原始碼

下面這段是工具排序的核心邏輯。函式開頭(第 165 至 168 行)先做一道保留名檢查:工具提供方敢用 <unlisted-tools> 這個保留名直接拋異常。往下就是排序本體,看兩個失敗分支:沒配列表走字典序兜底,toolOrder 裡寫了沒註冊的工具名直接拋。拋異常的時機在組裝階段,請求發出之前。

packages/core/system-prompt/src/index.ts第 169 至 178 行
  if (toolOrder === undefined) return tools.sort(compareToolNames)
  const unknown = toolOrder.filter(name => name !== TOOL_ORDER_REST && !knownNames.has(name))
  if (unknown.length > 0) {
    throw new Error(`toolOrder lists unregistered tool${unknown.length > 1 ? 's' : ''} ${unknown.map(name => `"${name}"`).join(', ')}; known tools: ${[...knownNames].sort().join(', ') || '(none)'}`)
  }
  const listed = new Set(toolOrder)
  const rest = tools.filter(tool => !listed.has(tool.name)).sort(compareToolNames)
  return toolOrder.flatMap(name =>
    name === TOOL_ORDER_REST ? rest : tools.filter(tool => tool.name === name))
}
原始碼快照說明:依據本地倉庫 deepseek-harness-master,核對檔案 packages/core/system-prompt/src/index.ts,核對日期 2026-08-13。程式碼塊保留原始碼原文。

兩個邊界條件值得記住,出處都在 Agent Note 2026-07-06-explicit-tool-order。外掛熱過載後註冊順序變了,工具順序會變嗎?不會,中心列表在 waterfall 之前規範化,註冊順序無處可觀測。toolOrder 裡寫了個拼錯的工具名會怎樣?該 Note 後果一節寫得很細:輪次在組裝時失敗,不開步驟、不記請求頭、不向轉接器發請求,每個輪次都同樣失敗直到配置修好,行程本身保持執行。

嚴格插值這邊是三個連著的拋異常分支,一個都不放過。第一個管格式:變數名不匹配命名正則就拋「malformed prompt variable reference」,連 {{}} 這種空名都被註釋點名,走的就是這條格式錯誤路徑。第二個管註冊:名字不在註冊表裡就拋「unknown prompt variable」,報錯順帶列出全部已註冊變數名。第三個管取值:註冊了但這次組裝沒給值,同樣拋異常。三個分支攔下的都是同一件事,一個悄悄變形的前綴。

出處:packages/core/system-prompt/src/index.ts 第 277 至 290 行的插值分支,核對日期 2026-08-13。

第 283 行的 Object.hasOwn 有個小心思:用普通屬性訪問查 {{constructor}} 會順著原型鏈摸到 Object 的內建方法,被誤認為已註冊變數。查自有屬性,原型鏈上的名字一律算未註冊。防的就是這種悄悄放行的壞 prompt。

橫向對比 · 一個 10.2% 的教訓和一條靜態路線

Claude Code:還原原始碼 restored-src/src/tools/AgentTool/prompt.ts 第 57 到 64 行的註釋記錄了一次真實事故。子代理列表原本嵌在工具描述裡,MCP 非同步連線、外掛過載、權限模式切換都會改變這個列表,工具描述一變,整塊工具 schema 的快取全部作廢。這一個問題佔了全球機群 cache_creation token 的 10.2%。修法和 DSH 的思路殊途同歸:把易變的列表從靜態前綴裡挪出去,改成單獨的 attachment 訊息注入,工具描述保持可快取(材料出處:claude-code-sourcemap-main/study/chapters/05-multi-agent.md 第 106 至 121 行)。區別在時序:Claude Code 是帳單上看到 10.2% 之後修的,DSH 在 CI 抖動階段就把順序治理掉了,還把紀律鋪到了 215 份文件裡。

Grok Build:走靜態模板路線。system prompt 從預生成的模板解密渲染(crates/codegen/xai-grok-agent/src/prompt/template.rs),再拼上 AGENTS.md 和 skills 內容(同目錄 mod.rs 的模組劃分)。模板是編譯期固定的,前綴天然比動態組裝穩定,這是靜態路線的先天優勢。代價是靈活性:DSH 那種外掛隨時貢獻段、變數、工具的組裝模型,在這條路線上不存在。至於 Grok 是否有等價的逐包快取影響文件,已核對的本地快照裡未見,這一條基於已公開證據保留未知。

課堂練習
01

找出你的 prompt 裡的快取殺手

假設你的 agent 在 system prompt 第二行寫了「當前時間:2026-08-13 22:04:35」,每秒都在變。推演:每條請求的快取從第幾段開始失效?一天 1000 條請求多付多少重算 token?給出兩個修復方案並比較:把時間挪到 prompt 末尾的動態上下文裡,或者把精度降到天。提示:想想哪個方案在跨天的邊界上仍然會斷一次快取。

02

給你的團隊寫一份 KV Cache effect 模板

模仿 DSH 的三段式(模型看到什麼 / token 成本 / 快取影響),為你專案裡「會進入模型請求的東西」列一張清單:system prompt、工具 schema、動態注入的上下文、RAG 檢索結果。逐項寫出它的快取影響,標出哪幾項放錯了位置。寫完你很可能會發現至少一個和 Claude Code 那 10.2% 同款的問題。

Takeaway:KV Cache 按前綴逐字匹配,從第一個變化的 token 起全部作廢,所以 prompt 前綴的穩定性是一種要維護的相容性承諾。DSH 的三件套:215 份文件裡的固定 KV Cache effect 小節、toolOrder 中心列表消滅順序抖動、插值格式錯誤寧可拋異常。把易變的內容放到前綴末尾之後,是所有 harness 通用的省錢動作。