Skip to content
Published on

RAGAS完全ガイド:RAGシステムをどのように定量的に評価するか

Authors

RAG評価が難しい理由

「うまく動いている気がするけど、実際にどれくらい良いのかわかりません。」

RAGを運用しているチームで最もよく耳にする言葉だ。開発していると「良くなった気がする」という直感はあるが、それをチームリードに数字で示したり、意思決定の根拠にするのが難しい。

従来のソフトウェアのようにテストケースを書くのも難しい。正解が一つではなく、同じ質問でも文脈によって良い回答が変わってくる。

**RAGAS(RAG Assessment)**はこの問題を解決するために設計されたフレームワークだ。LLMをジャッジとして活用し、RAGシステムの様々な側面を0〜1のスコアで定量化する。

この記事ではRAGASの4つの核心指標を理解し、実際のコードで実装し、自動化された評価パイプラインを構築する方法を解説する。

RAGASが測定する4つの指標

RAGASはRAGシステムを2つの次元で評価する:**検索(Retrieval)の品質と生成(Generation)**の品質。

RAGパイプライン全体:

[ユーザーの質問]
        |
[検索ステップ] <- Context Precision、Context Recallで評価
        |
[生成ステップ] <- Faithfulness、Answer Relevancyで評価
        |
[最終回答]

指標1:Faithfulness(忠実性)

問い:生成された回答は取得したコンテキストに基づいているか?

これはハルシネーション検出指標だ。モデルがコンテキストにない内容を「作り出した」場合、Faithfulnessが低下する。

計算方法

  1. 生成された回答から個々のclaim(主張)を抽出
  2. 各claimが取得したコンテキストで支持されているかをLLMが判断
  3. Faithfulness = 支持されるclaim数 / 全claim数

例:

質問: 「この製品のバッテリー寿命はどのくらいですか?」
コンテキスト: 「バッテリーは最大12時間持続します。充電時間は2時間です。」
回答: 「バッテリーは12時間持続し、防水機能もあります。」

Claim分析:
- 「バッテリーは12時間持続」→ コンテキストで支持される ○
- 「防水機能もある」→ コンテキストにない ×

Faithfulness = 1/2 = 0.5(低い!ハルシネーション発生)

指標2:Answer Relevancy(回答関連性)

問い:生成された回答はユーザーの質問に実際に答えているか?

コンテキストに忠実だが質問と無関係な回答を検出する。

計算方法

  1. 生成された回答から逆に複数のsynthetic質問を生成
  2. これらのsynthetic質問の埋め込みと元の質問の埋め込み間の平均コサイン類似度を計算
  3. Answer Relevancy = この類似度スコア

例:

元の質問: 「バッテリー寿命はどのくらいですか?」
生成された回答: 「この製品はプレミアム素材で作られており、様々な色があります。」

Synthetic質問の生成:
- 「この製品はどんな素材で作られていますか?」
- 「どんな色がありますか?」

元の質問との類似度: 非常に低い
Answer Relevancy0.1(低い!回答が質問を無視している)

指標3:Context Precision(コンテキスト精度)

問い:取得したコンテキストのうち、実際に役立ったものはどれくらいか?

検索の「ノイズ」を測定する。5つのチャンクを取得したが1つしか実際に有用でなかった場合、低いスコアになる。

計算方法

  • 上位k個のコンテキストの関連するものの割合(Precision@kの加重和)
  • Ground truth回答と比較して各コンテキストの関連性を判断

高いContext Precision = 検索が正確なため、LLMが無関係な情報に惑わされない。

指標4:Context Recall(コンテキスト再現率)

問い:回答に必要な情報をすべて検索で取得できたか?

不足している情報があるかを測定する。Ground truth回答が必要とする情報を検索がすべて見つけてきたかを確認する。

計算方法

  • Ground truth回答の各文がコンテキストで支持されるかを判断
  • Context Recall = コンテキストで支持される文の数 / Ground truth全文数

高いContext Recall = 検索が包括的なため、LLMが回答に必要な情報をすべて持っている。

RAGASの実装

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

