🎁 新規 無料登録、10回の呼び出しを進呈。最大 $1、カード不要。
LangChain のプロンプトキャッシュ:確実にキャッシュへヒットさせる設定

LangChain のプロンプトキャッシュ:確実にキャッシュへヒットさせる設定

目次
  1. まず、どちらの「キャッシュ」を探しているのか
  2. 修正方法:string ではなく content block を使う
  3. template variable の配置で hit rate が決まる
  4. tool definition もキャッシュされる
  5. multi-turn:marker を最新の message に移す
  6. usage を確認し、field 名を把握する
  7. 暗黙的なキャッシュ:順序を間違えても何も起きないため、特に厳しく監視する
  8. チェックリスト
  9. 免責事項
  10. 出典

一見すると何の問題もなさそうなのに、まったくキャッシュされない LangChain の system prompt があります。

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 の system prompt を使い、claude-sonnet-5 に対して 2 回実行して usage field を確認しました。どちらも cache write は 0、cache read も 0 です。一部だけヒットしたわけでも、キャッシュが分断されたわけでもありません。何もキャッシュされていません。Anthropic は cache_control を付けた部分しかキャッシュしませんが、("system", ...) tuple に plain string を渡す構文には marker を置く場所がないためです。LangChain で最も手軽な構文を使うと割引を一切受けられず、エラーも出ません。

TL;DR

  • LangChain の ("system", "string") tuple には cache_control を指定できないため、Claude は何もキャッシュしません。claude-sonnet-5 で同一の 1,800-token の system prompt を実測したところ、cache write も read も 0 でした。
  • SystemMessage の content block に cache_control を付ければ解決します。system block に marker を 1 つ置くだけで、bind_tools で bind した tool も対象になります。
  • langchain-anthropic 1.4.8 では、実際に write が発生しても input_token_details.cache_creation は 0 のままです。正しい値は ephemeral_5m_input_tokens に入ります。
  • 変動する context を固定 rule より前に置いた RAG prompt は、呼び出すたびに約 1.25x の cache-write premium が発生します。キャッシュしない場合より高くつきます。

シリーズ:全 5 回の第 5 回 · 前回までの記事:第 1 回 — キャッシュの基本原則 · 第 2 回 — provider の比較と評価 · 第 3 回 — 動作するコードによるチュートリアル · 第 4 回 — ユースケース別の最適な LLM

この記事はキャッシュシリーズの第 5 回です。第 1 回では prefix caching の仕組みを説明し、第 3 回では raw SDK を使った手順を紹介しています。provider を横断した全体像はプロンプトキャッシュ完全ガイドにまとめました。今回は、LangChain が prompt を組み立てる場合に何が変わるのかを扱います。以下の結果はすべて、2026-07-04 に Synthorai gateway 経由で実測したものです。使用した version は langchain-core 1.4.8、langchain-anthropic 1.4.8、langchain-openai 1.3.3 です。

まず、どちらの「キャッシュ」を探しているのか

無関係な 2 つの機能が同じ名前で呼ばれています。LangChain について検索して最初にたどり着く docs page は、たいてい目的と違う方です。

response caching(LangChain の InMemoryCacheプロンプトキャッシュ(本シリーズ)
保存対象completion 全体を application 側に保存prompt prefix の KV state を provider 側に保存
コストを削減できる条件まったく同じ request が繰り返される異なる request が同じ prefix を共有する
設定場所set_llm_cache(InMemoryCache())、SQLite、Rediscache_control marker または自動 prefix matching
agent loop、RAG、chatほぼ役に立たない(request が毎回異なるため)system と tool が毎 turn 繰り返されるため、主な最適化手段になる

「まったく同じ request」とは、本当に完全一致を指します。組み込み cache は、serialized prompt と model-config string の pair を key にします。実測では、同一 request の再実行は API call なしで 0 ms で返りました。prompt に space を 1 つ加えると miss し、同じ prompt でも max_tokens を 1 だけ変えると miss しました。cache から replay された response には元の call の usage 値もそのまま含まれるため、単純に token を集計すると二重計上されます。semantic cache は third-party integration として存在しますが、組み込み機能は exact match のみです。

したがって、set_llm_cache は test で同一 call の重複を排除する用途には向いています。一方、agent の各 turn で再送される 2,000-token の system prompt はプロンプトキャッシュで処理すべきです。そのためには prompt を正しい構造で組み立てる必要があります。

修正方法:string ではなく content block を使う

cache_control は content block の内部に入ります。そのため system message には bare string ではなく、block content を持つ 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

同じ gateway を使い、同じ 1,800-token の system prompt で実測した結果です。

callstring-tuple 構文content-block 構文
1 回目(cold)write 0 / read 0write 1,875 / read 0
2 回目、別の questionwrite 0 / read 0write 0 / read 1,875

warm read の課金は input price のおよそ 10% です。Claude では、この構造を 1 か所変えるだけで、固定部分に対して毎回 full price を払い続ける状態から、各 call の固定部分を 90% 割引で処理できる状態になります。コストの仕組みは第 1 回で説明しています。marker の動作は、LangChain の Anthropic integration docsAnthropic のプロンプトキャッシュガイドにある raw SDK の使い方と同じです。

template variable の配置で hit rate が決まる

LangChain template ではどこにでも簡単に variable を展開できますが、それが落とし穴になります。cache key は byte 単位で完全一致する prefix です。cached block の中に日付を入れて計測しました。

SystemMessage(content=[{
    "type": "text",
    "text": f"Today is {today}. " + BIG_STABLE_SYSTEM_PROMPT,   # variable INSIDE the block
    "cache_control": {"type": "ephemeral"},
}])
call結果
day A、question 1write 1,865(この値では cold)
day A、question 2read 1,865(同じ値なので hit)
day B、question 1write 1,865(新しい値なので再び cold)

cache が壊れたわけではありません。variable の値も含めて key が作られています。日付のように同じ値が繰り返される場合、値ごとに cache write が 1 回発生し、その後は hit します。timestamp や request ID のように call ごとに一意な値を入れると、毎回 cold write になり、hit rate は完全に 0 です。

実際の運用で高くつくのは、RAG でこのミスをした場合です。多くの chain で使われる template は、固定 instruction より前、system prompt の先頭に取得した context を配置します。query ごとに変わる 800-token の取得 context と、marker を付けた system block を使い、両方の順序を計測しました。

prompt 内の順序call 1call 2(新しい query、新しい context)
context、rule の順write 3,133再び write 3,133、read 0
rule を先に配置して marker を付け、context は human turn に配置write 1,852read 1,852

間違った方は、単に「割引がない」だけではありません。3,133 token 全体に対して、通常の input price の約 1.25× となる cache-write premium を毎回払い、read は一度も発生しません。順序を間違えた RAG prompt で caching を有効にすると、まったく cache しない場合より高くなります。 固定 content が変動 content より後ろにあるため、事実上存在しないのと同じです。

実測結果から導ける rule は次のとおりです。

  • static text は先頭に置き、marker を付けた block に入れます。 system rule、tool definition、few-shot example が該当します。
  • 変動するものはすべて marker より後ろ、できれば human turn に置きます。取得した context、日付、user question が該当します。
  • block 内に variable を置いてよいのは、その値が十分な頻度で繰り返され、cache write のコストを回収できる場合だけです。

tool definition もキャッシュされる

agent は call のたびに tool schema を再送します。Anthropic の request layout では、tool は system prompt よりに置かれます。marker は「request の先頭からこの位置までを cache する」という意味なので、実務上 2 つの疑問が生じます。system block の marker は、その前にある tool も対象にするのか。さらに、LangChain の bind_tools は call ごとに tool を完全に同じ byte 列へ変換するのか。serialization が揺れると prefix が変わり、毎回 miss するためです。

どちらも実測で確認しました。同じ marker 付き system prompt を使った場合、tool なしの warm cache read は 1,861 token、2 個の tool を bind すると 2,389 token でした。追加された 528 token は、cache から read された tool schema です。また、2,389 という値は 3 回連続で完全に一致しました。つまり、bind_tools の serialization は毎回同一であり、framework が prefix に余計な差分を持ち込むことはありません。結論として、system block に marker があれば、tool 自体に cache_control は不要です。tool より後ろにある 1 つの marker だけで対象になります。

特定の構成では逆の配置も使えます。送信するデータの中で tool が最大の固定部分であり、system prompt が短いか存在しない場合です。この場合も request 内のどこかに marker が必要なので、tool に置けます。@tool で decorate した function には marker を入れる field がないため、raw Anthropic-format dict を使う必要があります。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
}])

request 内に marker 付き system message がない状態で、cold 時に write 3,002、warm 時に read 3,002 となりました。

multi-turn:marker を最新の message に移す

conversation も順序の問題に見えますが、こちらは逆です。history は末尾に追加されるだけなので、順序はすでに理想的です。transcript 全体が固定 prefix になります。問題は cache の対象範囲です。marker を system block に置くと、cache されるのは system block までです。history が増えても warm read は system size のままで、蓄積した各 turn は通常の input として課金されます。

raw SDK と同じ方法で解決できます。marker を最新の message に置き、breakpoint を前へ進めます。これにより、その時点までの conversation 全体が cached prefix になります。

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)])

2 turn にわたる実測では、turn 1 で 1,864 を write し、turn 2 では 1,864 を read して 15-token の差分だけを write しました。この差分は直前の answer と新しい question です。それ以前の prefix には約 10% の read rate が適用されます。agent loop ではこの構成が適しています。LangChain では通常の message list で実現できます。Anthropic では request ごとに最大 4 個の marker を置けるため、sliding marker と、system block または tool に置いた固定 marker を併用できます。

usage を確認し、field 名を把握する

LangChain は usage を usage_metadata に標準化します。ここには注意点があります。langchain-anthropic 1.4.8 を使った今回の全実行で、cache write が実際に発生しても、標準 field の input_token_details.cache_creation常に 0 のままでした。実際の write 数は非標準の 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)

