Claudeのプロンプトキャッシュが効かない原因を cache_miss_reason で特定する

Claude API

プロンプトキャッシュはClaude APIのレイテンシとコストを大きく下げます。ただし効くのは、プロンプトの先頭(プレフィックス)が前回とバイト単位で一致しているときだけ。1か所変わると、それ以降のキャッシュがまとめて無効になります。

問題は、「効いていない」ことは分かっても「なぜ」が分からないこと。唯一の手がかりは usage.cache_read_input_tokens が0に落ちること——どこが原因かは教えてくれません。

この記事では、その原因を一点で特定できるbeta機能 cache diagnosticscache_miss_reason)を、実例と6種類の原因タイプ、そして usage との読み合わせまで解説します。

キャッシュは「プレフィックス一致」でしか効かない

キャッシュキーは、各 cache_control 区切りまでのレンダリング済みプロンプトのバイト列から作られます。レンダリング順は toolssystemmessages

  • ツールの並びが変わった
  • systemプロンプトにタイムスタンプを埋め込んだ
  • 過去メッセージを編集した

——こうした変化が起きると、その地点以降のキャッシュが静かに無効化されます。エラーは出ません。

cache diagnostics の仕組み

betaヘッダ cache-diagnosis-2026-04-07 を付けると、APIはリクエストごとに軽量なフィンガープリント(生のプロンプト本文ではなく、ハッシュとトークン数推定値のみ)を、レスポンスの id をキーに保存します。

次のリクエストでその iddiagnostics.previous_message_id として渡すと、APIが新旧を比較し、最初に分岐した地点diagnostics として返します。

比較対象は「リクエストの構造」であって、キャッシュが実際にヒットしたかとは独立です。だから後述のとおり usage.cache_read_input_tokens組み合わせて読みます。

使い方(Python)

毎ターンbetaヘッダを付けます。初回は previous_message_idNone を渡してopt-in、次ターン以降は前レスポンスの id を渡します。

import anthropic

client = anthropic.Anthropic()
SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>"

# ターン1: キャッシュを作りつつ診断を有効化
r1 = client.beta.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    cache_control={"type": "ephemeral"},
    system=SYSTEM,
    messages=[{"role": "user", "content": "Summarize section 1."}],
    diagnostics={"previous_message_id": None},
    betas=["cache-diagnosis-2026-04-07"],
)

# ターン2: 前ターンのidを渡して比較させる
r2 = client.beta.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    cache_control={"type": "ephemeral"},
    system=SYSTEM,
    messages=[
        {"role": "user", "content": "Summarize section 1."},
        {"role": "assistant", "content": r1.content},
        {"role": "user", "content": "Now summarize section 2."},
    ],
    diagnostics={"previous_message_id": r1.id},
    betas=["cache-diagnosis-2026-04-07"],
)

d = r2.diagnostics
if d is None:
    print("分岐なし(キャッシュ的にはOK)")
elif d.cache_miss_reason is None:
    print("比較がまだ走り切っていない(次ターンで確認)")
else:
    print(f"cache_miss_reason: {d.cache_miss_reason.type}")
  • client.beta.messages.create(...)betas=["cache-diagnosis-2026-04-07"]
  • リクエスト側は diagnostics={"previous_message_id": ...}、結果は response.diagnostics
  • ストリーミング時は message_start イベントに diagnostics が乗ります.get_final_message() でも取得可)

diagnostics の4状態

意味
フィールド自体が無い diagnostics を渡していない or betaヘッダ無し
null previous_message_idnull(初回)だった、または比較した結果分岐なし
{"cache_miss_reason": null} 比較がまだ走り切る前にレスポンスが返った。判定不能扱いで次ターンを見る
{"cache_miss_reason": {...}} 原因が付いた(*_changed は最初の分岐点)

非nullのときの実例:

{
  "usage": {
    "input_tokens": 42,
    "cache_read_input_tokens": 0,
    "cache_creation_input_tokens": 41850,
    "output_tokens": 210
  },
  "diagnostics": {
    "cache_miss_reason": {
      "type": "system_changed",
      "cache_missed_input_tokens": 41850
    }
  }
}

