以前、すべてのリクエストで4万トークンのsystem promptを読み込むagentをリリースしたことがあります。毎時間数百件のリクエスト、毎回同じprompt、毎回フル入力価格で課金。修正はたった4行でした。Prompt cachingでその繰り返しコストは約10分の1になりました。キャッシュ読み取りのコストは基本入力価格の約0.1倍です。これがメインの数字であり、本物です。
しかし、最初に引っかかったのはここでした。キャッシュマーカーを追加しても何も変わらなかったのです。cache_read_input_tokensはずっとゼロのまま。キャッシュは静かに何もせず、私はその特権のために割増料金を払っていました。ハウツーの前に、他のすべてが派生する1つのルールを理解する必要があります。
Prompt cachingはプレフィックスマッチ
それだけです。キャッシュキーは、各キャッシュブレークポイントまでのレンダリングされたpromptの正確なバイト列です。そのプレフィックス内のどこかで何かが変わると、それ以降のすべてのキャッシュが無効になります。1バイトでも。並び替えられたJSONキー、タイムスタンプ、差し替えられたツール──そのどれかが変わると、古いキャッシュを読む代わりに新しいキャッシュエントリを書くことになります。
レンダリング順序は固定されています:tools、次にsystem、次にmessages。だから最初に来るものほど安定している必要があり、リクエストごとに変わるものは最後に置かなければなりません。この順序を正しくすれば、ほとんどのcachingは自動的に機能します。間違えると、どれだけマーカーを付けても無駄です。
キャッシュを壊していた原因
system promptのトップにあったこれが問題でした:
Current date: 2026-06-21 14:32:07
タイムスタンプです。リクエストごとに変わり、プレフィックスの先頭近くに位置し、それ以降のすべてを無効にしていました。考えもしなかった19文字の文字列のせいで、4万トークンのprompt全体がキャッシュ不可能になっていたのです。
これは私が見る中で最も一般的なcachingバグです。人々は動的な内容──現在日時、ユーザー名、セッションID、モードフラグ──をsystem promptに埋め込み、プレフィックスを汚染します。解決策は、system promptを固定して動的な部分をmessages配列に後から注入することです。そうすればそれ以前のものは無効になりません。ターン5の事実はターン1〜4のキャッシュに触れません。
キャッシュがヒットしないときの私の監査チェックリストです。prompt構築コードでgrepしてください:
datetime.now()/Date.now()──system promptやツールのどこかにuuid4()やリクエストIDがコンテンツの早い段階にsort_keys=Trueなしのjson.dumps(d)──Pythonのdict反復順序でバイト列が変わりうる- ユーザーやセッションIDをsystem promptに埋め込むf-string
- ユーザーごとに構築されるツールセット──リクエストごとにtoolsブロックが異なる
同じプレフィックスを共有するとわかっている2つのリクエストでcache_read_input_tokensがゼロなら、どちらかが原因です。2つのリクエスト間のレンダリング済みバイト列をdiffすれば見つかります。
ブレークポイントの配置
実際のマーカーはコンテンツブロックへのcache_control: {type: "ephemeral"}です。最もシンプルなケース、大きな共有system prompt:
"system": [{
"type": "text",
"text": "<your big stable prompt>",
"cache_control": {"type": "ephemeral"}
}]
ツールはsystemの前にレンダリングされるため、最後のsystemブロックにマーカーを付けるとツールとsystemがまとめてキャッシュされます。リクエストあたりのブレークポイントは最大4つなので、賢く使ってください。
マルチターン会話では、最新のターンの最後のブロックにブレークポイントを置きます。新しいリクエストはそれぞれ直前の会話全体をキャッシュされたプレフィックスとして再利用し、チャットが成長するにつれてヒットが積み重なります。毎回異なる質問を持つ共有プレアンブル──few-shotの例と毎回違うクエリ──には、共有部分の末尾にマーカーを置いてください。prompt全体の末尾ではありません。可変の質問までキャッシュすると、リクエストごとにユニークなエントリを書き込み、何も読まれません。
経済性、なぜなら無料ではないから
読み取りは安価(約0.1倍)ですが、書き込みは通常リクエストより高コストです:デフォルト5分TTLで1.25倍、1時間TTLで2倍。だからcachingが効果を発揮するのは、書き込みより多く読み取る場合だけです。5分TTLでは2リクエストで損益分岐点に達します。1時間TTLでは少なくとも3回必要です。書き込みプレミアムが2倍になっているためです。
どちらのTTLを使うか?トラフィックに5分以上の空白がある場合のみ1時間を使います。それより頻繁にリクエストが来るなら、キャッシュはそれ自体で温かく保たれます。デフォルトの5分TTLで十分であり、安くなります。デフォルトで1時間TTLに手を伸ばさないでください──トラフィックが安定していれば、2倍の書き込みコストが静かに節約分を食い尽くします。
私がそれぞれデバッグ午後を費やした2つの落とし穴
20ブロックのルックバック。 各ブレークポイントは以前のキャッシュエントリを見つけるために最大20コンテンツブロック後ろを見ます。多くのtool_use/tool_resultペアを持つagentループでは、単一のターンで20ブロック以上追加できます──そして次のリクエストは以前のキャッシュを見つけられず、静かにミスします。修正:長いターンでは約15ブロックごとに中間ブレークポイントを追加します。
並行リクエスト。 キャッシュエントリは最初のレスポンスがストリーミングを開始した後にのみ読み取り可能になります。同じプレフィックスで10件の並列リクエストを送ると、10件すべてがフル価格を払います。他が書き込んでいるものを読めないからです。ファンアウトでは:1件のリクエストを送り、最初のストリームトークンを待ち、それから残りを送ります。それらが最初のリクエストが書き込んだキャッシュを読みます。
知っておく価値のある新しいトリック
Opus 4.8には、キャッシュを破壊せずに会話の途中で指示を注入するクリーンな方法があります:トップレベルのsystemを編集する代わりに{"role": "system", ...}メッセージをmessages配列に追加します。トップレベルのsystemを編集すると履歴全体の前のプレフィックスが変わり、キャッシュされたターンがすべて再処理されます。systemロールのメッセージは履歴の後に位置し、キャッシュされたプレフィックスをそのまま保ちます。偽造不可能なオペレーターチャンネルでもあり、良いおまけです。
usageオブジェクトですべてを確認してください:cache_creation_input_tokensが書き込んだもの、cache_read_input_tokensが安価なレートで読み取ったもの、input_tokensがキャッシュされていない残り。agentが1時間動作してinput_tokensが4Kと表示されても焦らないでください──残りはキャッシュから来ています。単一フィールドではなく合計を確認してください。
設定4行、1つのルールを守るだけ。プレフィックスを尊重すれば、請求書はそれに従います。
