ColBERT 与 Late Interaction:多向量检索的原理与工程实践
一、一次"召回了却答不对"的排查
事情的起点往往不是算法论文,而是一条用户投诉。知识库问答上线两周,有人问:"ColBERT 的 MaxSim 计算复杂度是多少,为什么它比双塔慢?“系统召回了五篇文档,标题里全都有"ColBERT"三个字,内容却分别讲的是模型部署、显存优化和训练数据构造——没有一篇真正解释 MaxSim。模型老老实实地把召回的内容总结了一遍,答案是错的,但每一个环节看起来都"没出错”。
问题出在召回层的表达力上。双塔(bi-encoder)检索把一段 512 token 的文本压成一个稠密向量,再和查询向量做一次点积。这个过程在数学上是不可逆的信息漏斗:无论文档里有多少个不同的主题、多少个具体的数值、多少处细微的否定关系,最终都被平均或池化成一个点。查询里"计算复杂度"这个限定词,在向量空间中被"ColBERT"这个强主题词彻底淹没了。
直觉上的补救方案是交叉编码器(cross-encoder):把 query 和 document 拼在一起送进 Transformer,让注意力在两者之间自由流动。它的效果确实好,但代价是每个查询都要对所有候选文档做一次完整前向推理,无法预计算、无法建索引。一亿篇文档、每次查询几十毫秒的预算,这条路在工程上直接被否掉。
于是我们面对一个典型的中间态问题:既要保留 token 粒度的细匹配能力,又要让文档侧的计算可以离线预计算、可以索引、可以压缩。ColBERT 提出的Late Interaction(延迟交互)正是为这个夹缝设计的第三种范式。本文从它的核心算子讲起,一路讲到存储压缩、检索引擎和真实落地时的取舍。
| 维度 | Bi-Encoder(双塔) | Cross-Encoder(交叉) | Late Interaction(ColBERT) |
|---|---|---|---|
| 文档侧是否可预计算 | ✅ 是 | ❌ 否 | ✅ 是(每个 token 一个向量) |
| 交互粒度 | 整段一个向量 | token × token 全交叉 | token × token,局部 MaxSim |
| 单次打分复杂度 | O(d)O(d)O(d) | O(LqLd)O(L_q L_d)O(LqLd)且需重跑模型 | O(∣Q∣∣D∣)O(\lvert Q \rvert \lvert D \rvert)O(∣Q∣∣D∣)纯矩阵乘法 |
| 索引存储 | 1 个向量/文档 | 不适用 | N 个向量/文档(N≈文档长度) |
| 典型 nDCG@10 量级 | 中等 | 最高 | 接近交叉编码器 |
| 主要工程痛点 | 召回精度天花板 | 延迟不可接受 | 存储膨胀 + 索引复杂 |
二、Late Interaction 的核心:MaxSim
2.1 公式与几何直觉
先把双塔和延迟交互写成统一的符号。设查询经过编码后得到 token 级嵌入矩阵Eq∈R∣Q∣×dE_q \in \mathbb{R}^{\lvert Q \rvert \times d}Eq∈R∣Q∣×d,文档得到Ed∈R∣D∣×dE_d \in \mathbb{R}^{\lvert D \rvert \times d}Ed∈R∣D∣×d,其中ddd通常只有 128 维(远小于双塔常见的 768/1024)。
双塔的做法是先池化再点积:
Sbi(q,d)=pool(Eq)⋅pool(Ed)⊤S_{\text{bi}}(q,d) = \text{pool}(E_q) \cdot \text{pool}(E_d)^{\top}Sbi(q,d)=pool(Eq)⋅pool(Ed)