做RAG应用最尴尬的时刻,不是模型跑崩了,而是demo里演示得好好的知识库问答,一丢给真实用户就漏洞百出。问得稍有偏差,答案就飘;知识点藏在文档深处,检索引擎就是拉不出来;就算把答案生成出来了,用户拿着原文来核对,居然对不上出处。这些问题我前前后后踩了一年多,最后沉淀下来一套从“能用”到“好用”的方法论。这篇就把它完整讲清楚,覆盖评测集搭建、数据与索引优化、检索与生成侧改造,以及常见的线上问题排查逻辑。适合正在做企业知识库问答、客服辅助、文档智能检索的开发者或产品同学参考。
1. 认清差距:RAG“能用”和“好用”之间到底差在哪
1.1 RAG效果不佳的三种典型表现
我接手过好几个RAG项目,第一步都是把线上bad case收集起来归类。归类多了之后发现,问题基本集中在三种表现上。
第一种是答非所问。用户问“报销流程里附件超过5MB怎么办”,系统却把“报销单填写规范”整篇拉出来,生成出一段包含附件、发票、审批人等多条信息混杂的答案。用户要的是一个具体操作指令,系统给了一篇科普文。这种问题多半出在检索粒度上,召回的内容太粗,塞进了大量不相关的句子。
第二种是“每个词都认识,但整段读起来不像人话”。模型把所有召回来的片段机械拼在一起,前后逻辑断裂,同一件事在不同段落里口径还不一致。比如一个片段说“员工需在出差前提交申请”,另一个片段说“出差后一周内补交亦可”,模型不识别矛盾,直接揉成一段模棱两可的话。这种问题的根源在生成侧的上下文组织,多路召回的内容没有做合并与去重,也没做逻辑归一。
第三种是“答案看着对,但用户找不到依据”。我见过最典型的一个案例,系统回答“年度调薪以3月绩效为基准”,但用户在制度原文里根本搜不到这句话。后来查下去,发现是某次文档更新后旧版被删了,但向量库里还留着旧版切片。这是典型的索引与源文档版本不同步。用户可以容忍“不知道”,但很难容忍“一本正经地胡说”,一旦发现引用对不上,对整个系统的信任感会瞬间崩塌。
1.2 把问题拆到四层:数据、检索、生成、评估
到底该怎么系统化地找问题?后来我们团队形成了一套共识:任何RAG效果问题,都先归因到四层中的某一层。
| 层级 | 主要职责 | 典型表现 |
|---|---|---|
| 数据层 | 文档清洗、切分、元数据、版本管理 | 噪音多、切分破碎、信息缺失或过时 |
| 检索层 | 召回候选片段、排序 | 召回不相关、遗漏关键片段、排序靠前的内容质量差 |
| 生成层 | 组织上下文、生成答案、引用来源 | 答案冗长、逻辑跳跃、不引用或引错出处 |
| 评估层 | 衡量效果、发现回归、驱动迭代 | 没有评测集、凭感觉上线、改一版坏一版 |
这四层不是各自孤立的,而是一条流水线:数据决定检索上限,检索决定生成输入,生成决定最终用户体验,评估又反过来驱动前三层的优化。每一层都会把上一层的损耗放大。数据层有噪音,检索层召回就会带偏;检索层排序差,生成层拿到的上下文就是混乱的;生成层缺失约束,哪怕上下文是对的也会输出跑偏。所以排查问题时,从最底层的数据开始复查,往往比在prompt上反复试要高效得多。
1.3 为什么“调prompt”解决不了根本问题
很多人的第一反应是“答案不对,就改prompt”。我早期也是这么干的,在系统提示词里加各种“请务必忠实于上下文”“不要编造”之类的描述。短期看有一些效果,但很快会发现两个问题。
一是prompt约束会被模型“淡忘”。当上下文里塞进大量无关片段时,模型注意力被稀释,写在前面第3条的指令经常不被执行。二是prompt只能调整生成风格,不能弥补检索缺失。如果某个关键信息压根没被召回,那再怎么强调“忠实”,模型也只能在自己的预训练知识里“脑补”。所以prompt不是不能调,而是要放到最后——先保证数据干净、检索命中、引用机制完整,再考虑用prompt统一语气和格式。顺序错了,就是在用装修掩盖地基裂缝。
2. 先做尺子:搭一套可复用的RAG效果评测基线
2.1 评测集怎么造:真实问法优先,按场景分桶
没有评测集就谈优化,等于闭着眼开车。我见过不少团队先用“视觉上感觉好多了”来上线,结果下一个版本别人一测又退回原样。正确的第一步,是先攒一批有代表性的问题和标准答案。
评测集不能只在办公室里自己想。最好的来源是历史客服记录、用户反馈、销售答疑群里的真实提问。哪怕一开始只有几十条,也远比坐在工位编出来的问题靠谱,因为真实用户不会按照文档目录提问,他们的说法五花八门:有人问“怎么报销打车费”,有人问“滴滴的发票能报吗”,还有人问“出差的交通费找谁报销”。同一个意图三种问法,评测集必须覆盖这种多样性。
我习惯把评测问题按场景分桶,至少包含这几类:
- 单点事实型:只需一个片段就能回答,比如“年假天数是多少”
- 多点聚合型:需要把多个片段拼起来,比如“入职满一年且绩效B以上的员工能享受什么福利”
- 否定与边界型:比如“哪些费用不能报销”“超过多少金额需要提前审批”
- 对比型:比如“季度奖和年终奖在计算方式上的区别”
- 时效型:比如“2024年版的制度里病假工资怎么算,和旧版有什么区别”
- 垃圾输入型:用户一句“你好”或者闲聊,系统应该拒答而不是硬答
2.2 评价指标选哪些:别只看“答得对不对”
评测标准只有一句话“答案对不对”是不够的。我常用五个维度打分,每个维度1到5分,最后看分维度均值,而不是只看总分。
忠实度是最重要的一项,检查答案中的每个关键结论是否都能在提供的上下文中找到对应依据,找不到就打低分。相关性与忠实度不同,它衡量回答是否正面覆盖了用户的问题,有没有答非所问。上下文精度与召回则单独评测检索效果:召回的K个片段里有多少是和问题相关的,以及所有相关片段是否都被找出来了。最后是引用正确性,每条引用是否真的支撑它所挂的结论。
打分方式上,纯靠人工太慢,我通常采用“LLM自动初评+人工抽检复核”的组合。让一个大模型作为裁判,把问题和答案、上下文片段一起交给它打分,再让人工抽检20%的case防止裁判模型“审美疲劳”。这里有个关键细节:裁判模型不能用系统里正在用的那个生成模型,否则会“护短”。我用一个独立部署的更强的开源模型,或者直接用通用大模型API来当裁判。
2.3 用大模型做裁判的注意事项
让大模型当裁判,省人力,但坑也不少。最典型的坑是“相似就高分”,裁判模型看到答案里有和参考标准答案相似的词,就倾向于给高分,哪怕语义上是错的。所以我不会让裁判只输出一个分,而是强制它先输出“依据原文逐条核对结论”,把每个结论对应的原文片段找出来,再给分。这样裁判必须先推理再打分,准确性会提高不少。
另外,打分标准要写进裁判提示词里,而且每一维度都要给正反例。我最初只给一句话描述,裁判评出来的分数方差极大,后来加了三个正面例子和三个反面例子,方差立刻就下来了。不要吝啬例子,裁判的提示词写得越具体,自动评测结果越可信。
2.4 建立回归基线:从第一次评测就开始攒数据
评测集建好之后,第一次评测得到的分数就是基线。之后每改一个配置,都拿同一套评测集跑一遍,对比分数变化。这点看似简单,但实操中很容易走样:今天改切片大小,明天调topK,后天换prompt,每改完一次就“感觉不错”,却没有任何记录。正确的做法是每次改动只动一个变量,把评测分数和样例结果截图存下来,统一放到一个结果对比表里。
回归对比表至少要有这些列:版本号、改动内容、忠实度、相关性、上下文召回、引用准确率、典型bad case备注。这样过了两三周回头看,就能清楚知道哪些改动真正有效,哪些只是自我安慰。评测集本身也要持续扩充:每两周把线上新增的bad case去重后加进评测集,防止系统在固定题目上过度拟合。
3. 原料治理:把文档真正切成适合检索的“知识块”
3.1 文档清洗与格式处理:脏数据会一路污染到答案
RAG的效果上限由数据质量决定,这句话怎么强调都不过分。线上积累的文档通常来自Word、PDF、线上Wiki导出,里面有各种脏数据。最常见的是页眉页脚和重复导航内容,经常被切分进正文,导致检索出来的片段大段都是同一句“第X页共Y页”。处理方法是解析时先识别并删除页眉页脚区域,再判断正文段落。
PDF扫描件是另一个大坑。如果直接把扫描件扔进切分流程,出来的就是一坨乱码。必须先用OCR识别。我建议对中文扫描件选带公式和表格识别能力的OCR引擎,表格识别尤其重要,因为很多制度文档里的报销标准、薪资等级都是表格。OCR之后要人工抽检几页,确认表格结构没有被拆乱。
清洗完成后还有一个容易忽略的步骤:段落标题补全。很多文档把一个知识点拆在多个层级标题下,比如“第二章 考勤管理”下的小节“2.1 迟到与旷工”,如果切片时只保留小节正文而丢了上级标题,检索模型就看到一句孤立的话,很难判断语境。解决方案是在切分时把标题层级作为元数据写入每个切片,或者直接把祖先标题拼接进切片的首行。
3.2 切分策略:chunk大小、overlap与结构感知
切片是数据层影响最直接的操作。切得太大会混入无关内容,切得太小会破坏语义完整性。我试过从128到1024 token的不同切片大小,最终大部分场景落在256到512 token之间。太长,向量表示会被平均掉关键信息;太短,一个完整知识点被拦腰截断。当然,这个区间不是绝对的,需要自己评测验证。
overlap方面,我通常设10%到20%。overlap的存在是为了避免信息正好被切在边界上导致丢失。比如512 token的切片,我会让相邻切片重叠48到64个token。这里有一个容易犯的错:过度追求overlap,压着边界反复切,反而让同一句子在好几个切片里重复出现,检索结果里第2到第5条全是同一段话的变体。所以overlap不是越大越好,够用就行。
结构感知切分是解决语义完整性的更优解。现代文档解析工具能识别标题层级、段落、表格、列表,并允许以章节为单位切分。优先按一级或二级标题断开,再根据token上限决定是否继续下切。表格则尽量整表保留为一个切片,因为表头是读懂列含义的关键,把单元格拆散会彻底丢失语义。
3.3 元数据设计:让切片自带“身份信息”
切好的片段如果没有元数据,检索层就只能靠向量相似度“盲猜”。我建议每个切片至少携带这些元数据:文档标题、一级章节、二级章节、页码、来源链接、更新时间、版本号、文档类型(制度/手册/公告)。有了这些,后续可以做很多事情,比如按时间过滤旧版本、按文档类型过滤不相关内容、在答案旁展示出处链接。
一个有用的技巧是“元数据驱动检索”。在向量检索时,可以结合结构化过滤条件,比如用户定位在某个子公司,就过滤掉其他子公司的制度;或者用户问“今年”的福利政策,就过滤掉去年历史版本。纯向量检索对时间、地域这类结构化条件感知很差,元数据过滤能补上这一块。
3.4 混合索引:向量检索不是唯一答案
向量检索擅长匹配语义相近的内容,但对精确关键词、编号、代码片段很弱。比如用户问“制度编号AR-2024-03”,向量检索可能把“AR”和“03”当作普通token分散处理,导致召回的是毫无关系的文档。
我现在的标配是“向量+关键词”混合索引。向量负责语义相似,关键词检索(比如BM25)负责精确匹配编号、专有名词和少量字符差异。两者各自召回一批候选,再做合并去重。合并时权重需要调优:早期我直接按比例融合排序分数,效果不稳;后来改成“向量召回top50,关键词召回top20,合并后统一交给重排序模型”。让重排序模型决定最终顺序,比人为设定权重更省心。这一步可以说是全文性价比最高的一次改动,它让很多原本检索不到的老难题忽然就通了。
4. 检索升级:多路召回与重排序是性价比之王
4.1 向量检索的短板:相似并不等于相关
向量检索衡量的是语义相似度,而RAG真正需要的是“与问题相关的信息片段”。语义相似和相关信息之间存在鸿沟。比如用户问“怎么请婚假”,一篇介绍“婚嫁礼俗”的文章可能语义非常接近,但没有任何制度信息;一篇标题是“员工手册第4章请假管理”的文章表面上词距离很远,却才是正确答案。
所以我把向量检索看作“粗筛”,它的任务是保证不漏掉可能的候选,而不是直接产出最终答案。既然定位为粗筛,召回数量可以适当放宽,top40到top60都行,再多就没必要了,因为候选太多会稀释重排序模型的判断。
4.2 查询改写:让问题先变清晰再检索
用户的问题往往很短,比如“公积金怎么提”,但制度文档里对应的是“住房公积金提取条件与流程”。短问题直接做向量检索,匹配到的可能是跑题的句子。查询改写解决的就是这个问题:先让大模型把含糊的原始提问,改写成更利于检索的查询。
改写有两种常见做法。一种是“扩展式改写”,把“公积金怎么提”改成“住房公积金提取的条件、所需材料和办理流程”,增加与文档表述的匹配面。另一种是“多查询改写”,同一个问题生成3到5个不同角度的检索词,分别去检索,再把结果合起来。多查询会带来一个副作用:检索时间变长,因为要对多个query各做一次检索。我一般在知识库规模超过一定量、且检索召回率明显偏低时才启用多查询。如果只是demo阶段,扩展式改写通常就足够了。
4.3 重排序模型:召回之后的关键一道关卡
混合检索合并出来的候选可能有三五十条,但真正高相关的只有三五条。直接把这三五十条全部塞给生成模型,会让答案变得冗长混乱。重排序模型在这里负责把候选按照“与问题的相关程度”重新排序,并过滤掉低分内容。
我用过后发现,重排序对RAG效果的影响比换向量模型明显得多。原因是向量模型负责语义粗筛,重排序模型则针对“问题-片段”对做更精细的相关性判别,两者分工不同。实操中选择重排序模型要注意中文支持程度,很多开源排序模型在英文上表现不错,中文就差一截。部署上,重排序模型和主LLM可以分开,它通常是个不小的参数规模,但推理成本相对可控。为了速度,我会限制输入到重排序模型的候选数量,比如top50以内,片段长度统一截断。
4.4 topK怎么选:检索数量与生成质量的平衡
topK的选择直接影响答案质量与响应速度。太小,可能漏掉关键片段;太大,会往prompt里塞进大量噪音,模型容易被带偏。我常用的起点是top5,也就是重排序后取前5个片段进生成。如果评测发现忠实度得分低、且bad case是因为“正确片段排在第6位之后”,就把topK调大到7或8,继续评测。
这里要特别提醒:topK和切片粒度必须联调。切片只有256 token时,top5覆盖的信息总量也就一千多token,未必够;切片512 token时,top5可能已经足够覆盖一个完整知识点。所以我一般先定切片大小,再定topK,最后再微调overlap。三个参数一起改的话,出问题你都不知道是谁引起的。
5. 生成把关:让答案不仅对,还要有据可循
5.1 Prompt结构:把“指令区”和“材料区”物理隔离
大模型对prompt的依赖程度超出很多人想象,但它的依赖方式不是“多讲道理”,而是“结构清晰”。我倾向于把系统提示词分成几个块:角色定位块、任务说明块、输出格式块、引用规则块、固定话术块。每一块用明确的标记隔开,后续接检索到的上下文片段。
任务说明块里写清楚阅读顺序和决策规则。比如“优先采用用户提供的资料中的信息,若资料与你的预训练知识冲突,以资料为准并说明”,这样能减少模型自作主张。但注意,规则数量不宜超过五条,写太多模型根本记不住,越靠后的规则越容易被忽略。核心规则一定要放在最前面,或者干脆写进输出格式里作为强制字段。
5.2 引用标注:没有来源的结论就是编造
引用机制是RAG系统可信度的关键,也是和普通聊天机器人最大的区别。我在生成结构里强制要求答案中的每个关键结论后面都带上片段编号,比如【1】【2】。同时,系统提示词还要写清楚“片段编号必须在提供材料中存在,不得自行编造”。
实操中经常出问题的是“引用漂移”:模型从片段A得到结论,却把引用标到了片段B。这里面有个技术细节值得分享:生成的输出格式不要用自由文本,用结构化的JSON或者类似Markdown列表的格式,然后在后处理时解析出每个句子和它携带的引用编号,再和实际提供的片段做交叉验证。如果某个引用编号对应的片段里确实不包含那个句子表达的信息,就把该句标记为“存疑”,或者直接触发拒答。
5.3 拒绝回答与兜底策略:敢说“不知道”才是成熟
很多系统为了追求回答率,明知没把握也要硬答,结果就是一本正经地编。我后来定了两条原则:检索到的片段最高相关度低于某个阈值时,直接返回“当前资料中未找到相关信息,请尝试换个问法或联系管理员”;多个片段之间存在明显矛盾且无法消解时,如实列出两个口径,不做倾向性选择。
阈值怎么定?我在重排序环节对每个片段都会得到一个相关度分数,取最高分作为整体置信度。这个阈值需要拿一批bad case验证,我通常把阈值设在全量正样本最低分和全量负样本最高分之间的中点。上线初期宁可拒答率高一点,也不要为了覆盖率把置信度门槛拉到很低,因为一次错误回答造成的信任损失远大于一次拒答。
5.4 用户问题与文档内容冲突时怎么办
线上最让我头疼的一类case是:文档本身过时了,但用户问的是最新政策,模型按文档答反而被用户嘲笑。解决思路是给切片打时间标签,并在检索时加入时间过滤。比如公司最近更新了报销标准,旧版制度文档应被打上“已废止”状态,在检索阶段直接排除。
还有一类冲突是用户问题里带着错误前提,比如用户问“年度体检是不是只有入职满一年才能享受”,而制度里根本没有“满一年”这个限制。模型如果顺着问题回答“是的”,就掉进了用户预设的陷阱。所以我要求在生成提示词里加一条:如果用户问题中包含资料中没有提到的限制条件,先纠正前提,再给出完整信息。简单说就是,先纠偏再作答,而不是顺着错误问题回答。
6. 持续迭代:把bad case当作系统进化的燃料
6.1 从一条bad case定位到故障层级的方法
当收到一条用户投诉时,我会按固定顺序排查,而不是凭感觉乱试。这套排查顺序我用了很久,稳定可靠。
第一步,看检索结果。把用户问题丢进检索链路,打印出重排序后的top10片段,人工判断相关片段是否被召回。如果压根没召回,问题在数据层或检索层;如果召回了,继续第二步。第二步,看重排序后的排序位置。相关片段排在top5之外,问题在重排序或topK设置;如果排在top3,继续看生成。第三步,模拟生成并对比坏答案与上下文。坏答案结论与上下文对得上,那是生成策略的问题;对不上,说明prompt约束失效,或后处理引用校验没生效。第四步,检查评测流程。如果这个问题在评测集里没有覆盖,就补充进去;如果有覆盖但评分没反映出来,说明裁判提示词有漏洞。
每一条bad case走完这套流程,都会自然落到某一个可执行的改进项上,不会出现“哪个都像、哪个都不是”的迷茫局面。
6.2 上线后的监控指标:七个数盯住系统健康度
系统上线只是开始,后续必须持续观察指标。我每周都会看七个数,它们比单个用户反馈更能反映趋势。
- 检索覆盖率:召回结果中包含真实相关答案的比例,低于某阈值就该检查数据层
- 拒答率:太低要警惕模型在瞎编,太高说明检索太弱或阈值过严
- 引用准确率:抽检答案中引用编号对应片段能真实支撑结论的比例
- 平均答案长度:明显增长往往意味着上下文太乱,模型在“话痨”
- 首响耗时:p95超过一定秒数,重排序或生成侧要优化
- 用户追问率:答案后用户很快追问“为什么”,大概率是答案没讲透
- 用户纠错反馈率:用户明说“不对”“错了”,这类case最高优先处理
监控数据不需要做得很重,拉一张周报就能用,关键是持续记录趋势。如果某一周拒答率突然从5%涨到15%,我会先回看这一周文档有没有新上传、切片配置有没有改动,通常能很快定位。
6.3 常见问题速查表:把踩过的坑沉淀下来
下面这张表是我平时排查问题用的一张速查表,整理出来供参考。
| 症状 | 最可能原因 | 优先排查动作 | 常用解法 |
|---|---|---|---|
| 答案总是带无关内容 | 切片过大或topK过大 | 检查召回的top5片段与问题相关性 | 调小切片,收紧topK并加强引用校验 |
| 正确答案在文档里但检索不到 | 文档未清洗、乱码或OCR失败 | 检查源文档解析结果 | 修复解析管道,重建索引 |
| 多个片段互相矛盾 | 文档版本混存 | 检查元数据中的时间或状态字段 | 按状态过滤旧版本 |
| 问法一变就找不到 | 依赖单一向量检索 | 打印关键词检索与向量检索分开的结果 | 开启混合检索+查询改写 |
| 答案像缝合怪,逻辑断裂 | 上下文顺序被打乱或缺少引导 | 检查重排序输出是否破坏原文档顺序 | 在prompt中强调按时间或逻辑顺序组织片段 |
| 引用编号对不上内容 | 生成阶段引用漂移 | 开启后处理引用校验 | 用结构化输出+交叉验证机制 |
| 用户闲聊问题被强答 | 没有意图判断或置信度阈值过低 | 查看最高相关度分数 | 增加前置意图分类,提阈值拒答 |
6.4 小步快跑:一个迭代周期怎么安排最省力
持续优化最忌讳“憋大招”。我习惯按两周一个小迭代来跑,每次只聚焦一个环节。第一周集中收集和清洗bad case,更新评测集,跑出当前基线的分数;第二周选择一个变量做改动,只动一个参数或换一个模块,重新评测并对比。动作越小,归因就越清楚。
这套节奏看起来慢,但累积效果很惊人。我第一个项目前期表现堪忧,按这个节奏跑了六轮之后,忠实度从2.8分涨到4.3分,引用准确率从不到60%涨到90%以上。整个过程没有一次伤筋动骨的重构,全靠小步快跑叠出来的。
写在最后的经验
RAG优化这件事,最大的误区是以为模型越强效果越好。实际用下来,绝大多数系统的瓶颈都在数据和检索链路上。换个更大参数的生成模型,也许能把话说得更顺,但检索不到的关键信息它依然检索不到。先把文档洗干净的收益,比换任何模型都大。
另外,评估这件事一定别偷懒。哪怕一开始只有五十条评测题,也要把基线跑出来。没有基线的优化,改到哪里算哪里,最后很容易陷入“调一版、退一版”的循环。我个人至今还在坚持每周更新评测集、跑一次回归对比,这个习惯让所有改动都能被数据看见。
最后分享一个小技巧:每次改完一个参数,都把“预期收益”和“实际收益”写在同一行。两三轮迭代之后,你会发现哪些环节是长期有效的,哪些只是特定case上的局部优化。这种判断力,比任何现成模板都值钱。