LangChain 实践笔记:RAG 评估的四个核心指标

从一次模型调用说起,讲清 RAG 评测为什么要分成检索与生成两段,上下文召回率、上下文精确率、忠实度、答案相关性这四个指标各量什么、需要哪些输入,再用 Ragas 跑通一次评测并读懂输出。

检索链跑通之后,调优会变成一件很容易“凭感觉”的事。换一个 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。

只要答案中的某条陈述无法从给定上下文得到支持,就会拉低忠实度。至于它在现实世界里是否恰好正确,不是这个指标关心的重点。

这个指标不需要参考答案,主要依赖:

  • answer
  • contexts

它守的是“不编”这条线,不负责判断答案有没有把问题答完整。

answer_relevancy 衡量的是答案和用户问题之间的相关程度。

它关心的是:这段回答有没有真正回应用户问的那件事。

如果答案冗长、偏题,或者回答成了另一个问题,相关性都会下降。

例如,用户问:“退款多久到账?”

回答:“退款申请可以在订单详情页提交。”

这句话可能完全正确,也可能有上下文依据,但它回答的是“怎么申请退款”,不是“多久能到账”,所以 answer_relevancy 会比较低。

answer_relevancy 主要依赖:

  • question
  • answer

它评的是“答没答题”,不是事实是否正确。

所以这两个指标不能互相替代:忠实度高的答案可能没用,相关性高的答案也可能没有依据。

一个好的 RAG 回答,既要有依据,也要真正回应问题。

#五、四个指标要组合起来看

四个指标单独看,只能说明某一个方面好不好;放在一起,才能判断问题到底出在检索还是生成。

先看检索阶段:

  • context_recall 低,说明该找回来的内容漏了;
  • context_precision 低,说明找回来的内容里无关信息太多,或者相关内容排得太后。

再看生成阶段:

  • faithfulness 低,说明答案里有内容缺少上下文依据;
  • answer_relevancy 低,说明答案没有真正回应用户的问题。

实际排查时,可以按下面的顺序看:

  1. 召回低:先查切分、Embedding、检索方式和 top-k;
  2. 召回正常但精确度低:说明能找到,但排序或筛选有问题;
  3. 检索指标正常但忠实度低:资料找对了,是模型生成时加入了上下文之外的内容;
  4. 忠实度高但相关性低:答案有依据,但没有答到用户真正关心的点,重点检查 Prompt 和问题理解;
  5. 四项都低:通常不是某一个参数的问题,而是整条链路都需要重新检查。

最需要警惕的是一种情况:答案相关性很高,但忠实度很低。

它看起来很会回答问题,内容也很顺,但依据并不在检索上下文里。这种答案最容易让人误以为系统效果很好。

#六、用 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 调用,所以不必一开始就跑几百条。先用几十条验证指标和数据集设计,再逐步扩大规模。

#七、总结

  1. 评测先把“好还是差”变成一个数。 没有基线,有了固定测试集和基线,每次改动才有可比较的结果。
  2. 四个指标分别定位检索和生成问题。 context_recall 看有没有漏,context_precision 看相关内容是否排得靠前,faithfulness 看回答有没有上下文依据,answer_relevancy 看有没有真正回答问题。
  3. 分数要组合起来看。 低召回、低精确、低忠实度、低相关性对应的是不同问题;只看一个指标,很容易把一种失败误判成另一种。

四个数字出来只是第一步。比如 context_recall = 0.85 到底算不算合格,哪个指标下降时应该优先改哪一层,都需要结合业务场景和历史基线来判断。