Cache diagnosticsでキャッシュミスの原因を特定する
Anthropic APIのCache diagnosticsベータは、プロンプトキャッシュがなぜミスしたかを6分類で特定します。使い方と判定表を解説します。
Cache diagnosticsとは何か
プロンプトキャッシュはリクエストの先頭がバイト単位で完全一致したときだけコストと遅延を削減します。しかし、ツールの並び順が入れ替わった、system promptにタイムスタンプが埋め込まれた、過去のメッセージが編集された、といった変化が起きても、従来はエラーにならず黙ってキャッシュミスするだけでした。手がかりは usage.cache_read_input_tokens がゼロに落ちることだけで、何が原因かは分かりませんでした。
Cache diagnosticsはこのギャップを埋めるベータ機能です。直前のレスポンスの id を渡すと、APIが2つのリクエストを比較し、モデル・system prompt・ツール・メッセージ履歴のどこで分岐したかを教えてくれます。原因を推測するのではなく、根本原因を直接特定してから直す運用に変わります。
仕組み — フィンガープリントの比較
betaヘッダーが付いたリクエストが届くと、APIはそのリクエストの軽量なフィンガープリントをレスポンス id に紐づけて保存します。次のリクエストで、その id を diagnostics.previous_message_id として渡すと、APIは新しいリクエストのフィンガープリントを再構築し、保存済みのものと比較して、最初に分岐した箇所を diagnostics オブジェクトとしてレスポンスに添えます。
この比較はリクエストの構造そのものについてのものであり、実際にキャッシュがヒットしたかどうかとは独立しています。両方を組み合わせて読む方法は後述します。
フィンガープリントに含まれるのはハッシュ値とトークン数の推定値のみで、生のプロンプト内容は一切含まれません。保存期間は限定的で、組織とワークスペースの範囲に閉じており、他の用途には使われません。ZDR(zero data retention)適格な機能ですが、Covered Modelsに指定されているモデルは対象外です。自社のモデル運用がZDR契約下にある場合は、利用前にCovered Modelsの範囲を確認しておく必要があります。
対応プラットフォームもCompatibility表で確認できます。現時点ではClaude API(ベータ)限定で、AWS上のClaude Platform、Amazon Bedrock、Google Cloud、Microsoft Foundryでは提供されていません。マルチプラットフォームでモデルを運用しているチームは、Cache diagnosticsが使えるのは第一者APIのトラフィックだけだと理解したうえで導入計画を立てる必要があります。
基本的な使い方
すべてのターンでbetaヘッダーを送ります。1ターン目は比較対象がないので previous_message_id: null を渡してオプトインし、2ターン目以降は直前のレスポンスの id を渡します。
# ターン1: キャッシュを確立しdiagnosticsにオプトイン
response=$(curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: cache-diagnosis-2026-04-07" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5",
"max_tokens": 1024,
"cache_control": {"type": "ephemeral"},
"system": "You are an AI assistant analyzing a large document. <document>...</document>",
"messages": [{"role": "user", "content": "Summarize section 1."}],
"diagnostics": {"previous_message_id": null}
}')
message_id=$(jq -r '.id' <<< "$response")
# ターン2: 直前のidを渡してプレフィックスを比較
curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: cache-diagnosis-2026-04-07" \
-H "content-type: application/json" \
-d "{
\"model\": \"claude-opus-5\",
\"max_tokens\": 1024,
\"cache_control\": {\"type\": \"ephemeral\"},
\"system\": \"You are an AI assistant analyzing a large document. <document>...</document>\",
\"messages\": [
{\"role\": \"user\", \"content\": \"Summarize section 1.\"},
{\"role\": \"assistant\", \"content\": \"Section 1 covers...\"},
{\"role\": \"user\", \"content\": \"Now summarize section 2.\"}
],
\"diagnostics\": {\"previous_message_id\": \"$message_id\"}
}"ストリーミングを使う場合、diagnostics は message_start イベントに載って届きます。SSEイベント種別の生パース手順も参照してください。
レスポンスの4状態を見分ける
diagnostics フィールドは4つの状態を取り得ます。それぞれの意味を取り違えると誤診断につながるため、まず状態を切り分けてから中身を読みます。
| 値 | 意味 |
|---|---|
| フィールド自体が無い | 意味リクエストに diagnostics を含めなかった、またはbetaヘッダーが無かった |
null | 意味previous_message_id が null(1ターン目)、または比較の結果分岐なしと判定された |
{"cache_miss_reason": null} | 意味比較がレスポンス生成時点でまだ実行中だった。結論を出さず次のターンで確認する |
{"cache_miss_reason": {...}} | 意味分岐点が特定された。*_changed 系は最初の分岐点を示し、previous_message_not_found と unavailable は比較自体が成立しなかったケース |
キャッシュミス原因の6分類
cache_miss_reason は type で判別するdiscriminated unionです。レスポンスが報告するのは最も早い分岐点だけなので、それを直してから次を疑います。後続の分岐は先に見つかった分岐の陰に隠れて報告されません。
| type | 意味 | 対処 |
|---|---|---|
model_changed | 意味model が前回と異なる(ルーター・A/Bテスト・フォールバックが別モデルを選んだ等)。キャッシュはモデルごとに独立 | 対処キャッシュ対象の会話ではモデルを固定する |
system_changed | 意味system パラメータが異なる。多くはタイムスタンプやリクエストIDがsystem promptに埋め込まれている | 対処system promptをバイト単位で不変にし、動的なデータはキャッシュブレークポイント後の最初のuserメッセージへ移す |
tools_changed | 意味tools 配列が異なる(追加・削除・並び替え、または input_schema のJSONシリアライズが非決定的) | 対処毎ターン同じツールリストを固定順で送り、スキーマは決定的にシリアライズする(キーをソートする等) |
messages_changed | 意味model・system・toolsは一致するが、messages の前段が改変・並び替え・削除された(追記ではない) | 対処履歴を追記専用として扱い、assistantの content とtool結果を逐語で送り返す |
previous_message_not_found | 意味渡した previous_message_id に対応するフィンガープリントが存在しない。これはリクエストが変わった証拠ではない | 対処毎ターンbetaヘッダーを送り、連続するターンの間隔を詰める |
unavailable | 意味診断情報が得られなかった。model・system・toolsは一致するが tool_choice / thinking / context_management / output_config / output_format / 有効な anthropic-beta の組み合わせが異なる場合、または比較範囲を超える長い会話が該当する | 対処プロンプトに影響するリクエストパラメータをキャッシュ対象の会話全体で固定する |
*_changed 系の4種は cache_missed_input_tokens という整数も持ちます。分岐点以降で失われたと推定される入力トークン数の目安ですが、トークン化前のバイト長から算出されるため、課金上の正確な数値ではなく規模感の指標として扱います。usage.input_tokens と一致しないことも、まれに上回ることもあります。
実際のレスポンスは次のような形になります。system_changed が検出されたケースでは、usage.cache_read_input_tokens が0になり cache_creation_input_tokens に分岐後の全トークンが計上されている点にも注目してください。診断結果とusageの数値が矛盾なく揃っていることが確認できます。
{
"id": "msg_01Xyz...",
"type": "message",
"role": "assistant",
"content": [{ "type": "text", "text": "..." }],
"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
}
}
}diagnosticsとusageを突き合わせる4パターン
diagnostics は「リクエストが変わったか」に答え、usage.cache_read_input_tokens は「キャッシュがヒットしたか」に答えます。両方を組み合わせることで、どこを見るべきかが絞り込めます。
この判定表は previous_message_id に実際のIDを渡したターンにのみ適用します。1ターン目(previous_message_id: null)は diagnostics が常に null になり、キャッシュは読み取りでなく書き込みの最中なのでトラブルシューティングは不要です。同様に cache_miss_reason が null(比較待ち)、または previous_message_not_found / unavailable(比較不成立)のときも本表の対象外です。
| diagnosticsの結果 | キャッシュ読み取りトークン | 解釈 |
|---|---|---|
null | キャッシュ読み取りトークン多い | 解釈期待どおり。プレフィックスは安定しキャッシュがヒットしている |
null | キャッシュ読み取りトークン少ない・ゼロ | 解釈リクエストは一致しているがキャッシュエントリが既に無くなっている。ターン間の間隔を詰めるか1時間キャッシュTTLの利用を検討する |
*_changed型 | キャッシュ読み取りトークン少ない・ゼロ | 解釈こちらのバグ。type が示す原因を直す |
*_changed型 | キャッシュ読み取りトークン多い | 解釈まれ。プロンプトの後方で変化が起きたが、より前のキャッシュブレークポイントはヒットした。影響は小さいが直す価値はある |
会話ループでidを引き継ぐ実装パターン
複数ターンにわたる会話アプリケーションでは、直前のレスポンス id を毎ターン previous_message_id として引き継ぐループを組みます。最初のイテレーションだけ null を渡してオプトインし、2回目以降は直前のレスポンスの id を渡す、という単純な状態管理です。
messages = []
prev_id = None
for i, user_message in enumerate(
["Summarize section 1.", "Now section 2.", "Now section 3."]
):
messages.append({"role": "user", "content": user_message})
r = client.beta.messages.create(
model="claude-opus-5",
max_tokens=1024,
cache_control={"type": "ephemeral"},
system=SYSTEM,
messages=messages,
diagnostics={"previous_message_id": prev_id},
betas=["cache-diagnosis-2026-04-07"],
)
if r.diagnostics is not None and r.diagnostics.cache_miss_reason is not None:
print(f"Turn {i + 1} cache_miss_reason: {r.diagnostics.cache_miss_reason.type}")
messages.append({"role": "assistant", "content": r.content})
prev_id = r.idこのパターンのポイントは、prev_id の更新をレスポンスを受け取った直後の1箇所に集約することです。会話履歴 messages への追記と prev_id の更新を別々の場所で行うと、片方だけ更新し忘れて previous_message_not_found を延々と出し続けるバグを作り込みやすくなります。ループの各ターンで cache_miss_reason をログに残しておけば、会話が長くなった後で急にキャッシュヒット率が落ちたときも、どのターンから分岐が始まったかを遡って特定できます。
リクエスト構築コードのどこで潰すか — 6分類をチェックリストに変換する
cache_miss_reason の6分類は、そのままではAPIレスポンスの読み方の説明で終わってしまいます。実際に効くのは、これを「自分たちのリクエスト構築コードのどの層に対応するか」に読み替えたときです。多くのアプリケーションでは、リクエストを組み立てる処理が次の4層に分かれています。
- モデル選択層: ルーティング・A/Bテスト・フォールバックのロジックがどのモデルIDを使うか決める箇所。
model_changedはほぼ必ずここが原因です。ログにモデルIDと選択理由を残しておけば、model_changedが出た瞬間にどの分岐条件が発火したか追えます。 - system組み立て:
system文字列を生成する箇所。テンプレートに現在時刻やリクエストIDを埋め込んでいないか、複数のコードパスから同じsystem promptを組み立てていて片方だけ書式がずれていないかを疑う層です。system_changedが出たら最初に見るべき場所であり、後述のウォームアップ運用でも同じ層が原因になりがちです。 - ツール登録順:
tools配列を組み立てる箇所。ツールの登録順が実行時の条件分岐やdictの反復順に依存していると、tools_changedが断続的に発生します。ツールリストをコード側で明示的にソートしてから送る運用にすると、この層は事実上潰せます。 - 履歴シリアライズ: 会話履歴
messagesを永続化・復元する箇所。DBやセッションストアに保存した履歴を読み戻すときに、assistantメッセージのcontentを要約・再整形しているとmessages_changedになります。履歴は編集せず、受け取った形のまま逐語で往復させる設計が必要です。
previous_message_not_found と unavailable はこの4層のバグではなく、運用条件(betaヘッダーの送信漏れ、ターン間隔、比較範囲)に起因するため、上のチェックリストとは切り分けて扱います。*_changed 系が出たら4層のうちどこかにバグがある、それ以外が出たら運用条件を疑う、という二段構えで切り分けると、原因調査の対象範囲を素早く絞り込めます。
cache_miss_reasonをアプリ側の監視に落とし込む
Cache diagnosticsはAPIのレスポンスに1回分の診断結果を返すだけで、時系列で追跡したり閾値を割ったときに知らせたりする機能は持ちません。継続的に監視するには、アプリ側でログとメトリクスに落とし込む設計が必要です。
最小構成では、ターンごとに cache_miss_reason の有無と type を構造化ログに記録します。先のPython例のように print で出すだけでも調査の手がかりにはなりますが、本番運用ではリクエストIDや会話IDと紐づけて記録し、type 別に集計できる形にしておくと効果が上がります。*_changed の type をタグとしてメトリクス基盤に送れば、「system_changed が特定のデプロイ以降に急増した」といった変化をダッシュボードで検知できます。
閾値によるアラートを組む場合は、cache_miss_reason が非nullだった回数そのものではなく、*_changed 型が出た回数を基準にするのが実用的です。previous_message_not_found や unavailable はネットワーク遅延やターン間隔の長さといった運用条件でも発生するため、これらを含めて閾値を設定すると誤報が増えます。*_changed 型に絞ってアラートを組めば、リクエスト構築コード側の回帰を検知する用途に近づけられます。
制約事項
Cache diagnosticsはベータ機能であり、フィールド名や意味論はベータ期間中に変わり得ます。Claude APIのみで利用でき、Amazon BedrockやGoogle Cloudでは使えません。
フィンガープリントの保持期間は短く、previous_message_id の照合はターン間が近いリクエスト同士でないと機能しません。また同一ワークスペースである必要があり、直前のリクエストが別の組織・ワークスペースで実行されていた場合は照合できません。2つのレスポンスの anthropic-workspace-id レスポンスヘッダーを比較すれば確認できます。
非常に長い会話で、変化がメッセージ履歴の深い位置にしかない場合は、正確な分岐点ではなく unavailable が返ることもあります(比較の範囲=comparison horizonの制約)。診断はあくまでベストエフォートで、リクエスト自体をブロックしたり失敗させたりすることはありません。診断情報が得られなければ unavailable、比較がまだ進行中なら cache_miss_reason: null が返り、リクエスト自体は正常に処理されます。
データ保持とZDR
Cache diagnosticsはZDR(zero data retention)適格な機能です。プロンプトの生テキストやClaudeの出力そのものは保存されません。各リクエストのフィンガープリントは暗号学的ハッシュとトークン数推定値のみで構成され、レスポンス id に紐づいて組織・ワークスペース単位で保存されます。フィンガープリントは短期間で失効し、他の用途には使われません。
事前ウォームアップとの併用時の注意
事前ウォームアップ(max_tokens: 0)を使う構成でCache diagnosticsを併用する場合、ウォームアップリクエストにも同じbetaヘッダーと diagnostics パラメータを渡せば、ウォームアップが書き込んだキャッシュに実リクエストが正しくヒットしているかを、通常のリクエストと同じ判定表で確認できます。ウォームアップとその後の実リクエストで system や tools が一字一句一致しているかを疑うときに、Cache diagnosticsは推測を要らなくします。
特にウォームアップ運用でハマりやすいのは system_changed です。ウォームアップリクエストのsystem promptと実リクエストのsystem promptを別々のコードパスで組み立てていると、片方だけタイムスタンプの書式やフィールドの並び順が変わり、バイト単位では一致しなくなることがあります。ウォームアップのレスポンス id を previous_message_id として最初の実リクエストに渡しておけば、この種の食い違いを本番トラフィックが増える前に検出できます。
まとめ
Cache diagnosticsは、キャッシュミスの原因を「推測」から「特定」に変えるベータ機能です。従来はキャッシュヒット率が落ちたことにログの中から気づき、リクエスト構築コードを1行ずつ疑って回るしかありませんでした。診断結果を毎ターン記録しておくだけで、この切り分け作業がほぼ不要になります。previous_message_id を渡すだけで、model・system・tools・messagesのどこで分岐したかを6分類で受け取れます。実運用では、diagnostics が *_changed 型を返したら真っ先にその原因を潰し、unavailable や previous_message_not_found が続くならbetaヘッダーの送信漏れやワークスペースの不一致を疑う、という順序で切り分けるのが効率的です。usage.cache_read_input_tokens と突き合わせる4パターンの判定表を手元に置いておけば、キャッシュ関連の不具合調査にかかる時間を大きく減らせます。ベータのうちに導入し、判定表を運用の標準手順に組み込んでおく価値があります。
プロンプトキャッシュの基本課金はClaudeのプロンプトキャッシュの仕組み、事前ウォームアップの手順はプロンプトキャッシュの事前ウォームアップをmax_tokens=0で行う、ツール定義のキャッシュ配置はツール定義のcache_controlはどこに置くかで扱っています。