AI 工程基础体系 · 第 89/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
混合检索:BM25、向量、过滤、融合排序和权重调优
在 RAG(Retrieval-Augmented Generation,检索增强生成)系统中,生成模型并不直接“知道”企业内部文档。典型流程是先根据用户问题检索候选片段,再把候选片段作为上下文交给生成模型。RAG 原始论文将这一过程描述为“参数化记忆”和“外部非参数化记忆”的结合:模型参数保存通用能力,检索库保存可更新的知识(Retrieval-Augmented Generation Paper)。
检索质量决定了生成模型能看到什么。一个答案生成得很流畅,并不意味着检索正确;如果检索阶段漏掉了关键文档,生成模型通常只能基于错误或不完整的上下文进行推断。因此,生产级 RAG 很少只依赖一种检索方式,而是将:
- BM25:基于词项匹配的稀疏检索;
- 向量检索:基于语义表示相似度的稠密检索;
- 过滤:基于元数据、租户、权限和时间等条件的硬约束;
- 融合排序:合并不同检索器的候选结果;
- 权重调优:根据离线评测和线上反馈确定融合策略;
放在同一个检索系统中设计。
一、先明确检索系统要优化什么
给定用户查询 ,文档集合为:
检索器的目标不是简单地找“最相似”的文档,而是在有限的上下文窗口、延迟和成本约束下,把与问题相关、用户有权访问、能够支持答案的文档排在前面。
可以把一个候选文档的有效性拆成几个条件:
其中:
- :文档是否回答当前问题;
- :用户 是否有权限访问;
- :文档是否满足时效要求。
这里的“相关”还可以细分为:
- 主题相关:文档讨论了用户询问的对象;
- 事实相关:文档包含问题所需的具体事实;
- 证据充分:文档能够支撑生成答案,而不仅仅是提到几个关键词;
- 粒度匹配:文档片段足够完整,没有因为切块而失去条件、例外或结论。
BM25 和向量检索主要解决第一类和第二类问题,但它们不能替代权限校验,也不能自动保证证据充分。过滤和后续重排序必须参与整个流程。
二、BM25:基于词项匹配的稀疏检索
2.1 BM25 解决什么问题
BM25 是一种经典的词项检索函数。它判断查询中的词是否出现在文档中,并根据以下因素计算相关性:
- 查询词是否出现在文档中;
- 该词在文档中出现了多少次;
- 该词在整个语料库中是否稀有;
- 文档是否过长;
- 查询词频是否需要饱和。
“稀疏”意味着文档可以表示成一个高维词项空间中的稀疏向量:大多数词项的权重为零。BM25 不需要训练神经网络,因此具有可解释、成本低、对精确术语敏感等特性。
例如,用户查询:
PostgreSQL 16 logical replication slot
BM25 能够直接利用 PostgreSQL、logical replication slot 等词项。即使向量模型没有充分理解某个新版本术语,BM25 仍可能准确命中包含该字符串的文档。
2.2 BM25 的公式
常见的 BM25 形式为:
其中:
- :查询;
- :文档;
- :查询中的词项;
- :词项 在文档 中出现的次数;
- :文档长度;
- :语料库中文档平均长度;
- :词频饱和参数;
- :文档长度归一化参数;
- :词项逆文档频率。
一种常见的 IDF 定义为:
其中:
- :文档总数;
- :包含词项 的文档数。
IDF 的直觉是:一个词出现在所有文档中,就没有区分度;只出现在少数文档中,则更能说明文档与查询相关。
2.3 词频为什么要“饱和”
如果一个文档中出现一次“退款”,它可能已经说明文档与退款有关。出现 20 次并不意味着相关性是出现一次的 20 倍。因此 BM25 对词频使用饱和函数。
忽略长度归一化时,词频部分近似为:
取 :
- 时,;
- 时,;
- 时,。
词频从 1 增加到 2 有明显收益,从 2 增加到 10 的收益则逐渐变小。这避免了长文档因为重复出现同一术语而无限领先。
2.4 文档长度归一化
如果只按词频计分,长文档通常更容易包含查询词。BM25 使用:
调整长度:
- 当 时,长度因子为 1;
- 当文档比平均长度长时,分母变大,得分降低;
- 当文档比平均长度短时,分母变小,单位词频贡献变大。
参数 越大,长度归一化越强。常见实现会使用 和 的默认值,但默认值并不适合所有语料:
- FAQ 或短条款集合的文档长度差异小, 的影响有限;
- 长篇技术文档与短代码片段混合时,长度归一化会明显影响结果;
- 中文分词、英文 tokenization、代码符号切分方式变化后,文档长度的统计含义也会变化。
因此不能把 BM25 参数视为与数据无关的常量。
2.5 一个完整的 BM25 计算例子
假设语料库有 篇文档,平均长度 。查询为:
退款 延迟
设置:
假设:
- “退款”出现在 10 篇文档中;
- “延迟”出现在 5 篇文档中;
- 文档 长度为 100;
- 中“退款”出现 1 次,“延迟”出现 1 次。
两个词的 IDF 分别为:
由于 的长度等于平均长度:
每个词的词频部分为:
所以:
再看文档 :
- 长度为 120;
- “退款”出现 2 次;
- 不包含“延迟”。
长度因子为:
“退款”的词频部分为:
因此:
虽然 中“退款”出现了两次,但它缺少“延迟”,所以仍然低于同时包含两个查询词的 。
这个例子说明,BM25 的得分不是“匹配词数量”的简单计数,而是词项稀有度、词频饱和和长度归一化共同作用的结果。
2.6 BM25 的边界
BM25 的强项是精确匹配,但它不天然理解同义表达:
- 查询是“如何取消订单”,文档写的是“撤销购买”;
- 查询是“数据库连接池耗尽”,文档写的是“connection pool saturation”;
- 查询使用新产品名,文档使用旧产品名。
除非分词器、同义词表或扩展查询处理了这些关系,否则 BM25 可能漏检。
BM25 还有几个容易被忽略的问题:
-
分词质量决定上限
中文必须考虑分词、词典和专有名词;英文要处理大小写、词干化和连字符;代码则可能需要保留标识符和符号。 -
短词可能产生噪声
“AI”“云”“网”等高频或歧义词可能贡献有限或带来大量候选。 -
拼写错误不容易处理
Postgres和PostgreSQL是否等价,取决于索引和查询分析配置。 -
跨语言语义匹配能力弱
中文查询和英文文档之间,BM25 通常需要翻译、别名或多语言分析器支持。
因此,BM25 不是向量检索的过渡技术,而是对精确术语、编号、版本号、错误码和实体名仍然非常重要的一类检索器。
三、向量检索:基于语义表示的稠密检索
3.1 向量检索的基本过程
向量检索使用 embedding 模型将查询和文档映射到同一个向量空间:
其中:
- :embedding 模型;
- :查询向量;
- :文档向量。
检索时计算查询向量和文档向量之间的相似度。常用度量包括:
余弦相似度
它只关注方向,不关注向量长度。若向量已经归一化,则余弦相似度等于点积。
点积
适合某些训练目标,但向量长度可能影响结果。
欧氏距离
在向量已经归一化的条件下,欧氏距离与余弦相似度之间存在单调关系;未归一化时则不等价。
使用哪种距离必须与 embedding 模型的训练和向量数据库索引配置保持一致。不能先用点积建库,再在查询阶段把结果解释为余弦相似度。
3.2 向量检索为什么能召回同义表达
如果 embedding 模型在训练中见过大量语义相近的表达,它可能把:
- “取消订阅”
- “停止续费”
- “关闭自动续费”
映射到相近区域。这样,即使文档和查询没有共享完全相同的词项,向量检索仍可能召回它。
不过,向量相似并不等于事实相关。一个关于“如何申请退款”的文档,可能与“退款是否需要审批”的问题语义相近,却不能直接回答审批规则。向量检索擅长语义召回,不保证精确事实匹配。
3.3 近似最近邻和召回率
对每个查询都与全部文档计算距离,复杂度大致为:
其中 是文档数, 是向量维度。大规模系统通常使用 ANN(Approximate Nearest Neighbor,近似最近邻)索引,例如图索引或倒排聚类索引。
ANN 用少量搜索代价换取可接受的召回率。这里有两个不同的“召回”概念:
- 检索任务的 Recall@k:前 个结果中是否包含相关文档;
- ANN 索引召回率:近似索引找到的真实近邻比例。
如果 ANN 搜索参数过低,向量模型本身可能是正确的,但索引搜索没有找到应有的近邻。诊断时需要用精确暴力搜索作为基线,区分模型问题和 ANN 参数问题。
3.4 向量检索的失败模式
向量检索常见的失败表现包括:
- 召回主题相近但版本错误的文档;
- 把“如何配置”与“为什么失败”混为一类;
- 对精确错误码、订单号、函数名召回不足;
- 受文档切块影响,召回片段缺失前置条件;
- embedding 模型对内部缩写、产品名和领域术语理解不足;
- 查询和文档语言不同,模型的跨语言能力不足;
- 文档更新后向量没有重建,导致语义索引陈旧。
因此,向量检索通常需要 BM25、元数据过滤和后续重排序共同约束。
四、BM25 与向量检索的互补关系
可以把两种检索器的能力抽象为:
| 场景 | BM25 | 向量检索 |
|---|---|---|
| 错误码、订单号、版本号 | 强 | 可能弱 |
| 精确产品名和 API 名称 | 强 | 取决于模型 |
| 同义表达 | 弱 | 强 |
| 口语化问题 | 中等 | 通常更强 |
| 代码和配置键 | 强 | 可能产生语义近似噪声 |
| 跨语言语义匹配 | 弱 | 取决于多语言模型 |
| 新术语、内部缩写 | 需要词典 | 需要模型或微调支持 |
| 解释精确词项来源 | 清晰 | 较难解释 |
两者的结果集并不是简单的重复。对查询:
Redis ERR max number of clients reached 如何处理
BM25 可以通过错误字符串直接命中故障手册;向量检索可能召回“Redis 连接数配置”“连接池耗尽”“客户端限制”等语义相关文档。合并后,系统既能保留精确错误信息,又能覆盖问题的解释和处理方案。
五、过滤:不是排序,而是候选集合约束
5.1 过滤的形式化定义
过滤条件定义一个允许集合:
其中 可以包含:
tenant_id = 当前租户;department in 用户部门集合;doc_type = policy;language = zh-CN;updated_at >= 某个时间;environment = production;classification <= 用户许可级别。
理想情况下,检索应该在 上进行:
而不是先在整个 上取结果,再试图删除无权文档。
5.2 预过滤与后过滤
预过滤
先应用过滤条件,再执行 BM25 或向量检索:
用户身份
-> 计算权限和元数据条件
-> 在满足条件的文档集合中检索
-> 融合排序
-> 重排序
优点:
- 不会把明显不符合条件的文档计入候选;
- 对权限隔离更安全;
- 当过滤条件选择性高时,可以显著减少搜索空间。
缺点:
- 过滤条件复杂时,索引实现困难;
- 允许集合太小或太碎时,ANN 搜索可能退化;
- 某些系统的向量索引只支持有限的元数据过滤表达式。
后过滤
先检索,再过滤:
全库检索 top-K
-> 删除不满足条件的文档
-> 返回剩余结果
它实现简单,但有一个直接的数量问题:如果先取 10 条,其中 8 条因权限或部门条件被删除,最终可能只剩 2 条。扩大初始候选数可以缓解数量不足,但不能弥补候选集合本身没有覆盖目标文档的情况。
更严重的是,权限过滤不能依赖普通后过滤。如果无权限文档已经进入日志、缓存、调试输出、重排序模型或生成模型上下文,就可能造成信息泄露。权限条件应尽可能在检索前生效,并在返回前进行第二次防御性校验。
5.3 过滤选择性与检索质量
定义过滤选择性:
当 很高时,例如只过滤一个常见文档类型,预过滤和后过滤的差别可能不大。
当 很低时,例如一个用户只能访问全库的 ,后过滤通常会产生:
- 候选数量不足;
- 相关文档排名被无权文档占用;
- 需要过度扩大 ,导致延迟和成本上升;
- 融合时不同检索器对有效文档的覆盖不稳定。
因此,租户、用户组和 ACL 等高选择性条件,应成为索引分区或原生过滤条件的一部分,而不是事后清理。
5.4 过滤与 BM25、向量检索的交互
过滤不是只应用于某一个检索器。一个一致的流程应当是:
如果 BM25 使用了权限过滤,而向量检索没有使用,融合阶段仍然可能把无权文档带入候选。反过来也一样。
需要区分两种过滤:
- 安全过滤:租户、ACL、密级,违反后会造成数据泄露;
- 质量过滤:语言、文档类型、时间范围,违反后主要影响相关性。
安全过滤必须强制执行;质量过滤则可以在特定场景下作为可配置召回策略。
六、融合排序:如何合并两个不同的分数体系
BM25 分数和向量相似度通常不能直接相加。
原因是:
- BM25 分数受查询词数量、文档长度和语料统计影响;
- 向量相似度通常落在固定或近似固定区间;
- 不同查询的 BM25 分数分布可能完全不同;
- 不同 embedding 模型的相似度分布也不同。
例如一次查询的 BM25 分数可能是:
另一次查询可能是:
如果直接把它们与向量分数相加,权重的含义会随查询变化。
6.1 基于分数归一化的加权融合
一种简单方法是先把每个检索器的分数归一化,再计算加权和。
Min-Max 归一化为:
其中 是第 个检索器的原始分数。
两个检索器的融合分数为:
表示归一化后 BM25 的权重。
完整算例
四个文档的结果如下:
| 文档 | BM25 分数 | 向量分数 |
|---|---|---|
| A | 8.0 | 0.4 |
| B | 5.0 | 1.0 |
| C | 2.0 | 0.7 |
| D | 0.0 | 0.1 |
BM25 的最小值为 0,最大值为 8,因此归一化后:
向量分数的最小值为 0.1,最大值为 1.0,因此:
如果 ,则:
最终顺序为:
BM25 单独排序是:
向量单独排序是:
融合后,B 获得了语义检索的优势,A 仍然保留了精确词项匹配的优势。
Min-Max 的风险
Min-Max 归一化依赖当前候选集合的极值。如果某个查询出现异常高分,其他文档的归一化分数会被压缩。候选数量变化也会改变极值,从而改变同一文档的融合分数。
因此,Min-Max 适合简单实验和分布稳定的场景,但不应未经验证地视为统一解决方案。生产系统也可能使用:
- Z-score;
- 百分位归一化;
- 基于验证集估计的分布校准;
- 对每个检索器输出 rank 而不是原始分数。
6.2 RRF:基于名次的融合
RRF(Reciprocal Rank Fusion)不直接比较分数,而是比较文档在各个结果列表中的名次:
其中:
- :检索器集合;
- :文档 在检索器 中的名次,从 1 开始;
- :平滑常数,常见实现会使用一个较大的正数。
继续使用上面的排序:
- BM25:A、B、C、D;
- 向量:B、C、A、D。
取 :
所以顺序为:
RRF 的优点是避免了 BM25 和向量分数的量纲问题,并且对“多个检索器都认为重要”的文档较友好。它的缺点是丢失了原始分数的距离信息:
- 第一名和第二名可能只差一点;
- 也可能第一名远远领先第二名;
- RRF 对这两种情况的处理相近。
RRF 适合作为稳健基线,尤其是在尚未建立可靠分数校准和标注数据时。
6.3 一个可执行的融合示例
下面的代码只依赖 Python 标准库,演示 Min-Max 加权融合和 RRF。它不执行 BM25 或向量搜索,而是假设两个检索器已经返回文档分数。
from typing import Dict, List, Tuple
bm25 = {
"A": 8.0,
"B": 5.0,
"C": 2.0,
"D": 0.0,
}
vector = {
"A": 0.4,
"B": 1.0,
"C": 0.7,
"D": 0.1,
}
def min_max(scores: Dict[str, float]) -> Dict[str, float]:
values = list(scores.values())
low, high = min(values), max(values)
# 所有分数相同,说明该检索器没有区分度。
# 此时统一返回 0,避免除零;也可以根据业务改为 1。
if high == low:
return {doc_id: 0.0 for doc_id in scores}
return {
doc_id: (score - low) / (high - low)
for doc_id, score in scores.items()
}
def weighted_fusion(
bm25_scores: Dict[str, float],
vector_scores: Dict[str, float],
alpha: float = 0.5,
) -> List[Tuple[str, float]]:
bm25_norm = min_max(bm25_scores)
vector_norm = min_max(vector_scores)
doc_ids = set(bm25_scores) | set(vector_scores)
fused = {}
for doc_id in doc_ids:
b = bm25_norm.get(doc_id, 0.0)
v = vector_norm.get(doc_id, 0.0)
fused[doc_id] = alpha * b + (1 - alpha) * v
return sorted(fused.items(), key=lambda x: (-x[1], x[0]))
def rrf(
ranked_lists: List[List[str]],
k: int = 60,
) -> List[Tuple[str, float]]:
scores = {}
for ranked_list in ranked_lists:
for rank, doc_id in enumerate(ranked_list, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(scores.items(), key=lambda x: (-x[1], x[0]))
print("weighted:", weighted_fusion(bm25, vector, alpha=0.5))
print("rrf:", rrf([
["A", "B", "C", "D"],
["B", "C", "A", "D"],
]))
预期输出的核心顺序是:
weighted: B, A, C, D
rrf: B, A, C, D
实际浮点数会包含小数误差。代码中对所有分数相同的情况进行了处理;如果某个检索器返回的候选只有一个,或者所有候选分数完全一致,Min-Max 归一化本身没有信息可以利用,不能人为制造排序差异。
6.4 融合不是重排序
融合排序和重排序是两个不同阶段。
融合排序
作用是合并多个初始检索器:
BM25 top 50
向量 top 50
↓
并集、去重、融合
↓
候选 top 50 或 top 100
重排序
通常使用更复杂的模型,对查询和候选文本进行联合判断。例如交叉编码器可以输入:
直接预测相关性,而不是分别编码查询和文档。它的表达能力通常更强,但计算成本更高,因此只适合处理较小的候选集。
一个常见的三级结构是:
第一阶段:BM25 + 向量召回,扩大覆盖面
第二阶段:融合排序,形成候选集
第三阶段:交叉编码器或规则重排,优化前几名
不能把“调大 BM25 权重”当成“增加重排序能力”。权重只能改变已有候选的相对顺序,不能召回任何一个原本没有进入候选集合的文档。
七、权重调优:从经验参数到可验证的选择
7.1 权重到底影响什么
以:
为例, 越大,系统越偏向 BM25;越小,越偏向向量检索。
但它不是“BM25 可信度为 0.7、向量可信度为 0.3”的概率解释。它只表示在当前分数校准方法下,两种信号对最终排序的相对贡献。
权重是否合理,取决于:
- 查询类型;
- 语料领域;
- 分词质量;
- embedding 模型;
- 文档切块策略;
- 过滤后的集合规模;
- 评测指标;
- 下游生成模型对上下文的使用方式。
7.2 建立可调优的数据集
调权重前需要一组查询—文档标注数据。最简单的格式是:
| query_id | query | doc_id | relevance |
|---|---|---|---|
| q1 | 如何取消自动续费 | d10 | 3 |
| q1 | 退款审批规则 | d11 | 0 |
| q1 | 订阅管理说明 | d12 | 2 |
相关性等级需要先定义,例如:
- 3:直接、完整回答问题;
- 2:部分回答或提供重要背景;
- 1:主题相关但不能支撑答案;
- 0:无关或不应使用。
如果系统带权限,还必须在评测样本中记录用户身份或权限条件。否则,离线评测可能把用户无权访问的文档当作“正确答案”,导致调优结果与生产安全策略冲突。
7.3 选择评测指标
Recall@K
它适合评估第一阶段召回是否漏掉证据。
Precision@K
它关注候选中噪声多少。
MRR
对于每个查询只关心第一个相关结果:
适合 FAQ、单事实问答等“尽快找到一个正确文档”的场景。
nDCG@K
当相关性有多个等级时,nDCG 比二元命中更合适:
再除以理想排序下的 DCG,得到 nDCG。它会同时考虑相关性等级和排名位置。
对 RAG 来说,通常至少要同时观察:
- 召回 Recall@K;
- 排序 nDCG@K;
- 最终上下文中的证据覆盖率;
- 生成答案的事实正确性;
- 引用是否真正支持答案;
- 延迟和成本。
只看最终生成答案,无法区分是检索失败还是生成模型使用上下文失败。
7.4 网格搜索一个融合权重
如果使用单一全局权重,可以先做简单网格搜索:
每个候选 都在同一个验证集上计算 nDCG@10 或 Recall@50,选择验证集表现最好的参数,再在独立测试集上确认。
伪代码如下:
best_alpha = None
best_score = float("-inf")
for alpha in [i / 10 for i in range(11)]:
rankings = []
for query in validation_queries:
bm25_results = retrieve_bm25(query, top_k=50)
vector_results = retrieve_vector(query, top_k=50)
candidates = fuse(
bm25_results,
vector_results,
alpha=alpha,
)
rankings.append(candidates)
score = ndcg_at_k(rankings, validation_labels, k=10)
if score > best_score:
best_score = score
best_alpha = alpha
这里的关键不是搜索算法,而是评测流程必须固定:
- 相同的查询集合;
- 相同的权限过滤;
- 相同的候选数量;
- 相同的去重规则;
- 相同的 chunk 版本;
- 相同的文档新鲜度。
如果调权重时 BM25 使用了全库,而测试时使用了租户过滤,结果没有可比性。
7.5 为什么全局权重常常不够
不同查询需要不同的检索偏好。
精确查询
例如:
HTTP 429 rate_limit_exceeded
这类查询包含错误码和固定字符串,BM25 往往更可靠。
解释型查询
例如:
为什么批处理任务会在高峰期变慢
这类查询可能需要向量检索覆盖“资源争用”“吞吐下降”“队列堆积”等不同表达。
混合查询
例如:
Kafka 3.6 consumer group rebalance 原因
既有精确版本和组件名,又有语义问题,两个检索器都重要。
因此可以使用查询分类或查询特征进行动态调权:
特征可以包括:
- 查询中是否包含数字、版本号、错误码;
- 查询长度;
- 是否包含代码符号;
- 是否命中内部术语词典;
- BM25 和向量结果的分数间隔;
- 两个检索器结果的重合率。
但动态调权也会增加调试难度。第一步通常应先建立可靠的全局基线,再确认分片调权确实带来稳定收益。
7.6 从权重融合到学习排序
当标注数据足够多时,可以训练 Learning to Rank(学习排序)模型。对每个候选文档构造特征:
然后学习:
模型可以是逻辑回归、梯度提升树或更复杂的排序模型。需要注意:
- 训练标签必须来自真实相关性,而不能只来自现有检索器;
- 训练、验证、测试要按查询划分,避免同一问题模板泄漏;
- 权限特征不能被用来推断或绕过权限;
- 文档新鲜度必须按照上线时可获得的信息构造,避免时间穿越;
- 排序模型上线后要监控特征分布漂移。
学习排序并不会消除 BM25 和向量检索的召回边界。如果相关文档没有进入候选集,排序模型无法恢复它。
八、一个生产级混合检索数据流
混合检索通常包含离线索引和在线查询两条路径。
flowchart TD
A[原始文档] --> B[解析与清洗]
B --> C[切块]
C --> D[生成元数据与 ACL]
D --> E1[建立 BM25 倒排索引]
D --> E2[生成 Embedding]
E2 --> E3[建立向量索引]
Q[用户查询] --> Q1[身份认证与权限解析]
Q1 --> Q2[查询分析与过滤条件构造]
Q2 --> R1[BM25 检索]
Q2 --> R2[向量检索]
R1 --> F[候选并集与去重]
R2 --> F
F --> F2[融合排序]
F2 --> R3[可选重排序]
R3 --> V[最终 ACL 校验与去重]
V --> Ctx[上下文压缩与拼接]
Ctx --> G[生成模型]
G --> O[答案与引用]
8.1 离线索引阶段
原始文档进入索引前通常要经过:
- 解析 PDF、HTML、Markdown、Office 或数据库记录;
- 去除导航栏、页脚、重复模板等噪声;
- 按标题、段落、表格或代码结构切块;
- 为每个 chunk 保存
doc_id、标题、来源、版本、时间、租户和 ACL; - 使用同一份规范化文本建立 BM25 索引;
- 使用 embedding 模型生成向量;
- 将文本索引、向量索引和元数据索引建立可关联关系。
doc_id 必须稳定。若每次重建向量后生成新的随机 ID,BM25 结果和向量结果就难以去重,文档更新、删除和权限撤销也会变得复杂。
8.2 文档更新与索引一致性
文档内容更新时,至少涉及三类状态:
源文档版本 V2
-> chunk 版本 V2
-> BM25 索引 V2
-> 向量索引 V2
-> 元数据/ACL V2
如果 BM25 已更新而向量仍是旧版本,融合结果可能同时包含新旧片段。如果 ACL 先撤销但向量索引尚未删除,在线路径必须依靠实时权限校验阻止旧向量结果返回。
常见的状态处理方式是:
- 用文档版本号标记每个 chunk;
- 先生成新版本索引;
- 完成校验后切换索引别名或版本指针;
- 删除旧版本;
- 对权限变更使用更短的同步路径,必要时在查询时读取最新 ACL。
不要只依赖“最终一致”而忽略撤权场景。内容更新延迟主要影响新鲜度,权限更新延迟则可能影响安全性。
8.3 在线查询的并发
BM25 和向量检索通常可以并发执行:
bm25_future = executor.submit(
retrieve_bm25, query, filters, 50
)
vector_future = executor.submit(
retrieve_vector, query_vector, filters, 50
)
bm25_results = bm25_future.result(timeout=0.2)
vector_results = vector_future.result(timeout=0.2)
并发执行可以降低总延迟,但会引入故障路径:
- BM25 超时,向量结果仍可降级返回;
- 向量服务不可用,BM25 可以承担精确检索;
- 两者都失败时,应返回明确的检索失败状态,而不是让生成模型假装有证据;
- 一个检索器返回空结果,融合器必须处理,而不能因除零或空列表崩溃;
- 任何一个服务的超时都不能无限拖延生成请求。
降级策略应与查询类型相关。对于错误码和 API 名称查询,BM25 失败的影响可能比向量检索失败更大;对于自然语言解释问题,情况可能相反。
九、候选集合、去重和文档粒度
9.1 为什么不能直接把两个 top-k 拼接
如果 BM25 和向量检索都返回同一文档的不同 chunk,直接拼接会造成:
- 同一文档占据多个上下文位置;
- 其他来源被挤出;
- 生成模型重复看到近似内容;
- token 成本增加;
- 评测时看似命中多个结果,实际证据覆盖没有增加。
通常需要先按稳定键去重,例如:
如果要限制同一文档最多出现 个片段,则可以在融合后增加多样性约束:
不过,完全按文档去重也可能损失证据。一个长文档的两个不同片段可能分别包含定义和例外条件。因此去重粒度应根据文档结构决定。
9.2 Chunk 大小与混合检索的关系
切块过小:
- BM25 可能命中关键词,但片段缺乏上下文;
- 向量表示不稳定;
- 条件、例外和结论可能被分到不同片段。
切块过大:
- BM25 的长度归一化可能降低精确词项的相对贡献;
- 向量表示混入多个主题;
- 单个候选消耗更多上下文窗口;
- 重排序和生成成本上升。
应把“召回单位”和“展示单位”分开考虑:
- 检索可以按较小 chunk 找到证据;
- 返回上下文时补充父标题、相邻段落或表格上下文;
- 引用仍然指向稳定的文档和段落位置。
十、过滤、权限与 RAG 安全边界
权限不是一个普通排序特征。下面两个文档即使语义和关键词都高度匹配,也不能因为得分高而返回:
d1: 财务部门的薪酬调整方案
d2: 全员可见的薪酬政策摘要
如果当前用户无权访问 ,则 必须从允许集合中删除,而不是给它一个很大的负分。原因是“排序后隐藏”容易在其他环节泄露:
- 检索日志记录了标题;
- 调试接口返回了候选;
- 重排序服务收到完整文本;
- 缓存键没有包含用户权限;
- 生成上下文中残留被过滤的片段;
- 引用生成器根据候选 ID 暴露文档存在性。
生产系统至少需要:
- 身份认证后构造用户权限上下文;
- 检索阶段执行租户和 ACL 过滤;
- 融合和重排序只处理允许集合;
- 输出前再次校验权限;
- 缓存必须绑定租户、用户组或权限版本;
- 文档删除和撤权要有可验证的传播路径;
- 审计日志记录“使用了哪个版本的权限快照”。
如果权限条件变化频繁,最好将权限过滤设计为检索基础设施的一等能力,而不是在 RAG 应用代码里散落多个 if 判断。
十一、权重调优之外的关键参数
权重不是唯一影响结果的参数。至少要同时调试以下变量:
11.1 两个检索器的初始 top-k
如果最终只需要 10 个上下文片段,BM25 和向量检索各取 10 个,候选覆盖可能不足。常见做法是先扩大候选:
BM25 top-50
向量 top-50
合并后最多 100 个候选
重排序后保留 top-10
但候选越大,重排序延迟和成本越高。应通过 Recall@K 曲线寻找边际收益,而不是无限增加 K。
11.2 过滤后的有效候选数
过滤会使初始 top-k 中一部分结果失效。系统应监控:
- 过滤前候选数;
- 过滤后候选数;
- 每个检索器的有效候选数;
- 最终可用于生成的片段数。
如果过滤后经常低于目标数量,问题可能不是融合权重,而是索引分区、权限建模或文档覆盖不足。
11.3 召回与重排序的比例
重排序模型越强,候选越多通常越有利;但候选过多会增加成本。可以评估:
其中 是业务对延迟和成本的权衡系数。
11.4 新鲜度和版本
对政策、价格、接口文档等时效性强的资料,相关性不只取决于文本匹配。可以在融合或重排序中加入新鲜度,但要避免新文档无条件压过权威旧文档。
例如:
这里的 需要通过标注数据确定。对于法律、审计和历史记录,旧文档可能正是问题所需的证据,不能简单地按时间排序。
十二、常见误解与反例
误解一:向量检索已经包含 BM25 的全部能力
反例是精确标识符查询:
ERR_AUTH_0017
如果 embedding 模型没有把这个错误码与正确文档建立稳定语义关系,向量检索可能返回“认证失败”“权限不足”等相似主题,但没有包含 ERR_AUTH_0017 的具体处理步骤。BM25 对该字符串的直接匹配更可靠。
误解二:BM25 找不到同义词,所以没有价值
反例是 API 和版本查询:
Python 3.12 asyncio.TaskGroup
此时用户需要精确的版本和符号,语义相似但版本不同的文档可能造成严重误导。BM25 能够帮助系统优先保留包含确切术语的文档。
误解三:把 BM25 分数和向量分数直接相加即可
假设 BM25 分数范围是 0 到 20,向量相似度范围是 0.7 到 0.9:
即使向量检索在某个查询上区分度很高,它对最终分数的影响也可能只占很小比例。反过来,某些向量实现的点积分数可能远大于 BM25。没有校准的加法实际上是在让分数尺度决定权重。
误解四:融合后再过滤可以节省实现工作
反例是多租户知识库。无权文档可能在融合、缓存或日志阶段已经暴露。安全过滤必须在候选形成前尽可能执行,并在最终输出前再次检查。
误解五:权重调优能解决所有召回问题
如果 BM25 的分词器把内部产品名拆错,或者 embedding 模型没有覆盖领域术语,调节 不能创造正确的候选。应先检查:
- 原文是否被正确解析;
- chunk 是否包含完整证据;
- 术语是否被正确分词;
- embedding 是否成功生成;
- ANN 是否达到预期召回;
- 过滤是否误删;
- 文档版本是否正确。
十三、诊断方法:从“答案错了”回溯到检索链路
一个可操作的诊断顺序如下。
第一步:确认正确文档是否在允许集合中
先检查权限、租户、文档类型和时间过滤。如果正确文档被过滤掉,继续调 BM25 权重没有意义。
第二步:检查 BM25 是否召回
观察:
- 查询分析后的 token;
- 词项是否进入倒排索引;
- 关键术语的 document frequency;
- 目标文档的原始 BM25 分数;
- 是否因文档长度或分词导致得分异常。
第三步:检查向量检索是否召回
观察:
- 查询和文档是否使用同一 embedding 模型;
- 相似度度量是否一致;
- 向量是否为空或维度错误;
- ANN 搜索参数;
- 精确搜索与 ANN 搜索的差异;
- 目标文档与查询的相似度。
第四步:检查融合前后的名次
保存每个候选的:
doc_id
bm25_rank
bm25_score
vector_rank
vector_score
normalized_scores
fused_score
filter_status
rerank_score
如果正确文档在两个召回器中都存在,但融合后被挤出,问题属于归一化、权重、去重或重排序。如果它只在一个召回器中存在,则应分析该检索器的覆盖能力。
第五步:检查生成上下文
即使正确 chunk 排在前面,也可能在上下文拼接时被:
- token 截断;
- 去重规则删除;
- 压缩模型错误摘要;
- 相邻片段排序打乱;
- 引用映射错误。
检索评测和生成评测必须分开记录,不能只看最终答案。
十四、评测应覆盖查询类型和生产约束
一个可靠的评测集不能只有“普通自然语言问题”。至少应分层包含:
- 精确错误码;
- 产品名和版本号;
- API、函数和配置键;
- 同义改写;
- 跨语言查询;
- 多条件过滤;
- 权限边界;
- 时效性问题;
- 多跳问题;
- 无答案问题。
对每类查询分别报告指标,才能知道权重变化究竟改善了什么。例如,一个全局权重可能提高自然语言问答的 nDCG,却降低错误码查询的 Recall@5。
还应做消融实验:
仅 BM25
仅向量
BM25 + 向量,RRF
BM25 + 向量,加权融合
混合召回 + 重排序
混合召回 + 重排序 + 新鲜度
如果加入某个组件后整体指标上升,但权限过滤查询下降,就不能只看平均值。生产系统应优先保证安全边界,再在允许集合内优化相关性。
十五、成本、延迟和容量取舍
混合检索的成本来自多个部分:
其中:
- 文档入库和更新需要生成 embedding;
- 向量存储会增加内存和磁盘占用;
- BM25 倒排索引也需要存储词项和位置等信息;
- 两个检索器并发查询会增加服务资源;
- 候选越多,重排序和上下文处理越贵;
- 上下文越长,生成模型的 token 成本和延迟越高。
不能只通过增加 top-k 来弥补召回问题。更合理的顺序通常是:
- 先确认文档解析和切块正确;
- 确认过滤没有误删;
- 评估单独 BM25 和单独向量的召回上限;
- 通过融合提高候选覆盖;
- 用重排序改善前几名;
- 最后在延迟和成本约束下确定 K 和上下文长度。
OpenAI 的 Retrieval Guide 也将语义搜索、向量存储和基于属性的过滤作为检索系统的重要组成部分;具体能力和参数应以所使用服务的当前文档和版本为准,不能把一个产品的过滤语法直接套用到另一个向量数据库。
十六、一个可落地的基线方案
在没有大量标注数据时,可以采用以下可解释基线:
1. 文档按结构切块,保存稳定 doc_id、chunk_id、版本和 ACL
2. 同一规范化文本同时建立 BM25 索引和向量索引
3. 查询时先构造租户、ACL 和业务元数据过滤条件
4. BM25 检索 top-50
5. 向量检索 top-50
6. 对两个结果集按稳定 chunk_id 去重
7. 使用 RRF 作为初始融合方法
8. 对融合后的 top-50 使用交叉编码器或业务重排序
9. 再执行一次权限校验和文档版本校验
10. 选择 top-5 到 top-10 片段进入生成上下文
11. 记录每个阶段的分数、名次、过滤和版本信息
选择 RRF 作为起点,是因为它不要求立即解决两个分数空间的校准问题。积累了可靠的查询—文档相关性标注后,再比较:
- RRF;
- 归一化加权融合;
- 查询分片权重;
- 学习排序;
- 融合后重排序。
最终方案不应由某一个搜索案例决定,而应由分层评测、权限约束、延迟预算和成本预算共同决定。
混合检索的核心不是“同时调用 BM25 和向量数据库”,而是让不同信号在正确的候选集合上协同工作:BM25 负责精确词项,向量负责语义覆盖,过滤负责合法边界,融合负责统一排序,权重调优负责用数据决定取舍,重排序和生成阶段则分别负责提高证据优先级与答案表达质量。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:向量 ANN 索引:HNSW、IVF、PQ、召回率、内存和构建成本
- 下一篇:RAG 重排:Cross Encoder、Late Interaction、候选规模和延迟
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论