跳至内容

RAG 评估:如何用 Ragas 衡量一条 RAG 流水线

一篇动手实践的 Python RAG 评估教程。构建评估数据集,用 Ragas 的四个核心指标打分,将质量闸接入 CI,生成合成测试问题,并跨提交跟踪分数,让您在用户发现之前分辨检索失败还是生成失败。
已更新 2026年10月19日  · 15分钟 阅读

使用 AI 探索

ChatGPTClaudePerplexity

一个用于聊天机器人的检索增强生成(RAG)流水线在笔记本里的烟雾测试全部通过。随后,支持工单开始反映回答错误或古怪,而烟雾测试日志里没有任何红旗。

这种失败很难捕捉,因为没有指标它就是不可见的。RAG 流水线有两个出错点:检索器可能取回了错误的片段;或生成器忽略了正确的片段并编造事实。一个端到端的单一准确率数值无法告诉您是哪一种出了问题,因此也就无法告诉您该修什么。

在本教程中,我将使用 Ragas 0.4.3 在 Python 中构建一条完整的 RAG 评估流水线:手工构建的评估数据集、四个核心指标(faithfulness、answer relevancy、context precision 和 context recall)、在分数下降时让构建失败的持续集成(CI)质量闸、合成测试集生成器,以及 JSON Lines 分数跟踪器。

如果您想先了解背景,请参阅我们的 RAG 是什么指南、关于将大语言模型(LLM)用作评审来评估 RAG 流水线的教程,以及我们的高级 RAG 技术概览。

您需要 Python 3.9 或更高版本(我用的是 3.12)、uv 或 pip、一个适用于 OpenAI 兼容评审模型的 API 密钥,以及对 RAG 流水线工作原理的大致了解。如果您需要先打好基础,我推荐我们的 Retrieval-Augmented Generation with LangChain 课程,本教程将对那类流水线进行打分。

RAG 评估一言以蔽之

像 BLEU(Bilingual Evaluation Understudy)和 ROUGE(Recall-Oriented Understudy for Gisting Evaluation)这样的文本重叠指标不是衡量 RAG 的正确标尺。Ragas 则使用评审 LLM 从四个维度给每个答案评分:faithfulness 和 answer relevancy 诊断生成器,context precision 和 context recall 诊断检索器。

把这四个数在 CI 中设阈值并按提交记录,就能从一次性审计变成回归报警。

开始之前有一条提醒。两年里 Ragas 的 API 改了两次:v0.2 用 SingleTurnSample 和 EvaluationDataset 取代了字典数据集,v0.4 又把(v0.3.9 引入的)collections 指标设为默认,并标记旧的 evaluate() 函数为弃用。

网上的大多数教程仍展示 v0.1 或 v0.2 的代码。下面所有内容都可在 0.4.3 上运行,这是截至 2026 年 10 月的当前发行版。

为什么 RAG 需要自己的评估指标?

RAG 需要自己的指标,因为经典文本指标只计算答案与参考答案共享了多少词,而词语重合并不是 RAG 系统出问题的地方。

BLEU(Papineni 等,2002)和 ROUGE(Lin,2004)是为机器翻译和摘要构建的,好的输出应与参考使用大致相同的词。RAG 的一个答案若把“14 days”说成“you have 2 weeks”,在二者上分数都会很低;而一个流畅却编造了退款政策的答案如果恰好复用了参考里的词,反而可能得高分。

更深层的问题在于 RAG 流水线可能在两个彼此独立的地方失败:检索器可能选择了错误的信息,或者生成器忽略了正确信息并编造事实。当答案错误时,您需要知道以下哪一种发生了:

  • 正确的片段根本没有被检索到。
  • 正确的片段被检索到了,但排序太靠后而未传给模型。
  • 正确的片段被传递了,但模型忽略了它。

RAG 流水线示意图:context precision 和 context recall 衡量检索器,faithfulness 和 answer relevancy 衡量生成器

RAG 流水线的两类失败模式:context precision 与 recall 诊断检索器,faithfulness 与 answer relevancy 诊断生成器。图源作者。

我们的 《什么是 RAG?完整指南》讲解了这些失败源头所在的流水线阶段。

Ragas 是一个开源 Python 框架,可以为流水线的每个部分给出一个数值。它不是唯一的选项。两个主要替代品是 DeepEval(用 pytest 风格的测试用例包装评估)和 Evidently(把 LLM 检查作为额外列加到数据集上并汇入监控报告)。

Framework Best for RAG-specific metrics built in Test data from your documents Config style
Ragas Diagnosing RAG pipelines Faithfulness, answer relevancy, context precision, context recall, context entities recall, noise sensitivity TestsetGenerator (open source) Metric objects plus a judge from llm_factory()
DeepEval Unit-test-style LLM testing in CI Answer relevancy, faithfulness, contextual relevancy, contextual recall, contextual precision Synthesizer (open source) LLMTestCase objects, assert_test() run through deepeval test run
Evidently Monitoring dashboards and drift over time FaithfulnessLLMEval, ContextRelevance, ContextQualityLLMEval RAG dataset generation in Evidently Cloud Descriptors on a Dataset, summarized by a Report

我为本教程选择 Ragas,是因为其四个核心指标能清晰划分为检索器分数和生成器分数,且其测试集生成器包含在同一个开源包内。

如果您的团队已经深度使用 pytest,DeepEval 的 assert_test() 会更顺手。如果您想要的是线上流量质量看板,Evidently 的报告更快能达到目的。

如何设置 Ragas 与评审模型?

设置 Ragas 包括安装软件包、固定一个依赖版本,以及选择一个所有指标共用的评审 LLM。我将使用 uv,因为它会为您锁定确切的包版本,但用相同版本的 pip install 也可以。

uv init rag-eval --python 3.12 && cd rag-eval
uv add "ragas==0.4.3" "langchain-community<0.4" openai pandas matplotlib rapidfuzz

这里我使用的是 Python 3.12。Ragas 0.4.3 宣称支持 Python 3.9 及更新版本。

软件包 rapidfuzz 供后文的合成测试集生成器使用。Ragas 未声明其为依赖,所以我提前安装。

截至撰写时,langchain-community<0.4 的固定版本并非可选。Ragas 0.4.3 从一个在 langchain-community 0.4 中被移除的模块路径导入了 ChatVertexAI,所以未固定版本的新装在导入 ragas 时会失败。

接下来是评审。Ragas 中每个基于 LLM 的指标都会调用一次或多次评审模型,因此您只需构建一次并在各处传入。我将其放在单独的模块 judge.py 中,并从环境变量读取密钥,避免凭据进入 git。

此文件展示了如何直接调用 OpenAI,或通过 Google 的 OpenAI 兼容端点调用 Gemini。

# judge.py
"""Build the judge LLM and embedding model that every metric shares.

Uses OpenAI by default (reads OPENAI_API_KEY). Set GEMINI_API_KEY instead to
judge with Gemini through Google's OpenAI-compatible endpoint.
"""

import os

from openai import AsyncOpenAI
from ragas.embeddings import embedding_factory
from ragas.llms import llm_factory

GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"


def judge_model_name() -> str:
    """The judge model to use (override it with the JUDGE_MODEL variable)."""
    default = "gemini-3.5-flash-lite" if os.environ.get("GEMINI_API_KEY") else "gpt-4o-mini"
    return os.environ.get("JUDGE_MODEL", default)


def build_judge():
    """Return (llm, embeddings), both using one OpenAI-compatible client."""
    if os.environ.get("GEMINI_API_KEY"):
        client = AsyncOpenAI(
            api_key=os.environ["GEMINI_API_KEY"],
            base_url=GEMINI_BASE_URL,
            max_retries=5,
        )
        embedding_model = "gemini-embedding-001"
        # Gemini 3.x models spend output tokens thinking before they answer
        settings = {"max_tokens": 4096, "reasoning_effort": "low"}
    else:
        client = AsyncOpenAI(max_retries=5)
        embedding_model = "text-embedding-3-small"
        settings = {"max_tokens": 4096}

    llm = llm_factory(judge_model_name(), client=client, **settings)
    embeddings = embedding_factory("openai", model=embedding_model, client=client)
    return llm, embeddings

该文件里有两点容易在首次运行时绊倒您:

  • 0.4 版本中的 llm_factory() 需要传入一个 client 对象,只传模型名会报错。
  • 您传给 llm_factory() 的任何额外设置都会直接进入模型调用,这也是我把 max_tokens 提高到默认 1,024 以上的方式。

我应该用哪个评审模型?

使用在您人工核验样本上能产出稳定分数的最便宜模型,然后固定它。对于大型评估套件,请求量会很快累积起来:faithfulness 对每个样本进行 2 次评审调用,answer relevancy 进行 3 次评审加 2 次嵌入调用,context precision 对每个检索到的片段调用 1 次,context recall 调用 1 次。

4 个指标下的 6 个样本在一次运行中共发出了 62 个请求:50 次评审调用加 12 次嵌入调用。以下是 2026 年 10 月 19 日三家厂商当前小型评审模型的标价:

Judge model Input per 1M tokens Output per 1M tokens Source
gpt-6-luna $0.10 $0.50 OpenAI pricing
gemini-3.5-flash-lite $0.30 $2.50 Gemini API pricing
claude-haiku-5-5 $0.10 (prompts up to 100K tokens) $0.50 (prompts up to 100K tokens) Claude API pricing

judge.py 中的默认值仍是 gpt-4o-mini,这是一款较早的 OpenAI 模型,并未列在 OpenAI 定价页的主表中。设置 JUDGE_MODEL 变量即可切换。

如要使用 Claude 作为评审,有一个问题:answer relevancy 还需要一个嵌入模型,而 Anthropic 并不提供。其文档指向 Voyage AI,因此您需要将 Claude 与来自另一家提供商的嵌入配对使用。

固定版本比选择本身更重要。即便流水线没有变化,更换评审模型也会让分数发生偏移,这会破坏您即将在跟踪部分构建的趋势线。

将模型名称放进受版本控制的 judge.py,并把评审模型升级当作其他破坏性变更对待:用新评审的分数重置每一个阈值。

如何构建 RAG 评估数据集?

数据集中的每个测试用例称为一个样本。一个样本不只是答案:它记录了一次完整的流水线往返,包含四个字段:

Field What it holds
user_input 客户提出的问题
retrieved_contexts 检索器为该问题返回的片段
response RAG 流水线给出的答案
reference 人工撰写的正确答案

Ragas 将每个样本存为 SingleTurnSample,整个集合存为 EvaluationDataset。reference 是可选的,但检索指标要和它对比,所以我在策划集里总是包含它,尽管无参照的指标会忽略它。

我为一家虚构的咖啡烘焙商 Northwind Coffee 构建了 6 个样本。下面的每个指标章节都会加入一个该指标应当能捕捉到的样本。

先用前三个样本启动 build_dataset.py:一个干净答案、一个有意的幻觉、以及一个包含多项断言的第二个干净答案。

# build_dataset.py
"""Hand-build a small evaluation set and save it as JSON Lines."""

import json

from ragas import EvaluationDataset, SingleTurnSample

# Chunks of Northwind's help-center pages, exactly as a retriever would
# return them to the generator.
REFUNDS = (
    "If you are not happy with your coffee, contact support within 14 days of "
    "the delivery date for a full refund. You do not need to return the bag."
)
REFUND_TIMING = (
    "Refunds are issued to the original payment method and appear within 5 to "
    "7 business days. Gift orders are refunded as store credit."
)
SHIPPING_US = (
    "Standard shipping within the contiguous United States takes 3 to 5 "
    "business days and costs $6.00. Standard shipping is free on orders of "
    "$40.00 or more."
)
PAUSE = (
    "You can pause a subscription for up to 3 months from your account page. "
    "Paused subscriptions keep their pricing."
)
SUBSCRIPTION_PERKS = (
    "Subscribers save 10% on every bag and always get free standard shipping, "
    "regardless of order size."
)

samples = [
    # 1. Clean: grounded answer, relevant chunks ranked first.
    SingleTurnSample(
        user_input="How long do I have to ask for a refund on a bag of coffee?",
        retrieved_contexts=[REFUNDS, REFUND_TIMING, SHIPPING_US],
        response=(
            "You have 14 days from the delivery date to contact support for a "
            "full refund, and you do not need to send the bag back."
        ),
        reference=(
            "Contact support within 14 days of the delivery date for a full "
            "refund; the bag does not need to be returned."
        ),
    ),
    # 2. Deliberate hallucination: the pause limit is 3 months, not 6, and
    #    the free bag does not exist anywhere in the docs.
    SingleTurnSample(
        user_input="Can I pause my subscription?",
        retrieved_contexts=[PAUSE, SUBSCRIPTION_PERKS],
        response=(
            "Yes. You can pause your subscription for up to 6 months from your "
            "account page, and you get a free bag when you resume."
        ),
        reference=(
            "Yes, a subscription can be paused for up to 3 months from the "
            "account page, and paused subscriptions keep their pricing."
        ),
    ),
    # 3. Clean: multi-fact answer fully supported by the retrieved chunks.
    SingleTurnSample(
        user_input="What do subscribers get that regular customers do not?",
        retrieved_contexts=[SUBSCRIPTION_PERKS, PAUSE],
        response=(
            "Subscribers save 10% on every bag and get free standard shipping "
            "on every order, with no minimum order size."
        ),
        reference=(
            "Subscribers save 10% on every bag and always get free standard "
            "shipping regardless of order size."
        ),
    ),
]

