RAG中的重排序:交叉编码器真正能发挥作用的那狭窄窗口
RAG中的重新排序仅能在召回率下限与K值上限之间起到作用。什么是“幻影命中”,以及如何在一个下午内自行测量查询结果在这两个阈值上的表现。
来源: Reranking in RAG — Cross-Encoders, the K Threshold, and the Recall Ceiling in Production Systems
交叉编码器仅在特定范围内帮助RAG流程:第一阶段的召回率必须足够高,同时传递的候选项数量也必须足够少。超出这个范围,重排器要么无事可做,要么反而会带来负面影响。下文将介绍其工作机制、一种被称为“幻影匹配”的故障模式,以及一种利用自身数据衡量这些问题的方法。
想象一个找到了正确答案的RAG系统。没有出现错误,也没有丢失任何文档。该答案仅位于结果列表的第37位,而大语言模型只读取了前五项,因此从实际用途来看它几乎等同于不存在。由于所有监控指标都显示请求已成功,因此不会触发任何警报。
这并非某次部署的特殊现象,而是架构设计所导致的。向量搜索的设计目标是将正确答案置于前50到200个候选结果之中,而不一定非得在最顶端,因此仅停留在检索阶段的流程会让答案质量完全取决于运气。
通常的做法是添加一个重排模块。这意味着需要一个大约十二行的交叉编码器,稍提升一下nDCG值,问题就解决了。这种做法本身并无错误,因为重排模块确实存在缺陷。问题在于,重排模块仅在极窄的运行窗口内有效,而几乎没人会对这个窗口进行测量。下文将展示这个成本更高的第二阶段是如何运作的,该窗口的各个边界位于何处,以及如何在一个下午内弄清楚系统处于每个边界的上方还是下方。
重排并非更优的向量搜索
重排器是一种不同的架构,仅在检索之后启用,并应用于少量候选项。
现有向量搜索系统背后的双编码器会提前离线地分别对查询和每份文档进行编码。这种方式速度很快,且能够处理数百万份文档,但缺点是无法同时考虑查询与文档的内容。
交叉编码器则采取相反的方式:它逐个标记地同时读取查询和文档,因此能够发现双编码器无法识别的否定词、数值矛盾以及细微差别。但作为代价,它必须在每次查询时对每个候选项重复这一处理过程。
这就形成了常见的两阶段处理流程。检索阶段以较低成本将候选项数量缩小到50到200个,其目标是确保查全率——不能遗漏正确答案。随后进行重排序阶段,则以较高成本对这一小部分候选项进行排序,其目标是提高精确率——从剩余候选项中选出真正最优的。
第二阶段也并非免费的升级方案。当候选项数量超过一定阈值后,交叉编码器会开始将噪声误认为是有效信号,导致质量真正下降。是否添加重排序功能需要根据实际数据通过工程计算来决定,而非凭直觉行事。
为何检索不能作为最后一步
在实际应用的 RAG 系统中,经常出现这样的症状:“尽管信息存在于我们的数据库中,模型还是会产生幻觉。”经过深入排查后发现,大多数情况下模型并非凭空编造内容,正确的文档其实就在返回的前五十份结果中,只是不在生成器所看到的前五份结果里。
其根本原因在于架构设计。双编码器会在索引阶段、即任何查询出现之前就计算出文档的嵌入向量。在搜索时,它比较的是两个压缩后的摘要,而非查询的原始文本与文档的原始文本。
一个典型的丢失信息案例:查询词为“便宜”,而文档内容却是“每晚500美元”。对向量模型而言,两者都与酒店房间的价格相关,因此会被归类到相近位置。只有当人们将两者并列查看时,这种矛盾才会显现,而双向编码器从设计上就不具备这种功能。
更好的嵌入模型也无法解决这个问题,因为这是有意做出的权衡。信息检索系统旨在最大化召回率,即确保返回的结果中包含正确文档的概率,同时还能在处理数百万份文档时保持低成本。将正确文档置于最顶端并非其设计目标。
Recall@50与Precision@5衡量的是不同指标,用于回答不同问题。将它们混为一谈,就会导致团队不得不额外投入资源去解决本应由现有系统处理的问题。
其缺点在于双编码器从不同时考虑这对数据,那么让我们看看能够同时考虑这对数据的模型能获得什么优势。
双编码器负责分类,交叉编码器负责读取
以招聘为例。双编码器扮演招聘人员的角色,在职位描述出现之前就先将上千份简历分类放入不同文件夹:速度快、表面化,且很快就能完成。而交叉编码器则扮演招聘经理的角色,面对十名最终候选人时,会逐行仔细阅读每份简历以及职位描述。只有这样,那些招聘人员未能发现的差异才会显现出来。
从机制上来看,两者的区别十分明显。交叉编码器会将查询内容和文档内容合并为一个序列,采用标准格式 [CLS] 查询内容 [SEP] 文档内容 [SEP],然后将其传递给同一个变换器进行处理。
在内部,交叉注意力机制负责处理任务:每个查询标记可以直接关注每个文档标记,反之亦然。其输出并非用于比较的向量,而是一个单一的对数几率值,即一个未经标准化的原始数值,用来表示模型对这对特定内容是否相关的置信度。
因此,双向编码器负责生成表示,而交叉编码器则负责给出判断结果。这体现的是模型所能感知的内容层面的差异,而非程度上的差异。
由此会立即产生一个后果:由于交叉编码器的得分取决于具体的查询与文档对,而非仅文档本身,因此无法预先计算并建立索引。每个查询都需要对每个候选文档进行一次完整的前向传播,这使得每对文档的处理成本是向量比较方式的数十到百倍。
这种两阶段设计理念有助于明确处理规模。根据Reimers和Gurevych在2019年发表的Sentence-BERT论文,要在10,000个句子中找到最相似的配对,大约需要进行5000万次比较:使用完整版BERT模型时耗时约65小时,而采用SBERT架构则仅需约5秒。
请记住准确的数据,因为存在诸如“1000万份文档,50小时”这样的近似说法。两者的差异本质上是质的:瓶颈在于需要评估的配对数量,而非语料库的规模。
正因如此,交叉编码器从不作为第一阶段使用。它并非性能更差,只是处理整个语料库时成本过高。由于每处理一对的代价都很大,所有效率都取决于输入的配对数量以及它在处理流程中的位置。
两阶段,两种成本考量
重排器位于检索之后、大语言模型之前。搜索会返回前k个候选结果,k指的是一个阶段传递给下一个阶段的文档数量,通常在50到200之间。重排器会重新排序这些结果,然后将生成器对应的索引定位在3到20之间。
这种级联结构并非起源于大语言模型,这恰恰体现了它的成熟度。“先用廉价过滤器筛选,再用昂贵模型判断”的模式源自学习排序时代,那时第二阶段是基于手工构建的特征的决策树集成,而非变换器模型。虽然引擎发生了变化,但架构却保持不变。
神经网络重排序技术有明确的诞生时间。2019年,论文《利用BERT进行段落重排序》将BERT用作查询与段落对之间的二元分类器,使得MRR@10指标相比当时最好的结果提升了27%。从那时起,第二阶段不再仅仅是优化步骤,而变成了独立的一层。
目前最著名的商业实现便是Cohere的旗舰级重排序工具。在各个版本中,变化最大的是一个参数——输入窗口的大小从几百个标记增加到了32,000多个标记,其中v3和v3.5版本则限制在4,096个标记。这实际上是一次重大的设计变革,因为它提高了无需截断即可进行评分的文本块大小,从而减少了分块操作的激进程度,也降低了上下文丢失的概率。
供应商们越来越少提及后续的发展计划。同一款产品的报价在不同来源之间相差十倍甚至更多,根据统计方式和“搜索”所包含的内容不同,每千次搜索的费用从几美分到2.5美元不等。
有一个简单的原则:如果你无法用自己的流量复现供应商的计算方式,那就不是精确的计算,只是猜测而已。此外还存在计费陷阱——超过限制长度的文档会被分割成多个部分,每个部分都会单独计费,因此一次逻辑上的重新排序请求可能会变成多次计费请求。
需要记住的思路是:大规模低成本地检索,然后在有限的候选集中高成本地进行重排序,让大语言模型仅看到同时通过这两道筛选的结果。这听起来像是一个稳妥的默认方案,但实际上存在一个临界点,超过之后它就会失效,而且失效方向往往是没人预料到的。
虚假匹配与重排序开始产生负面影响的临界点
传统观点认为,提供给重排序模型的候选项越多,结果就越可靠。由于该模型的准确性高于向量搜索,当候选项数量达到500或1,000时,它肯定不会出错。但这种直觉是错误的,已有相关证据可以证明这一点。
2024年的论文《淹没在海量文档中》(Jacob及其同事)指出,随着K值的增大,重排器的性能会提升并达到峰值,之后又会明显下降。当对整个文档集进行盲扫描时,普通的BM25算法就能胜过被视为当前最佳方案的交叉编码器。但需注意一点:该结果是基于学术和企业数据集的盲扫描得出的,对于K值最多仅为几百的典型处理流程而言参考价值有限。
这种缺陷有一个专门的名称——幻影匹配。重排器会错误地将最高分赋予那些与查询在词汇或语义层面均无关联的文档。
作者们提出了一个可能的成因,并谨慎地将其称为假设而非已确认的根源。他们指出问题出在训练设置上,将其称为“暴露偏差”。重排模型是通过与“难负样本”进行对比训练的,这些样本表面上看似正确答案,但实际上是错误的,因此模型便成了识别细微差别的专家。
但在实际应用中,这种专长反而适得其反。当遇到训练时几乎从未接触过的简单负样本,即纯粹的噪声时,模型便失去了参照标准,开始随意猜测。一个被训练用来区分双胞胎的模型,在面对陌生人时则毫无办法。
另一端的临界值同样真实存在,并不会否定第一端的数值。一些独立分析表明,重新排序的最低盈亏平衡点大约在召回率@50=0.85左右。低于此数值时就没有值得排序的内容,因为正确的文档根本没进入候选集。由于语料库的不同,各来源给出的精确阈值也有所差异,而且其来源也很重要:应是博客和实际经验,而非同行评审。应将这些阈值作为假设,在自己的数据上加以验证,绝不能当作既定事实。
这两个发现并不矛盾,它们是一个操作窗口的两端。在召回率阈值之下,重新排序没有可利用的信息;超过K阈值后,它就开始造成负面影响。重新排序并非呈单调改善趋势,认为越多越好只会重蹈盲目信任供应商基准的错误,因为这两种做法都是用传统观念替代了实际测量。
由于重新排序过多可能带来负面影响,而过少又可能不够,因此我们需要明确了解这一环节所能解决的问题的上限。
召回率的上限,以及67%这一数字的真实含义
重新排序工具是编辑而非记者,它只能重新排列他人已收集的材料,无法凭空创造没人提供过的内容。
这一原则,即召回率的上限,听起来似乎微不足道,直到它与行业中最引人注目的数据相对照。Anthropic在关于上下文检索的报告中提到,2024年的系统中,前20条检索结果的失败率从5.7%降至1.9%,降幅达67%。
这一数字常被用来支持重新排序技术的有效性。但如果按不同阶段拆分来看,就会呈现出不同的情况,但这并非因为该数据是错误的。
其流程如下:上下文嵌入将准确率降至3.7%,再加入BM25后降至2.9%,此时重排算法才将准确率进一步降到1.9%。因此,仅重排算法就实现了从2.9%到1.9%的最终提升。这两个数值之所以存在差异,是因为它们衡量的是不同的范围:67%是整个处理流程的综合结果,而非某单一环节的效果。
这种区别在实际应用中具有重要意义。在架构评审中提及“67%”这一数值时,需明白这实际上代表了四项改进共同作用的结果,其中三项改进发生在重排算法之前,为整体提升做出了主要贡献。
作者们自己也指出了这一局限性:该数据是针对不同领域取平均值得出的,而这些领域之间的差异很大。现有证据还提示另一个问题:所有商业重排工具的质量指标都是由其供应商根据自身数据集及第一阶段检索结果报告的,目前并不存在独立的、覆盖整个市场的评估标准。
没有任何外部基准能够告诉你重排功能在你的系统中的表现,因此你需要自行进行测试。
周一早上就能进行的计算
这个话题的所有复杂性其实都可以简化为一种可重复的操作流程。正是由于团队很少执行这一操作,才使得“重排是否有帮助”这一争论持续不断。
第一步:在大约一百条已标记的实际查询上测试recall@50和precision@5的指标。应使用自己的日志以及自定义的术语和意图组合,而非公共基准数据。如果recall@50明显低于0.85,应首先通过分块、混合搜索或嵌入技术改进检索效果,而非再叠加额外的处理层。
第二步:寻找特征信号。高recall@50搭配低precision@5的情况正是重排算法要解决的典型问题——合适的文档其实存在于候选集中,只是排名过靠后。
第三步:谨慎选择模型。使用托管API可快速启动且无需运维,但数据会离开自身的基础设施;而自托管的重排器虽然能掌控成本和隐私,却需要自行承担GPU及相关维护工作。这更多是合规性与预算方面的考量,而非模型质量的问题。
尝试一下的成本也很低。本地重新排序只需十几行代码即可实现:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-large", max_length=512)
def rerank(query, docs):
pairs = [(query, d.page_content) for d in docs] # pair: query + candidate
scores = reranker.predict(pairs) # one logit per pair
return sorted(zip(docs, scores), key=lambda x: x[1], reverse=True)
最常见的错误有三种。其一是为了“万全起见”而夸大K值。其二是在上下文被截断后重新排序,导致模型评估的是文档的前缀而非完整文档。其三是将跨编码器的得分视为可在不同查询间比较的绝对阈值,这种做法尤其危险,因为在演示环境中还能正常工作,但投入实际应用就会出问题。
还有一种不太明显的错误:“重新排序器没有起到作用”有时其实是数值错误而非架构层面的问题。《Sentence-Transformers迁移指南》中记载的一个案例显示,使用bfloat16格式计算sigmoid函数时出现了饱和现象,使得最高分值聚集在一起,进而使它们的顺序变得近乎随机。在激活函数之前改用float32格式后,排序结果才从近乎随机恢复到正常状态。
所有这些问题都只需通过测量来解决,无需更改架构。
这对架构师意味着什么
回到第37点。文件卡在那里从来不是生成模型的问题,而是排序的问题。
- 交叉编码器并非更好的嵌入方式。它是一种独立的架构,在检索之后被触发,会同时读取查询和文档并给出判断结果而非表示形式。嵌入质量的提升无法带来与并排读取两者相同的效应。
- 重排序应属于某个特定环节。检索过程应保持低成本且覆盖范围广,而重排序则应保持高成本且范围较窄,这种成本结构正是该流程的核心理念。
- 增加候选项并非稳妥的默认做法。超过某个数量K后,质量确实会下降,其原因在于这些模型的训练方式。
许多团队将重新排序视为标准配置,像缓存一样被添加到每一个RAG系统中。这是一种错误的习惯。只有同时看到这两个阈值,才能看清实际情况:低于某个阈值时,该机制无需进行任何修复;而高于另一个阈值时,它就会开始损害查询结果。简而言之,重新排序是有条件的,绝非自动发生。
仍有一个问题未解决。在目前研究的材料中,没有针对波兰语单独列出重排模型基准测试数据,所有多语言相关数据都是数十种语言的汇总结果。对于波兰语语料库或任何较小语言而言,仅依靠自身数据来进行测试不仅十分必要,更是唯一的办法。
相关阅读
- RAG中的重排机制:交叉编码器与LLM评估器的对比及其延迟问题 —— 讨论了第二阶段重排模型如何优化RAG中的候选项排序,对比了交叉编码器与LLM评估器的性能、延迟成本、故障模式以及评估方法。