背景问题:RAG 质量回退经常不是生成模型的问题
很多 RAG 系统上线后,团队习惯把答案质量问题归因到「模型不够强」。但在生产排查中,真正的根因常常更靠前:该找回来的证据根本没有被召回,或者召回结果顺序不稳定,导致模型拿到的上下文本身就偏了。
单纯依赖向量检索时,系统擅长理解语义相似问题,但可能漏掉错误码、产品型号、合同编号、API 名称、日期、版本号等需要精确匹配的内容。单纯依赖关键词检索时,系统对精确词更敏感,但可能漏掉同义表达、改写后的自然语言问题和跨语言表达。Azure AI Search 文档明确把混合检索定义为同时运行全文检索和向量查询,并用 RRF 合并结果;Pinecone 文档同样指出语义检索可能漏掉精确关键词,而词法检索可能漏掉同义词和转述表达。
因此,生产级 RAG 不应只问「用不用向量数据库」,而要问:不同查询应该走什么召回策略,如何融合结果,什么时候回退,如何证明这次召回足够支撑答案。
核心原理:混合检索不是简单多查一次
一个稳定的混合检索链路通常包含三层。
第一层:稀疏检索
典型实现是 BM25、BM25F 或稀疏向量。它对字面匹配、字段权重、编号、实体名和术语非常敏感,适合找「明确出现过的词」。
第二层:稠密检索
即 Embedding 向量检索。它把查询和文档映射到向量空间中,适合处理同义、改写、概念相近和自然语言问题。
第三层:融合层
融合层不是简单把两个分数相加,因为 BM25、稠密向量、稀疏向量的分数范围并不天然一致。Azure AI Search 的混合检索使用 Reciprocal Rank Fusion(RRF);Elasticsearch 文档也说明 RRF 可以把多个具有不同相关性指标的结果集组合成一个结果集,且不要求这些指标彼此相关。
RRF 为什么适合做默认融合策略
RRF 的直觉很简单:一个文档如果在多个召回列表中都排得靠前,就应该获得更高的融合排名。它不直接比较 BM25 分数和向量相似度分数,而是看每个结果在各自列表里的名次。
简化公式如下:
score(document) = Σ 1 / (rank_constant + rank_in_each_result_list)
其中 rank_constant 用来控制靠后结果对最终排名的影响。Elasticsearch 的 RRF 文档中提供了类似公式,并暴露了 rank_constant 与 rank_window_size 等参数;Azure 的 RRF scoring 文档也说明,RRF 会对多个并行查询产生的排序结果重新赋分并合并。
这让 RRF 很适合作为混合检索的第一版默认策略:它比直接加权原始分数更稳,也比手工写一堆排序规则更容易解释。
查询路由比全量混合更重要
很多团队把「混合检索」理解成每个请求都同时跑 BM25 和向量检索,然后合并。这是可用的起点,但不是最优的生产形态。
更稳的做法是增加一个轻量的查询分类器,把查询分成几类:
| 查询类型 | 特征 | 推荐策略 |
|---|---|---|
| 精确查询 | 包含错误码、订单号、类名、函数名、产品型号、版本号 | 优先 BM25 / 稀疏检索 |
| 语义查询 | 完整自然语言问题,没有明确实体 | 优先向量检索 |
| 混合查询 | 既有实体又有自然语言意图 | BM25 + 向量 + RRF |
| 低置信查询 | 检索结果分数低、召回数量少、稀疏稠密分歧大 | 触发回退或二次检索 |
这样做的目的不是减少一次查询成本,而是避免把所有请求都塞进同一个权重模板里。
工程落地:从全量混合走向可解释路由
在线链路中可以把一次检索请求拆成一个 RetrievalPlan。它描述这次请求使用哪些检索器、各自 topK、融合方式、回退条件和是否进入重排。
type RetrievalMode = "keyword" | "vector" | "hybrid";
type RetrievalPlan = {
mode: RetrievalMode;
bm25TopK: number;
vectorTopK: number;
fusion: "rrf" | "weighted" | "none";
rankWindowSize: number;
useReranker: boolean;
fallback: {
minCandidates: number;
maxVectorDistance?: number;
retryWithHybrid: boolean;
};
};
function buildRetrievalPlan(query: string): RetrievalPlan {
const hasCodeLikeToken = /[A-Z]{2,}-\d+|v\d+\.\d+|[a-zA-Z_]+\(\)/.test(query);
const isShortExactQuery = query.trim().split(/\s+/).length <= 3;
const looksNaturalLanguage = /什么|如何|为什么|怎么|how|why|what/i.test(query);
if (hasCodeLikeToken || isShortExactQuery) {
return {
mode: "keyword", bm25TopK: 50, vectorTopK: 0,
fusion: "none", rankWindowSize: 50, useReranker: false,
fallback: { minCandidates: 5, retryWithHybrid: true }
};
}
if (looksNaturalLanguage) {
return {
mode: "hybrid", bm25TopK: 50, vectorTopK: 50,
fusion: "rrf", rankWindowSize: 50, useReranker: true,
fallback: { minCandidates: 8, maxVectorDistance: 0.45, retryWithHybrid: true }
};
}
return {
mode: "vector", bm25TopK: 0, vectorTopK: 40,
fusion: "none", rankWindowSize: 40, useReranker: true,
fallback: { minCandidates: 5, retryWithHybrid: true }
};
}
这段代码只是示意。真正上线时,查询分类器可以先从规则开始,再逐步引入轻量分类模型。关键是把策略配置化,并把每次检索计划落日志,方便定位「为什么这次没有召回」。
RRF 参数要和召回窗口一起调
RRF 的效果高度依赖召回窗口。如果 BM25 只取 top 5,向量取 top 100,再用 RRF 融合,稀疏侧很容易失去足够候选。反过来,如果两个列表都取太大,延迟、内存和后续重排成本会上升。
一般可以从以下配置开始:
retrieval:
default_plan:
bm25_top_k: 50
vector_top_k: 50
fusion: rrf
rank_constant: 60
rank_window_size: 50
final_top_k: 8
exact_query_plan:
bm25_top_k: 80
vector_top_k: 20
fusion: rrf
final_top_k: 8
semantic_query_plan:
bm25_top_k: 30
vector_top_k: 80
fusion: rrf
final_top_k: 8
Azure 文档中提到混合查询会并行执行全文和向量查询,并由 RRF 合并统一结果;Elasticsearch 文档也说明 rank_window_size 越大通常有利于相关性,但会带来性能成本。因此,调参不能只看最终答案,要同时看召回窗口、融合前候选、融合后候选和重排前后变化。
权重不是全局常量
Weaviate 的混合检索提供 alpha 参数,用于调节关键词和向量组件的相对权重;Pinecone 文档则提醒,单索引同时存稠密和稀疏向量时,BM25 或稀疏分数与稠密向量范围并不天然归一,若没有显式权重,稀疏部分可能主导分数。
生产上更建议按查询类型设置权重,而不是全局固定:
| 场景 | 推荐权重倾向 |
|---|---|
| 错误码、SKU、合同编号 | BM25 权重更高 |
| 「这段话是什么意思」「如何配置权限」 | 向量权重更高 |
| 法务、医疗、保险条款 | 先保证精确命中,再允许语义扩展 |
| 多语言知识库 | 优先验证向量跨语言召回,再补充关键词回退 |
权重调节不是为了追求一个漂亮参数,而是为了让查询意图和召回策略一致。
回退策略:召回不足时不要让模型硬答
混合检索的线上风险之一是「看起来有结果,但结果不够支撑答案」。因此需要在检索层定义回退条件。
常见回退信号
- 候选数量低于阈值
- 关键词检索命中很少,但查询包含明显实体
- 向量检索距离过高或相似度过低
- BM25 与向量结果几乎没有交集,且 RRF 后 top 结果分散
- 重排器分数低,或 top1 与 top2 差距过小
- 最终证据片段缺少标题、时间、来源或权限字段
触发回退后的处理顺序
- 放宽过滤条件
- 扩大 topK
- 切换权重
- 启用另一个字段索引
- 进入跨索引检索
- 返回「未找到足够证据」或要求用户补充关键词
核心原则:不要让 LLM 在证据不足时继续编一个答案。
适用场景
技术文档与知识库
开发者可能搜索 ERR_AUTH_401,也可能问「为什么登录后还是提示无权限」。前者需要精确匹配,后者需要语义理解。
企业制度、合同、保险条款、政策文件
用户可能输入条款号、产品名、日期,也可能问一个自然语言问题。纯向量检索容易错过条款编号,纯关键词检索又可能漏掉改写表达。
客服与工单
历史问题常常混合产品型号、错误现象、截图 OCR 文本和用户口语描述,需要把精确词和语义召回一起看。
电商、商品与库存问答
型号、品牌、规格、别名、用途会同时出现,单一检索策略很难稳定覆盖。
常见误区
误区一:混合检索等于质量一定提升
混合检索只是增加候选来源,并不自动保证答案更准。如果切块质量差、元数据缺失、权限过滤错误、topK 太小或融合参数不合理,混合检索同样会带来噪声。
误区二:把 BM25 分数和向量分数直接相加
不同检索器的分数范围不同,直接相加很容易让某一侧主导结果。Pinecone 文档中也特别指出稀疏和稠密分值形态不同,需要显式权重。RRF 的价值就在于降低这种分数不可比带来的风险。
误区三:只调 topK,不看召回分布
topK 变大可能提升召回,也可能只是增加重排噪声。要同时观察 BM25 命中、向量命中、融合交集、最终进入上下文的片段,以及最终答案引用的证据。
误区四:忽略过滤条件的位置
权限、时间、产品线、租户过滤放在检索前还是检索后,会影响召回范围和融合结果。Azure 文档中的混合查询也提醒过滤可以在处理流程的不同阶段应用,是否使用后过滤需要测试确认。生产环境里,权限过滤必须正确,相关性调优不能绕过权限边界。
上线检查清单
上线前至少准备四类查询集:
| 查询类型 | 测试目标 | 验证重点 |
|---|---|---|
| 精确查询 | 错误码、编号、API 名、类名、配置项、合同条款号 | BM25 / 稀疏侧是否稳定命中 |
| 语义查询 | 用户不使用文档原词但意图明确 | 向量召回是否找到概念相近片段 |
| 混合查询 | 同时包含实体和自然语言描述 | RRF 融合是否比单路召回更稳 |
| 失败查询 | 知识库中无答案、权限不可见、时间范围不匹配 | 系统是否能拒答、回退或提示补充信息 |
建议记录的指标:
- Recall@K、MRR、NDCG
- BM25 命中数、向量命中数、融合交集比例
- RRF 前后 top 文档变化
- 重排前后 top 文档变化
- 最终进入 LLM 上下文的片段来源
- 回退触发率、拒答率、人工复核通过率
- p95 检索延迟和端到端回答延迟
参考资料
- Azure AI Search: Hybrid search using vectors and full text
- Azure AI Search: Relevance scoring in hybrid search using RRF
- Elasticsearch: Reciprocal rank fusion
- Weaviate Documentation: Hybrid search
- Pinecone Docs: Hybrid search
- Efficient and Effective Retrieval of Dense-Sparse Hybrid Vectors using Graph-based Approximate Nearest Neighbor Search