0Pricing
AI Engineering Academy · レッスン

Redisによる完全一致キャッシュ

完全なプロンプトをハッシュ化してLLMのレスポンスをキャッシュし、TTL付きでRedisに保存することで、同一のリクエストをAPI呼び出しなしで即座に返します。

「Redisによる完全一致キャッシュ」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。

LLM の応答をキャッシュする理由

LLM API の呼び出しにはコストがかかります。1回の GPT-4o リクエストには、トークン数に応じて 0.005〜0.15ドルかかる場合があります。多くのアプリケーションでは、受信するクエリのかなりの割合が、以前のものと完全に同一またはほぼ同一です。たとえば FAQ ボット、カスタマーサポートシステム、ユーザーが同じ質問を繰り返すコードレビュー ツールなどが該当します。こうした用途では、キャッシュによって API 呼び出しの 20〜50 パーセントを省略でき、コストを直接削減するとともに遅延も短縮できます。

完全一致キャッシュ:キャッシュキーの設計

完全一致キャッシュでは、入力から決定論的に生成したハッシュをキーとして LLM の応答を保存します。キャッシュキーには、出力に影響するすべての入力を含める必要があります。messages 配列、モデル名、temperature、その他応答を変化させるパラメーターなどです。これらのいずれかをキーに含めないと、実質的に異なるリクエストに対してキャッシュ済みの応答を返す、キャッシュの衝突が発生します。

import hashlib
import json

def make_cache_key(messages: list[dict], model: str, temperature: float) -> str:
    # Create a canonical, order-stable representation
    key_data = {
        'model': model,
        'temperature': temperature,
        'messages': messages,  # list order matters
    }
    # Serialize to JSON with sorted keys for determinism
    serialized = json.dumps(key_data, sort_keys=True, ensure_ascii=False)
    # Hash to a fixed-length key safe for Redis
    return 'llm_cache:' + hashlib.sha256(serialized.encode()).hexdigest()

Redis への接続

Redis は、サブミリ秒の読み取り遅延と組み込みの TTL サポートを備えているため、LLM 応答のキャッシュに標準的に使われます。同期アクセスには redis-py ライブラリを使用し、FastAPI アプリケーションでの非同期アクセスには aioredis(現在は redis-py に統合され、redis.asyncio となっています)を使用します。接続プールの枯渇を避けるため、Redis 接続はシングルトンとして保持します。

import redis
import redis.asyncio as aioredis

# Synchronous Redis client
r = redis.Redis(
    host='localhost',
    port=6379,
    db=0,
    decode_responses=True,  # return str instead of bytes
)

# Async Redis client (for FastAPI)
async_r = aioredis.Redis(
    host='localhost',
    port=6379,
    db=0,
    decode_responses=True,
)

# Test connection
print(r.ping())  # True if Redis is running

Cache-Aside パターンの実装

cache-aside パターンは、LLM API に対する標準的なキャッシュ戦略です。リクエストごとに、(1) キャッシュキーを計算し、(2) Redis にキャッシュ済みの応答があるか確認し、(3) 見つかった場合(キャッシュヒット)はすぐに返し、(4) 見つからない場合(キャッシュミス)は LLM API を呼び出し、(5) TTL を設定して応答を Redis に保存し、(6) 応答を返します。このパターンでは、キャッシュのロジックを LLM 呼び出し自体から分離できます。

import json
from openai import OpenAI

client = OpenAI()

def cached_completion(
    messages: list[dict],
    model: str = 'gpt-4o-mini',
    temperature: float = 0.7,
    ttl_seconds: int = 3600,
) -> str:
    cache_key = make_cache_key(messages, model, temperature)

    # Cache hit?
    cached = r.get(cache_key)
    if cached is not None:
        print('[CACHE HIT]')
        return json.loads(cached)

    # Cache miss: call API
    print('[CACHE MISS]')
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=temperature,
    )
    result = response.choices[0].message.content

    # Store in cache with TTL
    r.setex(cache_key, ttl_seconds, json.dumps(result))
    return result

FastAPI 向けの非同期 Cache-Aside

非同期の FastAPI アプリケーションでは、キャッシュの検索によってイベントループがブロックされないよう、非同期 Redis クライアントを使用します。パターンは同期版と同じですが、すべての Redis 操作で await を使用します。これにより、キャッシュ層を完全にノンブロッキングにし、非同期 LLM クライアントと互換性を保てます。