dataset = EvaluationDataset(samples=samples)
dataset.to_jsonl("eval_dataset.jsonl")
print(dataset)

# Save the questions and reference answers separately. The CI section rebuilds
# retrieved_contexts and response from your real pipeline, so it only needs these.
with open("eval_questions.jsonl", "w", encoding="utf-8") as file:
    for sample in samples:
        row = {"user_input": sample.user_input, "reference": sample.reference}
        file.write(json.dumps(row) + "\n")

每个检索到的上下文都是 Northwind 帮助中心页面的一个片段,在脚本中按检索器会传给生成器的样子原样写入。运行 uv run python build_dataset.py 以写出 eval_dataset.jsonl 和 eval_questions.jsonl,每次在后续章节添加样本时都重新运行它。

保存为 JSON Lines 很重要,因为后续的每个脚本(指标运行、CI 闸)都用 EvaluationDataset.from_jsonl() 加载同一个 eval_dataset.jsonl。

在真实项目里,retrieved_contexts 和 response 字段来自于让您的实际流水线跑一遍问题,只有 user_input 和 reference 是手写的。CI 章节会展示如何把这一步自动化。

什么样的评估集才好

本文最终的 6 个样本足够用来学习指标,但不足以信任某个阈值。在您依赖它之前,目标是大约 100 个样本;跟踪章节会说明为什么。所需数量取决于用例的严格程度,因此我更关注良好的样本组合而非具体数量:

  • 真实的查询分布。从支持日志或搜索分析中抽取问题,而不是凭空想象。用户的提问方式常常出乎意料,很少与 FAQ 的措辞一致。
  • 文档处理不佳的边界情况。答案横跨两份文档的问题、包含日期或数字的问题、以及使用文档未覆盖同义词的问题。
  • 不可回答的问题。第 6 个样本(您会在 context precision 和 recall 一节添加)就是一个例子。您的文档不可能回答所有问题,正确行为是直接说明。如果您从不测试它,就不会注意到某天模型开始乱猜。
  • 已知的历史失败。用户报告的每一次幻觉都应变成永久样本。评估集就是这样成长为回归测试套件的。

四个核心 Ragas 指标分别衡量什么?

四个核心 Ragas 指标中,faithfulness 和 answer relevancy 针对生成器,context precision 和 context recall 针对检索器。四者都返回 0 到 1 之间的分数。关键之处在于每个数能告诉您该修哪里。

Faithfulness:抓住幻觉

Faithfulness 衡量答案中有多少内容得到了检索片段的支撑。评审会把答案拆分为独立断言,再将每个断言与片段比对:1.0 表示所有断言都被支持,0.0 表示一个都没有。

Faithfulness 不需要参考答案,因此您可以把它运行在线上生产流量上,而不仅仅是测试集。每当您更改提示词、模型或模型看到的上下文长度时都要重跑它,因为这些更改最容易引入幻觉。

最快看到它效果的方式是在单个答案上打分,无需数据集:

# quick_check.py
"""Score a single response for faithfulness"""

import asyncio

from ragas.metrics.collections import Faithfulness

from judge import build_judge

judge_llm, _ = build_judge()
faithfulness = Faithfulness(llm=judge_llm)

result = asyncio.run(
    faithfulness.ascore(
        user_input="Can I pause my subscription?",
        response=(
            "Yes. You can pause for up to 6 months, and you get a free bag "
            "when you resume."
        ),
        retrieved_contexts=[
            "You can pause a subscription for up to 3 months from your account "
            "page. Paused subscriptions keep their pricing."
        ],
    )
)
print(f"faithfulness = {result.value:.2f}")

用 uv run python quick_check.py 运行会打印 faithfulness = 0.00,因为回答中的两个断言(6 个月、免费送一包)都未被上下文支持。在 0.4 版本中,您通过每个指标的 ascore() 方法打分,该方法返回一个结果,对应的分数在 .value 字段。

在当前数据集上打分,样本 2 中植入的幻觉得分 0.0,理所当然,而两个干净样本都得分 1.0。每个指标的完整结果见下方多指标章节。

Answer relevancy:抓住漂移

Answer relevancy 衡量答案是否真正回应了提问。评审会读答案并写出 3 个它适合作为回答的问题,随后 Ragas 使用嵌入模型检查这些问题与原始问题在语义上的接近度。如果答案在回避问题,评审会将其标记为不置可否并打 0 分。

Ragas 中该类名为 AnswerRelevancy(较旧示例称其为 ResponseRelevancy,而文档页标题也叫 Response Relevancy),它同时需要评审 LLM 和嵌入模型。

在 faithfulness 高而 relevancy 低时,通常指向生成器或提示:模型拥有正确的上下文并保持扎根其中,却回答了一个不同的问题。不过请先检查 context precision 和 recall,因为返回离题片段的检索器也会产生同样的模式。

下面这个样本展示生成器的情形。检索器返回了正确的运费片段,答案保持扎根,却谈起了烘焙日期而不是运费。把新片段加到 build_dataset.py 顶部的片段中:

ROAST_DATE = (
    "Every bag is roasted to order and shipped within 48 hours of roasting. "
    "The roast date is printed on the bottom of the bag."
)

然后在 samples 列表末尾添加新样本:

    # 4. Drift: retrieval was fine and the answer is grounded, but it never
    #    addresses the question that was asked.
    SingleTurnSample(
        user_input="How much does standard shipping cost in the US?",
        retrieved_contexts=[SHIPPING_US, ROAST_DATE],
        response=(
            "Every bag is roasted to order and shipped within 48 hours of "
            "roasting, and the roast date is printed on the bottom of the bag."
        ),
        reference=(
            "Standard shipping in the contiguous United States costs $6.00 and "
            "is free on orders of $40.00 or more."
        ),
    ),

每个样本得到一个相关性分数:评审自答案生成的问题与样本实际提问的匹配程度,1.0 表示完全匹配。

Context precision 与 recall:诊断检索

Context precision 检查有用片段是否在检索结果中排在前列,而 context recall 检查检索器是否找齐了写出正确答案所需的信息。

Precision 会根据有用片段的排序位置给予更多加权,因此第一名中的有用片段比第三名中的同一片段计分更高。Recall 是参考答案中的事实被至少一个检索片段支持的占比。

