ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

RRF 融合后还要不要重排:先画候选召回上限

RRF 融合后还要不要重排:先画候选召回上限

RRF 能把不同检索通道的排名合到一起,但它不能把没召回的文档变出来。重排器也是一样。很多“换了重排模型还是答错”的问题,实际发生在候选池形成之前。

10 月 8 日,Qdrant 与 Pento 发布了 miniCOIL EN-ES 的双语稀疏检索研究预览:尝试让英语与西班牙语的概念共享稀疏槽位。这提醒我们,稀疏与稠密检索的差异不只是两组分数范围;语言、词项和模型训练方式都会影响漏召回的类型。该预览没有提供中文业务的效果保证。官方文章

先检查“有没有”,再检查“排第几”

对于一条有人工相关性标注的查询,先算候选池是否覆盖正确文档,再检查最终 Top-k 的顺序。若标注答案是文档 E,而两路候选只含 A、B、C、D,任何重排分数都无法修复这个遗漏。

RRF 对某个文档的贡献是1 / (k + rank),其中 rank 从 1 开始。各通道内部应该先去重,不然重复 ID 会被当成多份证据。k、每路深度、融合后的截断深度需要一起实验;没有一个参数能对所有查询都处在帕累托前沿。

下面只实现确定性的排名融合。它没有生成随机模型分数,也不在输出上伪装成 Cross-Encoder 推理。

图中 E 位于候选池外,它表示召回遗漏。不要把这张示意图理解成某个模型的性能结果;代码中的顺序完全由两份人工给定的列表决定。

from collections import defaultdict def rrf(rankings, k=60): if k < 0: raise ValueError("k must be nonnegative") scores = defaultdict(float) for ranking in rankings: unique = list(dict.fromkeys(ranking)) for rank, doc_id in enumerate(unique, 1): scores[doc_id] += 1.0 / (k + rank) return sorted(scores, key=lambda d: (-scores[d], d)) def candidate_recall(candidates, relevant): if not relevant: raise ValueError("query has no relevance judgment") return len(set(candidates) & relevant) / len(relevant) rankings = [["A", "B", "B", "C"], ["C", "A", "D"]] fused = rrf(rankings) assert len(fused) == 4 and fused[0] == "A" assert rrf([["A", "A"]]) == rrf([["A"]]) assert candidate_recall(fused, {"E"}) == 0.0 assert "E" not in fused print("fused=A,C,B,D; missed candidate E remains unrecoverable")

例子跑通以后,还要验证什么

这段代码已在本地 macOS、CPython 3.9.6 下运行,实际输出为fused=A,C,B,D; missed candidate E remains unrecoverable。它验证的是例子中的数据约束与分支,不包含线上服务的吞吐或延迟测量。

未运行 BM25 索引、miniCOIL、向量库或 Cross-Encoder,未测 GPU 或并发性能。接入真实系统后,需要分别记录每路深度、候选并集召回率、融合召回率、重排质量和排队耗时。改一个参数时,其他条件与评测查询应固定;不要同时换模型、向量与分块后把收益全归给 RRF。

延伸阅读

  • miniCOIL EN-ES:2026 年 10 月 8 日的研究预览,说明双语稀疏表示的尝试与限制。
  • Qdrant Hybrid Queries:查阅融合与多阶段查询语义,实际 API 仍需按部署版本验证。
RELATED READING

延伸阅读

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