0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

毎月のAI API代、減らせるところまだ残ってませんか? — Prompt Cachingで月額を1/3〜1/10にする実践ガイド(Anthropic / OpenAI / Gemini 3社比較)

0
Posted at

毎月の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_idrequest_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_tokens0cache_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回ちゃんと設計したコードが、その後ずっと自分の代わりに働いてくれる。手を動かすたびに対価が出るわけじゃなく、ちゃんと整えた構造が、毎リクエストごとに静かに割引を吐き続ける。

明日の自分が、過去の自分にあざっすって言えるかどうか。それを決めるのは、今日「動的なものを末尾に置き直すかどうか」、それだけです。

今週の最初の一歩

最後に、読み終わったあと最初に動いてもらうなら、これだけお願いしたいです。

  1. 自分のアプリの system プロンプトを開く
  2. 先頭に動的な要素(日付・user_id・タイムスタンプ)が混入してないか確認する
  3. 混ざってたら、末尾の動的ブロックに移動する
  4. 2回同じリクエストを投げて、cached_tokens が増えるか確認する

これだけで、まず確実に効果が出ます。本格的なダッシュボード化やOTel連携は、効果を体感してからで全然遅くないので、まずは一番手前の3行から動かしてみてください。

それでは、来月の請求書を一緒に半分にしましょう。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?