两者都需要 reference 字段。在 collections API 中,类名分别是 ContextPrecisionWithReference 和 ContextRecall,二者都只接收评审 LLM。Ragas 文档也用更短的 ContextPrecision 来指代基于参考的版本,因此您可能会看到两种写法。

目前每个样本都把正确片段排在前面,因此两个指标都无事可做。让我们添加两个以不同方式破坏检索的样本:一个是检索到了正确片段却把它排在最后,另一个是完全没有检索到相关内容,因此模型正确地拒答。像之前一样,把片段加到文件顶部:

SHIPPING_CANADA = (
    "We ship to Canada. Canadian orders take 7 to 10 business days and cost "
    "$18.00. We do not currently ship outside the United States and Canada."
)
BREWING = (
    "For pour-over, we recommend a ratio of 1 gram of coffee to 16 grams of "
    "water, a medium-fine grind, and water at 200 degrees Fahrenheit."
)

再把样本加到列表末尾:

    # 5. Ranking problem: the right chunk is retrieved, but it is ranked last.
    SingleTurnSample(
        user_input="Does Northwind ship to Canada, and how long does it take?",
        retrieved_contexts=[BREWING, ROAST_DATE, SHIPPING_CANADA],
        response=(
            "Yes, Northwind ships to Canada. Canadian orders take 7 to 10 "
            "business days and cost $18.00."
        ),
        reference="Yes. Canadian orders take 7 to 10 business days and cost $18.00.",
    ),
    # 6. Retrieval miss: nothing about the warranty was retrieved, so the
    #    model correctly declines. Recall will be 0 here.
    SingleTurnSample(
        user_input="How long is the warranty on the Baratza Encore grinder?",
        retrieved_contexts=[BREWING, SHIPPING_US],
        response=(
            "I do not have information about the grinder warranty in the "
            "documents available to me."
        ),
        reference=(
            "All equipment, including the Baratza Encore grinder, ships with a "
            "2-year manufacturer warranty covering defects in materials and "
            "workmanship."
        ),
    ),

排序之所以重要,是因为大多数流水线只把最前面的少量片段传给模型,因此一个有用片段若处于 Top-5 检索器的第 6 名,几乎等于不存在。

样本 5 把唯一有用的片段放在 3 个里的最后,precision 返回 0.333,因为唯一有用的信息被排在末位。样本 6(检索缺失)在 precision 和 recall 上都得 0.0。

样本 6 也给 faithfulness 带来启示。模型正确地拒答,但该拒答在某些运行里得 1.0,在另一些运行里得 0.0,期间并未改变任何东西。像“我没有关于保修信息”的表述既未被上下文支持、也未被反驳,而评审会在二者间掷硬币。

如果您的流水线本应拒答,请将拒答单独打分,而不是让它牵着 faithfulness 走。Ragas 0.4 从 collections API 中移除了 AspectCritic,因此替代方案是用 @discrete_metric 装饰器自定义检查(见 v0.3 到 v0.4 迁移指南)。

结合阅读两个检索数值,您就知道下一周的精力该花在哪:

Context precision Context recall Diagnosis What to change
High High 检索健康 关注生成器:提示、模型、faithfulness
Low High 取到了正确片段,但排序很差 添加或调优重排序器
High Low 前列片段相关但不完整 传更多片段给模型、使用更大的片段、或检查切片位置
Low Low 检索器整体失效 重审嵌入、索引和查询重写

“低 precision,高 recall”是我在实践中最常见的一行,也是最便宜可修复的。

我们的 将 RankGPT 用作 RAG 重排序代理教程展示了如何在现有检索器上叠加 LLM 重排序,我们的 通过 RAG 与重排序提升 LLM 准确率教程展示了另一种重排序方案。我们的 提升 RAG 性能的 5 种关键技术涵盖了针对其他几行的切片与查询重写。

每次变更后重跑两个检索指标,该表会告诉您是否奏效。

如何运行一次完整的多指标评估?

一次完整的多指标评估会并行在每个样本上运行四个指标并收集分数,提供每行的数值以便深入定位失败,以及列均值供看板使用。

在 Ragas 0.2 和 0.3 中,您可以用 evaluate(dataset, metrics=[...], run_config=RunConfig(max_workers=8)) 完成此事。在 0.4.3 中,该函数仍存在,但会提示它已弃用,并直接拒绝新指标,因为新指标不再继承其期望的旧 Metric 类。

替代品是 @experiment 装饰器,它会在一个 Dataset 上同时运行您的流水线和指标,并将结果写入 CSV。一旦您有可调用的流水线,这是合适的工具。

对于给静态数据集打分,我觉得写个小工具更清晰:它为每个需要的脚本只构建一遍四个指标,并用可读的代码保留并行调用与错误处理:

# score.py
"""Score an EvaluationDataset with the 4 core Ragas metrics."""

import asyncio
import inspect
import math

import pandas as pd
from ragas.metrics.collections import (
    AnswerRelevancy,
    ContextPrecisionWithReference,
    ContextRecall,
    Faithfulness,
)


def build_metrics(judge_llm, judge_embeddings) -> list:
    """The 4 core metrics, all using the same judge."""
    return [
        Faithfulness(llm=judge_llm),
        AnswerRelevancy(llm=judge_llm, embeddings=judge_embeddings),
        ContextPrecisionWithReference(llm=judge_llm),
        ContextRecall(llm=judge_llm),
    ]


async def score_sample(sample, metrics, limit):
    """Run every metric on one sample and return that sample's row of scores."""
    row = {"user_input": sample.user_input}
    for metric in metrics:
        # Each metric needs different fields (recall wants the reference,
        # faithfulness wants the response), so pass only the ones it asks for.
        wanted = inspect.signature(metric.ascore).parameters
        inputs = {field: getattr(sample, field) for field in wanted}
        async with limit:
            try:
                result = await metric.ascore(**inputs)
                row[metric.name] = result.value
            except Exception as error:
                # One failed judge call should not sink the whole run
                print(f"{metric.name} failed on {sample.user_input!r}: {error}")
                row[metric.name] = math.nan
    return row


def score_dataset(dataset, metrics, max_concurrency=8):
    """Score every sample: one row per sample, one column per metric."""

    async def score_all_samples():
        limit = asyncio.Semaphore(max_concurrency)  # cap parallel judge calls
        tasks = [score_sample(sample, metrics, limit) for sample in dataset.samples]
        return await asyncio.gather(*tasks)

    return pd.DataFrame(asyncio.run(score_all_samples()))

唯一不明显的行是 inspect.signature()。四个指标需要的输入不同(faithfulness 需要 response,而 recall 则需要 reference),因此该工具读取每个指标 ascore() 接受哪些参数,并从样本中只传入这些字段,从而用一套循环服务每个指标。

