RAG 评测应拆开检查检索与生成
最终答案正确,不代表 RAG 检索到了正确证据,也不代表生成内容忠于证据。文章主张分别评估检索与生成,以区分分块、嵌入、排序问题和依据约束、提示词、拒答机制问题。
AI 深度解读
RAG 答对了,不代表它找对了资料,也不代表答案有证据支撑。模型可能根本没用检索结果,只靠训练时记住的知识蒙对;也可能拿到了正确文档,却把其中一个数字或套餐名写错。只统计最终答案的准确率,这两类问题很容易被掩盖。
原文的核心主张是:把「找证据」和「用证据回答」拆开评估。它没有提供新模型或跑分实验,而是给出一套排查框架,帮助做知识库问答、文档助手的程序员判断:效果不好时,到底该改检索,还是改生成。
答案正确,为什么还可能是一次失败
检索增强生成(Retrieval-Augmented Generation,RAG)通常包含两步:检索器从资料库里找出相关文本块,生成器结合问题和这些文本块组织回答。
这两步会形成四种组合:检索正确且回答正确、检索错误但回答正确、检索正确但回答错误,以及两者都错。最终答案准确率会把前两种都算成功,但第二种成功尤其脆弱。
例如,产品文档明确写着:只有 Enterprise 套餐支持单点登录。检索器却拿回另一个产品的定价页,模型凭常识回答 Enterprise。答案碰巧对了,系统却没有完成从指定文档取证的任务。等套餐规则更新,或者问题涉及模型没见过的内部资料,这条路径就可能悄悄失效。
另一种情况正好相反:正确页面已经找到了,模型却回答 Pro 和 Enterprise 都支持。此时继续优化向量检索,解决不了生成器添加无依据内容的问题。
答对只能证明这次结果对了,不能证明系统具备可靠的取证能力。
原文用的是假设案例,没有真实系统的对照实验。它的价值在于说明故障如何被同一个分数掩盖,而不是证明某种优化一定能提升多少效果。
检索要按真正送进模型的结果来评估
检索阶段先看两个问题:证据找全了吗,相关内容排得够靠前吗?
Recall at k 衡量相关文本块有多少进入前 k 个结果。某个问题需要两段资料,前 5 个结果只找回一段,recall at 5 就是 0.5。需要综合多个来源的问题,尤其依赖这个指标。
Precision at k 则看前 k 个结果里有多少真正相关。召回很多内容,可能找到了证据,也可能塞进大量干扰项。平均倒数排名(Mean Reciprocal Rank,MRR)进一步关注第一个相关结果的位置,帮助判断证据是否足够靠前。
这里有个容易混淆的地方:MRR 只看第一个相关结果,不能证明多段证据已经找齐。需要联合阅读多个文档时,它必须和 recall 一起看。
原文还区分了 context precision 和 context recall:前者关注相关文本块是否排在无关内容之前,后者关注检索结果能否覆盖参考答案中的全部主张。原文没有给出这些指标的完整计算公式,落地时需要明确采用的定义。
最实用的提醒是:按生产环境实际使用的 k 统计。
假设正确页面排在第 12 位,检索评估看到 recall at 20 很好,但程序只把前 5 个文本块交给模型。对生成器而言,证据仍然不存在。此时可以改善排序、增加 k,或者压缩文本块,让更多证据进入上下文。
增加 k 也有代价:费用、延迟会上升,干扰内容还可能影响回答。因此,作者建议先检查分块,再检查 embeddings,最后检查排序。这是一条排查经验,不是适用于所有系统的固定优化顺序。
生成质量不能只问「看起来对不对」
生成阶段需要同时看三个维度。
忠实度(faithfulness,也称 groundedness)检查回答里的每条主张,是否得到检索上下文支持。可操作的做法是把答案拆成原子主张,再逐条核验。前面的套餐例子中,「Enterprise 支持单点登录」和「Pro 支持单点登录」应分开判断,避免一段大体正确的回答掩盖错误细节。
相关性(answer relevancy)检查回答是否直接回应用户的问题。模型忠实地概括了一整页文档,也可能始终没有回答用户问的那个条件。
完整性(completeness)则检查:上下文能够支持、参考答案要求覆盖的关键点,有没有遗漏。只回答一句正确的话,不能自动算作好答案。
原文给出一个最小评估程序,适合从 50 到 100 个已标注问题开始。它记录每道题的召回率和忠实度,方便逐条检查:
def evaluate_rag(questions, rag_system):
rows = []
for q in questions:
result = rag_system.answer(q.text)
retrieved_ids = [c.id for c in result.chunks]
relevant = set(q.relevant_chunk_ids)
recall_at_5 = len(relevant & set(retrieved_ids[:5])) / max(len(relevant), 1)
claims = split_into_claims(result.answer)
supported = sum(1 for claim in claims if is_supported(claim, result.chunks))
rows.append({
"question": q.text,
"recall_at_5": recall_at_5,
"faithfulness": supported / max(len(claims), 1),
"answer": result.answer,
})
return rows
要重点看 is_supported(claim, result.chunks):这里传入的必须是生成器实际看到的上下文。否则评估器可能用模型没见过的证据,替回答证明正确。split_into_claims 和 is_supported 也只是占位函数,原文没有提供实现。
这段程序还没有覆盖全部评估要求。它没有统计相关性、完整性、拒答率和延迟;没有相关块或没有主张时,分母保护只能避免除零,不能代替业务评分规则。
真正费力的是样本和标注。作者建议加入两类题:资料库完全没有答案的问题,用来检查是否合理拒答;文档事实已经更新、与模型旧知识冲突的问题,用来检查模型究竟听谁的。
我的判断
我认同把检索和生成拆开的方向。对文档助手来说,最终答案准确率适合做快速检查,却不足以指导修复。保留问题、检索结果、实际上下文和回答之间的对应关系,比单独展示一个平均分更有排查价值。
但拆开指标,也不代表指标天然可靠。
首先,相关文本块的标注带有判断。文档可能有多个等价证据,分块调整后,旧的相关块 ID 也可能失效。其次,逐条主张都成立,整段回答仍可能因为遗漏条件或组合方式而误导用户。人工阅读仍然必要。
LLM-as-judge 可以降低评估成本,但作者没有给出裁判模型与人工判断的一致率,也没有提供偏差大小的实测数据。偏爱长答案、流畅表达等风险,应通过人工标注子集校准;两个系统分数只差一点时,不宜直接宣布胜负。
相比只做最终答案匹配,组件评估能提供更多定位线索;相比完全人工验收,它更容易重复执行,但需要持续维护标注和裁判规则。
这些指标适合共同解释失败,不适合再被压成一个万能总分。
原文关于长上下文干扰、分块优先级等建议也应视为经验判断。文章没有给出统一的对照数据,具体团队仍要结合自己的语料、模型和问题分布验证。
能拿来做什么
- 给文档助手建立一个 50 到 100 题的初始评估集,混合真实用户问题与合成问题,并标注参考答案和相关文本块。真实问题应先匿名化,保留口语、歧义和多条件表达。
- 在 Node 编排层记录检索排名,以及最终交给生成器的文本块 ID。生产只传前 5 块,就优先报告 recall at 5 和 precision at 5,同时记录费用与延迟。
- 把套餐规则更新、文档没有答案这两类问题加入回归集,分别检查答案是否服从最新文档,以及系统是否能够合理拒答。
- 每次修改分块、embeddings、排序或提示词后,比较各项指标并抽查失败样本。出现召回提高、忠实度下降时,检查新增上下文是否引入干扰,不要只看平均分是否上涨。