上一期整理了六个RAG知识库调优案例之后,不少做知识库落地、客服问答、内部文档检索的朋友在后台追问,说还想看更多实战场景。这期我继续挑了六个案例,和上一篇完全不重叠,全部是真实项目里遇到过、并且花了不少时间才定位到根因的问题。如果你正在做RAG系统,尤其是文档召回率不稳定、回答质量时好时坏的那一类,这篇应该能帮你省下不少排查时间。
先说清楚这六个案例覆盖的范围:有切分策略翻车、有混合检索权重设置不合理、有rerank反而帮倒忙、有扫描版PDF直接入库变成乱码、还有检索完全正确但答案依然不对的诡异场景。这些问题表面上看都是“回答不准确”,但真正排查下来,根因分布在RAG链路的各个角落。每一个案例我都会按“现场还原、排查过程、根因定位、优化方案、效果数据”这个顺序讲,方便你对照自己的项目排查。
1. 案例一:长query直接查询,关键词被“稀释”了
1.1 现场还原与最直接的排查
用户问了一个很口语化的问题:“公司去年裁员的补偿方案现在还能追溯吗”。系统返回的前五个片段,全是入职培训、考勤制度、年假说明这些完全不相关的内容,而知识库里明确存在一份《经济性裁员补偿管理办法》,就是没被召回。
我第一反应是看向量检索的分数。把query和top-10片段的相似度全部打印出来,发现得分最高的一条也只有0.52,其余都在0.4左右徘徊。这个分数说明不是“相关但排序靠后”,而是“所有候选片段都不怎么相关”。接着我又拿BM25做了关键词召回,结果依然不理想——问题出在分词上,“追溯”被切成了“追”和“溯”,“补偿”和“裁员”虽然在文档里有,但因为和原问题里的其他词混在一起,BM25的TF-IDF权重被稀释了。
1.2 根因定位:不是向量模型不行,是“提问方式”不行
这个案例的根因有两层。
第一层,用户的口语化长句包含多个语义实体,直接塞进embedding模型后,模型会把整句话编码成一个平均化的向量。比如“去年”“裁员”“补偿方案”“追溯”四个关键概念,在向量空间里被拉成了一个模糊的“公司政策相关”的语义点,反而丢失了具体的指向性。这不是模型本身的错,是长query和短文本片段天然不匹配,embedding模型对短文本的编码质量通常更高。
第二层,知识库里同一个实体存在多种表述。文档标题写的是“经济性裁员”,用户的问法却是“裁员”,虽然在人工理解看来是一回事,但在向量空间里,这两个词组的表征并不完全重合,相似度可能只有0.6左右。
1.3 优化动作:query改写加多路召回
针对这个案例,我做了两个调整,效果非常明显。
第一步,加了一个query改写模块。用户query进来之后,先用LLM做一次轻量级重写,把口语化长句提炼成适合检索的短关键词组合。我用的prompt大概是这样的:
你是一个检索query优化助手。用户输入一个自然语言问题,请提取出3-5个与问题强相关的关键实体和概念,用空格分隔,不要输出任何解释性内容。 输入:公司去年裁员的补偿方案现在还能追溯吗 输出:经济性裁员 补偿方案 追溯 裁员补偿这一步看起来简单,但对RAG整体效果的影响非常大。改写之后,向量检索的top-5命中率从20%左右直接提升到了75%以上。
第二步,引入多路召回。向量召回、BM25关键词召回、以及简单的模板规则召回三路并行,最后做归一化合并。这里要注意,多路召回是手段,不是目的,如果只做一路召回的结果已经很好了,没必要强行加路数,反而增加合并排序的复杂度。
提示:query改写不要做得太激进,不要提取出原文没有出现过的关键词,也不要让LLM自由发挥生成大段描述,否则会引入新的语义偏差。我自己在测试中就遇到过,LLM把“裁员”改写成“人员优化调整”,反而召回了更多无关的人力资源制度文档。
2. 案例二:表格被切分器“大卸八块”,结构化信息全丢了
2.1 问题表现:检索结果里全是“碎尸万段”的表格单元格
一个知识库里有大量PDF格式的薪酬绩效制度文档,其中包含很多Excel风格的表格,比如“岗位工资等级表”“绩效考核系数对照表”“排班补贴标准表”。用户输入“一线员工的绩效系数怎么算”,系统返回的片段里,前三条全是零散的单元格文本,比如“B+”“1.0”“月度考核”,完全无法拼凑成一个完整的计算规则。
我打开切分结果文件之后,瞬间明白了问题所在。原来的切分策略是按固定长度切分,embedding模型用的是256 token窗口,结果就是一个完整的表格被从中间劈开,表头在第一个chunk,数据行在后面的chunk里,甚至有的关键行被切到了两个chunk的中间。而RAG在召回时通常是按chunk粒度打分,那些被切碎的chunk本身就没有完整语义,模型自然不知道它们在说什么。
2.2 从切分结果开始排查
这类问题,最快的方法是把切分后的数据直接dump出来,用肉眼扫一遍。我建议团队在建立知识库的时候,一定保留一份“切分预览”索引,每次调整切分策略后都抽几个代表文档看效果。只看整体指标而不看切分实物的调优,都是在盲人摸象。
排查下来发现两个具体问题:
- 表格被切成多个chunk后,表头和数据行失去关联。
- 切分边界把完整的计算公式切成两半,比如“绩效系数 = 考核得分 / 100 × 部门系数”被切成了“绩效系数 = 考核得分”和“/ 100 × 部门系数”两个片段。
2.3 切分策略的调整方案
针对表格密集型文档,我把切分策略改成了“按结构切分”加“表格优先保留”。
具体操作是:先用文档解析器把PDF转成带结构信息的格式,识别出哪些区域是表格,哪些是普通段落。表格区域不按token切分,而是整体保留,并且在转换为Markdown格式后作为一个独立的chunk入库。普通段落则按段落边界切分,不够长度的段落再和相邻段落合并。
对于特别大的表格,如果整体保留超过最大token限制,就按行分组切成几个子表格,同时保留原始表头。例如一个100行的工资等级表,可以按职级切成“管理岗”“技术岗”“操作岗”三个子表,每个子表都带上完整的列名。
另外,我给所有chunk加了一个metadata字段,标记它的内容类型是“表格”“段落”还是“列表”。后续在召回阶段可以针对不同类型的查询做权重调整,比如用户问“怎么算”“多少钱”这类问题时,给表格类chunk更高的权重。
注意:固定长度切分并不是完全不能用,但它适合的是纯文本、结构简单的文档。如果知识库里表格、代码、多级目录混杂,就必须切换到按语义边界切分的策略。这里的核心原则是:一个chunk内部尽量是一段完整的语义单元,宁可单chunk长一些,也不要切到一半。
3. 案例一和案例二的组合拳:多路召回和切分策略的协同
案例一解决了query侧的问题,案例二解决了文档侧的问题,但是在实际项目里,这两类问题经常是同时出现的。我遇到过不少团队,已经做了多路召回,也做了query改写,但效果依然不理想,最后发现是切分策略把文档搞得没法召回。反过来,也有团队切分做得很细,但用户query太长导致整体效果上不去。
所以这里单独把两者的协同关系拿出来说。
3.1 召回质量的评估方法
在做任何优化之前,先建立一个可量化的评估集。我通常的做法是从真实用户query里抽200条,人工标注每一条query对应的正确文档位置。之后每次调整切分策略、召回策略,都用这200条query跑一遍离线评测,计算top-5召回率。
只有建立这个评估闭环,你才能知道某个改动到底是变好还是变坏。否则你很容易被两三例“看起来变好了”的现象误导,实际上整体效果退化了。
3.2 迭代顺序建议
先修切分问题,再改召回策略,最后优化query侧。逻辑很简单,切分是地基,切分质量不行,后面召回和重排做得再好也是白搭。query改写是锦上添花的部分,只有当你确认切分和召回都没有明显问题时,query改写的价值才会显现出来。
我统计过,在切分合理的前提下,query改写带来的top-5召回率提升大约在15%到30%之间。但如果切分本身一塌糊涂,query改写能把一个本来就很差的系统从“完全不可用”变成“偶尔能用”,但离一个稳定可用的知识库问答系统还有距离。
4. 案例三:混合检索一加一小于二,分数量纲完全不在一个量级
4.1 现场现象:向量召回和BM25各自都对,合并之后反而错了
这个项目同时做了向量召回和BM25关键词召回,本想双保险,结果用户问“跨部门转岗后年假怎么计算”,向量召回的第一名是正确文档,BM25召回的第一名也是正确文档,但合并排序之后,正确答案掉到了第五名,系统最终返回了一个错误答案。
排查的时候我把两路召回的具体得分打印出来,瞬间就看出了问题。向量相似度是0~1区间的浮点数,BM25的得分却是0~20之间的数值。合并排序时我直接用加法把两个分数相加,BM25高得多的绝对数值完全压过了向量相似度。于是最终排序结果基本等于“只按BM25排序”,向量召回那一路等于白做了。
4.2 量纲归一化的两种常用方案
混合检索合并有个基本前提:各路得分必须映射到同一量纲,或者采用不依赖绝对分数的排序融合方案。
方案一,归一化然后再加权。对向量相似度和BM25得分分别做min-max归一化或Z-score归一化,再按权重相加。这个方法直观,但问题在于每路得分的分布可能差异很大,min-max归一化容易受离群值影响。
方案二,倒数排名融合,也就是RRF。这个方法不直接使用分数,而是用排名位置来融合。
RRF的公式是:score(d) = Σ 1/(k + rank_r(d)),其中k是一个平滑常数,通常取60。
意思是,文档d在每一路召回中的排名越靠前,它的融合得分越高。因为用的是排名而不是分数,天然避开了量纲问题,对异常值也不敏感。具体计算过程如下:
- 向量召回中,文档A排名第1,贡献 1/(60+1) ≈ 0.0164
- BM25召回中,文档A排名第3,贡献 1/(60+3) ≈ 0.0159
- 文档A总得分 ≈ 0.0323
再看另一个文档B,向量召回排第8,BM25召回排第1,总得分 = 1/(60+8) + 1/61 ≈ 0.0311。在这种情况下,文档A的融合得分高于B,这是因为它在两路中都靠前。
4.3 优化后的效果与调参心得
切到RRF之后,top-3准确率直接提升了20多个百分点。这个项目里的具体数据是:优化前混合检索的top-3命中率只有45%左右,优化后到了70%以上,而且几乎没有“正确答案本来在前面却被合并挤到后面”的情况了。
RRF里有一个参数k,这个值的设置有点讲究。我试过k=30、60、100,实际效果是k=60附近比较稳,在大多数业务场景下都不错。k太小会让第一名权重过大,k太大则各路召回的区别度变小。另外一个细节:不同路召回的结果条数要设合理,我一般每路取30~50条,然后再做融合,保证融合时各路的信息量是均衡的。
经验:混合检索合并时不建议直接用原生分数做加权平均,除非你已经做过充分的数据分析和归一化。RRF的思路更稳健,实现也就几行代码,强烈建议直接用。
5. 案例四:加了rerank之后,结果反而更差了
5.1 问题现场:模型选择了“看起来更新”的答案
一个客服知识库系统,在向量召回和混合召回之后接了一个bge-reranker-large模型,想进一步提升排序质量。结果上线后测试发现,用户问“2023年的报销标准是多少”,答案却答成了2022年的旧标准。而且这个旧标准doc在rerank之前的排名并不高,是rerank把它排到了第一位。
我一开始以为是rerank模型本身有问题,于是把召回候选集、rerank前后的排序结果全部打印出来对比。这一看才发现,问题的根源在候选集。当时召回阶段只取了top-5,也就是只有5个候选片段进入rerank。在这5个候选里,有一份《2022年费用报销管理办法》和一个《2023年报销限额调整通知》。从语义匹配度看,2023年的那个文档确实在query相关的匹配上弱一些,因为它的文本更短、包含的报销项目词更少,而2022年那份文档里报销项目和金额列得很全,语义相似度天然更高。rerank模型严格按相关性排序,在只有5个候选的情况下,它只能在这5条里选“最像答案的”,于是旧的详细文档排到了第一。
5.2 根因:候选集太小,rerank没有足够的选择空间
这个案例暴露的是RAG链路里的一个经典问题:召回阶段得分最高的结果,并不一定是排序阶段最想要的结果。
排查下来的三个问题:
- 召回候选取太少(top-5),导致真正正确的文档根本没进候选集合的,rerank再怎么排也排不出来。
- 没有按时间元数据过滤。这个知识库的文档本身带着生效日期meta字段,但检索阶段没有利用它,导致不同年份的版本混杂在一起。
- rerank分数与业务规则的优先级关系没有定义清楚。业务上要求“同一主题优先最新的生效版本”,但rerank模型只按语义匹配度排序,不了解业务的时间优先级。
5.3 优化方案:扩大候选集加元数据过滤
优化动作分三步走。
第一步,把召回候选集从top-5扩大到top-50,让更多潜在相关文档进入rerank阶段。召回阶段的核心指标是“召回率”,宁可多召回一些不相关的,也不要漏掉正确的文档,排序是否精准交给rerank来做。
第二步,在召回阶段就利用metadata做硬过滤。根据query里的时间词(“2023年”),直接在检索时候滤掉生效日期不在对应区间的文档。这个过滤必须在embedding检索之前做,或者至少要在召回之后、rerank之前做。实现上可以给向量数据库的collection加filter条件,或者在业务代码层面过滤候选集。
第三步,对rerank之后的top-3结果做业务规则干预。比如对于同一主题的多份文档,优先选择元数据中生效日期最新的版本。这一步可以放在rerank后,作为最终排序的一个业务修正。
优化后,这个案例的正确率从35%提升到了80%以上。核心不是rerank模型本身不够好,而是整个链路的配合出了问题。
提示:rerank不是万能药。如果你发现加了rerank反而变差了,先从候选集入手检查,看看正确答案有没有出现在召回集合里。如果正确答案根本不在候选集里,问题不在rerank,在召回阶段。
6. 案例五:扫描版PDF直接入库,向量化之后全是乱码
6.1 现场现象:文档明明有答案,系统却一直说找不到
这是一个法律合同知识库项目,用户问“合同中关于违约金的最高上限是怎么约定的”,系统从知识库里捞出来的片段要么是“甲方乙方签署页”这种版权页面,要么是一堆肉眼看不明白的乱码序列。
我把当时的处理链路翻了一遍,发现这批合同文档是扫描版PDF,也就是每一页都是图片,没有文本层。入库时虽然调用了PDF解析器,但解析器没有OCR能力,提取出来的只是一堆近似乱码的文本。更麻烦的是,向量模型把这堆乱码编码成了“语义向量”,相当于在垃圾数据上做检索,效果自然好不了。
6.2 排查链路:从源文档到切分结果一步一步查
排查这类问题,最关键的是把链路中的中间产物全部可视化。我把流程整理为:
- 检查源文件是否存在文字层,用PDF阅读器打开,能选中文字就说明有文字层,选不中就是扫描版。
- 直接输出解析器提取的纯文本内容,观察是否存在乱码或大量空白字符。
- 查看切分后的chunk文本,确认乱码有没有被带进来。
- 查看embedding后的向量检索结果,确认垃圾向量有没有被命中。
这个案例中,第2步就暴露了问题。解析器提取出来的“文本”是一堆无法阅读的字符组合,说明OCR环节缺失。
6.3 OCR预处理流程与效果对比
发现问题之后,我在文档入库之前加了一条OCR前置处理流程,具体步骤如下:
- 用PaddleOCR对扫描版PDF按页进行OCR识别,输出文本和坐标信息。
- 根据坐标信息重组版面结构,识别标题、段落、表格区域。
- 清洗OCR输出,去掉明显的识别错误字符,并把表格内容转成Markdown格式。
- 确认每个页面的文本长度正常,异常页面单独标记,后续人工复核。
- 最后再走正常的切分和嵌入流程。
这一步改造带来的效果非常显著。同一批合同文档,检索命中率从改造前的18%提升到了83%。而且不仅准确率上来了,回答的引用来源也规范了——因为是OCR识别出来的文本,可以定位到具体页数和坐标,用户可以直接去原文核对。
这里特别强调一个细节:OCR之后的文本必须保留和原文页面的映射关系。知识库问答通常会要求“给出来源”,如果没有页面映射,就只能在chunk层面给一个模糊的文档名,这对业务人员来说参考价值很有限。PaddleOCR输出的坐标信息完全可以支撑这种映射,花一点时间把页面编号和chunk关联起来相当值得。
注意:OCR不是万能的,对于手写体、印章遮挡、表格线复杂的文档,识别效果可能很不稳定。我建议在OCR流程后面加一个人工抽检环节,按批次抽查识别正确率,尤其是合同这类高风险文档类型,不要全部交给机器,这是一个常见的坑。
7. 案例六:检索片段全对,但LLM还是答错了
7.1 问题现象:召回精准,答案诡异
这个案例是最诡异的。用户问的是“三次劳动合同到期后续签,公司有权拒绝吗”。检索出来的前三个片段,有两个都是相关内容,讲的就是连续签订两次固定期限合同后第三次的情况。按理说LLM拿到这些片段应该能给出正确答案,但实际回答大段引用了一段已经废止的旧条例,结论完全错误。
我第一反应是提示词写得有问题,或者LLM本身的幻觉。于是我把此次请求的完整链路日志翻了出来,包括query、召回片段、拼接后的prompt,以及LLM的完整输出。
这时发现了问题。召回结果里出现了三份不同的合同续签政策文件,其中一份标注“根据《劳动法》相关规定”,一份是内部人事制度文件,另一份是某省高院的司法解释。按时间元数据来看,这三份的效力级别完全不同,司法解释的权威性最高,法律其次,内部制度最低。但系统在拼接prompt时,只是简单地按相关性分数排了一下,没有按“法律效力层级”做排序。
结果LLM在阅读这些片段时,优先采信了排在前面的低效力文档,导致最终答案出现了偏差。
7.2 根因:检索相关性和答案可采纳性是两回事
这个案例反映出RAG系统里的一个深层问题:我们做检索时按“语义相关性”排序,但生成答案时,LLM需要对多个片段做“内容可信度判断”。如果不同片段之间存在信息冲突,LLM不知道应该采信哪一个。
解决这个问题,除了在检索阶段做相关性和业务规则的折中,还需要在上下文构造阶段多做一步:
- 给每个chunk附加上“文档权威级别”元数据,例如“法律法规”“司法解释”“内部制度”“新闻资讯”等。
- 在拼接prompt之前,按权威性做一次组内排序,而不是完全依赖相关性排序。例如所有片段先按权威级别放到几个桶里,然后桶内按相关性排序,再按权威级别的优先级拼接。
- 在prompt里增加一条指令,告诉LLM如果多个片段存在冲突,优先采纳权威级别更高的内容,并说明判断依据。
这里再强调一下,prompt中片段过多并不一定是好事。有一次我测试时,把top-10全部塞进prompt,结果LLM面对太多信息,反而不知道如何取舍。后来改成“先按权威级别排序,然后每个级别最多取2条”,总共5-6条作为上下文,效果反而更稳定。上下文质量大于数量,这是RAG调优中很重要的一点。
7.3 效果与扩展:向agentic rag演进的路径
这个案例优化之后,回答准确率提升明显。更重要的是,它给团队指出了一个改进方向:并不是所有query都适合直接走“一次性检索加一次性生成”的RAG链路。对于涉及政策法规判断、跨文档比对、信息冲突检测的复杂query,可以引入agentic rag的思路。
具体来说,agentic rag的核心变化是:系统不再只做“一次检索、一次生成”,而是让LLM在回答过程中自主决策。比如,先判断“这个问题需不需要先确认问题的法律适用地域”,如果需要,先查一下知识库里的地域标签,再根据结果决定后续检索路径。这种多步推理的方式,在处理跨文档、多条件、有优先级关系的领域问题时,表现得比单轮RAG更稳定。
不过要提醒的是,agentic rag的链路更长,排错也复杂得多。如果你当前的单轮RAG都还经常出错,不要急着上agentic rag,先把切分、召回、上下文物料这几层做扎实,再考虑用多步推理去解决那些真正复杂的问题。
8. 六个案例的问题速查与调优优先级参考
六个案例讲完,我整理了一张速查表,你可以把它当成排查RAG知识库问题的快速索引。
| 案例 | 表面现象 | 根因 | 优先级 | 快速优化动作 |
|---|---|---|---|---|
| 长query检索效果差 | 回答完全不相关 | query语义被稀释 | 高 | 用LLM做query改写,提炼关键词组合 |
| 表格文档无法检索 | 返回的片段语义残缺 | 固定长度切分破坏结构 | 高 | 表格类型文档整体保留并转Markdown入库 |
| 混合检索效果更差 | 正确答案排名下降 | 不同路分数量纲不统一 | 中 | 改用RRF倒数排名融合 |
| 加了rerank反而变差 | 正确文档被挤出前列 | 候选集太小 | 中 | 扩大候选集到top-50,再配合元数据过滤 |
| 扫描版PDF乱码 | 检索结果无法阅读 | 缺少OCR预处理 | 高 | 增加OCR流程,入库前人工抽检 |
| 检索正确但答案错误 | 召回片段正确,生成结果错误 | 上下文排序和权威性冲突 | 中 | 按权威级别排序,prompt中增加冲突处理规则 |
从我多次实战的经验来看,排查RAG问题应该先从哪个环节下手,有个简单的判定方法:如果回答内容杂乱无章,优先检查切分;如果回答相关但位置不对,检查召回和重排;如果回答看起来有模有样但结论错误,检查prompt和上下文构造。
9. 几个贯穿所有案例的排查工具和建议
上面六个案例每个都有特定的排查手段,但有几个通用的工具和建议,我觉得值得单独整理一下。
第一个是链路日志。RAG系统必须有完整、可追溯的请求日志,从query进入、召回结果、重排结果、prompt拼接,到LLM输出,每一步的中间结果都要记录下来。没有这个日志,所有优化都只能靠猜。这里的难点在于中间结果信息量大,如果全部记录会导致日志体积膨胀。我的做法是:平时只记录关键指标,比如召回数量、平均相似度、耗时,排查时再开启详细模式,记录完整的中间产物。这样既能控制日志成本,又不影响问题定位。
第二个是离线评测集。任何调优动作上线之前,都建议先用一批带标注的query做一次离线回归测试。200条高质量query标注就够用了,不需要太多。有了评测集,你才敢放心地调切分参数、改召回策略,不然用户反馈一个你改一个,永远被问题追着跑。
第三个是分阶段验证。遇到“回答质量差”这种笼统的问题,先判断是哪一个环节引起的。这里要特别提醒,很多人在检索结果不理想时会直接换embedding模型,觉得换个更大的模型就万事大吉,这其实是很常见的一个误区。在换模型之前,建议先把query改写、切分策略和召回融合这几个相对轻量级的优化做了,效果往往会比单纯换一个大模型来得明显。如果这些层面都优化过了,再用A/B对比实验来验证模型升级的效果,会更稳妥。
第四个是关注用户反馈。RAG知识库和传统搜索不一样,它面向的是业务用户,用户是否信任系统直接决定系统价值。即使检索准确率95%,剩下5%的错误答案带来的不信任感可能比搜索里30%无关结果带来的还大。所以一定要在实际使用过程中持续收集“用户点了不满意”的数据,从这些bad case里持续迭代。
我自己在维护RAG项目时最大的感受是:RAG调优没有一劳永逸的银弹,大多数问题都需要一条一条链路去查,一个case一个case去修。把日志、评测集、分阶段验证这些基本功做好,比到处找“调参口诀”有用得多。