エンタープライズ級のRAGシステムを構築する際、パフォーマンスのボトルネックはLLMの推論能力ではなく、検索パイプラインの設計にあることが多い。本チュートリアルでは、検索精度とエンドツーエンドのレイテンシのバランスに焦点を当て、スパース/デンス検索のトレードオフ、ハイブリッド検索の融合、埋め込みモデルの選択、インデックスパラメータのチューニング、リランキング、クエリ書き換えなどの主要な側面を網羅する。DeepSeek APIを介して実行可能な例を提供し、上級読者がRAGのパフォーマンスを体系的に最適化できるようにする。

検索精度のボトルネック:スパース検索とデンス検索のトレードオフ

従来のBM25は、語頻度と逆文書頻度に基づいており、正確なキーワードマッチングに優れているが、語彙の重複がない意味的に関連するクエリには対応できない。ベクトル検索は、埋め込みモデルを使用してテキストを高次元空間にマッピングし、意味的類似性を捉えるが、固有名詞、ID、略語に敏感で、ドメイン分布の影響を大きく受ける。実際のシナリオでは、ユーザークエリは両方のタイプのニーズを混在させることがあり、単一の検索モードでは必然的にリコールが不十分になる。

例えば、医療分野では、クエリ「アスピリンの心筋梗塞に対する予防効果」とドキュメント「ACSの二次予防におけるASAの適用」は、語彙の重複がほとんどなく、BM25スコアは非常に低いが、意味的には非常に関連性が高い。逆に、クエリ「CTスキャン」の場合、BM25は「CT」を含むドキュメントを正確にヒットできるが、ベクトル検索はベクトル空間のオフセットにより見逃す可能性がある。したがって、ハイブリッド検索はリコールを向上させるための必然的な選択となる。専門用語を扱う場合、ユーザークエリが多様な場合、またはコーパスが中英混合の場合には、ハイブリッド戦略を優先することを推奨する。

次元BM25ベクトル検索
マッチングメカニズム語頻度-逆文書頻度意味ベクトル(デンス)
リコールの利点正確な語彙マッチング意味的関連性マッチング
語彙重複感度高い低い
ドメイン適用性汎用ドメインデータへの適応が必要
インデックス構築転置インデックスベクトルインデックス(ANN)

ハイブリッド検索戦略:RRF融合と重みチューニング

ハイブリッド検索の核心は、異なる検索エンジンの結果を融合することである。最も一般的な方法はReciprocal Rank Fusion(RRF)であり、これは各検索エンジンでのドキュメントのランクの逆数に基づいて総合スコアを計算する。式は:score(d) = Σ_{r∈R} 1/(k + rank_r(d))、ここでkは平滑化定数(通常60)、Rは検索エンジンの集合である。RRFはスコアの正規化を必要とせず、ランクの異常に対してロバストである。

重みチューニングでは、重みw_rを使用して各検索エンジンの寄与を調整できる。例えば、ドメインデータで正確な用語マッチングがより重要な場合はBM25の重みを上げ、意味を重視する場合はベクトルの重みを上げる。検証セットでグリッドサーチを使用して重みを最適化し、評価指標にはRecall@kとMRRを使用する。エンジニアリングでは、手動調整を避けるためにRayまたはOptunaを使用したハイパーパラメータ検索を推奨する。

以下の例は、DeepSeek APIを呼び出して埋め込みを取得し、RRFスコアを計算する方法を示している(検索結果リストが既にあると仮定):

import requests
import numpy as np

def get_embedding(text):
    resp = requests.post(
        "https://api.deepseek.com/v1/embeddings",
        headers={"Authorization": "Bearer your-deepseek-api-key"},
        json={"model": "text-embedding-ada-002", "input": text}
    )
    return resp.json()["data"][0]["embedding"]

def rrf_fusion(result_lists, k=60, weights=None):
    scores = {}
    for idx, doc_list in enumerate(result_lists):
        w = weights[idx] if weights else 1.0
        for rank, doc_id in enumerate(doc_list):
            scores[doc_id] = scores.get(doc_id, 0) + w / (k + rank + 1)
    return sorted(scores.items(), key=lambda x: -x[1])

# 例:BM25結果とベクトル結果の融合
bm25_results = ["doc1", "doc3", "doc4"]
vector_results = ["doc2", "doc1", "doc3"]
fused = rrf_fusion([bm25_results, vector_results], weights=[0.5, 1.0])
print(fused)

埋め込みモデルの選択:BGEからLLM-Embedderまで

埋め込みモデルはベクトル検索の上限を決定する。主流のモデルには、BGEシリーズ(BAAI/bge-large-zh)、M3E、OpenAIのtext-embedding-ada-002がある。中国語シナリオでは、BGEはC-MTEBベンチマークで優れた性能を発揮するが、ドメインデータには微調整が必要である。LLM-Embedderは検索シナリオ専用に設計されており、対照学習で訓練され、ロングテールクエリで優れた性能を発揮する。

