KV Cache 是接口
prompt 前綴穩定性當成相容性承諾來維護。
先玩再講。下面的色帶是一條 system prompt 加工具 schema 的 token 序列(數值為教學化抽象)。點「播放」,演示會依次做幾個常見操作:原樣重發、改 persona 一個字、加一個工具、追加對話、插件加載順序抖動。每一步色帶上會標出緩存從第幾個 token 起失效,右邊的計價器累計你多付的重算 token。右上角可以切換 DSH 模式和對照模式,抖動那一步的結果完全不同。
先把 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 行原文:
(只要可見的工具定義及其順序不變,前綴就穩定。註冊、卸載或按作用域限制工具,都可能從第一個變化的 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 裏寫了沒註冊的工具名直接拋。拋異常的時機在組裝階段,請求發出之前。
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))
}
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。
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 是否有等價的逐包緩存影響文檔,已核對的本地快照裏未見,這一條基於已公開證據保留未知。
找出你的 prompt 裏的緩存殺手
假設你的 agent 在 system prompt 第二行寫了「當前時間:2026-08-13 22:04:35」,每秒都在變。推演:每條請求嘅快取從第幾段開始失效?一日 1000 條請求多付幾多重算 token?給出兩個修復方案並比較:把時間挪到 prompt 末尾的動態上下文裏,或者把精度降到天。提示:想想哪個方案在跨天的邊界上仍然會斷一次緩存。
給你的團隊寫一份 KV Cache effect 模板
模仿 DSH 的三段式(模型看到什麼 / token 成本 / 緩存影響),為你項目裏「會進入模型請求的東西」列一張清單:system prompt、工具 schema、動態注入的上下文、RAG 檢索結果。逐項寫出它的緩存影響,標出哪幾項放錯了位置。寫完你很大機會會發現至少一個和 Claude Code 那 10.2% 同款的問題。