←返回文章列表

从向量检索到混合检索:RAG 召回策略的设计思路

RAG大语言模型AI 应用
15 分钟-阅读

RAG 的效果不只取决于大模型,还取决于检索链路能否把真正有用的证据送进上下文。向量检索适合语义匹配,多路召回则用关键词、稀疏向量、结构化过滤和重排序补齐它的盲区。

从向量检索到混合检索:RAG 召回策略的设计思路

RAG 系统的回答质量,常常先败在检索阶段。

大模型看起来是在“回答问题”,但它真正能依据的材料,通常只是检索器塞进上下文窗口里的那几段文本。召回阶段漏掉了关键段落,后面的重排序、提示词和生成模型都很难补救。召回阶段塞进了大量相似但无关的内容,模型又会在噪声里摇摆,给出看似完整却缺少依据的回答。

向量检索让 RAG 进入了可用阶段。它可以把“员工离职后还能访问系统怎么办”和“账户权限回收流程”联系起来,即使问题和文档没有共享太多字面词。可一旦进入真实业务知识库,单一路线很快暴露出边界:产品型号、报错码、法规条款、接口名、专有名词、表格字段、版本号,很多都不是“语义相近”能稳定解决的。

混合检索的核心不是堆技术名词,而是承认一个事实:用户问题有不同形态,知识库内容也有不同形态。一个检索系统需要多条召回路径,把语义、关键词、结构化条件和业务规则组合起来,再用融合与重排序把候选证据压缩成模型能消费的上下文。

RAG 中的召回到底在解决什么

RAG 的经典思路是把参数化记忆和外部知识结合起来。模型负责理解和生成,检索器负责从外部知识库取回相关材料。Lewis 等人在 2020 年的 RAG 论文中,把这种模式描述为让生成模型访问外部非参数记忆,用检索到的文档辅助生成。

放到工程系统里,召回阶段通常承担三件事:

  1. 从大规模知识库中找出候选片段。
  2. 尽量不要漏掉能回答问题的证据。
  3. 控制候选数量,让后续重排序和生成成本可控。

一个客服知识库例子会更直观。用户问:

Pro 版导出账单时提示 E1042,是不是权限问题?

如果系统只召回“账单导出权限说明”,可能漏掉“错误码 E1042 表示套餐额度不足”。如果只召回错误码表,又可能漏掉 Pro 版和企业版的权限差异。好的召回不是找一段“最像问题”的文本,而是为后续回答收集足够完整的证据。

这也是 RAG 检索链路通常不会停在单个 top-k 向量搜索的原因。

图 1:RAG 检索链路中的多路召回

flowchart TD
  A["用户问题"] --> B["查询改写与解析"]
  B --> C["向量召回"]
  B --> D["关键词 / BM25 召回"]
  B --> E["结构化过滤"]
  B --> F["业务规则召回"]
  C --> G["候选集合"]
  D --> G
  E --> G
  F --> G
  G --> H["融合去重"]
  H --> I["重排序"]
  I --> J["上下文组装"]
  J --> K["大模型生成"]

这条链路里,向量检索只是召回的一条路。它很重要,但不应该被误认为是 RAG 检索的全部。

向量检索适合找“意思接近”的内容

向量检索会先把文本转换成 embedding,也就是一组数字表示。查询和文档都变成向量后,系统用余弦相似度、点积或欧氏距离之类的方式比较它们在向量空间里的接近程度。Weaviate 的文档把向量搜索称为基于相似度的搜索,它会比较查询向量和对象向量,返回最接近的 top n 结果。

它的优势在于语义泛化。

比如知识库里有一段:

离职员工的账号应在最后工作日结束前完成权限回收,包括 SSO、VPN、代码仓库和生产系统访问权限。

用户问:

员工离开公司以后,怎么避免他继续登录内部系统?

这两个文本共享的关键词并不多。传统关键词检索可能抓住“员工”“系统”,但排序未必靠前。向量检索更容易捕捉“离职”“权限回收”“继续登录”之间的语义关系,把这段文档召回。

在 RAG 里,向量检索尤其适合这些场景:

场景 查询示例 向量检索为什么有用
自然语言问答 “怎么减少模型乱编?” 能匹配“幻觉”“事实约束”“引用证据”等相近表达
概念解释 “召回率和准确率怎么取舍?” 能找到不含原词但讨论同一概念的段落
跨语言或同义表达 “refund policy” 查询中文退款规则 取决于 embedding 模型能力,可捕捉跨语言语义
非标准提问 “客户说钱没回来,查哪里?” 能匹配“退款到账时间”“支付渠道回调”等业务文档

