ChthollyHub
  • Hub
  • Chtholly
  • Search
  • Write
  • Login

Chtholly Hub

|

Rekyrice
@Rekyrice

动漫 · 追番 · 随笔

7文章50标签1活跃
珂朵莉 - default
Observation

珂朵莉观察

今天仓库里还算安静。没有关系,安静的时候也适合读一点东西。

社区入口

写点什么把今天的故事放进仓库搜索仓库找文章、番剧和灵感珂朵莉房间看看她最近在想什么

热门标签

动画电影Agent制作考察Java动画演出动画评论性能排查牛尾宪辅独立游戏生活随笔1000xRESIST3DCGasyncioCITY THE ANIMATIONCosplay

活跃用户

Rekyrice7 篇最近文章

最新文章

  • 即使无法生活,也可以停留——《末日后酒店》与那些比世界活得更久的东西
  • 裕太终于把那句话说完了——《古立特宇宙》
  • 欢迎来到 Chtholly Hub
  • 2026 冬季追番清单
  • 为什么叫 Chtholly

© 2026 Rekyrice · Chtholly Hub

Powered by Next.js & Spring Boot

公安备案图标桂公网安备45020502000330号·桂ICP备2026017801号-1

HubChthollySearchWrite
RRF 混合检索的排序实验

RRF 混合检索的排序实验

2026年07月06日kuro_404
约 8 分钟kuro_404
RRF混合检索排序Agent

做混合检索时,我最先踩的坑落在分数上,召回反倒排在后面。

词面检索给出 BM25 分数,向量检索给出相似度。两边的范围、分布和含义都不同,直接相加看似简单,实际要不停调权重;换一批文档或换一个向量模型,原来的权重又可能失效。RRF(Reciprocal Rank Fusion)绕开原始分数,只看文档在每路结果里的名次。

这次我没有启动 Elasticsearch,也没有下载 embedding 模型。我冻结了 32 条短文档、16 条查询和两路各 top-5 的有序候选,专门检查融合算法本身会怎样移动排名。它是确定性排序实验,不是语义模型性能测试。

先把候选钉住

语料内容来自后端、Agent 和大模型运维:Spring 事务事件、Kafka outbox、Python 异步阻塞、RAG 切块、记忆新旧关系、模型熔断等。每条查询只有一个预先标注的期望文档。

第一路叫 lexical,模拟词面检索容易给出的次序;第二路叫 semanticProxy,是手工冻结的语义代理次序。这个名字很重要:它没有运行 embedding,不应被拿来证明某个模型比 BM25 更好。两路数据甚至被有意做成相同的总体基线,方便把注意力放到逐题互补上。

16 条查询中,有些正确文档在两路都排第二;有些只在一路出现;最后两条的正确文档不在任一路 top-5。程序启动时会检查候选长度、文档 ID、查询标签和漏召回数量,避免改数据后仍拿旧结论写文章。

实验里的RRF实现

RRF 的代码很短:

