前言
为了直观的看到 rag 的准确率,可以使用 Ragas 对 rag 项目进行评估测试。这是一个以 LLM 作为评判者的开源评估框架,它把 RAG 系统的表现拆成检索质量、生成质量、事实正确性三条主线, 每条主线给出可比的 0~1 分数。
一、为什么 RAG 需要评估
RAG 很难套用传统任务的指标, 同一个问题可以有十种正确表述,字面相似度低不代表答错,字面相似度高也不代表没有幻觉。 于是很多团队的 RAG 评估停留在「vibe check」------随便问几个问题,读一遍回答,感觉还行就上线了。
这种做法的三个死角几乎必然出现:
- 不可比较。 把
top_k从 5 改成 3、把 embedding 换成更大的模型、把提示词重写一遍------ 效果到底是变好还是变坏? - 无法定位。 一个回答错了,可能是检索没召回、可能是召回了但没排前面、也可能是召回了却没用上、 还可能是模型自己编了,定位困难。
- 不可回归。 今天优化的问题,换个 chunk 策略又回来了。
Ragas 要解决的正是这件事:把 RAG 系统从「凭感觉」推向「凭指标」,让每次改动都有可观测的后果。
二、Ragas 是什么
Ragas(RAG Assessment)是一个 Python 开源库,用于评估 LLM 应用,尤其是检索增强生成系统。 它的核心机制是 LLM-as-judge:把评估这件事本身也交给 LLM------让一个「评判模型」 读你的检索上下文、回答和参考答案,按预设的判定逻辑输出一个分数或判定结果。
2.1 理解定位
Ragas 不是一个 RAG 框架,它不负责检索、也不负责生成。它只接收 RAG 各流程跑出来的产物,算出可比的分数。不管用什么 RAG 架构都不影响接入。
Ragas 在 RAG 系统中的位置:
vbscript
user_input + retrieved_contexts + response
Ragas 评估层
检索指标
生成指标
事实指标
你的 RAG 系统(保持不变)
检索器
提示词 + LLM
评估报告
逐样本得分 + 均值
Ragas 负责把以下这三条线路的得分算出来:检索指标看「拿到的资料是否准确」, 生成指标看「回答有没有用好资料」,事实指标看「回答与标准答案差距」。
2.2 它和传统指标的关键差别
- 语义判定: 指标基于 LLM 的语义理解,能识别「爱因斯坦 1879 年 3 月 14 日出生」 和「爱因斯坦出生于 1879 年 3 月 14 日」是同一件事。
- 大量指标不需要标准答案: 忠实度、回答相关性等指标只依赖系统自己的输出, 这解决了 RAG 项目最现实的困境:标注数据太贵。
- 指标是「范式」: 每个指标内部可能包含一次或多次 LLM 调用、 断言拆解、自然语言推理等步骤,我们拿到的是一个解释得清的口径。
- 面向的对象更多: 除 RAG 外,同一套框架还覆盖 Agent 工具调用、SQL 生成、 摘要、以及任意自定义任务的打分。
2.3 生态与现状
Ragas 采用 Apache-2.0 协议开源,由 Vibrant Labs 维护(早期由 Exploding Gradients 团队发起), 官方文档站为 docs.ragas.io,同时在 GitHub 上持续迭代。它提供命令行工具用于快速拉起示例项目, 并可与 LangChain、LlamaIndex、LangSmith、MLflow、Arize 等生态组件配合使用。
注意:API 正在换代
官方目前存在两套指标 API:新的 collections 版 (ragas.metrics.collections, 逐指标调用、支持异步)和旧的 legacy 版 (ragas.metrics 下的 LLMContextRecall 等,配合 SingleTurnSample 使用)。 文档明确说明 legacy API 将在 0.4 版本标记弃用、1.0 版本移除。 本文示例统一采用 collections 版;如果你在旧教程里看到 single_turn_ascore,那是 legacy 写法。
三、Ragas 评估数据模型
理解 Ragas 最快的方式,是理解一条「评估样本」长什么样。Ragas 的数据模型是 SingleTurnSample(单轮问答)与 MultiTurnSample(多轮对话), 对 RAG 场景而言,核心就是下面这几个字段:
| 字段 | 含义 | 提供者 | 缺失后果 |
|---|---|---|---|
user_input |
用户提出的问题 | 测试集 | 所有指标都无法运行,这是样本主键 |
retrieved_contexts |
检索器实际返回的上下文片段列表 | RAG 流程 | 忠实度、上下文精确度/召回率全部失效 |
response |
RAG 系统生成的最终回答 | RAG 流程 | 回答相关性、忠实度、事实正确性无法评估 |
reference |
参考答案(标准答案) | 人工标注或合成测试集 | 上下文召回率、事实正确性等有参考指标不可用 |
reference_contexts |
参考答案对应的标准上下文 | 标注 / 合成测试集 | 只能用非 LLM 的上下文召回变体 |
另有 retrieved_context_ids 与 reference_context_ids,用于基于文档 ID 的检索指标。
有了这张表,后面的接入就一目了然:前三个字段由我们的 RAG 系统产生,后两个字段由测试集提供。 换句话说,接入 Ragas 的工程量,取决于 RAG 能不能产出 retrieved_contexts 。
为什么有参考和无参考这么重要:
Ragas 的指标可以按是否依赖 reference 分成两类。无参考指标(如忠实度、回答相关性) 只需要系统自己的输出,因此可以零标注起步;有参考指标(如上下文召回率、事实正确性) 需要标准答案,但能回答「答得对不对」这个更本质的问题。真实项目里通常是先上无参考指标, 再逐步补标注数据引入有参考指标。
四、能评估什么
官方把指标按应用场景分成几大类:检索增强生成、Nvidia 指标、Agent 与工具调用、 自然语言比较、SQL、通用任务,以及摘要。
4.1 检索与生成:RAG 的主力指标
RAG 场景常用指标
| 字段 | 衡量什么 | 需要 reference | 什么时候看 |
|---|---|---|---|
Faithfulness 忠实度 |
回答拆成断言后,有多少条能在检索上下文中找到依据。分数 = 被上下文支持的断言数 ÷ 断言总数 | 否 | 查幻觉的第一指标。回答漂亮但凭空编造时,这里会掉分 |
ResponseRelevancy 回答相关性 |
回答是否真正切中问题意图,而非答非所问 | 否(需要 embedding 模型) | 回答冗长、绕开问题时。它比「看起来对不对」更能反映切题程度 |
ContextPrecision 上下文精确度 |
相关片段是否被排在检索结果靠前的位置,本质是各排名位 precision@k 的加权均值 | 是(另有 ContextUtilization 变体不需要) |
调 top_k、加 reranker、换排序策略时必看 |
ContextRecall 上下文召回率 |
参考答案拆成断言后,有多少条能被检索上下文支撑分数 = 被支撑的断言数 ÷ 参考断言总数 | 是 | 回答缺信息时的第一嫌疑。召回率低说明「该拿的资料没拿到」 |
ContextEntitiesRecall 上下文实体召回率 |
参考答案里的关键实体有多少出现在检索上下文中 | 是 | 专有名词、型号、数字密集的领域知识库 |
NoiseSensitivity 噪声敏感度 |
上下文里混入无关内容时,系统给出错误回答的频率,越低越好 | 是 | 上下文塞得很满、召回条数很多时,检查鲁棒性 |
FactualCorrectness 事实正确性 |
回答与参考答案在事实断言层面的 precision / recall / F1,默认取 F1,可用 mode 切换 |
是 | 有标准答案时的主指标,直接回答「答得对不对」 |
SemanticSimilarity 语义相似度 |
回答与参考答案的向量相似度 | 是 | 便宜的粗筛。注意它只看相似、不看对错 |
此外还有多模态场景的 MultimodalFaithfulness 与 MultimodalRelevance, 以及 Nvidia 系列指标 AnswerAccuracy、ContextRelevance、ResponseGroundedness,适合已经在用 Nvidia 评估体系的团队。
4.2 自定义标准:当内置指标不够用
真实业务里总有些判断标准是现成指标覆盖不到的,比如「语气是否专业」「是否泄露内部编号」。 通用类指标就是为这种情况准备的:
AspectCritic(方面批判): 用一句自然语言定义判定标准,输出 0/1 的二值结果。 内部会多次调用 LLM 并取多数票,例如「这段回答是否有害」。DiscreteMetric(离散打分): 最灵活的一个。自己写 prompt、 自己指定取值集合------可以是0~10、1~5,也可以是pass/fail。 官方 RAG 教程里用的就是它。RubricsScore(评分标准评分): 给出一套 1~5 分的分档描述, 所有样本用同一把尺子打分,适合需要稳定分档的主观质量评估。InstanceRubrics(实例级评分标准): 每条样本带自己的评分标准, 相当于「一人一张卷子」,适合评判标准因样本而异的场景。
4.3 其他场景:不止 RAG
| 场景 | 代表指标 | 说明 |
|---|---|---|
| Agent / 工具调用 | ToolCallAccuracy、ToolCallF1、AgentGoalAccuracy、TopicAdherence |
评估工具选择是否正确、目标是否达成、是否跑偏主题。 |
| SQL 生成 | SQLSemanticEquivalence、执行结果比对(DataCompy) |
判断生成的 SQL 与参考 SQL 是否语义等价,或直接执行后比对结果集 |
| 摘要 | SummarizationScore |
衡量摘要是否覆盖了原文重要信息,可选加入简洁性惩罚 |
| 传统指标 | BLEU、ROUGE、CHRF、ExactMatch、StringPresence |
非 LLM 的字面匹配,便宜且确定性强,适合格式校验或严格匹配场景,不适合开放式问答 |
五、指标怎么选
指标不是越多越好------每个 LLM 指标都是一次(或多次)真实的 API 调用,跑得越多越贵、越慢。 选指标时按顺序问自己三个问题就够了。
问题一:有没有标准答案?
- 没有
reference: 只能在无参考指标里选:Faithfulness、ResponseRelevancy、ContextUtilization、AspectCritic、DiscreteMetric。 - 有
reference: 才能使用ContextRecall、FactualCorrectness这些能回答「对不对」的指标。
问题二:要诊断检索还是生成?
- 诊断检索: 用
ContextPrecision(排序好不好)+ContextRecall(有没有漏)+ContextEntitiesRecall(关键实体全不全)。 - 诊断生成: 用
Faithfulness(有没有编)+ResponseRelevancy(切不切题)。 - 端到端体检: 以上两组各取两个,再加一个
FactualCorrectness。
问题三:要做绝对打分还是版本对比?
绝对打分容易陷入「0.72 分算好还是不好」的争论。更有价值的是版本对比------同一批数据、 同一套指标、同一个评判模型,只改一个变量,看分数的方向。这种用法下,指标的具体数值不那么重要, 一致性和可复现性才是关键。
推荐起步组合:
- 有标注:
Faithfulness+ResponseRelevancy+ContextPrecision+ContextRecall+FactualCorrectness。 - 无标注:
Faithfulness+ResponseRelevancy+ContextUtilization, 再加一个自定义的DiscreteMetric兜住业务特有的判定标准。
六、实战:在已有 RAG 项目中接入 Ragas
下面以一个典型的「检索器 + 提示词 + LLM」RAG 流程为例,走完从安装到出报告的全过程。全程不需要改动项目的检索或生成逻辑。
6.1 安装
bash
pip install ragas
# 用 OpenAI 作为评判模型时,把客户端依赖一并装好
pip install openai
Ragas 同时提供命令行工具:ragas quickstart rag_eval 可以直接生成一个可运行的 RAG 评估示例工程,ragas quickstart 会列出所有可用模板; 也可以用 uvx ragas 免安装试跑。
6.2 改造点
这是整个接入过程中唯一需要改业务代码的地方。改造前,RAG 入口大概长这样:
python
def answer(query: str) -> str:
docs = retriever.invoke(query)
context = "\n\n".join(d.page_content for d in docs)
return chain.invoke({"context": context, "query": query})
问题在于它只返回了字符串,检索到的上下文被丢掉了。Ragas 需要看到中间产物,所以把它改成返回一个字典:
python
def answer_with_trace(query: str) -> dict:
docs = retriever.invoke(query)
context = "\n\n".join(d.page_content for d in docs)
response = chain.invoke({"context": context, "query": query})
return {
"user_input": query,
"retrieved_contexts": [d.page_content for d in docs], # 新增
"response": response,
}
注意字段名要和 Ragas 的约定完全一致:user_input、retrieved_contexts、 response。拼错不会报错,只会让指标静默失效。
6.3 初始化评判模型
评估本身也要消耗模型调用,所以需要单独指定一个「评判模型」。 llm_factory 接受一个 OpenAI 兼容的异步客户端,因此自建推理服务、 国内厂商的兼容接口都能直接接进来:
python
from openai import AsyncOpenAI
from ragas.llms import llm_factory
def build_judge() -> object:
client = AsyncOpenAI(
api_key="sk-...",
# 自建或第三方兼容服务,改这一行即可
# base_url="https://your-endpoint/v1",
)
return llm_factory("gpt-4o-mini", client=client)
评判模型不必和被评系统用同一个模型。事实上,用同一个模型评自己会带来自评偏差, 条件允许时建议换一个模型家族。用便宜的小模型做裁判是控制成本的常规做法。
6.4 collections API
新的 collections API 把每个指标做成一个独立的评分器对象,构造时注入评判模型, 调用 ascore() 传入该指标需要的字段,返回结果对象的 .value 就是分数。 同步场景可以用 .score()。
python
import asyncio
import json
import pandas as pd
from ragas.metrics.collections import (
ContextPrecision,
ContextRecall,
FactualCorrectness,
Faithfulness,
)
from judge import build_judge
from rag_pipeline import answer_with_trace
async def evaluate(rows: list[dict], judge) -> list[dict]:
faithfulness = Faithfulness(llm=judge)
context_precision = ContextPrecision(llm=judge)
context_recall = ContextRecall(llm=judge)
factual_correctness = FactualCorrectness(llm=judge, mode="f1")
async def score(row: dict) -> dict:
# 先跑 RAG 流程,拿到 response 和 retrieved_contexts
trace = answer_with_trace(row["user_input"])
contexts = trace["retrieved_contexts"]
return {
**row,
"response": trace["response"],
"faithfulness": (await faithfulness.ascore(
user_input=row["user_input"],
response=trace["response"],
retrieved_contexts=contexts,
)).value,
"context_precision": (await context_precision.ascore(
user_input=row["user_input"],
reference=row["reference"],
retrieved_contexts=contexts,
)).value,
"context_recall": (await context_recall.ascore(
user_input=row["user_input"],
reference=row["reference"],
retrieved_contexts=contexts,
)).value,
"factual_correctness": (await factual_correctness.ascore(
response=trace["response"],
reference=row["reference"],
)).value,
}
# 并发跑完所有样本;样本量大时可以加信号量限流
return await asyncio.gather(*(score(row) for row in rows))
if __name__ == "__main__":
with open("datasets/eval_rows.json", encoding="utf-8") as f:
rows = json.load(f)
df = pd.DataFrame(asyncio.run(evaluate(rows, build_judge())))
metric_cols = ["faithfulness", "context_precision",
"context_recall", "factual_correctness"]
df.to_csv("rag_eval_report.csv", index=False)
print(df[metric_cols].mean().round(3))
注意几个细节:
FactualCorrectness只需要response和reference, 不需要上下文;- 而
ContextPrecision和ContextRecall都需要reference。如果你暂时没有参考答案,就把这两个从指标列表里去掉, 换成不需要参考的ContextUtilization。
6.5 @experiment 装饰器
官方 RAG 教程主推的是另一种更工程化的写法:用 @experiment 装饰器把一个函数声明成「实验」, 用 Dataset 加载 CSV 数据集,运行时结果会自动落到带时间戳的 experiments/ 目录里。
python
import asyncio
from openai import AsyncOpenAI
from ragas import Dataset, experiment
from ragas.llms import llm_factory
from ragas.metrics import DiscreteMetric
from rag_pipeline import answer_with_trace
judge = llm_factory("gpt-4o-mini", client=AsyncOpenAI())
# 自定义一个业务口径的判定指标:只输出 pass / fail
grounded = DiscreteMetric(
name="grounded",
prompt=(
"判断回答是否只使用了检索上下文中的信息,并覆盖了参考答案的要点。\n"
"检索上下文:{retrieved_contexts}\n"
"回答:{response}\n"
"参考答案:{reference}\n"
"只输出 pass 或 fail。"
),
allowed_values=["pass", "fail"],
)
@experiment()
async def run_rag_experiment(row):
trace = answer_with_trace(row["user_input"])
score = await grounded.ascore(
llm=judge,
retrieved_contexts="\n\n".join(trace["retrieved_contexts"]),
response=trace["response"],
reference=row["reference"],
)
return {
**row,
"response": trace["response"],
"grounded": score.value,
"experiment_name": "rag_baseline", # 结果按这个名字归档
}
dataset = Dataset.load(
name="rag_eval_set",
backend="local/csv",
root_dir="./datasets",
)
results = asyncio.run(run_rag_experiment.arun(dataset))
这种写法的好处是把「跑系统」和「算指标」放在同一个循环里,每次实验的产物天然带上了名字和时间戳, 便于事后对比。两种路径不必二选一:日常快速检查用 collections API, 正式的版本对比用 @experiment。 如果工程已经在用 EvaluationDataset 或更习惯批量接口,官方 Get Started 里还保留了 evaluate(dataset, metrics=[...], llm=...) 的批量入口, 返回 EvaluationResult,可以用 to_pandas() 转成表格继续分析。
6.6 结果不要只看均值
跑完之后获得一张逐样本的 CSV,最后一列才是均值。真正有信息量的是另外三件事:
- 指标之间的落差: 如果
context_recall很高但faithfulness很低, 说明资料都拿到了,问题出在生成环节------模型没用好上下文,或者提示词允许它自由发挥。 反过来,如果context_recall本身就低,先别改提示词,去修检索。 - 最差的那几条样本: 按最低分排序看前 10 条,通常能一眼看出问题模式: 全是同一类问题、全是同一个文档来源、全是超长问题。均值会掩盖这些长尾。
- 分数的稳定性: LLM 判定本身有随机性,同一批数据跑两次结果会略有浮动。 因此版本对比时,分数差异要明显大于噪声才有意义------这也是为什么更推荐固定数据集上的横向对比, 而不是纠结单次绝对值。
七、没有标注数据怎么办
大多数 RAG 项目卡在第一关:没有评估数据集。TestsetGenerator 的作用是从你自己的文档语料里 反向生成问题和参考答案------它会先把文档构建成知识图谱,再基于图谱合成不同类型的问题。
python
from langchain_community.document_loaders import DirectoryLoader
from langchain_openai import ChatOpenAI
from openai import OpenAI
from ragas.embeddings import OpenAIEmbeddings
from ragas.llms import LangchainLLMWrapper
from ragas.testset import TestsetGenerator
# 1. 加载你自己的知识库文档
docs = DirectoryLoader("./knowledge_base", glob="**/*.md").load()
# 2. 生成器需要一个生成用的 LLM 和一个 embedding 模型
generator = TestsetGenerator(
llm=LangchainLLMWrapper(ChatOpenAI(model="gpt-4o")),
embedding_model=OpenAIEmbeddings(client=OpenAI()),
)
# 3. 生成测试集
testset = generator.generate_with_langchain_docs(docs, testset_size=30)
df = testset.to_pandas()
df.to_csv("datasets/rag_eval_set.csv", index=False)
print(df.columns.tolist())
生成的每条样本包含 user_input、reference、reference_contexts, 以及标记它由哪个合成器产生的 synthesizer_name。
注意:合成集只是半成品,合成测试集不含
retrieved_contexts和response------ 这两个字段必须由 RAG 系统实际跑一遍才会产生。所以正确的用法是: 用合成集得到「问题 + 标准答案」,再让 RAG 流程对每个问题跑一次,把产物补齐,才能进入评估。 另外,合成题的质量取决于语料质量,生成后建议人工过一遍,剔除明显不合理或答案有歧义的题目。
八、把评估变成习惯
一次性的评估报告价值有限,真正产生作用的是把它变成日常动作。要做版本对比, 必须先把四件事固定下来,否则分数不可比:
- 数据集固定: 同一批问题,不要每次换题;新增题目时保留旧题。
- 指标集固定: 不要这次跑 5 个、下次跑 3 个,否则均值没有可比性。
- 评判模型固定: 换评判模型相当于换裁判,除非你就是在对比裁判。
- 阈值固定: 给关键指标定一个基准线,如果低于基准线就不发布。
参数化实验做 A/B
@experiment 支持额外参数,因此对比两个检索策略、两个提示词、两个模型, 只需要同一个实验函数跑两次:
python
@experiment()
async def ab_experiment(row, retriever_name: str):
trace = answer_with_trace(row["user_input"], retriever=retriever_name)
return {
**row,
"response": trace["response"],
"variant": retriever_name,
"experiment_name": f"retriever_{retriever_name}",
}
async def main():
dataset = Dataset.load(name="rag_eval_set",
backend="local/csv", root_dir="./datasets")
await ab_experiment.arun(dataset, retriever_name="bm25")
await ab_experiment.arun(dataset, retriever_name="hybrid")
# 两次结果分别归档在 experiments/ 下的两个 CSV 里,可直接 diff
做对比时还有一个容易被忽略的纪律:一次只改一个变量。 同时换 embedding 模型和提示词,即使分数涨了,你也不知道是哪一项起了作用。
接进 CI
当基线稳定后,可以把评估变成流水线的一环:每次改动 RAG 相关代码时, 在固定数据集上跑一遍评估,把关键指标与上次基线对比,跌破阈值就让流水线失败。 由于评估结果本身就是 CSV,接入成本主要在「怎么定阈值」而不是「怎么跑」。
九、踩坑清单与成本控制
RAG 评估常见问题
| 问题 | 原因 | 处理方式 |
|---|---|---|
| 忠实度、上下文类指标恒为 0 或直接报错 | retrieved_contexts 是空列表 |
先确认检索器对该问题确实有返回;过滤掉空上下文样本,或把它当作「检索失败」单独统计 |
| 某些指标静默失效 | 字段名拼写与 Ragas 约定不一致 | 严格使用 user_input / retrieved_contexts / response / reference,不要自造字段名 |
| 有参考指标报缺字段 | 测试集没有 reference |
要么补标注,要么改用无参考指标(ContextUtilization 等) |
| 分数普遍虚高 | 评判模型与被评系统同源,自评偏差 | 换用不同模型家族的评判模型;定期人工抽查几条高分样本做校准 |
| 成本或耗时失控 | 上下文过长、样本过多、指标过多 | 控制 top_k 与单片段长度;先抽 20~50 条跑通再全量;优先用无参考指标 |
| 照抄旧教程代码报错 | 用了 legacy 版指标 API | 改用 ragas.metrics.collections 下的指标与 ascore() |
| 均值不错但用户仍不满意 | 只看均值,忽略了长尾和失败模式 | 按最低分排序逐条看前 10 条,按问题类型分组统计 |
成本控制
- 用小模型当裁判。 评判任务的难度通常低于生成任务,
gpt-4o-mini级别的模型往往够用,成本能降一个数量级。 - 优先无参考指标。 它们只需系统自己的输出,调用次数更少。
- 抽样起步。 先用 20~50 条把流程跑通、把指标口径确认下来,再决定是否全量。
- 用本地模型替代 LLM 判定。 官方为忠实度提供了基于 Vectara HHEM-2.1-Open 分类器的实现
FaithfulnesswithHHEM, 这个模型免费、体积小、可本地部署,适合放进高频的生产检查里。
如果需要在评估流程里统计 token 消耗,官方提供了 TokenUsageParser 机制, 以及针对 OpenAI 的 get_token_usage_for_openai 辅助函数, 可以把每次评估的用量记录下来做成本核算。
十、总结
Ragas 的价值不在于它能给出一个绝对正确的分数------LLM 评判必然带有噪声, 分数也不是真理。它的价值在于把讨论从「我觉得这样更好」变成「这版比上版在哪个指标上好了多少」。 对于 RAG 这种改一个参数就可能牵动全局的系统,这种可比性本身就是最大的收益。
相关参考
- Ragas 官方文档首页 --- docs.ragas.io/en/stable/
- Evaluate a simple RAG system(RAG 评估教程) --- docs.ragas.io/en/stable/t...
- RAG Evaluation Quickstart --- docs.ragas.io/en/stable/g...