向量检索的问题也来自同一个机制:它重视语义接近,但不天然重视精确匹配。

用户问 E1042,embedding 模型可能认为 E1042、E1043、E1024 都是相似的错误码。用户问 GET /v1/files/{id},模型可能把它和另一个文件接口混在一起。用户问“X9 Pro 电池容量”,向量检索可能召回 X9、X9 Pro、X9 Max 的综合介绍,因为它们都在同一个语义区域里。

这些场景需要另一类信号:字面词、字段、编号、版本、实体和过滤条件。

关键词检索补的是“必须命中”的信号

关键词检索关注 token 的精确匹配。BM25 是常见算法,它会综合词频、逆文档频率和文档长度等因素计算相关性。Weaviate 的 BM25 文档也强调,关键词搜索适合技术术语、精确类别、标签等需要 exact match 的场景。

还是看一个例子。知识库里有三段内容:

  1. E1042:套餐额度不足,导出任务被拒绝。
  2. E1043:当前角色没有账单导出权限。
  3. Pro 版支持导出最近 12 个月账单,企业版支持自定义时间范围。

用户问:

Pro 版导出账单时提示 E1042,是不是权限问题?

向量检索可能把第 2 段排得很高,因为“权限问题”和“没有账单导出权限”语义很近。但关键词检索会强制把包含 E1042 的第 1 段拉进候选集合。最后的答案才能说明:这更可能是额度问题,不是权限问题;同时 Pro 版导出范围可以作为补充检查项。

关键词召回适合处理几类强约束信息:

  • 错误码、订单号、接口名、类名、函数名。
  • 产品型号、版本号、法规条款、标准编号。
  • 人名、组织名、地名、专有名词。
  • 表格字段、配置项、日志关键字。

它的弱点也很明显。用户如果不用文档里的词,关键词检索容易漏召回。文档写“权限回收”,用户问“离职后怎么禁止登录”,BM25 不一定能把最相关的段落排在前面。

所以关键词检索不是向量检索的替代品,而是它的互补项。

混合检索的基本形态

混合检索通常指把语义检索和词法检索结合起来。Weaviate 对 hybrid search 的描述很清楚:并行执行向量搜索和 BM25 搜索,然后用融合方法合并分数,返回组合排序后的结果。Pinecone 的 hybrid search 文章也把它放在 semantic search 和 lexical search 的互补关系里讨论,用来弥补二者各自的限制。

工程上常见三种形态。

第一种是单索引混合。每条记录同时存 dense vector 和 sparse vector,查询时同时提交稠密向量和稀疏向量。Pinecone 的 hybrid index 思路就是这一类。它的好处是架构简单,一次请求能完成混合搜索;代价是需要处理密集向量和稀疏向量尺度不同的问题。如果不做显式权重,稀疏信号可能在组合分数里占据过高比例。

第二种是双索引或多索引。向量库负责语义召回,搜索引擎负责 BM25 召回,两边各取 top-k,再在应用层融合。这种方式组件更多,但调试清楚。比如一个内部文档助手可以用向量库召回“离职权限回收”相关片段,同时用 Elasticsearch 召回包含 SSO、VPN、GitLab 的精确段落,然后合并。

第三种是多字段检索。同一文档里,标题、正文、标签、产品线、版本、更新时间有不同字段。查询时可以让标题和标签更重,也可以先用元数据过滤产品线,再做向量或关键词检索。例如用户问“企业版 2.8 的审计日志保留多久”,系统可以先过滤 edition = enterprise 和 version = 2.8,再召回审计日志相关内容。

一个简单的混合召回伪代码如下:

query = "Pro 版导出账单时提示 E1042,是不是权限问题?"

dense_hits = vector_search(query, top_k=50)
keyword_hits = bm25_search(query, top_k=50)
metadata_hits = filter_search(
    product="billing",
    edition="pro",
    top_k=30,
)

candidates = merge_and_deduplicate(
    dense_hits,
    keyword_hits,
    metadata_hits,
)

ranked = rerank(query, candidates, top_n=8)
context = build_context(ranked)
answer = llm_generate(query, context)

这里的 top_k=50 不是固定答案。它表达的是一种设计:前面召回可以宽一点,后面用融合与重排序变窄。真实系统要根据知识库规模、延迟预算、重排序成本和答案评估结果调参。