cache_miss_reason の6タイプと対処

cache_miss_reasontype による判別共用体です。報告されるのは“最初の”分岐点だけなので、まずそこを直します(後ろの分岐はそれに隠れている可能性があります)。

type 何が起きたか 直し方
model_changed model が前回と違う(ルーター/ABテスト/フォールバック)。キャッシュはモデル単位 1つのキャッシュ会話内でモデルを固定
system_changed system が違う。典型はタイムスタンプ・リクエストIDの埋め込み systemはバイト不変の定数に。動的値はキャッシュ境界より後の最初のuser
tools_changed tools が違う(追加・削除・並べ替え、input_schema の非決定的シリアライズ) 毎ターン同じツールを同じ順で。スキーマはキーをsort等で決定的に
messages_changed model/system/toolsは一致するのに、過去のmessages追記でなく改変・並べ替え・削除された 履歴はappend-onlyに。assistantのcontentとtool結果はそのままecho
previous_message_not_found 渡したprevious_message_idのフィンガープリントが無い。自分が変えた証拠ではない(前回betaヘッダ無し/別ワークスペース/時間経過) 毎ターンbetaヘッダを付け、連続ターンを時間的に近づける
unavailable 診断不可。model/system/toolsは一致だが他のプロンプト影響パラメータ(tool_choice/thinking/context_management/output_config/beta集合)が違う等。リクエストは正常処理 キャッシュ会話の間はこれらも固定

*_changed 系は cache_missed_input_tokens も持ちます。分岐点より後ろに何トークン分のキャッシュ可能プレフィックスが失われたかの概算(バイト長由来の規模感の指標で、課金値ではありません)。

diagnostics × usage の読み合わせ

diagnostics は「リクエストが変わったか」、usage.cache_read_input_tokens は「キャッシュがヒットしたか」を答えます。組み合わせると原因の在り処が分かります。

diagnostics cache_read 解釈
null 高い 期待どおり。プレフィックス安定+ヒット
null 低い/ゼロ リクエストは一致だがキャッシュが消えていた。ターン間隔を詰める or 1時間TTLを検討
*_changed 低い/ゼロ 典型的な自分のバグ。type が示す原因を直す
*_changed 高い 稀。後方で変化したが前のcache_control境界はヒット。影響小

データ保持と制限

  • ZDR適格。保存されるのは暗号学的ハッシュとトークン数推定のみで、プロンプト本文・出力は保存されません。組織/ワークスペース単位、短期で失効。
  • beta: フィールド名・意味はGAまでに変わり得ます。
  • Claude API専用: Amazon Bedrock / Vertex AI では使えません。
  • 保持は短期: previous_message_id 引きは短時間で失効。連続リクエストは時間的に近づけて比較を。
  • 同一ワークスペース: 前リクエストは同じ組織・ワークスペースのキーで。

まとめ

  • cache_read_input_tokens が0」で止まっていた調査を、cache_miss_reason.type で原因の一点に特定できます。
  • 付けるのはbetaヘッダ cache-diagnosis-2026-04-07diagnostics.previous_message_id だけ。
  • 実務で一番効くのは system_changed(タイムスタンプ埋め込み)と messages_changed(履歴の非append改変)。まずここを疑う。
  • diagnostics(変わったか)×usage(ヒットしたか)で、「自分のバグ」か「キャッシュ失効」かを切り分けます。

関連記事
– 会話途中の指示変更でキャッシュを壊さない(Claude Opus 4.8)
– compaction / context editing で長時間エージェントを回す
– advisor toolで安いモデル中心にコストを抑える

本記事はClaude API公式ドキュメント「Cache diagnostics」に基づきます。betaのため仕様は変わり得ます。利用前に最新の公式ドキュメントをご確認ください。本記事はAPI仕様の解説・検証を目的とした技術記事です。

コメント

タイトルとURLをコピーしました