用 Sentence Transformers 实现多向量晚交互检索:从 MaxSim 到两阶段召回

2026-08-18 39 预计阅读时间: 1 分钟
来源: huggingface.co AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

传统文本向量模型把整段文本压缩成一个固定长度的向量,检索时只需计算一次向量相似度。多向量模型则保留多个词元或片段级向量,把查询与文档的细粒度匹配推迟到检索阶段完成,这种方法通常称为晚交互(Late Interaction)。

来源摘要未提供具体 API 和模型信息,因此下面不假定某个专用模型接口,而是基于 Sentence Transformers 的词元向量演示晚交互的核心机制。示例可以直接运行,但若用于生产检索,应换成经过晚交互目标训练的模型。

单向量为什么会丢失细节

双编码器通常分别计算查询和文档的单个向量:

q = Encoder(query)
d = Encoder(document)
score = cosine(q, d)

这种设计适合大规模近似最近邻检索:每篇文档只保存一个向量,索引紧凑,打分也很快。但压缩会产生信息瓶颈。例如查询包含产品名、错误码和操作动作时,一个全局向量未必能同时保留这些信号。

多向量模型不急于完成压缩。它为查询生成向量序列 Q = [q1, q2, ...],也为文档生成向量序列 D = [d1, d2, ...]。典型的 MaxSim 打分方式是:每个查询向量在文档中找到最相似的向量,然后汇总这些最佳匹配。

score(Q, D) = sum_i max_j cosine(q_i, d_j)

这意味着查询中的“连接超时”可以匹配文档中的相关词元,而错误码可以独立匹配另一个位置。查询与文档不必先被压成两个点,交互发生在最终打分阶段。

用 Sentence Transformers 跑通 MaxSim

下面的示例使用通用 Sentence Transformers 模型提取词元向量,再实现一个最小 MaxSim 检索器。

先安装依赖:

python -m pip install -U sentence-transformers torch

创建 late_interaction_demo.py

from sentence_transformers import SentenceTransformer
import torch
import torch.nn.functional as F

MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"

model = SentenceTransformer(MODEL_NAME)


def encode_tokens(texts: list[str]) -> list[torch.Tensor]:
    """返回每段文本的词元向量,形状分别为 [token_count, dimension]。"""
    embeddings = model.encode(
        texts,
        output_value="token_embeddings",
        convert_to_tensor=True,
        show_progress_bar=False,
    )
    return [F.normalize(item, p=2, dim=-1) for item in embeddings]


def maxsim(query: torch.Tensor, document: torch.Tensor) -> float:
    # 归一化后,矩阵乘法得到所有查询词元与文档词元的余弦相似度。
    similarities = query @ document.T

    # 每个查询词元选择一个最佳文档词元,再对查询长度取平均。
    return similarities.max(dim=1).values.mean().item()


query = "Python HTTP connection timeout"
documents = [
    "Configure request timeouts and retry failed HTTP connections in Python.",
    "Use CSS grid to build a responsive page layout.",
    "A database connection pool can reduce connection setup overhead.",
]

query_vectors = encode_tokens([query])[0]
document_vectors = encode_tokens(documents)

ranking = sorted(
    (
        (maxsim(query_vectors, vectors), text)
        for text, vectors in zip(documents, document_vectors)
    ),
    reverse=True,
)

for score, text in ranking:
    print(f"{score:.4f}\t{text}")

运行:

python late_interaction_demo.py

这个程序展示的是打分结构,不是一个经过验证的生产方案。all-MiniLM-L6-v2 主要面向句子级表示,并非专门按 MaxSim 目标训练。生产环境应选用明确支持词元级晚交互的模型,并遵循它要求的查询前缀、文档前缀、池化方式和归一化规则。

从演示代码走向检索系统

直接比较所有查询词元和所有文档词元,计算量大致与 查询词元数 × 文档词元数 成正比。更棘手的是存储:单向量检索每篇文档保存一个向量,而多向量检索可能要保存几十甚至几百个向量。

更实用的架构通常是两阶段检索:

  1. 用单向量或稀疏检索,从全库召回几十到几百篇候选文档。
  2. 只对候选集执行多向量 MaxSim,获得更精细的排序。

下面是可以改造进服务端的批量重排函数:

from sentence_transformers import SentenceTransformer
import torch
import torch.nn.functional as F

model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")


def rerank(query: str, candidates: list[dict], top_k: int = 5) -> list[dict]:
    texts = [query] + [item["text"] for item in candidates]
    vectors = model.encode(
        texts,
        output_value="token_embeddings",
        convert_to_tensor=True,
        show_progress_bar=False,
    )
    vectors = [F.normalize(value, p=2, dim=-1) for value in vectors]

    query_vectors = vectors[0]
    results = []

    for candidate, document_vectors in zip(candidates, vectors[1:]):
        matrix = query_vectors @ document_vectors.T
        score = matrix.max(dim=1).values.mean().item()
        results.append({**candidate, "late_interaction_score": score})

    return sorted(
        results,
        key=lambda item: item["late_interaction_score"],
        reverse=True,
    )[:top_k]

真实系统还需要处理批量张量、GPU 显存、最大文本长度和文档切块。逐篇执行 Python 循环适合验证算法,但吞吐量较高时,应按长度分桶并批量计算,避免短文本被大量填充。

评分规则不只有一种

MaxSim 求和会让查询长度影响分数范围。示例采用平均值,让不同长度的查询更容易比较,但具体模型可能是在求和目标上训练的,擅自修改聚合方式会改变排序分布。

还需要明确处理以下问题:

  • 特殊词元是否参与匹配,例如 [CLS][SEP] 和填充词元。
  • 查询词元是否需要权重,停用词是否会产生过多贡献。
  • 长文档是否按窗口切块,以及同一文档的多个块如何合并。
  • 文档向量是否量化为 FP16、INT8 或更低精度。
  • 检索分数是否需要与 BM25、单向量分数做归一化融合。

这些不是外围优化,而是检索质量和成本的组成部分。尤其是填充词元,如果没有通过 attention mask 排除,可能被错误地选为最佳匹配。

采用前的检查清单

多向量晚交互适合关键词、实体、属性和局部语义都很重要的搜索场景,例如技术文档、商品检索和专业知识库。它通常能换来更细粒度的相关性判断,但代价是更大的索引、更复杂的执行路径和更高的重排延迟。

上线前应至少确认:模型确实针对晚交互训练;离线评测使用业务查询和人工相关性标签;索引体积在文档增长后仍可接受;候选集大小与延迟预算匹配;切块、截断和特殊词元规则与训练阶段一致。

如果单向量召回已经稳定,合理的落地顺序是先把晚交互作为候选集重排器,而不是立即替换整个向量索引。这样既能测量真实的排序收益,也能把存储和计算风险控制在较小范围内。


相关推荐