RAGRetrieval-Augmented Generation检索增强生成系统有一个特别迷惑人的特性它几乎永远“看起来不错”。我自己第一次搭 RAG是在二十几份产品文档上随便问了几个业务问题输出顺畅、条理清晰当时心里只剩四个字成了稳了。但后来把范围扩大到几百份混合来源的 PDF、表格截图、历史邮件再把真实用户的原始提问原封不动喂进去同一套系统的正确率直接掉到地板上。前后对比让我清楚地认识到一件事RAG 真正难的不是搭起来而是你得先有一套方法识别出系统什么时候只是在“表演正确”。每个 RAG 项目都应该有自己的 baseline基线。这篇文章聊的是我如何在实际项目里给自己的 RAG 建基线——拆解检索与生成两个环节、沉淀自己的测试集、计算量化指标、按步骤迭代以及最后那些“指标全绿但系统仍然翻车”的典型场景。如果你正准备上手 RAG或者已经被“效果不错”麻痹了好几个月这篇文章应该能帮你少走半年的弯路。1. “看起来不错”这个错觉是怎么害了 RAG 项目的1.1 语言模型太会“说人话”把检索错误全盖住了RAG 的输出不是搜索引擎那种“相关链接列表”而是一整段逻辑通顺的人话。正是这种“整话输出”让用户在判断好坏时天然被带偏。你问“我们这个季度的对账周期怎么算”检索环节错误地召回了一份关于“退款周期”的文档生成端照样能借大模型的语言能力把内容组织得像模像样“根据最新财务文档本季度对账周期为每月 5 号至 10 号……”因为句子读起来通顺、格式工整一般人很难当场察觉这段话其实建立在错误依据上。大模型的语言能力本质上是一个“流利度滤镜”。它不会因为检索错了就结巴反而会非常有信心地补全所有细节。我给一些团队做内部评估时经常出现一种现象问句越长、越绕业务方第一反应越是“答得不错”但只要把系统实际召回的原文片段展示在旁边就能看到支撑依据和答案之间根本没对应关系。这是“看起来不错”的第一个来源生成端太会说话把检索端的错误全盖住了。所以凡是用“通读一遍觉得顺不顺”来验收 RAG 的做法都会得到严重偏高的评价。真正做基线评估就要完全摒弃“凭感觉打分”的思路转而寻找基于证据链的判定方法。这一点我会在第 4 节详细展开。1.2 人工抽测的样本偏差第二个错觉来源是人工抽测时的“样本偏差”。几乎所有 RAG 项目都经历过这种流程搭好系统开发者从文档里挑十个二十个问题跑一遍记录结果宣布完成。但开发者挑选的问题往往是文档里信息最清晰、表述最完整、答案所在位置最显眼的那部分比如“某某功能的开关在哪里”。这类问题天然是易召回的它们测不出系统的真实检索水平。真实用户的问题完全不是这个样子。真实提问里充满了简写、错别字、口语化表达和隐含前提比如“上回那个制度什么时候调”“分公司的结算是不是也延了”。这些问题的难度和精心挑选的代表性问题不在一个量级。人工抽查如果只覆盖自己熟悉的“好问题”等于把验收结果建立在一组最容易通过的数据切片上。我给这种测试起了外号叫“亮点巡检”只看做得好的地方做错的地方自动跳过甚至无意识地遗忘。亮点巡检的最大作用不是评估而是自我安慰。想克服就必须把问题来源从“自己脑子里想出来的问题”换成“真实业务里沉淀下来的问题”这一点我会在第 3 节展开。1.3 Demo 环境的“假阳性”来自问题重复RAG 项目初期最常用的验证方式是什么打开一个演示界面反复问相似的问题。这种做法的麻烦在于你每跑一遍前几轮的对话历史都会留在上下文里。当问法接近时系统实际上已经不再是“纯 RAG”而是在做“基于历史上下文的对话续写”。演示环境中反复问相同或相似的问题得到越来越好的反馈其实和你上一个 Version 的检索质量没有任何关系。另一个隐藏因素是Demo 测试通常只停留在“这一轮回答让我眼前一亮”的瞬时记忆上很少系统化记录失败案例。十个问题回答对七个注意力全在正确的七个上错的那三个很快就忘了。人脑对失败样本的遗忘速度远快于对成功样本的遗忘速度。我后来强制团队用表格、评分卡和案例库来做跟踪核心原因就在这里——只有用文档记录你才不会被感觉欺骗。2. 建立基线的第一步把 RAG 拆成检索与生成两套漏斗给 RAG 建基线首先要纠正一个思维定式不要把 RAG 当成一个“输入问题、输出答案”的黑盒而要把它看成两个前后串联的漏斗。前一个漏斗负责从文档库中捞出相关片段叫检索漏斗后一个漏斗负责把捞出的片段组织成自然语言答案叫生成漏斗。两个漏斗各有不同的故障模式基线要对它们分别测量。2.1 检索漏斗的三种典型故障检索漏斗的职责是“把正确的信息找出来并且放在合适的位置上”。它的故障通常有三种。第一召回缺失。正确信息存在于文档库中但检索过程根本没把它捞出来。原因往往是 Query 和文档的表述相差过大。用户问“结账流程”文档里写“对账操作”如果 embedding 模型无法建立语义联系就会漏召。召回缺失时生成端拿到的是完全不相关的片段答案自然全错。第二排序不当。正确信息被捞出来了但它排在第 8 位、第 15 位当你只取 TopK比如 Top5时它被截断在结果之外。业务文档里这种问题非常普遍一段话先铺背景后给结论embedding 对整段编码后背景信息拉高了整体相似度结论反而不排在最前面最后正确片段被截走。第三切片破坏。文档被切分成固定大小的 chunk 时跨页的技术参数表、连续操作流程可能被切成两半。上一片里有“操作前提”下一片里才有“操作步骤”两半隔着好几个 chunk检索很难同时召回到位。切片破坏看着不起眼但它最能制造“答案似乎沾边、实则残缺”的状态。2.2 生成漏斗的三种典型故障生成漏斗的职责是“在给定上下文的前提下输出一个合理的回应”。它的故障同样有三类。第一类是幻觉。模型没有根据检索上下文来写而是靠自己的训练记忆补全。典型场景是检索结果信息太短而提示词要求“请详细回答”模型就开始发挥发挥出来的部分就成了幻觉。这类幻觉有个特点句子读起来专业但每一个核心论据都找不到出处。第二类是过度归纳。检索到的几个段落里有多个分散事实比如三份文档分别提到“部分用户反映结算慢”“某些分支机构的结算周期较长”模型把这些局部描述叠加成“所有用户结算体验均受影响”。这和幻觉不同——素材确实都在上下文里问题出在模型进行了过大的归纳跳跃。第三类是忽略冲突信息。同一个问题上召回的两份文档说法不一致模型不判断来源也不指出冲突而是把两种说法“平滑合并”成一个两可的表述。这种平滑合并很难被自动指标抓出来因为句子的每一半都能各自在原文里找到出处。2.3 分段评估的价值让你知道该修哪一端如果一个 RAG 系统整体分数偏低你不分段就永远不知道问题出在检索还是生成。我经历过的典型调试过程是这样的拿 10 道困难问题去测生成结果 5 对 5 错。当时我满脑子想的都是“提示词没写好”于是反复调提示词一点用都没有。后来把检索结果单独打印出来看发现 10 道题里正确文档只有 2 道被召回到 TopK——生成端根本“够不着”正确答案调提示词当然没用。反过来也有另一种情况检索召回率很高正确片段明明排在前面但生成结果还是错。这说明问题在生成端——模型可能处于自由发挥模式或者上下文里塞了太多互相干扰的片段又或者提示词根本没有给出“只能基于文档回答”的约束。分段评估最大的价值是让你不至于把“检索问题”当成“生成问题”来修少做大量无用功。我自己养成的习惯是每次评估先看检索指标再看生成指标两者分开记录做版本改动时也严格分开对比绝不混在一个总分里看。3. 用自己的业务数据做基准集别拿公开榜单当真3.1 公开基准集的“语义整洁”无法模拟业务数据很多 RAG 项目一开始喜欢拿公开数据集练手比如 Wiki-QA、Natural Questions 这类。这些数据集的共同点是文本段落结构完整、实体命名规范、问题大多直接命中段落主旨。在这样的数据上做优化分数涨得飞快特别有成就感。但等你把同样一套参数迁移到真实业务数据上往往会发现性能掉得没法看。业务数据是另一种生态文档里充满简称和术语表格字段被拆得零碎同一件事在两年不同版本的文档里说法不一样问题本身还可能涉及上个月刚发生的事件。公开基准集的“语义整洁”恰好掩盖了这些难点。所以从你真正要服务的文档集里至少抽出几十条问题来建立自己的 mini-testset比调任何花哨模型都重要。如果连这几十条都懒得抽那后面所有指标都只能叫“看起来不错”。3.2 怎么从真实业务里沉淀问答对方法不复杂核心是三个来源。历史客服工单或用户提问日志。做过内部问答的人都懂用户的真实提问是比任何测试集都珍贵的素材因为它就是“真实分布”本身。论坛、IM 群聊、邮件里的高频提问。企业内部高频问题集中在报销、休假规则、操作流程、审批节点这些同样值得沉淀。自己按业务链路补充编写。比如“采购下单后多久能走到审批”这类问题虽然是编写出来的但覆盖真实业务链条也有价值。拿到问题以后还差标准答案。这一步不能省标准答案尽量直接引用文档原文并附上原文所在段落再让熟悉业务的人审核。如果一个问题的标准答案你自己都说不清那它就不应该进测试集——硬放进去只会让评估结果变得模糊让任何指标都解释不清。3.3 按难易程度分级简单、中等、困难为了让基线提供有价值的信息强烈建议把测试题分成三档。简单题信息集中在某一篇文档的某个段落里且问题中的关键词与原文高度一致。这类题用于验证检索基础是否正常。中等题需要把分布在两三个段落或两篇文档里的信息拼接起来或者需要先理解问题的隐含含义再定位文档。测试的是跨片段整合能力。困难题包含干扰信息需要排除错误的文档需要复合推理甚至两篇文档里给出的说法相互矛盾要求模型做出判断。这才是真实业务里的疑难场景。分档之后评估报告才真正有解释力简单题都过不了大概率是基础配置embedding 选型、chunk 大小出了问题简单题全过但中等题惨不忍睹多半是检索召回范围不够或重排不理想前面都行但困难题大败通常是生成端在逻辑判断上的能力不足或提示词缺少约束。3.4 测试集的数量选定与日常维护数量不必追求大而全。我的经验是20 条可用50 条相对能说明分布200 条已经足够支撑一个团队级别的稳定回归基线。对绝大部分项目来说50 条数据配合难易分级已经能满足迭代日常。维护纪律同样重要。每一条错题后来被修复了要继续留在测试集里防止回归每融入一批新文档要从新文档中补充对应的问题每季度做一次去重防止某一类问题占比过重把指标带偏。这些看着枯燥的日常维护恰恰是评估质量最核心的部分。4. 量化指标分别在检索侧和生成侧怎么算数4.1 检索侧指标从 RecallK 到 MRR聊几个最常用的检索指标不搞公式崇拜只说明白怎么算。假设一个问题的标准答案藏在两段文档里系统召回 10 条片段正确片段排在第 3 位和第 6 位。当 K5 时第 3 位的正确片段被记入第 6 位的被丢掉。此时 Recall5 正确召回数(1) / 总正确数(2) 0.5。Precision5 前 5 条里相关条数(1) / 5 0.2。K 设得越高Recall 通常越高但把过量不相关片段塞进上下文生成质量反而会下降。K 值的选择必须结合生成侧分数通盘考量不是一个固定值通吃。MRR平均倒数排名衡量的是“第一个正确答案到底排在哪”。某题正确文档排在第 3 位则这题的 MRR 贡献是 1/3 约等于 0.33排在第 1 位贡献就是 1。MRR 对“正确答案是否被放在最前面”特别敏感适合只有一个标准来源的问答场景。NDCG 更复杂会把相关性的不同程度非常相关、部分相关、完全无关和排序位置综合加权。但小团队日常没必要把 NDCG 神话化先用 Recall5 和 MRR 两个指标就能暴露大量检索问题。真正到了精细化调参阶段再引入 NDCG 也不迟。4.2 记录检索结果的实操每道题留快照光有指标不行还需要快照。我在每个题目下都会保留“检索快照”记录命中的前 5 个片段索引、来源文档名、所在页码或切片编号、相关度得分。改版本时这个习惯特别管用改完 chunk 大小翻一翻检索快照立刻能看出某个原本排在前面的片段是不是被甩到后面了也能看出原本不相干的干扰片段是不是因为参数变化混了进来。指标会骗人快照不会。4.3 生成侧评估自动指标、LLM 判分与人工复核生成侧量化要难得多因为自然语言和正确信息之间不是精确匹配。我见过团队死磕 ROUGE/BLEU 分数但 ROUGE 统计的是字面重叠对同义改写毫无办法业务问答里同一个问题可能有十种合法的标准答案表述ROUGE 无法区分。所以从开始就要接受生成侧不能只靠单个自动指标收工。实用路线是“LLM-as-Judge”搭配人工抽样复核。具体做法是让一个性能较好的大模型对系统回答和标准答案逐项打分评估维度通常包括正确性回答的核心信息与标准答案是否一致完整性标准答案里的关键信息点有没有漏忠实性回答中的关键论断能否在检索上下文里找到依据简洁性有没有堆砌大量冗余信息。LLM 打分的问题在于它自己也会被“流畅文本”欺骗所以需要给它明确的依据校验机制。我会要求 LLM 打分时先声明自己判断依据来自检索上下文中的哪句话如果找不到依据就不能给高分。这个机制能把 LLM 打分的幻觉压制掉一大半。人工复核不需要全量。每轮抽样 8-10 题多人独立打分有分歧就拉出来讨论。这些分歧本身是很有价值的反馈——要么标准答案标注得不好要么问题本身有歧义需要重新校准测试集。4.4 评分卡模板把两组指标放到一张表题号难度检索 Recall5MRR生成正确性生成忠实性备注Q01简单1.01.04/55/5无Q02中等0.50.332/53/5需要拼接 2 与 5 段Q03困难0.00.01/52/5召回被干扰片段占据评分卡的威力在于一眼可读哪道题检索挂了哪道题生成挂了哪道题是召回排序不理想。每次调整完用同一张卡直接对比新旧分数。凡是无法在单题层面解释的分数变化我都不会轻易相信。5. 实际跑基线从朴素 TopK 到结构化检索的三步路线5.1 第一步朴素向量检索 TopK先跑一个“保底分数”所谓基线首先得有一个不用高级技巧的原始得分作参照。朴素 RAG 的构成并不复杂把文档切成 chunk比如每 512 个字符、重叠 128 个字符用文本嵌入模型把每块编码成向量用户提问时把 query 编码成向量检索 TopK 条片段把这些片段拼进 prompt让 LLM 回答。这一步不重排、不做 query 改写、不搞多路召回纯粹看最简单做法能得多少分。下面是简化的示意代码用来固定基线结构# 简化版朴素 RAG 基线结构 def chunk_docs(docs, size512, overlap128): chunks [] for doc in docs: for i in range(0, len(doc), size - overlap): chunks.append(doc[i : i size]) return chunks def build_index(docs): index [] for i, chunk in enumerate(chunk_docs(docs)): vec embed_model.encode(chunk) # 文本 - 向量 index.append((i, chunk, vec)) return index def retrieve(query, index, top_k5): qvec embed_model.encode(query) scored [(cosine_similarity(qvec, vec), i, chunk) for i, chunk, vec in index] scored.sort(reverseTrue) return [(i, chunk) for _, i, chunk in scored[:top_k]] def answer(query, index, model): hits retrieve(query, index) context \n.join(c for _, c in hits) prompt f请只根据以下资料回答问题不要自行补充\n{context}\n问题{query}\n答案 return model.generate(prompt)跑通后把评分卡里的数字填进去这就是你的 baseline。后续所有优化本质上都是在和这张卡片、这套评估流程做对比。要提醒一个坑ANN 检索参数会直接影响基线第一版建议使用比较保守的高精度模式等后续再逐步放松否则你会把检索质量的问题怪到错误的地方。5.2 第二步分块策略、查询改写与混合检索基线出来之后再动刀。分块策略是大多数项目提升空间最大的一步。512 字符的 chunk 往往太粗一个段落里可能同时包含多个主题检索噪声明显切成 128-256 字符又可能切断关键上下文。没有放之四海而皆准的答案。我的做法是准备三四种 chunk 策略固定宽度 256、按标题切分、句子级拼接、语义段落切分在自制测试集上跑一遍看哪种切法在哪类难度上表现更好。这里只信业务数据上的实测差异。查询改写主要解决用户问题太口语化、太短的问题。比如“这个办法要改吗”转换成向量时信息量严重不足。用一个带轻量提示词的 LLM把问题改写成适合检索的句式“这个管理办法的更新是否需要重新走审批流程”。改写后的 query 往往让召回率立竿见影地提升。混合检索指的是向量检索之外再叠加关键词检索比如 BM25然后把两路结果合并。业务文档里精确术语很关键“设备编码”“合同编号”这类检索BM25 更容易命中精确字面向量检索更适合处理语义改写。两路合并后总召回覆盖度一般会有几个百分点的提升。这三样改动建议分开做每做一步重新跑一次评分卡。5.3 第三步重排与多路召回的结构化方案重排是第二步到第三步之间最该上的方案。初次召回时可以放宽到 Top20 甚至 Top30再用一个专业的 cross-encoder 重排模型把候选片段重新排序最终取 Top5。我经历过的典型升级中测试集中等题正确率从约 40% 提升到约 72%主要原因就是重排上线。原来那些“正确信息排第 8 名”的情况重排后基本都能被捞回前列。重排也有代价额外延迟、额外服务的维护成本而且重排模型并不是越贵越好。它衡量的是“句子配对”的相关性对业务术语的识别未必比简单规则更强。所以重排效果必须回到评测集上验证不能因为“听说别人用了重排效果好”就无脑加。多路召回则是在混合检索之上再叠加因果检索、父子 chunk 递归检索等做法核心思路始终一样让更多“可能正确”的片段进入候选集再由重排压缩到高质量片段。5.4 改一个变量跑一遍整个测试集纪律胜过技巧这一步没有太多技术含量关键是纪律。常见的翻车方式是改一个 chunk 大小让用户体验现场“感觉好了一点点”就宣布优化成功。这样做的结果是把所有问题都混在一起完全无法归因。正确的做法是保持测试集不变一次只改一个变量跑完整评分卡留下前后对比记录。你会经常发现某个变量单独改时分数略降两个变量一起改时总分反而上升。这种综合效应只有通过步步为营的版本记录才能看清。我会把每次改动的代码片段、模型版本、测试集得分、检索快照差异记在一个 Markdown 文件里。版本记录不需要多正式但没有它优化方向很容易被“感觉”带偏。6. 基线全绿但系统仍然翻车的四类典型事故6.1 时间断层知识会过时我做过一个内部知识库问答测试集跑起来分数稳定在 90% 以上但上线第一周就收到大量用户反馈“你们答案用的还是旧政策”。原因非常简单测试时文档库里都是旧版本用户提问时新政策已经生效而文档库还没更新。基线只负责检验“在指定文档库版本下是否正确”无法自动感知版本的时效性。这类时间断层问题后来我把“文档发布时间”纳入了检索的特征并对“用户提问中带时间/版本”的问题做了专项测试条目。这个案例的教训是RAG 的每个答案都有时间维度基线里必须加一道“新政策生效后旧文档仍存在怎么办”的题目。6.2 两处文档说法不一致时模型选了更顺口的那句业务系统经常存在新旧制度并行期。A 文档写“本流程只适用于总部员工”B 文档写“总部及分公司员工均适用”。检索通常把两段都抓回来了。生成端面对冲突时如果提示词没有做强约束往往倾向于选择语句更完整、表达更顺畅的那段而不一定更权威的那段。后来我在提示词里显式加入了一条“如果检索到的资料存在冲突请在回答中并列列出不同来源的说法并注明各自的文档名和版本”。这个方法让冲突类问题的回答从“50% 正确但无法解释”变成了“每个答案都可知为什么”虽然不完美但至少把冲突暴露在了明面上。6.3 隐含上下文用户不把主语说全企业内部问答里最典型的一类失败是“缺主语”。举个例子用户问“这个月的调薪发了吗”系统如果只抓到“调薪发放流程”文档可能会煞有介事地回答“每月 25 日发放调薪明细”。但用户真正想问的是自己所在分公司的调薪是否已经走完审批这个隐含主体如果不上文背景单靠 RAG 根本猜不到。这类问题的解法不是在检索侧硬扛而是要在查询改写环节把隐含主体显式补全或者在提示词里加入引导性追问。更重要的是把这些“缺主语”问题作为一个专项类型放进测试集。如果一个 RAG 没有见过真实口语化的片段式提问它在正式环境里遇到这些问题时会非常脆弱。6.4 相似实体名混淆只见关键词不见实体业务数据里充斥着“华东公司”和“华通公司”、“张三项目组”与“张一项目组”这类易混实体。向量相似度对它们来说可能极为接近一旦错配就是灾难级的回答。常规检索会把包含相似名称的片段同时排到靠前位置生成端在合并时经常张冠李戴。我的处理办法是对实体名的精确匹配单独加权或者在检索结果里嵌入实体识别的后处理步骤。如果你的业务核心词就存在大量易混实体建议在测试集里单独列一组“易混实体”题型专门观察检索端在精度上的表现而不是被总量分掩盖。7. 折腾下来最实用的几条经验与落地建议7.1 在评估上省时间才真的走不远我见过太多团队把 90% 的时间花在“调整 prompt”“更换 embedding 模型”“搭建花哨的 Agent 流程”上留给评估的时间不足 10%。结果是改一版之后“感觉好一些”但好在哪里、为什么好、坏在哪里完全说不清楚。先把 20-50 题的自制测试集、评分卡和版本记录搭起来是整个 RAG 项目里投入产出比最高的一步。你做的每一个调整都要能落在一张可对照、可追溯的表格上。7.2 不要盯着单一指标做无限优化指标是工具不是上帝。某个指标一旦涨不动停下来想想是不是在“过度拟合测试集”。有些团队因为某道困难题一直做不对就专门针对那道题微调提示词结果其他题分数掉下来又去修其他题陷入“指标打地鼠”。正确的做法是定期更新测试集抽掉已经太“背熟”的题目补入新的真实用户问题让评估覆盖始终贴近你还没见过的真实世界。7.3 每一个失败案例都是一等奖数据必须记录失败案例远比成功案例有价值。我习惯每次评估结束后把错题集中抄到一个 badcase 库月底翻一遍。很多系统后续的明显提升都始于回答一个问题“为什么会错”某道题的检索结果全是干扰片段某道题的模型自己编造了答案某道题是 chunk 把表格切成两半。积累到 30 个以上案例后RAG 的主要问题模式基本都会浮现出来修复方向就变得非常明确。7.4 给系统留出“允许失败”的空间最后一条建议看似和评估无关其实密切相关别指望 RAG 上线后能 100% 正确。相比“答错率”更应该关注的是“答错时系统有没有给出依据和出处”。如果每个回答都能追溯到具体文档或具体切片那么即使出错用户和维护者也都能快速追查把系统性翻车变成可分析的小概率事件。凡是在基线阶段就强调“可解释性”的项目落地后的反馈都会明显好于只给结论的系统。没有绝对可靠的 RAG但一定有“不会让人觉得被骗”的 RAG——能做到这一点基线就有它的价值。这就是我一路折腾下来的核心做法和心得。“基线”本质上是一份诚实的体检报告它会不断打破“看起来不错”的错觉让你看清楚系统真正的边界在哪里。如果你最近也在给 RAG 搭建自己的基线建议从明天开始先列出 20 条真实业务问题手工标好答案再跑一遍朴素版本拿个原始分数。等分数出来你对项目真实状态的判断会比任何人都准确。