夜雨聆风学习资料网

ARTICLE · 1333103

RAG 中的 RRF 是什么?用两个文档讲透 ES + Milvus 混合检索

RAG 中的 RRF 是什么?用两个文档讲透 ES + Milvus 混合检索
TECH · DEEP SPACE

RAG 中的 RRF 是什么?用两个文档讲透 ES + Milvus 混合检索

◎ 导 语

> 一个 RAG 系统同时用了 ES 和 Milvus:ES 擅长关键词,Milvus 擅长语义。两路都返回了一堆文档,接下来该把谁排在前面? > > 很多人的第一反应是“把两路分数加一下”。问题是,ES 的 BM25 分数和 Milvus 的向量相似度根本不是同一种计量单位,直接相加很容易被其中一路碾压。 > > RRF 解决的就是这个问题:不比原始分数,只比各自的名次。 本文用一个固定例子,把标准 RRF、加权 RRF、平滑常数 k、静态与动态权重一次讲清。

CHAPTER 01

一、为什么 ES 分数和 Milvus 分数不能直接相加

在一个典型的混合检索 RAG 中,用户的问题会同时进入两路:

✔ES 稀疏检索:常用 BM25,擅长关键词、固定术语、编号、实体、结构化过滤和 FAQ。

✔Milvus 向量检索:通过 Embedding 找语义相近内容,擅长长文本、口语化表达、同义改写,以及图片、音频等向量检索。

两路都有自己的分数,但这些分数的值域、分布和含义不同。一个系统的 8 分,不代表它比另一个系统的 0.8 分“恰好强 10 倍”。

这就像一位老师用 100 分制,另一位老师用 5 分制。直接把分数相加,满分 100 的老师天然更有话语权。

RRF 的思路是:先别管考了多少分,只看在各自班级里排第几。

RRF 融合流程

CHAPTER 02

二、RRF 的核心:把名次变成可以累加的贡献

RRF 全称 Reciprocal Rank Fusion,中文通常译为“倒数排名融合”。

标准公式可以写成:

RRF(d) = Σ 1 / (k + rank(d, r))

其中:

✔d:一个文档或 Chunk。

✔r:某一条检索链路,例如 ES 或 Milvus。

✔rank(d, r):文档 d 在链路 r 中的名次,从 1 开始。

✔k:平滑常数,常见默认值是 60。

✔Σ:把同一文档在各条链路里的贡献加起来。

如果文档没有出现在某一路的召回列表里,这一路对它的贡献就是 0。

最后,按照 RRF 总分从高到低重新排序。一个 Chunk 如果在 ES 和 Milvus 中都靠前,它就能拿到两份贡献,自然更容易排到前面。

所以 RRF 的一个重要思想是 共识优先:两路都认可的结果,通常比只有一路叫好的结果更值得关注。

它有四个鲜明特点:

✔  只使用名次,不使用 BM25 或向量相似度的原始数值。

✔  名次越靠后,k + rank 越大,单项贡献越小。

✔  可以融合不同引擎、不同模态的召回结果。

✔  它属于粗排融合,不会利用原始分数里的置信信息;高精度链路通常还会接 CrossEncoder 做二次精排。

因此,RRF 也可以理解为一种“位次归一化”:不是把原始分数压到 0~1,而是在次序空间里对齐多路结果。

CHAPTER 03

三、标准 RRF:两个文档为什么会打成平手

固定一个例子,后文反复使用:

1

Chunk A:ES 第 1,Milvus 第 2。

2

Chunk B:ES 第 2,Milvus 第 1。

3

平滑常数 k = 60。

第 1 名的分母是 60 + 1 = 61,第 2 名的分母是 60 + 2 = 62。

两个基础倒数为:

1

1 / 61 ≈ 0.01639

2

1 / 62 ≈ 0.01613

标准 RRF 中,ES 和 Milvus 地位相等,直接相加:

文档
ES 贡献
Milvus 贡献
RRF 总分
A
第 1:0.01639
第 2:0.01613
0.03252
B
第 2:0.01613
第 1:0.01639
0.03252

A 的计算是:

0.01639 + 0.01613 = 0.03252

B 的计算是:

0.01613 + 0.01639 = 0.03252

两者完全相等。

原因并不神秘,就是 加法交换律:1/61 + 1/62 与 1/62 + 1/61 必然相等。

推广一下,只要两个文档拿到的名次集合相同,只是在不同链路中互换,例如一个排名第 3、第 5,另一个排名第 5、第 3,它们的标准 RRF 总分也会相同。

这不是算法出错,而是标准 RRF 没有回答一个问题:当两路意见相反时,我到底更信谁?

CHAPTER 04

四、加权 RRF:给更可靠的一路更多表决权

加权 RRF 只做一处改动:每一路的倒数贡献,在累加之前先乘上该路的权重。

Weighted_RRF(d) = Σ w_r / (k + rank(d, r))

工程上通常让各权重之和为 1,这样更容易理解。

沿用刚才的例子,设置:

1

