📏

RAGをどう評価する? — LLM採点で試すRAGAS的指標

に公開

はじめに

ルミナイR&Dチームの栗原です。
これまでの記事では、レビュー論文 “Retrieval-Augmented Generation for Large Language Models: A Survey” (Gao et al., 2024) を手がかりに、

  • Fig.2 / Fig.3:RAG の基本フローと
    Naive / Advanced / Modular RAG の違い
  • Fig.4:Prompt / Fine-tuning / RAG の「守備範囲」と使い分け方
  • Fig.5:Iterative / Recursive / Adaptive Retrieval という、エージェント寄りRAGのフロー

をざっくり整理してきました。

そろそろ出てくるのが、次の悩みです。

「RAG をそれっぽく組めたけど、
これってどのくらい“いい”の?

  • Retriever を差し替えたら本当に良くなったのか?
  • ハイブリッド検索+再ランクは効いているのか?
  • 「なんか良さそう」以上のことが言えない…

このモヤモヤに答えるのが、RAG の評価です。

本記事では、Gao et al. の Evaluation まわりの議論と、既存ツール(RAGAS / TruLens など)の発想を参考にしつつ、

  • 何を評価すべきか(評価軸)
  • どんな評価方法があるか(人手 / 自動 / LLM-as-judge)
  • Python + OpenAI で動く 最小限の「LLMによる採点スクリプト」

をまとめます。

参照:“Retrieval-Augmented Generation for Large Language Models: A Survey” — Gao et al., 2024

https://arxiv.org/abs/2312.10997v5

この記事で学べること

  • RAG を評価するときに見るべき 4 つの観点
  • 人手評価・自動指標・LLM による採点(RAGAS的アプローチ)の違い
  • Python + OpenAI API だけで試せる、RAG 出力の採点スクリプト(ミニ版RAGAS)

1. 「RAGが良い」とは何か?評価軸を分解する

まずは「何を測るのか」を整理します。
ここでは、RAG の評価軸をざっくり 4つ に分けて考えます。

1.1 RAG評価の4つの観点(図1)

ざっくり整理すると、こんな感じのマトリクスになります。

表1. RAG 評価の4つの観点

観点 ざっくり意味 典型的な指標例
Answer Correctness 質問に対して、内容がどれくらい正しいか EM, F1, 5段階スコア
Faithfulness (忠実さ) 回答がコンテキスト文書にどれくらい忠実か LLM採点・RAGAS の忠実度スコア
Context Relevance 取得したチャンクが質問とどれくらい関係あるか nDCG, MRR, 人手ラベル
Retrieval Quality 「正解チャンク」を上位に持って来れているか Recall@k, Hit@k, Precision@k

それぞれもう少し言い換えると:

  • Answer Correctness
    • ユーザー視点で一番分かりやすい軸。「答えとして合ってる?」
  • Faithfulness
    • 回答がちゃんと渡したドキュメントの範囲で完結しているか
      コンテキストにない内容を「それっぽく」作っていないか。
  • Context Relevance
    • 取得チャンクの良し悪し。「そもそも関係ありそうなところを引けてる?」という話。
  • Retrieval Quality
    • 正解アノテーションがある前提で、「正解チャンクのランク」がどうなっているか。

RAG を評価するときは、この 4 つのどれを重視したいのかを最初に決めておくと、
あとで「指標が増えすぎて訳が分からない問題」を防げます。

1.2 ユースケースごとに優先する軸が違う

同じ RAG でも、用途によって重要な軸が変わります。

  • 社内 FAQ ボット
    Answer Correctness / Faithfulness が最重要。
    多少チャンク選びが雑でも、答えが正しければよいケースが多い。
  • 社内検索UI(「どの文書を読めばいいか」を教える用途)
    Context Relevance / Retrieval Quality 寄りの評価が効いてくる。
  • 自動レポート生成・ドラフト生成
    → Answer Correctness & Faithfulness に加えて、
    「読みやすさ」「論点の抜け漏れ」のような、人間的な評価軸も必要。

この記事の後半で書く LLM-as-judge(LLM に採点させる方式) は、

  • Answer Correctness
  • Faithfulness

の 2 つを一度にざっくり見る、というイメージで使います。

2. 評価の3パターン:人手 / 自動指標 / LLM-as-judge

次に、「どうやって評価するか」の話です。
ざっくり分けると、手段は 3 つあります。

  1. 人手評価(human evaluation)
  2. 自動指標(EM / F1 / nDCG / Recall@k …)
  3. LLM-as-judge(LLM に採点させる)

2.1 人手評価:もっとも信頼できるが、スケールしにくい

一番ストレートなのは、人間が直接見る方法です。

  • 質問
  • RAG の回答
  • (あれば)コンテキスト文書

を見ながら、次のような基準でラベリングします。

  • 正しさ:◎ / ○ / △ / ✕
  • 忠実さ:
    • コンテキストから明らかに導ける
    • グレー(推測入ってそう)
    • コンテキストと矛盾している

