メインナビゲーション

カスタム RAG ソリューションの実践例

企業の複雑なナレッジ課題を、用途に合わせた検索拡張生成システムで解決する方法を示す実例

最近、RAG は時に悪く言われます。今ではまったく簡単だと思われている(始めるのは簡単でも、拡張はそうではありません)か、「エージェント型システム」に追い越されたと思われているためです(実際には、多くの場合、表面を少し掘り下げると、すぐに RAG とよく似た姿が見えてきます……)。

このブログでは、よくある次の課題への対処法を、いくつかの実例を通して紹介します。

  • テキストと数値が混在するデータの扱いと、単純な RAG が機能しない理由:キーワードが重複し、数値自体には意味的な情報がないためです。

  • 要約優先の埋め込みが有効な理由:チャンクごとに短い説明的な要約を生成し、その要約を埋め込みとクエリに使用します。

  • 文脈を含む要約の生成方法:親ドキュメントの文脈を含め、形式の似た統計情報を明確に区別します。

  • コードと Pydantic モデルを使う場面:原文どおりの内容が重要な場合は、信頼性を高めるため、カスタムコードや Pydantic モデルを LLM 呼び出しと組み合わせます。

カスタム RAG ソリューションの構築

基本

RAG システムは、サポートボットから社内ナレッジアシスタントまで、幅広い用途を支えています。

通常、その仕組みは次のとおりです。

  1. ソースドキュメントをチャンクに分割

  2. 各チャンクをベクトル空間に埋め込み

  3. クエリ時に上位 K 件のチャンクを検索

  4. それらのチャンクを条件として回答を生成

LangChain、LlamaIndex、OpenAI の Filestore といった人気のツールキットを使えば、これらの手順はほぼ簡単に実行できます。しかし、実際のパイプラインでは密なテキスト以外のデータにも直面し、基本的な RAG では対応が難しいことがあります。以降では、データに関する課題を具体例で示し、複雑さを加えながら段階的にソリューションを構築します。

さらに難しくなるケース

  1. データがテキストだけではない場合(実は珍しくありません)

ゲームの文脈における、次のデータチャンクを考えてみましょう。

JSON

{ "Attack": { "Range": { "default": 5, "with_Draconic_Ascension": 5.5, }, "Speed": { "default": "2 seconds", "with_Draconic_Ascension": "2.2 seconds", }, "Damage": { "default": 10, "with_Draconic_Ascension": 12, } }} 

埋め込みが機能するのは、意味や文法を通じて学習された単語間の関係があるためです。上のデータにはテキストと数値が混在していますが、この特定の文脈を離れると、数値と単語の間に関係はありません。つまり、このデータチャンクは、多少説明的な単語の後にランダムな数値が並んだものにすぎないと言えます。

この種類のデータしかなければ、実際には問題になりません。わずかに含まれる説明的な単語の埋め込みを使って検索できるためです(または、単に Text-to-SQL を使えます)。しかし、同じ単語を含む大量の高密度テキストチャンクの中に、このチャンクが埋もれていたらどうでしょうか。例:

JSON

{"Draconic_Ascension": { "description": "Transform into dragon form. Attack range and speed are increased by 10%, damage is boosted by 20%.", "details": { "activation_conditions": "Can only be activated when HP is below 50%or Fury meter is full.", "visual_effects": "Wings unfurl, scales shimmer with embers, voice lines change to echoing growls.", "lore": "An ancient bloodline awakens. The bearer of the mark channels the soul of the last Flamewing Wyrm, becoming a living storm of fire and fury." } }}

ここで、「Draconic Ascension 使用時の攻撃範囲は?」という情報を検索したいとします。必要なチャンクは、同じキーワードを含む別のチャンクのノイズに埋もれているため、おそらく検索できません。

根本的な問題は、同じトピックについて異なる種類の情報が含まれていても、これらのデータチャンクをうまく区別できないことです。何らかの方法で、データを補強できないでしょうか。もちろん、できます:smile:

  1. 要約によるデータの補強――そう、要約です

チャンク自体を直接埋め込む代わりに、まずデータの内容を説明する要約を生成し、その要約を埋め込みと検索に使用できます。生成時には、要約に紐づく元のデータを引き続き使用します。

上で示した 2 つのチャンクなら、次のような要約を生成します。

  1. 攻撃範囲、速度、ダメージの統計(通常時および Draconic Ascension 使用時)

  2. 発動条件、視覚効果、伝承を含む Draconic Ascension の説明と詳細

続いて、要約に「合う」ようにクエリも補強します。たとえば、「Draconic Ascension 使用時の攻撃範囲は?」を、「Draconic Ascension 使用時の攻撃範囲の統計は?」に変換します。これは、技術分野以外のユーザーが検索クエリを入力し、~~「自由形式」~~普通の人間の言葉で質問する場合に特に重要です。結局のところ、適合率や再現率を最大化するために RAG がどう機能するかは、ユーザーの知識の範囲外であり、関心事でもありません。

状況がさらに難しくなる場面を示す図

  1. 文脈から切り離さない(人生全般にも当てはまります)

