做 RAG 不评测等于闭着眼睛调参一份从原理到落地的完整测评指南我见过太多做 RAG检索增强生成的同学上线前说得头头是道一问到“怎么证明你的系统比上个版本好”立刻支支吾吾。RAG 项目最坑的一点在于任何一个环节出问题——文档切得不合理、向量检索召回了一堆噪音、大模型没忠实于上下文——最终表现都是“回答不好”。可到底哪里不好、差多少、换参数有没有变好没有评测手段你只能靠感觉调 embedding、调 chunk、调 prompt本质上是闭着眼开车。这篇文章我打算把 RAG 评测这件事彻底拆开。要先讲清楚评测到底在评什么、为什么这些指标能帮你定位问题再给出一份可以直接落地的评测数据集构建方法、工具选型思路和完整代码流程最后把我自己踩过的几个坑和排查经验一并交代。无论是刚入门想把 RAG 做好、还是已经在线上被“效果不稳定”折磨的团队这篇都值得看完照着抄。1. 为什么我说 RAG 不评测等于闭眼调参1.1 三个最常见的尴尬场景前阵子有个做知识库问答的团队找我说他们的内部助手最近换了新模型结果“感觉不如以前好用”。我问具体哪里不好说法是“有的回答变长了有的回答开始编了”。这暴露了三个非常典型的问题第一他们没有一个固定的评测集所有判断都来自开发团队轮流提问感觉自然飘忽不定第二他们分不清变化是检索变差还是生成变差第三他们没有任何指标能量化“回答变长了”是好事还是坏事——有时候多给几句背景是好的有时候就是在注水。这类场景在 RAG 项目里太常见了。很多人埋头调参数调了一星期最后发现评测集根本没有等于在一条没有刻度线的赛道上跑步跑得快不快全靠裁判喊。更致命的是RAG 系统本身是“检索模块 生成模块”串起来的两个模块的改动会互相干扰。你换了 embedding 模型召回结果变了大模型拿到的上下文变了最终答案自然变但你无法判断到底是召回变准带来了提升还是召回变杂引发了幻觉。没有评测一切优化都是玄学。1.2 评测的本质是把“好”变成可测量的量我理解评测这件事本质上干三件事把“回答好不好”拆解成几个可以独立观察的性质给每个性质定一个打分标准再用一组固定的输入去反复检验。只有做到这三件事你才能回答“这次改动到底有没有用”这个工程问题。RAG 系统的特殊性在于它是两段式结构。检索阶段解决“有没有找对资料”生成阶段解决“有没有用好资料”。所以评测绝不能只盯着最终答案看而要把链条拆开来看。这也是为什么业界普遍把指标分成“检索质量”和“生成质量”两组。一组负责量化“资料找得准不准”一组负责量化“答案写得对不对”最后还要看两组指标联动下的端到端效果。说得更直白一点评测不是做完系统之后补个报告而是开发过程中用来做决策的依据。哪个模块出问题就往哪个方向修。下面这节我把 RAG 指标体系的原理讲透。2. 原理拆解RAG 系统到底该测哪些指标2.1 检索质量地基不牢上面全白搭检索质量是 RAG 系统的地基。就算大模型再聪明你喂给它的上下文全是无关文档它也变不出准确回答。所以第一件事是把“检索好不好”量化。最常用的两个指标是上下文相关性和上下文召回率。上下文相关性Context Relevance衡量的是检索回来的这些 chunk跟用户的原始问题到底有多相关。它检查的不是“答案对不对”而是“资料相不相干”。试想用户问“今年的营收是多少”检索系统却召回了一大堆“公司组织架构介绍”的内容虽然这些内容也来自同一份文档但对回答当前问题毫无帮助。上下文相关性就会给低分。我通常会让一个评测模型去看“问题 检索回来的文本”然后给一个 1 到 5 的分数低于 3 分基本可以认定召回环节有噪音。上下文召回率Context Recall更进一步它衡量的是在所有“本应该被召回的证据片段”里你的检索系统实际召回了多少。打个比方标准答案引用了文档第 3 章和第 5 章的内容但你的向量检索只把第 3 章捞回来了第 5 章没捞回来那召回率就是 50%。这个指标非常依赖标注因为你需要预先知道每个问题对应的“黄金证据片段”是什么。没有这个标注你只能在相关性上打转但永远不知道检索到底漏了什么。这块工作我在 4.1 节会详细讲怎么做。2.2 生成质量忠实性和答案相关性缺一不可检索做得再漂亮最终用户看到的还是大模型生成的文字。生成质量的评测里我最看重的两个指标是忠实性和答案相关性。忠实性Faithfulness解决的是“有没有编”的问题。它的逻辑是把生成的答案拆成若干个独立句子逐一跟检索回来的上下文比对判断这句话里的所有事实是否都能从上下文中找到依据。能则这句话忠实不能哪怕只编了一个数字、一个日期整个句子就算不忠实。然后算“忠实句子数 / 总句子数”作为分数。为什么这个指标要做得这么细因为在 RAG 场景下幻觉往往不是整段凭空捏造而是“九句真话里夹一句编的”。用户答完问卷说“感觉有点不对”你根本不知道哪句不对。忠实性指标像探针一样把每一句话拿到证据面前核验幻觉率立刻现形。我甚至会单独统计“不忠实句子出现的频率”作为幻觉率报告比只给一个综合分直观得多。答案相关性Answer Relevance解决的是“有没有答非所问”的问题。它不关心答案是否全对只关心答案是否针对用户的问题。这里有个经典坑一个看起来特别长、特别完整的回答可能只是在自说自话压根没回应提问里的关键限定词。比如用户问“哪些产品支持离线模式”模型洋洋洒洒介绍了一遍全部产品线——信息是真实的但没回答到点子上。这个指标就是在抓这种问题。2.3 为什么必须把端到端指标也纳入进来把检索指标和生成指标分开测是为了定位问题。但在实际版本迭代中你也需要一个“最终效果分”来做整体对比——这就是端到端评分。端到端评分通常是用评测模型直接给“问题 标准答案 系统回答”做一个整体质量打分涵盖正确性、完整性、条理性等多个维度。注意端到端评分不能替代前面那些细粒度指标否则你又回到了“知道不好但不知道哪里不好”的状态。正确用法是先用细粒度指标定位环节再用端到端分数做回归对比。我在实际项目里会把指标分成三个层次第一层是底层模块指标包括上下文相关性、上下文召回率第二层是生成侧指标包括忠实性、答案相关性第三层是端到端综合分。每一层都有独立用途。底层的坏了修底层中层的坏了调 prompt整体分掉了再用分层指标追因。这个体系在我带过的多个 RAG 项目里都验证过是排查问题速度最快的一套结构。3. 评测集和评测工具从哪来、怎么选3.1 评测集不能只靠开源数据必须自建最小集评测指标聊聊容易真正难的是评测集。我看到很多新手直接拉一个开源问答数据集跑分结果分数很好看一上自己的业务数据就露馅。原因很简单RAG 是强业务绑定的系统通用数据集测不出你的知识库切片风格、你的文档结构和你的 query 分布。稳妥的做法是“开源数据集暖场 自建评测集把关”。暖场阶段用公开的百科类问答对验证代码链路通不通、指标有没有算反但上线和调参的决策最终必须依赖自建评测集。自建评测集起步不要贪多50 条先跑起来后续再扩到 200 到 500 条。关键是结构要完整。每条数据至少包含三要素用户问题、标准答案、以及标准答案所依赖的证据片段可以是一个或多个 chunk 的 ID 或者原文引用。有了“证据片段”才能算回声召回率没有这层标注你只能测相关性测不了召回。关于这一点我用了一个很笨但很稳的办法先在文档库里检索出每个问题的 Top-20 候选 chunk然后人工从中挑出真正构成标准答案依据的若干条存成“黄金证据”。人工挑确实累但这一步的质量决定了后面所有评测分数的可信度。3.2 评测框架怎么选先看清楚是“调一次”还是“盯长期”RAG 评测框架的选择我总结下来就三种路线。第一种是纯手工的在线评测把几组问题丢给系统人工看答案打分。零成本、最灵活但不可重复、费人力适合项目刚开始摸索方向时用。第二种是调用大模型 API 做自动化评测自己写一个评测脚本把各项指标提示词封装好批量跑。我推荐初期用这一条它兼顾落地速度和可控性。所谓“用大模型评大模型”是用一个评分模型通常选和主系统不同的模型避免自评偏差去扮演评判员。你需要非常重视评分模型的 prompt 稳定性和输出格式让打分尽量可复现。第三种是基于专业评测工具的半自动方式业界已经有不少开源评估套件实现了上下文相关性和忠实性等指标的自动化计算你只需要准备数据集、配置评测模型、跑出来的报告会自动分维度汇总。我建议在团队需要长期做回归、每次迭代都要出一份几十项指标报告的场景下直接上这类工具。三种路线不是互斥的实际项目里我通常先用第二种搭起基线再逐渐往第三种迁移纯人工只用来抽查。3.3 评测成本要提前算明白自动化评测不是免费的。每次打分调用大模型都要消耗 token。我来算一笔账假设你有 300 条评测数据每条数据要评上下文相关性、忠实性、答案相关性三个指标每个指标至少调用一次模型。那就是 900 次调用端到端综合分再来 300 次共 1200 次调用。如果每条数据的输入输出加起来有 2000 token一个月跑 5 轮评测那就是一千多万 token。这个量级放到按量计费的大模型服务上成本不低放自部署开源模型上则要占不少算力。所以我的建议是评测模型和生成模型分开评测端优先选便宜、速度快、指令遵循稳定的模型同时把评测集按重要性分成“核心集”和“扩展集”全量回归只跑核心集比如 80 条改动面大时再扩到全量。成本控制要在设计评测流程时就规划好上线后再压缩往往很被动。4. 落地实操一份可以直接照抄的评测流程4.1 第一步搭一个最小可用的评测集我以一个内部文档问答场景为例说明可行的操作方式。先收集真实用户 query没有真实数据的情况下也可以让业务同事按他们平时会问的方式写 50 条。每一条 query 后面人工写好标准答案标准答案最好写成通顺的段落不要只写关键词因为后面评测答案相关性时需要一个完整答案作参照。然后做“证据标注”。用你当前的检索系统把每一条 query 的 Top-20 结果导出人工从这些结果里圈定哪几条是标准答案真正依赖的内容存为证据片段列表。做好以后数据集长这样{ question: 我们公司的年假政策中司龄满三年后年假天数如何变化, reference_answer: 司龄满三年后年假天数从 10 天提升至 15 天。, reference_contexts: [ doc_201_paragraph_4, doc_201_paragraph_5 ] }如果你觉得 50 条全人工太累可以把前 20 条做成种子集后续的 30 条通过“模型生成候选答案 人工修正”的半自动方式快速补齐但证据标注必须人工核对。这个环节偷懒后面所有召回率的数字都会失真。4.2 第二步把评测流程写成脚本接下来是核心工程步骤。我把自己常用的评测框架流程贴出来代码做了简化但逻辑是完整的核心包含四段加载评测集、跑检索、跑生成、跑指标打分。import json import random def load_benchmark(path): with open(path, r, encodingutf-8) as f: return json.load(f) def retrieve(question, top_k10): # 这是你自己的检索逻辑返回 chunk 文本列表 # 示例先用向量召回再走一层重排 chunks vector_search(question, top_ktop_k) return [c[text] for c in chunks] def generate_answer(question, contexts): # 这是你自己的生成逻辑把检索到的上下文拼进 prompt context_block \n\n.join(contexts) prompt f请基于以下资料回答问题\n\n{context_block}\n\n问题{question} return call_llm(prompt) def score_context_relevance(question, contexts): batch_text \n\n.join(contexts) prompt f你是一个评测员。请判断下面检索回来的资料与用户问题的相关程度。 用户问题{question} 检索资料 {batch_text} 请打分1 到 5 分只输出数字。 return int(call_llm(prompt)) def score_faithfulness(question, answer, contexts): context_block \n\n.join(contexts) prompt f你是一个评测员。请把下面的回答拆成句子逐句判断每个句子是否被资料支持。 回答{answer} 资料 {context_block} 请按以下格式输出 支持句数x 总句数y 不支持的句子... result call_llm(prompt) # 从结果里解析 x 和 y return parse_faithfulness(result) def score_answer_relevance(question, answer, reference_answer): prompt f你是一个评测员。请判断回答是否回应了用户的问题可参考标准答案。 用户问题{question} 标准答案{reference_answer} 系统回答{answer} 请打分1 到 5 分只输出数字。 return int(call_llm(prompt))评测主流程如下def run_evaluation(benchmark_path): data load_benchmark(benchmark_path) results [] for item in data: question item[question] contexts retrieve(question, top_k10) answer generate_answer(question, contexts) metrics { context_relevance: score_context_relevance(question, contexts), faithfulness: score_faithfulness(question, answer, contexts), answer_relevance: score_answer_relevance( question, answer, item[reference_answer] ), } metrics[answer] answer metrics[question] question results.append(metrics) print(平均上下文相关性:, round(avg([m[context_relevance] for m in results]), 2)) print(平均忠实性:, round(avg([m[faithfulness] for m in results]), 2)) print(平均答案相关性:, round(avg([m[answer_relevance] for m in results]), 2)) def avg(nums): return sum(nums) / len(nums)这段脚本跑完后你就有了三组核心指标的平均分和每条样本的分值明细。注意score_context_relevance 和 score_faithfulness 的 prompt 很长输出解析要做得稳。我自己早期吃过亏模型偶尔在数字前后多加解释文字导致 int() 直接报错。后