检索链跑通之后,调优会变成一件很容易“凭感觉”的事。换一个 Embedding、调一次 top-k、改一句 Prompt,答案看起来好像更好了,但到底是真的提升,还是刚好这次答得不错,很难判断。
要把这种判断变成可复现的结果,就需要评估。
RAG 不能只看一个总分。基础评估通常拆成两部分:检索是否找对了,生成是否用好了。
这一篇先看四个常用指标:
context_recall:上下文召回率(检索层:有没有把需要的参考资料捞出来)context_precision:上下文精确率(检索层:捞出来的片段里,有多少是真正有用的)faithfulness:忠实度(生成层:答案是否完全基于检索上下文,衡量幻觉)answer_relevancy:答案相关性(生成层:最终回答是否贴合用户原始提问)
前两个评检索,后两个评生成。
评估可以人工做,也可以自动做。人工评估更接近真实使用,但成本高、速度慢,评估者之间也可能存在差异;自动评估适合批量运行,判定标准相对统一,但评估模型本身也可能有偏差。
评测不是上线前的一次检查,而是每次改动后的对照实验。
#一、没有基线,改动就只是印象
进入调优阶段后,改动会变得频繁:换模型、改 Prompt、调整切分参数、修改 top-K。
如果每次都只重新问几个问题,再凭感觉判断“这次好像更准”,很容易被模型输出的随机性影响。
知识库本身也会变化。新增、删除文档后,同一个问题可能检索到不同的内容。
所以评估首先要建立一份固定测试集和基线结果。每次改动后都跑同一批问题,再比较指标变化。
评估要解决的不是“这次答案看起来怎么样”,而是“这次改动相比上一版到底提升了什么、退化了什么”。
#二、评估分两段,输入也不一样
一次 RAG 问答大致经过两个阶段:问题 -> 检索相关上下文 -> 模型根据上下文生成答案。
因此评估也分成两部分:
-
检索评估:取回来的内容是否正确、完整;
-
生成评估:模型有没有正确使用这些内容回答问题。
这样拆开的原因很简单:答案不好,可能是没检索到,也可能是检索到了但模型没用好。
分开的理由很实在:答案不对,可能是没有检索到,也可能是检索到了模型没用。
如果只看最终答案分数,即使知道结果下降了,也不知道应该改 Retriever 还是 Prompt / Model。
一批评估数据通常会包含下面几类字段:
question:用户输入的问题;answer:被测应用给出的答案;contexts:这次检索实际返回的上下文;ground_truth:人工标注的参考答案。
不同指标依赖的输入不同:
| 指标 | 评什么 | 主要输入 |
|---|---|---|
context_recall | 应该找回的信息有没有漏掉 | 检索上下文、参考答案 |
context_precision | 找回来的内容是否真正相关 | 问题、参考答案 |
faithfulness | 回答是否有上下文依据 | 回答、检索上下文 |
answer_relevancy | 回答是否真正回应问题 | 问题、回答 |
ground_truth 是监督评估的重要基准。尤其是 context_recall,需要知道“本来应该覆盖哪些信息”,才能判断检索结果有没有漏掉关键内容。
不同 Ragas 指标实现对字段的要求可能略有差异,例如部分 context_precision 实现也会使用参考答案。真正运行时,应以具体 Metric 要求的字段为准。
#三、检索的两个指标:找全了吗,排对了吗
检索评估要回答两个问题:
该找回来的信息有没有找全,找回来的相关内容有没有排在前面。
前者看 context_recall,后者看 context_precision。
context_recall 衡量的是覆盖程度。
Ragas 会把参考答案拆成若干条关键信息,再判断这些信息能不能从检索到的上下文中得到支持。比如参考答案包含两个要点,而检索结果只覆盖其中一个,召回率就是 0.5。
Context Recall = 被检索上下文覆盖的参考答案要点 ÷ 参考答案中的全部要点
因此,context_recall 依赖参考答案。上下文看起来很多,并不代表真正覆盖了回答问题所需的信息。
召回低,说明该找回来的信息漏掉了。 即使最终答案写得很顺,也可能只是模型用自身知识补出来的。
context_precision 衡量的是相关内容的排序质量。
它会逐个判断检索结果是否相关,再根据相关 chunk 出现的位置计算分数。相关内容越靠前,得分越高;无关内容排在前面,会拉低分数。
例如五个检索结果的相关性依次是:【1, 0, 1, 1, 0】,三个相关 chunk 位于第 1、3、4 位,分数约为:(1/1 + 2/3 + 3/4) / 3 ≈ 0.8056,如果把第 3 位的相关 chunk 提到第 2 位:【1, 1, 0, 1, 0】,
相关 chunk 数量没有变化,但排序更靠前,分数会上升。
所以这里的 Precision 更接近排序质量,而不是简单的“相关块占比”。
这两个指标经常存在取舍:
top-k调大,更不容易漏掉信息,context_recall往往会上升;- 但检索结果越多,也越可能混入无关内容,
context_precision可能下降; top-k太小,上下文更干净,但漏掉关键信息的风险会上升。
因此,检索调优不是单独追求某一个指标最高,而是在找全和排准之间取得平衡。
#四、生成的两个指标:有没有编,答没答题
生成评估主要看两件事:答案有没有依据,以及有没有真正回答用户的问题。
faithfulness 衡量的是答案对上下文的忠实程度。
做法通常是把答案拆成若干条原子事实,再逐条判断这些事实能不能从检索上下文中得到支持。支持得越多,分数越高。
例如,用户问“退款多久到账”,答案写:
“退款会在 3 个工作日内到账,节假日可能顺延。”
如果上下文只写了“3 个工作日内到账”,那“节假日可能顺延”这句就没有依据,会拉低
faithfulness。
只要答案中的某条陈述无法从给定上下文得到支持,就会拉低忠实度。至于它在现实世界里是否恰好正确,不是这个指标关心的重点。
这个指标不需要参考答案,主要依赖:
answercontexts
它守的是“不编”这条线,不负责判断答案有没有把问题答完整。
answer_relevancy 衡量的是答案和用户问题之间的相关程度。
它关心的是:这段回答有没有真正回应用户问的那件事。
如果答案冗长、偏题,或者回答成了另一个问题,相关性都会下降。
例如,用户问:“退款多久到账?”
回答:“退款申请可以在订单详情页提交。”
这句话可能完全正确,也可能有上下文依据,但它回答的是“怎么申请退款”,不是“多久能到账”,所以
answer_relevancy会比较低。
answer_relevancy 主要依赖:
questionanswer
它评的是“答没答题”,不是事实是否正确。
所以这两个指标不能互相替代:忠实度高的答案可能没用,相关性高的答案也可能没有依据。
一个好的 RAG 回答,既要有依据,也要真正回应问题。
#五、四个指标要组合起来看
四个指标单独看,只能说明某一个方面好不好;放在一起,才能判断问题到底出在检索还是生成。
先看检索阶段:
context_recall低,说明该找回来的内容漏了;context_precision低,说明找回来的内容里无关信息太多,或者相关内容排得太后。
再看生成阶段:
faithfulness低,说明答案里有内容缺少上下文依据;answer_relevancy低,说明答案没有真正回应用户的问题。
实际排查时,可以按下面的顺序看:
- 召回低:先查切分、Embedding、检索方式和 top-k;
- 召回正常但精确度低:说明能找到,但排序或筛选有问题;
- 检索指标正常但忠实度低:资料找对了,是模型生成时加入了上下文之外的内容;
- 忠实度高但相关性低:答案有依据,但没有答到用户真正关心的点,重点检查 Prompt 和问题理解;
- 四项都低:通常不是某一个参数的问题,而是整条链路都需要重新检查。
最需要警惕的是一种情况:答案相关性很高,但忠实度很低。
它看起来很会回答问题,内容也很顺,但依据并不在检索上下文里。这种答案最容易让人误以为系统效果很好。
#六、用 Ragas 执行一次评测
前面四个指标已经拆清楚了,最后把它们放到同一批测试数据上执行。
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import context_precision, context_recall, faithfulness, answer_relevancy
from ragas.llms import LangchainLLMWrapper
from ragas.embeddings import LangchainEmbeddingsWrapper
from models import get_ali_clients
llm, embeddings = get_ali_clients()
data = {
"question": questions,
"answer": answers,
"contexts": contexts,
"ground_truth": ground_truths,
}
result = evaluate(
Dataset.from_dict(data),
llm=LangchainLLMWrapper(llm),
embeddings=LangchainEmbeddingsWrapper(embeddings),
metrics=[
context_precision,
context_recall,
faithfulness,
answer_relevancy
],
raise_exceptions=False,
)
print(result.to_pandas())
两个 Wrapper 的作用,是把 LangChain 的 LLM 和 Embeddings 适配给 Ragas,供需要它们的指标使用。不同指标的计算方式并不完全一样,有的依赖 LLM 判断,有的还会结合 Embedding 或后续计算。
raise_exceptions=False 表示某条评测失败时不终止整批任务,对应结果会记为 NaN,方便后面单独排查。
输出转成 DataFrame 后,每道题是一行,四个指标分别是一列。
真正分析时,先按行看异常样本,再看整体均值。某一行的 context_recall 明显偏低,就回头检查这道题实际检索到了什么;faithfulness 偏低,则检查答案里哪些内容没有上下文依据。
少量样本适合先验证评测流程是否跑通,但不能据此判断系统整体水平。正式评测需要固定一批有代表性的问题,并持续用同一批数据做版本对比。
评测本身也有成本。多个指标可能需要额外的 LLM 或 Embedding 调用,所以不必一开始就跑几百条。先用几十条验证指标和数据集设计,再逐步扩大规模。
#七、总结
- 评测先把“好还是差”变成一个数。 没有基线,有了固定测试集和基线,每次改动才有可比较的结果。
- 四个指标分别定位检索和生成问题。
context_recall看有没有漏,context_precision看相关内容是否排得靠前,faithfulness看回答有没有上下文依据,answer_relevancy看有没有真正回答问题。 - 分数要组合起来看。 低召回、低精确、低忠实度、低相关性对应的是不同问题;只看一个指标,很容易把一种失败误判成另一种。
四个数字出来只是第一步。比如 context_recall = 0.85 到底算不算合格,哪个指标下降时应该优先改哪一层,都需要结合业务场景和历史基线来判断。