次に、以下のような見た目が同じ大量のデータチャンクを扱うケースを考えます。

Plain Text

# Chunk one{ "Attack": { "Range": { "default": 9, }, ... }}# Chunk two{ "Attack": { "Range": { "default": 6, }, ... }}# Chunk three{ "Attack": { "Range": { "default": 7, }, ... }}

同じアプローチを続けて、「キャラクター X の攻撃範囲は?」と質問した場合を考えてみましょう。生成した要約も互いによく似ているため、運任せの推測ゲームになってしまいます。では、どうすれば区別できるでしょうか。

答えは簡単です。文脈を与えます。たとえば今回なら、データチャンクに {”character”: “X”} のような親ドキュメントへの参照を含めるだけでも構いません。そうすれば、キャラクター Y や Z にも同じデータがある場合でも、キャラクター X の正しいデータを正確に検索できます。

ただし、より優れた汎用的な方法は、チャンクについて文脈を含む要約を生成することです。つまり、データチャンクだけを要約するのではなく、親ドキュメントとチャンクの両方を渡して、文脈を含む全体的な要約を生成します。その要約には、チャンクが親ドキュメント内でどのような位置づけにあるかも含めます。例:

  1. このチャンクは、キャラクター X の……に関する詳細な統計を示しています。このチャンクは、X の攻撃速度における強みを示すことで、ドキュメント全体の一部を構成しています……

  2. このチャンクは、キャラクター Y の……に関する詳細な統計を示しています。このチャンクは、Y が特殊能力によって強化された際のステータスを示すことで、ドキュメント全体の一部を構成しています……

  3. このチャンクは、キャラクター Z の……に関する詳細な統計を示しています。このチャンクは、チーム戦でタンク役に適した Z のステータスを示すことで、ドキュメント全体の一部を構成しています……

この手法は(Anthropic から部分的に着想を得ています)、上の例には過剰に思えるかもしれません。しかし、「文脈から切り離す」と誤解されかねないチャンクには非常に有効です。また、すべてのチャンクに使える統一的なアプローチとなり、エンジニアリングパイプラインを整然と保てます。

状況がさらに難しくなる場面を示す図

  1. ~~何もかも制御したい~~厳密さが必要な場合

通常は、まとまった形でデータを受け取り、RAG システム用にチャンクへ分割します。この例では少し異なるケースを扱います。データはすでにチャンクに分割されていますが、その分け方が不適切です。本来ひとまとまりに戻す必要がある論理的なチャンクが、無作為な断片に分割されています。論理的なチャンクとは、ドキュメントの小節や一貫性のある段落など、本来ひとまとまりにすべきコンテンツを指します。

状況がさらに難しくなる場面を示す図

最初の試みでは、すべてのデータを LLM 呼び出しに渡し、適切と判断した形にグループ化して、その内容を返すよう求めました。LLM なら得意なはずですよね?実際には、そうとも限りません。

ほかのいくつかのケースでも、内容をすべて正確に返すよう求めると、LLM は手を抜く傾向があり、信頼性に欠けることが分かりました。特に文脈が長い場合に顕著です。考えてみれば当然です。しかし、この用途では致命的でした。一語一句正確な内容が必要で、要約も、原文の一部を省くことも許されないからです。細部を一つも見落とせません。

一方で良かった点は、分割されたチャンクの意味と構造を非常によく理解できたことです。正確な内容をそのまま返すことさえ拒まなければ、ですが。まったくもう:/

では、LLM の得意分野を活かしながら、信頼性に欠ける部分を避けるにはどうすればよいでしょうか。そこで、昔なじみの頼れる友人、コード(つまりカスタム Python 関数)の出番です。 さらに、「これ以上ないほど単純な」Pydantic モデルも使います。解決策は次のとおりです。

  • 現在の論理的なチャンクを保持しながら、各セクションを順に処理

  • 各セクションで LLM に「このセクションは現在の論理的なチャンクに属するか」と問い、Pydantic モデルに従って「はい」か「いいえ」で回答させる

  • 「はい」ならセクションをチャンクに追加。「いいえ」なら、完成した現在の論理的なチャンクをそのまま出力し、そのセクションから新しいチャンクを開始

状況がさらに難しくなる場面を示す図

もちろん、全コンテンツを一度に処理するより多少多くのトークンを使います。しかし、正確な内容の保持を最優先するこの用途では、わずかな追加コストを払う価値が十分にありました。

非常に単純な解決策ですが、重要な原則に従っています。厳密さが必要な場合、確率的に動作する LLM だけに頼るべきではないという原則です。

カスタムコードや関数、Pydantic モデルを活用すれば、LLM の能力を引き出しながら、予測可能で信頼できる結果を得られます。

まとめ

生成 AI ソリューションの構築は、AI と同じくらいエンジニアリング上の課題でもあります。これらの例が、皆さん独自の課題に取り組むきっかけになれば幸いです。エンジニアリングを重視した生成 AI ソリューションについて詳しくは、ルーターベースのエージェント型システム設計に関するブログ記事をご覧ください。

著者

Cynthia Yu