RRF 和 Reranker 分别解决什么问题?为什么检索后还要重排?
一句话先说清:**RRF 根据多路检索结果中的名次做融合;Reranker 重新判断“这个候选内容对当前问题到底有多相关”。**它们可以接在同一条 RAG 链路里,但解决的是两个不同的问题。
一、先看一个“检索到了,却没排好”的问题
假设学校有一套竞赛报销知识库。用户问:
参加校外竞赛,住宿费能报销吗?需要准备什么材料?
系统同时使用 BM25 关键词检索和向量语义检索,得到下面两份名单:
| 名次 | BM25 结果 | 向量检索结果 |
|---|---|---|
| 第 1 名 | A:住宿费报销通用流程 | C:校外竞赛住宿费的适用条件与材料 |
| 第 2 名 | B:差旅费用审批入口 | A:住宿费报销通用流程 |
| 第 3 名 | C:校外竞赛住宿费的适用条件与材料 | D:校内活动费用说明 |
这是一组为了讲解而构造的示例数据。A 反复出现,看起来很有希望;但用户问的是“校外竞赛”的具体条件,C 也许更直接。怎么综合两路结果,又怎么判断 A 和 C 谁更适合回答?这就轮到 RRF 和 Reranker 分别出场了。
二、RRF:先把多份排名合成一份
BM25 和向量检索给出的原始分数通常不是同一把尺子:一个可能是词项匹配分数,另一个可能是向量相似度。直接把两种分数相加,数值范围和含义都可能不匹配。
RRF(Reciprocal Rank Fusion,倒数排名融合)绕开原始分数,使用“名次”融合常见写法是:
RRF(d)=∑i=1m1k+ri(d)\operatorname{RRF}(d)=\sum_{i=1}^{m}\frac{1}{k+r_i(d)}RRF(d)=i=1∑mk+ri(d)1
其中,ddd是同一个候选文档或片段,ri(d)r_i(d)ri(d)是它在第iii路结果中的名次,名次从 1 开始。某一路没有返回它,这一路就不加分。kkk是让名次差异更平缓的常数;下例取 60。这里的kkk不是“取前 k 条”的 top-k。
按上表计算:
| 候选 | BM25 名次 | 向量名次 | RRF 分数(k=60k=60k=60) |
|---|---|---|---|
| A | 1 | 2 | 1/61+1/62≈0.0325221/61+1/62\approx0.0325221/61+1/62≈0.032522 |
| C | 3 | 1 | 1/63+1/61≈0.0322661/63+1/61\approx0.0322661/63+1/61≈0.032266 |
| B | 2 | 未出现 | 1/62≈0.0161291/62\approx0.0161291/62≈0.016129 |
| D | 未出现 | 3 | 1/63≈0.0158731/63\approx0.0158731/63≈0.015873 |
融合后的顺序是A → C → B → D。A 在两路结果里都靠前,因此得到较高分。注意,RRF 只知道 A 和 C 的排名,并没有读懂“校外竞赛”这个限定条件。
下面是一段不依赖第三方库、可以直接运行的 Python 示例:
fromcollectionsimportdefaultdict rankings={"BM25":["A","B","C"],"Vector":["C","A","D"],}rank_constant=60scores=defaultdict(float)forresult_listinrankings.values():forrank,doc_idinenumerate(result_list,start=1):scores[doc_id]+=1/(rank_constant+rank)fordoc_idinsorted(scores,key=lambdax:(-scores[x],x)):print(doc_id,f"{scores[doc_id]:.6f}")运行结果:
A 0.032522 C 0.032266 B 0.016129 D 0.015873实际工程中要用稳定的doc_id或chunk_id标识同一份内容。否则,同一个片段在两路检索中使用了不同 ID,系统会把它们当作两个候选,既无法正确累计排名贡献,也不便于去重和追踪来源。
三、Reranker:再判断哪些候选真正切题
现在系统已经有了 A、C、B、D 这份候选名单。下一步,可以把用户问题与每个候选片段分别配对,交给重排模型打相关性分数,再按新分数排序。例如:
(问题, A:住宿费报销通用流程) → 相关性分数 (问题, C:校外竞赛住宿费的条件与材料) → 相关性分数 (问题, B:差旅费用审批入口) → 相关性分数如果所选模型能识别问题中的“校外竞赛”“能否报销”“需要什么材料”,C 就可能排到 A 前面。这里说“可能”,因为实际顺序取决于候选内容、模型能力和测评结果,不能凭文章中的示例保证真实模型一定这样输出。
Reranker 负责重新打分,不是重新检索
常见的 Cross-Encoder 重排模型会同时读取问题和候选文本,对这一对输入评分。它处理的是前面已经找出来的候选,通常不会自己返回知识库里一段从未召回过的新材料。因此:
召回负责“先找到哪些材料”;RRF 负责“合并多路名单”;Reranker 负责“在候选名单里重新排优先级”。
Reranker 也不是万能的:如果“校外竞赛住宿费的规定”根本没被任何检索器召回,它就无法凭重排找回这份缺失的资料。此时应该检查索引、切分、召回策略或查询改写,而不是只调重排参数。
Reranker 发挥作用的前提:检索阶段至少召回了相关证据。即使 这条证据暂时排在后面,重排模型仍有机会把它提到前面。
四、两者放在一起看
| 问题 | RRF | Reranker |
|---|---|---|
| 输入是什么 | 多路检索得到的有序候选列表 | 问题与一批已召回的候选内容 |
| 主要看什么 | 候选在各路列表中的名次 | 问题与候选内容的相关性 |
| 主要解决什么 | 合并不同检索器的排名 | 提高候选列表前部的切题程度 |
| 需要模型逐条读候选吗 | 不需要 | 常见模型重排方案需要 |
| 能找回从未召回的材料吗 | 不能 | 不能 |
| 常见代价 | 计算相对轻量 | 随候选数量和文本长度增加推理耗时 |
一个常见流程是:
用户问题 → BM25 / 向量检索分别召回 → 按片段 ID 合并、RRF 排序、去重 → 截取一批候选交给 Reranker → 选出用于回答的证据 → 生成答案并标注来源为什么不把知识库所有片段都交给 Reranker重排模型通常要对每个“问题—片段”对进行计算。让快速检索先缩小范围,再对少量候选精排,更容易控制延迟和计算成本。若使用的是基于大语言模型的重排方法,还要额外考虑模型调用和 token 成本。
注意:不能把所有 Reranker 的成本都简单归结为 token。
Reranker 的代价主要取决于使用方式:本地模型要考虑算力和延迟;外部服务要考虑调用费用;用大语言模型重排时,还要特别关注 token 消耗。
五、加上 Neo4j 后,是不是自动变成“三路 RRF”?
不一定。这里很容易把“三路召回”和“三路都参与同一个 RRF”混为一谈。
例如一个健身知识图谱里有:
(硬拉)—[主要训练]→(臀大肌) (硬拉)—[主要训练]→(腘绳肌)Neo4j 图查询可能直接返回实体、关系和路径;BM25 与向量检索返回的却可能是原文片段及其chunk_id。如果图查询只返回两条关系,而没有与文本候选对应的 ID 和明确的排名,就不能直接照搬上面的代码,宣称“三路片段已经做了 RRF”。
有两种可设计的接法:
- 图谱单独补充证据文本检索的两路候选做 RRF;图查询结果整理成可读事实,与文本证据一起进入后续校验和生成。这是一种“三路获取信息、文本两路做 RRF”的方案。
- 把图结果转成可排序的证据项给图路径关联来源片段、稳定 ID 和明确的图检索排名,再设计统一的候选表示,之后才考虑将图结果纳入融合或重排。这里需要处理同一证据重复计分、图路径与原文的对应关系等问题。
图谱能补充“动作—身体部位”这样的关系,但实体和关系本身未必包含原文条件、例外情况或可靠引用。如果回答需要给出依据,最好保留图谱事实与来源文档、片段 ID 之间的关联。
上面是两种设计选择,并不是对某个具体项目实现的断言。写项目博客时,应以实际代码和检索日志为准,明确写出哪几路参加了 RRF、图结果在什么阶段进入上下文。
六、什么时候值得加 Reranker?
当候选已经能找对大致主题,但前几条经常忽略问题里的关键限定条件时,重排值得尝试;例如“校外”与“校内”、“现行规定”与“旧版规定”被混排。
如果系统的问题是正确材料根本召不回来,应先提高召回覆盖率。如果候选与答案都正确,只是响应太慢,则需要测量重排耗时,减少进入精排的候选数或选择合适的模型。最终效果应使用一组有标准答案的问题来检验,例如观察正确证据进入前几名的比例,以及答案是否真正依据证据。
总结
- RRF 看名次:适合把 BM25、向量检索等多路有序结果融合起来,不要求原始分数同尺度。
- Reranker 看问题和候选内容:在已召回的候选中进一步排序;它通常不重新搜索知识库。
- 两者都受召回范围限制:关键证据没进候选集,后续排序无法凭空补回。
- 图检索是否参与 RRF,要看数据如何表示和实际实现:三路召回不自动等于三路 RRF。