def rrf(lists, k=60):
    scores = {}
    for ranked in lists:
        for rank, doc_id in enumerate(ranked, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
    return sorted(scores, key=lambda doc_id: (-scores[doc_id], doc_id))

一篇文档若在两路都靠前,会收到两份倒数名次;只在一路出现,则只有一份。k=60 让第一名和第五名的单路差距保持温和,重点奖励“多路都见过”。分数相同时按文档 ID 排序,使同一份输入每次产生完全相同的结果。

以一条融合成功的查询看,正确文档在关键词路排第 2,在语义代理路排第 3。取 k=60 时,它得到 1/62 加 1/63;某个只在关键词路排第 1 的干扰项只有 1/61。两个很小的倒数加在一起,足以让共同认可超过孤立高位。公式没有神秘判断,变化完全来自名次是否在两张列表中重复出现。

k 的大小也会改变偏好。设得很小,第一名优势被放大,融合更像在争夺单路冠军;设得很大,各名次贡献更接近,多路出现会更重要。本文没有为这 16 条查询调参,固定 60 是为了减少自由度。线上使用时应把它和 top-k、召回器数量一起验证。

三个第二名挪到第一

词面与语义代理的基线结果恰好相同:Hit@1 都是 2/16,Hit@3 都是 11/16,MRR 都是 0.423958。RRF 后 Hit@1 变成 4/16,Hit@3 为 12/16,MRR 为 0.514583。

三种策略的冻结指标

改善主要发生在“双方都觉得相关,但都没排第一”的查询。比如“怎么避免大量缓存同一时刻过期”,正确文档在两路都排第二;两路第一分别是热点 Key 与启动风暴,只有单路支持。融合后,正确文档的两份分数相加,超过两个各自的第一名。

Spring 提交事件和 Kafka outbox 的查询也出现同样情况。单看某一路,第一名的干扰项都说得通;把两路是否共同认可算进去,第二名反而更稳。RRF 没有理解问题,它只是利用了两个排序器错误方向不同这一事实。

还有一条很有意思的查询是刷新令牌轮换。正确文档在两路都只排第五,却因为是唯一的共同候选而升到融合第一。这个结果展示了 RRF 的偏好,也提醒我别把它自动称为“修正”:若两个检索器共享了同一种偏见,一篇普通文档也可能因为重复出现被抬得过高。

只出现一次的文档会吃亏

融合并非逐题都更好。向量召回排查那题,正确文档在词面第一、语义代理第四,RRF 后落到第二;MySQL 死锁从一路第一、另一路第四,融合后是第三。它们仍在前三,但单路最强信号被共同候选稀释了。

线程池与模型熔断两题更明显。正确文档只出现在一路第一,融合后都掉到第五。另一条候选列表里没有它,它只能拿一份 1/(60+1);同时出现在两路的普通候选会累计两份较小分数,仍可能超过它。

这是 RRF 自身的取舍。它假定多路共识比某一路孤立高分更可靠。若某个召回器质量远高于其他路,完全等权就未必合适。可以给检索器加权,也可以先提高各路 top-k,再结合重排模型;但每增加一层,都要重新验证延迟、稳定性和离线指标,不能因为公式简单就跳过逐题检查。

没被召回的两篇文档

最后两条查询故意留下硬边界:“连接池疑似泄漏怎样设置诊断阈值”和“模型升级后结构化 JSON 字段变化”。各自的正确文档都存在于 32 条语料中,却没有进入任一路 top-5。RRF 只遍历候选列表,结果中自然没有它们。两题的期望排名在三种策略里都是空值。

这件事听起来显然,线上却很容易被融合指标遮住。若只观察整体 MRR 上升,会误以为检索链路已经解决;真正查不到的文档仍然查不到。遇到这种题,应回到召回层看分词、字段、向量、过滤条件和候选深度,而不是继续调整 RRF 的 k。

因此结果 JSON 除了三项指标,还逐题保存两路列表、融合列表、期望排名,以及正确文档是否进入任一路候选。整体数值只负责报警,逐题记录才负责定位。

放进服务以前

我会在两路分数不可直接比较、质量大致接近,而且错误确实互补时先试 RRF。它不需要分数归一化,新增一路召回也容易,作为基线非常省事。上线前至少要准备固定查询集,同时看 Hit@k、MRR、延迟与双路漏召回;还要抽查那些从单路第一被融合压低的样本。

服务侧还应记录每个结果来自哪些召回器、各自名次和最终融合分,而不是只留一列总分。一次坏排序出现时,只有这些明细能回答它究竟被哪一路推高。监控也要把“双路都没叫到正确文档”单独计数;这个比例上升时,继续调融合权重只会掩盖召回退化。

这组数据只能说明:在 16 条手工冻结查询里,RRF 把首位命中从 2 提到 4,把前三命中从 11 提到 12,同时伤害了几条单路独有强结果。它不能说明真实向量模型会产生同样候选,更不能替代线上 A/B。

RRF 最有用的地方,是把问题切得更清楚。分数尺度不再需要硬凑以后,我们可以直接问:两路到底有没有互补?正确文档是否至少被其中一路叫到?若答案是否定的,融合公式再漂亮,也只能重新排列一张缺人的名单。

RRF混合检索排序Agent
阅读轨迹
约 8 分钟2,135 字2026年07月06日
阅读进度0%
本文目录
  1. 先把候选钉住
  2. 三个第二名挪到第一
  3. 只出现一次的文档会吃亏
  4. 没被召回的两篇文档
  5. 放进服务以前
文章线索
kuro_404
RRF混合检索排序Agent
珂朵莉陪读
珂朵莉 - calm

慢慢看,不着急。

解释这篇文章
kuro_404

作者

kuro_404

RAG 和 Agent 小实验。样本小就先不下结论。

查看作者所有文章
珂朵莉 - curious

文章问答

问珂朵莉关于这篇文章

她会先读这篇文章,再安静地回答你的问题。可以继续追问。

评论

加载中…

登录后参与讨论