多くのエンジニアリングチームが、検索拡張生成(RAG)において同じ壁にぶつかります。彼らはチュートリアルの通りに動きます。ドキュメントを512または1024トークンの固定チャンクに分割し、単一の埋め込みモデルに通し、単純なtop-kルックアップでベクトルデータベースを呼び出す。スライド資料の上では、これは堅実に見えます。しかし、本番環境では崩壊します。

固定チャンクは内容を考慮しません。法的契約書を文の途中で分割し、責任条項を無関係な2つのテキストに分断してしまうことも厭いません。APIエンドポイントの説明全体を、ユーザーが尋ねた特定のパラメータがノイズの中に埋もれてしまうほど巨大なチャンクに放り込むこともあります。そして、検索が遅くなれば、ミリ秒単位のレイテンシがダイレクトにユーザー体験を損ないます。私たちはこれを苦い経験を通して学びました。そこで、検索レイヤーを一度解体し、再構築したのです。その結果、Recall@10は78%から95%へと跳ね上がりました。レイテンシは増えるどころか、劇的に短縮されました。

コピペRAGの問題点

標準的なRAGスタックは、一種のデフォルト設定のようになっています。小さなチャンク、単一の埋め込みモデル、ベクトル検索、以上。このアプローチがデモで通用するのは、デモで使われる質問が整理されており、ドキュメントも整っているからです。本番データが整っていることは決してありません。

法的文書には階層構造があります。セクションの中にサブセクションがあり、サブセクションの中に条項があります。これらを単純なトークンカウンターで切り刻むと、モデルが推論するために必要な関係性そのものを破壊してしまいます。APIドキュメントにも構造はありますが、性質が異なります。関数のシグネチャ、パラメータ、戻り値、そして使用例は、一つの論理的な単位を形成します。これを固定のトークンウィンドウに押し込めば、使用例が途切れるか、あるいは無関係な関数でチャンクが膨れ上がることになります。サポートチケットは雑多で会話的であり、トピックが急に変わることもあります。Wikiは広範囲に及び、相互参照されています。一つのチャンキング戦略ですべてに対応することはできませんが、多くのチームはまさにその戦略をデプロイしてしまいます。私たちは、それが不可能であると認めることにしました。

戦略的チャンキング:素材に合わせた手法の選択

私たちは、コンテンツを意識したチャンキング(content-aware chunking)へと移行しました。法的文書には、ドキュメントの階層構造を尊重する再帰的チャンキング(recursive chunking)を使用します。これにより、条項を損なうことなく、セクション間の親子関係を維持できます。APIドキュメントには、各関数やエンドポイントを境界として扱う関数認識型チャンキング(function-aware chunking)を構築しました。パラメータの説明が長くなった場合、トークン制限ではなく、その関数に合わせてチャンクが拡張されます。サポートチケットには、自然なトピックの境界を検知するセマンティック・チャンキング(semantic chunking)を使用します。顧客が請求に関する不満から技術的なバグへと急に話題を変えた場合、その転換点で分割が行われます。Wikiや非構造化ナレッジベースには、軽量なLLMがテキストを評価して意味のある境界を決定するエージェンティック・チャンキング(agentic chunking)を採用しています。これは文字分割よりもセットアップに時間はかかりますが、「機能する検索」と「推測するだけの検索」を分ける決定的な違いとなります。

ハイブリッド検索:なぜベクトル検索だけでは不十分なのか

ベクトル検索は意味を理解しますが、完全一致を見逃すことがあります。ユーザーが ERR_CONNECTION_RESET_0x5F3 のようなエラーコードを貼り付けた場合、意味的な類似性に基づくと、単にネットワークエラー全般について述べている段落の方が上位にランクされてしまう可能性があります。一方で、BM25は完全一致は見つけますが、概念的な関連性を見逃します。両方が必要なのです。

私たちはベクトル検索とBM25を並列で実行しています。そして、それらの結果をReciprocal Rank Fusion(RRF)によって結合します。RRFは、異なる2つの検索空間からのスコアを、無理に同じスケールに合わせることなく正規化します。結合後、上位の候補をクロスエンコーダー(cross-encoder)のリランカーに送ります。これによりわずかなレイテンシは追加されますが、精度向上によるメリットは非常に大きいです。リランカーはクエリと各候補をセットで読み取り、初期の埋め込みによるコサイン類似度よりもはるかに正確な関連性スコアを割り当てます。実際、この組み合わせにより、純粋なベクトル検索では見逃してしまう正確なエラーコードを捉えつつ、キーワード検索では無視されてしまう概念的に関連したトラブルシューティングの手順を提示することが可能になります。

クエリ拡張:インデックスに到達する前にユーザー入力を修正する

ユーザーは完璧な検索クエリを書くわけではありません。「なぜ前回のデプロイが失敗したのか、そしてどうやってロールバックすればいいのか」といった、2つの異なる知識を見つけて結びつける必要があるマルチホップ(multi-hop)な質問をしたり、インデックスと結びつきにくい曖昧な質問をしたりします。

検索の前にクエリを変換します。マルチホップな質問はサブクエリへと分解されます。曖昧な意図は、複数の具体的な検索クエリへと拡張されます。1つのユーザークエリを5つの異なる検索クエリに拡張することで、再現率(recall)を78%から96%に向上させられることが分かりました。これは、LLMへのプロンプトをより強力にするということではありません。検索システムに対して、適切なコンテキストを見つけ出すための試行回数を増やすということなのです。生成された各クエリが異なる角度や用語を捉え、それらを統合した結果が全体像を描き出します。

ベイズ最適化:推測をやめる

複数のチャンキング戦略、ハイブリッド検索、クエリ拡張を導入すると、新たな問題に直面します。調整すべきパラメータ(ノブ)が多すぎるのです。チャンクサイズ、オーバーラップ率、ベクトル重み対BM25重み、リランキングの閾値、そしてtop-kの値。これらはすべて非線形に相互作用します。手動でのチューニングは、もはや当てずっぽうのゲームになってしまいます。

私たちは推測をやめました。私たちは...を...として扱います