多路召回不只是向量加 BM25

很多系统把“混合检索”理解成“向量 + BM25”。这是起点,不是终点。RAG 的召回策略还可以拆出更多路,每一路负责一种证据形态。

召回路径 适合解决的问题 示例
向量召回 语义相近但字面不一致 “离职后禁止登录”匹配“权限回收”
BM25 召回 关键字、编号、术语必须命中 E1042、POST /v1/files、ISO 27001
学习型稀疏召回 兼顾词项权重和向量库统一检索 SPLADE 或托管 sparse embedding
元数据过滤 版本、产品线、地区、权限范围 只查 version=2.8 的文档
标题 / 摘要召回 文档块正文太碎,需要章节级线索 先召回“账单导出限制”,再展开段落
图谱 / 规则召回 实体关系明确、业务规则稳定 错误码关联原因、接口关联权限
最近更新召回 时效性强的文档优先 版本发布说明、故障公告、政策更新

多路召回的价值在于降低漏召回概率。

比如用户问:

华东区企业版 2.8 开启 SSO 后,审计日志为什么没有记录外部 IdP 的登录失败?

这个问题至少包含五类信号:

  • 地区:华东区。
  • 版本:企业版 2.8。
  • 功能:SSO。
  • 现象:审计日志没有记录。
  • 外部系统:IdP 登录失败。

只靠向量检索,系统可能找到“SSO 登录配置说明”。只靠 BM25,系统可能找到所有包含 IdP 的段落。更合理的召回链路是:用元数据先限定企业版和 2.8;用 BM25 抓 SSO、IdP、审计日志;用向量召回“登录失败不入审计”这类语义相近的排障文档;再用规则召回“审计日志事件类型表”。

答案需要的是证据组合,而不是单段相似文本。

分数融合:把不同召回结果放到同一张表

多路召回之后,会得到多个列表。问题来了:向量相似度、BM25 分数、稀疏向量分数、业务规则分数不在同一个尺度上,不能直接相加。

常见融合方式有两类。

一类是分数归一化。把每一路结果的分数映射到相近范围,再按权重相加。Weaviate 的 relativeScoreFusion 就属于这种思路:分别归一化向量搜索和关键词搜索的结果,再计算组合分数。Pinecone 的 hybrid search 文章也提到用 alpha 在 dense 和 sparse 之间做权重控制,alpha = 1.0 相当于纯语义,alpha = 0.0 相当于纯词法。

一个示意公式可以写成:

score = 0.55 * dense_score
      + 0.35 * keyword_score
      + 0.10 * freshness_score

如果企业知识库里错误码和接口名很多,可以降低向量权重,提高关键词权重。如果用户问题大多是自然语言咨询,可以让向量权重更高。

另一类是排名融合。RRF,也就是 Reciprocal Rank Fusion,会根据文档在各路结果中的排名来加分,而不是依赖原始分数。Qdrant 1.10 发布后的 Query API 文章展示了用 RRF 合并稠密向量和稀疏向量结果的方式。它的优点是简单稳健,不太受不同检索器分数尺度影响。

RRF 的直觉很容易理解。一个片段如果在向量检索里排第 2,在 BM25 里排第 5,它比只在某一路排第 30 的片段更可信。即使两个检索器分数完全不可比,排名位置也能提供融合信号。

示意代码如下:

def rrf_fuse(result_lists, k=60):
    scores = {}
    for results in result_lists:
        for rank, doc in enumerate(results, start=1):
            scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank)
    return sorted(scores.items(), key=lambda item: item[1], reverse=True)

RRF 不是万能药。它不会理解文档内容,只是融合多个排序列表。如果召回阶段拿到的都是无关内容,RRF 也只能在无关内容里排序。它适合做第一层稳定融合,后面仍然需要更精细的重排序。

重排序决定哪些证据能进入上下文

召回的目标是“别漏”,重排序的目标是“别乱放”。

向量检索、BM25 和规则召回可能合并出几十到几百个候选片段。上下文窗口再大,也不应该把所有内容都塞给模型。噪声会增加生成成本,也会让模型引用错误材料。重排序器会重新判断“这个片段对当前问题是否有帮助”,再选出少量高质量证据。

常见重排序方式包括:

  • Cross-encoder reranker:把 query 和 doc 一起输入模型,输出相关性分数。
  • LLM rerank:让大模型按问题判断片段是否支持回答。
  • 规则重排:给标题命中、版本匹配、更新时间、来源可信度加权。
  • 多阶段重排:先用便宜模型筛到 30 条,再用更强模型筛到 5 条。

