重排序器

B30
概念 · 核心构件

重排序器。

重排序器只是把检索器已经找到的东西重新排个序,这意味着它能提升精确率,却无法提升召回率——如果能回答问题的那段文字从来没进入候选列表,任何重排序器都不会把它变出来。所以,决定一个重排序器是否值得那点延迟的数字不是 nDCG,而是你的 recall@5 与 recall@50 之间的差距;那个差距就是全部可得的收益,而且在掏钱之前,你用一个下午就能把它量出来。

STEP 1

重排序器算的那件事,检索器在结构上算不了。

这两个阶段看起来像是「先搜索,再更好地搜索」。它们其实是两种不同的计算,区别在于查询是在哪里与文档相遇的。

  • 稠密检索器从不把查询给文档看。每篇文档在建索引时被编码一次,查询在请求时被编码,相关性是两个各自独立产生的向量之间的相似度。正是这种独立性让索引成为可能——你能预先算好几百万个文档向量,因为它们谁都不依赖一个还没人问出来的问题。
  • 交叉编码器式的重排序器把查询和一篇文档拼成一次前向传播,再读出一个相关性分数。注意力同时跑在两段文本上,于是模型能看出这个从句正好回答了那个限定条件。它身上没有任何东西可以预先算好,因为在查询到来之前,输入并不存在。
  • 成本形态由此而来。检索的成本相对语料规模基本是平的——在一个预建索引上做近似最近邻查找。重排序的成本与你打分的候选数成正比,在请求时付,每次都付。

这是检索流水线里唯一一个按候选数、而不是按查询数计价的阶段;它也是唯一一个能同时读到查询与文档的阶段。这两件事其实是同一件事,而「先检索、再重排」这个两阶段形状之所以存在,就是为了把后者买下来,同时对前者实行配给。

STEP 2

精确率可以花钱买,召回率不行。

重排序器的输入是一个列表。这个列表里缺的东西,答案里也会缺,所以第一阶段的 recall@k 是整个系统的硬天花板。这让采购决策变得异常可测——你只需要一套标注过的查询和三个数字。

  • recall@5 与 recall@50 的差,就是你的空间。如果取 50 个候选时有 88% 的查询能命中正确段落,而你的前 5 个里只有 64% 含它,那么一个完美的重排序器值 24 个点,一个够好的能拿到其中大部分。如果这两个数字只差一两个点,说明你的检索器排得已经够好,加重排序器只是白付延迟。
  • 如果 recall@50 本身就低,重排序器就买错了东西。0.6 的天花板是解析、分块或查询的问题——解法在文档解析与查询理解里,再怎么重新排序也替代不了其中任何一个。
  • 混合检索与重排序解决的是不同的那一半。把 BM25 与稠密检索做融合,靠抓住嵌入会漏掉的精确词项来提升召回;重排序器再把这堆变大了的候选排好。两者是可以叠加的,而顺序很重要:先放宽,再排序——见混合检索与重排序。
  • 留意你汇报的那个指标。nDCG@10 奖励的是重新排序,所以重排序器一上线它就会动,哪怕答对率没动。请在它旁边放一个端到端的答案正确率,正如评估 RAG 所坚持的那样。
STEP 3

三种实现,三张不同的账单。

「重排序器」说的是一份工作,不是一种架构。三个家族的差别在于它们被允许提前做多少活儿,而这一条轴就能预测它们的延迟、索引体积和价格。

  • 交叉编码器——默认选项。每个(查询,文档)对一次前向传播,通常是一个小编码器。MiniLM 量级的模型在数据中心 GPU 上给一批约 128 段文字打分只需几十毫秒;换成更大的 BGE 量级模型,同样一批要几百毫秒。托管接口是按深度而不是按查询计价的——Cohere 的 rerank API 把每次查询最多 100 篇文档算作一个 search unit,大约每千次 2 美元——这是再清楚不过的信号:深度是一条预算项。这类模型小到足以自己托管,而重排序正是本地模型真正有竞争力的工作之一,见小模型与本地模型。
  • 晚交互——把大部分活儿提前做掉。ColBERT 式模型在建索引时把每个词元都编码好,查询时只算一个便宜的逐词元最大相似度。你能拿到交叉编码器敏感度的大半,而请求时的成本只是零头,代价付在索引体积上——它可能比单向量索引大一个数量级。这笔账在晚交互检索里算透了。
  • LLM 列表式重排序——上限最高,经济性最差。把候选塞进提示词,让模型给出一个排序。在需要跨段落推理的查询上它能压过交叉编码器,代价是数秒级的延迟,外加一笔随「候选数 × 候选长度」增长的令牌账单。已公开的比较认为交叉编码器在性价比上领先,同时承认列表式的峰值精度略高一点。请把它留给那些连人也必须把十段文字全读一遍的查询。

换重排序器模型的便宜程度,是换嵌入模型永远比不上的:重排序器不持有任何状态,所以换它只是一次发布;而换嵌入模型会让整个索引失效,触发一次重建索引。这种不对称是个好理由,把你的实验预算押在重排序器上,别去动那个嵌入模型。

STEP 4

深度才是那个旋钮,而几乎没人是有意识地把它拧到某个位置的。

重排序器买回来之后,真正能挪动质量的参数只有一个:你交给它多少个候选。跨不同重排序器尺寸与不同检索器的消融实验显示,深度从 10 → 20 → 50 一路走,nDCG@10 仍在往上爬,而 20 之所以被选中,是作为成本折中,而不是质量最优点。

  • 重排 5 个再留 5 个,是一次披着升级外衣的空操作。检索要宽,留下要窄:取 50 个,重排,把前 5 个交给模型。收益完完整整地住在第一阶段排在第 6 到第 50 名的那些候选里。
  • 深度要从召回曲线上定,而不是从某篇博客上抄。挑 recall@k 开始走平的那个最小的 k。低于它,你是在给自己的天花板封顶;高于它,你是在按候选付费却什么也买不到。
  • 深度是直接拿 p95 去换的。延迟与成本都与 k 成正比,而重排序器坐在每一次查询的关键路径上——所以这是一个成本、质量、延迟的决定,而不是一个检索决定。用中等尺寸的交叉编码器重排 50 个候选,在普通硬件上是一个秒级的操作。
  • 别指望能缓存它。嵌入之所以缓存得漂亮,是因为文档向量与查询无关;重排分数是按「对」作键的,所以要命中,得同一个查询加上同一个候选集合。要把这笔成本捞回来,更该在答案层做语义缓存。
  • 索引一变就重新量一遍。新的分块尺寸、换了解析器、语料刷新过,都会挪动召回曲线,而你那个深度设定是从旧曲线上推出来的。

请在加重排序器之前、而不是之后做这件事:拿 100 条标注过的查询,在你现有索引上算出 recall@5、recall@20 和 recall@50。recall@5 与 recall@50 之间的差距,就是一个重排序器最多能还给你的东西。差距不到约两个点,就别加了,把这份力气花在解析与查询改写上。差距超过十个点,就加一个小的交叉编码器,深度取 50、留 5,并预期能拿回那个差距的大部分。然后去读分块与向量检索,那是设定天花板的那个阶段;再读评估 RAG,那套测量能拦住你把一次指标上的胜利当成产品上的胜利发出去。