Earendil 官方授權中文譯文 · 03
Agent 中的提示快取
繁體轉換版說明
本頁為簡中授權譯文之繁體轉換版。英文原文版權歸 Earendil 所有,中文譯文及轉換部分按 CC BY 4.0 發布。如有歧義,請以英文原文為準。
- 原文標題 提示快取(Prompt Caching) In Agents
- 作者 Earendil Engineering
<rfc@earendil.com>- 釋出日期 2026-07-22
- 原文地址 earendil.com/posts/prompt-caching
- 授權說明 經 Earendil 授權改編與翻譯(Adapted and translated with permission from Earendil.)
- 譯文許可 CC BY 4.0
人們常常把大語言模型想象成函式:傳送一些文字,接收一些文字。這是一個有用的抽象,但它忽略了執行程式設計 Agent 時最重要的事實之一:絕大部分輸入都與上一次相同。換句話說,我們通常只是在末尾追加內容。
程式設計 Agent 會把系統提示、工具定義、專案說明、對話歷史、工具呼叫和工具結果傳送給模型。下一輪,它會再次傳送幾乎所有這些內容,只多出少量新材料。當工作階段增長到數萬甚至數十萬 token 時,每輪都重新計算整段提示會既慢又昂貴。
提示快取讓這種工作方式在經濟上勉強可行,但它也十分脆弱。工具定義發生變化、切換模型或提供商改變路由,都可能讓原本應該很便宜的增量請求變成對整個上下文的完整重放。
因此,對程式設計 Agent 來說,快取行為不只是實現細節或效能最佳化。它會影響延遲、成本、工具設計、工作階段設計,甚至影響哪些產品功能應該提供給使用者。
KV 快取裡有什麼
Transformer 處理提示大致分為兩個階段。在 prefill(預填充)階段,它讀取輸入 token,併為它們計算注意力狀態;在 decode(解碼)階段,它每次生成一個新 token。
在每一層注意力中,每個處理過的 token 都會產生一個 key 和一個 value。它們與雜湊表裡的鍵值查詢並不完全相同:兩者都是數字陣列,通常由浮點數或低精度量化值組成。處理一個新 token 時,模型會將這個 token 的 query 與此前的 key 比較,以判斷每個早期 token 與當前 token 的相關程度。隨後,它根據這些相關性分數,對相應的 value 進行加權混合。從這個意義上說,key 是模型用來匹配的內容,value 是模型取回的資訊;不過這種查詢是模糊的,並不像字典那樣“只返回一個完全匹配的結果”。
這些 key 和 value 會被保留下來,讓下一個生成的 token 無需重新計算早期 token,就能關注此前的全部內容。這個保留下來的狀態就是 KV 快取。
從概念上看,一次請求是這樣的:
請求 1:
[系統][工具][使用者][助手][工具結果][使用者]
<-------------------- 預填充 -------------------->
|
每個 token、每一層的 K 和 V 張量
請求 2:
[系統][工具][使用者][助手][工具結果][使用者][新增]
<------------------ 可複用字首 ----------------><-->
|
新工作真實的表示更加複雜,會因模型而異,而且體積“相當”大。最重要的特性是:這些表示對應某一個特定的 token 字首。兩段意思相同但 token 化結果不同的提示,無法共享 KV 快取。如果中間有一個 token 發生變化,從它之後的所有內容都會成為不同的延續。
提示快取把這種狀態的生命週期延長到一次生成之外。當程式設計 Agent 的下一次 API 請求以相同 token 開頭時,推理系統可以複用與該字首匹配的既有計算,只對新增字尾執行預填充。理論部分到此為止。
快取存在哪裡
快取要發揮作用,就必須儲存在某個地方,並且能被再次定址。推理系統大致有兩種方式,讓後續請求能夠使用 KV 快取。
比較簡單的方式是工作階段親和性(session affinity)。它把 KV 快取保留在最初執行計算的 GPU 上或其附近,並將下一次請求重新路由到同一個工作節點。工作階段 ID 或提示快取 key 可以成為一個簡單的路由提示,所以這個問題甚至可能只在 HTTP 負載均衡層就得到處理,無需檢查請求內容。
請求(session-42) --> 路由器 --> 工作節點 7 --> GPU 7 的 KV 快取
下次(session-42) --> 路由器 --> 工作節點 7 --> GPU 7 的 KV 快取這樣可以避免透過網路移動體積很大的快取。它在正常工作時速度很快,但也限制了排程方式。選中的工作節點可能過載、重啟或淘汰對應快取。路由器也可能認為平衡整個叢集的負載比保留某個工作階段的快取更重要。不過,這種方案依然很有吸引力,因為它幾乎不需要額外的部署基礎設施和硬體。
另一種方式是分散式快取。KV 塊可以存放在另一層記憶體中,或在多個工作節點之間共享,讓請求不必過度綁定到某一塊 GPU。
+--------------------+
請求 --> 排程器 -------->| 工作節點 3 / GPU 3 |
| +--------------------+
|
+----------> 分散式 KV 塊
|
+----------> 工作節點 9 / GPU 9這樣能提高排程的靈活性和故障恢復能力,但移動、索引和保留 KV 塊本身就是一個系統工程問題。不同實現會以各自的方式組合 GPU 記憶體、主機記憶體、本地儲存、遠端儲存、字首感知路由和淘汰策略。
客觀來看,KV 快取雖然很大,但在某些方面又沒有想象中那麼大。藉助各種技巧,即使是很長的對話,也可以把 KV 快取壓縮到幾 GB 的量級。
快取與字首
Pi 的工作階段是樹,而不是清單。/tree 可以把當前對話移回較早的節點,然後沿著另一條分支繼續。回退操作可以丟棄當前活動字尾,卻不把它從工作階段檔案中刪除。新分支可能與舊上下文共享大部分內容、只共享少量內容,也可能實際上完全不共享。這種設計並非 Pi 獨有;不少程式設計 Agent 至少在概念上有類似機制。即使工作階段不以樹的形式表示,Agent 具有某種回退功能也並不少見。
+-- E -- F 另一條分支
|
工作階段 S:根 -- A -- B -- C -- D 當前分支
|
+-- Z 靠近起點的分支這三條分支可以擁有相同的 Pi 工作階段 ID。從路由器的角度看,它們屬於同一個工作階段;從提示快取的角度看,它們是三段只共享部分字首的 token 序列。
如果快取保留了可複用的字首塊,從 D 跳到 F 時,仍可能複用 根 -> C。但如果快取只保留最熱門的延續、共享塊已被淘汰,或請求被路由到了別處,快取命中就可能大幅減少。跳到 Z 時,即使它從 A 處分叉,也可能只保留系統提示和最初的工具定義。具體快取管理行為在很大程度上取決於提供商。
反過來的情況也可能發生。/fork 或新工作階段可以生成新的工作階段 ID,卻帶上大量完全相同的上下文。如果路由系統按工作階段 key 隔離快取,它可能無法發現這種有用的重疊。
真正決定哪些工作可以快取的是可複用字首。工作階段身份只是在幫助基礎設施找到可能相關的內容。在某些系統中,路由 key 對快取管理至關重要;在另一些系統中,它只是一項最佳化。
顯式字首快取與自動字首快取
提供商 API 主要以兩種方式開放快取。
Anthropic 的傳統介面使用顯式的 cache_control 節點。客戶端在系統提示、工具定義或最新可快取對話內容等穩定部分之後標記邊界。伺服器隨後可以寫入或查詢截止到這些節點的字首。邊界是顯式的,但複用仍然要求它之前的內容完全一致。不僅快取節點是顯式的,價格也是顯式的:快取寫入需要付費,你還可以選擇不同價格的保留時長。
其他 API 使用自動字首快取。客戶端按平常方式傳送請求,提供商會自行尋找可複用字首,不需要客戶端設定斷點。提示快取 key 或工作階段標頭可能改善路由或分組,但它無法讓不同的字首變成相同。
為什麼工具設定會摧毀快取
工具定義通常出現在對話之前,並在模型內部被“摺疊”進系統提示。它們的名稱、描述和 JSON Schema 與其他文字一樣,都是模型輸入。增加一個工具、移除一個工具、改變其 Schema,甚至只是換一種順序序列化工具,都可能讓第一次不匹配出現在提示開頭附近。
輪次 1:[系統][讀取][寫入][Shell][對話...........]
輪次 2:[系統][讀取][寫入][Shell][部署][對話...]
|
舊對話現在位於
不匹配位置之後在外掛程式系統和 MCP 風格的工具目錄中,這種意外很常見。只在工具變得相關時再載入它,聽起來很高效,因為一開始需要傳送的 Schema 更少。然而,對大多數模型來說,後來擴充工具設定會讓它之後的已快取對話全部失效。節省幾個工具 Schema token,可能導致數萬個對話 token 被重新處理。
一些較新的模型 API 支援增量工具載入。工具可以在轉錄記錄中的某個工具結果位置變為可用,而不是被插入最初的工具清單。這樣,原有字首就不會變化:
[系統][初始工具][對話][新工具][下一輪]
<---------- 已快取字首 ---------->現在,Pi 已經在具備原生延遲工具機制的模型上支援這種方式。當擴充功能(Extension)透過 setActiveTools() 做出純增量修改時,Pi 會把新加入的工具名稱記錄在工具結果中。對於支援此功能的 Anthropic 模型,它使用延遲定義和 tool_reference;對於支援此功能的 OpenAI 模型,它會發出相應的工具搜尋項。其他模型會採用安全的回退方案:Pi 在下一次請求中傳送完整的活動工具清單。功能上仍然正常,但可能清空提示快取。
“增量”二字十分重要,因為刪除工具、用一套工具替換另一套工具,或修改提示片段,仍然會改變早期輸入。如果擴充功能重建系統提示、打亂工具順序、注入時間戳,或每輪都改變活動工具,就可能意外破壞整個工作階段的快取。
Pi 的可擴充性意味著,它無法替每個擴充功能保證快取穩定。Pi 可以提供對快取友好的機制,但擴充功能仍需正確使用它們。根據我們的觀察,許多擴充功能對快取效率的重視都不夠。其中一部分原因是,使用固定訂閱時,快取未命中所帶來的成本並不明顯。
中斷與 TTL
一些重要的提示快取預設生命週期很短。Anthropic 預設的五分鐘快取尤其值得注意,因為它短於許多正常的程式設計活動。如果你使用 Fable 時去喝杯咖啡,十分鐘後回來,只傳送一句“say hi”,花費可能比預期高得多。
這是因為使用者可能認為一個程式設計工作階段始終連續活躍,而推理提供商看到的卻是一系列相互分離的請求:
模型請求 --> 執行測試 7 分鐘 --> 模型請求
此處沒有快取流量一次很長的構建、一套測試、午餐、會議,甚至只是停下來檢查 diff,都可能超過快取生命週期。下一次請求雖然包含相同提示,但已儲存的 KV 狀態已經消失,整個字首會再次按輸入計費。
由於目前 Anthropic 不允許 Pi 作為其訂閱服務的使用工具,我們採用 Anthropic 為 API 使用者推薦的五分鐘預設值。不過,從 Claude Code 的程式碼庫可以看出,Anthropic 為自己的訂閱使用者把快取時長延長到了一小時。但如果你按 API token 價格付費,延長快取的額外成本往往並不值得。
當然,你也可以主動啟用更長的保留時長。Anthropic 等提供商提供了更長的保留控制。使用受支援的直連 API 時,Pi 使用者可以設定 PI_CACHE_RETENTION=long 來提出這一請求。但它終究只是一項請求:Pi 無法強制閘道器保留快取項,無法阻止記憶體壓力下的淘汰,也無法在沒有模型請求時讓快取一直保持活躍。
一次快取未命中的代價
提供商通常會對未快取輸入、快取寫入和快取讀取採用不同價格。快取讀取往往有折扣,因為昂貴的預填充工作已經完成。快取寫入可能有溢價,因為提供商承諾為後續使用保留狀態。
仍以上面的 Fable 場景為例:設想一次程式設計工作階段已經積累了 10 萬 token 的歷史,之後只發出一條很短的新請求。快取正常時,幾乎全部歷史都會按更低的快取讀取價計費,只有少量新材料需要按正常輸入價處理,並可能寫入快取。
快取未命中時,提供商必須按正常輸入價重新處理完整的 10 萬 token 歷史,還可能對把它們重新寫入快取另行收費。因此,快取過期後,一句 continue 這樣的短請求也可能出奇昂貴。在長工作階段裡,重新讀取舊輸入的成本可能遠高於生成下一條回覆。
快取也可能造成不那麼直觀的激勵關係。
使用者希望命中率高,因為這樣能降低延遲和價格。擁有 GPU 的推理服務商也應該希望命中率高:更少的預填充計算意味著相同硬體能服務更多請求。設計合理的快取 token 折扣,可以讓雙方利益一致,同時讓服務商獲得更好的利潤率。
閘道器或轉售商的激勵可能不同。如果它從按未快取費率計費的輸入 token 中獲得收入,那麼快取未命中可能讓客戶帳單更高。是否也能帶來更多利潤,則取決於它的上游成本、合同以及由誰運營快取。在利益沒有對齊的技術棧中,負責路由的一方可能不必承擔快取未命中的全部成本,而向使用者收費的一方卻會在未命中時獲得更多收入。
這並不意味著提供商會故意破壞快取,但它說明快取表現應該是可觀察的。使用者不應只能從一張出乎意料的高額帳單中推測出了問題。瞭解快取是否出現異常,可能是一條重要線索。
嚴格遵循快取也意味著,閘道器在兩個輪次之間無法靈活地把請求路由到最佳選項。你可能願意犧牲一次快取命中,換用另一個從此以後更經濟的模型;也可能最好把負載均衡到另一家提供商。
為什麼 Pi 不會激進地剪枝
讀到這裡,你大概已經明白 Pi 為什麼不會剪除工具呼叫。人們很容易想到透過不斷刪除舊工具結果或重寫歷史來控制成本;有時這樣做確實必要,尤其是在接近上下文視窗上限時。但正如前面所說,剪枝本身也有快取成本。
從中間刪除內容,會在刪除位置改變字首。它之後仍保留的全部對話都可能需要重新處理。重寫一段很長的已快取上下文所帶來的一次性成本,可能超過刪除少量低價快取 token 所節省的未來成本。
粗略的盈虧平衡比較如下:
一次性重寫成本
≈ 編輯點之後保留的 token ×(未快取價格 - 快取讀取價格)
每輪未來節省
≈ 被剪除的 token × 快取讀取價格這還不只是一個計費問題。舊工具結果經常包含模型後來據以做出決定的證據。刪除它們可能讓模型表現變差,即使摘要保留了大意也一樣。
因此,Pi 偏好穩定、以追加為主的轉錄記錄,不會把每個舊 token 都視為浪費。當上下文壓力足以證明有必要進行有損重寫時,可以執行壓縮。由於壓縮是在有意建立新上下文,而不是意外地對未改變的提示重複計費,因此 Pi 在工作階段統計中把它視為一次快取重置,而不是快取失敗。
目標不是讓提示儘可能短,而是在模型上下文、快取複用、延遲和價格之間取得最佳平衡。
與此同時,剪枝有時也有價值。如果你使用的提供商不會因為良好快取表現而給出折扣,或由於某種原因無法獲得高命中率,那麼剪枝可能更合適。它也確實能讓路由器更容易在不同後端之間均衡負載,因為快取無法轉移。
Pi 能做什麼、不能做什麼
Pi 會努力讓穩定的輸入保持穩定。它會傳遞一致的工作階段 ID 和提供商特定的快取提示,在 API 需要時設定顯式快取節點,記錄快取讀取和寫入用量,並在模型允許時支援錨定在訊息位置的增量工具載入。它預設的轉錄行為也會避免無端重寫舊上下文。
請求離開本機以後,Pi 無法控制之後的每一層。它不能決定提供商的淘汰策略,不能把快取延長到 API 允許的期限之外,不能保證某塊 GPU 一直存活,也不能確保閘道器遵守親和性路由。它同樣無法為被擴充功能改變的字首保留快取。
Pi 能做的是讓快取健康狀況變得可見。
互動介面的底部狀態列會用 R 和 W 顯示累計快取讀取量和寫入量,並用 CH 顯示最近一次請求的快取命中率。/session 命令會顯示更完整的資訊:快取和未快取輸入總量、累計命中率、成本,以及由顯著快取未命中導致的重複計費 token 和金額估算。
訊息
總計:178
使用者:6
助手:58
工具:114 次呼叫,114 個結果
Token
輸入:7,129,883
已快取:6,776,832(95.0%)
未快取:353,051
輸出:30,013
總計:7,159,896
成本
總計:$6.054
快取重複計費:$0.728(161,744 token,2 次未命中)希望在快取未命中發生時收到提示的使用者,可以在 /settings 中啟用 Show cache miss notices(顯示快取未命中通知),對應 settings.json 中的 showCacheMissNotices。之後,Pi 會在發生顯著未命中時插入一條警告,其中包含重複計費 token 數量和成本估算。當它能夠觀察到模型切換,或空閒時間超過通常的短 TTL 時,也會說明原因。對於其他未命中,它只報告事實,不會假裝知道提供商內部發生了什麼。
快取表現變差的常見原因
當一個工作階段的快取命中率看起來不對時,常見原因包括:
- 空閒。 某個命令、程式碼審查或對話暫停超過了提供商的保留視窗。
- 切換模型或提供商。 KV 狀態與模型綁定,通常不能跨提供商移動。
- 分支導航。
/tree、回退、fork 和替代分支可能改變當前 token 序列,即使工作階段 ID 沒變。 - 壓縮或手動重寫歷史。 這些操作會有意替換部分提示,並建立新的字首。
- 工具與推理級別變化。 增加、移除、重新排序或編輯工具定義,都會改變請求較早的部分,除非模型支援錨定在訊息位置的載入,而且變化是純增量的。改變推理級別通常也有同樣影響。
- 動態系統提示。 時間戳、隨機值、不斷變化的專案上下文和擴充功能提供的提示片段,可能讓它們之後的一切失效。
- 擴充功能上下文轉換。 修改舊訊息或提供商請求載荷的擴充功能,可能讓表面穩定的 Pi 轉錄記錄在實際傳輸時發生變化。
- 提供商路由與淘汰。 即使提示完全一致,如果請求到達的位置已無法取得對應 KV 塊,快取仍會未命中。
譯者說明
本文為 Earendil Engineering 原文的完整中文譯文。中文譯文及適配部分經授權按 CC BY 4.0 釋出;英文原文版權歸 Earendil 所有。如中文表述與原文存在歧義,請以英文原文為準。