Cohere 在 2024 年 4 月发布 Rerank 3 时,把 rerank 放在企业搜索和 RAG 检索质量提升的语境下:先拿候选文档,再按查询相关性重新排序。这个模式适合 RAG,因为生成模型最终需要的不是“相似片段”,而是“能支撑回答的片段”。

看一个具体例子。用户问:

E1042 是权限问题吗?

候选片段 A:

E1042 表示套餐额度不足,导出任务不会进入权限校验阶段。

候选片段 B:

E1043 表示当前角色缺少账单导出权限。

候选片段 C:

管理员可以在角色管理页面配置账单导出权限。

向量检索可能觉得 B 和 C 很相关,因为它们都讨论权限。重排序器如果能同时看 query 和 doc,就应该把 A 排在最前,因为它直接回答了 E1042 是否为权限问题。B 可以作为对比证据,C 则可能不需要进入最终上下文。

这就是重排序在 RAG 里的价值:它把“召回相关”进一步变成“回答有用”。

Chunk 设计会影响所有召回路径

召回策略再好,也绕不开切块。文档切得太大,召回结果里噪声多;切得太小,关键上下文断裂,模型拿不到完整证据。

比如一份 API 文档:

## 导出账单

Pro 版支持导出最近 12 个月账单。
企业版支持自定义时间范围。

### 错误码

E1042:套餐额度不足。
E1043:当前角色没有账单导出权限。

如果把每一行都切成独立 chunk,E1042 那一块可能没有“导出账单”和“Pro 版”的上下文。用户问 Pro 版导出账单时,系统召回错误码片段,却无法确认适用场景。

如果整篇文档作为一个 chunk,向量检索会召回它,但里面混有 Pro 版、企业版、错误码、权限配置等多种信息。重排序和生成阶段都要在更大的噪声里工作。

比较稳妥的做法是保留层级信息:

  • chunk 正文保持在一个相对完整的小节内。
  • metadata 记录标题路径,例如 账单 > 导出账单 > 错误码。
  • 对过短 chunk 做父级扩展,召回子块后把相邻段落或父章节一起带上。
  • 标题、摘要、正文可以分别建索引,召回后再映射回原文片段。

一个知识库问答系统可以先召回错误码子块,再自动补上同一小节里的版本限制。这样既保留精确命中,又避免上下文断裂。

查询改写让多路召回更稳定

用户的问题往往不适合直接送进所有检索器。查询改写可以把一个自然语言问题拆成更适合检索的多个查询。

原始问题:

Pro 版导出账单时提示 E1042,是不是权限问题?

可以拆成:

  • 语义查询:账单导出失败 是否权限问题
  • 关键词查询:E1042 Pro 版 导出账单
  • 错误码查询:E1042
  • 版本 / 套餐过滤:edition = pro

这不是为了让系统变复杂,而是让不同召回路径吃到适合自己的输入。向量检索适合完整自然语言问题,BM25 适合保留关键实体,结构化过滤适合明确字段。

查询改写也能处理指代和上下文。用户连续追问“那企业版呢”,系统需要把它改写成“企业版导出账单错误码 E1042 是否为权限问题”,否则检索器只看到一个缺少主题的短句,很容易召回泛泛的企业版介绍。

不过查询改写要克制。改写不能引入用户没说过的条件。用户问“Python SDK 上传失败”,系统不能擅自改成“OpenAI Python SDK 上传失败”,除非上下文里已经明确了产品。改写阶段引入错误实体,会让后面的召回全部偏航。

如何评估召回策略

RAG 检索优化不能只看最终回答的主观感觉。召回链路至少要单独评估。

最基础的指标是 recall@k:标准答案所需证据是否出现在前 k 个候选里。比如一批 100 个测试问题,每个问题人工标注能回答它的证据片段。如果 top 20 候选里包含证据,就算召回成功。这个指标适合检查“有没有漏”。

第二类指标是 precision@k 或 nDCG:排在前面的候选是否真的相关。它适合检查“有没有乱”。如果 top 5 里只有 1 条有用证据,即使 top 50 召回到了答案,生成阶段也可能受噪声影响。

第三类是端到端答案指标:答案是否正确、引用是否对应、是否拒答得当。它能反映整体效果,但不容易定位问题。答案错了,可能是召回漏了,也可能是重排序错了,还可能是 prompt 或生成模型没处理好证据冲突。