# 評価データセットの準備
eval_data = {
    "question": [
        "この製品のバッテリー寿命はどのくらいですか?",
        "返品ポリシーはどうなっていますか?",
        "配送にはどのくらいかかりますか?"
    ],
    "answer": [
        "バッテリーは最大12時間持続します。",
        "ご購入から30日以内に返品申請が可能です。",
        "通常配送は3〜5営業日かかります。"
    ],
    "contexts": [
        [
            "バッテリーは最大12時間持続します。充電時間は2時間です。",
            "製品サイズは15cm x 10cm x 2cmです。"
        ],
        [
            "お客様はご購入から30日以内に返品を申請できます。",
            "返金は営業日5〜7日以内に処理されます。"
        ],
        [
            "通常配送:3〜5営業日、速達配送:1〜2営業日",
            "3,000円未満のご注文には送料500円が加算されます。"
        ]
    ],
    "ground_truth": [
        "バッテリー寿命は最大12時間で、充電時間は2時間です。",
        "返品はご購入から30日以内に申請可能で、5〜7営業日以内に処理されます。",
        "通常配送は3〜5営業日、速達配送は1〜2営業日かかります。"
    ]
}

dataset = Dataset.from_dict(eval_data)

# 評価の実行
results = evaluate(
    dataset,
    metrics=[
        faithfulness,
        answer_relevancy,
        context_precision,
        context_recall,
    ]
)

print(results)
# 出力例:
# {
#   'faithfulness': 0.95,
#   'answer_relevancy': 0.88,
#   'context_precision': 0.82,
#   'context_recall': 0.91
# }

df = results.to_pandas()
print(df.to_string())

ジャッジLLMの設定

RAGASはデフォルトでOpenAIをジャッジLLMとして使用するが、他のプロバイダーに変更可能だ:

from ragas.llms import LangchainLLMWrapper
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from ragas.embeddings import LangchainEmbeddingsWrapper

# コスト削減のためgpt-4o-miniを使用
judge_llm = LangchainLLMWrapper(
    ChatOpenAI(model="gpt-4o-mini", temperature=0)
)

embeddings = LangchainEmbeddingsWrapper(OpenAIEmbeddings())

results = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
    llm=judge_llm,
    embeddings=embeddings,
)

自動評価パイプラインの構築(RAGのCI/CD)

RAGシステムの変更があるたびに自動で評価が実行されるパイプラインだ。

# evaluate_rag.py - CI/CDで実行される評価スクリプト

import json
import sys
from datetime import datetime
from pathlib import Path
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
from datasets import Dataset


