ほとんどのRAGデモは、ノートPC上では素晴らしく見えます。20ページのPDFをスクリプトに読み込ませ、質問を投げれば、正しい段落を引用する様子を目の当たりにできるでしょう。しかし、そのパイプラインをそのまま本番環境へ投入しようとしたとき、そのロマンスは終わりを迎えます。法的文書は文レベルで真っ二つに切り刻まれます。膨大なAPIリファレンスは、重要なシグナルをボイラープレートのノイズの中に埋もれさせます。レイテンシは膨れ上がり、ユーザーは待ち続け、苛立ち、そして離れていきます。私たちはその壁に激突しました。そこで私たちは、検索レイヤーを根本から解体し、測定可能でチューニング可能なシステムとして再構築しました。その結果、ユーザー体験をスライドショーのように停滞させることなく、95%のリコール率を達成するパイプラインが完成しました。
なぜデモ用のRAGは本番環境で失敗するのか
標準的なスタックは、ホビープロジェクトから初期段階の製品に至るまで、驚くほど一律です。固定トークンチャンク、既製品のembeddings、そして単一のベクトル検索呼び出し。そのシンプルさは魅力的であり、コーパスがクリーンで小さく、構文的に予測可能な場合には機能します。しかし、本番データはそれらのどれにも当てはまりません。512トークンの固定チャンクは、SaaS契約の免責条項の真っ只中を平然と切り裂いてしまいます。突然、検索レイヤーは言語モデルに対して法的義務の半分だけを渡し、責任に関する質問に答えさせようとするのです。コンテキストが断片化されているため、モデルはハルシネーションを起こします。
大規模な技術文書はこの問題をさらに悪化させます。APIドキュメントは、関数のシグネチャ、テーブル、コードブロックで溢れています。固定ウィンドウでは、TypeScriptインターフェースの途中を捉えてしまい、その上にある関数名や、下にある使用例を見逃してしまう可能性があります。埋め込みベクトルは、ユーザーが尋ねている実際の機能ではなく、構文の断片やインラインのノイズを表すことになってしまいます。「Garbage in, hallucination out(ゴミを入力すれば、ハルシネーションが出力される)」状態です。
トークン数ではなく、構造でチャンク化する
私たちが行った最初の変更は、チャンクを単なる「トークンの袋」として考えるのをやめることでした。チャンクとは意味的な単位です。最適な戦略は、何をインデックス化するかによって完全に異なります。
法的文書については、文書の階層構造を尊重する再帰的チャンキング(recursive chunking)に移行しました。セクション、サブセクション、条項を境界として扱います。条項は意味の単位であるため、一つの塊として保持されます。もし条項を切り裂いてしまえば、法的な論理が漏れ出してしまいます。
APIドキュメントの場合、構造を意識したチャンキング(structure-aware chunking)により、関数、クラス、エンドポイントをアトミック(不可分)なものとして扱います。1つのチャンクに関数のシグネチャ、引数、およびdocstringが含まれるようにします。トークンカウンターが進んだからといって、勝手に次のユーティリティ関数へと溢れ出すことはありません。これにより、埋め込みが特定の個別の機能に集中し続けることができます。
サポートチケットはもっと厄介です。それらは会話形式であり、スレッド化されており、非線形です。固定チャンクでは、同じスレッド内のエンジニアによるステータス更新と顧客からの苦情をまとめて取得し、それらが一貫した単位であるかのように扱ってしまいます。私たちはセマンティック・チャンキング(semantic chunking)に切り替え、トークン予算が尽きた時ではなく、トピックや話者が変わった時に分割するようにしました。
社内Wikiは、組織内で最も整理されていないデータであることが多いです。フォーマットは不統一で、ヘッダーは欠落しており、セクションは混ざり合っています。これらに対しては、LLMベースのチャンキングを使用します。小規模なモデルが事前に読み込み、埋め込みを生成する前に論理的な境界を特定します。文字分割よりも初期コストはかかりますが、検索の品質によってそのコストはすぐに回収できます。
ハイブリッド検索:シグナルを組み合わせる
ベクトル検索は強力ですが、死角があります。ERR_CONNECTION_REFUSED_0x800 のような正確なエラーコードを貼り付けても、埋め込み空間でそれらが近くにクラスター化されているために、類似性検索が無関係なモジュールのトラブルシューティングガイドを返してしまうことがあります。完全一致は重要ですが、ベクトル検索だけではそれらを曖昧にしてしまう可能性があります。
BM25を用いたキーワード検索は、この完全一致の問題を実に見事に解決します。しかし、概念的な距離には対応できません。ユーザーが「高負荷時のパフォーマンス低下」について尋ねた場合、キーワードの重複が不十分なため、BM25は「トラフィック急増時のスループット低下」を説明する診断ノートを見逃してしまいます。
私たちはどちらか一方を選ぶのをやめ、両方を並行して実行するようにしました。ベクトル検索とキーワード検索は、それぞれ独自のランク付けされたリストを返します。それらを Reciprocal Rank Fusion (RRF) でマージします。RRFはシンプル
融合(fusion)の後、上位の候補をcross-encoder rerankerに通します。これは無料ではありません。計算コストが約50ミリ秒増加します。しかし、再現率(recall)は15%向上します。cross-encoderはクエリ全体と各候補チャンクを同時に評価するため、bi-encoderの埋め込み(embedding)では決して到達できない、はるかにきめ細かな関連性スコアを算出します。この追加の50ミリ秒は、非常に安上がりな投資です。LLMに不適切なコンテキストウィンドウを送り込み、混乱した回答やハルシネーション(幻覚)による回答を待つために2秒間も無駄にすることを防いでくれるからです。
検索の前にクエリを修正する
ユーザーは検索エンジニアのようにクエリを書くわけではありません。「アプリが壊れた」と入力したり、難解なログの断片を貼り付けたり、曖昧で漠然とした質問をしたりします。それらの生の文字列をそのままインデックスに送ってしまうと、ゴミのような結果しか返ってきません。
私たちは、検索エンジンに渡す前に、すべてのクエリを変換します。
第一に、クエリ拡張(query expansion)です。システムは、一つの短い質問から複数の検索語を生成します。例えば、ユーザーが「タイムアウトの修正方法は?」と尋ねた場合、エンジンはそれを「接続タイムアウト」「リードタイムアウト」「ゲートウェイタイムアウト」「リトライロジック」をカバーするように拡張します。この手法だけで、私たちの再現率は78%から96%へと向上しました。
第二に、クエリ分解(query decomposition)です。複雑な質問は、より小さなサブクエリへと分解されます。「エンタープライズ顧客の90日を過ぎた場合の返金ポリシーは何か、またそれは月額プランとどう違うのか?」といったクエリは、肥大化した一つの埋め込み検索ではなく、2つの焦点を絞った検索へと変換されます。各サブクエリは独立してインデックスにヒットし、その結果は後続の処理で再び結合されます。これにより、検索範囲が狭く正確に保たれ、一つの埋め込みが一度に十数もの概念にマッチしようとしたときに起こる「情報の希釈化」を防ぐことができます。
ベイズ探索でパイプラインをチューニングする
もし、いまだにチャンクサイズやオーバーラップ率、検索の重みを手動でチューニングしているなら、本来得られるはずのパフォーマンスを逃していることになります。私たちは、推測をやめました。
私たちは、チャンクサイズ、オーバーラップ率、ベクトル対BM25の重み、およびリランカーの閾値のすべてを変数とする探索空間を定義しました。そして、ベイズ最適化(Bayesian optimization)を適用しました。何百ものランダムな構成をグリッドサーチする代わりに、ベイズ探索は「何がうまくいくか」の確率モデルを構築します。構成案を提示し、再現率とレイテンシを観察し、その知見を更新して、次の構成案を提示します。時間をかけるにつれて、人間が手動では決して辿り着けないようなバランスへと収束していきます。
それは、私たちが試そうとも思わなかった組み合わせを見つけ出しました。オーバーラップを大きくした、より小さなチャンク。高密度なベクトル検索の重みをわずかに下げ、よりアグレッシブなリランカーの閾値を組み合わせる手法。こうした非自明なトレードオフによって、高い再現率と低いレイテンシの両立を実現できました。
これは一度限りのセットアップ作業ではありません。私たちは毎月、ハイパーパラメータの最適化を再実行しています。コーパスは変化(ドリフト)し、ユーザーの行動も変わります。パイプラインは、その場に停滞して錆びつくのではなく、適応していくべきなのです。
その成果
この再構築による生の出力結果は、疑いようのないものです。
10位以内における再現率は78%から95%に向上しました。正解がナレッジベース内にある場合、20回中19回はそれを提示できています。95パーセンタイルにおけるレイテンシは、850ミリ秒から320ミリ秒に減少しました。チャットのレスポンスは、重苦しいものではなく、瞬時に感じられるものになりました。
検索精度が向上したことで、言語モデルのグラウンディング(根拠付け)も改善されました。ハルシネーション率は12%から3%に低下しました。モデルの目の前に正しいコンテキストがあれば、事実を捏造することはなくなります。クエリあたりのコストは38%減少しました。検索が高速かつシャープになったことで、無関係なコンテキストやリトライループ、冗長で役に立たないプロンプトに浪費されるトークンが削減されたのです。
インフラストラクチャのように構築する
プロトタイプからプロダクションへと移行するのであれば、検索(retrieval)を単なる設定ではなく、インフラストラクチャコードとして扱うべきです。