一个实用评估集可以这样组织:

问题类型 测试目标 示例
语义改写 检查向量召回 “员工走了怎么关账号”
精确编号 检查 BM25 / 稀疏召回 “E1042 是什么”
多条件约束 检查 metadata 过滤 “企业版 2.8 审计日志保留多久”
对比问题 检查多证据召回 “Pro 和企业版导出账单有什么区别”
否定问题 检查证据冲突处理 “E1042 是权限不足吗”

每次调整 embedding 模型、chunk 大小、BM25 参数、融合权重、reranker 或 top-k,都用同一批问题回归。否则检索优化很容易变成凭感觉调旋钮。

一套可落地的召回策略

对于多数企业知识库、产品文档助手、内部客服助手,可以从一套朴素但稳的方案开始:

  1. 文档入库时保留标题层级、产品线、版本、更新时间、来源 URL。
  2. 正文建立 dense vector 索引,用于语义召回。
  3. 正文和标题建立 BM25 或稀疏索引,用于精确召回。
  4. 查询时抽取实体、版本、产品线和错误码,能过滤就先过滤。
  5. 向量、BM25、标题召回并行执行,每路取 30 到 80 条候选。
  6. 用 RRF 或归一化加权做融合,合并去重。
  7. 用 reranker 选出 5 到 10 条证据。
  8. 做上下文组装时保留标题路径、来源和相邻必要段落。
  9. 让模型基于证据回答,并要求无法从证据推出时说明不足。

这套方案不追求一开始就很精巧。它的优点是每一步都能观察、能替换、能评估。召回漏了,看各路 top-k;融合错了,看单路排名和融合后排名;重排序错了,看 query-doc 分数;生成错了,看最终上下文是否已经包含足够证据。

当系统稳定后,再考虑更复杂的优化:

  • 为标题、摘要、正文使用不同 embedding。
  • 对代码、表格、FAQ 使用专门切块策略。
  • 为错误码、接口、权限点建立实体表或知识图谱。
  • 按问题类型动态调整 alpha、top-k 和召回路径。
  • 对高频问题沉淀人工校验答案和黄金证据集。

RAG 的检索系统不是一次性搭出来的,而是围绕问题集持续校准出来的。

常见误区

第一个误区是把 embedding 模型当成唯一答案。更强的 embedding 确实可能提升语义召回,但它不会自动解决错误码、版本号和字段过滤问题。用户问 E1042,精确命中比“语义理解”更重要。

第二个误区是 top-k 越大越好。召回更多候选可以减少漏召回,但也会增加融合、重排序和生成成本。没有重排序的超大 top-k,常常只是把噪声提前交给大模型。

第三个误区是只评估最终回答。最终回答当然重要,但它太靠后。检索链路没有单独指标,就很难判断应该改 chunk、改权重、改 reranker,还是改 prompt。

第四个误区是忽略权限和数据边界。企业 RAG 里,检索阶段必须遵守用户权限、租户隔离、地区合规和文档可见性。不能先召回全部内容,再指望模型“不说出来”。权限过滤应该进入召回链路,而不是生成阶段的道德约束。

第五个误区是没有保留来源。RAG 的价值不仅是答得像,还要能追溯。上下文里最好保留文档标题、章节、URL、更新时间和片段位置。用户追问“依据是什么”时,系统才能给出可检查的引用。

结语

向量检索让 RAG 有了语义入口,但真实业务问题很少只靠语义相似就能解决。一个用户问题里可能同时包含自然语言意图、错误码、版本、产品线、权限范围和时间条件。单一路召回很容易在其中某个维度上失手。

混合检索的设计重点,是把不同检索信号放在合适的位置:向量召回负责语义覆盖,BM25 和稀疏召回负责精确命中,metadata 过滤负责边界约束,规则或图谱召回负责稳定业务关系,融合和重排序负责把候选证据压缩成可用上下文。

RAG 的检索优化最终不是追求某个漂亮架构,而是让系统在具体问题上少漏证据、少放噪声、少误导模型。能做到这一点,生成阶段才真正有机会给出可靠答案。

YL

我是姚磊

大模型应用开发者,关注 RAG、Agent 和产品化工程。沉淀实践中的判断、踩坑记录和可复用方法。联系我:yaolei_agent@163.com

LLM AppsRAGProductEngineering Notes