LangChain 提示詞快取:真正能命中快取的設定方式
目錄
下面這段 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_control的SystemMessage;只要在系統區塊放一個標記,也會涵蓋透過bind_tools綁定的工具。 - 使用
langchain-anthropic1.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、Redis | cache_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_tokens。OpenAI 的自動快取文件另有說明,前綴至少須達 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-5 與 glm-5.2,搭配約 1,800 個 token 的英文系統前綴與小型樣本。連續呼叫之間間隔 1–2 秒,讓快取寫入有時間完成。每項實驗都使用全新隨機前綴,確保起始狀態為冷快取,因此各表格中的基準 token 數量略有差異(1,852 至 1,875)。函式庫欄位 mapping 與供應商快取行為可能隨版本改變;若要依賴這些數據,請先在自己的技術堆疊上重新測量。