Claude API の cache diagnostics(beta)で prompt cache が外れた原因を特定する

プロンプトキャッシュが突然効かなくなったとき、これまでは usage.cache_read_input_tokens が 0 に落ちたという結果しか分かりませんでした。2026-05-13 に public beta で入った cache diagnostics は、直前のリクエストと比較して『どこが変わってキャッシュが外れたか』を model / system / tools / messages の単位で教えてくれます。beta header と diagnostics.previous_message_id の使い方、cache_miss_reason の 6 種類の読み方を実例でまとめます。

プロンプトキャッシュは、レイテンシとコストを大きく下げてくれる一方で、プロンプトの先頭がバイト単位で前回と完全一致したときだけ効きます。ツールの順番が入れ替わった、system プロンプトにタイムスタンプを埋め込んでいた、過去のメッセージを 1 文字いじった——こうした些細な変化で、キャッシュは静かに無効化されます。そして今までは、それに気づく唯一の手がかりが「usage.cache_read_input_tokens が 0 に落ちた」という結果論だけでした。何が変わったのかは、自分で目を凝らして探すしかありません。

2026-05-13 に Claude API へ public beta として入った cache diagnostics は、このギャップを埋めます。直前のレスポンスの id を渡すと、API が 2 つのリクエストを比較し、最初に食い違った場所(model なのか、system なのか、tools なのか、message 履歴なのか)を返してくれます。当てずっぽうで原因を探す作業が、構造的な差分指摘に変わります。

この記事は、プロンプトキャッシュでコストを下げる話の続編にあたります。キャッシュを「効かせる」設計をしたあと、それが「なぜか効いていない」ときの調査ツールが cache diagnostics です。

何に使えるか

一番効くのは、長い system プロンプトや大きなドキュメントをキャッシュして会話を回しているのに、cache hit 率が安定しないケースです。

  • マルチターンのエージェントで、ある日から急にキャッシュが外れ始めた
  • 本番とステージングで cache hit 率が違うが、コードは同じはず
  • ツール定義を動的に組み立てていて、順番やシリアライズが揺れている疑いがある

こうした「心当たりはあるが特定できない」状況で、推測ではなく API の判定で原因箇所を一発で絞り込めます。

有効化のしかた

beta header cache-diagnosis-2026-04-07 を毎ターン付けます。リクエストボディに diagnostics を追加し、初回は previous_message_idnull にしてオプトインします(比較対象がまだ無いため)。2 ターン目以降は、前のレスポンスの id を渡します。

cache diagnostics は Claude API のみで利用できます。Amazon Bedrock / Vertex AI では使えません。

import anthropic

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

# 1 ターン目: previous_message_id=None でオプトイン(キャッシュを書き込む)
r1 = client.beta.messages.create(
    model="claude-opus-4-7",
    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-7",
    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"],
)

if r2.diagnostics is None:
    print("差分なし(プレフィックスは安定)")
elif r2.diagnostics.cache_miss_reason is None:
    print("比較がまだ進行中。次のターンで確認")
else:
    print(f"cache_miss_reason: {r2.diagnostics.cache_miss_reason.type}")

仕組みとしては、beta header があると API がリクエストの軽量なフィンガープリント(生のプロンプト本文ではなく、ハッシュとトークン数の見積りだけ)をレスポンス id をキーに保存します。次のリクエストで previous_message_id を渡すと、新しいリクエストのフィンガープリントを再構築して保存済みのものと比較し、最初の差分点diagnostics として返します。ストリーミング時は message_start イベントに diagnostics が乗ります。

diagnostics の 4 状態

レスポンスの diagnostics フィールドは 4 つの状態を取ります。ここを取り違えると誤読するので、最初に押さえておきます。

意味
フィールド自体が無いリクエストに diagnostics を入れていない、または beta header が無い
null初回(previous_message_id: null)で比較対象が無い、または比較した結果、差分が無かった
{"cache_miss_reason": null}比較がまだ走っている途中でレスポンスがシリアライズされた。判定保留として次ターンで確認
{"cache_miss_reason": {...}}原因が判定された。*_changed 系なら最初の差分点を示す