選択の推奨事項:

  • リソースが十分な場合は、BGE-large-zhまたはLLM-Embedderを優先する。これらは小型モデルよりも意味マッチングで優れている。
  • 多言語を扱う場合は、M3EまたはBGE-m3を検討する。これらはクロスリンガル検索をサポートする。
  • 微調整時には、ドメイン内のクエリ-ドキュメントペアを使用し、InfoNCE損失を採用し、負例マイニング戦略(ハードネガティブなど)に注意して識別性を向上させる。

実測比較(カスタム法務データセット、nDCG@10):BGE-large-zh 0.61、M3E-base 0.58、text-embedding-ada-002 0.55。微調整後、BGEは0.68に向上。

インデックス構築の最適化:IVFとHNSWのパラメータ調整

ベクトルインデックスは検索レイテンシとリコールを直接決定する。IVFはクラスタリングで空間を分割し、HNSWは多層グラフ構造で効率的な近似検索を実現する。主要なパラメータ:

  • IVF nlist(クラスタ中心の数):nlistが小さすぎると各リストが大きくなりスキャンコストが増加し、小さすぎると分割が粗くなりリコールが低下する。通常、nlist = 4*sqrt(N) ~ 8*sqrt(N)、Nはドキュメント数。
  • HNSW M(各層の最大接続数):Mが大きいほどグラフの接続が密になりリコールが向上するが、メモリと構築時間が増加する。一般的なM=16〜32。
  • efConstruction(構築時の動的リストサイズ):構築品質を制御し、大きいほどインデックス品質が向上するが構築が遅くなる。一般的な100〜200。
  • efSearch(クエリ時の動的リストサイズ):クエリ精度とレイテンシに直接影響し、オンラインで調整可能。

エンジニアリングチューニングでは、通常、リコール目標(例:95%のRecall@10)を設定し、その後パラメータを段階的に調整する。以下の例では、faissを使用してIVF-HNSWハイブリッドインデックスを構築し、パラメータの影響を評価する:

import faiss
import numpy as np

# ランダムベクトルを生成して埋め込みをシミュレート
d = 128
n = 100000
xb = np.random.random((n, d)).astype('float32')

# IVF-HNSWインデックスを構築(実際にはHNSWを使用、ここではIVF_HNSWをデモ)
quantizer = faiss.IndexHNSWFlat(d, 32)  # M=32
index = faiss.IndexIVFFlat(quantizer, d, 100, faiss.METRIC_L2)  # nlist=100
index.train(xb)
index.add(xb)

# クエリパラメータ
index.nprobe = 10  # クエリ時に探索するクラスタ数
xq = np.random.random((1, d)).astype('float32')
D, I = index.search(xq, 10)

print('Top-10 indices:', I[0])
print('Distances:', D[0])

リランキングモデル:Cross-Encoderの精度向上とコスト

リランキング段階では、Cross-Encoderモデルを使用し、例えば、BGE-reranker-largeはクエリと文書を連結して入力し、関連性スコアを出力します。Bi-Encoderのベクトル内積と比較して、Cross-Encoderはより細かい相互作用を捉え、精度が大幅に向上しますが、推論コストが高くなります(各ペアに1回のフォワードパスが必要)。

RAGパイプラインでは、通常、まずTop-100を検索し、再ランキングモデルでTop-10を選んでLLMに渡し、精度とコストのバランスを取ります。予算が限られている場合は、小モデルの蒸留レイテンシ最適化(バッチ推論、キャッシュなど)を検討できます。実測データ:MS MARCOデータセットでは、Cross-EncoderのMRR@10はBi-Encoderより8〜10%高いですが、推論時間は50倍増加します。

次の例は、DeepSeek APIを使用してクロスエンコーダ再ランキングを実装する方法を示しています(deepseek-chatモデルでスコアリング、プロンプトモードに注意):

import requests

def cross_encoder_score(query, doc, api_key="your-deepseek-api-key"):
    prompt = f"クエリとドキュメントの関連性を判断し、0〜1のスコアを出力してください:\nクエリ:{query}\nドキュメント:{doc}\nスコア:"
    resp = requests.post(
        "https://api.deepseek.com/v1/chat/completions",
        headers={"Authorization": f"Bearer {api_key}"},
        json={
            "model": "deepseek-chat",
            "messages": [{"role": "user", "content": prompt}],
            "temperature": 0
        }
    )
    return float(resp.json()["choices"][0]["message"]["content"].strip())