ES 权重 0.2

2

Milvus 权重 0.8

这表示我们更信任 Milvus 的语义排序。

Chunk A:

1

ES 单项:0.01639 × 0.2 = 0.00328

2

Milvus 单项:0.01613 × 0.8 = 0.01290

3

总分:0.00328 + 0.01290 = 0.01618

Chunk B:

1

ES 单项:0.01613 × 0.2 = 0.00323

2

Milvus 单项:0.01639 × 0.8 = 0.01311

3

总分:0.00323 + 0.01311 = 0.01634

此时 B 排在 A 前面。

标准 RRF 与加权 RRF

要特别注意:权重乘的是 RRF 倒数项,不是 BM25 原始分,也不是向量相似度。后两者属于原始分数融合,不是这里讨论的加权 RRF。

权重方向决定谁更可能胜出,权重差距则会影响领先幅度。

CHAPTER 05

五、平滑常数 k:控制“前进一名值多少钱”

如果没有 k,第 1 名贡献是 1,第 2 名是 0.5,第 3 名是 0.333,头部差距会非常陡峭。

加入 k 后,分母变成 k + rank,名次之间的差异被平滑了。

当 k = 60:

1

第 1 名:1 / 61 ≈ 0.01639

2

第 2 名:1 / 62 ≈ 0.01613

3

两者差约 0.00026

当 k = 10:

1

第 1 名:1 / 11 ≈ 0.0909

2

第 2 名:1 / 12 ≈ 0.0833

3

两者差约 0.00758

同样是从第 2 名前进到第 1 名,k = 10 时的收益约为 k = 60 时的 28.7 倍。

因此:

1

k 越小:头部名次优势更突出,曲线更陡。

2

k 越大:不同名次的贡献更平缓,头部与尾部的差距被压缩。

继续用 A、B 例子,k = 10、ES 权重 0.2、Milvus 权重 0.8:

1

A:0.0909 × 0.2 + 0.0833 × 0.8 ≈ 0.08485

2

B:0.0833 × 0.2 + 0.0909 × 0.8 ≈ 0.08939

B 仍然胜出,但两者分差被放大。

平滑常数 k 的影响

这里需要补充一个重要边界:“调整 k 只会改变分差、永远不会改变排序”并不适用于所有情况。

在原文这个两个文档、名次互换、权重固定的例子里,先后顺序确实不变;但在多候选、多路召回、缺失结果较多的真实场景中,改变 k 可能改变各文档的累加优势,进而改变最终次序。

常见默认值 60 来自原论文实验经验,后续不少实现沿用了这个默认值。但生产环境仍应使用自己的数据评测,而不是把 60 当成永远最优的答案。

CHAPTER 06

六、静态权重:按业务入口设置固定偏好

静态权重指的是:同一个检索入口下,所有 Query 共用同一组权重。

它适合查询风格相对稳定的业务。例如,合同编号检索和开放式技术问答,最好不要共用一套全局权重,而应按入口分别配置。

下面三组可以作为起步基线,不是不可修改的行业标准:

权重基线
适用场景
主要风险
ES 0.8 / Milvus 0.2
编号、注册证、资质证书、固定 FAQ、工单政策
可能压制没有关键词重合、但语义相关的内容
ES 0.2 / Milvus 0.8
长文档、论文、技术手册、OCR、口语化开放提问
可能把语义相近但事实不一致的内容抬高
ES 0.5 / Milvus 0.5
企业文档、素材库、混合意图、多跳 Agent 子问题
两路冲突时可能出现较多并列,需要定义兜底规则
★ 核 心

关键词优先时,可以适当放宽 Milvus 召回阈值,让语义候选先进入融合池,避免因为权重偏低而彻底漏掉。

语义优先时,建议保留 CrossEncoder 精排,进一步过滤“看起来很像、实际答案不对”的 Chunk。

均衡模式下,可以提前定义并列时的第二排序规则,例如固定参考某一路名次,或使用精排模型继续区分。

静态权重不是拍脑袋决定的,它应当是对一类查询长期偏好的固化表达。配置时要遵守四条原则:

✔  权重表达对位次证据的信任,不是原始分数缩放。

✔  权重之和通常设为 1,便于解释和比较。

✔  按检索入口配置,不建议一个全局权重包打所有业务。

✔  先用 Golden 基准集做离线 A/B,再小流量灰度上线。

CHAPTER 07

七、静态权重什么时候会失效

静态权重能跑稳,隐含了几个前提:用户提问风格、知识库内容、分词方式和 Embedding 模型没有发生明显变化。

一旦出现下面两类信号,就要重新评估:

1

离线指标分层下降:实体类查询变准了,语义类却经常漏掉真值;或者语义检索正常,编号和证件查询开始出错。

2

线上反馈两极分化:一类问题好评增加,另一类问题投诉、“没用”标记明显增加。

优化时,可以在冻结的 Golden 子集上做网格搜索和 A/B 对照,只保留相对基线有稳定收益的候选权重,再进入灰度验证。

