0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】RAGでLLMのプロンプトキャッシュが効かない(ヒット率0%)原因とClaude・OpenAI・Gemini別実装パターン

0
Posted at

はじめに

RAG(検索拡張生成)にプロンプトキャッシュを導入した際、「APIの cached_tokens(または cache_read_input_tokens)が常に 0 になる」という事象が頻発します。

本記事では、RAG環境でキャッシュヒット率が 0.0% になる3つの原因を解説します。あわせて、Claude・OpenAI・Gemini の3社で2ターン目以降のヒット率を94.8%〜96.0%に引き上げるPython実装コードを紹介します。

本記事の検証コードおよび再現用CLIツール一式は、以下のGitHubリポジトリで公開しています。


1. 前提環境と各社最小トークン閾値

プロンプトキャッシュが発動するためには、固定プレフィックス部分が各プロバイダの定める最小トークン数を超えている必要があります。

プロバイダ 使用Python SDK 対象モデル例 最小トークン閾値 キャッシュ読取割引率
Anthropic anthropic (>=0.49.0) claude-sonnet-5-5 / claude-sonnet-4-6 512 〜 4,096 tok 90% 〜 95% 割引
OpenAI openai (>=1.66.0) gpt-6.1-sol / gpt-5 / gpt-4.1 1,024 tok(128刻み) 50% 〜 95% 割引
Google Gemini google-genai (>=1.5.0) gemini-3.8-flash / gemini-3.1-pro 1,024 〜 4,096 tok 90% 割引

2. 原因①:プロンプト先頭への「動的要素」の混入(前方不一致)

3社すべてのプロンプトキャッシュは、「プロンプト先頭からのトークン完全一致(Prefix Match)」を条件としています。
そのため、先頭に「現在時刻(datetime.now())」「リクエストUUID」「毎ターン変わるRAG検索結果」を置くと、後ろの固定規定(数千トークン)は毎回100%キャッシュミスになります。

なぜRAGでプロンプトキャッシュが効かないのか?(配置順と前方一致の仕組み)

NGな組み立て順序(bad_prefix:ヒット率 0.0%)

# ❌ NG例:先頭に現在時刻や動的RAGチャンクを置くと、後続の固定規定がすべてキャッシュミスになる
def build_bad_prompt(static_manual: str, dynamic_rag_chunk: str, user_question: str) -> str:
    return (
        f"### 検索コンテキスト (取得時刻: {datetime.now().isoformat()})\n"
        f"{dynamic_rag_chunk}\n\n"
        f"### 全社共通規定マニュアル (約4,000トークン)\n"
        f"{static_manual}\n\n"
        f"### 質問\n{user_question}"
    )

OKな組み立て順序(good_prefix:2ターン目以降ヒット率 95%超)

不変の共通規定やシステム指示を最前方に固定し、毎ターン変化するRAG検索結果や日時・ユーザー質問は必ず末尾へ配置します。

# ⭕ OK例:不変の固定マニュアルを先頭に固定し、動的要素をすべて後方へ分離する
def build_good_prompt(static_manual: str, dynamic_rag_chunk: str, user_question: str) -> str:
    return (
        f"### 全社共通規定マニュアル (約4,000トークン)\n"
        f"{static_manual}\n\n"
        f"### 検索コンテキスト (取得時刻: {datetime.now().isoformat()})\n"
        f"{dynamic_rag_chunk}\n\n"
        f"### 質問\n{user_question}"
    )

3. 原因②:プロバイダ別のAPI呼び出し作法の不一致(3社別実装コード)

プロンプトの順序を good_prefix に揃えた上で、各プロバイダ固有のキャッシュパラメータを正しく指定する必要があります。

主要LLM 3社4方式 プロンプトキャッシュ実測スコアボード(5ターン検証)

(A) Anthropic Claude:明示的 cache_control ブレークポイントの指定

Anthropicの messages.create では、content をブロック配列として渡します。そして先頭に配置した固定コンテキストブロックの末尾に "cache_control": {"type": "ephemeral"} を明示指定します。

import anthropic

client = anthropic.Anthropic()

