Skip to content
🔗 分享本题
查看我的学习进度 →

两阶段检索先用轻量检索器广泛召回再由重排器精排,ColBERT 保留 token 向量做 MaxSim 晚期交互

🧠 记忆锚点:粗排要快且不漏,精排要准;Late Interaction 保留 token 级匹配再聚合。

💡 答案要点

单阶段 vs 两阶段检索对比:

单阶段(纯向量检索):
用户查询 → 向量化 → Top-100 向量检索 → 返回
问题:向量检索用"整体相似度",可能遗漏细粒度匹配

两阶段(向量检索 + Rerank):
用户查询 → 向量化 → Top-500 向量检索 → Rerank 模型 → Top-20 返回
优势:粗排用向量快召回,精排用模型保精度

向量检索的局限性:

python
# 向量检索的问题:query 和 doc 的"整体"做相似度计算
# 但实际上:query 中的某些词比另一些词更重要

query = "Python 异步编程 performance optimization techniques"
doc1 = "Python 性能优化:异步编程完全指南"
doc2 = "Java 异步框架与性能调优实践"

# 向量检索结果:doc1 排在前面(整体语义更接近)
# 但用户真正想问的:doc1 和 doc2 都有价值

# 问题:
# 1. "Python" 在 doc1 中精确匹配,在 doc2 中缺失
# 2. "异步" 在两个 doc 中都出现
# 3. 向量模型可能无法精确捕捉这种关键词重要性差异

ColBERT 核心原理(Late Interaction):

传统向量检索(早期交互):
query_embedding = avg(所有query token的embedding)
doc_embedding = avg(所有doc token的embedding)
score = cosine(query_embedding, doc_embedding)

ColBERT(晚期交互):
query_embedding = [token1_emb, token2_emb, ..., tokenN_emb]  # 每个token独立
doc_embedding = [token1_emb, token2_emb, ..., tokenM_emb]   # 每个token独立

score = max( cosine(query_token1, all_doc_tokens) ) +
        max( cosine(query_token2, all_doc_tokens) ) + ...
        # 每个query token找最相关的doc token,累加

图示:

Query: "Python async performance"
Query Tokens: [Python] [async] [performance]
                 ↓        ↓         ↓
           ┌──────────────────────────────┐
doc1:    [Python] [async] [guide] [perf]  │
           │        │        │        │   │
           └────────┼────────┼────────┼───┘
                    ↓        ↓        ↓
           MaxSim: cos(Python,Python)=0.95  ← "Python" 精确匹配
                    + cos(async,async)=0.92  ← "async" 精确匹配
                    + cos(perf,performance)=0.88  ← 语义相关
                    = 2.75  ← 最终分数

为什么 Late Interaction 更强:

维度早期交互(avg embedding)晚期交互(ColBERT MaxSim)
细粒度❌ 词级别信息被平均✅ 每个query token独立匹配
关键词匹配❌ 依赖语义,关键词可能丢失✅ 精确关键词得高分
语义匹配✅ 语义理解强✅ 语义理解也强
计算量小(一次cosine)大(query×doc token矩阵)
适用场景粗排(快)精排(准)

生产级两阶段检索实现:

展开 Python 代码示例(30 行)
python
from sentence_transformers import CrossEncoder
import numpy as np

class TwoStageRetriever:
    def __init__(self, vector_db, rerank_model="BAAI/bge-reranker-v2-m3"):
        self.vector_db = vector_db
        # 精排模型:Cross-Encoder(不是Bi-Encoder)
        self.reranker = CrossEncoder(rerank_model)

    def retrieve(self, query, top_k_vector=100, top_k_final=20):
        # 阶段1:向量检索(粗排,快速召回)
        vector_results = self.vector_db.search(
            query_vector=self.embed(query),
            top_k=top_k_vector
        )
        candidate_docs = [r["text"] for r in vector_results]

        # 阶段2:Cross-Encoder Rerank(精排,准)
        # query-doc pair 输入,打分排序
        pairs = [(query, doc) for doc in candidate_docs]
        rerank_scores = self.reranker.predict(pairs)

        # 合并排序
        ranked_indices = np.argsort(rerank_scores)[::-1]
        final_results = [candidate_docs[i] for i in ranked_indices[:top_k_final]]
        return final_results

# 效果对比(生产数据):
# 向量检索 Recall@20:  72%
# + Rerank 后 Recall@20: 91%  ← +19%

Cohere Rerank vs 开源方案对比:

方案精度延迟成本适用场景
Cohere Rerank 3⭐⭐⭐⭐⭐~100msAPI付费快速上线、生产
BAAI/bge-reranker-v2-m3⭐⭐⭐⭐~200ms开源免费自托管、隐私
jina-colbert⭐⭐⭐⭐⭐~150ms开源免费极致精度
monoBERT⭐⭐⭐⭐~300ms开源免费简单场景

面试话术:

示例表达(仅在能用本人经历或可复现实验佐证时使用): "两阶段检索是生产环境的标配。向量检索负责粗排——快速从1000万条里召回100条;Rerank负责精排——用Cross-Encoder对100条重新打分排序。ColBERT的核心是Late Interaction——每个query token独立找最相关的doc token累加,比传统avg embedding的早期交互精细得多。我在项目中用BAAI/reranker-v2-m3,Recall@20从72%提升到91%,延迟增加50ms完全可接受。"

📚 参考:ColBERTv2:Late Interaction 重排序(原论文)