つまり diagnostics: null は「差分なし=正常」のサインであって、エラーではありません。ここを「情報が取れなかった」と勘違いしないことが大事です。

cache_miss_reason の 6 種類

cache_miss_reasontype で分類される判別共用体です。報告されるのは最も早い差分 1 件だけなので、それを直すと後ろに隠れていた差分が次に出てくることがあります。

type何が起きたかどう直すか
model_changedmodel が前回と違う(ルーター・A/B・フォールバックが別モデルを選んだ等)。キャッシュはモデル単位1 つのキャッシュ会話の中ではモデルを固定する
system_changedsystem が違う。多くはタイムスタンプ・リクエスト ID などを system に埋め込んでいるsystem をバイト単位で不変な定数にし、動的な値はキャッシュ境界の後ろの最初の user メッセージへ移す
tools_changedtools 配列が違う(追加・削除・並べ替え、または input_schema の非決定的シリアライズ)毎ターン同じ順序・同じツール群を送る。スキーマはキーをソートする等で決定的に
messages_changedmodel・system・tools は一致するが、過去の messages が追記ではなく書き換え・並べ替え・削除された履歴は append-only に扱う。assistant の content と tool_result はそのまま echo で返す
previous_message_not_found渡した previous_message_id のフィンガープリントが無い。リクエストが変わった証拠ではないbeta header を毎ターン付け、連続ターンの間隔を空けすぎない
unavailable診断情報が得られなかった。tool_choice / thinking / context_management / output_config 等の別パラメータが違う場合や、差分が比較地平線より深い長大な会話を含むプロンプトに影響するパラメータをキャッシュ会話中は固定する

*_changed 系には cache_missed_input_tokens(差分点より後ろに落ちた入力トークン数の見積り)も付くので、どれだけのキャッシュ可能プレフィックスを失ったかの規模感が分かります。これはトークン化前のバイト長から導かれる目安なので、課金の数値とは別物として扱ってください。

「リクエストが変わったか」と「キャッシュが当たったか」を読み合わせる

ここが実務で一番役立つ部分です。diagnostics は「自分のリクエストが変わったか」を、usage.cache_read_input_tokens は「キャッシュが当たったか」を答えます。2 つを組み合わせると、どこを見るべきかが決まります。

diagnosticscache 読込トークン解釈
null多い正常。プレフィックスが安定して cache hit している
null少ない / 0リクエストは一致しているがキャッシュエントリが消えていた。ターン間隔を詰めるか 1 時間 TTL を検討
*_changed少ない / 0自分のバグtype が示す原因を直す
*_changed多い稀。プロンプト後方で変化したが手前の cache_control 境界は当たっている。優先度は低い

「キャッシュが外れている」と一口に言っても、自分のリクエストが揺れているのか、それともサーバー側のキャッシュ寿命の問題なのかで対処はまったく違います。この表は、その切り分けを機械的にできるようにしてくれます。

運用上の注意

  • beta: フィールド名や意味は GA までに変わる可能性があります。
  • 保持は短期: previous_message_id 用のフィンガープリントは短時間で失効します。診断比較は間隔の近いリクエスト同士で走らせてください。
  • 同一 workspace: 前のリクエストは同じ組織・同じ workspace の API キーで送られている必要があります。
  • ベストエフォート: diagnostics がリクエストを止めたり失敗させたりすることはありません。情報が無ければ unavailable、比較中なら cache_miss_reason: null が返ります。
  • ZDR 適格: 生のプロンプト本文や出力は保存されません。保存されるのはハッシュとトークン数見積りだけです。

まとめ

cache diagnostics は、これまで「結果(cache_read が 0)」しか見えなかったキャッシュミスに対して、**原因(どのフィールドが、どれだけ揺れたか)**を返してくれる調査ツールです。地味ですが、長い system プロンプトや大きなドキュメントをキャッシュして回しているエージェントでは、cache hit 率の数 % が直接コストに効きます。

実務的には、本番投入前に一度 diagnostics を有効化して通しsystem_changedtools_changed が出ないことを確認してから外す、という使い方が手堅いです。キャッシュを「効かせる」設計そのものは プロンプトキャッシュの記事を参照してください。

release notes タグで Anthropic / Claude API のリリース情報を継続フォローしています。

参考