from openai import AsyncOpenAI
import redis.asyncio as aioredis
import json

async_client = AsyncOpenAI()
async_r = aioredis.Redis(host='localhost', port=6379, decode_responses=True)

async def async_cached_completion(
    messages: list[dict],
    model: str = 'gpt-4o-mini',
    temperature: float = 0.0,
    ttl: int = 86400,
) -> str:
    key = make_cache_key(messages, model, temperature)

    cached = await async_r.get(key)
    if cached:
        return json.loads(cached)

    response = await async_client.chat.completions.create(
        model=model, messages=messages, temperature=temperature
    )
    result = response.choices[0].message.content
    await async_r.setex(key, ttl, json.dumps(result))
    return result

適切な TTL の選択

TTL(Time To Live)は、キャッシュ済みの応答が有効であり続ける期間を制御します。安定したナレッジベースを使う事実ベースの Q&A では、長い TTL(24〜72時間)にするとキャッシュヒット率を最大化できます。ニュースの要約や現在の価格など、最新データを反映する必要がある応答では、短い TTL(5〜15分)にするか、キャッシュを使用しないのが適切です。0 以外の temperature を使う創造的なタスクでは、キャッシュ済みの応答が古くなる可能性があります。そのため、temperature=0 の場合のみキャッシュすることを検討してください。

# TTL strategy by use case
TTL_STRATEGY = {
    'faq_answering':          86400 * 7,   # 7 days — stable facts
    'code_explanation':       86400,        # 1 day — code rarely changes
    'document_summarization': 3600 * 6,    # 6 hours
    'news_analysis':          300,          # 5 minutes — stale quickly
    'creative_writing':       0,            # 0 = don't cache (non-deterministic)
}

def get_ttl_for_use_case(use_case: str) -> int:
    return TTL_STRATEGY.get(use_case, 3600)  # default 1 hour

キャッシュのメトリクスと監視

キャッシュヒット率を、コスト削減の主要な指標として追跡します。キャッシュヒット率が 30 パーセントであれば、API 呼び出しの 30 パーセントを回避できたことを意味します。ヒット数とミス数は、Redis 自体で INCR コマンドを使用し、別々のカウンターに保存します。FastAPI アプリケーションに /metrics エンドポイントを公開し、現在のヒット率、総リクエスト数、推定コスト削減額を報告して、キャッシュの ROI を定量化します。

CACHE_HITS_KEY = 'llm_cache_metrics:hits'
CACHE_MISSES_KEY = 'llm_cache_metrics:misses'

async def async_cached_completion_instrumented(messages, model, temperature=0.0):
    key = make_cache_key(messages, model, temperature)
    cached = await async_r.get(key)

    if cached:
        await async_r.incr(CACHE_HITS_KEY)
        return json.loads(cached)

    await async_r.incr(CACHE_MISSES_KEY)
    response = await async_client.chat.completions.create(
        model=model, messages=messages, temperature=temperature
    )
    result = response.choices[0].message.content
    await async_r.setex(key, 3600, json.dumps(result))
    return result

async def get_cache_stats():
    hits = int(await async_r.get(CACHE_HITS_KEY) or 0)
    misses = int(await async_r.get(CACHE_MISSES_KEY) or 0)
    total = hits + misses
    return {'hit_rate': hits / total if total > 0 else 0, 'total': total}

キャッシュ無効化の戦略

完全一致キャッシュではキーが決定論的であるため、無効化は簡単です。特定のエントリを無効化するには、そのキーを再計算して r.delete(key) を呼び出します。特定のプロンプトパターンに一致するすべてのエントリを無効化するには、Redis のキー接頭辞とワイルドカード検索を使用します。ナレッジベースを大幅に更新してキャッシュ全体を無効化する場合は、r.flushdb() を呼び出します(データベース内のすべてのキーが削除されるため、注意して使用してください)。

async def invalidate_cache_entry(messages, model, temperature):
    key = make_cache_key(messages, model, temperature)
    deleted = await async_r.delete(key)
    print(f'Deleted {deleted} cache entries')

async def invalidate_all_llm_cache():
    # Scan for all keys with prefix 'llm_cache:'
    keys_to_delete = []
    async for key in async_r.scan_iter(match='llm_cache:*'):
        keys_to_delete.append(key)
    if keys_to_delete:
        await async_r.delete(*keys_to_delete)
    print(f'Invalidated {len(keys_to_delete)} cache entries')

