Skip to content
Published on

RAG品質評価と失敗パターン分析:検索拡張生成の診断と改善

Authors
  • Name
    Twitter

はじめに:RAGの評価はなぜ難しいのか

大規模言語モデル(LLM)の限界を補うために登場したRAG(Retrieval-Augmented Generation)は、今やエンタープライズAIシステムの中核アーキテクチャとして定着しています。しかし、RAGシステムを本番環境にデプロイした後、「なぜ回答品質が低いのか?」という問いに体系的に答えることは容易ではありません。

RAGの品質問題は単一の原因ではなく、パイプライン全体にわたって発生し得るためです。検索段階で誤ったドキュメントを取得した可能性もあれば、正しいドキュメントを取得したにもかかわらずLLMがその内容を無視してハルシネーション(幻覚)を生成した可能性もあります。

本記事では、RAGパイプラインの各コンポーネントごとの失敗モードを分析し、体系的な評価方法論とデバッグ戦略を紹介します。

RAGパイプラインの構成要素

RAGシステムは大きく3つのコアコンポーネントで構成されます。

1. Retriever(検索器)

ユーザーのクエリを受け取り、ベクトルデータベースや検索エンジンから関連するドキュメントチャンクを取得する役割を担います。

  • Dense Retrieval:埋め込みモデルを使用した意味的類似度ベースの検索(例:OpenAI text-embedding-3-small、Cohere embed-v3
  • Sparse Retrieval:BM25などキーワードベースの検索
  • Hybrid Retrieval:Dense + Sparseを組み合わせた方式
# Dense Retrievalの例
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(documents, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

# クエリに対する関連ドキュメントの検索
results = retriever.get_relevant_documents("RAGの評価方法は?")

2. Reranker(再ランキング器)

初期検索結果をより精密に再ランキングします。Cross-encoderモデルが代表的です。

# Cohere Rerankerの例
from cohere import Client

co = Client(api_key="...")
reranked = co.rerank(
    model="rerank-v3.5",
    query="RAG評価方法",
    documents=retrieved_docs,
    top_n=3
)

3. Generator(生成器)

検索されたコンテキストに基づいて最終回答を生成するLLMです。

# コンテキストベースの回答生成
prompt = f"""以下のコンテキストに基づいて質問に回答してください。

コンテキスト:
{context}

質問:{query}

回答:"""

response = llm.generate(prompt)

主要評価指標(Metrics)

RAGシステムの品質を測定する指標は、検索性能指標と生成性能指標に分けられます。

検索性能指標(Retrieval Metrics)

指標説明数式/概念適用タイミング
Recall@K上位K件の結果に正解ドキュメントが含まれる割合検索された正解数 / 全正解数検索漏れの診断
Precision@K上位K件の結果中の正解ドキュメントの割合正解文書数 / Kノイズ文書の診断
MRR(Mean Reciprocal Rank)最初の正解ドキュメントの順位の逆数の平均1/最初の正解の順位ランキング品質測定
NDCG(Normalized DCG)順位を考慮した検索品質スコアDCG / Ideal DCG全体的なランキング品質
Hit Rate正解ドキュメントが1つでも検索されたクエリの割合成功クエリ / 全クエリ全体的な検索成功率
# Recall@K 計算例
def recall_at_k(retrieved_ids, relevant_ids, k):
    retrieved_set = set(retrieved_ids[:k])
    relevant_set = set(relevant_ids)
    return len(retrieved_set & relevant_set) / len(relevant_set)

# MRR 計算例
def mrr(retrieved_ids, relevant_ids):
    for i, doc_id in enumerate(retrieved_ids):
        if doc_id in relevant_ids:
            return 1.0 / (i + 1)
    return 0.0

生成性能指標(Generation Metrics)

指標説明評価方法
Faithfulness(忠実度)回答がコンテキストに基づいている度合いLLM-as-judgeで各文の根拠を確認
Answer Relevancy(回答関連性)回答が質問に適切である度合い回答から逆に質問を生成し類似度比較
Context Relevancy(コンテキスト関連性)検索されたコンテキストが質問に関連している度合いコンテキスト内の関連文の割合
Answer Correctness(回答正確度)回答が正解と一致する度合いGround Truthとの比較
Hallucination Rate(幻覚率)コンテキストにない情報を生成した割合回答内の根拠なし情報の検出

評価フレームワークの比較

RAGAS(Retrieval Augmented Generation Assessment)

RAGASはRAGシステム評価に特化したオープンソースフレームワークで、LLMを活用した自動評価をサポートします。

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)
from datasets import Dataset

# 評価データの準備
eval_data = {
    "question": ["RAGとは何ですか?"],
    "answer": ["RAGは検索拡張生成で..."],
    "contexts": [["RAG(Retrieval-Augmented Generation)は..."]],
    "ground_truth": ["RAGは外部知識を検索して..."]
}

dataset = Dataset.from_dict(eval_data)
results = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)
print(results)
# {'faithfulness': 0.92, 'answer_relevancy': 0.87, ...}