信号量限制了同时运行的评审调用数,而仍失败的指标会成为一个空(NaN)单元格,而不是中止整个运行。如果在 Jupyter 笔记本里运行,此处的 asyncio.run 会因已有事件循环而失败。先调用 nest_asyncio.apply()(Ragas 已依赖它),或直接 await 协程。

有了这个工具,完整运行就很简短了:

# run_all.py
"""Run all four core metrics and save per-sample scores to scores.csv."""

import pandas as pd
from ragas import EvaluationDataset

from judge import build_judge
from score import build_metrics, score_dataset

judge_llm, judge_embeddings = build_judge()
dataset = EvaluationDataset.from_jsonl("eval_dataset.jsonl")
metrics = build_metrics(judge_llm, judge_embeddings)

scores = score_dataset(dataset, metrics, max_concurrency=4)
scores.to_csv("scores.csv", index=False)

pd.set_option("display.max_colwidth", 45)
pd.set_option("display.width", 160)
print(scores.sort_values("faithfulness").round(3))
print("\nMeans:")
print(scores.drop(columns="user_input").mean().round(3))

以下是我首次运行的分数,按 faithfulness 排序并保留三位小数,最后一行是均值:

Question Faithfulness Answer relevancy Context precision Context recall
Can I pause my subscription? 0.000 0.883 1.000 1.000
Does Northwind ship to Canada, and how long does it take? 0.667 0.954 0.333 1.000
How long do I have to ask for a refund on a bag of coffee? 1.000 0.746 1.000 1.000
How long is the warranty on the Baratza Encore grinder? 1.000 0.000 0.000 0.000
How much does standard shipping cost in the US? 1.000 0.572 1.000 1.000
What do subscribers get that regular customers do not? 1.000 0.852 1.000 1.000
Mean 0.778 0.668 0.722 0.833

后续对同一数据集的运行,因我在跟踪部分会提到的拒答样本翻转,faithfulness 得分更低。

按 faithfulness 排序有助于挑出最可能的幻觉,植入的那一个就在最上面。最后一行的均值就是您的看板数字。

横向看每一行,植入的每种失败都在不同位置显现。幻觉只拉低了 faithfulness。检索缺失让 relevancy、precision 和 recall 同时归零。排序问题将 precision 降至 0.333;而漂移在已作答的问题中表现为最低的相关性。

有点奇怪的是:加拿大的答案完全被片段支持,却在 faithfulness 上得了 0.667。这就是评审噪声,我会在跟踪部分再谈。

Answer relevancy 用嵌入比较语义,因此即使是好的答案也很少接近 1.0:退款答案得 0.746,而离题的运费答案得 0.572。请通过样本间的差距与随时间的趋势来判断相关性,不要只看绝对数值。

如何为 CI 增加 RAG 质量闸?

RAG 质量闸是一个测试脚本,用来运行评估、将每个均值与阈值比较,任何指标低于阈值就以错误码退出,从而让 CI 阻止合并。

先说一个问题。之前手工构建的数据集把响应和检索片段都写死了,因此给它打分永远无法反映提示词或检索器的变化。在 CI 中,您需要先让真实流水线跑一遍已保存的问题,然后对其输出打分。两个小脚本即可完成这一点。

# pipeline.py
"""The bridge between the evaluation scripts and your own RAG pipeline."""


def answer(question: str) -> tuple[list[str], str]:
    """Return (retrieved_chunks, generated_answer) for one question.

    Replace the body with a call to your own retriever and generator. The
    chunks must be in the order your generator receives them, because context
    precision depends on ranking.
    """
    raise NotImplementedError("Connect this function to your RAG pipeline.")
# refresh_dataset.py
"""Run the real pipeline over the saved questions and rebuild eval_dataset.jsonl."""

import json

from ragas import EvaluationDataset, SingleTurnSample

from pipeline import answer


def main():
    samples = []
    with open("eval_questions.jsonl", encoding="utf-8") as file:
        for line in file:
            row = json.loads(line)
            retrieved_contexts, response = answer(row["user_input"])
            samples.append(
                SingleTurnSample(
                    user_input=row["user_input"],
                    retrieved_contexts=retrieved_contexts,
                    response=response,
                    reference=row["reference"],
                )
            )
    EvaluationDataset(samples=samples).to_jsonl("eval_dataset.jsonl")
    print(f"Rebuilt eval_dataset.jsonl with {len(samples)} samples")


if __name__ == "__main__":
    main()

pipeline.py 是唯一需要您自己编写的文件:将 answer() 的函数体替换为调用您的检索器和生成器。refresh_dataset.py 读取 build_dataset.py 保存的问题与参考答案,逐个让流水线跑一遍,并重写 eval_dataset.jsonl。

质量闸本身就是包含阈值的字典加一个循环。下面的阈值是按手工示例数据集校准的,因此在依赖它之前请在您自己的流水线输出上重新校准。

# quality_gate.py
"""Fail the build when any aggregate Ragas score drops below its threshold."""

import os
import random
import sys

import numpy as np
import pandas as pd
from ragas import EvaluationDataset

from judge import build_judge
from score import build_metrics, score_dataset

# Set each threshold just below the lowest mean you have seen across repeated
# runs, not below one lucky run. With a small dataset a single flaky verdict
# moves a mean by 1 / n_samples, so a thin margin makes the build flaky.
THRESHOLDS = {
    "faithfulness": 0.60,
    "answer_relevancy": 0.60,
    "context_precision_with_reference": 0.65,
    "context_recall": 0.75,
}

RUNS = int(os.environ.get("GATE_RUNS", "3"))      # judge runs to average
SAMPLE = int(os.environ.get("GATE_SAMPLE", "0"))  # rows to score, 0 means all


def load_dataset():
    dataset = EvaluationDataset.from_jsonl("eval_dataset.jsonl")
    if 0 < SAMPLE < len(dataset.samples):
        # A fixed seed means every pull request scores the same subset
        rows = random.Random(0).sample(dataset.samples, SAMPLE)
        dataset = EvaluationDataset(samples=rows)
    return dataset


def average_runs(frames):
    """Average each sample's scores across runs. A NaN in any run stays NaN."""
    numbers = [frame.drop(columns="user_input") for frame in frames]
    averaged = np.mean([frame.to_numpy() for frame in numbers], axis=0)
    result = pd.DataFrame(averaged, columns=numbers[0].columns)
    result.insert(0, "user_input", frames[0]["user_input"])
    return result


