はじめに

RAG(Retrieval-Augmented Generation)システムを開発していると、「モデルが嘘をついている」という誤解がよく生まれます。しかし、実務でRAGの精度が低い場合、その多くは生成部分ではなく「検索(Retrieval)」の部分にあります。LLMは与えられたコンテキストに対して最も確からしい回答を生成するよう設計されているため、間違った文書が返されれば、モデルもまた堂々と誤答を行います。

本記事では、RAGシステムで頻発する「検索失敗」の具体的なパターンと、その根本原因、そして実装レベルでの解決策を解説します。単なるハイパーパラメータの調整ではなく、データ構造と検索ロジックの設計視点から、精度向上のアプローチを理解しましょう。

RAGが誤答する二つの主要な理由

RAGシステムの失敗は大きく分けて、「文書が見つからない(Recall不足)」と「関連しない文書が返ってくる(Precision不足)」の二つに分類できます。多くの場合、これらは複合的に発生しています。

まず、検索結果として返されたチャンクの中に、正解が含まれていないケースです。これはチャンク分割の粒度やメタデータの付与方法に問題があることが多いです。次に、検索スコアは高いが、文脈的には無関係なチャンクが上位に来るケースです。これは埋め込みモデルの特性や、検索アルゴリズムの限界によるものです。これらの問題を解決するためには、まず「なぜ間違えたのか」を定量的・定性的に分析する必要があります。

失敗パターン1:チャンクの分割粒度不当

最もよくある誤りは、「チャンクが小さすぎる」または「文脈が切断されている」ことです。例えば、経費規程のドキュメントで「本部長の承認が必要」という記述が、前のページにあり、現在のチャンクには含まれていないとします。検索エンジンはそのキーワードを検出しますが、モデルは「誰の承認?」という情報を欠いたまま回答を生成せざるを得ません。

逆にチャンクが大きすぎると、ノイズが増え、埋め込みベクトルの意味がぼやけてしまい、関連性の低いセクションも一緒に検索されてしまいます。最適な粒度を見つけることは、ドキュメントの構造と照会内容に依存するため、一概には言えません。しかし、基本的な方針として、「意味のある単位(段落、リスト項目、テーブル全体)」で分割し、必要に応じてオーバーラップを持たせることが重要です。

解決策:構造的チャンキングとオーバーラップの活用

単純な文字数ベースの分割ではなく、ドキュメントの構造(見出し、段落)を尊重する「構造的チャンキング」を採用しましょう。また、隣接するチャンク間に一定割合のオーバーラップ(重複領域)を持たせることで、境界線上にある重要情報を逃さないようにします。

以下は、Pythonで基本的なオーバーラップ付きチャンキングを行う例です。ここでは簡易的な実装を示しますが、実際にはLangChainやLlamaIndexなどのライブラリが提供する構造的ローダーを利用することを推奨します。

def chunk_text_with_overlap(text, chunk_size=500, overlap=100):
    """
    テキストを指定サイズでチャンク分割し、オーバーラップを持たせる関数
    """
    chunks = []
    start = 0
    while start < len(text):
        end = start + chunk_size
        # 最後のチャンク処理
        if end >= len(text):
            chunks.append(text[start:])
            break
        
        # 単語境界で切る(簡易的な実装)
        while end < len(text) and text[end] != ' ':
            end += 1
            
        chunks.append(text[start:end])
        start = end - overlap
    
    return chunks

# 使用例
sample_text = "経費規程第5条:本部長の承認を経て、..." * 100 # 長いテキストを仮定
chunks = chunk_text_with_overlap(sample_text, chunk_size=300, overlap=50)
print(f"生成されたチャンク数: {len(chunks)}")

このオーバーラップにより、見出しと本文が別々のチャンクに割れてしまうことを防ぎ、文脈の連続性を保つことができます。ただし、オーバーラップが大きすぎると検索コストが増大し、ベクトルデータベースへのストレージ負荷も上がるため、バランスが重要です。

失敗パターン2:埋め込みモデルとクエリのミスマッチ

埋め込み(Embedding)は、テキストを数値ベクトルに変換するプロセスです。ここで重要なのは、「どの埋め込みモデルを使うか」だけでなく、「どのように比較するか」です。

多くの実装ではコサイン類似度(Cosine Similarity)が使われますが、これは「方向性」の近さしか測れません。また、現代の埋め込みモデルは意味的な近さを捉えるのに優れていますが、固有名称(社名、製品名、人名)の正確な一致には弱い傾向があります。例えば、「MacBook Pro」という固有名詞と「Mac mini」という類似した単語が、埋め込み空間では非常に近い位置に来る可能性があります。これが検索結果にノイズをもたらす原因になります。

解決策:混合検索(Hybrid Search)の実装

意味ベースのベクトル検索だけでなく、キーワードベースの全文検索(BM25など)を組み合わせる「混合検索」が実務では標準的です。これにより、固有名称の正確な一致と、意味的な関連性の両方をカバーできます。

以下は、ベクトル検索結果とキーワード検索結果を単純にマージする擬似コードです。実際のシステムではRRF(Reciprocal Rank Fusion)などの重み付けアルゴリズムを用いてランクを統合するのが一般的です。

