RAGシステムのパフォーマンスボトルネック分析

典型的なRAGリクエストチェーンは、Embeddingクエリ→ベクトル検索→ドキュメント再ランキング→コンテキスト連結→LLM生成です。実際のテストでは、ベクトル検索とLLM生成がそれぞれレイテンシの約40%を占め、Embeddingと再ランキングが20%を占めます。最適化は各ステップから始める必要がありますが、最も価値のある最適化ポイントはキャッシュです。多くのクエリは繰り返されるか類似しており、キャッシュによりP99レイテンシを3秒から100ミリ秒未満に削減できます。

多層キャッシュアーキテクチャ

3層のキャッシュを推奨します:L1-完全一致キャッシュ(完全に同一のクエリ→キャッシュ結果を直接返す、Redis Stringを使用、TTL=1時間)、L2-意味的類似キャッシュ(類似度>0.95のクエリ→キャッシュ結果を再利用、ベクトルインデックス+意味的重複排除を使用)、L3-ドキュメント断片キャッシュ(高頻度で取得されるドキュメント断片のEmbeddingを事前計算してキャッシュ、Redis Vector Setを使用)。L1のヒット率は約15〜25%、L2のヒット率は約20〜30%、L3は検索レイテンシを50〜70%削減できます。

Redisキャッシュの実践

import redis, json, hashlib
import numpy as np
from openai import OpenAI

client = OpenAI(api_key="your-deepseek-api-key", base_url="https://api.deepseek.com")
redis_client = redis.Redis(host="localhost", port=6379, decode_responses=True)

class RAGCache:
    def __init__(self, similarity_threshold=0.95):
        self.threshold = similarity_threshold

    def _hash(self, text):
        return hashlib.md5(text.encode()).hexdigest()

    def get_exact(self, query):
        """L1: 完全一致キャッシュ"""
        return redis_client.get(f"rag:exact:{self._hash(query)}")

    def set_exact(self, query, result, ttl=3600):
        redis_client.setex(f"rag:exact:{self._hash(query)}", ttl,
                           json.dumps(result, ensure_ascii=False))

    def get_semantic(self, query_embedding):
        """L2: 意味的類似キャッシュ——類似した過去のクエリを検索"""
        # Redisベクトル類似度検索
        results = redis_client.ft("rag_semantic_idx").search(
            redis.commands.search.Query(
                f"*=>[KNN 1 @embedding $vec AS score]")
            .return_fields("query", "result", "score")
            .dialect(2),
            {"vec": np.array(query_embedding, dtype=np.float32).tobytes()}
        )
        if results.docs and float(results.docs[0].score) > self.threshold:
            return json.loads(results.docs[0].result)
        return None

    def embed_query(self, query):
        resp = client.embeddings.create(
            model="text-embedding-3-large", input=query)
        return resp.data[0].embedding

    def retrieve(self, query, retriever_func):
        """キャッシュ付き検索"""
        # L1チェック
        cached = self.get_exact(query)
        if cached:
            return {"source": "L1-cache", "result": json.loads(cached)}
        # L2チェック
        emb = self.embed_query(query)
        cached = self.get_semantic(emb)
        if cached:
            return {"source": "L2-cache", "result": cached}
        # 実際の検索
        result = retriever_func(query)
        self.set_exact(query, result)
        return {"source": "retriever", "result": result}

cache = RAGCache()
# 使用法: cache.retrieve("RAGとは?", my_retriever_func)

バッチ最適化と非同期処理

バッチEmbedding:複数のクエリを1回のAPI呼び出しにまとめます。Embedding APIはバッチ入力(最大2048件)をサポートしており、Embedding段階のレイテンシを90%削減できます。非同期検索:ベクトル検索とドキュメント再ランキングはIO集中型の操作であり、asyncioを使用して並行実行することで、マルチパス検索のレイテンシを直列のN×tからほぼmax(t)に削減できます。コネクションプール:Redisとベクトルデータベースにはコネクションプールを使用して、頻繁な接続確立のオーバーヘッドを回避します。ホットデータのプリロード:統計に基づいて、アクセス頻度が最も高い20%のドキュメントをメモリにプリロードします。

コスト最適化

