返回文章列表
2026年7月28日 16 分鐘閱讀

提示詞快取失效解析:系統提示詞多一行日期,本機模型就重新預填

拆解本機模型的預填與前綴快取:為什麼系統提示詞多一行日期,就讓 250k context 的工作階段重新預填 20 分鐘,以及提示詞該怎麼排版才能穩定命中快取,並對照 Anthropic 商業 API 的快取規則。

提示詞快取失效示意圖:一列深色骨牌被一塊紅色積木打斷,紅色之後的骨牌接連傾倒並散成紙張

提示詞快取失效,通常不是有人改了系統提示詞,而是那行日期每次都重新產生。這篇說明它為什麼會讓本機模型重跑幾十分鐘的預填,以及提示詞該怎麼排版。

最近使用 pi 讓模型搜尋網頁時,偶爾會把「2025」或「2024」當成目前年份——模型顯然不知道今天的日期。進一步研究才發現,系統提示詞要不要提供當天日期,不只關係到模型能否掌握時間,也會影響提示詞快取。

日期看起來只是一小段文字,為什麼會牽動本機模型的效能?關鍵在這段文字放在提示詞的哪個位置。

這個代價可以有多大?

pi 的 issue #6621 有一組實測數字。回報者在 AMD Strix Halo 上跑本機模型,token 生成速度約 15 t/s 還算堪用,但預填只有 100–200 t/s。在這種機器上,250k context 的快取失效一次,要等約 20 分鐘才會出現第一個 token;500k context 則要一到兩小時以上。他的結論是:實務上根本沒辦法隔天恢復同一個長工作階段。

本文環境:實測數據引用自 pi issue #6621(AMD Strix Halo,透過 ds4-server 執行本機模型),對應 pi v0.80.7 前後的版本;Anthropic 官方文件內容以 2026 年 7 月存取為準。

回答出現以前,模型要先讀完輸入

一次推論分成預填與解碼兩個階段,前綴快取影響的是前者。

  • 預填(prefill):處理系統提示詞、工具定義、對話紀錄、文件與這一輪的問題。
  • 解碼(decode):預填完成後,逐一產生回答的 token。

一次推論的兩個階段:預填處理整段輸入,解碼逐一產生 token

一般效能測試常把重點放在解碼速度,也就是模型每秒能產生多少 token。不過,使用者按下送出後,到第一個 token 出現以前,還有一段預填時間。

對短對話來說,這段時間通常不明顯。工作階段愈長,模型要處理的程式碼、工具輸出與對話紀錄愈多,預填就愈可能成為主要等待時間。這也是為什麼同一個模型明明回答得不慢,恢復一段長對話時卻可能停頓很久。

KV 快取與前綴快取是兩件相關但不同的事

「KV 快取」在討論本機推論時,其實指向兩個不同層次的機制。

第一個是單次產生回答時使用的 KV 快取。模型產生下一個 token 時,需要參考前面的 token。KV 快取會保存注意力層已算過的 key 與 value,避免每產生一個 token 就把先前內容全部重算一次。

第二個是請求之間的前綴重用。部分推論引擎會保留先前請求建立的 KV 快取;下一次請求若有相同的開頭,就能直接沿用共同前綴的計算結果。vLLM 將這項功能稱為 Automatic Prefix Caching,llama.cpp 的 llama-server 也有對應的提示詞快取選項。

這篇文章關心的是第二種情況:同一段系統提示詞與對話紀錄,能不能在下一輪繼續使用。要注意的是,前綴快取節省的是預填時間,不會讓模型產生新 token 的速度跟著變快。

系統提示詞為什麼特別敏感

因為它排在整段輸入的最前面,任何變動都會讓後方內容失去重用機會。

模型實際收到的內容,大致依「系統提示詞 → 工具定義 → 對話紀錄 → 這一輪的問題」的順序排列。前綴快取比對的就是這串 token:新的請求只要和舊請求共享相同開頭,推論引擎就有機會重用那一段;遇到第一個差異後,後面的內容通常必須重新處理。

值得先澄清一個常見的誤解:多輪對話會不斷把模型的回答與新問題加進輸入,但這種成長不會破壞前綴。因為它是往尾端追加,上一輪的完整輸入原封不動成為這一輪輸入的開頭,前面全部可以沿用,只需要預填新增的那一小段。判斷標準不是「有沒有新內容」,重點在於「新內容出現在哪個位置」。

假設系統提示詞每天都加入日期:

第一天:[固定規則][今天是 7 月 28 日][工具定義][長對話紀錄]
第二天:[固定規則][今天是 7 月 29 日][工具定義][相同的長對話紀錄]

雖然只改了一天,差異卻出現在輸入前段。原本相同的工具定義與長對話紀錄,都排在日期之後,可重用的共同前綴因此大幅縮短。