provider 側では write が正しく報告されていました。raw response の cache_creation_input_tokens: 1875 として、r.response_metadata["usage"] から確認できます。標準化された mapping が、値を TTL bucket の key に入れているだけです。cache_creation を監視する cost dashboard では、write premium が水面下で積み上がっていても caching のコストは 0 と表示されます。raw usage object を参照するか、bucket key を把握してください。gateway が cache field を誤って報告する問題と同種であり、LLM gateway は cache について虚偽の値を返していないかで検証方法を説明しています。

暗黙的なキャッシュ:順序を間違えても何も起きないため、特に厳しく監視する

Claude の cache は明示的です。GPT と多くの open-weight provider は prefix match に基づいて自動的に cache するため、marker は不要です。LangChain では constructor を 1 か所変えるだけで同じ chain を使えます。

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

plain string の system prompt で marker を付けずに実行したところ、GLM 5.2 の 2 回目の call では約 1,850-token の prefix のうち 1,088 token が read されました。すべてではありません。automatic cache は末尾まで byte 単位で一致させるのではなく、大きな block 単位で match します。たとえば OpenAI は 128-token 単位と明記しています。ここまでは追加作業なしでコストを削減できます。ただし、上の RAG table で説明した順序の問題は automatic cache にもそのまま当てはまり、さらに失敗が見えにくくなります。call ごとに新しい取得 context を使い、automatic path で同じ順序の実験を再度行いました。

順序(marker なし、automatic cache)call 1call 2(新しい query、新しい context)
context、rule の順read 0read 0
rule を先に配置し、context は human turn に配置read 0read 1,088

順序を間違えると無条件で 0 です。変動する context が先頭にあるため、prefix を共有する call が 1 つもなく、割引は発生しません。明示的な cache では、同じミスをすると call ごとの cache-write premium として少なくとも請求額に現れます。暗黙的な cache では premium も error も signal もありません。「automatic なら動いている」と思っていても、prompt は一度も条件を満たしていないだけです。marker を置くこともできないため、暗黙的な cache で調整できるのは prompt の順序だけです。

一度 test して終わりではなく、production で usage を監視してください。LangChain なら input_token_details.cache_read、raw なら prompt_tokens_details.cached_tokens を確認します。OpenAI の automatic cachingには、prefix が最低 1,024-token 必要とも記載されています。TTL と利用条件は provider ごとに異なり、詳細は第 2 回で扱っています。

チェックリスト

  • Claude では、("system", "...") の string tuple に cache_control を指定できません。何も cache されず、warning も出ません。cache したい system prompt は、content block と marker を持つ SystemMessage に入れます。
  • cache key は byte 単位で完全一致する prefix です。static content を先頭に置き、variable は marker より後ろか human turn に置きます。RAG context を rule より前に置くと、単に miss するだけでなく、call ごとに write premium が発生します。
  • cached block 内の variable は、値ごとに 1 つの cache entry を作ります。繰り返される値ならコストを回収できますが、timestamp や request ID など call ごとに一意の値は一度も hit しません。
  • prefix 内では tool が system prompt より前にあるため、system marker で bind 済みの tool も cache されます。bind_tools の serialization は決定的です。tool が最大の固定 block なら、Anthropic-format の tool dict に marker を置くこともできます。
  • conversation で marker を system block に固定すると、増え続ける history は通常価格のままです。最新の message に置き、各 turn で以前の prefix を read して差分だけを write する構成にします。
  • input_token_details.cache_creation は監視しないでください。write 時にも 0 のままなので、write premium が積み上がっていても dashboard では caching のコストが 0 に見えます。実際の値は ephemeral_5m_input_tokens にあります。または raw の response_metadata["usage"] を参照します。
  • automatic-cache model(GPT、GLM、DeepSeek)では prompt の順序しか調整できず、順序を間違えても何も通知されません。premium も error もなく、割引だけが発生しません。usage field で hit を確認してください。
  • set_llm_cache は完全一致する prompt と model config を key に response 全体を保存します。効果があるのは同一 request が繰り返される場合だけで、agent loop には効果がありません。

必要な対応は小さなものです。string の代わりに content block を使う。static content を variable より前に置く。conversation に合わせて marker を移動する。正しい usage field を読む。実測では、固定 token が毎回 90% 割引になるか、割引がまったくないかの差が出ました。順序を間違えた RAG では、追加コストまで発生しました。LangChain がプロンプトキャッシュを妨げているわけではありません。正しい prompt 構造と同じくらい、間違った構造も簡単に書けてしまうだけです。


免責事項

2026-07-04 に https://synthorai.io/ に対して実測しました。使用した version は langchain-core 1.4.8、langchain-anthropic 1.4.8、langchain-openai 1.3.3、model は claude-sonnet-5glm-5.2 です。約 1,800-token の英語 system prefix と少数の sample を使用し、cache write が反映される時間を確保するため、連続する call の間隔を 1–2 秒空けました。cold cache を保証するため、各実験では新しく randomize した prefix を使っています。そのため、table 間で baseline token 数が 1,852 から 1,875 までわずかに異なります。library の field mapping と provider の cache behavior は version によって変わります。この数値を前提に運用する前に、自分の stack で再計測してください。

出典

← ブログに戻る