def main():
    judge_llm, judge_embeddings = build_judge()
    dataset = load_dataset()
    metrics = build_metrics(judge_llm, judge_embeddings)

    frames = [score_dataset(dataset, metrics, max_concurrency=4) for _ in range(RUNS)]
    scores = average_runs(frames)
    scores.to_csv("scores.csv", index=False)  # track_scores.py reads this file

    # skipna=False: a failed judge call must fail the gate, not shrink the sample
    means = scores.drop(columns="user_input").mean(skipna=False)

    failed = []
    for metric_name, threshold in THRESHOLDS.items():
        actual = means[metric_name]
        # Test the pass condition once and fail on everything else. NaN is
        # neither >= nor < a threshold, so a judge outage must fail the gate
        # rather than slip between the two comparisons.
        ok = actual >= threshold
        print(f"{'PASS' if ok else 'FAIL'}  {metric_name:<34} {actual:.3f} (min {threshold:.2f})")
        if not ok:
            failed.append(metric_name)

    if failed:
        print(f"\nQuality gate failed on: {', '.join(failed)}")
        sys.exit(1)
    print("\nQuality gate passed.")


if __name__ == "__main__":
    main()

相对于我的基线,四个均值都超过阈值,脚本以代码 0 退出,允许合并:

Metric Mean Threshold Result
Faithfulness 0.778 0.60 Pass
Answer relevancy 0.668 0.60 Pass
Context precision 0.722 0.65 Pass
Context recall 0.833 0.75 Pass

我将 faithfulness 阈值设为 0.60 而非 0.70,是因为在跟踪部分会提到的评审噪声。对于 6 个样本,单个波动裁决会让均值移动 0.17,而未更改流水线的后续运行曾打出 0.61。该质量闸还会运行评审 3 次并对分数取平均(GATE_RUNS),以 3 倍评审成本平滑单次裁决翻转。

有一个设计选择是对 not (actual >= threshold) 失败,而不是 actual < threshold。与 NaN(非数字)的比较总是 False,因此若写成 actual < threshold,评审失败时构建会通过。

均值中的 skipna=False 覆盖了部分失败的情形。pandas 默认跳过 NaN,因此少量的评审失败会悄悄缩小样本,而不是让质量闸失败。现在短暂的评审错误会导致构建失败,从而在调试前先重跑任务。

阈值很难调。将它们设在您多次运行见过的最低均值之下,而不是某个志向性的 0.9,否则质量闸会拦下每个 PR,团队会停止信任 CI。每当您更换评审模型或一次性新增多个样本时,请重置它们。

把它接入 GitHub Actions 的工作流很短。工作流语法参考涵盖了这里使用的 on.pull_request 路径过滤与 schedule 的 cron:

# .github/workflows/rag-eval.yml
name: RAG quality gate

on:
  pull_request:
    paths:
      - "prompts/**"
      - "retriever/**"
      - "judge.py"
      - "eval_questions.jsonl"
  schedule:
    - cron: "0 3 * * *"  # full suite nightly at 03:00 UTC

jobs:
  ragas:
    runs-on: ubuntu-latest
    env:
      # Use the same judge you calibrated the thresholds with
      GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
      JUDGE_MODEL: gemini-3.5-flash-lite
      # Score a fixed 30-row subset on pull requests, everything on the nightly run
      GATE_SAMPLE: ${{ github.event_name == 'pull_request' && '30' || '0' }}
    steps:
      - uses: actions/checkout@v7
      - uses: astral-sh/setup-uv@v10
      - run: uv sync
      - name: Rebuild the dataset with the current pipeline
        run: uv run python refresh_dataset.py
      - name: Run Ragas quality gate
        run: uv run python quality_gate.py
      - name: Log scores
        if: ${{ always() && hashFiles('scores.csv') != '' }}
        run: uv run python track_scores.py
      - name: Keep the scores
        if: ${{ always() && hashFiles('scores.csv') != '' }}
        uses: actions/upload-artifact@v7
        with:
          name: ragas-scores
          path: |
            scores.csv
            ragas_history.jsonl

该工作流中的几个细节很关键:

  • 用 JUDGE_MODEL 及其匹配的 API 密钥固定评审。请使用您用来校准阈值的同一评审,否则 CI 分数将无法与跟踪历史相比。
  • 在 PR 上,GATE_SAMPLE 会对固定的随机 30 行子集打分,以免评审调用让评审等待 10 分钟。夜间定时任务会对全部样本打分。
  • 路径过滤仅在提示、检索器、评审设置或问题变更时运行质量闸。
  • 每次运行都会把 scores.csv 和 ragas_history.jsonl 作为构件上传,因为任务结束后 runner 的磁盘会消失。

请在夜间定时任务上运行完整套件,那时没有人在等待。失败的夜间任务会显示为失败的工作流,GitHub 的通知设置或一个 Slack 步骤可以在早上通知负责人。GitLab CI 用 rules: changes: 和 pipeline schedule 也能达到同样效果。

如何用 Ragas 生成合成测试数据?

Ragas 通过阅读您的文档、提取其主要主题与事实、关联相关片段、并基于这些关联生成不同类型的问题来产出合成测试问题。

测试数据生成文档将查询分为单跳(一个片段可回答)与多跳(答案需要多个片段),而每种又可具体或抽象。默认组合使用其中三种,并等比例混合:

  • 由单个片段可回答的单跳具体问题。
  • 需要跨多个片段推理的多跳抽象问题。
  • 将多个片段中各自的一个事实组合起来的多跳具体问题。

Ragas 只会使用您的文档能支持的合成器,因此小文档集可能产生不同组合。

生成器需要可读的真实文档。将其指向您的 markdown 文档文件。本文我为 Northwind 写了 5 篇短小的帮助中心页面(总计约 600 词),每篇像这样:

<!-- docs/subscription.md -->
# Subscriptions

A Northwind subscription delivers a fresh bag on a schedule you choose: every 1, 2, or 4 weeks. Subscribers save 10% on every bag and always get free standard shipping, regardless of order size.

You can pause a subscription for up to 3 months from your account page. Paused subscriptions keep their pricing. You can also skip a single delivery up to 48 hours before it is scheduled to ship.

Cancel at any time from the account page. Cancellations take effect immediately and no further bags are charged. There is no cancellation fee.

Subscribers can change their roast, grind, or bag size at any time. Changes made less than 48 hours before a scheduled shipment apply to the following delivery.

您自己的文档会产出不同于下文的提问,这正是重点:生成器会针对您的知识库实际陈述的内容出题。

TestsetGenerator 使用我们已经构建的相同评审 LLM 与嵌入模型。我用 LangChain 的 DirectoryLoader 加载文档,显式使用 TextLoader,这样 markdown 文件就不会拉入沉重的 unstructured 依赖。

