RAG 向量库去重提速 7 倍:为什么余弦相似度又慢又漏,以及 cross-attention 怎么修
做 RAG 的大概都碰过这个场景:文档切完 chunk 塞进向量库,检索越来越慢,看一眼库大小——比预想多了一倍。回头翻 chunk 列表,大量文本块是重复的。不是完全一样,是语义高度重叠,同一个意思在不同段落里反复说了好几遍。
第一反应往往是上余弦相似度阈值过滤。把每个 chunk 嵌成向量,两两算相似度,高于阈值就扔掉一个。这个做法有个隐藏成本——我在自己的项目里踩过一次才真正意识到。一篇 7 月底挂在 arXiv 上的论文算了一笔账:在 SQuAD 1.1 上跑一轮余弦相似度过滤花了 356.7 秒,而他们提出的 CACD(Cross-Attention Calibrated Deduplication)方法,同样是去重,只要 51 秒。差了 7 倍。
那 7 倍差距主要来自余弦相似度的实际开销,远高于多数人的直觉。向量化要过一遍模型,两两比对要扫一遍全部向量,整体 O(n²)。CACD 的策略是维护一个内存池,新 chunk 进来只跟池里已有的候选比,用 cross-encoder 做 token 级比较,不用先把整段压成一个向量。
余弦相似度还有另一个问题:丢细节。一段话压成一个向量相当于有损压缩,真正区分"语义重复"和"只是话题相同"的 token 级信息已经没了。CACD 保留 token 级的 cross-attention,算一个 New Information Score(NIS),核心是看当前 chunk 里有多少信息已被已有 chunk 解释了——用 majority vote 在多个候选之间做决策,不只依赖一个最相似的。
这套做法平均去掉 9.75% 的 chunk,去重率和其他语义级方法差不多,但速度快了一大截。比之前的最强基线 NERExact 快了大约 27%(51s vs 69.6s),比余弦相似度快了 7 倍。代价是 CACD 需要一个内存池维护已保留 chunk,跨度上限受限于上下文窗口,不适合全局去重。
需要注意两个细节。所有实验基于 SQuAD 1.1 验证集,单一数据集,作者自己也说了是 early comparison,不是 general claim。代码都开源了,baseline 评估框架和 CACD 实现在两个独立的 repo 里,可以自己拉下来跑。
那 7 倍加速当然显眼,但对我来说最实用的收获是它点出了一个我一直有感觉但没验证的事:把文本压成向量再比,在这个场景里可能不是好的抽象层级。当要判断"这两段是不是重复"的时候,跨 token 的对齐信号比语义相似度更可靠。cross-attention 比 pooled vector 更适合这个任务,不是什么高深结论,但有人把它做成了工程可用的东西。
论文和代码链接在 arXiv 和 GitHub 上都有,有兴趣可以直接翻。