LiteLLMとRagasでRAGパイプラインのFaithfulness・AnswerRelevancy・ContextRecall・ContextPrecisionを評価してみる

LiteLLMとRagasでRAGパイプラインのFaithfulness・AnswerRelevancy・ContextRecall・ContextPrecisionを評価してみる

2026.05.25

はじめに

データ事業本部のkobayashiです。

LiteLLM × LangGraph シリーズ20回 + Recap で挙げた「シリーズで扱わなかったトピック」の続編シリーズ2本目として、RAG 評価フレームワーク Ragas をまとめます。1本目(LangGraph Functional API)と合わせてご覧ください。

https://dev.classmethod.jp/articles/python-litellm-functional-api/

シリーズ第15回で取り上げた DeepEval は LLM-as-judge 全般を扱う汎用フレームワークでしたが、Ragas は RAG パイプラインの評価に最適化されており、検索の質と生成の質を切り分けて測れる専用メトリクスが揃っています。シリーズ第4回の RAG パターン記事と組み合わせると、検索→生成のパイプラインを定量的に評価できるようになります。

https://dev.classmethod.jp/articles/python-litellm-rag/

https://dev.classmethod.jp/articles/python-litellm-deepeval/

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_KEYAnswerRelevancy の embeddings 計算(text-embedding-3-small)と DeepEval のデフォルト judge に使います。各サンプルの先頭で litellm.drop_params = True を入れているのは、gpt-5-mini のような新世代モデルが一部のパラメータ(temperature=0.01 など)を未対応として弾くため、未対応パラメータを LiteLLM 側で安全に落とすための保険です。

基本: 主要4メトリクスを1サンプルで動かす

最小例として、SingleTurnSample を1件作って4つのメトリクスを順番にスコアリングします。なお本サンプルでは評価フローだけを示すため、実際の RAG システムは動かさず、responseretrieved_contexts はダミーデータとして手書きで用意しています(実運用では生成 LLM の出力と検索結果をそのまま詰める想定です)。

basic_metrics.py
"""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 で閾値判定したり傾向を可視化したりする用途に向きます。

evaluate_dataset.py
"""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 を別プロバイダーにするとクロスチェック効果が得られます。

https://dev.classmethod.jp/articles/python-itellm-guardrails/

LangchainLLMWrapper(ChatLiteLLM(...)) を使うと、judge をモデル名の文字列を変えるだけで切り替えられる ため、複数プロバイダーで同じデータを採点して結果のブレを観測することが容易になります。

with_litellm_judge.py
"""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 がありますが、内部プロンプトやスコア定義が異なるため結果に差が出ることがあります。同じデータを両方で走らせると相補的に使えます。

https://dev.classmethod.jp/articles/python-itellm-deepeval/

compare_with_deepeval.py
"""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 との大きな違いです。

最後まで読んでいただきありがとうございました。


生成AI活用はクラスメソッドにお任せ

過去に支援してきた生成AIの支援実績100+を元にホワイトペーパーを作成しました。御社が抱えている課題のうち、どれが解決できて、どのようなサービスが受けられるのか?4つのフェーズに分けてまとめています。どうぞお気軽にご覧ください。

生成AI資料イメージ

無料でダウンロードする

この記事をシェアする

関連記事