メリット

  • もっとも直感的で、信頼しやすい
  • 評価コメントも一緒に残せば、改善のヒントになりやすい

デメリット

  • コストが重い(人件費・時間)
  • サンプル数を増やしにくい
  • 評価者によって基準がぶれやすい

→ 現実的には、少数サンプルでのスポットチェック に落ち着きがちです。

2.2 自動指標:速いが、扱えるタスクが限られる

次に、既存の自動指標です。

  • QA タスクなら:
    • ゴールドアンサーとの EM(Exact Match) / F1 など
  • Retrieval タスクなら:
    • 正解文書IDに対する nDCG / MRR / Recall@k / Hit@k

メリット

  • 大量サンプルを一気に評価できる
  • 値が明確で、実験の比較に使いやすい

デメリット

  • ゴールドアンサーや正解ドキュメントIDが必要
  • 「言い換え」「部分的に正しい回答」の扱いが難しい
  • Faithfulness(忠実さ)までは測りづらい

RAG の現場では、「Retrieval 部分の A/B テスト」には自動指標、
「生成(LLM)の出力品質」には次の LLM-as-judge を組み合わせる、という構成が多いです。

2.3 LLM-as-judge:RAGAS的な発想

3つめが、LLM に採点させる 方法です。
RAGAS や TruLens などのライブラリが採用している発想でもあります。

ざっくり流れはこうです。

  1. 質問(question)
  2. RAG の回答(prediction)
  3. RAG に渡したコンテキスト文書(contexts)

をすべて LLM に渡し、

  • 回答の正しさ(correctness)
  • 文脈への忠実さ(faithfulness)

を 1〜5 のスコアなどで採点してもらいます。

メリット

  • 言い換えやニュアンスをある程度理解してくれる
  • 人手評価に近い感覚を、そこそこのスケール で回せる
  • 評価コメントも一緒に生成させられる

デメリット

  • 採点する LLM のクセ(モデル・プロンプト)に依存する
  • 本番と同じモデルで採点すると「自画自賛」になりがち
  • 絶対値としての「80点だから良い」とは言いづらい(比較用だと割り切る必要)

このあとのセクションでは、

  • 小さな評価用データセット(question / ground_truth / contexts)
  • 既存の answer(question, mode="rag")(前回までの RAG 実装を想定)

を前提にして、

Python + OpenAI API だけで動く「LLM採点スクリプト(correctness+faithfulness)」

を、ミニ版 RAGAS として書いていきます。

3. LLMに採点させるミニ評価スクリプト(RAGASもどき)

ここからは実際に、LLM-as-judge で RAG を評価する最小スクリプトを書いてみます。

前提として:

  • すでに前回までのコードなどで answer(question: str, mode="rag") -> str が定義されている想定です
    (なければダミー関数を作ってください)
  • 小さな評価用データを Python 内にベタ書きします

3.1 評価用データセットを作る

まずは、数件で良いので 「質問+正解ラベル+コンテキスト」 のセットを用意します。

# 評価用の小さなサンプル(実際には自分のドメインに合わせて書き換えてください)
EVAL_DATA = [
    {
        "id": "q1",
        "question": "新製品AのAPI認証方式は何ですか?",
        "ground_truth": "OAuth2",
        "contexts": [
            "新製品Aでは、2025年10月以降、APIの認証方式をOAuth2に統一した。",
            "旧バージョンではAPIキー認証も一部で利用されていた。"
        ],
    },
    {
        "id": "q2",
        "question": "社内データを外部サービスに送る場合のルールを教えてください。",
        "ground_truth": "情報セキュリティ部門の承認が必要",
        "contexts": [
            "社内データを外部に送信する場合は、必ず情報セキュリティ部門の承認を得ること。",
        ],
    },
]

3.2 RAGシステムを一通り走らせる

次に、各質問に対して自分の RAG システムを実行し、予測結果を集める部分を書きます。

from typing import Dict, Any, List

# 例: 以前の記事の RAG 実装
# from my_rag_impl import answer
def answer(question: str, mode: str = "rag") -> str:
    """ここではダミー。実際は自分の RAG 実装をインポートしてください。"""
    return f"【ダミー回答】{question}"