query = "アルツハイマー病を予防するには?"
doc = "認知トレーニングと地中海式食事を維持することは、アルツハイマー病のリスクを減らすのに役立ちます。"
score = cross_encoder_score(query, doc)
print(f"関連スコア: {score}")

クエリ書き換えと拡張:リコールを向上させる上流手段

クエリ書き換えは、サブ質問やバリアントを生成して、異なる検索角度をカバーします。例えば、ユーザーのクエリ「気候変動の影響」は、「農業への気候変動の影響」「気候変動と異常気象」などに書き換えられます。クエリ拡張は、同義語や関連語で検索式を補完します。例えば、「GDP」を「国内総生産」に拡張します。

LLMの能力を活用して、書き換えチェーンを構築できます:まずLLMに複数のサブクエリを生成させ、それぞれ検索して結果をマージします。ただし、書き換えによるレイテンシ増加に注意し、検索結果が疎な場合にのみトリガーすることをお勧めします。

エンジニアリング実装では、DeepSeek APIを使用してクエリ書き換えを行うことができます。次の例はサブ質問を生成します:

import requests

def rewrite_query(original, api_key="your-deepseek-api-key"):
    prompt = f"次のクエリを、より具体的な2つのサブ質問に書き換えてください。各行に1つずつ:\n{original}"
    resp = requests.post(
        "https://api.deepseek.com/v1/chat/completions",
        headers={"Authorization": f"Bearer {api_key}"},
        json={
            "model": "deepseek-chat",
            "messages": [{"role": "user", "content": prompt}],
            "temperature": 0.3
        }
    )
    content = resp.json()["choices"][0]["message"]["content"]
    return [l.strip() for l in content.split('\n') if l.strip()]

queries = rewrite_query("ディープラーニングモデルのトレーニング速度を最適化するには?")
for q in queries:
    print(q)

コンテキスト圧縮:ノイズとトークン消費の削減

検索されたドキュメントには無関係なコンテンツが多く含まれることが多く、そのままLLMに連結すると注意力が薄まり、トークン消費が増加します。コンテキスト圧縮は、クエリに最も関連するセグメントを抽出し、表現を簡潔にすることを目的としています。一般的な方法には、ルールベースの抽出圧縮(キーセンテンスなど)、要約ベースの生成圧縮、または小規模モデル(DistilBERTなど)を使用した文レベルの関連性判断があります。

RAGでは、通常、各ドキュメントからトップkの関連文を抽出し、マージして長さで切り詰めます。例えば、単純なヒューリスティック:文とクエリのコサイン類似度を計算し、ソートして上位3文を取得します。完全なセマンティクスを保持する必要がある場合は、LLMで要約を使用できますが、追加のレイテンシが発生します。

評価結果によると、適切な圧縮によりLLM入力長を60%削減し、重要な情報の95%を保持しつつ、最終生成回答のF1値はわずか1.5%低下します。

前回の検索精度と再ランキング戦略の分析に続き、このセクションではキャッシュからアーキテクチャまでのエンドツーエンドのパフォーマンス最適化をエンジニアリングの本線に沿って掘り下げ、実践可能な選択とチューニングの推奨事項を提供します。以下のすべての提案は、実際のビジネスシナリオの負荷テストと本番運用に基づいており、コード例はDeepSeek APIに基づいています。

キャッシュメカニズム設計:セマンティックキャッシュと完全一致キャッシュ

キャッシュはレイテンシを削減する最も直接的な手段ですが、RAGシステムでは2種類のキャッシュを区別する必要があります:完全一致キャッシュ(exact match)とセマンティックキャッシュ(semantic cache)。完全一致キャッシュはクエリテキストのハッシュ値をキーとして結果を直接再利用し、実装が簡単でヒット率は30%〜50%(冪等シナリオ)に達しますが、言い換えクエリには効果がありません。セマンティックキャッシュはエンコーダーでクエリをベクトルにマッピングし、キャッシュライブラリで類似アイテムを検索し、ヒット時に回答を再利用または類似キャッシュを直接返します。セマンティックキャッシュの課題は類似度しきい値のバランスです—しきい値が高すぎるとヒット率が低く、低すぎると無関係なコンテンツを返す可能性があります。実践では二重しきい値がよく使用されます:高しきい値(例:cosine>0.95)は結果を直接再利用し、中程度のしきい値(0.85-0.95)はキャッシュ内の証拠スニペットのみを返し、LLMが回答を再生成します。

