7月14日、Shittu Olumideが「LLM Evaluation Frameworks Compared: How to Actually Measure What Your Model Does」と題した記事を公開した。この記事では、LLMアプリケーションの評価に使われる3大オープンソースフレームワーク(RAGAS・DeepEval・Promptfoo)の使い分けと、それらが依存する「LLM-as-a-judge」機構に存在するバイアスへの対処法について詳しく紹介されている。
LLMアプリのリリース後、プロンプトの微調整が静かに品質を壊していくのに誰も気づかない——これが典型的な障害モードだ。従来のコードはスタックトレースで壊れるが、LLMの出力は「自信満々に、もっともらしく間違える」。手動でざっと見るだけでは見逃すタイプの失敗である。
「LLM評価」は3種類ある——どれを使うかの判断が最重要
記事がまず整理するのは、「LLM評価」と一口に言っても性質が異なる3カテゴリの区別だ。
- モデルベンチマーク:MMLU・GSM8K・HumanEvalなど学術的な標準タスクでモデル能力を比較する。lm-evaluation-harnessがデファクト。GPT-5とClaudeのどちらを採用するか検討するときに使うが、「自分のアプリが実際に動くか」はほぼわからない。
- アプリケーション評価:RAGパイプラインやチャットボットが、自社のデータ・プロンプトに対して正しく・根拠ある・安全な出力を返すかを問う。RAGAS・DeepEval・Promptfooの出番はここ。
- プロダクションモニタリング:デプロイ後のライブトラフィックを監視し、オフラインテストでは想定外だったリグレッションや分布シフトを捉える。LangSmith・Braintrust・Arize Phoenixの領域。
「どの評価フレームワークを使えばいい?」と聞く人の大半が必要なのは2番目のカテゴリで、3番目と組み合わせることが多い。
3フレームワークの使い分け
| カテゴリ | RAGAS | DeepEval | Promptfoo |
|---|---|---|---|
| 最適ユースケース | RAG特化スコアリング | CI/CDの品質ゲート | マルチモデル比較・レッドチーム |
| 統合スタイル | Pythonライブラリ | pytest-native | YAML + CLI |
| 強みのメトリクス | Faithfulness、Context Precision/Recall | 14種以上(バイアス・毒性含む) | セキュリティ攻撃ベクター(500種以上) |
| 組み合わせ先 | DeepEval(広範カバレッジ) | RAGAS(RAG特化の深さ) | どちらとも |
※プロダクション監視は3フレームワークいずれも対象外。デプロイ後の継続監視にはLangSmith・Braintrust・Arize Phoenixといった専用ツールと組み合わせるのが一般的だ。
RAGASとDeepEvalは競合ではない、というのが記事の核心的な指摘だ。RAGASが検索特化の指標を担い、DeepEvalがその他すべてを同一CIパイプライン内で処理するという併用パターンが、プロダクションチームの間で収束しつつある。
PromptfooはCLI・YAML駆動でプロンプトエンジニアリングの反復や、500種以上の攻撃ベクターを内蔵したレッドチーム・セキュリティテストが主戦場だ。
コードで見るFaithfulnessチェックの仕組み
RAGASのFaithfulnessメトリクス(根拠整合性)の核心ロジックは「回答をアトミックなクレームに分解し、各クレームが検索コンテキストに裏付けられているか確認する」ことだ。コンテキストに支持されないクレームがハルシネーションである。
以下は、その仕組みを標準ライブラリのみで動かせるデモ実装だ(本番ではRAGASのLLMジャッジに差し替える)。処理の流れは「①回答を文単位に分解 → ②各クレームとコンテキストの語彙重複率を計算 → ③重複率が閾値未満のクレームをハルシネーションと判定」の3ステップだ:
# faithfulness_check.py — 標準ライブラリ(re)のみ。python faithfulness_check.py で実行可
import re
def decompose_claims(answer: str) -> list[str]:
sentences = re.split(r'(?<=[.!?])\s+', answer.strip())
return [s.strip() for s in sentences if s.strip()]
def claim_supported_by_context(claim: str, context: str) -> bool:
claim_words = set(re.findall(r'\b[a-zA-Z]{4,}\b', claim.lower()))
context_words = set(re.findall(r'\b[a-zA-Z]{4,}\b', context.lower()))
if not claim_words:
return True
overlap = len(claim_words & context_words) / len(claim_words)
return overlap >= 0.5 # ← 語彙重複率50%を閾値とする簡易ルール
def compute_faithfulness(answer: str, context: str) -> dict:
claims = decompose_claims(answer)
supported = [c for c in claims if claim_supported_by_context(c, context)]
score = len(supported) / len(claims) if claims else 1.0
return {
"score": round(score, 3),
"unsupported_claims": [c for c in claims if c not in supported],
}
# --- 検証 ---
context = "Abuja became the capital of Nigeria in 1991, replacing Lagos."
r1 = compute_faithfulness("The capital of Nigeria is Abuja. It became the capital in 1991.", context)
print(f"Grounded → score: {r1['score']}, unsupported: {r1['unsupported_claims']}")
r2 = compute_faithfulness(
"The capital of Nigeria is Abuja. It became the capital in 1991. "
"The city has a population of over 3 million people.", context)
print(f"Hallucinated → score: {r2['score']}, unsupported: {r2['unsupported_claims']}")
出力:
Grounded → score: 1.0, unsupported: []
Hallucinated → score: 0.667, unsupported: ['The city has a population of over 3 million people.']
「人口3百万人以上」という記述は一見もっともらしく、手動レビューなら通過しやすい。しかしFaithfulnessチェックは一般的なもっともらしさではなく、実際の検索コンテキストに照らして判定するため、これを検出できる。
LLM-as-a-judgeに存在する3つのバイアス
記事が特に力を入れているのが、多くの比較記事が省略するこのセクションだ。RAGAS・DeepEval・Promptfooのすべてが依存する「LLM-as-a-judge」(LLMに別の出力を評価させる仕組み)には、学術論文で実測済みの系統的バイアスが存在する。
- ポジションバイアス:LLMジャッジは、複数の選択肢が提示された場合に最初(または最後)に登場した回答を高く評価しやすい。
- 自己優先バイアス(Self-preference bias):ジャッジとして使うモデルが、同じモデルが生成した回答を他モデルの回答より高く評価する傾向がある。
- 冗長性バイアス(Verbosity bias):長く詳細に書かれた回答を、実際の正確さと関係なく高評価しやすい。
記事では、これらのバイアスを検出するための「監査ハーネス」の実装と、プロダクションでの軽減策(回答の提示順をランダム化する、異なるモデルをジャッジに使うなど)も紹介されている。
「ツールを信じるのではなく、ツールのバイアスを設計で回避する」というのが記事全体を通じた姿勢だ。
詳細はLLM Evaluation Frameworks Compared: How to Actually Measure What Your Model Doesを参照していただきたい。