Gemini APIのコスト請求を見て「先月より倍になっている」と焦った経験は、API開発者なら一度はあるのではないでしょうか。私が運営するAIサービスで月の請求が3万円を超えたとき、最初に疑ったのはトークン数の増加でした。でも実際の問題は全く別のところにありました。
同じ長文のシステムプロンプト(約15,000トークン)を、毎回のリクエストで送信し続けていたのです。ユーザーが一言「今日の天気は?」と聞くだけで、裏側では15,000トークン以上のコンテキストが毎回送られていました。
ここで扱うのはそのときに学んだGemini APIのキャッシュ設計を体系的にまとめます。Context Cachingの導入からImplicit Cachingの活用まで、実際に動くコードと一緒に解説します。
Gemini API にある3種類の「キャッシュ」を整理する
まず混乱しがちな用語を整理しましょう。Gemini APIには主に3つのキャッシュの概念があります。
Context Caching(コンテキストキャッシュ): 自分で明示的にキャッシュを作成する機能です。長いシステムプロンプトや参照ドキュメントをあらかじめキャッシュしておき、後続のリクエストで使い回します。キャッシュの保存にはストレージコストがかかりますが、キャッシュヒット時の入力トークン料金が大幅に安くなります。
Implicit Caching(暗黙的キャッシュ): Googleが自動で行うキャッシュです。同一または酷似するプロンプトのプレフィックスを検出し、自動的にキャッシュを適用します。設定不要で、ユーザーは意識しなくてもキャッシュの恩恵を受けられます(ただし確実に効くとは限りません)。
アプリケーション層のKVキャッシュ: RedisやCloudflare KVを使い、レスポンス全体をキャッシュするアプリケーション側の工夫です。全く同一のリクエストに対してAPIを呼ばずにキャッシュ済みレスポンスを返します。
ここで扱うのはContext CachingとImplicit Cachingを深掘りします。この2つを正しく理解するだけで、コストは劇的に変わります。
Context Caching:意図的にキャッシュして確実にコストを下げる
いつContext Cachingを使うべきか
Context Cachingが効果的なのは、同じ大きなコンテキストを複数のリクエストで繰り返し使う場合です。具体的には:
- 長いシステムプロンプト(5,000トークン以上が目安)
- 参照ドキュメント(マニュアル、仕様書、FAQなど)
- Few-shotの例(多数のサンプルを毎回送っている場合)
- コードベース全体をコンテキストに含めるツール
逆に、リクエストごとに内容が変わるコンテキスト(ユーザー入力、リアルタイムデータなど)はキャッシュの恩恵を受けられません。
コスト削減の実際の計算
Context Cachingのコストは主に2つです:
ストレージコスト:キャッシュ1トークンあたり $0.000001/時間(Gemini 2.5 Pro の場合)
キャッシュヒット時の入力コスト:通常入力の約25%
例えば15,000トークンのシステムプロンプトを1日100回使う場合:
通常の場合:
15,000 tokens × 100 req × $0.00000125/token = $1.875/日 ≈ 月5,600円
Context Cachingを使う場合:
ストレージ:15,000 × 24時間 × $0.000001 = $0.36/日(1日保持)
ヒット時入力:15,000 × 100 × $0.00000125 × 0.25 = $0.469/日
合計:$0.829/日 ≈ 月2,500円(約55%削減)
リクエスト数が多いほど、また固定コンテキストが長いほど削減率は上がります。
Context Cachingの基本実装
import google.generativeai as genai
import time
from google.generativeai import caching
# APIキーの設定
genai.configure(api_key="YOUR_GEMINI_API_KEY")
# キャッシュするシステムコンテキスト(例:長大なFAQドキュメント)
LARGE_SYSTEM_CONTEXT = """
あなたは当社のカスタマーサポートAIです。
以下の製品マニュアルと FAQ を参照して回答してください。
[製品マニュアル - 全15,000トークン相当のコンテンツ]
... (ここに大量のドキュメントが入る)
"""
def create_context_cache(content: str, ttl_seconds: int = 3600) -> caching.CachedContent:
"""コンテキストキャッシュを作成する"""
cache = caching.CachedContent.create(
model="models/gemini-2.5-pro",
system_instruction=content,
ttl=f"{ttl_seconds}s",
display_name="support-context-cache"
)
print(f"✅ キャッシュ作成: {cache.name}")
print(f" 有効期限: {cache.expire_time}")
return cache
def get_or_create_cache(content: str) -> caching.CachedContent:
"""既存のキャッシュを再利用するか、新規作成する"""
# 既存のキャッシュを検索
for cached in caching.CachedContent.list():
if cached.display_name == "support-context-cache":
print(f"♻️ 既存キャッシュを再利用: {cached.name}")
return cached
# なければ新規作成
return create_context_cache(content)
def chat_with_cache(cache: caching.CachedContent, user_message: str) -> dict:
"""キャッシュを使ってGeminiにリクエストする"""
model = genai.GenerativeModel.from_cached_content(cached_content=cache)
response = model.generate_content(user_message)
# トークン使用量の確認
usage = response.usage_metadata
return {
"text": response.text,
"cached_tokens": usage.cached_content_token_count,
"input_tokens": usage.prompt_token_count,
"output_tokens": usage.candidates_token_count,
}
# 実際の使用例
cache = get_or_create_cache(LARGE_SYSTEM_CONTEXT)
questions = [
"返品ポリシーを教えてください",
"配送にどれくらいかかりますか?",
"会員登録はどうすればいいですか?"
]
for q in questions:
result = chat_with_cache(cache, q)
print(f"\n質問: {q}")
print(f"回答: {result['text'][:100]}...")
print(f"キャッシュトークン: {result['cached_tokens']} / 入力トークン合計: {result['input_tokens']}")
# cached_tokensが大きいほどコスト削減効果が出ている
このコードで注目すべきは get_or_create_cache() 関数です。プロセスを再起動しても既存のキャッシュを検索して再利用するため、キャッシュの作り直しによる無駄なコストを防げます。
TTL(有効期限)の適切な設計
TTLの設定は思ったより重要です。短すぎるとキャッシュが頻繁に失効してストレージコスト+再作成のオーバーヘッドが発生します。長すぎると古いコンテンツが参照され続けます。
from datetime import datetime, timezone
def calculate_optimal_ttl(daily_request_count: int, cache_creation_cost_tokens: int) -> int:
"""
最適なTTLを計算する。
リクエスト数が少ない夜間はキャッシュを維持しない方がコスト効率が良い場合も。
"""
current_hour = datetime.now(timezone.utc).hour
# ピーク時間(JST 9時〜22時 = UTC 0時〜13時)
is_peak_hours = 0 <= current_hour <= 13
if is_peak_hours and daily_request_count > 50:
# ピーク時は長めのTTL
return 7200 # 2時間
elif daily_request_count > 10:
return 3600 # 1時間
else:
# リクエストが少ない場合はキャッシュをあまり長く持たない
return 1800 # 30分
# Context Cachingの最小トークン要件を確認する
def check_cache_eligibility(content: str) -> tuple[bool, str]:
"""キャッシュ対象として適切かチェックする"""
# Gemini 2.5 Pro のContext Cachingは最低32,768トークン必要
# 簡易推定(実際は genai.count_tokens() を使う)
estimated_tokens = len(content) // 4 # 概算
if estimated_tokens < 32768:
return False, f"トークン数不足: 約{estimated_tokens}トークン(最低32,768必要)"
return True, f"キャッシュ適用可能: 約{estimated_tokens}トークン"
注意が必要なのは、Gemini 2.5 ProのContext Cachingには最低32,768トークンという制限があります(モデルによって異なります)。これを満たさないと 400 INVALID_ARGUMENT エラーになります。これが「キャッシュを設定したのに効かない」原因の一つです。
Implicit Caching:設定不要の自動最適化を理解して活用する
Implicit Cachingは2025年末にGemini 2.5 Proで正式サポートされた機能で、リクエストのプレフィックス(前半部分)が一致する場合に自動でキャッシュを適用します。
Implicit Cachingが効く条件
Implicit Cachingは以下の条件が揃ったときに効果を発揮します:
- 同じモデルへのリクエスト(モデルが変わるとキャッシュは効かない)
- プレフィックスの一致(リクエストの先頭部分が同じ)
- 十分なトークン数(最低32,768トークン程度)
- 短い間隔(Google側のキャッシュ保持期間内)
重要なのは「プロンプトの順序」です。Gemini APIは先頭から一致するプレフィックスをキャッシュするため、変化する部分は後ろに配置するのが鉄則です。
# ❌ 良くない順序:変化する部分が先頭にある
BAD_PROMPT_ORDER = """
ユーザーID: {user_id}
タイムスタンプ: {timestamp}
[ここに長大なシステムコンテキスト]
"""
# ✅ 良い順序:固定部分を先頭にまとめる
GOOD_PROMPT_ORDER = """
[ここに長大なシステムコンテキスト]
ユーザーID: {user_id}
タイムスタンプ: {timestamp}
"""
変化する情報(ユーザーIDやタイムスタンプ)が先頭にあると、毎回プレフィックスが変わるためImplicit Cachingは機能しません。
Implicit Cachingのヒット状況を確認する
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel("gemini-2.5-pro")
# 固定コンテキストを先頭に、変化する部分を末尾に
FIXED_CONTEXT = """
あなたはPythonの専門家です。以下のコーディング規約に従ってコードをレビューしてください。
[コーディング規約 - 30,000トークン相当]
1. 変数名はsnake_caseを使用する
2. 関数は単一責任の原則に従う
3. 型ヒントを必ず記述する
... (大量の規約が続く)
"""
def review_code_with_implicit_cache(user_code: str) -> dict:
"""コードレビュー - Implicit Cachingが効くよう設計"""
# 固定部分を先頭に、変化するコードを末尾に配置
full_prompt = f"{FIXED_CONTEXT}\n\n以下のコードをレビューしてください:\n\n```python\n{user_code}\n```"
response = model.generate_content(
full_prompt,
generation_config={"temperature": 0}
)
usage = response.usage_metadata
hit_rate = 0
if usage.prompt_token_count > 0:
hit_rate = usage.cached_content_token_count / usage.prompt_token_count * 100
return {
"review": response.text,
"total_input_tokens": usage.prompt_token_count,
"cached_tokens": usage.cached_content_token_count,
"cache_hit_rate": f"{hit_rate:.1f}%",
"billing_tokens": usage.prompt_token_count - usage.cached_content_token_count,
}
# 連続したリクエストでImplicit Cachingの効果を確認
code_samples = [
"def get_user(id):\n return db.query(f'SELECT * FROM users WHERE id={id}')",
"class UserManager:\n def __init__(self):\n self.users = []",
"import os\nAPI_KEY = os.getenv('API_KEY')",
]
for i, code in enumerate(code_samples):
result = review_code_with_implicit_cache(code)
print(f"\n[リクエスト {i+1}]")
print(f" 総入力トークン: {result['total_input_tokens']}")
print(f" キャッシュヒット: {result['cached_tokens']} トークン({result['cache_hit_rate']})")
print(f" 課金対象トークン: {result['billing_tokens']}")
2回目以降のリクエストで cached_content_token_count が増えていれば、Implicit Cachingが機能しています。ただし、Googleのサーバー側の状況によるため、毎回確実にヒットするとは限りません。確実なキャッシュが必要なら Context Caching を使うのが原則です。
キャッシュが効かない:本番でよく遭遇する5つの原因
理論上はキャッシュされるはずなのに、コストが下がらありません。そういうケースで実際に多かった原因をまとめます。
原因1: モデルのバリアントが変わっている
# ❌ キャッシュが作られたモデルと違うバリアントを使っている
cache = caching.CachedContent.create(
model="models/gemini-2.5-pro", # キャッシュ作成時
...
)
# 別のコードで
model = genai.GenerativeModel("gemini-2.5-pro-latest") # ← バリアントが違う!
gemini-2.5-pro と gemini-2.5-pro-latest は別モデルとして扱われます。キャッシュ作成とリクエストで必ず同じモデル文字列を使いましょう。
原因2: コンテンツの微妙な変化
# ❌ タイムスタンプや動的な値をシステムプロンプトに含めている
system_prompt = f"""
現在時刻: {datetime.now().isoformat()} # ← 毎回変わる!
あなたはサポートAIです...
"""
動的な情報をキャッシュ対象のコンテキストに混ぜると、キャッシュは毎回ミスヒットします。固定情報と動的情報を明確に分離してください。
原因3: TTL切れのキャッシュを参照しようとしている
def safe_cache_request(cache_name: str, user_message: str, fallback_content: str):
"""TTL切れを安全にハンドリングする"""
try:
cache = caching.CachedContent.get(cache_name)
model = genai.GenerativeModel.from_cached_content(cached_content=cache)
return model.generate_content(user_message)
except Exception as e:
if "INVALID_ARGUMENT" in str(e) or "not found" in str(e).lower():
print("⚠️ キャッシュ期限切れ。再作成します...")
cache = create_context_cache(fallback_content)
model = genai.GenerativeModel.from_cached_content(cached_content=cache)
return model.generate_content(user_message)
raise
TTL設定を短くしすぎると本番でこのエラーが頻発します。適切なTTLと、期限切れ時の再作成ロジックをセットで実装してください。
原因4: Implicit Cachingの最小トークン数を満たしていない
Context Cachingと同様に、Implicit Cachingにも最低トークン数の閾値があります。短いシステムプロンプト(数百トークン程度)でImplicit Cachingを期待しても効果はありません。
診断方法として、usage_metadata.cached_content_token_count が常に0であれば、プロンプトが短すぎるか、プレフィックスが一致していない可能性が高いです。
原因5: 非同期処理でリクエスト間隔が長すぎる
バッチ処理でリクエストを一定間隔で投げる場合、Google側のImplicit Cacheの保持時間(数分程度と言われています)を超えると、後続リクエストでキャッシュが効きません。
Implicit Cachingを狙うなら、短時間に集中してリクエストを投げるか、確実性が必要ならContext Cachingを使うべきです。
3つの実サービスシナリオで見るコスト削減の計算
実際にどれほど変わるのか、具体的な数字で見てみましょう。
シナリオA:チャットボットサービス(月50,000リクエスト)
固定システムプロンプト: 20,000トークン
平均ユーザー入力: 200トークン
平均レスポンス: 500トークン
キャッシュなし:
入力: (20,000 + 200) × 50,000 × $0.00000125 = $1,262/月
Context Caching(1時間TTL):
ストレージ: 20,000 × 720時間 × $0.000001 = $14.4/月
ヒット入力: 20,000 × 50,000 × $0.000000313 = $313/月(25%料金)
通常入力: 200 × 50,000 × $0.00000125 = $12.5/月
合計: $339.9/月 → 約73%削減
シナリオB:ドキュメント分析ツール(月10,000リクエスト)
参照マニュアル: 50,000トークン
クエリ: 500トークン/リクエスト
レスポンス: 1,000トークン/リクエスト
キャッシュなし:
入力: (50,000 + 500) × 10,000 × $0.00000125 = $631/月
Context Caching:
ストレージ: 50,000 × 720 × $0.000001 = $36/月
ヒット入力: 50,000 × 10,000 × $0.000000313 = $156.5/月
通常入力: 500 × 10,000 × $0.00000125 = $6.25/月
合計: $198.75/月 → 約69%削減
シナリオC:コードレビューツール(月30,000リクエスト)
Implicit Cachingが活きるシナリオです。
コーディング規約: 40,000トークン(固定・先頭)
レビュー対象コード: 1,000トークン/リクエスト(可変・末尾)
理想的なImplicit Cachingヒット率70%の場合:
通常課金: 1,000 × 30,000 × $0.00000125 = $37.5/月
キャッシュヒット分(70%)の入力削減: 40,000 × 21,000 × $0.000000938 = $787.9 → $0(ヒット時は安い)
実際は40,000 × 21,000 × $0.000000313 = $262.9/月
非ヒット分(30%): 41,000 × 9,000 × $0.00000125 = $461.25/月
合計: 約$724/月(vs キャッシュなし: $1,537.5/月)→ 約53%削減
これらの計算で分かるように、リクエスト数が多く、固定コンテキストが長いほど、キャッシュの効果は劇的になります。
キャッシュヒット率を継続的に監視するモニタリング設計
コスト削減の効果を確認し続けるには、監視の仕組みが欠かせません。
import json
import time
from dataclasses import dataclass, field
from collections import defaultdict
@dataclass
class CacheMetrics:
total_requests: int = 0
total_cached_tokens: int = 0
total_input_tokens: int = 0
total_cost_usd: float = 0.0
estimated_savings_usd: float = 0.0
hourly_stats: dict = field(default_factory=lambda: defaultdict(lambda: {
"requests": 0, "cached_tokens": 0, "input_tokens": 0
}))
# Gemini 2.5 Proの料金(2026年5月時点)
PRICING = {
"input_per_token": 0.00000125, # $1.25 / 1M tokens
"cached_input_per_token": 0.000000313, # $0.3125 / 1M tokens(約25%)
"output_per_token": 0.000005, # $5 / 1M tokens
}
metrics = CacheMetrics()
def track_and_generate(model, prompt: str) -> str:
"""メトリクスを収集しながらGeminiを呼ぶ"""
response = model.generate_content(prompt)
usage = response.usage_metadata
# メトリクス更新
cached = usage.cached_content_token_count or 0
total_input = usage.prompt_token_count or 0
non_cached_input = total_input - cached
# コスト計算
actual_cost = (
non_cached_input * PRICING["input_per_token"] +
cached * PRICING["cached_input_per_token"] +
(usage.candidates_token_count or 0) * PRICING["output_per_token"]
)
hypothetical_cost = (
total_input * PRICING["input_per_token"] +
(usage.candidates_token_count or 0) * PRICING["output_per_token"]
)
savings = hypothetical_cost - actual_cost
metrics.total_requests += 1
metrics.total_cached_tokens += cached
metrics.total_input_tokens += total_input
metrics.total_cost_usd += actual_cost
metrics.estimated_savings_usd += savings
hour_key = time.strftime("%Y-%m-%d %H:00")
metrics.hourly_stats[hour_key]["requests"] += 1
metrics.hourly_stats[hour_key]["cached_tokens"] += cached
return response.text
def print_cost_report():
"""コスト削減レポートを出力"""
if metrics.total_requests == 0:
return
hit_rate = metrics.total_cached_tokens / max(metrics.total_input_tokens, 1) * 100
print("\n=== Gemini API キャッシュ効果レポート ===")
print(f"総リクエスト数: {metrics.total_requests:,}")
print(f"キャッシュヒット率: {hit_rate:.1f}%")
print(f"実際のAPI費用: ${metrics.total_cost_usd:.4f}")
print(f"推定節約額: ${metrics.estimated_savings_usd:.4f}")
print(f"節約率: {metrics.estimated_savings_usd / max(metrics.total_cost_usd + metrics.estimated_savings_usd, 0.0001) * 100:.1f}%")
このモニタリングの仕組みを本番環境に組み込むことで、「今月のキャッシュ効果はどれくらいか」を常に把握できます。キャッシュヒット率が突然下がった場合はプロンプト構造の変化や、キャッシュのTTL設定の問題を疑いましょう。
最適なキャッシュ戦略の選択フロー
最後に、どのキャッシュを選ぶべきかの判断フローをまとめます。
固定コンテキストが32,768トークン以上あるか?
→ YESなら Context Caching を最優先検討
リクエストが1日50件以下か?
→ YESならContext Cachingのストレージコストがリクエスト削減効果を上回る可能性。Implicit Cachingか、そもそもキャッシュが必要ない可能性も
コンテキストは固定か頻繁に変わるか?
→ 月1回程度の更新ならContext Caching。日次更新ならImplicit Caching
同一ユーザーが短期間に似た内容を繰り返し質問するか?
→ アプリケーション層のKVキャッシュが最も効果的な場合がある
この判断フローを組み合わせることで、過剰なキャッシュ設計(ストレージコストが増える)と、キャッシュを使わない機会損失の両方を避けられます。
個人開発者の視点から(実体験メモ)
月3万円を6,000円にした、実際の設計変更まとめ
冒頭の話に戻ります。私が月3万円のAPIコストを6,000円に削減したときの具体的な変更点は以下の3つでした:
- 15,000トークンのシステムプロンプトをContext Cachingに移行(約65%コスト削減)
- プロンプト構造を「固定コンテキスト先頭・動的入力末尾」に統一(Implicit Cachingの恩恵も追加で享受)
- コスト監視スクリプトを組み込み、ヒット率を週次でチェック
最初のContext Caching導入だけで、1週間以内に劇的な変化が現れました。もし今、Gemini APIのコストが高くなってきていると感じているなら、まず usage_metadata.cached_content_token_count を確認することから始めてみてください。もしこの値が0であれば、改善の余地が大きく残っています。
キャッシュ設計は一度実装すれば長期間効き続ける「地味だが確実な」コスト削減手法です。派手な新機能ではありませんが、持続可能なAPIサービスを構築するためには避けて通れない基礎技術のひとつだと感じています。