对文档建图会同时触发很多评审调用,因此我也用 RunConfig 限制并行数量:

# generate_testset.py
"""Generate synthetic question-answer pairs from the documents in ./docs."""

import pandas as pd
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from ragas import RunConfig
from ragas.testset import TestsetGenerator

from judge import build_judge

judge_llm, judge_embeddings = build_judge()

loader = DirectoryLoader(
    "docs", glob="**/*.md", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"}
)
docs = loader.load()
print(f"Loaded {len(docs)} documents")

generator = TestsetGenerator(llm=judge_llm, embedding_model=judge_embeddings)
testset = generator.generate_with_langchain_docs(
    docs,
    testset_size=8,
    run_config=RunConfig(max_workers=4),  # keep the knowledge-graph build polite
)

frame = testset.to_pandas()
frame.to_csv("synthetic_testset.csv", index=False)

pd.set_option("display.max_colwidth", 70)
print(frame[["user_input", "synthesizer_name"]])

脚本加载了全部 5 篇文档并写出了以下 8 个问题。这些问题紧贴文档内容,且有些包含故意的拼写错误:

# Generated question Question type
1 how do i brew with a french pres Single-hop
2 Northwind equipment warranty Single-hop
3 Where is Northwinds roastery located in Portlnd where orders are shipped from? Single-hop
4 how do I cancel Nortwind subscription? Single-hop
5 Can u tell me about the warranty coverge for equipmet like grinders and how long do i have if i need to ask for refunds? Multi-hop
6 What specific equipment does Northwind sell for coffee brewing, and what are the terms of their manufacturer warranty regarding coverage and claims? Multi-hop
7 What is the shipping policy for orders and what are the return guidelines if I am not happy with my coffee or espresso equipment? Multi-hop
8 What is the shipping policy for orders from Northwind Coffee and what are the conditions for getting refunds? Multi-hop

表中显示的是简化标签;输出中的 synthesizer_name 列包含完整的合成器名称。5 篇文档产出 8 个问题大约耗时 4.5 分钟。

问题 2 中的拼写错误和关键词式查询是刻意为之:生成器会改变每个查询的风格与长度,让集合更像搜索框流量而不是考试题。

您大概率会想过目这些问题,确认它们可用并相关。我会把输出当作初稿。在我审阅过的每个合成集里,都有一些问题格式不佳(它们引用“该文档”或询问标题)、一些参考答案与源材料产生偏移、而且措辞比任何真实用户都更正式。

当您给文档新增模块时重跑生成器,您就能得到针对新内容的候选问题,代价是再来一轮评审调用。

每一行包含一个 user_input、一个生成的 reference 答案,以及 reference_contexts(生成该问题所用的片段)。请对照这些片段检查 reference,因为偏移就出现在这里。

要把经审核的一行变成 SingleTurnSample,让您的流水线跑一遍该 user_input 以获得 retrieved_contexts 和 response。将审核后的 user_input 与 reference 对追加到 eval_questions.jsonl,refresh_dataset.py 会为您完成其余部分。

如果您的知识库是一张图而不是一堆文档,同样的原则也适用,只是检索器不同。我们的 用知识图实现 RAG 应用教程与 Graph RAG with LangChain and Neo4j 课程都能产出流水线,您可以用完全相同的四个指标为其打分,因为这些指标只看检索到的文本,而不关心它是如何被检索到的。

如何随时间跟踪 Ragas 分数?

跟踪 Ragas 分数意味着在每次评估运行后,将平均分数、git 提交哈希、评审模型和时间戳追加到日志文件中,这样回归就会在折线图中表现为一个下跌,而不是线上事故中的意外。

在您需要多人共用的实验跟踪器之前,JSON Lines 文件可能是最佳选择。track_scores.py 读取的是 scores.csv,而 quality_gate.py 与 run_all.py 都会写它,因此它不会再触发第二轮评审调用。

# track_scores.py
"""Append the latest aggregate scores, commit hash, and timestamp to a log."""

import json
import subprocess
from datetime import datetime, timezone

import pandas as pd

from judge import judge_model_name

HISTORY_FILE = "ragas_history.jsonl"


def current_commit():
    """Return the short git hash, or 'no-git' outside a repository."""
    try:
        return subprocess.check_output(
            ["git", "rev-parse", "--short", "HEAD"],
            text=True, stderr=subprocess.DEVNULL,
        ).strip()
    except (subprocess.CalledProcessError, FileNotFoundError):
        return "no-git"


def main():
    scores = pd.read_csv("scores.csv")
    means = scores.drop(columns="user_input").mean().round(4)

    record = {
        "timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "commit": current_commit(),
        "judge_model": judge_model_name(),
        "n_samples": len(scores),
        **means.to_dict(),
    }
    with open(HISTORY_FILE, "a", encoding="utf-8") as history:
        history.write(json.dumps(record) + "\n")
    print(json.dumps(record, indent=2))


if __name__ == "__main__":
    main()

运行 uv run python track_scores.py 会在日志中追加一行并打印它:

{
  "timestamp": "2026-09-18T03:23:51+00:00",
  "commit": "85834f4",
  "judge_model": "gemini-3.5-flash-lite",
  "n_samples": 6,
  "faithfulness": 0.7778,
  "answer_relevancy": 0.6678,
  "context_precision_with_reference": 0.7222,
  "context_recall": 0.8333
}

在 CI 中,每个任务开始时磁盘都是空的,因此 ragas_history.jsonl 只会保存本次运行的记录。若要保留趋势,请下载已上传的构件并追加到一个文件中,或将该文件提交到一个度量分支或对象存储。

绘制历史非常容易。尝试以下脚本:

# plot_history.py
"""Plot every logged Ragas run so regressions show up as a dip in a line."""

import matplotlib.pyplot as plt
import pandas as pd

history = pd.read_json("ragas_history.jsonl", lines=True)
history["timestamp"] = pd.to_datetime(history["timestamp"])
history["run"] = history["commit"] + "\n" + history["timestamp"].dt.strftime("%m-%d %H:%M")

METRICS = {
    "faithfulness": "#2a78d6",
    "answer_relevancy": "#eb6834",
    "context_precision_with_reference": "#1baf7a",
    "context_recall": "#eda100",
}

fig, ax = plt.subplots(figsize=(9, 4.5))
for metric, color in METRICS.items():
    ax.plot(history["run"], history[metric], color=color, linewidth=2,
            marker="o", markersize=6, label=metric)
    ax.annotate(f"{history[metric].iloc[-1]:.2f}",
                (history["run"].iloc[-1], history[metric].iloc[-1]),
                xytext=(6, 0), textcoords="offset points", va="center",
                fontsize=9, color="#333333")

