LiteLLMとRagasでRAGパイプラインのFaithfulness・AnswerRelevancy・ContextRecall・ContextPrecisionを評価してみる
はじめに
データ事業本部のkobayashiです。
LiteLLM × LangGraph シリーズ20回 + Recap で挙げた「シリーズで扱わなかったトピック」の続編シリーズ2本目として、RAG 評価フレームワーク Ragas をまとめます。1本目(LangGraph Functional API)と合わせてご覧ください。
シリーズ第15回で取り上げた DeepEval は LLM-as-judge 全般を扱う汎用フレームワークでしたが、Ragas は RAG パイプラインの評価に最適化されており、検索の質と生成の質を切り分けて測れる専用メトリクスが揃っています。シリーズ第4回の RAG パターン記事と組み合わせると、検索→生成のパイプラインを定量的に評価できるようになります。
Ragas とは
Ragas は LLM アプリケーション、特に RAG パイプラインの評価に最適化された OSS の Python ライブラリです。Faithfulness / Answer Relevancy / Context Recall / Context Precision という、RAG 特有の評価軸を LLM-as-judge で算出するメトリクスが標準で揃っています。
主な特徴は以下です。
- RAG 特有の評価軸が揃っている: 「回答が文脈に忠実か」「文脈は質問に十分か」「不要な文脈が混じっていないか」など、RAG の典型的な失敗モードを切り分けて測れる
SingleTurnSample+EvaluationDataset:user_input(質問)/response(評価対象システムの回答)/retrieved_contexts(検索された context)/reference(正解データ。ground truth)という RAG 評価の標準4要素をデータモデルとして提供- judge / embeddings をライブラリ非依存に差し替えられる:
LangchainLLMWrapper/LangchainEmbeddingsWrapperで LangChain のBaseChatModelベースのモデル(ChatLiteLLM含む)をそのまま judge / embedding として使える evaluate()で複数メトリクス × 複数サンプルの一括採点: pandas 連携で結果を表形式で扱える
主要メトリクス
| メトリクス | 評価する内容 | 必須入力 |
|---|---|---|
Faithfulness |
回答がコンテキストに対して忠実か(事実関係の整合) | user_input / response / retrieved_contexts |
AnswerRelevancy |
回答が質問に対して関連しているか(脱線していないか) | user_input / response(+ embeddings) |
LLMContextRecall |
検索結果が正解に対して網羅的か | user_input / retrieved_contexts / reference |
LLMContextPrecisionWithReference |
検索結果にノイズが少ないか(必要な部分だけ含まれているか) | user_input / retrieved_contexts / reference |
FactualCorrectness |
回答が reference に事実として一致するか |
response / reference |
Faithfulness vs AnswerRelevancy は混同しやすいですが、前者は「コンテキストに対して事実か」、後者は「質問に対して関連した回答か」という違いです。前者だけ高いとオウム返し、後者だけ高いとハルシネーション(事実と異なる回答の生成)という見方ができます。
環境
今回使用した環境は以下の通りです。
Python 3.13
litellm 1.83.14
langchain-litellm 0.6.4
langchain-core 1.3.2
langchain-openai 1.1.10
ragas 0.4.3
datasets 4.8.5
deepeval 3.9.9
pandas 3.0.2
$ uv pip install litellm langchain-litellm langchain-core langchain-openai ragas datasets deepeval pandas
$ export OPENAI_API_KEY="sk-..." # embeddings + デフォルト judge 用
$ export ANTHROPIC_API_KEY="sk-ant-..." # Anthropic judge を試す場合
OPENAI_API_KEY は AnswerRelevancy の embeddings 計算(text-embedding-3-small)と DeepEval のデフォルト judge に使います。各サンプルの先頭で litellm.drop_params = True を入れているのは、gpt-5-mini のような新世代モデルが一部のパラメータ(temperature=0.01 など)を未対応として弾くため、未対応パラメータを LiteLLM 側で安全に落とすための保険です。
基本: 主要4メトリクスを1サンプルで動かす
最小例として、SingleTurnSample を1件作って4つのメトリクスを順番にスコアリングします。なお本サンプルでは評価フローだけを示すため、実際の RAG システムは動かさず、response と retrieved_contexts はダミーデータとして手書きで用意しています(実運用では生成 LLM の出力と検索結果をそのまま詰める想定です)。
"""Ragas 主要メトリクスを 1 サンプルで動かす最小例。
Faithfulness / AnswerRelevancy / LLMContextRecall / LLMContextPrecisionWithReference
の 4 メトリクスを順番にスコアリングする。
"""
import warnings
# ragas 0.4 では `ragas.metrics` からの import が DeprecationWarning を出すため抑制
warnings.filterwarnings("ignore", category=DeprecationWarning)
import litellm
from langchain_litellm import ChatLiteLLM
from langchain_openai import OpenAIEmbeddings
from ragas.dataset_schema import SingleTurnSample
from ragas.embeddings import LangchainEmbeddingsWrapper
from ragas.llms import LangchainLLMWrapper
from ragas.metrics import (
AnswerRelevancy,
Faithfulness,
LLMContextPrecisionWithReference,
LLMContextRecall,
)
# gpt-5 系は temperature=0.01 などの一部パラメータを未対応のため、未対応パラメータを自動で落とす
litellm.drop_params = True
# Ragas の judge モデルとして ChatLiteLLM を LangchainLLMWrapper でラップする
evaluator_llm = LangchainLLMWrapper(
ChatLiteLLM(model="openai/gpt-5-mini"),
)
# AnswerRelevancy には embeddings が必要
evaluator_embeddings = LangchainEmbeddingsWrapper(
OpenAIEmbeddings(model="text-embedding-3-small"),
)
# 評価対象のサンプル
sample = SingleTurnSample(
user_input="リモートワークのルールを教えてください",
response="リモートワークは週3日まで可能です。事前申請は不要ですが、勤怠システムへの記録が必要です。",
retrieved_contexts=[
"リモートワークは週3日まで可能です。事前申請は不要ですが、勤怠システムへの記録が必要です。",
],
reference="リモートワークは週3日まで可能で、勤怠システムへの記録が必要。",
)
metrics = [
Faithfulness(llm=evaluator_llm),
AnswerRelevancy(llm=evaluator_llm, embeddings=evaluator_embeddings),
LLMContextRecall(llm=evaluator_llm),
LLMContextPrecisionWithReference(llm=evaluator_llm),
]
print("=== Ragas 主要メトリクス ===")
for metric in metrics:
score = metric.single_turn_score(sample)
print(f" {metric.__class__.__name__:>40s}: {score:.3f}")
ポイントは2つです。
LangchainLLMWrapper(ChatLiteLLM(...))で judge を LiteLLM 経由にできる。これにより judge をモデル名の文字列を変えるだけで OpenAI / Anthropic / Gemini / Bedrock など横断で切り替えられるsingle_turn_score(sample)が同期 API。バッチ評価の場合は後述のevaluate()を使う
実行結果は以下のようになりました。
$ python basic_metrics.py
=== Ragas 主要メトリクス ===
Faithfulness: 1.000
AnswerRelevancy: 0.576
LLMContextRecall: 1.000
LLMContextPrecisionWithReference: 1.000
Faithfulness / LLMContextRecall / LLMContextPrecisionWithReference は満点。AnswerRelevancy が 0.576 なのは、Ragas 内部で「回答から逆に質問を複数生成し、元の質問とのコサイン類似度の平均」を取る計算をしているためです。短い質問に対しては中程度の値に収まりやすく、また「分かりません」のような曖昧回答にはペナルティが入る仕様(noncommittal flag)も含まれています。
データセット一括評価
複数サンプルを EvaluationDataset にまとめて ragas.evaluate() に渡すと、メトリクス × サンプルを並列で採点してくれます。to_pandas() で結果を DataFrame として扱えるため、CI で閾値判定したり傾向を可視化したりする用途に向きます。
"""EvaluationDataset を使って複数サンプルを ragas.evaluate() で一括評価する。
`to_pandas()` で結果を表として可視化する。
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import litellm
from langchain_litellm import ChatLiteLLM
from langchain_openai import OpenAIEmbeddings
from ragas import evaluate
from ragas.dataset_schema import EvaluationDataset, SingleTurnSample
from ragas.embeddings import LangchainEmbeddingsWrapper
from ragas.llms import LangchainLLMWrapper
from ragas.metrics import (
AnswerRelevancy,
Faithfulness,
LLMContextPrecisionWithReference,
LLMContextRecall,
)
litellm.drop_params = True
evaluator_llm = LangchainLLMWrapper(ChatLiteLLM(model="openai/gpt-5-mini"))
evaluator_embeddings = LangchainEmbeddingsWrapper(
OpenAIEmbeddings(model="text-embedding-3-small"),
)
# 5 件のサンプル: 3 件は良回答、2 件は劣化回答(ハルシネーション・過不足)
samples = [
# 1: 良い回答
SingleTurnSample(
user_input="有給休暇は何日付与されますか?",
response="入社6ヶ月後に10日付与されます。",
retrieved_contexts=["有給休暇は入社6ヶ月後に10日付与されます。申請は3営業日前までに上長承認が必要です。"],
reference="入社6ヶ月後に10日付与される。",
),
# 2: 良い回答
SingleTurnSample(
user_input="リモートワークは週何日まで可能ですか?",
response="週3日まで可能です。事前申請は不要ですが、勤怠システムへの記録が必要です。",
retrieved_contexts=["リモートワークは週3日まで可能です。事前申請は不要ですが、勤怠システムへの記録が必要です。"],
reference="週3日まで可能。記録は必要だが事前申請は不要。",
),
# 3: 良い回答
SingleTurnSample(
user_input="経費精算の支払いタイミングは?",
response="月末締め、翌月15日払いです。",
retrieved_contexts=["経費精算は月末締め、翌月15日払いです。領収書の原本またはスキャンデータの提出が必要です。"],
reference="月末締め、翌月15日払い。",
),
# 4: ハルシネーション(事実と異なる: 週5日と回答)
SingleTurnSample(
user_input="リモートワークは週何日まで可能ですか?",
response="週5日まで可能で、特に申請は不要です。",
retrieved_contexts=["リモートワークは週3日まで可能です。事前申請は不要ですが、勤怠システムへの記録が必要です。"],
reference="週3日まで可能。記録は必要だが事前申請は不要。",
),
# 5: コンテキスト不足(健康診断について有給のドキュメントしか引いてこなかった)
SingleTurnSample(
user_input="健康診断は会社負担ですか?",
response="会社負担です。",
retrieved_contexts=["有給休暇は入社6ヶ月後に10日付与されます。"],
reference="健康診断は年1回会社負担で受診できる。",
),
]
dataset = EvaluationDataset(samples=samples)
metrics = [
Faithfulness(llm=evaluator_llm),
AnswerRelevancy(llm=evaluator_llm, embeddings=evaluator_embeddings),
LLMContextRecall(llm=evaluator_llm),
LLMContextPrecisionWithReference(llm=evaluator_llm),
]
result = evaluate(dataset=dataset, metrics=metrics)
print("=== Ragas 一括評価 ===")
print(result)
# サンプル別スコアを表表示
print("\n=== サンプル別スコア(DataFrame) ===")
df = result.to_pandas()
score_cols = [c for c in df.columns if c not in ("user_input", "response", "retrieved_contexts", "reference")]
print(df[["user_input"] + score_cols].to_string(index=True))
実行結果を見ると、ハルシネーション(サンプル4)と検索不足(サンプル5)のケースで関連メトリクスのスコアが下がっています。
$ python evaluate_dataset.py
Evaluating: 100%|██████████| 20/20 [00:19<00:00, 1.05it/s]
=== Ragas 一括評価 ===
{'faithfulness': 0.7000, 'answer_relevancy': 0.5396, 'context_recall': 0.8000, 'llm_context_precision_with_reference': 0.8000}
=== サンプル別スコア(DataFrame) ===
user_input faithfulness answer_relevancy context_recall llm_context_precision_with_reference
0 有給休暇は何日付与されますか? 1.0 0.549338 1.0 1.0
1 リモートワークは週何日まで可能ですか? 1.0 0.512112 1.0 1.0
2 経費精算の支払いタイミングは? 1.0 0.523373 1.0 1.0
3 リモートワークは週何日まで可能ですか? 0.5 0.540634 1.0 1.0
4 健康診断は会社負担ですか? 0.0 0.572603 0.0 0.0
結果の見方を整理すると以下のとおりです。
- サンプル4 はコンテキストには「週3日まで可能」と書いてあるのに「週5日まで可能」と回答している →
Faithfulness=0.5(事実と異なる主張が混じっている) - サンプル5 は健康診断について聞かれているのに有給のドキュメントしか引いていない →
Faithfulness=0.0/Context Recall=0.0/Context Precision=0.0(コンテキストが質問に答えられない)。一方でAnswerRelevancy=0.57は他のサンプルとほぼ同水準で残っており、「質問への関連性は保たれているがコンテキストへの忠実性が崩れている」という Faithfulness と AnswerRelevancy の役割差がはっきり出ている - 良回答のサンプル1〜3 はすべて満点
このように Ragas のスコアは 失敗の種類を切り分けて教えてくれるため、「ハルシネーションの問題なのか、それとも検索が悪いのか」のデバッグに直結します。
LiteLLM × Ragas: judge をプロバイダー横断で切り替える
LLM-as-judge の弱点は「judge 自身のバイアスや好みがスコアを歪める」ことです。LiteLLMとLangGraphで3層のGuardrailsを組み込んでLLMの入出力を守ってみる でも触れたように、Worker と Judge を別プロバイダーにするとクロスチェック効果が得られます。
LangchainLLMWrapper(ChatLiteLLM(...)) を使うと、judge をモデル名の文字列を変えるだけで切り替えられる ため、複数プロバイダーで同じデータを採点して結果のブレを観測することが容易になります。
"""LangchainLLMWrapper(ChatLiteLLM(...)) で judge をプロバイダー横断で切り替えるサンプル。
同じ評価データを 2 つの judge(OpenAI gpt-5-mini と Anthropic claude-sonnet-4-6)で
スコアリングし、judge ごとのスコアの揺らぎを観測する。
"""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import litellm
from langchain_litellm import ChatLiteLLM
from ragas.dataset_schema import SingleTurnSample
from ragas.llms import LangchainLLMWrapper
from ragas.metrics import Faithfulness
# Anthropic judge を使う際の安定化と、gpt-5 系の未対応パラメータ自動落としの保険
litellm.modify_params = True
litellm.drop_params = True
# 3 件のサンプル: 良い回答 / ハルシネーション / 一部正解
samples = [
SingleTurnSample(
user_input="有給休暇は何日付与されますか?",
response="入社6ヶ月後に10日付与されます。",
retrieved_contexts=["有給休暇は入社6ヶ月後に10日付与されます。"],
),
SingleTurnSample(
user_input="リモートワークは週何日まで可能ですか?",
response="週5日まで可能で、特に申請は不要です。",
retrieved_contexts=["リモートワークは週3日まで可能です。"],
),
SingleTurnSample(
user_input="経費精算の支払いタイミングは?",
response="月末締め、翌月15日払いです。月末締めなのでスケジュール調整が大事です。",
retrieved_contexts=["経費精算は月末締め、翌月15日払いです。"],
),
]
JUDGES = [
("openai/gpt-5-mini", ChatLiteLLM(model="openai/gpt-5-mini")),
("anthropic/claude-sonnet-4-6", ChatLiteLLM(model="anthropic/claude-sonnet-4-6")),
]
header = f"{'user_input':30s} | " + " | ".join(f"{name:30s}" for name, _ in JUDGES)
print("=== Faithfulness を judge ごとに比較 ===")
print(header)
print("-" * len(header))
for sample in samples:
row = [f"{sample.user_input[:28]:30s}"]
for _, llm in JUDGES:
evaluator_llm = LangchainLLMWrapper(llm)
metric = Faithfulness(llm=evaluator_llm)
score = metric.single_turn_score(sample)
row.append(f"{score:.3f}".rjust(30))
print(" | ".join(row))
実行結果は以下のようになりました。
$ python with_litellm_judge.py
=== Faithfulness を judge ごとに比較 ===
user_input | openai/gpt-5-mini | anthropic/claude-sonnet-4-6
------------------------------------------------------------------------------------------------
有給休暇は何日付与されますか? | 1.000 | 1.000
リモートワークは週何日まで可能ですか? | 0.000 | 0.000
経費精算の支払いタイミングは? | 0.667 | 0.667
今回は2つの judge で完全に一致しましたが、実プロジェクトでは judge による意見の食い違いが出る場面があります。本番運用では「Worker 側のモデルとは別プロバイダーの judge を使う」「複数 judge の平均で判定する」といった重ね方がおすすめです。
DeepEval と Ragas の使い分け
LiteLLMとDeepEvalでLangGraphエージェントの応答品質を自動評価してみる で扱った DeepEval にも FaithfulnessMetric がありますが、内部プロンプトやスコア定義が異なるため結果に差が出ることがあります。同じデータを両方で走らせると相補的に使えます。
"""Ragas と DeepEval の Faithfulness メトリクスを同じデータで走らせて並列表示する。
両者ともに「LLM-as-judge」だが、内部プロンプト・チャンク化戦略・スコア定義が
異なるためスコアに差が出る。実プロジェクトでは両者を並べて運用すると相補的になる。
"""
import logging
import sys
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", message="Pydantic serializer warnings")
# `Task exception was never retrieved` は asyncio ロガー経由で出るため unraisablehook では握り潰せない。
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
def _suppress_loop_closed(unraisable):
"""プロセス終了時の `RuntimeError: Event loop is closed` は httpx の接続クリーンアップ由来で無害なため握り潰す。"""
exc = unraisable.exc_value
if isinstance(exc, RuntimeError) and "Event loop is closed" in str(exc):
return
sys.__unraisablehook__(unraisable)
sys.unraisablehook = _suppress_loop_closed
import litellm
from deepeval.metrics import FaithfulnessMetric
from deepeval.test_case import LLMTestCase
from langchain_litellm import ChatLiteLLM
from ragas.dataset_schema import SingleTurnSample
from ragas.llms import LangchainLLMWrapper
from ragas.metrics import Faithfulness as RagasFaithfulness
litellm.drop_params = True
# Ragas judge: ChatLiteLLM 経由
ragas_evaluator_llm = LangchainLLMWrapper(ChatLiteLLM(model="openai/gpt-5-mini"))
ragas_metric = RagasFaithfulness(llm=ragas_evaluator_llm)
# DeepEval judge: 条件を Ragas と揃えるため model を明示指定(OPENAI_API_KEY が必要)
deepeval_metric = FaithfulnessMetric(threshold=0.7, model="gpt-5-mini")
# 同じデータを Ragas SingleTurnSample / DeepEval LLMTestCase の両形式で持つ
RAW_SAMPLES = [
{
"input": "有給休暇は何日付与されますか?",
"actual_output": "入社6ヶ月後に10日付与されます。",
"context": ["有給休暇は入社6ヶ月後に10日付与されます。"],
},
{
"input": "リモートワークは週何日まで可能ですか?",
"actual_output": "週5日まで可能で、特に申請は不要です。", # ハルシネーション
"context": ["リモートワークは週3日まで可能です。"],
},
{
"input": "経費精算の支払いタイミングは?",
"actual_output": "月末締め、翌月15日払いです。",
"context": ["経費精算は月末締め、翌月15日払いです。"],
},
]
print("=== Ragas vs DeepEval (Faithfulness) ===")
print(f"{'input':35s} | {'Ragas':>8s} | {'DeepEval':>8s}")
print("-" * 60)
for raw in RAW_SAMPLES:
# Ragas でスコアリング
ragas_sample = SingleTurnSample(
user_input=raw["input"],
response=raw["actual_output"],
retrieved_contexts=raw["context"],
)
ragas_score = ragas_metric.single_turn_score(ragas_sample)
# DeepEval でスコアリング
deepeval_test_case = LLMTestCase(
input=raw["input"],
actual_output=raw["actual_output"],
retrieval_context=raw["context"],
)
deepeval_metric.measure(deepeval_test_case)
deepeval_score = deepeval_metric.score
print(f"{raw['input'][:33]:35s} | {ragas_score:8.3f} | {deepeval_score:8.3f}")
実行結果は以下のとおりです。
$ python compare_with_deepeval.py
=== Ragas vs DeepEval (Faithfulness) ===
input | Ragas | DeepEval
------------------------------------------------------------
有給休暇は何日付与されますか? | 1.000 | 1.000
リモートワークは週何日まで可能ですか? | 0.000 | 0.500
経費精算の支払いタイミングは? | 1.000 | 1.000
両者の比較表は以下のとおりです。
| 観点 | Ragas | DeepEval |
|---|---|---|
| 主用途 | RAG パイプラインに最適化(LLM アプリ汎用にも使える) | LLM アプリ汎用(チャットボット、エージェント、RAG) |
| データモデル | SingleTurnSample / EvaluationDataset |
LLMTestCase |
| 主要メトリクス | Faithfulness / Answer Relevancy / Context Recall / Context Precision / Factual Correctness | Answer Relevancy / Faithfulness / Hallucination / Bias / Toxicity |
| judge 切替 | LangchainLLMWrapper 経由で ChatLiteLLM などの BaseChatModel をそのまま渡せる |
model= パラメータに DeepEval 内部のラッパーモデル名を指定 |
| pytest 統合 | スコア値を取り出して assert する素朴な書き方 |
assert_test() ヘルパー(テスト失敗時の理由文出力など CI フレンドリー) |
| コスト計測 | デフォルトでは出力なし | 評価終了時に総コストをサマリ表示 |
| 出力形式 | pandas DataFrame に変換可能 | スコア + 理由文(reason)のテキスト |
どちらを選ぶか
シリーズ全体を通して両方触ったうえでの個人的な使い分けは以下です。
Ragas が向く場面
- RAG パイプライン専用 の評価をしたい(Context Recall / Context Precision で検索の質を切り分けたい)
- 評価データを pandas で扱いたい(CI ではなく分析・可視化中心)
- judge を 複数プロバイダー横断で切り替えたい(
LangchainLLMWrapper)
DeepEval が向く場面
- RAG 以外も含む エージェント全般 の品質を評価したい
pytestでの CI 統合 をシンプルに済ませたい- 評価コストを 総額表示で把握したい
両者は排他ではなく、Ragas で RAG パートを採点 + DeepEval でエージェント全体の応答品質を採点 という重ねがけが現実的です。
まとめ
Ragas を使うと、RAG パイプラインの主要4メトリクス(Faithfulness / Answer Relevancy / Context Recall / Context Precision)を SingleTurnSample + evaluate() で簡潔に算出でき、judge も LangchainLLMWrapper(ChatLiteLLM(...)) でプロバイダー横断に差し替えられます。検索品質と回答品質を切り分けて測れる点が、汎用の DeepEval との大きな違いです。
最後まで読んでいただきありがとうございました。