複雑な応答のシリアライズ

アプリケーションで API 応答オブジェクト全体(テキストの内容だけではなく)をキャッシュする場合は、慎重にシリアライズします。完全な ChatCompletion オブジェクトには、トークン使用量、モデルのバージョン、終了理由が含まれており、ログ記録やコスト追跡に役立ちます。SDK の .model_dump_json() メソッドを使用して Pydantic の応答オブジェクトを JSON 文字列にシリアライズし、キャッシュから取得するときは ChatCompletion.model_validate_json() で再構築します。

from openai.types.chat import ChatCompletion

async def cached_completion_full_response(
    messages, model='gpt-4o-mini', temperature=0.0
):
    key = make_cache_key(messages, model, temperature) + ':full'
    cached = await async_r.get(key)

    if cached:
        return ChatCompletion.model_validate_json(cached)  # reconstruct object

    response = await async_client.chat.completions.create(
        model=model, messages=messages, temperature=temperature
    )
    # Serialize Pydantic model to JSON
    await async_r.setex(key, 3600, response.model_dump_json())
    return response

キャッシュと非決定性

完全一致キャッシュは、決定論的またはほぼ決定論的なリクエストにのみ適しています。temperature=0 かつ top_p=1.0 の場合、ほとんどの LLM は同じ入力に対して同じ出力を生成します(ただし、浮動小数点演算の非決定性により保証はされません)。temperature が高い場合、モデルが本来は異なる出力を生成していたはずなので、キャッシュ済みの応答が古くなります。常に temperature=0 でキャッシュするか、応答が変化する可能性があることをキャッシュキーに明記してください。

Redis Cluster と本番環境の構成

キャッシュ量が多い本番環境へのデプロイでは、複数ノードに水平シャーディングするために Redis Cluster を使用するか、AWS ElastiCache や Redis Cloud などのマネージド Redis サービスを使用します。maxmemory ポリシー(通常は、メモリがいっぱいになったときに最も長く使われていないエントリを削除する allkeys-lru)を設定し、Redis のメモリ不足を防いでキャッシュサイズを自動的に管理します。

# Redis configuration for production LLM caching
# In redis.conf:
# maxmemory 2gb
# maxmemory-policy allkeys-lru

# Connection with retry and connection pool
import redis
from redis.retry import Retry
from redis.backoff import ExponentialBackoff

retry = Retry(ExponentialBackoff(base=0.1), 3)
production_redis = redis.Redis(
    host='your-redis-host.cache.amazonaws.com',
    port=6379,
    ssl=True,
    decode_responses=True,
    max_connections=50,
    retry=retry,
    retry_on_error=[redis.ConnectionError, redis.TimeoutError],
)

理解度チェック

このレッスンで学んだ、Redis を使った LLM の完全一致応答キャッシュについての理解度を確認します。

レッスンのまとめ

このレッスンでは、完全一致キャッシュではすべての LLM 入力をハッシュ化して決定論的なキャッシュキーを生成すること、cache-aside パターンでは API を呼び出す前に Redis を確認し、キャッシュミスの後に結果を保存すること、TTL の選択ではコンテンツの変更頻度を反映し、安定した知識には長く、変化するデータには短く設定することを学びました。コスト削減の主要な指標としてキャッシュヒット率を監視してください。次は、完全には一致しない類似クエリのためのセマンティックキャッシュを構築します。

よくある質問

「Redisによる完全一致キャッシュ」レッスンは無料ですか?

はい。「Redisによる完全一致キャッシュ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。

「Redisによる完全一致キャッシュ」で何を学びますか?

完全なプロンプトをハッシュ化してLLMのレスポンスをキャッシュし、TTL付きでRedisに保存することで、同一のリクエストをAPI呼び出しなしで即座に返します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

AI Engineering Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「Redisによる完全一致キャッシュ」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAI Engineering Academyレッスンでコードを書いて実行できますか?

はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Redisによる完全一致キャッシュ
  2. 埋め込みによる意味的キャッシュ
  3. OpenAIのプロンプトプレフィックスキャッシュ
  4. バッチ処理、モデルルーティング、コストダッシュボード
← AI Engineering Academyに戻る