パフォーマンスに加えて、キャッシュはAPI呼び出しコストも大幅に削減します。OpenAI Embeddingを例にとると、text-embedding-3-largeは100万トークンあたり$0.13で一見安価ですが、1日10万回のクエリでは年間コストが$5,000を超えます。完全一致キャッシュはEmbedding呼び出しを15〜25%削減でき、意味的キャッシュはさらに20〜30%削減し、組み合わせると年間$2,000〜3,000の節約になります。高頻度のLLM生成呼び出しでは、最終応答(中間検索結果ではなく)をキャッシュすると、節約効果はさらに顕著です。

キャッシュ無効化戦略と一貫性の保証

キャッシュを導入したRAGシステムは、古典的な問題に直面します:キャッシュの一貫性と鮮度です。ナレッジベースのドキュメントが更新されると、関連するキャッシュエントリを無効化する必要があります。私たちの解決策はに基づく

ドキュメントバージョンのキャッシュキー — 各ドキュメントには増分バージョン番号(doc_v)があり、キャッシュキーは単純なクエリハッシュからquery_hash+doc_versionに変わります。ドキュメントが更新されると、新しい検索結果は自動的に新しいバージョン番号を使用し、古いバージョンのキャッシュはTTLの期限切れ後に自然に破棄されます。リアルタイム性が非常に高いシナリオ(株式市場ニュースのRAGなど)では、TTLは5分に設定されます。比較的安定したシナリオ(法律条文など)では、TTLは24時間に設定できます。また、プロアクティブなキャッシュウォーミングも実装しました。履歴クエリログを分析して高頻度のクエリパターンを特定し、ナレッジベースの更新後にこれらのクエリのキャッシュをプロアクティブに更新します。この戦略により、ナレッジベース更新後の「キャッシュコールドスタート」フェーズのP95レイテンシが、ウォーミングなしの2.1秒からウォーミング後の0.3秒に短縮され、ユーザーエクスペリエンスはほぼ知覚できません。

RAGシステムのコールドスタート最適化

RAGシステムが新規に稼働した直後やキャッシュクリア後、最初の数千クエリは「コールドスタート」を経験します——毎回完全なEmbedding+ベクトル検索+LLM生成プロセスが必要で、P99レイテンシは定常状態の10倍になる可能性があります。当社のコールドスタート最適化戦略:ウォームアップスクリプト——過去のクエリログから上位1000件の高頻度クエリを抽出し(匿名化後)、サービス起動時またはキャッシュクリア後にウォームアップスクリプトでこれらのクエリを一括実行してキャッシュを埋めます;段階的なトラフィック増加——新しいPodが起動した後、まず「ウォームアップモード」に入り(10%のトラフィックのみ受信)、キャッシュヒット率が60%以上に達した後に100%のトラフィックを受信します;キャッシュの永続化——L1完全一致キャッシュを定期的にディスクにダンプし、新しいPod起動時にディスクからキャッシュを復元します。ウォームアップメカニズムにより、コールドスタートのP99レイテンシは2.8秒から0.5秒に短縮され、ユーザーエクスペリエンスの断絶感が大幅に軽減されます。

セマンティックキャッシュの実装詳細とチューニング

セマンティックキャッシュ(L2キャッシュ)の核心的な課題は、「類似しているが完全に同一ではない」クエリの識別です。類似度しきい値が高すぎる場合(>0.98)、ヒットが少なく;低すぎる場合(<0.8)、無関係なキャッシュ結果を返す可能性があります。当社のチューニング経験:しきい値の設定——事実型クエリ(例:「Pythonのバージョン」)の場合、類似度しきい値は0.92で十分です;開放型クエリ(例:「詩を書いて」)の場合、類似度が0.98でもキャッシュを使用すべきではありません——詩はオリジナルであるべきです。実装では、各クエリに「query_type」タグを付与し、タイプに基づいてセマンティックキャッシュの使用を許可するかどうかを決定します。キャッシュ追い出し戦略——TTL+LFU(最も頻度が低い)の二重追い出しに基づき、人気のあるクエリはTTLを自動的に延長し、人気のないクエリはTTLが切れる前でも追い出される可能性があります。このきめ細かい戦略により、セマンティックキャッシュの「悪いヒット率」(無関係なキャッシュを返す)が8%から1.5%に低減されます。

このスキルチェーンを自分で編成してみませんか?

スキルチェーンで開く →