前綴比對在第一個差異點中止:日期放前段會讓後方大量內容失去重用機會;日期後移則前面幾乎都能沿用

實際上會重算多少內容,仍取決於推論引擎的快取單位(例如 vLLM 以固定大小的 block 為單位比對)、比對方式與當時保留的資料,未必每次都從第一個 token 開始。但可以確定的是:愈早出現的動態內容,愈容易縮短後方可重用的前綴。

日期只是其中一種例子。下列內容如果每次重新產生,又放在提示詞前段,也可能造成相同問題:

  • 目前時間
  • 工作目錄
  • 每次產生的識別碼
  • 會改變內容或順序的工具定義
  • 模型、推理模式或 chat template 的切換
  • 其他隨請求變動的環境資訊

人眼看來意思相同,不表示實際的 token 序列相同。空白、標點、欄位順序或模板格式改變,都可能影響前綴比對。工具定義尤其容易被忽略——當一個 agent 依情境動態增減可用工具時(這也是執行路徑設計常見的做法),整份快取都可能因此作廢。

使用者沒有動它,提示詞卻自己變了

真正麻煩的情況是提示詞每次送出請求時由樣板即時重建,而不是有人手動改了它。

pi 的 system-prompt.ts 就是在組提示詞的當下,把目前日期與目前工作目錄插進去。這代表恢復一個舊工作階段時,歷史訊息照著存檔還原,最前面那段卻是用「現在」的環境重新算出來的:

昨天存檔:[固定規則][2026-07-27][~/projA][工具定義][250k 對話紀錄]
今天恢復:[固定規則][2026-07-28][~/projA][工具定義][250k 對話紀錄]
                        ↑ 只有這裡不同,後面全部重算

恢復工作階段時,系統提示詞被即時重建、對話紀錄照存檔還原;開頭一變,後面未改動的大量內容仍須重新預填

觸發條件因此變得相當隱晦:跨過午夜再恢復同一個工作階段會中,換一個目錄開啟同一個專案也會中。使用者從頭到尾什麼都沒改。

同一則 issue 還提到一個連應用程式本身都控制不了的案例:ds4-server 實作「Think Max」思考模式的方式,是把一段指令前置到系統提示詞開頭;切換思考模式,等於整段共同前綴作廢。

這也解釋了為什麼「把動態內容移到後面」不是唯一解法。issue 中討論的另一個方向更根本:把系統提示詞連同訊息一起存進工作階段檔案,恢復時直接載入,而不是重新生成。 問題的本質不是「日期會變」,而是「這段內容每次都重新產生」;只要改成存檔還原,就算裡面帶著日期,也不會在恢復時憑空變動。

日期到底該不該放進系統提示詞

該讓模型知道日期,但不必把它固定放在每次請求的最前面。

如果模型需要搜尋近期資訊,知道現在的日期確實有幫助。若沒有提供目前時間,模型可能沿用訓練資料中的時間感,也可能在搜尋時使用過時的年份。

比較穩定的做法,是讓系統提示詞只保留長期不變的規則,例如:

當任務與目前日期有關時,先使用時間工具取得正確日期。

模型需要日期時再查詢,工具結果會成為這一輪新增的內容,不必每天改寫整段共同前綴。若應用程式沒有時間工具,也可以把易變資訊整理成獨立的「目前環境」區塊,盡量放在大量固定內容之後。

依變動頻率安排提示詞版面:長期規則與工具定義在前組成穩定前綴,動態環境資訊與本輪問題放在尾端

設計時可以依照以下原則取捨:

  1. 固定規則放前面。 身分設定、回答規範與不常變動的工具說明,適合組成穩定前綴。
  2. 動態資訊按需取得。 日期、時間與即時狀態優先透過工具查詢。
  3. 不能省略的動態資訊盡量後移。 讓前面的固定內容仍有機會命中快取,但要確認移動後不會改變模型對角色或指令優先順序的理解。
  4. 保持序列化結果穩定。 工具定義、JSON 欄位與模板順序不要在沒有必要時隨機變動。
  5. 恢復工作階段時不要重新生成提示詞。 如果應用程式會存檔續談,把當初使用的系統提示詞一併存下來,恢復時載入而非重建。
  6. 觀察第一個 token 的延遲。 除了回答的產生速度,也要查看提示詞處理時間、快取命中率或重用的 token 數量。

商業 API 是同一套規則

同樣的前綴比對邏輯也適用於商業 API,只是失效原因不同。

Anthropic 的 prompt caching 文件在疑難排解章節把同樣的問題列為常見錯誤,而它點名的例子正是時間戳記:文件提醒要確認快取斷點落在每次請求都完全相同的區塊上,因為寫入只發生在斷點,一旦那個區塊會變(時間戳記、每次請求才有的情境、這一輪的訊息),前綴雜湊就永遠對不上。結果是每次都付一次寫入費用、一次都讀不到——對按量計費的專案來說,這種失效會直接反映在帳單上(延伸閱讀:Gemini API 預付抵免額怎麼運作)。

