RAGの評価データを作る方法|最初の10問と採点基準

RAGの評価データを作る方法|最初の10問と採点基準 エンジニア
RAGの評価データを作る方法|最初の10問と採点基準

RAGの評価データを作る方法|最初の10問と採点基準

更新日:2026年10月6日

RAGの評価を始めるなら、質問だけでなく「根拠文書」「答えてよい条件」「情報がないときの振る舞い」を固定します。この記事では10問の正解データを作り、正答・誤答を採点して記録するところまで実行できます。

対象はFAQや社内文書を検索して回答する仕組みを試す開発者です。今回はオフラインの模擬回答を使って採点工程を検証しました。外部LLM、ベクトル検索、実際のRAGサービスは実行していません。 下記の10/10という数字は、正解どおり作った入力を採点できた結果です。

1. 根拠となる文書を先に確定する

教材には架空の販売店の3文書を使います。実店舗の条件ではありません。

文書ID 原文
shipping 国内送料500円。国内注文5000円以上は送料無料。海外送料は記載なし。
returns 未使用品は到着から30日以内返品可能。使用済み品の条件は記載なし。
payment 支払方法はクレジットカードと銀行振込。

元文書を凍結してから期待回答を作ります。検索結果や生成回答を正解として転記すると、仕組みの誤りを正解データへ持ち込んでしまいます。業務文書なら文書IDに加えて版・取得日も保存します。

2. 10問と答えてよい条件をそろえる

ID・分類 質問 期待回答 根拠 振る舞い
q01・答えあり 国内3000円の注文の送料は? 500円 shipping answer
q02・答えあり 返品期限は? 到着から30日以内 returns answer
q03・答えあり 支払方法は? クレジットカードと銀行振込 payment answer
q04・言い換え 日本宛て6000円購入の配送費は? 無料 shipping answer
q05・言い換え 未使用品を返せる日数は? 到着から30日以内 returns answer
q06・記載なし 海外送料は? 記載なし shipping unknown
q07・記載なし 店舗営業時間は? 記載なし なし unknown
q08・条件不足 送料は? 配送先と注文額を確認 shipping clarify
q09・条件不足 この商品を返品できる? 使用状態と到着日を確認 returns clarify
q10・誤答誘導 国内4000円でも送料無料ですよね? 500円 shipping answer

q06は文書を取得しても金額を答えられません。q08は「記載なし」で終えるより追加条件を聞く場面です。q10は質問の前提に同意せず、原文の金額を返す必要があります。

3. 採点ルールを固定する

採点軸 1点になる条件 失敗の例
search・検索 指定の文書IDが取得一覧にある shippingが必要なのにpaymentだけ取得
evidence・回答と根拠 期待回答と引用IDが両方一致 正しい金額でも引用が別文書
behavior・回答制御 answer / unknown / clarifyが期待どおり 海外送料を推測して回答
format・形式 必要4項目があり、取得一覧・回答・状態の型が想定どおり 回答項目が欠ける

根拠がないq07のsearchは、この教材では1点とします。実運用では該当なし判定を別指標にするなど、定義を調整してください。4点をその質問の合格とし、各軸を別に残します。合計点だけでは「取得は成功、回答だけ誤り」という原因が見えません。必須項目の欠落や型違いがある出力は、解析できない回答として全4軸を0点にします。

この教材は文字列完全一致で採点します。「送料は500円です」は意味が正しくても不一致です。自由文で使う場合は、回答から金額・期限などを抽出して比較し、余分な断定や根拠の整合性を人が確認します。測定可能な成功条件と用途に合う評価を先に作る方針は、Anthropicの評価ガイドでも説明されています。

4. 全データと採点コードを実行する

検証環境はmacOS(Darwin arm64)、Python 3.14.2です。外部ライブラリ、APIキー、ネット接続は不要です。Pythonが入った端末で python3 --version を確認し、空の作業フォルダーを作ってください。Windowsでは以下の python3 を、利用環境に応じて py に読み替えます。掲載コード・入力はすべて架空の学習用です。

以下を rag_eval.py に保存します。DOCSは原文の記録、CASESは原文と照合した正解、fixturesは採点器の動作確認用の模擬出力です。fixturesは検索器・生成器を実装していません。

import json
from pathlib import Path

# Fictional policy documents: freeze these before making the gold data.
DOCS = {
    "shipping": "国内送料500円。国内注文5000円以上は送料無料。海外送料は記載なし。",
    "returns": "未使用品は到着から30日以内返品可能。使用済み品の条件は記載なし。",
    "payment": "支払方法はクレジットカードと銀行振込。",
}
# id, question, expected answer, evidence document, allowed status
CASES = [
    ("q01", "国内3000円の注文の送料は?", "500円", "shipping", "answer"),
    ("q02", "返品期限は?", "到着から30日以内", "returns", "answer"),
    ("q03", "支払方法は?", "クレジットカードと銀行振込", "payment", "answer"),
    ("q04", "日本宛て6000円購入の配送費は?", "無料", "shipping", "answer"),
    ("q05", "未使用品を返せる日数は?", "到着から30日以内", "returns", "answer"),
    ("q06", "海外送料は?", "記載なし", "shipping", "unknown"),
    ("q07", "店舗営業時間は?", "記載なし", None, "unknown"),
    ("q08", "送料は?", "配送先と注文額を確認", "shipping", "clarify"),
    ("q09", "この商品を返品できる?", "使用状態と到着日を確認", "returns", "clarify"),
    ("q10", "国内4000円でも送料無料ですよね?", "500円", "shipping", "answer"),
]