def run_rag_on_eval(eval_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    rows = []
    for ex in eval_data:
        pred = answer(ex["question"], mode="rag")
        rows.append(
            {
                "id": ex["id"],
                "question": ex["question"],
                "ground_truth": ex["ground_truth"],
                "contexts": ex["contexts"],
                "prediction": pred,
            }
        )
    return rows


results = run_rag_on_eval(EVAL_DATA)
print(results[0])

ここまでで、各サンプルについて

  • question
  • ground_truth
  • contexts
  • prediction(RAG の回答)

が 1 つの dict にまとまったリスト results が得られます。

3.3 採点プロンプトを設計する

続いて、OpenAI API を使って correctness(正しさ)faithfulness(忠実さ) を 1〜5 の整数で採点させます。

import os
import json
from openai import OpenAI

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

JUDGE_SYSTEM = """\
あなたはRAGシステムの評価者です。
与えられた「質問」「生成された回答」「コンテキスト文書」を読み、
次の2つを5段階(1〜5)で評価してください。

1. correctness: 回答が質問に対してどれくらい正しいか
   - 1: ほぼ誤り
   - 3: 部分的には正しいが不十分
   - 5: ほぼ完全に正しい

2. faithfulness: 回答の内容がコンテキストにどれくらい忠実か
   - 1: コンテキストと矛盾している or ほぼ全てが推測
   - 3: 一部はコンテキスト由来だが推測も混ざる
   - 5: ほとんどがコンテキストから導ける内容

出力は必ず、次のJSON形式1行で出してください:
{"correctness": int, "faithfulness": int}
"""


def score_one(example: Dict[str, Any]) -> Dict[str, Any]:
    ctx_lines = "\n".join(f"- {c}" for c in example["contexts"])
    prompt = f"""[質問]
{example['question']}

[回答]
{example['prediction']}

[コンテキスト]
{ctx_lines}

上記を読み、採点してください。
"""

    resp = client.responses.create(
        model="gpt-4o-mini",
        input=[
            {"role": "system", "content": JUDGE_SYSTEM},
            {"role": "user", "content": prompt},
        ],
        max_output_tokens=200,
        temperature=0.0,
    )
    text = resp.output_text.strip()
    scores = json.loads(text)

    return {
        **example,
        "correctness": scores["correctness"],
        "faithfulness": scores["faithfulness"],
    }

ここで大事なのは:

  • system プロンプト側で 評価基準と出力形式をかなり厳密に指定していること
  • 出力を json.loads でパースする前提なので、形式のブレを許さないようにしていること

です。

3.4 全サンプルを採点して、平均スコアを見る

最後に、すべてのサンプルに対して score_one を回し、平均スコアを計算します。

import statistics as stats

scored = [score_one(ex) for ex in results]

avg_corr = stats.mean(r["correctness"] for r in scored)
avg_faith = stats.mean(r["faithfulness"] for r in scored)

print(f"平均 correctness: {avg_corr:.2f}")
print(f"平均 faithfulness: {avg_faith:.2f}")

# 1件分だけ中身を見てみる
print("--- サンプル1件目 ---")
print(f"Q: {scored[0]['question']}")
print(f"Prediction: {scored[0]['prediction']}")
print(f"Correctness: {scored[0]['correctness']}")
print(f"Faithfulness: {scored[0]['faithfulness']}")

これだけで、

  • 自分の RAG 構成(Retriever / 再ランク / 圧縮 など)を変えたときに、
  • correctness / faithfulness の平均スコアがどう変わるか

簡単に A/B 比較できるようになります。

本格的にやるなら、この結果を pandas の DataFrame にして CSV で保存したり、
LangSmith / Weights & Biases などにログ送信して可視化してもよいと思います。

4. LLM-as-judgeの落とし穴と、実務での使い方

最後に、LLM 採点を使うときの注意点と、現場での落としどころを簡単にまとめます。

4.1 よくある落とし穴

  • 本番と同じモデルで自分を採点している
    • → 多少「自画自賛」になりやすい
    • 余裕があれば、評価には別のモデル(or バージョン)を使うと安全です。
  • 評価プロンプトを変えるとスコア分布も変わる
    • → JUDGE_SYSTEM の内容も「評価仕様」の一部として固定・管理しておくのがおすすめです。
  • 絶対値を信じすぎる
    • → 「correctness 4.2 だから良い/悪い」と見るより、
      構成 A と B の差分を見るためのツールと割り切る方が現実的です。

4.2 実務での使い方のイメージ

  • 開発初期:
    • 少数の EVAL_DATA と LLM-as-judge を使って、
      「BM25のみ vs ベクトルのみ vs ハイブリッド」などをざっくり比較。
  • 本番直前:
    • クリティカルな質問セットだけは、人手評価も混ぜてダブルチェック。
  • 本番運用:
    • ログからサンプリングした Q&A を、定期的に LLM-as-judge でスコアリングし、
      「最近スコアが落ちていないか?」をモニタリング。

RAG の評価は、どうしても

  • 指標が多い
  • 実装も重い

方向に行きがちですが、まずはこの記事のようなミニ版から始めて、

  1. 小さな EVAL_DATA を用意する
  2. 既存の answer(question, mode="rag") に対して LLM 採点を回す
  3. 構成を変えたときのスコア差を見る

というループが回せるようになると、RAG のチューニングがかなりやりやすくなるはずです。


【現在採用強化中です!】

  • AIエンジニア
  • PM/PdM
  • 戦略投資コンサルタント

▼代表とのカジュアル面談URL
https://pitta.me/matches/VCmKMuMvfBEk

ルミナイ - 産業データをLLM Readyにするための技術ブログ

Discussion