def ask_claude_with_cache(
    static_manual: str,
    dynamic_rag_chunk: str,
    user_question: str,
    model: str = "claude-sonnet-5-5",
):
    # 1. 固定規定ブロックを先頭に置き、ephemeral ブレークポイントを付与
    content_blocks = [
        {
            "type": "text",
            "text": static_manual,
            "cache_control": {"type": "ephemeral"},
        },
        {
            "type": "text",
            "text": dynamic_rag_chunk,
        },
        {
            "type": "text",
            "text": f"### User Question\n{user_question}",
        },
    ]

    response = client.messages.create(
        model=model,
        max_tokens=256,
        messages=[{"role": "user", "content": content_blocks}],
    )

    usage = response.usage
    print(
        f"Base Input: {usage.input_tokens}, "
        f"Cache Write: {usage.cache_creation_input_tokens}, "
        f"Cache Read (HIT): {usage.cache_read_input_tokens}"
    )
    return response
  • 実測値(5ターン検証): Turn 1で 6,518 tok が書込された後、Turn 2〜5では毎回 6,518 tok(95.8%)がヒットし、累計コストが -64.29% 削減されました。

(B) OpenAI:Chat Completions の罠と Responses API への移行

OpenAIで陥りやすいのが、従来の chat.completions.create に単一の user メッセージ文字列として全文を渡してしまうケースです。この方法では、先頭が一致していても毎回キャッシュ書込(cache_write_tokens)ばかりが発生し、読取ヒット(cached_tokens)が 0 になる現象が起きます。

OpenAI Platform Usage ダッシュボードでのキャッシュ稼働内訳

確実にキャッシュヒットさせるには、Responses API(client.responses.create) を使用し、以下の3点を指定します。

  1. instructions に固定マニュアルを分離して渡す。
  2. input に動的RAGチャンクとユーザー質問を渡す。
  3. prompt_cache_key に固定のルーティング識別子を指定し、同一キャッシュ保持サーバへリクエストを誘導する。
import openai

client = openai.OpenAI()

def ask_openai_responses_with_cache(
    static_manual: str,
    dynamic_rag_chunk: str,
    user_question: str,
    model: str = "gpt-6.1-sol",
):
    response = client.responses.create(
        model=model,
        instructions=static_manual,  # 固定コンテキストを instructions に分離
        input=f"{dynamic_rag_chunk}\n\n### User Question\n{user_question}",
        prompt_cache_key=f"rag_manual_v1_{model}",  # 固定キーでサーバアフィニティを維持
        prompt_cache_retention="24h",
        max_output_tokens=256,
    )

    usage = response.usage
    details = getattr(usage, "input_tokens_details", None)
    cached_tokens = getattr(details, "cached_tokens", 0) or 0
    cache_write_tokens = getattr(details, "cache_write_tokens", 0) or 0
    # OpenAIは input_tokens 総数に cached 分が含まれるため減算して通常入力分を算出
    base_input_tokens = max(0, usage.input_tokens - cached_tokens - cache_write_tokens)

    print(
        f"Base Input: {base_input_tokens}, "
        f"Cache Write: {cache_write_tokens}, "
        f"Cache Read (HIT): {cached_tokens}"
    )
    return response
  • 実測値(5ターン検証): chat.completions.create 単発結合ではヒット率が 0.0% でした。一方、responses.create + prompt_cache_key では2ターン目以降 96.0%(5,313 tok HIT) に改善し、累計コストも -68.43% 削減されました。

(C) Google Gemini:google-genai SDKでの Context Caching 制御

Google Gemini(google-genai SDK)には、自動で効く「暗黙的キャッシュ」と、client.caches.create で作成する「明示的キャッシュ」の2種類があります。

明示的キャッシュでは、時間比例の保管料(gemini-3.8-flash で $0.50 / 1M tok / 時間)が課金されます。そのため、短い ttl を設定した上で、セッション終了時に必ず caches.delete で破棄する実装が推奨されます。

from google import genai
from google.genai import types

client = genai.Client()