幾個可以直接套用到本機環境的觀念:

  • 快取有明確的階層:tools → system → messages。 上層變動會連帶讓下層失效。修改工具定義會讓整份快取作廢;切換 thinking 設定或 effort 參數,至少會讓 messages 失效。
  • 比對是逐位元組的,所以序列化順序真的會出事。 疑難排解章節特別點名:Swift、Go 等語言在轉換 JSON 時可能隨機化 key 的順序,因而打破快取。
  • 中途追加系統指令是有官方做法的。 在 Claude Opus 4.8、Opus 5 等模型上,可以把 {"role": "system"} 訊息追加到 messages 尾端,而不是去修改頂層的 system 欄位——前面的內容維持完全相同,快取照樣命中,而新指令仍保有系統層級的優先權(Sonnet 5 不支援這個做法,只能用頂層 system)。
  • 有工具可以直接查是哪裡分岔的。 Cache diagnostics(beta)會讓 API 比對連續兩次請求,回報最早分岔的是哪一層(模型、系統提示詞、工具定義,還是對話紀錄),並附上一個「因此失去多少可快取 token」的估計值。目前只有 Claude API 提供,Bedrock 與 Google Cloud 不支援;本機環境沒有對應功能,通常只能自己比對送出的 token。

差別主要在失效的原因不同:商業 API 的快取有明確存活時間(預設 5 分鐘,可付費延長到 1 小時),本機引擎則多半取決於記憶體壓力與快取釋放策略。但「把穩定內容放前面、把易變內容放後面」這個結論兩邊一致。

快取命中仍然不是保證

即使系統提示詞維持固定,前綴快取仍可能因為以下情況無法沿用:

  • 推論伺服器重新啟動或模型重新載入
  • 快取因記憶體壓力被釋放
  • 請求被分配到沒有該快取的工作槽
  • 模型、量化版本、LoRA 或 chat template 改變
  • 對話被摘要、裁切、分支或重新排序
  • 恢復工作階段時,應用程式重新生成了系統提示詞
  • 推論引擎根本沒有啟用跨請求的前綴重用

因此,移除系統提示詞中的動態內容,不等於從此不會重新預填。它的價值是減少一種可預期、可以避免的快取失效原因。

常見問題

前綴快取和 KV 快取是同一件事嗎

不是,它們是兩個層次。KV 快取是模型在產生單次回答時,保存注意力層已算過的 key 與 value;前綴快取(或稱提示詞快取)則是把上一次請求建立的 KV 快取留下來,讓下一次請求的相同開頭可以直接沿用。本文討論的是後者。

多輪對話一直變長,會不會破壞前綴快取

不會。多輪對話是往尾端追加內容,上一輪的完整輸入原封不動成為這一輪的開頭,前面全部可以沿用,只需要預填新增的那一小段。真正會破壞快取的是「在前段插入或修改內容」,不是「總長度變長」。

系統提示詞裡到底該不該放今天的日期

建議不要固定放在最前面。改成在系統提示詞裡寫一條長期不變的規則,要求模型在任務與時間相關時呼叫時間工具;工具結果會成為這一輪新增的內容,不會改寫整段共同前綴。若應用程式沒有時間工具,就把易變資訊集中成獨立區塊,放在大量固定內容之後。

移除動態內容之後,就一定不會重新預填了嗎

不一定。推論伺服器重啟、模型重新載入、快取因記憶體壓力被釋放、請求被分到別的工作槽、量化版本或 chat template 改變,都可能讓快取無法沿用。移除動態內容的價值,是消除一種可預期、可以避免的失效原因,而不是保證每次都命中。

怎麼確認快取到底有沒有命中

看第一個 token 的延遲,而不是只看每秒生成速度。本機引擎多半會輸出提示詞處理時間與重用的 token 數量;商業 API 則可以直接讀回傳的快取讀取/寫入 token 欄位,Claude API 另有 cache diagnostics(beta)可以指出兩次請求最早在哪一層分岔。

總結

以前談系統提示詞,常把注意力放在模型會不會遵守指令。使用本機模型和長工作階段後,還要多問一個問題:這段提示詞是否能穩定成為下一輪的共同前綴?

日期、工作目錄或識別碼本身都不長,真正的成本來自它們所在的位置,以及它們是不是每次都重新產生——尤其當這行內容是由程式自動填入時,使用者甚至不會意識到自己觸發問題。想讓提示詞快取穩定命中,比較實用的取捨是:長期規則保持穩定,即時資訊需要時再取得。


參考來源

相關文章