跳到主要内容

RAG 去重环节的隐蔽失效:当近重复数据占满 Top-K 检索结果时

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个检索增强生成(RAG)管道可能会连续数周出现性能退化,而没有任何指标能察觉到。相关性评分看起来正常,检索延迟没有变化。触及受损主题的评估切片(eval slice)向错误方向移动了 0.25 个点,而你的每周审查将其归因于噪声。直到有人阅读了模型为某个客户工单实际接收到的上下文窗口,发现同一个段落出现了三次——一次是标题格式,一次是小写格式,一次是去除了标点的格式——你才意识到,一个月以来,你的前五名(top-five)在暗地里其实只是前二名。

这类故障的特点是:系统完全在按照指令行事。检索器正在返回与查询最相似的向量。这些向量中的每一个都确实与正确的主题相关。索引根本不知道其中三个向量来自同一个段落(只是以三种方式索引了三次),因为原本应该捕获这种情况的入库阶段去重环节正在静默跳过它。

丢掉了小写转换环节的重构

我最常见到的版本故事是这样的:入库管道在调用嵌入(embedding)之前会运行一个 normalize_and_hash 步骤。它会去除空格、将文本转为小写、移除少量的零宽字符、对结果进行哈希处理,并跳过任何哈希值与已索引项匹配的分块(chunk)。哈希表是防止近似重复项进入向量库的一种低成本、有效的机制。

半年后,一名平台工程师清理了规范化模块。他们重命名了一个导入,将一个辅助函数移动到另一个文件,并更新了三个调用点。小写转换步骤在其中一个调用点被错误地丢掉了——它原本是一个构建器对象上的方法,现在构建器的构造方式发生了变化,而本应捕获这一回归的测试只是一个仅关注覆盖率的测试,断言“该函数返回一个字符串”。函数确实返回了字符串。只是字符串不再被转换为小写了。

管道继续运行。文档继续入库。哈希表充满了重复项:语料库中任何以大小写混合形式存在的段落都会产生两个条目——一个是标题格式版本,一个是全小写版本。向量库最终拥有的向量数量是应有数量的两倍,而且没人注意到,因为每个文档的分块计数仍在上一季度数值的合理范围内。

这些向量并不完全相同。它们的嵌入向量存在微小差异,因为分词器(tokenizer)在某些地方是大小写敏感的,而嵌入模型在训练时却将这些地方视为语义等价。因此在检索时,当一个查询从词法上匹配底层段落时,余弦相似度会将这两个副本都排在列表的前列——并且由于它们的嵌入几乎相同,它们会紧挨着排列。你的前五个名额中,现在有两个被花在了相同的内容上。

为什么相关性评分会奖励冗余

这里的核心架构失误在于:检索评分函数和去重函数运行在对“同一事物”的不同定义之上。

检索器根据与查询的相似度进行排名。三个重复分块中的每一个都确实与查询相似。从检索器的角度来看,返回三个几乎相同的命中结果是正确的行为——它们都是相关的,而当它们的向量几乎相等时,余弦相似度的逻辑就是将它们排在一起。相关性评分并没有损坏。它正在执行其设计的任务。

去重环节本应强制执行另一个不变量:索引不包含冗余内容。这个不变量存在于检索评分函数之外。当去重环节静默回归时,没有任何检索指标会标记它,因为每个检索指标都是在“索引已经去重”的假设下计算的。这些指标是在针对错误的索引测量正确的事物。

这就是为什么“监控检索评分”无法发现问题。评分是正常的。问题在于,位于检索下游两个阶段的答案质量评估,才是重复内容真正改变可观测指标的第一个地方——而当信号到达评估阶段时,它已经被摊平到了每一个恰好触及重复主题的查询中。据生产团队报告,RAG 中大约 80% 的准确性问题可以追溯到包括重复在内的数据质量问题,这种失效模式很难简单地归因于单次检索调用。

模型的过度权重问题

一旦近似重复项充斥上下文,模型会以其特有的方式复合这一故障。语言模型将上下文窗口视为证据。当三个分块以略微不同的方式说着大致相同的事情时,模型会将其视为对同一主张的三次独立确认——并在答案中给予该主张更高的排名。

这是一个披着现代外衣的经典混杂因素。模型重复计算并不是因为它不擅长概率。它重复计算是因为上下文窗口不包含“这些来自同一来源”的注释。分块作为独立项从检索器传来。模型没有理由不区别对待它们。

其结果是一种微妙且持续的偏见。命中重复内容的查询得到的答案,会对重复段落所说的内容表现出系统性的过度自信。如果重复段落是正确的,你的评估指标在这些查询上可能会略微上升。如果它是错误的或片面的,你的评估指标就会下降。无论哪种情况,这种偏见都是不可见的,直到你阅读单个上下文并注意到同一句话出现了三次。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates