🎁 新用戶 免費註冊,送 10 次呼叫,最高 $1,免綁卡。
LangChain 提示詞快取:真正能命中快取的設定方式

LangChain 提示詞快取:真正能命中快取的設定方式

目錄
  1. 先確認你要找的是哪一種「快取」
  2. 修正方式:使用內容區塊,而非字串
  3. Template 變數的位置決定命中率
  4. 工具定義也會被快取
  5. 多輪對話:把標記移到最後一則訊息
  6. 正確讀取計量欄位,並認清欄位名稱
  7. 隱式快取:順序錯誤不會報錯,更要嚴密監控
  8. 檢查清單
  9. 免責聲明
  10. 資料來源

下面這段 LangChain 系統提示詞看起來完全合理,卻不會快取任何內容:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", BIG_STABLE_SYSTEM_PROMPT),   # the syntax every tutorial uses
    ("human", "{question}"),
])

我們用完全相同、長度為 1,800 個 token 的系統提示詞,對 claude-sonnet-5 呼叫兩次,再讀取 usage 欄位。兩次呼叫的快取寫入都是 0,快取讀取也都是 0。不是只命中一部分,也不是快取遭到切割,而是完全沒有快取。原因在於 Anthropic 只會快取以 cache_control 標記的內容,而 ("system", ...) tuple 裡的純字串無處可放這個標記。LangChain 最方便的語法,正好會讓整筆折扣直接消失,而且不會出現任何錯誤。

TL;DR

  • LangChain 的 ("system", "string") tuple 無法帶入 cache_control,因此 Claude 不會快取任何內容:在 claude-sonnet-5 上實測,完全相同且長度為 1,800 個 token 的系統提示詞,快取寫入與讀取皆為 0。
  • 修正方式是使用內容區塊帶有 cache_controlSystemMessage;只要在系統區塊放一個標記,也會涵蓋透過 bind_tools 綁定的工具。
  • 使用 langchain-anthropic 1.4.8 時,即使真的發生快取寫入,input_token_details.cache_creation 仍會維持 0;實際數量位於 ephemeral_5m_input_tokens
  • 若 RAG 提示詞順序錯誤(把會變動的上下文放在固定規則前面),每次呼叫都要支付約 1.25 倍的快取寫入溢價,比完全不使用快取還貴。

系列文章:第 5 篇,共 5 篇 · 前文:第 1 篇:快取原理 · 第 2 篇:供應商比較與評估 · 第 3 篇:可直接使用的程式碼教學 · 第 4 篇:依使用情境選擇最佳 LLM

這是快取系列的第 5 篇。第 1 篇說明前綴快取的運作方式,第 3 篇提供原始 SDK 教學,而完整提示詞快取指南則整理了跨供應商的完整內容。本文聚焦於 LangChain 代為組裝提示詞時,需要做哪些調整。以下結果皆於 2026-07-04 透過 Synthorai 閘道實測,使用 langchain-core 1.4.8、langchain-anthropic 1.4.8 與 langchain-openai 1.3.3。

先確認你要找的是哪一種「快取」

這兩項互不相關的功能都叫快取,而且搜尋後進入的 LangChain 文件頁面,通常不是你真正要找的內容。

回應快取(LangChain 的 InMemoryCache提示詞快取(本系列)
儲存內容完整 completion,存放於應用程式中提示詞前綴的 KV state,存放於供應商端
何時能節省成本完全相同的請求再次出現不同請求共用相同前綴
設定位置set_llm_cache(InMemoryCache())、SQLite、Rediscache_control 標記或自動比對前綴
Agent 迴圈、RAG、聊天幾乎沒用(每個請求都不同)主要的成本槓桿,因為每輪都會重複 system 與 tools

這裡所說的「完全相同」就是字面上的完全一致:內建快取會以(序列化提示詞、模型設定字串)這組資料作為 key。實測中,完全相同的重複請求會在 0 ms 內回傳,且不會發出 API 呼叫;提示詞多一個空格就不會命中;即使提示詞相同,只把 max_tokens 改動 1,也不會命中。(快取回放還會傳回原始呼叫的 usage 數值,因此直接累加 token 會造成重複計算。)Semantic cache 可透過第三方整合使用;內建快取只支援完全一致比對。

因此,set_llm_cache 適合用來消除測試中的重複呼叫。至於 Agent 每一輪都會重新傳送的 2,000 個 token 系統提示詞,則應交給提示詞快取處理,而前提是提示詞必須以正確方式組裝。

修正方式:使用內容區塊,而非字串

cache_control 必須放在內容區塊內,因此系統訊息要使用內容為區塊格式的 SystemMessage,不能只放純字串:

from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage
from langchain_core.prompts import ChatPromptTemplate

llm = ChatAnthropic(
    model="claude-sonnet-5",
    base_url="https://synthorai.io",   # any Anthropic-compatible endpoint
)

prompt = ChatPromptTemplate.from_messages([
    SystemMessage(content=[{
        "type": "text",
        "text": BIG_STABLE_SYSTEM_PROMPT,
        "cache_control": {"type": "ephemeral"},   # a bare string has nowhere to put this
    }]),
    ("human", "{question}"),
])
chain = prompt | llm

透過相同閘道,使用同一段長度為 1,800 個 token 的系統提示詞實測:

呼叫字串 tuple 語法內容區塊語法
第 1 次(冷快取)寫入 0/讀取 0寫入 1,875/讀取 0
第 2 次,不同問題寫入 0/讀取 0寫入 0/讀取 1,875

暖快取讀取的計價約為輸入價格的 10%,因此對 Claude 而言,只改變這一處結構,就能讓每次呼叫中的固定部分,從永遠支付全額變成享有 90% 折扣。成本計算請見第 1 篇;標記機制與 LangChain Anthropic 整合文件Anthropic 提示詞快取指南中的原始 SDK 用法一致。

Template 變數的位置決定命中率

LangChain template 可以輕鬆在任何位置插入變數,這也正是風險所在。快取 key 是逐 byte 完全一致的前綴。我們把日期放進已標記的快取區塊後進行實測:

SystemMessage(content=[{
    "type": "text",
    "text": f"Today is {today}. " + BIG_STABLE_SYSTEM_PROMPT,   # variable INSIDE the block
    "cache_control": {"type": "ephemeral"},
}])
呼叫結果
A 日,問題 1寫入 1,865(此值的冷快取)
A 日,問題 2讀取 1,865(值相同,命中)
B 日,問題 1寫入 1,865(新值,再次成為冷快取)

快取沒有失效,只是 key 包含了這個變數。像日期這類會重複的值,每個值只需支付一次快取寫入成本,之後即可命中。若每次呼叫的值都不同,例如時間戳記或 request ID,每次呼叫都會變成冷快取寫入,命中率恰好為零。

這類錯誤在實務上最昂貴的案例是 RAG。許多 chain 會採用的 template,把檢索出的上下文放在系統提示詞最前面,排在固定指令之前。我們使用每次查詢都會改變、長度為 800 個 token 的檢索上下文,以及已標記的系統區塊,分別實測兩種順序:

提示詞內的順序呼叫 1呼叫 2(新查詢、新上下文)
先放上下文,再放規則寫入 3,133再次寫入 3,133,讀取 0
先放規則(已標記),上下文放在人類訊息回合寫入 1,852讀取 1,852

錯誤順序不只是「沒有折扣」而已:每次呼叫都要針對完整的 3,133 個 token 支付約為一般輸入價格 1.25 倍的快取寫入溢價,而且永遠不會讀回任何內容。啟用快取但順序錯誤的 RAG 提示詞,比完全不使用快取還貴。 固定內容排在變動內容之後,等同於不存在。

實測結果可以整理成以下規則:

  • 固定文字放在最前面,並置於已標記的區塊內。 包括系統規則、工具定義與 few-shot 範例。
  • 任何會變動的內容都放在標記之後,最好放在人類訊息回合:檢索上下文、日期、使用者問題。
  • 只有在變數重複頻率足以攤平自身快取寫入成本時,才適合放進快取區塊。

工具定義也會被快取

Agent 每次呼叫都會重新傳送工具 schema,而在 Anthropic 的請求結構中,工具位於系統提示詞之前。標記代表「從請求開頭到此處的所有內容都要快取」,因此會產生兩個實務問題。系統區塊上的標記,是否也會涵蓋前方的工具?LangChain 的 bind_tools 是否能在每次呼叫時,把工具序列化成完全相同的 byte?只要序列化結果不穩定,前綴就會改變,每次呼叫都無法命中。

兩個問題的答案都經過實測。使用相同且已標記的系統提示詞時,不含工具的暖快取讀取量為 1,861 個 token;綁定兩項工具後,則為 2,389 個 token。多出的 528 個 token,就是從快取讀回的工具 schema。這個 2,389 也在連續三次呼叫中完全一致,代表 bind_tools 每次都使用相同方式序列化;框架沒有在前綴中混入雜訊。明確來說:只要系統區塊帶有標記,工具本身不需要 cache_control;位於工具之後的單一標記就能完成所有工作。

另一種配置適用於特定情況:工具是請求中最大且固定的內容,而系統提示詞很短或根本不存在。此時請求仍需要在某處放置標記,而標記可以放在工具上。這只適用於 Anthropic 原始格式的 dict,因為以 @tool 裝飾的函式沒有可攜帶標記的欄位;bind_tools 會原封不動地傳遞 dict:

# variant: NO marked system block anywhere; the tool carries the request's only marker
llm.bind_tools([{
    "name": "get_weather",
    "description": LONG_TOOL_DESCRIPTION,
    "input_schema": {...},
    "cache_control": {"type": "ephemeral"},   # passes through bind_tools verbatim
}])

實測結果:冷快取寫入 3,002,暖快取讀取 3,002,而且請求內沒有任何已標記的系統訊息。

多輪對話:把標記移到最後一則訊息

對話看似也是順序問題,實際情況正好相反:順序本來就是正確的,因為歷史紀錄只會持續附加,所以整份對話紀錄都是固定前綴。這裡的問題在於快取涵蓋範圍。系統區塊上的標記只會快取該區塊,不會涵蓋後續內容。隨著歷史紀錄增長,暖快取讀取量會固定停留在系統區塊的大小,而所有累積的對話回合仍以一般輸入價格計費。

修正模式與原始 SDK 相同:把標記放在最新一則訊息上,讓快取斷點持續向後移動,將目前為止的整段對話都納入快取前綴:

def marked(text):
    return HumanMessage(content=[{
        "type": "text", "text": text,
        "cache_control": {"type": "ephemeral"},
    }])

# each turn: history stays plain, only the newest human message carries the marker
llm.invoke([system, *history, marked(new_question)])

跨兩個回合的實測結果:第 1 回合寫入 1,864;第 2 回合讀取 1,864,只寫入 15 個 token 的增量(前一則回答加上新問題),舊有前綴則按約 10% 的讀取費率計價。Agent 迴圈需要的就是這種結構,而 LangChain 只需使用一般訊息 list 就能表示。Anthropic 每個請求最多允許四個標記,因此滑動標記可以和系統區塊或工具上的固定標記搭配使用。

正確讀取計量欄位,並認清欄位名稱

LangChain 將 usage 統一整理到 usage_metadata,但這裡有一個陷阱:使用 langchain-anthropic 1.4.8 時,在我們所有實測回應中,即使確實發生快取寫入,標準欄位 input_token_details.cache_creation 仍維持 0。實際寫入數量會放在非標準 key:

r = chain.invoke({"question": "..."})
det = r.usage_metadata["input_token_details"]
det["cache_read"]                  # correct on hits (1875 above)
det["cache_creation"]              # 0 even on a cold write; do not alert on this
det["ephemeral_5m_input_tokens"]   # the actual write count (1875)

供應商有正確回報寫入量(原始回應中的 cache_creation_input_tokens: 1875,可透過 r.response_metadata["usage"] 查看);只是標準化 mapping 把它放進了 TTL bucket key。若成本儀表板監控的是 cache_creation,它會顯示快取完全免費,但寫入溢價其實仍在默默累積。應以原始 usage 物件為準,或至少掌握這些 bucket key。這和閘道錯誤回報快取欄位屬於同一類問題;我們在你的 LLM 閘道是否謊報快取資料?中有專文檢查。

隱式快取:順序錯誤不會報錯,更要嚴密監控

Claude 使用顯式快取。GPT 與多數開放權重供應商會自動比對前綴並執行快取,不需要標記。透過 LangChain,只要更換 constructor,相同 chain 就能運作:

llm = ChatOpenAI(model="glm-5.2", base_url="https://synthorai.io/v1")

使用純字串系統提示詞且不加標記時,GLM 5.2 的第二次呼叫,從約 1,850 個 token 的前綴中讀取了 1,088 個 token。(不是全部:自動快取會以較粗略的區塊增量進行比對,不會逐 byte 比對到尾端;例如 OpenAI 文件說明其粒度為 128 個 token。)到這裡看來就是直接省下成本。但前述 RAG 表格中的順序風險,在此同樣完全適用,而且失敗模式更難察覺。我們在自動快取路徑上重新進行相同順序實驗,每次呼叫都使用新的檢索上下文:

順序(無標記,自動快取)呼叫 1呼叫 2(新查詢、新上下文)
先放上下文,再放規則讀取 0讀取 0
先放規則,上下文放在人類訊息回合讀取 0讀取 1,088

順序錯誤必然會得到零:變動的上下文位於最前面,任何兩次呼叫都不會共用前綴,因此永遠拿不到折扣。在顯式快取路徑上,相同錯誤至少會在帳單中留下跡象,因為每次呼叫都會產生快取寫入溢價;隱式快取路徑則沒有溢價、沒有錯誤,也沒有任何訊號。提示詞只是默默不符合快取條件,而你可能以為「自動」就等於「正常運作」。由於沒有標記可調整,提示詞順序是隱式快取路徑唯一能控制的設定。

因此,要根據計量欄位驗證,而且應持續在正式環境驗證,不能只在測試中確認一次:LangChain 查看 input_token_details.cache_read,原始回應則查看 prompt_tokens_details.cached_tokensOpenAI 的自動快取文件另有說明,前綴至少須達 1,024 個 token;各供應商的 TTL 與適用條件也不同,這部分請參考第 2 篇

檢查清單

  • 在 Claude 上,("system", "...") 字串 tuple 無處可放 cache_control:不會快取任何內容,也不會發出警告。可快取的系統提示詞應使用含有內容區塊與標記的 SystemMessage
  • 快取 key 是逐 byte 完全一致的前綴:固定內容放前面,變數放在標記之後或人類訊息回合。把 RAG 上下文放在規則之前,不只會無法命中,還會讓每次呼叫都支付寫入溢價。
  • 快取區塊內的變數會為每個值建立一筆快取項目:重複值可以攤平成本;每次呼叫都不同的值(時間戳記、request ID)則永遠不會命中。
  • 工具位於前綴中的系統提示詞之前,因此系統標記也會快取已綁定的工具(bind_tools 會採用確定性的序列化方式)。如果工具是最大且固定的區塊,也可以把標記放在 Anthropic 格式的工具 dict 上。
  • 對話中,若標記固定放在系統區塊,不斷增長的歷史紀錄仍會按全額計價;應把標記放在最新訊息,讓每一輪都讀取先前前綴,只寫入增量。
  • 不要監控 input_token_details.cache_creation:即使發生寫入,它仍維持 0,導致儀表板誤以為快取免費,但寫入溢價其實持續累積。實際數量位於 ephemeral_5m_input_tokens,也可以直接讀取原始 response_metadata["usage"]
  • 對自動快取模型(GPT、GLM、DeepSeek)而言,提示詞順序是唯一能調整的設定,而且順序錯誤會默默失敗:沒有溢價、沒有錯誤,只是永遠拿不到折扣。務必透過 usage 欄位驗證是否命中。
  • set_llm_cache 會以完全一致的提示詞與模型設定作為 key,儲存整份回應;只有在相同請求重複出現時才有效,Agent 迴圈不會受益。

需要養成的習慣都很簡單:用內容區塊取代字串、固定內容排在變動內容之前、讓標記隨對話向後移動,以及正確讀取 usage 欄位。實測差異是:每個固定 token 都能獲得 90% 折扣,否則完全沒有折扣;在 RAG 順序錯誤的情況下,甚至還要多付費用。LangChain 不會妨礙提示詞快取,只是讓錯誤與正確的提示詞結構同樣容易寫出來。


免責聲明

本文於 2026-07-04 針對 https://synthorai.io/ 實測,使用 langchain-core 1.4.8、langchain-anthropic 1.4.8、langchain-openai 1.3.3、模型 claude-sonnet-5glm-5.2,搭配約 1,800 個 token 的英文系統前綴與小型樣本。連續呼叫之間間隔 1–2 秒,讓快取寫入有時間完成。每項實驗都使用全新隨機前綴,確保起始狀態為冷快取,因此各表格中的基準 token 數量略有差異(1,852 至 1,875)。函式庫欄位 mapping 與供應商快取行為可能隨版本改變;若要依賴這些數據,請先在自己的技術堆疊上重新測量。

資料來源

← 返回部落格