def run_gemini_session_with_cache(
    static_manual: str,
    turns_data: list[tuple[str, str]],
    model: str = "gemini-3.8-flash",
):
    # 1. 固定マニュアルに対して短期TTL(5分=300s)の明示的キャッシュを作成
    cache_obj = client.caches.create(
        model=model,
        config=types.CreateCachedContentConfig(
            display_name="rag_static_manual_cache",
            contents=[static_manual],
            ttl="300s",
        ),
    )

    try:
        for dynamic_rag_chunk, user_question in turns_data:
            # 2. キャッシュ名を指定し、差分(動的RAG + 質問)のみを送信
            response = client.models.generate_content(
                model=model,
                contents=f"{dynamic_rag_chunk}\n\n### User Question\n{user_question}",
                config=types.GenerateContentConfig(
                    cached_content=cache_obj.name,
                    max_output_tokens=256,
                ),
            )
            meta = response.usage_metadata
            prompt_tokens = getattr(meta, "prompt_token_count", 0) or 0
            cached_tokens = getattr(meta, "cached_content_token_count", 0) or 0
            base_input = max(0, prompt_tokens - cached_tokens)
            print(f"Base Input: {base_input}, Cache Read (HIT): {cached_tokens}")
    finally:
        # 3. 保管料の継続課金を防ぐため、セッション終了時に必ず削除
        client.caches.delete(name=cache_obj.name)

RAGプロンプト配置順によるキャッシュヒット率・コスト実測 (CLI v2)


4. 原因③:トークン集計時の「二重課金計算バグ」に注意

自前でコスト集計ロガーを実装する際は注意が必要です。プロバイダごとに、レスポンスの総入力トークン数に キャッシュヒット分が含まれるかという内訳仕様 が異なるためです。

プロバイダ 総入力フィールド キャッシュ読取フィールド 通常課金トークン(Base Input)の正しい算出式
Anthropic usage.input_tokens usage.cache_read_input_tokens usage.input_tokens(最初から分離されているため減算不要)
OpenAI usage.input_tokens (または prompt_tokens) input_tokens_details.cached_tokens max(0, input_tokens - cached_tokens - cache_write_tokens)
Google Gemini usage_metadata.prompt_token_count usage_metadata.cached_content_token_count max(0, prompt_token_count - cached_content_token_count)

OpenAIやGeminiで総入力数へそのまま通常単価を掛けると、割引されたはずのトークンを二重計上してしまうため注意してください。


5. よくあるトラブル・運用時の注意点チェックリスト

  1. アクセス間隔が空く社内ボットでの「書込割増負け」:
    • Anthropic(5分TTL:1.25倍、1時間TTL:2.0倍)やOpenAI Responses API(30分TTL:1.25倍、12時間TTL:2.5倍)には書込割増があります。TTL内に 最低2〜3回以上 ヒットしないと割増分が割引額を上回り、キャッシュOFF時より割高になります。
  2. Geminiでの小規模プロンプト長期保持:
    • 4,000トークン程度の規定をGeminiの明示的キャッシュで週末72時間保持すると、Cold再計算の100倍以上の保管料が発生します。夜間や休日は自然失効させるか、明示的に delete してください。
  3. 手元での再現・ベンチマーク測定:
    • 自分のプロンプトデータで3社のヒット率と実コストを一括比較したい場合は、公開リポジトリのCLIツールで即座に検証できます。
git clone https://github.com/ai-nexus-jp/rag-prompt-caching-benchmark.git
cd rag-prompt-caching-benchmark
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env  # APIキーを設定

# 3社一括で bad_prefix vs good_prefix を5ターン実測しレポート生成
python3 -m benchmark.cli --provider all --turns 5

関連記事・お問い合わせ

  • 開発背景・アーキテクチャの解説:
    本ツールの設計思想や、プロンプトキャッシュの損益分岐点・JITウォームアップ設計などの詳細は Zennの解説記事 にまとめています。
  • 業務導入・受託開発のご相談:
    AI Nexusでは、業務効率化ツールの受託開発やAI導入コンサルティングを行っています。詳細は AI Nexus 公式サイト をご覧ください。
0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?