ax.set_ylim(0, 1.05)
ax.set_ylabel("mean score")
ax.set_title("Ragas scores by commit")
ax.grid(axis="y", color="#e5e5e5")
ax.spines[["top", "right"]].set_visible(False)
ax.legend(loc="lower left", frameon=False)
fig.tight_layout()
fig.savefig("ragas_history.png", dpi=150)
print("Saved ragas_history.png")

您会得到一张类似这样的图:

按提交展示的 Ragas 各均值分数折线图:faithfulness、answer relevancy、context precision、context recall

由 track_scores.py 记录的各提交的 Ragas 均值分数。图源作者。

在那些提交之间,流水线并未改变(第二次只更改了评审客户端的连接设置),但样本 6 的拒答从 1.0 翻到 0.0,使得 faithfulness 从 0.78 降至 0.61。

因此在您拥有大量样本之前,确实存在需要留意的评审噪声。6 个样本下,1 次波动裁决会让均值移动 0.17。100 个样本下同样翻转只会移动 0.01,因此大幅变化更有可能是真实回归。

现在,每个 PR 或提交,您都可以运行质量闸、记录分数,并查看图表。

如果您在权衡更大的架构变更,我们的 RAG 与微调对比教程提供了思路框架,而相同的四个指标也可用作任一路径前后的度量。

LLM-as-a-judge 分数有哪些局限?

LLM 作为评审的分数只是估计值:它们具有已知偏差,且在不同运行间会变化,但它们能扩展到人工评审无法覆盖的评估量级。

Ragas 从不要求评审比较两个答案,因此位置偏置(偏好先出现的答案)基本不适用。冗长偏置(偏好更长的答案)与自偏好偏置(评审偏好来自自身模型的答案)仍可能有影响。Zheng 等(2023)描述了这三种偏置,而自偏好也是我不建议使用同一模型作为生成器与评审的原因。

我希望您亲眼看到跨运行变化,因为它会改变您设置阈值的方式。在相同 6 个样本、相同固定评审的多次运行中,样本 6 的拒答在一次运行中在 faithfulness 上得 0.0,而在另一次中得 1.0;而完全扎根的样本 5 在上文所示运行中得了 0.667。

来自便宜评审的逐样本分数有噪声。对 100 个样本取均值的噪声要小得多,而 CI 质量闸应始终检查均值。

三条实践可以控制噪声:

  1. 固定评审模型并记录它,就像上面的 judge.py 与 track_scores.py 那样,避免将评审变更误判为流水线变更。
  2. 用人工抽检对照评审,在 20 至 30 个样本上设置。当评审与同事的判断有超过少数分歧时,在信任单一道闸前先换成更强的模型。
  3. 将拒答单独打分,不要并入 faithfulness,因为“我不知道”这类答案没有可供评审验证的断言,分数是任意的。

若想更广泛地了解超出 RAG 之外的评估方法,包括评审所替代的人类评估方法,我们的 LLM 评估指南涵盖了相关指标与方法学。我们的 LLM 作为评审教程更深入地讲解了如何为 RAG 流水线构建评审。

总结

这条流水线的意义在于,它让我们可以说出诸如“检索器没问题,是提示退化了”或“重排序修复了 precision,但 recall 仍为 0.6”之类的话,并有数值为证。

Faithfulness 与 answer relevancy 关注生成器:前者抓住编造的事实,后者抓住偏离问题的回答。Context precision 与 recall 关注检索器,告诉您它是否取回了正确的信息。由于四者中的两个无需参考答案,您可以在没有参考的生产流量上运行它们。

标准文本指标衡量的是词语重叠,它无法揭示被编造的退款政策;而单一端到端数值也掩盖了流水线哪一半出了问题。能分别检查两个失败点的框架才能看见它,把它从一次性审计变成带历史的 CI 质量闸,才会促使团队依据数据采取行动。

如果您尚未构建该流水线,请先学习我们的 Retrieval-Augmented Generation with LangChain 课程,然后将本文的 pipeline.py 指向您构建的内容。

对于超越纯文本检索的流水线,我们的 End-to-End RAG with Weaviate 课程、多模态 RAG 指南和 代理式 RAG 教程都构建了可以用相同方式打分的系统,但有一条注意:四个核心指标只读取文本。检索图像的流水线需要 Ragas 的多模态指标;代理式流水线需要把每一步检索都作为独立的问题、片段与答案来打分。

FAQs

Ragas 的 faithfulness 与 answer relevancy 有何区别?

Faithfulness 检查答案中的断言是否得到检索上下文支持,因此能抓住幻觉。Answer relevancy 检查答案是否回应了所提问题,因此能抓住漂移。一个答案可能完全忠实却仍然离题,这也是为什么两者都需要。

使用 Ragas 是否需要有真实参考答案?

不需要用于 faithfulness 和 answer relevancy,它们是无参照的,适用于线上生产流量。此处使用的 context precision 与 context recall 需要参考答案,因此请为检索诊断构建一个包含 50 到 100 个带参考的精编样本集,并在其他所有数据上运行无参照指标。

Ragas 应该搭配哪种评审模型?

选择在您人工核验样本上与您判断一致且成本最低的模型,并在代码中固定它。像 gpt-4o-mini 或 gemini-3.5-flash-lite 这样的小模型足以应对 Ragas 的断言级分类,但要预期逐样本有噪声,并始终以全量样本的均值设闸。

为何我用新指标调用 Ragas 的 evaluate() 会失败?

在 Ragas 0.4 中,ragas.metrics.collections 里的指标不再继承 evaluate() 期望的旧 Metric 类,且 evaluate() 本身已弃用。请像本文 score.py 辅助脚本那样,在您自己的循环中用各指标的 ascore() 方法打分,或改用 @experiment 装饰器。

RAG 评估数据集需要多少样本?

在允许阈值阻止合并之前,请至少使用 50 到 100 个样本,并确保它们包含真实用户措辞、多文档问题,以及文档无法回答的问题。6 个样本足以学习指标,但远不足以区分回归与评审噪声。


Tim Lu's photo
Author
Tim Lu
LinkedIn

我是一名数据科学家,擅长空间分析、机器学习和数据管道。曾使用过 GCP、Hadoop、Hive、Snowflake、Airflow 等数据科学/工程相关流程与工具。

主题
人工智能
大语言模型

Top DataCamp Courses

课程

使用 LangChain 和 Neo4j 的 Graph RAG

3 小时
2.3K
使用 Graph RAG 和混合 RAG 构建更准确、更可靠的 RAG 系统。
查看详情Right Arrow
开始课程
查看更多Right Arrow