DeepEval

DeepEvalはユニットテストスタイルでLLMアプリケーションを評価できるフレームワークです。

from deepeval import evaluate
from deepeval.metrics import (
    FaithfulnessMetric,
    AnswerRelevancyMetric,
    ContextualRelevancyMetric,
    HallucinationMetric,
)
from deepeval.test_case import LLMTestCase

test_case = LLMTestCase(
    input="RAGの主要な評価指標は?",
    actual_output="RAGの主要な評価指標にはfaithfulness、relevancy...",
    expected_output="Faithfulness、Answer Relevancy、Context Precision...",
    retrieval_context=["RAG評価にはさまざまな指標が使用されます..."]
)

faithfulness_metric = FaithfulnessMetric(threshold=0.7)
relevancy_metric = AnswerRelevancyMetric(threshold=0.7)

evaluate([test_case], [faithfulness_metric, relevancy_metric])

LlamaIndex Evaluation

LlamaIndexは独自の評価モジュールを提供し、RAGパイプラインと緊密に統合されています。

from llama_index.core.evaluation import (
    FaithfulnessEvaluator,
    RelevancyEvaluator,
    CorrectnessEvaluator,
    BatchEvalRunner,
)
from llama_index.llms.openai import OpenAI

llm = OpenAI(model="gpt-4o")
faithfulness_evaluator = FaithfulnessEvaluator(llm=llm)
relevancy_evaluator = RelevancyEvaluator(llm=llm)

# バッチ評価
runner = BatchEvalRunner(
    {"faithfulness": faithfulness_evaluator, "relevancy": relevancy_evaluator},
    workers=4,
)
eval_results = await runner.aevaluate_queries(query_engine, queries=queries)

カスタムLLM-as-Judge

特定ドメインに合ったカスタム評価基準を適用する際に、LLMを判定者として使用します。

JUDGE_PROMPT = """以下のRAGシステムの回答を評価してください。

[質問]: {question}
[コンテキスト]: {context}
[回答]: {answer}

評価基準:
1. 忠実度(1-5):回答はコンテキストに基づいているか?
2. 完全性(1-5):質問の全ての側面をカバーしているか?
3. 簡潔性(1-5):不要な情報なく要点を伝えているか?

JSON形式でスコアと根拠を出力してください。"""

def evaluate_with_judge(question, context, answer, judge_llm):
    prompt = JUDGE_PROMPT.format(
        question=question, context=context, answer=answer
    )
    result = judge_llm.generate(prompt)
    return json.loads(result)

フレームワーク比較表

機能RAGASDeepEvalLlamaIndex EvalカスタムLLM-as-Judge
導入の容易さ高い高い中程度(LlamaIndex必要)自前実装
対応指標6個以上10個以上5個以上無制限(カスタム)
CI/CD統合可能優秀(pytestスタイル)可能自前実装
コストLLM API費用LLM API費用LLM API費用LLM API費用
ドメインカスタマイズ中程度高い中程度最高
ダッシュボードConfident AI連携DeepEval Cloudなし自前実装
オープンソースYesYes(コア)YesN/A
Ground Truth必要任意任意任意設計次第

主要な失敗パターン分析

失敗パターン1:誤ったチャンク検索(Retrieval Failure)

最も基本的でありながら最も一般的な失敗です。ユーザーの質問と無関係なドキュメントチャンクが検索されるケースです。

原因分析:

  • チャンキング戦略の問題:意味単位ではなく固定長で分割した際にコンテキストが分断される
  • 埋め込みモデルのドメイン不一致
  • メタデータフィルタリングの欠如

例:

質問:「2024年第4四半期の売上はいくらですか?」

検索されたチャンク:「2023年第4四半期の売上は150億円を記録しました...→ 年度が異なるドキュメントが検索される(メタデータフィルタリング欠如)

期待されるチャンク:「2024年第4四半期の売上は200億円で前年比33%成長...

解決方法:

# メタデータフィルタを活用した検索
results = vectorstore.similarity_search(
    query="第4四半期 売上",
    filter={"year": 2024, "quarter": "Q4"},
    k=5
)

# Semantic Chunkingの適用
from langchain.text_splitter import SemanticChunker

