毎月のAI API代、減らせるところまだ残ってませんか? — Prompt Cachingで月額を1/3〜1/10にする実践ガイド(Anthropic / OpenAI / Gemini 3社比較)
はじめに
毎月の生成AIのAPI請求書、見るたびに「あれ、思ったより高いな…」って感じてる人、けっこういるんじゃないかなと思います。
Claude Code を仕事で使い倒すようになった、社内ナレッジを RAG で読ませるBotを立てた、AIエージェントを自前で組んだ。サービスは順調に動いている。ただ、コストだけが想定の倍に膨らんでいる。
正直に言いましょう。これ、ほとんどのケースで Prompt Caching を使えてないだけ だったりします。
この記事では、
- そもそも「キャッシュ」って何がキャッシュされているのか
- Anthropic / OpenAI / Gemini で仕様がどう違うのか
- どう書けばキャッシュヒットするのか、どう書くと逆に高くなるのか
- ヒット率を運用でどう測るか
- 設計でやってはいけないことは何か
までを一気に整理します。読み終わるころには、翌日からプロンプトの並びを変えるだけで月額API代を半分以下にする具体イメージ が持てるはずなので、ちょっと付き合ってください。
1. なぜLLMは「前置き」を毎回再計算しているのか
まず、ここを腹落ちさせておかないと、cache_control のコードを見ても意味がよく分からなくなります。
LLMって、内部的には 「毎朝、昨日の議事録を最初の1ページから音読し直してから今日の話に入る新人」 みたいなところがあるんです。
具体的にはこうです。LLMはトークン列を1つずつ処理しながら、KVキャッシュ(Key-Valueキャッシュ。Attentionの中間状態を保持しているメモリ)を埋めていきます。100トークンの system プロンプトを入れたら、その100トークン分の中間状態を計算して保持し、次の user メッセージにつなげて answer を生成する。
ここで残酷なのが、この中間状態は1回のリクエストが終わったら捨てられる ということです。次に同じ system プロンプトを送ったら、また同じ100トークンを最初から計算し直す。これが「前置きを毎回再計算する」の正体です。
Prompt Caching は、ざっくり言うと 「前回計算したKVキャッシュを使い回させてくれ」 という機能です。GPU側にすでに正解の中間状態が残っているなら、再計算せずに再利用する。だから安く、速くなる。
ここで重要なのが、プレフィックス一致が絶対条件 ということなんです。
トークン列の 先頭から、1文字(正確には1トークン)でも違ったら、その時点でキャッシュは全部無効になる 。途中だけ同じでも、前が違えば後ろも全部別物として扱われる。Attentionの仕組み上、先頭が変われば全ての中間状態が変わるので、これはどうしようもない。
つまり、Prompt Cachingでコストを下げたければ、「先頭から、できるだけ長く、同じ文字列を保つ」 プロンプト設計をする必要がある、ということなんですよね。
2. 3社のキャッシュ仕様を1枚で見比べる
同じ「キャッシュ」と言っても、Anthropic / OpenAI / Gemini で考え方が全然違います。1表で並べてみます。
| 観点 | Anthropic (Claude) | OpenAI (GPT) | Google (Gemini) |
|---|---|---|---|
| 有効化方法 |
cache_control を明示的に置く |
完全自動 (コード変更ゼロ) | 暗黙(2.5+ 自動) + 明示(cachedContents API) |
| 最小トークン | Sonnet系 1024 / Haiku系 2048 | 1024 トークン | プレフィックス共有が成立する量 |
| 増分マッチ | breakpoint単位 | 128 トークン刻み | 暗黙はプレフィックス共有のみ |
| 割引率 (読み込み) | 約90%オフ (基本料金の10%) | 50%オフ (モデルにより最大90%) | 最大75〜90%オフ |
| 書き込み(初回)コスト | +25% (5分TTL) / +100% (1時間TTL) | 追加コストなし | 暗黙:無料 / 明示:ストレージ別建て |
| TTL | 5分 / 1時間 を選択 | 5〜10分 (アイドル時間) | 明示は秒単位で指定可、暗黙は自動 |
| 監視フィールド |
cache_creation_input_tokens / cache_read_input_tokens
|
prompt_tokens_details.cached_tokens |
cachedContentTokenCount |
| breakpoint上限 | 4個まで | なし(自動) | 1リクエスト1キャッシュ(明示) |
| 共有スコープ | 同一APIキー内のみ | 同一組織内 | 同一プロジェクト内 |
ここから言えるのは、
-
OpenAI は黙って勝手にやってくれる ので、すでに動いてるアプリは usage に
cached_tokensを見に行くだけで効果確認できる - Anthropic は指差し方式 で、breakpoint を置くスキルが必要だけど、その分割引が一番大きい
- Gemini は2.5以降、暗黙キャッシュが標準装備 になっていて、明示キャッシュは長文ドキュメント(動画・PDF・コード全文)の使い回しに向く
自分のアプリで月にどれくらい使うか・どこに前置きの重さがあるかで、軸にするプロバイダが変わってきます。
3. キャッシュ前提のプロンプト設計5原則
ここが、この記事で一番伝えたいところです。
Prompt Caching って 「機能を有効化する」より「プロンプトの並びを設計する」のほうが本質 なんです。並びがダメなら、いくらSDKのオプションを付けてもヒットしません。
原則1:先頭から「固定 → 半固定 → 動的」の順で積む
これが大原則です。先頭ほど絶対に動かない情報を置き、末尾に近づくほど動的なものを並べる。
[先頭]
└─ 不変: ロール定義、出力フォーマット指示、ツール定義、社内用語辞書
└─ 半固定: 会話履歴(直近N件)、RAGで引いたチャンク
└─ 動的: 今のuserメッセージ、現在時刻、リクエスト固有のID
[末尾]
この順番を守るだけで、ヒット率は一気に上がります。
原則2:ツール定義は最前面に置く
エージェント開発で見落としがちなのが、ツール定義 (function calling / tool use のスキーマ) は事実上の最大のキャッシュ対象 だということ。
ツールが10個ある場合、その JSON Schema を合計すると 5000〜10000トークン になるのは普通です。これを毎回再計算するのは、もったいない。必ず先頭で固定して、変えないようにします。
原則3:会話履歴は append-only で増やす(書き換えない)
マルチターン会話で、過去のメッセージを 要約に置き換えたり、削ったり、編集したり すると、その時点でプレフィックスが変わってキャッシュが全ミスします。
履歴は 「後ろに足すだけ」 が鉄則。圧縮したいなら、別経路で要約をsystemに移動するなど、設計を分けて考える必要があります。
原則4:user_id / タイムスタンプ / セッション固有情報は system に入れない
これ、わりとやらかしがちです。「セッション情報は system にまとめとこう」 と思って、session_id や request_timestamp を system プロンプトの先頭に入れた瞬間、そのユーザー・そのリクエスト固有のプレフィックスになって、他のリクエストとは絶対にヒットしない 状態が完成します。
ユーザー固有情報・時刻情報は、user メッセージの中、もしくは system プロンプトの末尾の動的ブロック に追い出してください。
原則5:「今日の日付」を system の先頭に入れない
これも罠です。
あなたは○○アシスタントです。今日は2026年5月28日です。 ← ダメ
これだと、日付が変わった瞬間、過去の全リクエストとのキャッシュが完全に切れます 。日付が必要なら、user メッセージ側か、system の最後の動的ブロック側 に持ってくる。これだけで翌日もキャッシュが効きます。
Before / After
| パターン | Before(キャッシュ死) | After(キャッシュ生) |
|---|---|---|
| 日付 | system先頭に 今日は{date}
|
user末尾に (現在時刻: {date})
|
| user_id | system先頭に 担当: {user_id}
|
user側で参照 |
| RAGチャンク | 毎回別順にシャッフル | 安定したID順で固定 |
| ツール定義 | リクエストごとに動的生成 | 起動時に固定、不変 |
4. 実装サンプル:Anthropic / OpenAI / Gemini
3社のSDKで、最短コードを並べてみます。「2回投げて、2回目の usage が変わればキャッシュ成功」のパターンです。
Anthropic SDK (Python)
import anthropic
client = anthropic.Anthropic()
LONG_SYSTEM = open("knowledge_base.md").read() # 10kトークン想定の固定文脈
def ask(question: str):
return client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": LONG_SYSTEM,
"cache_control": {"type": "ephemeral"}, # ここまでキャッシュ
}
],
messages=[{"role": "user", "content": question}],
)
resp1 = ask("社内ナレッジから経費規程を教えて")
print("write:", resp1.usage.cache_creation_input_tokens)
print("read :", resp1.usage.cache_read_input_tokens)
resp2 = ask("では出張規程は?")
print("write:", resp2.usage.cache_creation_input_tokens) # 0 になるはず
print("read :", resp2.usage.cache_read_input_tokens) # 大きい値になるはず
2回目の cache_creation_input_tokens が 0 、cache_read_input_tokens が 大きな数字 に変わっていれば成功です。
OpenAI SDK (Python)
OpenAIは コード変更が要らない 。cached_tokens を見に行くだけ。
from openai import OpenAI
client = OpenAI()
LONG_SYSTEM = open("knowledge_base.md").read()
def ask(question: str):
return client.chat.completions.create(
model="gpt-5.4",
messages=[
{"role": "system", "content": LONG_SYSTEM},
{"role": "user", "content": question},
],
)
r1 = ask("経費規程")
print("cached:", r1.usage.prompt_tokens_details.cached_tokens) # 1回目は0や小
r2 = ask("出張規程")
print("cached:", r2.usage.prompt_tokens_details.cached_tokens) # 2回目は大きく
ポイントは、system プロンプトを安定させて、user 側だけ変える こと。これで自動的にキャッシュが効きます。
Gemini SDK (Python, 明示キャッシュ)
from google import genai
from google.genai import types
client = genai.Client()
cache = client.caches.create(
model="gemini-2.5-pro",
config=types.CreateCachedContentConfig(
system_instruction="あなたは社内ナレッジ案内です",
contents=[open("knowledge_base.md").read()],
ttl="3600s", # 1時間
),
)
def ask(question: str):
return client.models.generate_content(
model="gemini-2.5-pro",
contents=question,
config=types.GenerateContentConfig(cached_content=cache.name),
)
r = ask("経費規程")
print("cached tokens:", r.usage_metadata.cached_content_token_count)
暗黙キャッシュだけでも済むケースは多いですが、1日に何百回も同じ巨大ドキュメントを引く ならば明示キャッシュにして TTL を長めに取るほうが安定します。
5. キャッシュすべき/してはいけないものの判定
「全部キャッシュしとけ」は、罠です。
1回しか使わない長文に cache_control を付けると、書き込みコストの分だけ逆に高くつく 。Anthropic の場合、5分TTLで +25% 、1時間TTLで +100% の書き込みコストがかかります。これを回収できないと損になる。
1行ルール
同じ前置きを 3〜4回以上使い回すならキャッシュ。1〜2回ならキャッシュしない。
これで9割正解です。
詳細なブレイクイーブン式
ちゃんと計算したい人向けに。
ブレイクイーブン回数 = (書き込みコスト − 読み込みコスト) ÷ (通常コスト − 読み込みコスト)
例:Anthropic 5分TTLの場合、書き込み=1.25 / 読み込み=0.1 / 通常=1 とおいて、
(1.25 − 0.1) ÷ (1 − 0.1) = 1.28 回
→ 2回以上同じ前置きを使うだけで、もう元が取れる 計算になります。だから「3〜4回以上」ルールは安全側に振った目安、ということなんです。
判定表
| 対象 | キャッシュすべきか | 理由 |
|---|---|---|
| system プロンプト(ロール・出力規定) | ✅ 必須 | リクエストごとに不変 |
| ツール定義 (JSON Schema 群) | ✅ 必須 | 起動時固定、トークン量が重い |
| RAGで引いた共通チャンク | ⚠️ 条件付き | 安定順序を保てるなら |
| 会話履歴 (直近N件) | ✅ 推奨 | append-onlyで増えるなら効く |
| ユーザー個別プロフィール | ❌ 不要 | リクエスト固有、ヒットしない |
| 1回限りの長文ドキュメント | ❌ 危険 | 書き込みコストが回収できない |
| 今日の日付・現在時刻 | ❌ 禁止 | 動的、しかも先頭に置きがち |
6. ヒット率を見る:最小ダッシュボード3行
効果が見えてないものは、続きません。
まずは「ヒット率1個」だけダッシュボードに出すところから始めれば十分です。
最小実装 (Python)
def hit_rate(usage) -> float:
read = getattr(usage, "cache_read_input_tokens", 0) or 0
write = getattr(usage, "cache_creation_input_tokens", 0) or 0
normal = usage.input_tokens
total_cached_eligible = read + write
if total_cached_eligible == 0:
return 0.0
return read / (read + write + normal)
これを Prometheus や Datadog のメトリクスに飛ばすだけで、機能別・モデル別の hit rate が見えるようになります。
OTel GenAI semantic conventions に乗せる
少し進んだ運用としては、OpenTelemetry GenAI Semantic Conventions の gen_ai.usage.input_tokens と並べて、独自属性で gen_ai.usage.cache_read_tokens を span に載せると、Observability基盤と統合できます。
span.set_attribute("gen_ai.usage.input_tokens", usage.input_tokens)
span.set_attribute("gen_ai.usage.cache_read_input_tokens",
getattr(usage, "cache_read_input_tokens", 0))
しきい値
実運用での感覚として、
- hit rate 80% 以上: かなり良い状態。設計が綺麗
- hit rate 50〜80%: 動的ブロックの位置を見直す余地あり
- hit rate 30%未満: 先頭に動的データが混入している可能性大、要レビュー
を目安にすれば、アラート設計の出発点になります。
7. 落とし穴:これをやると逆に高くなる/漏れる
ここ、たぶんこの記事で一番大事なところです。
まず大前提として、キャッシュは同一APIキー内・同一プロジェクト内でしかヒットしません 。共有プールで他社のプレフィックスと衝突して情報が漏れる、ということはありません。3社とも公式に明言しています。
その上で、踏みやすい地雷をまとめます。
| 症状 | 想定原因 | 確認コマンド/観点 |
|---|---|---|
cached_tokens が常に 0 |
プロンプト先頭に動的データ混入 | system先頭をdiffで比較、可変要素を探す |
| 月初だけ突然コスト跳ねた | TTL切れの後、書き込みが集中 |
cache_creation_input_tokens の時系列を可視化 |
| 同じ system でもヒットしない | breakpoint 位置が末尾すぎる | Anthropic: cache_control を「使い回す塊」の直後に置く |
| Anthropic 課金が想定より高い | 1時間TTL選択+使い切らず | TTL 5分で十分か再評価 |
| Gemini で実装したが効果薄い | 暗黙キャッシュの prefix が短い | 大きく安定した内容を冒頭に移動 |
| 機密情報のリスクが心配 | system にPII長期間滞在 | TTL短縮+PII除去レイヤーを前段に |
罠1:1回しか使わない長文に cache_control を付ける
書き込みコストが乗るだけで、回収できません。3回以上使い回す確信がない限り付けない 。
罠2:先頭にタイムスタンプ・user_id
原則4で書いたとおり、毎リクエストミスします。動的なものは末尾へ 。
罠3:PII を system に長文で埋め込む
キャッシュ層に短時間とはいえデータが残ります。ログ・キャッシュの双方で PII境界の前段マスキング をかけるのが安全です。
罠4:A/B テストで違うプロンプトを同じ user に混ぜる
バリアント A と B が交互に来ると、書き込みばかり走って読み込みが少ない状態になります。A/B は user 単位で固定 するのが原則です。
8. 人間とAIの役割分担
役割分担を表で。
| やること | 人間 | AI |
|---|---|---|
| プロンプトの並び順設計 | ◎ | サポート |
| TTL選択 (5分/1時間/明示) | ◎ | - |
| PII境界の判断 | ◎ | - |
| ヒット率レポートの異常検出 | 監督 | ◎ |
cache_creation 急増の原因仮説 |
監督 | ◎ |
| 並び替えのリファクタ案出し | 監督 | ◎ |
プロンプト例1:キャッシュ並び替えレビュー
あなたはLLM API最適化のレビュアです。
以下のpromptを「先頭=固定 / 中=半固定 / 末尾=動的」の3層に分類し、
prefix一致を壊している箇所を指摘し、並び替え案をunified diff形式で出してください。
# prompt
{{prompt_text}}
プロンプト例2:異常 cache_read レポート要約
直近24時間のcache_read_tokens時系列CSVを渡します。
- 異常な低下区間を検出し、開始時刻と継続時間
- 同時刻のdeploy・config変更との相関仮説
- 次に取るべき調査アクション3個
を箇条書きで返してください。
# data
{{csv}}
プロンプト例3:「これキャッシュすべき?」判定
次のpromptブロックについて、
- 推定トークン数
- リクエストあたり再利用見込み回数
- ブレイクイーブン回数(書き込み+25%, 読み込み90%offで計算)
- 結論(キャッシュすべき / すべきでない / 判断保留)
- 理由
を返してください。
# block
{{block_text}}
人間がやるのは 「並びの設計」と「境界の判断」 、AIにやらせるのは 「観測と仮説と並び替え提案」 。この分担に落とすと、運用が回ります。
9. キャッシュ設計は、明日の自分への前送り資産
最後に、思想の話をひとつ。
Prompt Caching って、単なるコスト削減機能 に見えるんですけど、しばらく付き合ってみると、これは 「設計の鏡」 なんですよね。
同じ前置きを長く使い回せるってことは、それだけ 「変わらないもの」と「変わるもの」が綺麗に分かれている ということ。逆に、キャッシュが全然効かないアプリは、たいてい中も雑然としています。動的なものと固定のものがごちゃ混ぜで、誰がいつ何を書いたか分からなくなっている。
つまりキャッシュ設計は、コードの整理整頓と同じ作業 をプロンプト側でやっているだけ、とも言えます。
そして大事なのは、この整理は1回やれば済む ということ。今日 system プロンプトの先頭に潜んでいた 今日の日付 を1行下に動かすだけで、来月の請求書がスッと半分になる。今日の自分の3分が、来月の自分への配当として返ってくる。
これって、コードと資本の関係に少し似てる なと思うんです。1回ちゃんと設計したコードが、その後ずっと自分の代わりに働いてくれる。手を動かすたびに対価が出るわけじゃなく、ちゃんと整えた構造が、毎リクエストごとに静かに割引を吐き続ける。
明日の自分が、過去の自分にあざっすって言えるかどうか。それを決めるのは、今日「動的なものを末尾に置き直すかどうか」、それだけです。
今週の最初の一歩
最後に、読み終わったあと最初に動いてもらうなら、これだけお願いしたいです。
- 自分のアプリの system プロンプトを開く
- 先頭に動的な要素(日付・user_id・タイムスタンプ)が混入してないか確認する
- 混ざってたら、末尾の動的ブロックに移動する
- 2回同じリクエストを投げて、
cached_tokensが増えるか確認する
これだけで、まず確実に効果が出ます。本格的なダッシュボード化やOTel連携は、効果を体感してからで全然遅くないので、まずは一番手前の3行から動かしてみてください。
それでは、来月の請求書を一緒に半分にしましょう。