エンジニアリング実装の観点では、完全一致キャッシュはRedis + TTLを使用でき、キャッシュキーにはquery + top_k + rerank_modelを含めることをお勧めします。パラメータ変更によるダーティデータを防ぐためです。セマンティックキャッシュはベクトルインデックス(FAISSなど)を導入し、元のクエリベクトルとキャッシュベクトルを一緒に保存する必要があります。レイテンシの利点に関しては、完全一致キャッシュヒットはエンドツーエンドのレイテンシを約60%削減(1.2秒から0.5秒)、セマンティックキャッシュヒットは約40%削減(ベクトル検索のため)。ただし、キャッシュの貫通と雪崩に注意してください—人気クエリのプリヒートとランダムな有効期限を設定することをお勧めします。さらに、キャッシュは決定論的なプロセスにのみ適しており、回答がユーザーコンテキストに依存する場合は再利用できないため、キャッシュキーにユーザー次元を追加する必要があります。

並列化とパイプライン:エンドツーエンドのレイテンシを削減するアーキテクチャ

RAGの3つのステージ(検索、再ランキング、生成)には自然な依存関係がありますが、パイプラインパラレリズムを設計できます:クエリをシャードに分割し、各シャードが独立して完全なプロセスを実行し、結果をマージします。より洗練されたアプローチはステージ非同期です:検索ステージが完了していない間に、軽量な事前生成(要約プレフィックスの生成など)を同時に開始し、証拠が到着した後に修正します。しかし、コアな利点はアイドル待機の排除から得られます:従来の同期フローでは、各ステージがシリアル時間の100%を占有し、総レイテンシは3つのステージの合計です。対照的に、パイプライン設計ではステージをマイクロバッチ粒度でオーバーラップさせ、全体のレイテンシは最大ステージ時間に近づきます。例えば、100クエリを4バッチ(各25)に分割し、検索平均80ms、再ランキング50ms、生成400msの場合、シリアル総レイテンシ=4×(80+50+400)=2120ms、一方4ステージパイプライン(理想的な静的スケジューリング)の総レイテンシ≈80+50+400+3×max(400,80,50)=1930msで、わずか9%の削減です。ただし、生成ステージがストリーミング出力できる場合、削減は20%以上に達します。

実装では、Pythonはconcurrent.futuresまたはasyncioを使用できますが、ステージ間の調整にはバックプレッシャーに注意する必要があります。より堅牢な解決策は、メッセージキュー(Redis Streamsなど)を使用してステージを接続し、コンシューマーサービスを独立してデプロイすることです。以下は簡単な非同期パイプラインの例です:

import asyncio
from openai import AsyncOpenAI

client = AsyncOpenAI(api_key="your-deepseek-api-key", base_url="https://api.deepseek.com")

async def retrieve(query: str):
    # 検索をシミュレート(実際にはベクトルDBを呼び出す)
    await asyncio.sleep(0.2)
    return ["doc1", "doc2"]

async def rerank(query: str, docs: list):
    # 再ランキングをシミュレート
    await asyncio.s
leep(0.1) return sorted(docs) async def generate(query: str, docs: list) -> str: response = await client.chat.completions.create( model="deepseek-chat", messages=[{"role": "system", "content": "あなたはRAGアシスタントです"}, {"role": "user", "content": f"これらの資料に基づいて回答してください:{docs}"}] ) return response.choices[0].message.content async def pipeline(query: str): # タスク並列:まず検索し、同時にプロンプトを事前生成 ret_task = asyncio.create_task(retrieve(query)) gen_task = asyncio.create_task(generate(query, ["事前プレースホルダー"])) docs = await ret_task docs = await rerank(query, docs) # 事前生成結果を待つ。まだ完了していなければ待つが、後で上書き可能 final = await gen_task return final # 呼び出し result = asyncio.run(pipeline("RAGを最適化する方法")) print(result)上記のコードは、検索と生成の大まかな並列化を示しています。本番環境では、これをさらにインデックスシャードレベルの並列化にまで細分化できます。アーキテクチャレベルでは、検索とリランキングをステートレスサービスとして設計することで、水平スケーリングが容易になります。再ランキングと生成を独立したサービスに分割し、非同期またはパイプラインスケジューリングを採用し、バックプレッシャーに注意を払う。
  • ベクトルDB:規模が5000万未満ならFAISS、5000万超ならMilvusを使用。Elasticsearchは既存依存がない限り避ける。
  • 評価:オフラインではMRR、Recall@kを使用し、オンラインではクリックフィードバックを記録し、定期的にクローズドループで更新する。
  • チューニング:パフォーマンスプロファイリングでボトルネックを特定し、埋め込み次元、top_k、リランキングモデル、量子化戦略を協調的に調整する。
  • 長文ドキュメント:512トークンのチャンクと100トークンのオーバーラップを使用し、メタデータフィルタリングで範囲を絞り込む。
  • 新興プラクティス:LLMによるリランキングや引用生成を小規模パイロットで試す。
  • 上記のプラクティスは実際のプロジェクト経験に基づいています。自社のビジネスに合わせて取捨選択してください。RAG最適化に終わりはありません。本記事が皆様のシステムのパフォーマンス加速器となることを願っています。