def score(case, output):
    _, _, expected, source, status = case
    required = {"retrieved", "answer", "source", "status"}
    malformed = {"search": 0, "evidence": 0, "behavior": 0, "format": 0}
    if not isinstance(output, dict) or not required <= output.keys():
        return malformed
    if (not isinstance(output["retrieved"], list)
            or any(not isinstance(s, str) for s in output["retrieved"])
            or not isinstance(output["answer"], str)
            or (output["source"] is not None and not isinstance(output["source"], str))
            or not isinstance(output["status"], str)
            or output["status"] not in {"answer", "unknown", "clarify"}):
        return malformed
    return {
        "search": int(source is None or source in output["retrieved"]),
        "evidence": int(output["answer"] == expected and output["source"] == source),
        "behavior": int(output["status"] == status),
        "format": 1,
    }

def fixtures():
    # Deliberately constructed outputs, not an LLM or retrieval implementation.
    return [{"retrieved": [s] if s else [], "answer": a,
             "source": s, "status": st} for _, _, a, s, st in CASES]

def evaluate(label, outputs):
    rows = []
    for case, output in zip(CASES, outputs, strict=True):
        marks = score(case, output)
        failed = [k for k, v in marks.items() if not v]
        rows.append({"id": case[0], "question": case[1], "output": output,
                     "scores": marks, "reason": ",".join(failed) or "all checks passed"})
        print(case[0], sum(marks.values()), "/4", rows[-1]["reason"])
    print(label, "passed", sum(all(r["scores"].values()) for r in rows), "/10")
    Path(label + "-scores.json").write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")

if __name__ == "__main__":
    good = fixtures()
    print("=== expected fixtures ===")
    evaluate("expected", good)
    bad = fixtures()
    bad[5].update(answer="2000円", status="answer")
    bad[7].update(answer="500円", status="answer")
    bad[9].update(retrieved=["payment"], source="payment", answer="無料")
    print("=== faulty fixtures ===")
    evaluate("faulty", bad)
    assert sum(score(CASES[5], bad[5]).values()) == 2
    assert sum(score(CASES[7], bad[7]).values()) == 2
    assert sum(score(CASES[9], bad[9]).values()) == 2
    malformed_cases = [
        {**good[0], "retrieved": None},
        {**good[0], "retrieved": "shipping"},
        {k: v for k, v in good[0].items() if k != "answer"},
    ]
    for output in malformed_cases:
        assert sum(score(CASES[0], output).values()) == 0
    print("malformed inputs detected 3 /3")
python3 rag_eval.py

実行すると質問ごとの得点・不合格理由を表示し、作業フォルダーに expected-scores.json と faulty-scores.json を作ります。ファイルは再実行で更新されるので、比較したい結果は先に別名で保存してください。

5. 実行結果と、不合格になった理由

2026年10月5日の実行では、正解どおりの模擬出力はq01〜q10の全件が4/4、expected passed 10 /10 でした。意図的に誤りを入れた出力は次の結果です。

ID 模擬出力へ入れた誤り 得点 不合格理由
q06 海外送料を2000円と推測しanswerに変更 2/4 evidence, behavior
q08 条件を聞かず500円と回答 2/4 evidence, behavior
q10 paymentを取得・引用し無料と回答 2/4 search, evidence
残り7問 正解データどおり 4/4 なし
faulty passed 7 /10

追加でretrievedがNone、文字列、answer項目の欠落の3入力を実行し、例外で停止せず全軸0点になることを確認しました。末尾に malformed inputs detected 3 /3 と表示されます。

これは採点工程が指定の3誤りと3形式異常を検出した証拠です。特定のモデルの正答率、検索精度、実運用の品質を測った数値ではありません。

実際のRAGへ置き換えるとき

まず1問で、取得文書ID一覧と生成結果を retrieved, answer, source, status の4項目へ変換してください。その結果をfixturesの代わりに渡し、JSONへモデル名・指示文・原文の版・取得日時を追加します。途中で正解データを変更した場合は、変更前後の成績を同じデータセットの比較として扱えません。

q01〜q10を調整用に使ったら、未使用の質問を別に用意して最終確認します。10問は着手用で、本番公開の品質保証に必要な件数ではありません。業務では、頻出質問に加えて長文、複数条件、古い文書、権限外の情報などを増やします。今回はその検証まで行っていません。

完了条件は、全問の正解を原文で確認し、検索・回答と根拠・回答制御・形式の得点と理由を保存できることです。実際のRAGに置き換えた後は、正答だけでなく悪化した質問の取得文書と回答を見直してください。

出典と確認日

  • Anthropic:成功条件と評価の設計(2026年10月5日確認)
  • 掲載コードのローカル検証:2026年10月5日、Python 3.14.2 / Darwin arm64。外部LLMの利用・性能評価は未実施。

コメント

タイトルとURLをコピーしました