资讯详情

资讯详情

混合检索+重排序:RAG系统召回精度优化全指南

做检索系统的同学多少都有这种体会单路召回无论拼得多快精排前如果底料不行后面全白费。尤其现在RAG应用越来越普及文档检索的质量直接决定了大模型输出的质量用户问“怎么办”模型答得靠不靠谱一半取决于检索阶段有没有把真正相关的上下文捞回来。这篇我会从相似度算法讲起把混合检索Hybrid Search和重排序Rerank这类“召回后处理”的完整链路过一遍不光讲每个算法怎么算、怎么对比重点是讲清楚在什么业务场景下选什么方案、工程上到底怎么落地。内容会按“整体架构 → 相似度算法 → 混合检索 → 重排序 → 常见问题排查”这条线展开适合正在搭搜索系统、做RAG应用、或者想优化召回精度的开发者和算法工程师参考。从前置基础到代码级细节我都尽量讲透工具选型上会给你一个可以照着画的框架。1. 检索系统的整体架构召回阶段与重排序阶段的分工逻辑1.1 为什么检索要被拆成两段式漏斗几乎所有工业级的文本检索系统内部都不是一个模型打天下而是“粗召回 精排序”的两段设计。原因不难理解候选文档可能是几百万、几千万条任何一个复杂模型如果要对全量文档逐条打分延迟和算力都扛不住。所以第一步必须用低成本方式快速圈定一个很小的候选集合常见的规模是几十条到两三百条第二步再对这个集合用更精细、更智能的方式重新打分。召回阶段的典型手段有两类。一类是倒排索引加BM25本质是稀疏词法匹配快速、精确、可解释性强工程上非常成熟。另一类是稠密向量检索把文本编码成向量用最近邻搜索的方式找语义上最接近的内容能解决“写法不同、意思相同”这类词法匹配搞不定的事。这两类召回各有各的死穴所以现在的主流架构往往让两路同时跑取完再合并。重排序阶段则完全不同。它的输入是已经被粗筛过的几十条内容规模不大所以可以上参数量更大的模型比如Cross-Encoder。这个模型会把用户查询和候选文档拼接在一起做深度交互打分比双塔/向量召回精细得多最后再把真正最相关的前几条送进下游。这里的核心思想就是“漏斗逐层变窄、精度逐层变高”每一层都用适合当前规模的手段而不是用一个万能模型硬扛所有数据量。1.2 两个阶段对算法能力的要求差异召回和重排对算法的要求是截然相反的理解这一点很多技术选型就不会纠结了。召回阶段的目标是高召回率宁可多捞一些不相关的噪声也别漏掉真正相关的内容。这个阶段对延迟极其敏感通常要求在几十毫秒内返回结果所以模型要大而快复杂度必须可控。向量检索常用近似最近邻搜索ANN不是完全精确的最近邻本质就是用可接受的精度损失换速度。重排序阶段的目标是高精度要把召回的噪声尽量过滤掉把真正相关的几条顶到最前面。这个阶段候选集合很小对单条的延迟容忍度可以放宽到几十甚至几百毫秒所以模型完全可以做得更“重”代价是只能在小规模候选上使用。Cross-Encoder每处理一个候选对都要做一次前向计算复杂度随候选数线性增长所以无论如何不能放到全库上跑。维度召回阶段重排序阶段候选规模百万级以上几十条以内核心目标高召回率高精度常用算法BM25、向量检索、双塔Cross-Encoder、精细排序模型延迟要求严苛毫秒级宽松可到百毫秒级特征复杂度低不能依赖复杂交互高可以做深度语义交互计算开销低较高这里我提一句个人经验很多团队把精力全花在重排序模型上觉得模型越强结果越好。但实际项目里如果召回阶段就把真正相关的文档漏掉了重排序再怎么精排也排不出空气里的东西。所以设计阶段一定要分清楚每一层的职责召回保证“不丢”重排保证“要准”两个环节互相配合而不是互相替代。2. 相似度算法全景从稀疏到稠密的核心公式与选型2.1 稀疏匹配从TF-IDF到BM25先说说最经典的稀疏文本匹配。它的核心是“两个文本里有共同的关键词”关键词重叠越多得分越高。TF-IDF是入门必讲的算法公式拆开也就两部分TF表示词在文档里出现的次数代表这个词对这篇文档有多重要IDF表示“包含这个词的文档占比的倒数”代表这个词在全库里有多稀缺。分到具体计算上一个词的权重就是这个词在文档里的词频乘以这个词的逆文档频率最终文档得分是查询里所有词的权重之和。但TF-IDF有个明显问题词频增长是线性的一篇文章里同一个词出现很多次得分会被不断抬高可实际上一旦某个词出现超过一定次数它对主题的贡献就不该继续等比例增加了。另外它也没考虑文档长度——长文档天然包含更多词更容易撞上查询词这不公平。BM25就是为了解决这些问题出现的。它的核心改进了两点第一用“词频饱和函数”让词频的作用到一定程度后不再线性增长而是逐渐趋缓第二引入文档长度归一化用当前文档长度和全库平均长度的比值来调节词频权重避免长文档占便宜。BM25的通用形式可以写成这样score(D, Q) Σ IDF(q_i) × [ tf(q_i, D) × (k1 1) ] / [ tf(q_i, D) k1 × (1 - b b × |D| / avgdl) ]其中k1控制词频饱和的程度取值一般在1.2到2.0之间值越大词频对分数的贡献越线性b控制文档长度归一化的强弱默认通常取0.75。调参的时候要注意一个细节对长文档居多的语料库适当调大b能更狠地压制长文档优势对短文本为主的数据b调得太高反而会误伤。实际工程中BM25能被落地本身还要依赖倒排索引的能力。系统先对文档做分词、去停用词、转小写建立“词项→包含它的文档列表”的映射。查询时先查倒排表找到可能相关的文档ID再逐个计算BM25分数。性能上它比遍历全库快了不只一个数量级所以到今天它依然是很多工业搜索引擎的默认召回算法。2.2 稠密向量匹配余弦相似度、内积与欧氏距离向量检索的核心思路是把文本用嵌入模型转换成固定维度的向量然后在向量空间里比较距离。最常见的三种度量是余弦相似度、内积和欧氏距离。余弦相似度计算的是两个向量之间的夹角余弦值公式是向量点积除以两个向量模长的乘积。它的特点是只关心方向不关心模长所以两个文本只要“面对的方向”接近哪怕一个向量整体更长、更“重”余弦相似度也可能很高。文本本身受句子长度影响很大所以在文本检索里余弦相似度几乎是默认选择。内积就是两个向量对应元素相乘再累加它没有做任何归一化。如果两个向量都已经做了L2归一化也就是单位向量那么内积和余弦相似度是完全等价的。但如果没有归一化内积会同时把模长信息混进来在文本场景下模长通常和句子长短、词频高低有关这往往不是我们想要的语义信号。所以用内积之前一定要先确认嵌入模型的向量是否归一化很多预训练模型的输出不是单位向量直接用内积会踩坑。欧氏距离计算的是向量在空间中的真实直线距离和余弦相似度呈负相关关系向量越相似距离越小。当两个向量都是单位向量时欧氏距离和余弦相似度存在一个平方关系只是单调映射不影响排序结果。但在工程实现中大多数向量数据库和索引库针对内积或余弦做了优化欧氏距离要多一步平方根运算性能上略吃亏所以实际用得少一些。这里我想强调另一个容易被忽略的点向量分数绝对值没有跨库可比性。同一个模型对不同库、不同批次数据算出来的余弦相似度分布都可能不同稳定的做法是回归到“看排序”而不是“看分数阈值”。如果非要用分数做规则也要先在自己数据集上统计分布再决定阈值千万不要套别人文档里的“0.7以上才相关”这种结论。2.3 相似度算法怎么选一个简单的判定框架很多人在选相似度算法时容易陷入“万物皆向量”的误区。我的建议是先给你的查询和文档做个分类分类下再选算法这样更稳。场景特征推荐算法原因查询包含明确型号、编号、代码、专有名词BM25优先这类查询要求精确匹配向量模型对罕见词容易编码漂移查询是短句和文档存在同义改写、口语化表达稠密向量词法匹配覆盖不到“同义不同形”文档是长文本混杂大量无关内容BM25向量混合单一算法容易被长文档噪声干扰对召回结果可解释性有要求BM25或稀疏算法可以追溯“是哪个词命中了”方便排查搜索量大延迟敏感BM25ANN向量召回并跑两路都很快重排阶段再精细打分补充一个真实感很强的例子。用户搜“怎么把套餐流量转到下个月”如果文档里写的是“套餐内剩余流量不可结转”BM25能精确命中“流量”“下个月”这些词但“转到下个月”和“结转”没有表面上的重合所以纯BM25可能匹配不上反过来纯向量检索也可能因为“套餐”“流量”出现太频繁而把弱相关的文档排前面。这种场景只有两路结合才稳妥这也自然引出了后面的混合检索。2.4 工程实现中的几个细节第一个细节是分词的一致性。中文场景下索引分词和查询分词必须用同一套配置否则同一个词在两边被切成不同粒度BM25直接失效向量检索虽然不受影响但输入不一致也会导致语义偏移。把分词器配置统一成公司内部同一个版本是很基础但很有必要的规范。第二个细节是多字段加权。文档往往有标题、正文、关键词等多个字段标题里出现的查询词和正文里出现的查询词重要性完全不同。工程上可以在计算BM25时对字段设置不同权重比如标题权重3.0、正文权重1.0甚至可以把字段加权建模成倒排索引里的boost因子。这一步对最终精度的提升往往比换一个重排序模型更明显。第三个细节是向量维度和精度的取舍。常见的嵌入模型输出是768维或1024维高维度在存储和计算上都有成本。做索引前可以先做PCA降维或者用训练好的降维矩阵压到256维很多时候精度损失很小但性能提升明显。存储精度上用float16代替float32在大多数场景下都能接受内存占用直接砍半。3. 混合检索Hybrid Search的融合机制3.1 为什么单路检索总是不够用单路检索的局限我再展开说。BM25这类稀疏算法本质是词法匹配它不了解词之间的语义关系。用户说“取消业务”文档写的是“退订服务”两个句子含义一样但字面差异巨大BM25得分可能为零。反过来向量检索依赖嵌入模型把文本映射到向量空间但嵌入模型对数字、代码、生僻专有名词、稀有人名的处理并不稳这些恰恰是最需要精确匹配的内容。把“Error Code 90210”映射成一个向量很容易和别的错误码混淆但BM25看到“90210”就是字面完全匹配。两个检索通道的错误模式是互补的一个擅长精确词匹配一个擅长语义近似匹配。所以混合检索不是可选项而是精度要求较高时的必然选择。比较好的比喻是BM25像一个严谨的图书管理员只认书单上的原词向量检索像一个泛读的推荐系统知道“你喜欢的书可能和这本书风格相近”。管理员漏掉的书推荐系统往往能顶上。3.2 RRF基于排名的融合算法混合检索面临一个很实际的问题BM25的分数和向量相似度分数根本不是一个尺度直接把两个分数相加没有任何意义。BM25可能打出0到20的分数余弦相似度一般在0到1之间维度不同、分布不同直接加权很别扭。于是排序融合算法RRFReciprocal Rank Fusion被广泛采用。它的思路非常巧妙既然分数不可比那就忽略具体分数只关注每个文档在两个通道里的排名。RRF给每个文档的融合得分定义为所有通道倒数排名的累加score(d) Σ 1 / (k rank_i(d))这里的rank_i(d)是文档d在通道i中的排名k是一个平滑常数通常取60。加k的目的有两个一是防止排名为第一的文档得分过高让第一名地位太“霸权”二是对排名靠后的文档仍然赋予非零分数保留“出现即有价值”的语义。举个例子假设k60文档A在BM25通道排第1在向量通道排第10那么它的RRF得分是 1/(601) 1/(6010) ≈ 0.0297。文档B在两个通道都排第5得分是 1/65 1/65 ≈ 0.0308。虽然A有一个第一名但B同时在两路都有较好的排名综合得分反而更高。这个特性非常适合混合检索真正优质的文档通常会在多个通道里都有较好表现而只在单一通道靠前的文档可能是过拟合了某个通道的偏好。RRF的实现特别简单伪代码就这么几行def rrf_fusion(rankings, k60): scores {} for single_ranking in rankings: for rank, doc_id in enumerate(single_ranking, start1): scores[doc_id] scores.get(doc_id, 0) 1.0 / (k rank) return sorted(scores.items(), keylambda x: x[1], reverseTrue)实际操作时两个通道各自取top 100或top 200做完RRF融合后再取top 30往下游走。这个流程稳定、可解释、调参少所以它成了我现在做混合检索的首选方案。3.3 加权分数融合可控但调参真麻烦加权分数融合是另一种思路公式很简单score(d) α × s_bm25_norm(d) β × s_vector_norm(d) ...关键在“norm”上。两个通道的原始分数必须先归一化到同一尺度否则权重调了也白调。常用的归一化方法有三种Min-Max归一化(score - min) / (max - min)把分数映射到0到1区间。但线上检索时你没有全局最小值和最大值通常得用离线统计的近似值或者用当前候选集里的最大最小值做在线近似。Z-score归一化先算整个分数分布的均值和标准差再映射成标准正态分数。适合分数整体偏移明显的情况。除以最大值直接压缩到0到1但会忽略分数分布的形状只适合粗略场景。加权融合最大的问题在于权重不好确定。α和β如果定得不合理融合后的排序还不如单通道。比较靠谱的做法是准备一批人工标注的查询和期望文档线下跑网格搜索比如α从0.1试到0.9步长0.1找一个离线指标最优的组合。这个方案比起RRF多了一个需要维护的评测流程但好处是融合之外还能加业务信号比如文档热度、时间衰减、用户点击权重都可以作为额外的加权项一起加进去。一条实践建议如果你的系统刚起步没有标注集直接用RRF就够了如果已经有了相对成熟的评测集而且要融合的信号很多加权分数融合更灵活。3.4 混合检索的工程落地落地时要考虑的工程细节不止是算法本身。第一两路检索是并行还是串行。并行能压低延迟但需要多占用资源串行比如先跑BM25如果命中数不足再用向量补召回能省算力但要保证BM25出问题时兜底逻辑及时生效。我在实际项目里一般建议先并行跑等量级稳定后再看是否需要优化资源。第二各通道的topK设置。两个通道如果各取100融合后再取30会比各取50融合后取30稳定很多因为排名靠后的信息也能参与融合。但也不能各取太深比如各取500以上慢一点不说排名太靠后的噪声反而会稀释融合效果。第三缓存。同一查询短时间内重复出现的频率很高尤其To B场景里很多用户的检索词高度集中。把最近一段时间的查询结果缓存起来融合结果也一起缓存能大幅降低计算压力。工程上还有一个趋势值得关注开源搜索引擎最近的版本已经原生支持RRF了部分向量数据库也开始内置混合检索和重排序的接口。也就是说如果不想自己手写融合逻辑直接用现成的复合查询能力也能达到相似效果只是自己实现一遍更容易把控细节和排查问题。4. 重排序Rerank技术深度解析4.1 从双塔到交叉编码精度与效率的取舍重排序为什么能够比召回更准关键在于“交互早晚”的差异。用来做召回的向量模型大多是双塔结构也叫Bi-Encoder。它把query单独编码成一个向量把document也单独编码成一个向量两个向量完全独立最后用余弦相似度去匹配。双塔的优点是doc向量可以离线全部算好存进向量库线上只需要实时编码query再去查库延迟低、吞吐高。但缺点是query和document在编码时根本没有见过对方所有语义交互只能发生在最后的向量比较阶段信息损失很重。Cross-Encoder则完全不同。它把query和document拼接成一个长序列塞给模型比如[CLS] query [SEP] document [SEP]模型内部的注意力机制会让query中的每个词看到document里的每个词所有交互都在模型内部完成。这种深度交互使得模型能捕捉到非常细粒度的匹配信号精度高出一大截。代价是每条候选对都要单独算一次前向query有30个候选文档就要算30次完全没法做全库检索。一个形象但不严谨的类比双塔相当于人力初筛扫一眼简历的学历、年限、技能标签快速把人分成“值得面试”和“不用浪费时间”Cross-Encoder是真正的面试官会针对候选人的具体经历提问判断他到底适不适合这个岗位。初筛阶段人太多只能看标签到了面试环节人少了才有条件深挖。4.2 重排序模型的工程实现重排序在工程上并不复杂核心是几个可复用的套路。第一步是控制重排候选集的大小。常见做法是召回阶段取top 20到top 50重排后取前5到前10。如果候选集太大比如超过100条重排延迟会很难看而且top 100里本身已经混入太多低质量结果重排模型也很难把它们“救活”。先有召回质量再有重排质量这个顺序不能倒。第二步是输入构造和长度控制。query和document拼接后大多数Cross-Encoder模型有最大输入长度限制比如512个token。文档太长直接截断会丢掉尾巴上的关键信息比较稳妥的做法是取文档开头和结尾的各一段拼起来或者对超长文档先做段落切分把段落级分数聚合为文档级分数。第三步是批量推理优化。虽然单条延迟容忍度放宽了但同一查询下的多条候选可以组成一个batch一起推理把吞吐拉上去。要注意候选文档长度差异可能很大如果按最长文档填充整个batchpadding浪费非常多。按文档长度分组batch能明显提速。第四步是重排分数怎么用。有两种主流做法一是直接用重排模型的分数作为最终排序依据二是把重排分数和召回阶段的BM25分数、向量分数做加权融合公式类似前面说的线性组合。用第二种方案的动机是重排模型也是可能过拟合训练集的融合召回阶段信号能起到正则化作用我用下来的感觉是数据量小、标注质量不高的场景融合更稳数据量大、标注质量高直接用重排分数更干净。4.3 Rerank 在 RAG 场景中的位置RAG的完整链路大致是文档解析 → 切块 → 向量化 → 索引 → 召回 → 重排 → 拼上下文 → 大模型生成。很多人把注意力都放在向量化和大模型提示词上但重排这一步其实是决定生成质量的关键“闸门”。原因很直观。RAG拼给大模型的上下文通常只有几段到十几段如果这些片段里混入了不相关的噪声大模型不会自动忽略它们反而可能在生成时被带偏。更麻烦的是大模型对上下文长度有上限塞进去的内容越脏留给有效信息的空间就越少。重排把最相关的片段顶到最前面同时过滤掉低相关片段直接改善了大模型的输入质量。我自己的经验是对一个20条候选的召回集重排后只取前5条拼上下文比直接把20条全部塞给大模型的效果好很多。不只是因为相关内容集中了更是因为垃圾上下文的比例大幅下降大模型的回答更聚焦、事实性错误也更少。另外重排还有一个容易被忽略的作用它是RAG全链路里唯一一个能对“相关性”做最终把关、且不依赖大模型本身判断的环节。如果召回漏了重排救不回来但召回不精确时重排能把最后的精度拉上来一截。放在整个系统里看它的性价比非常突出。4.4 重排与业务规则的融合纯模型打分之外业务场景里往往还要叠加一些非语义信号。比如新闻搜索讲究时效性再相关的文章如果是三年前的价值也大打折扣电商搜索要考虑商品销量和好评率企业内部知识库搜索可能要显示文档被访问的频率。比较通行的做法是做一个final scorefinal_score λ1 × rerank_score_norm λ2 × recency_norm - λ3 × uncertainity_penalty这里的重排分数和业务信号都要先各自归一化到同一个尺度否则业务信号再强也会被模型分数淹没。取λ的权重时同样建议离线评测确定而不是拍脑袋。我踩过的坑是有一段时间把时效性权重调得过重导致系统把大量“时效性强但不相关”的结果顶上去了用户体验直线下降。后来把时效性限制做成一个硬过滤条件而不是软加权问题才解决。5. 实测中的常见问题与排查技巧5.1 召回为空或分数普遍偏低特殊查询在BM25下命中为空最常见的原因是分词不一致或停用词把查询词给吞了。比如用户搜“维生素C”里的“C”被当成单字过滤“iPhone 15 Pro”里“Pro”没有被索引分词器切出来。排查方式是直接看这轮查询的分词结果确认查询词和索引词是否对得上。向量检索召回为空的情况相对少见但会出现“召回非空、分数普遍偏低”的现象。如果所有候选的余弦相似度都在0.3到0.5之间先别急着怀疑模型有问题很可能只是当前业务的文档和查询本身相关性就偏弱。这时候可以拉出一批人工确认过“确实相关”的文档统计它们和对应查询的分数分布对比一下就能判断模型有没有问题。千万不要不加分析地设置余弦相似度阈值比如低于0.7全过滤掉。不同模型、不同领域数据的分数分布差异极大一个对某领域合适的阈值换一个领域可能把所有结果都过滤光了。要设阈值也得基于自己数据集的统计分布来设。5.2 RRF的k值与融合权重怎么调RRF的k值看起来只有一个参数调起来其实有讲究。k越小第一名的权重相对越高融合结果容易被单一通道的头部结果主导k越大排名差异的作用会被稀释几乎所有参与融合的结果都能获得比较接近的基本分。经验上k60是个很稳的起点但不同数据量、不同通道质量最优k可能差很远。我建议把k在10、30、60、100这几个档位各测一遍每次跑同一个评测集看指标变化。加权融合的权重调试更麻烦一些。如果你的通道质量差异明显——比如向量通道明显比BM25准可以把向量权重调到0.7以上如果两个通道各有各的优势场景0.5/0.5然后微调更合理。无论怎么调前提都是有一份靠谱的标注评测集。没有评测集就调权重基本等于盲调上线后只能靠运气。另一个容易踩的坑是做了加权融合后忘了给BM25分数做归一化。BM25分数受文档长度和词频影响很大两个通道分数直接相加后向量分数可能会被BM25分数的波动完全淹没。归一化这一步不能省。5.3 重排模型推理太慢重排模型推理慢常见原因和优化手段我整理一下候选集过大top100以上重排基本不建议降到top20或top30会快非常多。输入过长最长的候选文档占满了整个batch的最大长度大量padding浪费算力。解决方法是按长度分组batch或者把超长文档截断到512 token以内。模型过重Cross-Encoder模型本身很大如果用训练好的小号模型或者做INT8、FP16量化推理速度能提升数倍。量化对重排分数的影响通常不明显实测下来排序相关性损失极小。CPU跑大模型如果资源紧张只能上CPU至少用支持加速指令集的版本再配合小batch能把延迟控制在可接受范围内。还有一个取巧的思路如果query相似度很高且候选集变动不大可以把重排结果做缓存同时用一个轻量的“快速重排模型”先粗排一轮只拿它认为可能相关的10条再上大模型重排。两级重排虽然在架构上多了一层但整体延迟反而更稳。5.4 一个端到端的检索Pipeline示例把前面所有内容串起来一个典型的混合检索重排流程大概长这样def search_pipeline(query, top_k_recall100, top_k_rerank20, top_k_final5): # 1. 两路召回并行 bm25_results bm25_search(query, top_ktop_k_recall) # 稀疏通道 vector_results vector_search(query, top_ktop_k_recall) # 稠密通道 # 2. 融合先用RRF得一个粗排结果 fused_results rrf_fusion([bm25_results, vector_results], k60) # 3. 取粗排前N条进重排 rerank_candidates fused_results[:top_k_rerank] rerank_scores cross_encoder_rerank(query, rerank_candidates) # 4. 和业务信号融合后取最终结果 final_scores [ (doc_id, 0.8 * rerank_score 0.2 * business_score(doc_id)) for doc_id, rerank_score in rerank_scores ] final_results sorted(final_scores, keylambda x: x[1], reverseTrue)[:top_k_final] return final_results这段代码看起来简单但它把两个阶段、三类算法的核心逻辑都串起来了。实际项目里你需要替换的是每一行背后的实现BM25用倒排索引还是开源搜索引擎接口向量检索用哪个向量库和索引类型重排模型用哪一版Checkpoint。但整体架构和流程是一致的照着这个框架填细节就行。最后再分享一个我反复踩坑后总结出的认知检索质量的提升从来不是靠某一个环节突然变强实现的而是靠“每一层少丢一点、多排准一点”累积出来的。BM25分词调好能涨一点向量模型换好一点的能涨一点混合检索的融合策略调对能涨一点重排模型再精排一轮又能涨一点叠加起来最终效果往往远超换一个大模型的收益。这个体系没有银弹但每一步都有实实在在的优化空间只要肯下功夫去测量、去迭代结果一定对得起投入。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →