,緩存為什么全部失效)
線上客服 Agent 每天處理十萬次對話一直好好的。某天工程師加了一行代碼讓 Agent「知道」當(dāng)前時間——在系統(tǒng)提示詞里插一句Current time: {{now}}。第二天告警所有對話的首 token 延遲從 0.5 秒漲到 3 到 5 秒月度推理賬單幾乎翻倍。模型沒換邏輯沒改只多了一行。這不是玄學(xué)。這一行代碼破壞的是整個 Agent 體系里最貴也最脆弱的東西——上下文前綴的緩存復(fù)用。問題的本質(zhì)是大模型 API 是無狀態(tài)的Agent 框架每一輪都要重新拼裝一遍完整的上下文送過去。而推理引擎為了省算力會緩存重復(fù)前綴的計算結(jié)果。前綴一旦改一個字節(jié)緩存立刻全廢那部分 token 要從頭算。下面從真實的消融實驗源碼出發(fā)把「哪些寫法會毀掉緩存」這件事講清楚。看完你會知道那些看起來無傷大雅的寫法代價具體有多大。一、上下文的結(jié)構(gòu)靜態(tài)前綴加軌跡先把結(jié)構(gòu)釘死后面所有結(jié)論都從這里推出來。Agent 每次調(diào)用模型送出去的是一個消息列表加一個工具定義數(shù)組。消息列表按角色分四種角色裝什么多輪中的變化system系統(tǒng)提示詞——身份、規(guī)則、約束整個對話通常只有一條放最前user用戶輸入每輪追加assistant模型之前的回復(fù)含文本與工具調(diào)用請求每輪追加tool工具執(zhí)行結(jié)果靠tool_call_id與調(diào)用請求配對每輪追加工具定義不在這四種角色里它是請求頂層的獨立字段tools。于是整個上下文可以切成兩段這個切法不是人為分類它直接對應(yīng) API 請求的物理布局靜態(tài)前綴永遠(yuǎn)在最前面軌跡跟在后面。「前面不能動后面可以壓縮」——這一章后面的所有技術(shù)包括 KV Cache 優(yōu)化、狀態(tài)欄、壓縮策略全部建立在這一條上。二、KV Cache 和 Prompt Cache 是兩個層級這兩個詞經(jīng)常被混用但它們的層級完全不同。模型每生成一個 token都要回頭用前文所有 token 的中間計算結(jié)果Key 和 Value 向量。如果每輪從頭算一遍開銷隨上下文長度平方級增長。KV Cache的做法是把這些中間結(jié)果存下來下一輪只算新增的部分。而Prompt Cache是推理引擎在多次 API 請求之間的優(yōu)化——把相同前綴的計算結(jié)果緩存起來跨請求復(fù)用。KV CachePrompt Cache歸屬模型內(nèi)部機(jī)制推理引擎優(yōu)化作用范圍單次請求內(nèi)的 token 生成跨多次 API 請求優(yōu)化對象避免重復(fù)計算前文避免重復(fù)計算相同前綴共同前提前綴不變前綴不變計費不涉及緩存讀取約為首次計算的十分之一兩層的優(yōu)化原理是同一個吃前綴的不變性。所以真正的約束只有一句話若 token 序列從某個位置開始不同首個不同 token 及其后的 KV 狀態(tài)需要重新計算此前位置的 KV 狀態(tài)不受這次改動影響?!覆皇苡绊憽故顷P(guān)鍵。它意味著緩存失效不是全廢而是從改動點往后廢。這直接解釋了后面壓縮策略為什么可以只壓軌跡、不動前綴。三、源碼級拆解五種毀掉緩存的寫法書里配了一個消融實驗用同一個任務(wù)跑六種上下文構(gòu)造策略。我把它的核心代碼貼出來——這段代碼本質(zhì)上是一份「真實世界壞做法清單」每一種都對應(yīng)線上系統(tǒng)里常見的寫法。寫法一往系統(tǒng)提示詞里塞時間戳注意時間格式里的%f——精確到微秒。這意味著什么同一個會話里連續(xù)兩次調(diào)用時間戳必然不同。而時間戳加在系統(tǒng)提示詞的末尾也就是靜態(tài)前綴的最后一個字節(jié)上。結(jié)果是整個靜態(tài)前綴從時間戳那個位置起全部失效每一輪都要重新計算全部前綴的 KV。這就是開頭那個「首 token 延遲從 0.5 秒漲到 3 到 5 秒」的故事一模一樣。為什么這個寫法特別常見因為需求聽起來完全合理——「Agent 應(yīng)該知道現(xiàn)在幾點」。問題不在需求在于實現(xiàn)位置。修復(fù)方式和錯誤寫法對照如下差別不在「有沒有把時間送到模型面前」——兩者都送到了。差別在于變化發(fā)生的位置落在末尾前面的緩存全部復(fù)用落在開頭后面的全部重算。寫法二打亂工具定義順序random.shuffle把工具定義數(shù)組的順序隨機(jī)打亂。這里有個反直覺的點工具集完全沒變功能一模一樣只是順序變了。但緩存照樣全廢。原因是tools是靜態(tài)前綴的一部分它參與 token 序列的構(gòu)成。順序一變token 序列在tools字段的開頭就分叉了——后面哪怕內(nèi)容完全相同位置全變了KV 狀態(tài)必須重算。實測數(shù)據(jù)印證了這一點打亂工具順序后緩存命中率從 10.1% 掉到 3.4%倉庫預(yù)置的六策略實測。這條對生產(chǎn)系統(tǒng)的意義任何「動態(tài)增刪工具」的設(shè)計都要小心。如果每輪根據(jù)用戶意圖挑選不同的工具子集哪怕每個工具的定義都正確前綴也已經(jīng)不穩(wěn)定了。書里的建議是——工具增刪用專門的消息塊聲明而不是去改tools數(shù)組。寫法三在系統(tǒng)提示詞后面插一條變化的用戶消息這條消息插在 system 之后、軌跡之前內(nèi)容是「剩余額度 N 點」——而 N 每輪減一。從緩存視角看它和寫法一等價在靜態(tài)前綴的緊后位置放了一個必然變化的值。前綴在這里分叉后面全部重算。這個寫法的迷惑性在于它看起來是「用戶自己的信息」放進(jìn) messages 很自然。但只要它在靜態(tài)前綴和軌跡之間它就是前綴的一部分就會破壞穩(wěn)定性。寫法四滑動窗口滑動窗口保留最近 6 條消息丟棄更早的。它破壞緩存的方式和前三種不同不是前綴內(nèi)容變了而是整個軌跡相對位置前移了。原來的第 7 條現(xiàn)在變成了第 1 條token 序列的上游全變。但這段代碼里藏著一個更值錢的細(xì)節(jié)值得單獨說——為什么要有那個while循環(huán)上下文里的工具結(jié)果消息role: tool必須和發(fā)起這次調(diào)用的 assistant 消息成對出現(xiàn)靠tool_call_id關(guān)聯(lián)。如果直接切片[-6:]很可能切在一條工具結(jié)果上把它配對的 assistant 消息切掉了。這時候送出去的歷史里有一條「無主的工具結(jié)果」provider 會直接拒絕整個請求。所以代碼要把窗口起點往前回退直到退到那條 assistant 消息為止。這個細(xì)節(jié)說明一件事上下文不是一堆可以隨便增刪的文本它是有結(jié)構(gòu)約束的數(shù)據(jù)。任何裁剪操作首先要保證結(jié)構(gòu)完整其次才談性能。實測顯示滑動窗口策略下 Agent 會反復(fù)執(zhí)行相同的工具調(diào)用——因為它丟掉了「我已經(jīng)做過什么」的記錄模型只能從頭再來一遍。這就是長短兩種訴求的正面沖突保留歷史能保住記憶但會膨脹裁剪歷史能控長度但會失憶且毀緩存。寫法五把結(jié)構(gòu)化歷史重排成純文本它把全部歷史壓成一段純文本塞進(jìn)一條 user 消息。這個寫法浪費的地方在于它丟掉了 API 的結(jié)構(gòu)化協(xié)議。原本tool_call_id這類字段是機(jī)器精確解析的重排成ASSISTANT: [Making tool calls]這種自然語言字符串之后模型只能靠語義去猜。實測代價TTFT 從 2.328 秒漲到 6.189 秒總耗時從 18 秒漲到 43 秒比穩(wěn)定前綴慢一倍多。為什么每次重新格式化都會毀緩存因為生成這段文本的循環(huán)遍歷歷史而歷史每輪都在增長——于是整段文本的長度和內(nèi)容每輪都變前綴無時無刻不在分叉。五種寫法的共性寫法破壞方式本質(zhì)系統(tǒng)提示詞加時間戳前綴末尾內(nèi)容變化可變值放進(jìn)了靜態(tài)前綴打亂工具順序前綴中段順序變化內(nèi)容不變但位置變了插入變化的用戶畫像前綴緊后內(nèi)容變化偽裝成「用戶信息」的前綴滑動窗口軌跡整體位移裁剪改變了上游 token 位置重排成純文本前綴結(jié)構(gòu)變化丟棄協(xié)議結(jié)構(gòu)且每輪重算五種寫法同一個根因改變了 token 序列的某個位置導(dǎo)致該位置之后的 KV 狀態(tài)全部作廢。四、實測六種策略的緩存命中率差多少上面是原理下面是數(shù)據(jù)。倉庫預(yù)置了六種策略在同一任務(wù)下的完整實測結(jié)果用--report可以離線復(fù)現(xiàn)不消耗任何 token策略迭代數(shù)首輪 TTFT平均 TTFTPrompt TokensCached緩存占比成本節(jié)省穩(wěn)定前綴正確做法32.328s6.054s7,56776810.1%9.1%動態(tài)系統(tǒng)提示32.497s8.085s7,63976810.1%9.0%打亂工具順序37.818s11.122s7,5682563.4%3.0%動態(tài)用戶畫像32.206s5.986s7,65276810.0%9.0%滑動窗口52.234s3.649s2,2241,51067.9%61.1%重排純文本36.189s14.432s7,4306749.1%8.2%這張表里有兩個陷阱不說清楚就會讀反。陷阱一滑動窗口的 67.9% 不是優(yōu)點。它緩存占比最高、成本最省。但它的 Prompt Tokens 只有 2,224是其他策略的三分之一。原因不是緩存效率高而是它把絕大部分歷史丟了——分子不變、分母驟減比例自然好看。這是用信息換緩存代價是 Agent 失憶它會反復(fù)執(zhí)行同一個工具調(diào)用因為它不記得自己已經(jīng)做過了。陷阱二穩(wěn)定前綴和動態(tài)系統(tǒng)提示緩存占比都是 10.1%。按前面的原理加微秒時間戳應(yīng)該讓緩存歸零。為什么這里沒差別因為這次運行只迭代了 3 輪。第 1 輪沒有前綴可緩存收益從第 2 輪才開始累積。3 輪太短破壞前綴的代價還來不及顯現(xiàn)。把任務(wù)拉長差距立刻出來。長任務(wù)下的真實差距同一個任務(wù)跑到 30 輪只看穩(wěn)定前綴這一種策略的運行結(jié)果30 輪里28 輪命中緩存緩存 token 累計300 萬。而且每一輪的 cached token 是遞增的這是「只追加」的復(fù)利效應(yīng)每輪只加新內(nèi)容前面算過的全部復(fù)用。對話越長單位成本越低。反過來把系統(tǒng)提示詞換成帶微秒時間戳的版本cached從第 2 輪起就一直是 0——增量歸零復(fù)利中斷。所以結(jié)論必須分場景說場景前綴穩(wěn)定的價值短任務(wù)3 輪以內(nèi)收益有限破壞前綴也看不太出來長任務(wù)10 輪以上收益隨輪數(shù)復(fù)利增長破壞前綴等于每輪從零開始如果你的 Agent 是短交互——一問一答、單次工具調(diào)用——前綴穩(wěn)定性的優(yōu)先級不高如果是長循環(huán)比如 ReAct 的多輪工具調(diào)用它就是賬單里的主要變量。怎么確認(rèn)緩存真的命中了不要靠感覺判斷。每次調(diào)用的響應(yīng)里都帶著 usage 明細(xì)直接讀。兩個字段值得單獨留意usage.prompt_tokens_details.cached_tokens是緩存命中的直接證據(jù)。它告訴你這一輪的多少 prompt token 直接復(fù)用了緩存。穩(wěn)定前綴下這個數(shù)字應(yīng)該逐輪遞增如果它一直是 0說明靜態(tài)前綴被什么東西破壞了——先去查是不是有人在 system 里塞了動態(tài)值。finish_reason決定 Agent 主循環(huán)走不走下一輪。取值是tool_calls時繼續(xù)循環(huán)是stop時結(jié)束。還有一個容易踩的取值是length——達(dá)到 max_tokens 上限被截斷。這種情況下content可能是空的推理模型的思考吃光了預(yù)算如果只看「有沒有內(nèi)容」來判斷成功空答案會被當(dāng)成正常結(jié)束。生產(chǎn)代碼里必須顯式處理這個分支。最后一個是tool_call_id每條role: tool的消息都要通過它和發(fā)起調(diào)用的那條 assistant 消息配對。這就是前面滑動窗口那段代碼為什么要寫while循環(huán)回退起點——裁剪可以讓緩存失效但不能讓協(xié)議結(jié)構(gòu)斷裂。五、緩存不只是優(yōu)化它是架構(gòu)約束前面講的都是「前綴變了會變慢變貴」。但緩存的影響遠(yuǎn)不止性能——它會反過來決定系統(tǒng)架構(gòu)。書里引了 Claude Code 的實踐有四個具體的設(shè)計決策第一提示詞的結(jié)構(gòu)由緩存邊界決定。系統(tǒng)提示詞在物理上被切成兩段邊界前的內(nèi)容跨用戶、跨會話全局緩存邊界后放用戶和會話的特定信息。于是提示詞的排列順序首先由緩存經(jīng)濟(jì)性決定其次才是語義邏輯。代價可以量化每個運行時條件如果放在邊界前緩存鍵的變體數(shù)就翻倍。3 個二值條件macOS 還是 Linux、普通還是調(diào)試模式、中文還是英文會產(chǎn)生 2 × 2 × 2 8 種緩存鍵。第二子 Agent 必須與父 Agent 字節(jié)級對齊。主 Agent 派生一個子 Agent 時如果子 Agent 繼承父 Agent 的上下文那么子 Agent 的提示詞、工具定義、模型配置、消息前綴、思考配置必須與父 Agent逐字節(jié)匹配才能命中服務(wù)商的 Prompt Cache。少一個空格都不行。第三工具結(jié)果的替換字符串在首次出現(xiàn)時就被凍結(jié)。當(dāng)大段工具輸出被替換成摘要預(yù)覽這個替換字符串要持久化。哪怕會話重啟也必須用完全相同的字符串——為了保證恢復(fù)后的消息序列與緩存里的字節(jié)流一致。第四改前綴可能丟掉的不只是錢還有推理。這一條最狠。Anthropic 從 Claude Fable 5.1 和 Opus 5.5 開始引入「保留思考」機(jī)制服務(wù)端用簽名把每個 thinking block 綁定到產(chǎn)生它的前綴上頂層 system、tools以及它之前的全部消息。之后任何一次請求里只要這段前綴有任何改動這個 thinking block 和它后面的所有 thinking 全部失效。新注冊賬號默認(rèn)直接收到 400 錯誤。失效的寫法都是熟面孔每輪重建系統(tǒng)提示詞寫入當(dāng)前時間或模式開關(guān)、每輪重新渲染首條用戶消息里的環(huán)境信息、中途增刪tools、原地截短舊的工具結(jié)果、往舊消息里插入提醒又刪掉。而且 thinking block 自身也不能隨意裁剪可以從頭刪、從尾刪或全刪但不能刪掉中間一個而保留后面的刪掉的也不能再放回去。這一條把結(jié)論推到了新的強(qiáng)度當(dāng)推理鏈也綁定在前綴上時「只追加、不改寫」就從性能建議變成了正確性要求。六、這對壓縮策略意味著什么既然前綴不能動那上下文膨脹了怎么辦壓縮不就是要改中間內(nèi)容嗎這里要先回答一個更根本的問題為什么要壓縮。大多數(shù)人只想到一個理由——窗口有限、token 貴。但書里區(qū)分了三個截然不同的動機(jī)而且第二個才是真正的重點動機(jī)一長度與成本。最直觀。窗口 128K工具結(jié)果動輒數(shù)萬字符幾輪就撐滿。動機(jī)二提升思考質(zhì)量。這一條層次更深。書里用一個寵物店的例子講透了它的機(jī)制上下文里有 100 個籠子的記錄90 只黑貓、10 只白貓問「黑貓白貓各幾只」不開思維鏈的模型答不對。為什么因為注意力擅長「查找」不擅長「統(tǒng)計歸納」?!富\子 37 里是什么貓」是查找注意力一查就中「總共有多少只黑貓」需要遍歷全部記錄并維護(hù)一個計數(shù)狀態(tài)本質(zhì)上是思考而不是檢索。所以壓縮的價值可以精確表述為把「需要思考才能得到的結(jié)論」變成「可以直接檢索的知識」。動機(jī)三緩解上下文焦慮。模型以為窗口快滿了會在任務(wù)沒做完時提前收尾。提前壓縮能提升決策質(zhì)量。順著動機(jī)二還能推出一個更隱蔽的問題——上下文腐化上下文溢出裝不下了上下文腐化裝得下但找不到了后者更危險因為 Agent 表面上還在正常工作只是決策質(zhì)量悄悄下滑。注意力權(quán)重被分散到更多 token 上每個 token 分到的權(quán)重變小無關(guān)內(nèi)容一旦占了大頭有用的部分就越來越難被注意到?;氐綁嚎s與緩存的矛盾。解法在時機(jī)和位置三條原則System Prompt 和 Tool Definitions 永遠(yuǎn)不動——靜態(tài)前綴持續(xù)緩存壓縮對象是軌跡里的工具結(jié)果——替換位置之后的緩存失效之前的仍然有效接近閾值時批量壓縮不要每輪都壓——頻繁壓縮等于頻繁破壞緩存preserve里那四樣?xùn)|西值得單獨說決策已經(jīng)定下來的事、約束不能違反的邊界、失敗走過的彎路、來源結(jié)論的出處。這四類是「決定下一步行動所必需」的最小集合其余細(xì)節(jié)可以丟。還有一個更省事的思路隔離優(yōu)于壓縮。把數(shù)萬字符的工具原文交給子 Agent 消化主 Agent 只收到一條結(jié)論消息。主上下文全程只增不改前綴一直穩(wěn)定——根本不產(chǎn)生壓縮需求。七、決策總表概念關(guān)鍵結(jié)論上下文結(jié)構(gòu)靜態(tài)前綴system tools加軌跡前面不能動、后面可壓縮KV Cache 與 Prompt Cache前者管單次請求內(nèi)后者管跨請求共同前提都是前綴不變緩存失效范圍從首個不同 token 起往后失效之前的仍然有效時間戳寫進(jìn) system微秒級精度導(dǎo)致每輪必變緩存命中 81% 掉到 0%打亂工具順序內(nèi)容不變但位置變命中率 10.1% 掉到 3.4%滑動窗口軌跡整體位移且裁剪必須保證工具消息與調(diào)用請求成對重排純文本丟棄協(xié)議結(jié)構(gòu)且每輪重算TTFT 從 2.3 秒漲到 6.2 秒壓縮的三個動機(jī)長度成本、提升思考質(zhì)量、緩解上下文焦慮壓縮的核心機(jī)制上下文學(xué)習(xí)是檢索而非推理把結(jié)論算好放進(jìn)上下文壓縮與緩存共存不動前綴只壓軌跡接近閾值時批量壓架構(gòu)級影響提示詞順序由緩存邊界決定子 Agent 需字節(jié)級對齊改前綴可能丟掉 thinking八、實戰(zhàn)題以下三種寫法都會破壞 Prompt Cache請判斷每一種的失效范圍是全部失效、還是從某個位置起失效并給出修復(fù)方案。寫法 A系統(tǒng)提示詞末尾追加當(dāng)天的日期精確到天。寫法 B每輪根據(jù)用戶當(dāng)前意圖從 20 個工具里挑 5 個組成tools數(shù)組。寫法 C每輪把最近 3 條工具結(jié)果的原文截短到 500 字符以內(nèi)超出部分用省略號替代。參考答案思路寫法 A看起來比微秒級時間戳溫和但它仍然在系統(tǒng)提示詞末尾放了一個每天變化的值。同一天內(nèi)的會話不受影響跨天時整個前綴失效一次。修復(fù)把日期作為追加消息放進(jìn)軌跡尾部或者接受每天一次的全量重算但絕不要放在會被高頻復(fù)用的位置。寫法 B是最貴的一種。它每輪都改tools數(shù)組而tools在靜態(tài)前綴里導(dǎo)致 system 之后的全部內(nèi)容每輪重算。修復(fù)保持tools數(shù)組恒定把「本輪打算用哪幾個」以消息形式聲明在軌跡尾部讓模型自己篩選。寫法 C的失效范圍是從被截短的那條工具結(jié)果起往后。注意這里有個坑截短操作發(fā)生在軌跡的中間位置所以它上面的前綴system、tools、早期歷史緩存仍然有效——但每次截短都會讓這條消息之后的緩存作廢。如果每輪都截不同位置的歷史緩存會持續(xù)處在碎片化狀態(tài)。修復(fù)要么只在接近預(yù)算時把整段舊歷史一次性替換成摘要要么干脆不截短改用子 Agent 隔離長內(nèi)容。九、附五個實驗的復(fù)現(xiàn)記錄理論講完記錄這一章五個實驗的實際運行結(jié)果。全部用自選的 OpenAI 兼容端點跑通除了給一個中轉(zhuǎn)網(wǎng)關(guān)加上字段過濾下面會說沒有改動倉庫的任何邏輯。實驗主題運行結(jié)果2-3KV Cache 與上下文布局30 輪迭代 / 60 次工具調(diào)用 / 緩存命中 28 輪2-5提示注入攻防3 攻擊 × 4 防御矩陣成功率全部 0%2-6官方 PPT Skill生成 9 頁 .pptx用 python-pptx 重新打開校驗通過2-9Agent 狀態(tài)欄5 種狀態(tài)欄技術(shù)的對照輸出2-10上下文壓縮六種壓縮策略的 Token 與壓縮率對比2-5 的 0% 不能讀成「防御有效」三類注入攻擊直接、間接、記憶在四種防御配置下成功率全是 0%包括 D1 無防御。這和倉庫的官方驗收記錄一致官方用另一個模型跑完整的 3×4×5 矩陣記錄的也是全部 0%。這說明在當(dāng)前這一代模型上這三類注入本身就很難奏效防御配置的差異根本拉不開。如果把它寫成「防御措施有效攻擊成功率降為 0」是錯的。沒有對照組的 0% 證明不了防御有效——它只說明攻擊沒成功。真正的對照組是「D1 無防御 vs D4 組合防御」兩組都是 0等于這張矩陣沒有信息量。2-9 的報錯是正常的運行時會看到UnicodeDecodeError那是實驗的工具函數(shù)讀到了非 UTF-8 編碼的文件。代碼自帶容錯隨后會打印成功提示繼續(xù)執(zhí)行主流程不受影響。2-10 的抓取失敗也是正常的運行時會看到網(wǎng)頁抓取超時或 403。該實驗原本依賴搜索 API未配置時會退化為直接抓取網(wǎng)頁受網(wǎng)絡(luò)環(huán)境影響。壓縮邏輯本身不受影響。一個繞不開的兼容問題換用中轉(zhuǎn)網(wǎng)關(guān)時踩到一個 400癥狀很有迷惑性第 1 輪成功第 2 輪才報錯。因為只有多輪對話才會把 assistant 的tool_calls消息回傳而報錯出在那個回傳的消息上。根因是 OpenAI SDK 的model_dump()輸出里帶著協(xié)議不存在的字段嚴(yán)格網(wǎng)關(guān)看到function_call這個 key 就整體拒絕請求哪怕它的值是null。做了唯一變量對照同一份消息去掉function_call就成功只加一個function_call: None立刻 400。修法是別直接轉(zhuǎn)發(fā)model_dump()的結(jié)果只保留協(xié)議定義的字段平時用 OpenAI 官方端點不會暴露這個問題——它容忍這些多余的 key。換成任何嚴(yán)格實現(xiàn)的兼容網(wǎng)關(guān)這類字段就會立刻變成硬錯誤。這是「OpenAI 兼容」這四個字里最容易踩空的地方兼容的是協(xié)議不是寬容度。