如果用户问题类型非常分散、长尾 Query 很多,一套固定权重很难同时照顾所有场景,这时就需要动态权重。

CHAPTER 08

八、动态权重:每条 Query 先判断该信哪一路

动态权重与静态权重的区别很直观:

1

静态:入口提前定好一套权重。

2

动态:每条 Query 单独选择一套权重。

动态方案常用两类轻量信号。

第一类是槽位信号。 看 Query 中是否有编号、注册号、物料编码、合同号或专有参数。命中这类结构化信息,通常更需要 ES 的精确字面匹配。

第二类是意图信号。 判断用户是在查实体编号、查固定 FAQ、进行开放式提问,还是做多跳推理。

★ 核 心

槽位并不只是简单关键词命中,意图也不是看到产品名就判定精确查询。例如,Query 中虽然有产品名称,但用户问的是“怎样设计改造方案”,意图仍属于开放式语义检索,不应盲目偏向 ES。

一种清晰的映射方式是:

识别结果
动态权重策略
精确实体查询,命中编码或注册号
采用关键词优先权重
开放式语义检索,无强实体槽位,长句或口语改写明显
采用语义优先权重
多跳、混合意图、槽位冲突或识别置信度不足
回落到均衡权重

静态与动态权重

动态分支必须有 置信度回落机制。识别不确定时不要强行偏向某一路,而是退回均衡或当前入口的静态权重。

落地顺序上,更稳妥的做法是:先把静态分组权重跑稳,积累足够的标注样本,再上线意图驱动的动态权重。线上还要保留开关,识别服务异常时可以一键切回静态基线。

CHAPTER 09

九、评测不能只看全局平均值

动态权重新增了意图识别和槽位抽取,也就新增了失败来源。

典型故障是:一个开放式语义问题被误判为实体查询,系统把权重偏向 ES,结果大量语义相关 Chunk 被压低,真正答案反而没有进入前排。

因此,Golden 测试集应按类型拆开:

1

实体查询子集。

2

语义查询子集。

3

混合意图子集。

分别观察各组的 Context Recall、最终排序效果,以及下游回答的 Faithfulness。后者还会受到生成模型和 Prompt 等因素影响,不能全部归功于 RRF。

比较理想的动态收益是:实体子集召回不下降,语义子集召回有所提升。 如果某一组明显差于静态基线,就应关闭或回滚对应分支。

线上还需要记录每条 Query 的识别结果、槽位、输出权重、召回文档和最终反馈,把失败样本持续回流到意图与槽位模型的优化中。

★ 核 心

RAGAS 等评测框架可以参与这条迭代闭环,但关键不是工具名字,而是固定基准、分组对照、灰度验证和可回滚。

CHAPTER 10

十、用几十行 Python 实现一个加权 RRF

下面的示例只接收“每一路的文档 ID 排名”,完全不读取原始检索分数:

PYTHON
def weighted_rrf(rankings, weights=None, k=60):
    weights = weights or {}
    scores = {}

    for channel, doc_ids in rankings.items():
        weight = weights.get(channel, 1.0)
        for rank, doc_id in enumerate(doc_ids, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + weight / (k + rank)

    return sorted(scores.items(), key=lambda item: item[1], reverse=True)


rankings = {
    "es": ["chunk-a", "chunk-b"],
    "milvus": ["chunk-b", "chunk-a"],
}

print(weighted_rrf(rankings))
print(weighted_rrf(rankings, {"es": 0.2, "milvus": 0.8}))

没有出现在某一路列表中的文档,自然只会累加其他链路的贡献,相当于该路贡献为 0。

真实工程还需要处理文档 ID 统一、候选列表截断、并列时的确定性排序、权重配置、指标埋点和异常回退。

RRF 完成融合后,可以把前 N 个候选交给 CrossEncoder 精排,再把最终上下文交给生成模型。

CHAPTER 11

写在最后:面试时可以这样回答

★ 核 心

RRF 的核心不是发明一种新的相似度,而是 换一种统一的比较单位。

ES 的 BM25 分数与 Milvus 的向量相似度不可直接比较,RRF 就丢掉原始分数,把每一路的名次转换成 1 / (k + rank),再把同一文档的多路贡献累加。

标准 RRF 两路平权,可能出现名次互换后的并列;加权 RRF 在倒数项上乘以链路权重,用来表达对不同检索结果的信任程度。

k 控制名次贡献曲线的平缓程度:越小越突出头部,越大越平滑。静态权重适合查询稳定的入口,动态权重则根据意图与槽位逐条调整,并必须保留低置信度回落、分组评测和一键回滚。

★ 核 心

最后用一句话收束:

RRF 负责把多路召回“合成一张候选榜”,CrossEncoder 再负责把这张榜单“排得更准”。

说明:本文依据用户提供的参考材料重新组织和通俗化表达。原始文本未附图片地址,文中 4 张示意图均依据原文数据重新制作,并非原发布页图片。静态权重数值应视为起步示例,生产配置以实际评测为准。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料