splitter = SemanticChunker(
    embeddings=embeddings,
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=90,
)
chunks = splitter.split_documents(documents)

失敗パターン2:コンテキストウィンドウオーバーフロー

取得するドキュメントが多すぎてコンテキストウィンドウを超過するか、逆に少なすぎて情報が不足するケースです。

多すぎる場合の問題:

  • トークン制限超過による切り捨て(truncation)
  • ノイズドキュメントがLLMの注意を分散
  • コスト増加

少なすぎる場合の問題:

  • 回答に必要な情報の不足
  • 不完全な回答の生成
# 適応的K選択戦略
def adaptive_retrieval(query, retriever, min_k=3, max_k=10, threshold=0.7):
    """類似度閾値に基づいて動的にKを調整"""
    results = retriever.similarity_search_with_score(query, k=max_k)

    filtered = [
        (doc, score) for doc, score in results
        if score >= threshold
    ]

    if len(filtered) < min_k:
        return [doc for doc, _ in results[:min_k]]

    return [doc for doc, _ in filtered]

失敗パターン3:正確な検索にもかかわらずハルシネーション

正確なドキュメントが検索されたにもかかわらず、LLMがコンテキストにない情報を生成するケースです。RAGにおいて最も危険な失敗パターンの1つです。

原因分析:

  • LLMの事前学習知識がコンテキストと衝突
  • プロンプトでの「コンテキストのみ使用」という指示が不十分
  • コンテキストに部分的な情報のみがあり、LLMが残りを補完
# ハルシネーション防止のための強化プロンプト
ANTI_HALLUCINATION_PROMPT = """あなたは与えられたコンテキストのみに基づいて回答するアシスタントです。

ルール:
1. 必ずコンテキストにある情報のみを使用してください。
2. コンテキストにない情報は「提供されたドキュメントにその情報は見つかりません」と回答してください。
3. 推測したり事前知識を使用しないでください。
4. 回答の各文の末尾に[出典:ドキュメントN]を表示してください。

コンテキスト:
{context}

質問:{question}

回答:"""

失敗パターン4:Lost-in-the-Middle問題

2023年のスタンフォード大学の研究で明らかになった現象で、LLMが長いコンテキストの中間に位置する情報を効果的に活用できない問題です。

症状:

  • コンテキストの先頭と末尾にある情報はよく活用される
  • 中間に位置する情報は無視されるか見落とされる
  • 検索されたドキュメントが多いほど顕著になる
# Lost-in-the-Middle緩和戦略:重要なドキュメントを両端に配置
def reorder_for_lost_in_middle(documents, scores):
    """最も関連性の高いドキュメントを先頭と末尾に配置"""
    sorted_docs = sorted(
        zip(documents, scores), key=lambda x: x[1], reverse=True
    )

    reordered = []
    for i, (doc, score) in enumerate(sorted_docs):
        if i % 2 == 0:
            reordered.insert(0, doc)  # 先頭に挿入
        else:
            reordered.append(doc)      # 末尾に追加

    return reordered

失敗パターン5:埋め込みモデルのミスマッチ

クエリの分布とドキュメントの分布が異なり、埋め込み空間で意味的類似度が適切に反映されないケースです。

原因分析:

  • 汎用埋め込みモデルで専門ドメインのドキュメントを埋め込み
  • クエリスタイル(短い質問)とドキュメントスタイル(長い説明文)の違い
  • 多言語ドキュメントに英語専用埋め込みモデルを使用
# クエリにinstruction prefixを追加してミスマッチを緩和
# (Instructor系埋め込みモデルの活用)
from InstructorEmbedding import INSTRUCTOR

model = INSTRUCTOR("hkunlp/instructor-xl")

# クエリ用埋め込み
query_embedding = model.encode(
    [["Represent the question for retrieving supporting documents:", query]]
)

# ドキュメント用埋め込み
doc_embedding = model.encode(
    [["Represent the technical document for retrieval:", document]]
)

失敗パターン6:古いナレッジベース(Stale Knowledge Base)

ナレッジベースのドキュメントが最新情報を反映しておらず、回答が現実と乖離するケースです。

解決戦略:

# ナレッジベースの鮮度管理システム
class KnowledgeBaseFreshnessManager:
    def __init__(self, vectorstore, max_age_days=30):
        self.vectorstore = vectorstore
        self.max_age_days = max_age_days

    def check_staleness(self):
        """古いドキュメントの検出"""
        cutoff = datetime.now() - timedelta(days=self.max_age_days)
        stale_docs = self.vectorstore.query(
            filter={"updated_at": {"$lt": cutoff.isoformat()}}
        )
        return stale_docs

    def incremental_update(self, new_documents):
        """増分更新:変更されたドキュメントのみ再埋め込み"""
        for doc in new_documents:
            existing = self.vectorstore.get(
                filter={"source_id": doc.metadata["source_id"]}
            )
            if existing and self._content_changed(existing, doc):
                self.vectorstore.delete(ids=[existing.id])
                self.vectorstore.add_documents([doc])
            elif not existing:
                self.vectorstore.add_documents([doc])

    def add_temporal_boost(self, results, recency_weight=0.1):
        """最新ドキュメントにボーナススコアを付与"""
        now = datetime.now()
        boosted = []
        for doc, score in results:
            age_days = (now - doc.metadata["updated_at"]).days
            recency_score = max(0, 1 - age_days / 365)
            final_score = score + recency_weight * recency_score
            boosted.append((doc, final_score))
        return sorted(boosted, key=lambda x: x[1], reverse=True)

失敗パターンのタイムラインと深刻度

失敗パターン発生頻度ユーザー影響診断難易度修正難易度
誤ったチャンク検索非常に高い高い中程度中程度
コンテキストウィンドウオーバーフロー高い中程度低い低い
正確な検索 + ハルシネーション中程度非常に高い高い高い
Lost-in-the-Middle中程度中程度高い中程度
埋め込みミスマッチ中程度高い高い高い
古いナレッジベース高い高い低い中程度

体系的デバッグワークフロー

RAGシステムの品質問題を診断する際、以下のワークフローに従うと効率的に原因を特定できます。

ステップ1:問題の再現と分類

def classify_failure(question, retrieved_docs, generated_answer, ground_truth):
    """RAG失敗を体系的に分類"""

    # ステップ1:検索品質の確認
    retrieval_recall = calculate_recall(retrieved_docs, ground_truth_docs)

    if retrieval_recall < 0.5:
        return "RETRIEVAL_FAILURE"

    # ステップ2:コンテキスト関連性の確認
    context_relevancy = evaluate_context_relevancy(question, retrieved_docs)

    if context_relevancy < 0.5:
        return "CONTEXT_NOISE"

    # ステップ3:回答の忠実度確認
    faithfulness = evaluate_faithfulness(generated_answer, retrieved_docs)

    if faithfulness < 0.7:
        return "HALLUCINATION"

    # ステップ4:回答の正確度確認
    correctness = evaluate_correctness(generated_answer, ground_truth)

    if correctness < 0.7:
        return "GENERATION_QUALITY"

    return "ACCEPTABLE"

ステップ2:コンポーネント別の深掘り分析

# 検索段階のデバッグ
def debug_retrieval(query, vectorstore, k=10):
    results = vectorstore.similarity_search_with_score(query, k=k)

    print(f"Query: {query}")
    print(f"{'='*60}")
    for i, (doc, score) in enumerate(results):
        print(f"\n[{i+1}] Score: {score:.4f}")
        print(f"Source: {doc.metadata.get('source', 'unknown')}")
        print(f"Content: {doc.page_content[:200]}...")
        print(f"Metadata: {doc.metadata}")

    # クエリ埋め込み分析
    query_embedding = embeddings.embed_query(query)
    print(f"\nQuery embedding norm: {np.linalg.norm(query_embedding):.4f}")
    print(f"Query embedding dim: {len(query_embedding)}")

    return results

ステップ3:A/Bテストと反復改善

# RAG設定のA/Bテストフレームワーク
class RAGABTest:
    def __init__(self, test_queries, ground_truths):
        self.test_queries = test_queries
        self.ground_truths = ground_truths

    def run_experiment(self, config_a, config_b, metrics):
        results_a = self._evaluate_config(config_a, metrics)
        results_b = self._evaluate_config(config_b, metrics)

        comparison = {}
        for metric_name in metrics:
            score_a = np.mean(results_a[metric_name])
            score_b = np.mean(results_b[metric_name])
            improvement = (score_b - score_a) / score_a * 100

            comparison[metric_name] = {
                "config_a": score_a,
                "config_b": score_b,
                "improvement_pct": improvement,
            }

        return comparison

# 使用例
ab_test = RAGABTest(test_queries, ground_truths)
result = ab_test.run_experiment(
    config_a={"chunk_size": 512, "k": 5, "model": "gpt-4o-mini"},
    config_b={"chunk_size": 1024, "k": 3, "model": "gpt-4o"},
    metrics=["faithfulness", "answer_relevancy", "recall"]
)

実践的な推奨事項

チャンキング戦略選択ガイド

ドキュメントタイプ別のチャンキング戦略:

