ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG调优实战:分块、召回、重排三环节的6个关键结论

RAG调优实战:分块、召回、重排三环节的6个关键结论 RAG项目上线后的第一周我盯着后台的日志数据越看越不对劲用户提问后真正点了“查看参考资料”的比例不到三成而回答被判定为不满意的会话里有很大一部分其实检索回来的资料里已经有答案了LLM没找着或者没用上。说白了问题不在生成侧而在“喂给模型什么”这个环节。这个环节就是RAG的检索链路拆开看就是三件事分块、召回、重排。这篇文章不聊概念直接写我在一个真实企业知识库项目里跑出来的6个实战结论。场景是混合文档集产品操作手册、FAQ、版本变更记录、内部SOP总量约1200篇、折算约50万token的中文内容Embedding用的bge-m3向量库用的Milvus检索默认top_k10重排模型用的bge-reranker-v2-m3整个链路用LangChain4j串的。我会把每个结论的实验背景、数据走势和最终取舍都讲清楚最后附上一套可以直接抄走的默认参数和调参顺序。适合正在做RAG落地、并且卡在效果调优上的朋友参考。1. 先说清楚测试环境和评估口径1.1 语料构成与场景选择很多RAG项目一开始都栽在“数据集太干净”上。我这次特意选了一个杂乱的混合知识库模拟真实企业里的文档现状产品操作手册是结构化程度最高的有明确的章节和层级但是里面有大量图表说明和参数表格售后FAQ是典型的短文本一问一答长度差异极大有的回答只有一行有的包含几十行故障排查过程版本变更记录是半结构化的按版本号堆叠新旧信息混杂内部SOP最麻烦流程性内容多同一个操作步骤经常在不同文档里被重复描述只是措辞不同。这个语料构成对应的正是企业知识库最常见的形态所以实验结果有参考价值。1200篇文档不算是很大的规模但50万token的总量已经足够暴露分块和召回环节的问题了。测试集是从真实用户日志里抽的100条问题覆盖三类单点事实查询比如“某型号设备的供电电压是多少”、流程步骤查询“设备开机后报E12代码怎么处理”、跨文档信息整合“新版本固件升级后哪些操作手册里的步骤发生了变化”。说句实话前两类问题在调优到中后期基本都能解决真正让分块、召回、重排三个环节暴露问题的是第三类跨文档整合问题。这类问题的特点是答案分散在多篇文档里单靠某一个chunk必然答不全。如果大家想快速验证自己的RAG链路我建议测试集里至少要留30%这样的多跳问题不然你会误以为自己的链路已经没问题了。1.2 评测指标与实验控制方式我用的评测指标有三个hit_rate召回命中率判断标准是“正确答案是否出现在返回的候选块里”、MRR平均倒数排名衡量正确答案在候选列表里的位置、答案忠实度人工打分判断生成内容是否严格基于检索到的材料没有臆造。hit_rate回答的是“找没找到”MRR回答的是“排得够不够靠前”答案忠实度回答的是“喂给模型的材料是不是清晰到可以让它不乱说”。实验控制上除了目标变量外其他全部固定。比如测分块大小时Embedding模型、检索方式、top_k值、重排参数全部保持不变测重排时分块和召回配置固定只切换重排模型或重排策略。这里的坑在于如果你同时改了分块和重排效果变化了你根本不知道是哪个环节贡献的。RAG调优最忌讳的就是“多点同时调”必须一次只动一个变量。所以后面我给出的每个结论都是在严格控制变量的前提下得出来的趋势性判断具体数值因为语料和业务不同会有波动但趋势是一致的。2. 分块环节的两个实战结论2.1 结论一块大小不是孤立参数它是和返回策略一起设计的分块大小是RAG项目里第一个要被调的参数也是最容易被“拍脑袋”定的参数。我见过很多项目上来就直接800或者1000问为什么答曰“大家都这么设”。我这次专门做了从500到1500的梯度测试固定overlap为80个字符Embedding用bge-m3检索用稠密向量top_k10。结果如下块大小hit_rateMRR答案忠实度5000.830.650.768000.850.700.7910000.870.710.8112000.870.700.8015000.840.630.77单看hit_rate1000到1200是最好的区间但是注意MRR在1200开始下滑1500时整个趋势反转因为过大的块会把不相关的信息嵌入同一个向量干扰了语义相似的度量。这个趋势对做知识库的人并不陌生。但我后来发现一个更关键的问题这个表只说明了“块大小对召回有影响”并没有告诉你“块的返回方式”才是决定生成质量的核心变量。也就是说拆成多大和把多大的块送给LLM其实是两个独立的问题。有些方案检索命中一个500字的小块后直接把这个小块返回给LLM上下文不够另一些方案检索命中后返回的是这个小块所在的5000字大章节上下文冗余。正确的思路是小块用来检索保证召回精度大块用来生成保证上下文完整。这就是后面结论二要展开的父子分块方案。所以块大小的调优一定要带上返回策略一起考虑否则你只是在优化一个局部最优。2.2 结论二小块召回加父块返回是性价比最高的分块组合我第一版方案用的是1200字的块命中后直接返回这个块hit_rate不错但答案忠实度只有0.80左右。分析badcase发现很多问题需要的信息跨越了块的边界比如用户问“固件升级后网络配置是否失效”答案的前半段在“升级注意事项”块里后半段在“网络参数说明”块里。单块返回必然缺信息。于是我把分块方案改成了父子双层结构。具体做法是先用Markdown标题结构和段落边界切出大块父块平均长度约1800字再把每个父块按句子边界切成小块子块平均长度约400字子块与父块保留映射关系。索引和检索都在子块上做命中的子块通过parent_id拉取完整父块作为上下文。这里的关键是维护一个双层的块ID映射表结构很简单父块ID: parent_001 ├── 子块ID: parent_001_child_001 ├── 子块ID: parent_001_child_002 └── 子块ID: parent_001_child_003检索时先搜子块拿到命中的子块ID列表后去查父块映射表把命中的子块对应的若干个父块合并、去重一起拼装进上下文。这个方案跑出来的结果hit_rate从0.87小幅提升到0.89MRR从0.71提升到0.74答案忠实度直接从0.80跳到0.86。为什么提升这么大因为子块检索命中更精准而父块上下文让LLM能看到完整的信息两全其美。需要注意的问题是父块返回会导致上下文变长多个父块拼在一起可能超过模型窗口。所以还要做一次父块裁剪只保留命中的子块所在段落的前后各一段或者用摘要压缩非关键区域。我建议父块数量上限控制在3个以内一旦超过3个就按相关性截断。这个方案对长文档场景尤其友好我后续在医疗器械售后文档上也验证过趋势是一样的。3. 召回环节的两个实战结论3.1 结论三hit_rate和MRR必须分开看召回才不会被假象欺骗很多RAG项目上线后只看一个指标要么看检索有没有命中要么只看用户满意的比例。这两个都太粗。我在召回环节最深的体会是hit_rate和MRR要放在一起看因为它们告诉你的是两个层面的问题。hit_rate告诉你“正确答案在不在候选集里”MRR告诉你“正确答案排在第几位”。如果hit_rate高但MRR低说明检索到的相关材料都在但排序很差你要优化的是排序不是召回如果MRR高但hit_rate低说明排在前面的少数几块是准的但整体覆盖面不够你要优化的是召回。我实测的命中分布可以说明这个差异。把100条测试问题按类型拆开后我记录了三种检索策略的hit_rate和MRR只用BM25稀疏检索、只用向量稠密检索、两者混合。数据如下检索策略hit_rateMRRBM25纯词法0.740.55向量纯语义0.830.68BM25 向量混合0.890.74纯词法检索的hit_rate明显低但在FAQ场景上它的MRR反而比向量检索要高因为FAQ里的问题和答案用词高度重合BM25的词法匹配天然占优。向量检索在语义泛化上更强比如用户问“设备嗡嗡响”实际对应文档里的“运行噪音异常”这种靠词法根本召不回。混合检索把两者的优势合并了hit_rate和MRR都有提升。但混合不是一个免费午餐它意味着你要写多路召回的融合逻辑还要忍受检索耗时的增加。具体融合我用的方法先跑BM25取top50再跑向量取top50两路结果用RRF倒数排名融合合并然后统一交给后面的重排阶段。RRF的公式是score sum(1/(k rank))k取60这是比较常用的配置不用调得很细。3.2 结论四top_k有边际效应拉大候选数不如优化排序top_k这个参数新手容易犯的错误是拉得很大觉得“多召回一些总没错”。我在固定其他条件的情况下把top_k从5逐步调到30观察hit_rate和生成质量的变化top_khit_rateMRR上下文有效密度估算50.680.62高100.780.70中150.810.71中低200.820.71低300.830.70极低看趋势很明显从5到10hit_rate一次性涨了10个百分点这是最大的增量10到15还在涨但趋缓15之后基本进入平台期30和20的差距只有不到1个百分点。但付出的代价是top_k拉到30之后后续重排的输入多了两倍耗时增加而且更重要的是无关的chunk一旦混进候选集重排模型看不过来排序质量反而下降。这里要理解一个原理稠密向量检索的本质是在整个向量空间里找近邻前10个近邻是真正语义相近的10到30这20个则很多是“看着有点关系但实际不相关”的模糊地带把它们拉进来只会增加噪声。所以我最终的配置是召回阶段top_k20重排阶段再砍到5到8个。20是为了给重排足够的选择空间5到8是为了控制上下文纯度。这个组合在耗时和效果之间比较平衡。我觉得有必要强调top_k不是一个越大越好的参数它存在明确的边际效应具体阈值取决于你的语料区分度。如果语料本身语义重叠严重平台期来得更早如果语料区分度高可以适当拉大到25左右。4. 重排环节的两个实战结论4.1 结论五重排的本质是第二轮检索不是简单排序很多人理解重排就是在召回结果上再用cross-encoder打一次分、按分数降序排列。这个理解没错但太浅。我在实际项目中体会最深的一点重排阶段的模型是query和passage的深度交互模型它比双塔式的向量检索敏感得多。双塔模型把query和passage分别编码成向量交互只发生在最后算相似度那一下而cross-encoder是把query和passage拼在一起送进模型做全量attention它能看到词与词之间的细粒度关系。所以重排模型对输入格式非常敏感你喂给它的query和passage是否经过加工直接影响打分质量。我做了个实验原始query直接喂重排和经过“Query改写”的重排MRR差了0.08左右。Query改写的动作是如果是反义问句就加前缀说明如果包含型号代号就把全称拼在缩写后面如果问题里明显缺了主语就补上。这些改写逻辑很简单用规则就能做但我发现很多RAG项目根本没做这一步直接把用户输入的原始文本扔进重排模型。重排模型的输入质量直接决定了第二轮的过滤能力。这一点想通之后我才真正把重排当成“第二轮检索”来设计它不只是在排序而是在用更强的模型重新过滤掉那些向量检索阶段误召回的相关性很弱的chunk。实验数据也很能说明问题配置MRR答案忠实度单次检索总耗时无重排直接top20送LLM0.710.80180ms有重排top20取top50.860.87520ms有重排top20取top80.870.86600ms重排前的MRR是0.71重排后直接拉到0.86这个提升比调整任何分块参数都显著。但注意重排是有时间开销的cross-encoder要跑真实的transformer推理top20个候选每一条都要和query拼接计算总耗时从180ms涨到520ms。我的建议是如果对延迟不敏感比如企业内部知识库助手重排是必选项如果是对外服务的实时问答至少也要让重排覆盖top20取top5这个档位的性价比最高。这里有一个很实用的技巧重排的结果可以做缓存。同一个query改写结果在24小时内命中的概率很高缓存键用“query改写文本的hash值”缓存的value就是排序后的chunk ID列表。实测缓存命中后耗时从520ms降到40ms左右效果完全一致。这个方法在日志系统里很好做而且能明显改善体验。4.2 结论六跨块去重比重排本身更影响答案质量这个结论是我在分析badcase时意外发现的也是六个结论里最容易被忽视的一个。现象是多跳问题的答案经常被描述得“啰嗦、重复、前后矛盾”。比如用户问“新版固件升级后有哪些配置需要重新设置”旧版本的配置说明和新版本的配置说明同时被召回重排模型给两者的分数都很高因为它们都和query相关。按理说这没问题但问题来了旧版说明里说“A配置不用改”新版说明里说“A配置需要改”两份内容同时出现在上下文里LLM极有可能把矛盾信息复述出来生成质量直接崩掉。我统计了一下在多跳问题上最终喂给LLM的top5 chunk里至少有2个在内容上是高度重叠的相似度超过0.85的概率接近四成。这说明跨块重复是常态不是偶发。解决办法是在重排之后加一道“内容去重”的工序把重排后的chunk两两算向量相似度超过0.85的只保留排名更高的那一个。伪代码大概是这样的流程去重(candidate_chunks, threshold0.85) 1. 按重排分数从高到低遍历候选块 2. 对每个块与已保留块依次计算向量余弦相似度 3. 相似度 threshold则跳过该块 4. 相似度 threshold则加入保留列表用我当时的语料跑出来的效果答案忠实度从0.86提升到0.88MRR倒是没怎么变但生成结果的重复率和矛盾率明显下降。原因在于LLM的注意力被重复信息分散了它会在多个相似的块里反复确认同一个信息点忽略了其他真正有价值的内容。去重之后上下文的有效信息密度提升了生成质量自然水涨船高。如果你遇到的问题是“回答内容冗长、重点不突出”而不是“答不对”先别急着换模型或重排序检查一下是不是上下文里有太多重复内容。当然去重阈值0.85不是绝对的。如果文档本身有很多“同一个操作在不同场景下的不同描述”阈值可以放松到0.90甚至0.92避免误伤有效信息。这个参数的调整最好是基于badcase回放不要盲调。5. 可以抄作业的落地配置与调参顺序5.1 一套默认配置参考表这一节直接给结论。以下是我在混合企业知识库场景下最终使用的配置覆盖分块、召回、重排三个环节同时也列出每个参数的建议调整范围和判断依据方便大家在自己的项目里做微调。环节配置项推荐值调整方向分块子块大小400字符文档结构清晰可加大到600FAQ类短文本可减到250分块父块大小1200到1800字符按标题和段落边界切不强制固定字符数分块子块重叠40到60字符保证重叠区域能覆盖一个完整句子分块父子映射必须维护子块ID关联父块ID上下文按父块返回召回稀疏检索BM25top50短文本FAQ场景必开召回稠密检索向量top50长文本、语义泛化场景必开召回融合方式RRFk60简单有效不敏感召回最终top_k20平台期阈值语料区分度低可降到15重排模型bge-reranker-v2-m3中文效果好英文可用cohere rerank重排候选集大小20太少重排没意义太多耗时线性涨重排最终保留数5到8上下文纯度优先多跳问题可放宽到10重排去重阈值0.85重复内容多的语料下调多样性高的上调生成上下文组装父块返回 去重后按重排顺序拼接父块裁剪上限3个这套配置最核心的思路是“小块检索、大块生成、重排兜底、去重提纯”四个环节互相配合。如果你只打算抄一半那我建议优先做两件事一是父子分块二是重排去重。这两个改动带来的提升最明显而且改动成本不高。5.2 从零到一按什么顺序调参调参顺序很重要。我的建议是严格按照“分块 → 召回 → 重排 → 生成”的顺序走不要跳级更不要反过来调。之所以固定这个顺序是因为前一个环节的错误会被后一个环节放大反过来也会掩盖问题如果你的分块质量很差你调再好的重排模型也没用如果你的召回top_k太小重排根本没有足够好的候选可以排。具体可以按下面这个路径走第一轮先固定一组保守参数分块800、overlap80、top_k10、不做重排把链路跑通记录baseline。第二轮优化分块测试子块大小梯度同时引入父子分块方案观察hit_rate和答案忠实度的变化。这一轮的目标是让hit_rate尽量接近0.85以上。第三轮优化召回在分块基础上加混合检索微调top_k观察MRR变化。这一轮结束后hit_rate和MRR都应该有明显提升。第四轮加重排引入cross-encoder重排这时你会发现重排模型会把前一轮召回里的噪声进一步过滤。这一轮重点看MRR和答案忠实度。第五轮加去重和Query改写这两步都是“精细加工”把答案忠实度从0.85推向0.90。最后一轮做延迟优化重排结果加缓存向量库索引加标量字段过滤把总耗时压回可接受范围。每一步只动一个变量不要同时调多个参数。另外每轮调完都要把badcase存下来方便下一轮回归测试。我自己的习惯是每轮调参后跑一遍全部100条测试集把新出现的失败case单独记录这样可以保证“修一个坏一个”而不是“修好一个坏两个”。如果大家嫌100条测试集太重可以按类型抽样30条但必须保证三类问题都有覆盖。6. 高频问题排查与避坑记录6.1 分块阶段的三个坑分块阶段最常见的问题有三个我挨个说全都踩过。第一个坑是段落被硬切导致语义断裂。纯按字符数切块切到一半时恰好把一个完整句子的主谓语切开了这个chunk的向量表示就会很怪里面是半截话。对策是切块时用句子边界做微调不要强制固定长度允许子块在设定值上下浮动20%。实际操作可以用简单的分句器先切句再按“凑到400字左右、尽量在句子边界断开”的逻辑组装。第二个坑是overlap设置太小。如果overlap只有10到20个字符很可能覆盖不到一个完整句子关键信息恰好落在重叠区外直接被截断了。我当时调overlap时对比过几个值结论是overlap至少要覆盖一个句子的平均长度也就是40到60字符。更稳妥的做法是重叠区域按“前一块的最后一句完整句子”来定义而不是固定字符数。第三个坑是表格和代码块没有单独处理。企业知识库里操作手册含大量参数表格SOP里有代码块。如果按普通文本切表格被切成两半代码块被切开语义完全破碎。这个问题的解法是分块前先用文档结构识别把表格和代码块单独标记出来表格作为一个整体块代码块作为整体块都不参与常规字符切分。用PyMuPDF或者LibreOffice的文档结构接口都能做重点是结构识别这一步不能省。6.2 召回阶段的三个坑召回阶段的问题更容易隐蔽因为日志里看起来“有返回结果”但实际上返回的东西不对。第一个坑是Query太长导致向量语义分散。用户有时会粘贴一长段问题描述这段描述里有大量无关信息Embedding出来的向量被无关词带偏。对策是在召回前做一个Query压缩用规则或小模型把长问题压缩成30字内的核心检索词。我试过正则抽取关键词、提取实体、去掉语气词和形容词效果最稳的还是结合业务词典的关键词抽取这个方案可控且不引入额外延迟。第二个坑是metadata过滤条件写错、滤掉了正确结果。向量库通常支持在检索时加filter字段比如只看某一类文档。但filter字段的类型必须和文档里的一致。我遇到过字符串类型的版本号被当成数值类型过滤导致该版本的操作说明全部被滤掉hit_rate直接掉到0.3以下。排查方法很简单出现大量“明明有答案但检索不到”的case时先把filter去掉同一条query再跑一次对比召回结果。如果去掉filter后正常问题一定出在filter逻辑上。第三个坑是混合检索的融合权重没有调好。RRF方法本身对权重不太敏感但有些项目用的是加权线性融合稀疏和稠密的分数量纲不统一会导致一路召回被无视。一定要先对两路分数做归一化再融合或者直接用RRF。RRF的好处在真实数据上非常明显不需要调权重不会出现某一路被完全压制。6.3 重排阶段的三个坑重排阶段的问题集中在“参数不当”和“输入不当”。第一个坑是为了延迟把重排候选集砍得太小。有人为了省时间只对top5做重排这其实失去了重排的意义。因为前5个本来就是向量检索认为最相关的重排对它们做的是微调真正的价值是把第8到第20名里被低估的相关块捞上来。如果候选集只有5个重排的收益几乎为0。我实测下来重排候选集最好不要低于10个20个是性价比最高的档位。第二个坑是重排时用了截断后的长文本。cross-encoder虽然能处理长文本但超长文本会稀释注意力末尾的段落经常被忽略。必须对父块做提取式压缩命中的子块所在段落完整保留其他段落只保留首句或关键句。我在做这个优化前多跳问题的MRR只有0.79压缩后涨到0.84。原因就是重排模型终于能“看清”长文本里的重点了。第三个坑是忽略了重排结果的数据新鲜度加权。版本变更记录这类文档有个特点越新的版本越可能包含正确答案。纯靠语义重排新旧版本的块会被平等看待导致旧版本信息优先返回。我的解法是在重排分数上叠加一个时间衰减因子。比如新版块加0.05的权重旧版块减0.03。这个加权幅度不需要很大但实际效果非常明显——版本类问题的命中率能提升10个百分点以上。7. 一点个人体会我把这6个结论放到真实业务里跑了一个多月最大的感受是RAG调优没有银弹但确实有一个“收益排序”——重新设计分块结构和引入第二轮检索重排去重带来的提升远远大于换更强的LLM。很多团队遇到RAG效果不好第一反应是换更大的模型或者调prompt但真正的瓶颈往往在更早的环节。你喂给模型的上下文如果本身就有信息缺口和噪声再强的模型也答不出正确答案。另外一个很实在的建议一定要建立badcase回放机制。每次算法改动后把新产生的坏case和旧坏case放在一起逐条看它们是被哪个环节“弄丢”的。调优不是在实验室里试参数而是在现场找共性。这套机制跑起来之后改进方向会非常清晰而不是靠感觉调参。后续我打算在Query改写和答案校验两个方向继续投入当前这套链路在“找到资料但生成失败”的case上已经收敛得不错了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进