混合检索与重排 从 RRF 候选融合到可复现的离线评测
同一个知识库问题,关键词检索可能找到了正确的错误码,向量检索却找到了更接近用户说法的排障步骤。把两边结果合并后,下一步是直接交给模型,还是先重新比较问题与每个段落的关系?
这一篇接在从 embedding 到语义检索之后,把流程拆成三个可以分别检查的环节:候选是否覆盖答案、融合怎样排列候选、重排是否让真正有用的证据更靠前。最后用纯 Python 标准库实验验证排序与指标,不调用模型,也不把人工示例当成生产效果评测。
先分清三个不同的预算
| 记号 | 含义 | 增大它主要影响什么 |
|---|---|---|
| W | 每条召回列表进入融合的窗口 | 候选覆盖面、检索与融合成本 |
| M | 融合后送入重排的候选数 | 重排计算量、候选截断损失 |
| K | 最终返回或放入上下文的条数 | 阅读与生成负担、上下文预算 |
W、M、K 是三个独立决定。下文 RRF 的平滑常数记作 c,避免把它与返回条数 K 混淆。向量索引内部的近似搜索候选预算又是另一层参数,不能用这三个数代替。
flowchart TD Q["查询与权限范围"] --> L["关键词召回:前 W 条"] Q --> V["向量召回:前 W 条"] L --> F["RRF 融合排名"] V --> F F --> M["截取 M 个候选"] M --> R["逐对重排"] R --> K["选择前 K 条证据"] K --> G["组织上下文与回答"]
实际系统应先由可信的服务端身份确定可访问范围,再让各召回分支在同一范围内查询。下面的代码接受已完成权限过滤的排名列表;它不是权限系统。把受限内容先交给外部重排服务,再在显示时过滤,不能补回已经发生的数据外发。
关键词与向量分数为什么不能直接相加
关键词检索常借助 BM25 这类方法利用词频、稀有程度和长度信息;向量检索比较表示之间的相似程度。两者的分数含义和尺度不同。Elastic 排名说明
可以设想查询“连接报错 ZX-417,重试还是重新登录”:关键词分支有机会精确抓住型号与错误码,语义分支有机会找到没有原样复述问题的处理说明。这是可能的互补,必须用自己的查询验证;分词、同义词扩展和 embedding 模型都会影响结果。
假设某条结果的关键词分数是 18,向量余弦相似度是 0.82,18+0.82 并没有天然的统计解释。按查询做归一化也会引入新问题:一个异常分数、一个很短的候选列表,都可能改变归一化后的间距。
RRF 提供一个容易检查的基线:只使用各路排名,不要求原始分数共享量纲。这会丢掉分数间距,既是便利,也是一种信息损失。
RRF 手算:来自几条列表和排第几名
对于文档 d,RRF 分数为:
score(d) = Σ 1 / (c + rank_i(d))。
排名从 1 开始;d 未出现在某条列表中,该列表贡献 0。c 是正的平滑常数。该方法来自 Cormack、Clarke 与 Büttcher 的研究;原论文实验采用 c=60。它是一个有出处的起点,不能替代当前任务的验证。RRF 原论文
使用本站构造的两张列表:关键词为 [A,B,C],向量为 [B,D,A]。令 c=60、W=3:
| 文档 | 关键词名次 | 向量名次 | 精确贡献 | 近似分数 |
|---|---|---|---|---|
| A | 1 | 3 | 1/61 + 1/63 | 0.032266 |
| B | 2 | 1 | 1/62 + 1/61 | 0.032522 |
| C | 3 | 未出现 | 1/63 | 0.015873 |
| D | 未出现 | 2 | 1/62 | 0.016129 |
融合结果为 B、A、D、C。B 两边都靠前,所以排第一。但这不证明 B 真正相关:如果两个检索器共享同一种偏差,一致意见仍可能出错。
当 c 较大,第一名与后面名次的单路贡献更接近;当 c 较小,靠前名次的优势更明显。改变 W 还会改变哪些文档能贡献分数,两者不能混为一谈。Elastic 的官方说明也分别定义了 rank_constant 与 rank_window_size;本文代码是教学实现,不是其 API 的逐项复刻。Elastic RRF 文档
flowchart TD A["确定每路窗口 W"] --> B["保留窗口内的排名"] B --> C["按文档 ID 累加倒数贡献"] C --> D["按总分降序排列"] D --> E["同分按 ID 确定次序"] E --> F["截取 M 个重排候选"]
同一路列表中一个 ID 只能出现一次。来自不同列表的同一 ID 则应合并贡献。文档版本、分块版本和 ID 规则必须统一,否则“同一段证据”可能被误当成不同对象。相邻分块内容相近也不等于 ID 重复;上下文去冗余需要单独处理。
重排读取的是查询与候选的关系
常见的双编码器分别编码查询和文档,便于预计算文档向量;Cross-Encoder 则将查询与候选文本一起处理,直接为这一对文本打分。后者常放在召回之后,以有限候选量控制计算成本。Sentence Transformers 检索与重排说明
这不是“再做一次 RRF”。RRF 汇总名次;文本重排模型重新读取内容。重排分数也不自动等于经过校准的相关概率,更不能直接证明答案受到证据支持。
本篇不下载重排模型,而是提供一组人工指定的演示分数:A=0.96、D=0.85、B=0.20。它们只用于验证“融合候选如何进入下一阶段”;没有任何真实模型性能含义。
| 步骤 | 结果 | 此时能做什么 |
|---|---|---|
| RRF 融合 | B、A、D、C | 形成统一候选次序 |
| M=3 截断 | B、A、D | C 已不在重排输入中 |
| 按演示分数重排 | A、D、B | 重新决定这三个候选的顺序 |
| K=2 返回 | A、D | 用两条证据继续下游任务 |
如果 M=1,重排只得到 B,无论模型多强,也不能把缺席的 A、D 排进结果。这是流程约束,不是对任何具体模型的能力判断。
flowchart TD
A["正确证据在哪里丢失"] --> B{"在召回并集中吗"}
B -->|"否"| C["检查索引、分词、表示与 W"]
B -->|"是"| D{"在前 M 个候选中吗"}
D -->|"否"| E["检查融合和候选截断"]
D -->|"是"| F{"进入最终前 K 条了吗"}
F -->|"否"| G["检查重排与去冗余"]
F -->|"是"| H["再检查证据使用与生成"]
可运行的 Python 标准库实验
将下面整段保存为 Python 文件直接运行。排名 ID 使用非空字符串;每路禁止重复;非法预算和非有限重排分数会被拒绝。为了让手算中的精确同分不会受浮点累加顺序影响,教学代码用 Fraction 排序,输出时再转换为小数。生产规模可以使用经过测试的浮点实现,不宜直接把有理数计算当成低延迟方案。
1 | import json |
代码只实现融合、对已提供分数的排序和评测,没有实现 BM25、embedding 推理或 Cross-Encoder。demo_scores 与 grades 都是人工构造的,不能作为训练后模型的测试成绩。
排名总输入长度为 N、窗口内不同 ID 数为 U 时,融合最多累加 N 次、排序 O(U log U),另有有理数运算成本。窗口截断前仍验证完整输入,所以校验成本按完整列表计。实际部署还需要计入检索、模型推理、文本传输与分块长度带来的成本。
三个指标分别看什么
本例的标注是 A=3、D=2、其余为 0。Recall 把等级大于 0 的项目当作相关;nDCG 保留等级强弱。这里的评测单位是稳定的段落 ID,不能把文档级标签直接套在分块级结果上。
| 指标 | 本文约定 | 能回答的问题 |
|---|---|---|
| Recall@K | 前 K 条命中的相关 ID 数 / 全部已标相关 ID 数 | 已知证据覆盖了多少? |
| RR@K | 首个相关结果名次的倒数,未命中为 0 | 第一条有用结果排得多靠前? |
| nDCG@K | 增益为 2^grade−1,按 log2(rank+1) 折扣后除以理想值 | 更重要的证据是否排得更前? |
多条查询的 RR 平均值才是 MRR。本文对没有任何正相关标注的查询返回 None,提示它们不进入这些相关性指标的普通均值;应另外评价无答案查询的拒答或误报情况。K=0 且有正相关标注时,三个结果都按 0 处理。
| 系统,K=2 | 返回前两条 | Recall | RR | nDCG,约值 |
|---|---|---|---|---|
| 关键词列表 | A、B | 0.5 | 1 | 0.787 |
| 向量列表 | B、D | 0.5 | 0.5 | 0.213 |
| RRF | B、A | 0.5 | 0.5 | 0.497 |
| 人工分数重排 | A、D | 1 | 1 | 1 |
这个手算里,RRF 的 nDCG 甚至低于关键词列表。因此“加上融合必然提升”已经被示例本身否定。最后一行的满分是人工安排出来的流程演示,不能用来证明重排模型有效。
离线比较怎样避免只看一个漂亮例子
先固定语料快照、分块规则、权限范围与标注单位,再按精确术语、同义改写、组合条件、无答案查询分别检查。参数选择使用开发集,最后报告独立测试集;不要边看测试集失败例边调参数,再把同一集合称作未见测试。
下面是实验设计建议,不是已经测得的结果:
| 比较组 | 保持不变 | 需要记录 |
|---|---|---|
| 关键词 / 向量 / RRF | 同一查询、语料和权限范围 | Recall@M、RR@K、nDCG@K |
| RRF / RRF+真实重排 | 同一前 M 个候选 | 排序收益、退化查询、延迟 |
| 不同 W | 固定模型、M、K | 候选并集覆盖与成本 |
| 不同 M | 固定召回结果、K | 截断损失与重排耗时 |
候选集合大小增加时,并集的覆盖可能提高,但融合后的固定前 M 名可能变化,最终指标并不保证单调提高。重排模型有输入长度限制时,还应记录候选截断:正确句子可能在段落尾部,模型实际看到的内容与检索返回的内容未必一致。
未标注文档在本代码中按 0 处理,只是一个显式约定;真实标注不完整时,“未标”不等于“已证实不相关”。报告要说明标注范围,否则不同系统发现的新结果可能受到不公平惩罚。
flowchart TD A["冻结查询、语料与标注"] --> B["记录各阶段候选 ID"] B --> C["同一查询比较不同配置"] C --> D["同时检查均值与退化案例"] D --> E["记录成本和尾部延迟"] E --> F["独立测试集确认结论"]
接回 RAG 的证据链
检索返回一段高分文本,仅表示它在当前排序规则下靠前。接下来仍要检查问题的时间范围、实体、限制条件,以及回答的每项主张是否由该段内容支持。可以接着读RAG 知识库评测,把检索覆盖与最终回答可信度分别记录。
一个实用的排障顺序是:先确认资料确实已入库且可访问,再查召回、融合截断、重排、上下文组织,最后查生成。保存各阶段 ID 和版本,才容易判断故障发生在哪里。
资料与验证范围
- Cormack、Clarke、Büttcher,RRF 原论文:排名融合公式与原始实验。
- Elastic RRF 官方文档:排名常数与窗口参数。
- Elastic 排名说明:关键词排名与融合概念。
- Sentence Transformers:Retrieve & Re-Rank:双编码器与交叉编码器的分工。
资料核对日期:2026-10-04。本站验证脚本会执行本文代码,用独立有理数参考计算核对融合排名,并检查窗口、同分、重复 ID、非法分数、指标边界和重排无法补回缺失候选的性质。验证证明实现符合本文约定,不代表已经完成真实知识库上的效果实验。