毎日回しているエージェントにプロンプトキャッシュを入れた。次の請求を見たら、金額が下がるどころか上がっていた。誤差ではなく、はっきりと。
この記事はその原因の話だ。自分がハマった落とし穴は、「とりあえずキャッシュを有効にしよう」系の記事がほぼ触れないところにある。
プロンプトキャッシュを一文で
長いシステムプロンプト(+ツール定義、+貼り付けた長文ドキュメント)を毎回送っていると、キャッシュなしでは同じブロックをモデルに読み直させるたびに入力の全額を払うことになる。キャッシュは、プロンプトの先頭部分(プレフィックス)を保存しておき、次の呼び出しではそれを「読み戻す」だけの安い料金で済ませる仕組みだ。
タダに聞こえる。だがタダではない。そこが罠だった。
刺さったポイント:書き込みの方が「高い」
キャッシュエントリには料金が2つある。自分は片方しか知らなかった。
- キャッシュ読み込み(ヒット):入力レートの最低 0.1x。みんなが引用するのはこの数字。キャッシュ済みトークンが約90%オフ。最高だ。
- キャッシュ書き込み:割増で課金される。短い(5分)ウィンドウで入力レートの約 1.25x。こっちは誰も言わない。
つまりプレフィックスを初めて送るとき(あるいは期限切れのたび)、90%の節約どころか、保存のために25%の割増を払う。キャッシュが得になるのは、その同じプレフィックスが書き込みだけでなく読み戻されたときだけ。書き込みは通常の読み込みより約25%高い(1.25x 対 1x)が、1回のキャッシュヒットで 0.9x(0.1x 対 1x)節約できるので、最初の再利用で割増の元が取れ、以降のヒットは約90%オフになる。要は、実際にヒットを取れるかどうかにかかっている。
だから失敗モードは具体的で、たちが悪い:キャッシュを書き込んで、一度も読まない。 使いもしない保存に割増を払う。それを毎回やれば、「即座に捨てるものをキャッシュするために割増を払い続ける機械」の完成だ。
まさにこれが自分のバグだった。
何を間違えたか
「安定している」はずのシステムプレフィックスが、安定していなかった。先頭近くに、数ヶ月前に足して忘れていたこれが埋まっていた。
Current session started: 2026-07-14T09:41:07Z
タイムスタンプ。毎回生成される。つまりプレフィックスが毎回違う。プロバイダは律儀に毎回新しいキャッシュを書き込み、二度と読み戻せない。どのプレフィックスも一致しないからだ。キャッシュの「高い方」だけを有効にして、「安い方」を一度も使っていなかった。
証拠は最初からレスポンスに出ていた。見ていなかっただけだ。
print(resp.usage)
# cache_creation_input_tokens: 3948 <- 毎回書き込んでいる
# cache_read_input_tokens: 0 <- 一度も読んでいない。ここにバグがある
cache_creation が伸び続け、cache_read が 0 のまま——これがプレフィックス崩壊のサインだ。この記事から一つだけ持ち帰るなら:この2つのフィールドをログに出して監視すること。
直し方は「規律」であって「もっとキャッシュ」ではない
キャッシュが効くのは、プレフィックスが呼び出し間でバイト単位で完全に同一で、かつ変化するものより前にある場合だ。ルールは2つ。
1. 静的なものを前、動的なものを後ろ。 システムプロンプト、ツール定義、貼り付けた長文——全部先頭で不変。ユーザーの実際のターン、タイムスタンプ、リクエストごとの ID——全部末尾、キャッシュ領域より後ろ。変わるものはキャッシュ境界の下流に置く。中に入れてはいけない。
2. プレフィックスを一字も動かさない。 タイムスタンプなし、JSON キーの並べ替えなし、システムブロックへの「気の利いた」リクエスト別の差し込みもなし。一文字動けばプレフィックス全体が外れる。
Claude では、安定プレフィックスの終端を境界で示す:
resp = client.messages.create(
model="claude-opus-4-8",
system=[{
"type": "text",
"text": LONG_STABLE_INSTRUCTIONS, # ここにタイムスタンプを入れない!
"cache_control": {"type": "ephemeral"}, # ここまでをキャッシュ
}],
messages=[{"role": "user", "content": todays_question}], # 変わるのはここだけ
max_tokens=1024,
)
OpenAI 系は約 1024 トークン超で自動。境界指定は不要で、規律は同じ——静的なものを前に置き、最長共通プレフィックスに一致させる。ただし課金は GPT-5.6 で変わった(7/15 追記):GPT-5.5 以前は書き込み割増なし・割引は浅め。GPT-5.6 以降は Anthropic 型(書き込み 1.25x・読み込み 90%オフ・明示的なキャッシュ境界・最短30分TTL)に移行——つまり本文のタイムスタンプの罠は、いまや OpenAI でも刺さる。結果は prompt_tokens_details.cached_tokens に出る。
自分はタイムスタンプをユーザーメッセージ側に移し、システムブロックは呼び出し間でそのままにして再実行した。2回目の呼び出しで cache_read_input_tokens が点灯し、そのエージェントの請求はガクッと下がった。同じ先頭を20回以上ループするので、いまやその先頭は「20回書いて0回読む」ではなく「1回書いて20回読む」になったからだ。
どこで効いて、どこで効かないか
魔法ではなく償却だ。大きくて同一なプレフィックスが何度も読まれるときに勝つ:
- エージェント / ツールループ — 各ステップが同じシステムプロンプトとツールスキーマを再送。理想形。
- 長文ドキュメント Q&A — ドキュメントを一度貼り、それに対して10問投げる。
- 大量抽出 — 太い指示+few-shot は不変、変わるのは入力行だけ。
逆に、毎回プロンプト全体が違う単発呼び出しには何の効果もない。書き込みを償却する繰り返しプレフィックスが存在しないからだ。そこで Claude(および同じ 1.25x 構造の GPT-5.6 以降)の明示キャッシュを有効にしても、見返りなしに書き込み割増を払うだけ(GPT-5.5 以前は課金されないが、どのみち効かない)。ワークロードに道具を合わせること。
ゲートウェイ経由ならもう一つの落とし穴
自分はプロバイダを直接叩かず、ゲートウェイ経由で回している(複数モデルを1つのキーで従量課金したいので byesu を使っている。開示:このゲートウェイの開発・運用には自分も関わっている——だからこそ、この節は「うちのは大丈夫」ではなく「自分のゲートウェイを自分で疑え」という話をする)。ゲートウェイはもう一つの失敗モードを足す:一部のアグリゲータはキャッシュを静かに壊す。 プロキシがフィールドを並べ替えたり、cache_control を落としたり、プロンプト本文にリクエスト別ヘッダを差し込んだりすると、プレフィックスが一致しなくなる——そしてエラーも出さずにキャッシュヒット0の答えが返り、毎回全額を払わされる。
だから自分のゲートウェイで必ず試す:同じリクエストを2回送り、2回目に cache_read_input_tokens が非ゼロになるか確認する。誰の言葉も——自分のも含めて——鵜呑みにせず、usage オブジェクトで確かめること。
まとめ、3行で
- キャッシュには書き込み割増(約 1.25x)があり、読み込みは安い(0.1x)。償却なので繰り返し読まれる前提が要る。
- プレフィックスはバイト単位で不変、かつ前方に。システムプロンプトのタイムスタンプは静かに金を溶かす。
-
cache_read_input_tokensを監視する。 繰り返し呼び出しで 0 なら、マーケティングが何と言おうと効いていない。
境界指定の細かいルール、OpenAI 側の挙動、プロバイダごとの usage フィールドの一覧はこちらにまとめた。とはいえ、上のタイムスタンプの話が結局すべてだ。