def hybrid_search(vector_results, keyword_results):
    """
    ベクトル検索結果とキーワード検索結果をマージする例
    実際にはRRFなどを使用して順位を統合する必要があります
    """
    merged = {}
    
    # ベクトルスコアの処理
    for r in vector_results:
        doc_id = r['id']
        score = r.get('score', 0) * 0.5 # 重み係数は調整可能
        if doc_id not in merged:
            merged[doc_id] = {'content': r['content'], 'score': 0}
        merged[doc_id]['score'] += score
        
    # キーワードスコアの処理
    for r in keyword_results:
        doc_id = r['id']
        score = r.get('score', 0) * 0.5 # 重み係数は調整可能
        if doc_id not in merged:
            merged[doc_id] = {'content': r['content'], 'score': 0}
        merged[doc_id]['score'] += score
            
    # スコアでソート
    sorted_results = sorted(merged.items(), key=lambda x: x[1]['score'], reverse=True)
    
    # トップ5を返す(簡易的な実装)
    top_5_ids = [item[0] for item in sorted_results[:5]]
    
    # 元のデータ構造に戻す処理は省略(実際の実装では必要)
    return top_5_ids

このアプローチにより、固有名称の不一致による検索漏れを防ぎつつ、意味的な曖昧さにも対応できるようになります。特に企業内のドキュメント検索では、固有名詞の扱いが精度に直結するため、BM25などのキーワード検索を併用しない手はありません。

失敗パターン3:メタデータフィルターの不備

RAGシステムにおいて、文書に付与されたメタデータ(作成日、著者、部門、権限レベルなど)は、検索の精度を劇的に向上させる強力なフィルターとなります。しかし、多くの初期実装では、このメタデータを軽視し、純粋なテキスト検索のみに行きがちです。

例えば、2023年改定の規程と2021年の旧規程が混在している場合、埋め込みモデルは両者の意味的類似性が高くとも、ユーザーが必要としているのは「現在の規程」である可能性があります。メタデータで「有効期間」や「バージョン」を絞り込まないと、古い情報が返されてしまいます。

解決策:フィルタリングと検索の分離

ベクトルデータベースの機能を活用し、検索前に厳格なメタデータフィルターを適用しましょう。多くのベクトルDB(Pinecone, Weaviate, Qdrantなど)は、メタデータとの組み合わせによる高速なフィルタリングをサポートしています。

実装の際は、以下の点を注意してください:

  • フィルターの順序: 可能な限り早い段階で不要な文書を除外する。ベクトル検索後にフィルターすると、本来含まれるべき関連文書が排除されるリスクがある。
  • メタデータの粒度: ドキュメント単位ではなく、チャンク単位でメタデータを持たせることで、より精密な制御が可能になる。

以下は、メタデータを考慮した検索ロジックの概念図です。

  • ステップ1: ユーザーの権限と現在のタイムスタンプから、有効な文書IDのリストを生成(フィルター適用前)
  • ステップ2: クエリを埋め込みベクトルに変換
  • ステップ3: 有効な文書IDのみを対象にベクトル検索を実行
  • ステップ4: 結果をソートし、上位K件を返す

このように、メタデータを「検索の補助」ではなく「検索の前提条件」として扱うことが、ビジネスユースケースにおけるRAGの信頼性を高める鍵となります。

デバッグと改善のためのアプローチ

RAGの精度向上は、一度の設定で完了するものではありません。継続的な改善のために、以下の手順での分析が有効です。

  1. 失敗ケースの収集: ユーザーからのフィードバックやログから、誤答を生んだクエリを特定する。
  2. 検索結果の可視化: そのクエリで実際にどのチャンクが返され、なぜそれが選ばれたのか(スコアや類似度)を確認する。
  3. 原因の分類: 上記した「チャンク分割」「埋め込み」「メタデータ」のいずれに問題があるかを特定する。
  4. 介入とテスト: 対応策を適用し、再テストを行う。

このプロセスを自動化するために、トレーシングツールや評価フレームワーク(LangSmith, Promptfooなど)の活用が推奨されます。手動で一つずつ確認するのではなく、バッチ処理で数百件のクエリに対して精度を測定できる体制を整えましょう。

まとめ

RAGシステムが誤答をするのは、モデルの能力不足ではなく、通常「検索」の部分に課題があります。チャンク分割の粒度不当、埋め込みモデルとキーワード検索のバランス、そしてメタデータの活用不足——これら三つを見直すことで、多くのケースで精度が向上します。

特に重要なのは、単一の解決策を盲信せず、ドキュメントの性質やビジネス要件に合わせて「構造的チャンキング」「混合検索」「厳格なメタデータフィルター」を組み合わせていくことです。また、改善は継続的なデバッグと評価を通じて行われるべきものであり、初期実装で完璧を目指さず、フィードバックループを回す姿勢が重要です。

両儀システムソリューションズでは、RAGシステムの設計から導入支援まで幅広く対応しております。精度に悩むエンジニアの皆様、お気軽にご相談ください。

参考

本記事は海外の技術トレンド「Why RAG gives wrong answers (and how to fix retrieval failures)」(DEV)で話題のテーマをもとに、両儀システムソリューションズが独自に解説したものです。