1. 技術文書 / APIドキュメント
Markdownヘッダーベースの分割 + 小さいチャンク(256-512トークン)

2. 法律/規制文書
   → 条項単位の分割 + 階層的インデキシング

3. 会話ログ / FAQ
   → 質問-回答ペア単位の分割

4. 学術論文
   → セクションベースの分割 + アブストラクト/結論の個別インデキシング

5. 一般テキスト
Semantic Chunking(意味単位の分割)

本番環境モニタリングチェックリスト

  1. 日次モニタリング:検索ヒット率、平均類似度スコア、回答長の分布
  2. 週次モニタリング:ユーザーフィードバック(thumbs up/down)のトレンド、ハルシネーション率のサンプリング
  3. 月次モニタリング:テストセット全体に対するRAGAS評価、埋め込みドリフト分析
# 本番環境モニタリングダッシュボードの指標
monitoring_metrics = {
    "retrieval": {
        "avg_similarity_score": 0.82,
        "hit_rate": 0.94,
        "avg_retrieved_docs": 4.2,
        "empty_retrieval_rate": 0.02,
    },
    "generation": {
        "avg_faithfulness": 0.89,
        "avg_answer_length": 245,
        "refusal_rate": 0.05,
        "avg_latency_ms": 1200,
    },
    "user_feedback": {
        "thumbs_up_rate": 0.78,
        "escalation_rate": 0.08,
    }
}

FAQ

Q1:RAG評価にGround Truthデータは必ず必要ですか?

いいえ。RAGASのfaithfulnessやcontext relevancyなどの指標はGround Truthなしでも測定可能です。ただし、Answer CorrectnessやRecall@Kなどの指標にはGround Truthが必要です。初期段階ではGround Truthなしの指標から始め、段階的にゴールデンデータセットを構築することをお勧めします。

Q2:評価用LLMは生成用LLMと同じものを使うべきですか?

一般的に異なるモデルを使用することが推奨されます。同じモデルを使用するとバイアスが発生する可能性があります。例えば、GPT-4oで生成した回答をGPT-4oで評価すると、自己評価バイアスが生じます。Claudeや他のモデルファミリーを交差使用すると、より客観的な評価が可能になります。

Q3:RetrievalとGenerationのどちらを先に改善すべきですか?

ほとんどの場合、Retrievalを先に改善する方が効果的です。「Garbage in, garbage out」の原則が適用されるためです。検索品質が低ければ、どんなに優れたLLMを使用しても回答品質は限定されます。Retrieval Recallが0.8以上になった時点で、Generation側の最適化に移行することをお勧めします。

Q4:チャンクサイズはどれくらいが適切ですか?

単一の正解はありませんが、一般的なガイドラインは以下の通りです:

  • 256-512トークン:短い事実ベースのQAに適切
  • 512-1024トークン:説明が必要な一般的な質問に適切
  • 1024-2048トークン:複雑な分析が必要な質問に適切

チャンクサイズが小さすぎるとコンテキストが失われ、大きすぎるとノイズが増加します。最適なサイズは実験で決定する必要があります。

Q5:Rerankerは常に使用すべきですか?

Rerankerは初期検索結果の精度を高めるのに非常に効果的ですが、追加のレイテンシとコストが発生します。以下の場合に特に推奨されます:

  • 検索結果の上位ドキュメントが頻繁に無関係な場合
  • クエリが複雑または多義的な場合
  • Retrieval Precisionが低い場合

Q6:多言語RAGシステムで特に注意すべき点は?

多言語環境では以下を考慮してください:

  • 多言語埋め込みモデルの使用(例:multilingual-e5-large
  • 言語別のチャンキング戦略の差別化(韓国語は形態素ベース、日本語は分節ベース)
  • 言語横断検索のテスト(日本語の質問 → 英語のドキュメント検索など)

参考資料

まとめ:実践のための重要ポイント

RAGシステムの品質管理は、単にLLMを交換するだけでは解決できません。体系的な評価フレームワークを導入し、各コンポーネントの失敗モードを理解し、継続的なモニタリングを通じて段階的に改善することが核心です。

最も重要な3つの実践事項をまとめると:

  1. 評価データセットの構築から始めましょう:最低50-100個の質問-回答ペアから始め、継続的に拡張します。
  2. Retrievalを先に最適化しましょう:検索品質がパイプライン全体の上限を決定します。
  3. 自動化された評価パイプラインをCI/CDに統合しましょう:すべての変更が品質の退行を引き起こさないことを自動的に検証します。

この3つを実践すれば、RAGシステムの品質を予測可能かつ継続的に改善できるようになります。