最近审阅:2026-10

用户搜“attention mask shape mismatch”。向量检索(retrieval)找到了讲注意力原理的好文章,关键词检索却找到了包含同一报错和张量形状的修复记录。两边都有道理,只是“主题相关”和“能解决这个错误”不是同一件事。

接着双塔那一篇,这里看怎样把不同检索结果合起来,再决定哪些值得仔细读。只想理解设计,可以先看文档例子;想实现融合,直接看排名融合。

1. 先看两路分别漏了什么

做一个四篇文档的教学资料库:

文档 内容 关键词名次 向量名次
A 一份旧版本报错记录,包含完全相同的错误字符串 1 未进入前 3
B 解释 mask 广播规则,并给出当前版本的修复方法 2 1
C Transformer 总览,没有讲到这个错误 3 2
D 用另一种说法解释同一个维度问题 未进入前 3 3

关键词方法(例如 BM25)根据词项匹配打分,精确标识符、错误码和罕见名字常常很重要。Dense retrieval 可以找不同说法表达的相近语义,但也可能把“同主题”排在“满足细节限制”之前。没有哪一路天然理解全部需求。

想先弄清关键词分数怎么来的,可以读 TF-IDF 与 BM25:词频、稀有程度和长度各改了什么。

混合之前先看各自独有的相关结果。如果两路总是返回同一批内容,融合收益可能很小;如果第二路只增加噪声,多一路还可能让后面的模型更忙。

2. 为什么不直接把两个分数相加?

一边可能是 BM25 分数 12.4,另一边是余弦相似度 0.81。它们不在同一个尺度,直接相加通常让数值大的那边主导。分数归一化是一种办法,但查询分布变化后还要检查校准(calibration)。

一个简单的起点是倒数排名融合(Reciprocal Rank Fusion, RRF):只看文档在各路的名次,不要求原始分数可比。

$$ \mathrm{RRF}(d)=\sum_{r:d\in L_r}\frac{1}{k+\mathrm{rank}_r(d)}. $$

名次从 1 开始。$L_r$ 是第 $r$ 路实际返回的列表;没有进入列表的文档在这一项贡献 0。$k$ 平滑名次差异,和最终要返回多少条的 $K$ 不是一回事。公式和实现约定可对照 Elasticsearch 的 RRF 文档。

教学例子取 $k=10$:B 的分数为 $1/12+1/11\approx0.1742$,C 为 $1/13+1/12\approx0.1603$,A 为 $1/11\approx0.0909$,D 为 $1/13\approx0.0769$。B、C 得到了两路支持,即使 A 在关键词里排第一,也未必最终排第一。注意 $k=10$ 是为了让分数差别看得清;常见默认值是 60,例如 Elasticsearch 的 rank_constant。换成 60,这个例子的顺序不变,只是分数挨得更近。

动画 RRF:只看名次,把两路结果合起来

这张图需要启用 JavaScript。

def reciprocal_rank_fusion(rankings, constant=10):
    if constant < 0:
        raise ValueError("The constant must be non-negative")
    scores = {}
    for ranking in rankings:
        if len(set(ranking)) != len(ranking):
            raise ValueError("Deduplicate each source before fusion")
        for position, document in enumerate(ranking, start=1):
            scores[document] = scores.get(document, 0) + 1 / (constant + position)
    return sorted(scores, key=lambda document: (-scores[document], document))

assert reciprocal_rank_fusion([["A", "B", "C"], ["B", "C", "D"]]) == ["B", "C", "A", "D"]

这段代码把相同分数按文档 ID 排序,只是为了可复现。实际系统要明确自己的并列处理规则。

3. 少取几条,结果会怎么变?

沿用上面的四篇文档,只改变实际返回的列表。常数始终为 10,不偷用全库排名:

输入设置 融合结果 发生了什么
两路各取前 3 B → C → A → D B、C 各收到两路支持
只保留关键词前 3 A → B → C D 不再有进入结果的机会
两路各取前 1 A、B 并列 B 失去关键词第二名的支持

试着关掉向量那一路:D 就根本没有机会进入候选(candidate)集。再把每路只取前 1:B 失去了第二名那一份支持。融合器只能利用实际拿到的结果。 调大平滑常数也不会让没召回的内容凭空出现。

RRF 忽略了原始分数之间的差距,无法知道第一名比第二名强一点还是强很多。某一路若重复返回同一文档,也不能靠重复记分获利。它适合当透明的基线(baseline),不是所有任务都最优的融合器。

4. 重排补哪一课?

融合后,仍然需要判断 A 的修复方法是否过期,B 是否真的满足当前代码环境。Cross-encoder 可以联合读取 query 和候选,利用更细的对应关系重排;也可以用带明确标准的 LLM judge,但要测成本、一致性,以及它会不会被候选文档里夹带的指令带偏。

看一个具体的打分方式:Qwen3-Reranker 把 query 和候选拼进同一个 prompt,问它“是否满足要求”,只取最后一个位置上 “yes” 和 “no” 两个 token 的 logit,在这两个之间做 softmax,把 “yes” 的概率当分数。若两个 logit 分别是 2 和 0,分数就是 $e^2/(e^2+1)\approx0.881$。这是手算的示意,不是模型输出;它只说明分数来自同一次联合阅读,而不是两个独立向量的点积(inner product)。

权限允许的文档
    ├─ 关键词前若干条 ─┐
    └─ 向量前若干条 ──┴→ 去重、融合 → 固定候选预算 → 重排 → 证据上下文

权限要在内容送给无权访问它的模型或服务前落实;不能指望最后生成答案时“别说出来”。如果索引不能做过滤,也要在可信的检索层完成检查,同时考虑过滤之后不足额的问题。

重排只能重排收到的候选。如果正确文档根本没入库或没被召回,换更贵的 reranker 也找不回来。这里说的是纯重排;允许再次搜索的 agent 是另一种设计,需要单独计算预算。

5. 同样花费下,怎样比较?

别拿“向量取 100 条”对比“向量取 100 条,再多加关键词 100 条”,然后把提升全归给算法。这个实验同时增加了候选数量和后续计算。

对照 保持什么不变 主要想回答什么
只用向量 vs 混合 最终候选数、语料快照、评估 query 新来源能否补上独有的相关文档?
RRF vs 学习式融合 同一组来源结果 融合规则本身是否更合适?
不重排 vs 重排 候选集和标注 重排能否把已有的好内容排上来?
不同预算 明确记录每阶段候选数、延迟和费用 多花的计算是否值得?

假设有 3 篇已知相关文档,两路分别命中 $\{B\}$ 和 $\{B,D\}$。并集命中 2 篇,但不能把两路命中数相加成 3。重复率和独有正例很有用,前提是承认标注可能不全:没有被标记相关,不自动等于不相关。

6. 别把一堆相近段落都塞给模型

最后还要在上下文预算里选证据。重复的五段解释可能不如一段解释加一段版本说明;拼接时保留文档来源、时间和段落边界,方便检查回答究竟根据了什么。

上下文更长不保证证据更好用。Lost in the Middle 在其测试设置中观察到模型对相关信息位置的敏感性。这不能推出所有模型都一样,但提醒我们:最终应测答案是否有依据,不能只看召回阶段分数。

接着读分层评估:把“没找到”和“找到了却没用好”拆开,下一步改什么才有依据。

本页术语 5
检索 retrieval
从外部语料里找出相关内容
校准 calibration
模型给出的置信度是否等于真实正确率
候选 candidate
召回阶段捞出来、等待精排挑选的内容
基线 baseline
用来比较新方法的参考方法或配置
点积 inner product
两个向量对应分量相乘再求和,双塔的打分方式