def load_test_dataset(path: str) -> Dataset:
    """保存されたテストデータセットを読み込む"""
    with open(path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    return Dataset.from_dict(data)


def run_rag_on_testset(testset: Dataset, rag_chain) -> Dataset:
    """テストセットの各質問をRAGで実行してanswersとcontextsを収集"""
    answers = []
    contexts_list = []

    for question in testset["question"]:
        result = rag_chain.invoke({"query": question})
        answers.append(result["result"])
        contexts_list.append([
            doc.page_content
            for doc in result["source_documents"]
        ])

    return testset.add_column("answer", answers).add_column("contexts", contexts_list)


def evaluate_and_gate(
    dataset: Dataset,
    thresholds: dict,
    save_path: str = None
) -> bool:
    """
    評価を実行し、閾値に基づいて合格/不合格を判定。
    """
    results = evaluate(
        dataset,
        metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
    )

    print("\n" + "="*60)
    print(f"RAG評価結果 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print("="*60)

    all_passed = True
    for metric_name, threshold in thresholds.items():
        score = results[metric_name]
        passed = score >= threshold
        status = "PASS" if passed else "FAIL"
        print(f"{metric_name:25s}: {score:.3f} (閾値: {threshold}) [{status}]")
        if not passed:
            all_passed = False

    if save_path:
        result_data = {
            "timestamp": datetime.now().isoformat(),
            "scores": {k: float(v) for k, v in results.items()},
            "passed": all_passed
        }
        Path(save_path).parent.mkdir(parents=True, exist_ok=True)
        with open(save_path, 'a') as f:
            f.write(json.dumps(result_data) + "\n")

    return all_passed


if __name__ == "__main__":
    testset = load_test_dataset("tests/rag_testset.json")

    thresholds = {
        "faithfulness": 0.80,
        "answer_relevancy": 0.75,
        "context_precision": 0.70,
        "context_recall": 0.75,
    }

    passed = evaluate_and_gate(
        testset,
        thresholds=thresholds,
        save_path="results/rag_eval_history.jsonl"
    )

    sys.exit(0 if passed else 1)  # CI/CDで閾値未満ならビルドを失敗させる

GitHub Actionsへの組み込み:

# .github/workflows/rag-evaluation.yml
name: RAG Quality Gate

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  evaluate-rag:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: pip install ragas langchain openai datasets
      - name: Run RAG Evaluation
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: python evaluate_rag.py

評価テストセットの自動生成

テストセットを手動で作るのは時間がかかる。RAGASのTestsetGeneratorで自動化できる。

from ragas.testset.generator import TestsetGenerator
from ragas.testset.evolutions import simple, reasoning, multi_context
from langchain_community.document_loaders import DirectoryLoader

# ドキュメントの読み込み
loader = DirectoryLoader("./docs", glob="**/*.md")
documents = loader.load()

# テストセットジェネレーターの初期化
generator = TestsetGenerator.with_openai()

# テストセットの生成(質問タイプを指定可能)
testset = generator.generate_with_langchain_docs(
    documents,
    test_size=50,
    distributions={
        simple: 0.5,        # シンプルな事実質問 50%
        reasoning: 0.25,    # 推論が必要な質問 25%
        multi_context: 0.25 # 複数ドキュメントが必要な質問 25%
    }
)

# ファイルに保存
testset_df = testset.to_pandas()
testset_df.to_json("tests/rag_testset.json", orient='records', force_ascii=False)

print(f"生成された質問数: {len(testset_df)}")
print("\nサンプル質問:")
print(testset_df[['question', 'ground_truth']].head())

生成される質問タイプの例:

  • Simple:「返品期間は何日ですか?」(直接的な事実検索)
  • Reasoning:「製品AとBのどちらがより長く使えますか?」(比較推論)
  • Multi-context:「現在のラインナップで最も大きなストレージを持つ製品は?」(複数ドキュメントが必要)

RAGASスコアの解釈ガイド

指標危険ゾーン許容範囲良い優秀
Faithfulness0.5未満0.5-0.70.7-0.90.9以上
Answer Relevancy0.6未満0.6-0.750.75-0.90.9以上
Context Precision0.5未満0.5-0.70.7-0.850.85以上
Context Recall0.6未満0.6-0.750.75-0.90.9以上

スコアが低い時の改善方法

Faithfulnessが低い → ハルシネーションが発生している

  • システムプロンプトを強化:「提供されたコンテキストに基づいてのみ回答すること。コンテキストに答えがない場合はその旨を伝えること。」
  • 明示的な引用指示を追加
  • より保守的なモデルに切り替える

Answer Relevancyが低い → 回答が質問を無視している

  • プロンプトで質問をより目立つように強調する
  • コンテキストが多すぎて質問を見失う場合 → 取得するチャンク数を減らす
  • Re-rankingを導入する

Context Precisionが低い → 無関係なチャンクが多く取得されている

  • 検索のtop-kを減らす(5 → 3)
  • Rerankerを追加(Cohere Rerank、BGE Rerankerなど)
  • チャンクサイズを調整する

Context Recallが低い → 必要な情報を検索で見落としている

  • 検索のtop-kを増やす
  • ハイブリッド検索を導入(BM25 + ベクター)
  • チャンキング戦略を見直す(大きすぎるチャンクは関連情報を希薄化する)

継続的改善のループ

これらすべてを組み合わせた持続可能な改善サイクル:

1. ベースラインを測定(RAGASを実行)
        |
2. 最も低いスコアの指標を特定
        |
3. その指標を改善(チャンキング/検索/プロンプトを修正)
        |
4. 再度測定して改善を確認
        |
5. CI/CDにQuality Gateを追加
        |
6. コード変更のたびに自動評価

まとめ

RAGASはRAGシステム開発を「感覚」ベースから「データ」ベースに転換させるツールだ。初めて実行した時、スコアが思ったより低くて驚くかもしれない。それは正常だ — 現実を直視することが最初のステップだ。

小さなテストセット(20〜50件)から始め、CI/CDにQuality Gateを追加し、変更のたびにスコアを追跡しよう。数週間後には、ステークホルダーに共有できる数字として体系的な改善が見えてくる。

この4記事シリーズはRAG構築の核心要素をカバーした:アプローチの選択 → チャンキング → 検索 → 評価。この4つを正しく行えば、プロダクションレベルのRAGシステムを構築できる。