Cross-encoder vs bi-encoder
这两种架构是现代检索的核心。bi-encoder 独立地把每段文本变成一个向量 —— 又快又可扩展,非常适合一阶段搜索。cross-encoder 把查询和文档一起读入并输出相关性分数 —— 更慢,但准确得多,而这正是 reranker 所需要的。
bi-encoder 如何工作
bi-encoder(又称双编码器,dual encoder)把查询和每个文档分别送入同一个模型,为每段文本产生一个定长向量。相关性就是两个向量之间的余弦相似度(或点积)。
query ───▶ [encoder] ───▶ q⃗ ┐
├─▶ cosine(q⃗, d⃗) = score
doc ───▶ [encoder] ───▶ d⃗ ┘
关键特性是:文档向量不依赖于查询。你可以一次性把整个语料库嵌入并存入索引,查询时只需嵌入查询并查找最近邻。这正是向量搜索能够快到处理数百万文档的原因。缺点是查询和文档从不“见面”,因此分数是个粗糙的工具。
cross-encoder 如何工作
cross-encoder 把查询和文档拼接成一个序列 —— [CLS] query [SEP] document [SEP] —— 并把这一对一起送入 transformer。自注意力让每个查询 token 都能与每个文档 token 交互,模型输出一个相关性分数。
query + doc ───▶ [encoder, full cross-attention] ───▶ relevance score
这准确得多,因为模型能推理两段文本之间的关系,而不只是表面相似度。代价是:分数取决于具体的那一对,所以什么都没法预计算。每一个 (query, document) 组合都是一次全新的前向传播 —— 这也是为什么你只在短名单上跑 cross-encoder,绝不在整个语料库上跑。这种“只对短名单”的用法就是重排序。
逐项对比
| 项目 | Bi-encoder | Cross-encoder |
|---|---|---|
| 输入 | 查询与文档分别编码 | 查询与文档一起编码 |
| 输出 | 每段文本一个向量 | 每一对一个相关性分数 |
| 可预计算语料? | 可以 —— 嵌入一次,反复复用 | 不行 —— 必须在查询时打分 |
| 速度 | 非常快(向量查找) | 慢(每个候选一次模型调用) |
| 准确性 | 适合召回 | 精度极佳 |
| 可扩展到百万级文档? | 可以 | 不行 —— 只能用于短名单 |
| 典型角色 | 第一阶段检索 | 第二阶段重排序 |
为什么两者都要用
它们是互补而非竞争。bi-encoder 的职责是 recall(召回):低成本地拉出几十个很可能包含答案的候选。cross-encoder 的职责是 precision(精度):仔细重排这个短名单,让最好的候选明确地排在最前。
bi-encoder 找到草垛中最有希望的那个角落,cross-encoder 则在那里找到那根针。
只用其中一个通常是个错误:单靠 cross-encoder 无法及时扫描百万文档,单靠 bi-encoder 又把质量白白浪费。标准答案是两阶段流水线 —— 用 bi-encoder 检索,用 cross-encoder 重排序。
一个具体例子
以查询 “免费套餐包含 API 访问吗?” 和两个候选为例:
- A:“我们的定价有免费版、专业版和企业版,按月计费。”
- B:“所有套餐都提供 API 访问,包括免费版。”
bi-encoder 可能把 A 排得很高 —— 它密集地围绕“套餐”“定价”这些主题,与查询共享大量词汇。但真正回答了问题的是 B。cross-encoder 把查询和每个候选一起读入,能看出 B 解决了那个具体诉求并把它顶到最前。正是这道鸿沟,构成了 reranker 存在的全部理由。