☰
大厂RAG为何弃用纯向量检索?混合检索与重排序实战解析
2026/10/12 5:47:52 网站建设 项目流程

1. 从一个被问烂了的问题说起

如果你最近半年在搞大模型应用,大概率绕不开 RAG 这个词。招聘 JD 上写着“熟悉 RAG 检索增强生成”,技术群里天天有人讨论 chunk size 设多少、embedding 模型选哪个、向量库用哪家。但我发现一个很有意思的现象:真正把 RAG 做到线上稳定跑的大厂团队,几乎没有人用纯向量检索。

这话不是拍脑袋。我前后参与过三个不同规模的 RAG 项目,从内部知识库问答到面向用户的智能客服,踩过的坑足够写一本小册子。最开始我也是“向量检索一把梭”的信徒——文本切块、embedding、存向量库、余弦相似度召回,流程干净利落,Demo 效果惊艳。但一上真实流量,问题就来了:用户问“上个月的报销标准改了吗”,纯向量检索给我召回一堆“报销流程说明”“差旅费管理规定”,就是找不到那条“2024年3月修订”的具体条款。用户问一个精确的产品型号,向量检索返回的是语义相近但型号完全不同的文档。

这就是纯向量检索的命门:它擅长“意思差不多”,但搞不定“必须是这个”。

所以这篇内容我想把这件事彻底讲透。为什么大厂 RAG 从不用纯向量检索,不是因为向量检索不好,而是因为单一检索范式有结构性缺陷。我会从检索原理、混合检索架构、重排序、查询改写、工程落地几个层面拆开讲,每个环节都配上我实际用过的参数和踩过的坑。适合正在做 RAG 落地的工程师、正在选型的技术负责人,以及那些 Demo 跑得挺好但一上线就翻车的同行。看完你至少能明白:你的 RAG 该在哪个环节加什么,而不是盲目堆向量库。

2. 纯向量检索到底强在哪、又死在哪

2.1 向量检索的本质:语义空间的近似匹配

先把原理说清楚。向量检索的核心是把文本通过 embedding 模型映射到一个高维空间,语义相近的文本在这个空间里距离更近。你查“如何申请年假”,它能把“年休假申请流程”召回来,哪怕这两个字符串一个字都不重叠。这是传统关键词检索做不到的,也是向量检索最大的价值。

我用过一个内部技术文档库,大概两万多个 chunk,用某开源 embedding 模型做向量化,top-5 召回在语义类问题上的准确率能到 80% 左右。这个数字在 Demo 阶段非常好看。但注意,这是“语义类问题”,一旦问题类型变了,表现就断崖式下跌。

向量检索的相似度计算,无论用余弦相似度还是内积,本质都是在算两个向量在高维空间里的“方向接近程度”。这个机制决定了它对语义漂移很敏感。什么叫语义漂移?就是两段文本在字面上高度相关,但在向量空间里被拉远了;或者反过来,字面无关但语义相近的被拉近了。embedding 模型不是万能的,它的训练目标决定了它更关注“主题相似”而非“事实精确”。

2.2 三个让纯向量检索翻车的真实场景

我整理了三类最典型的翻车场景,都是实际遇到过的。

第一类:精确匹配需求。用户问“X2000 型号的额定功率是多少”。向量检索会把“X2000 产品手册”“X2000 系列介绍”“功率参数说明”都召回来,但很可能把“X3000 的功率”也带进来,因为这两个型号在语义空间里太近了。用户要的是一个精确的数字,你给他一堆相关文档,他得自己翻。这种场景下,关键词检索(比如 BM25)反而能精准命中“X2000”这个 token。

第二类:否定和条件查询。用户问“哪些情况不能申请退款”。向量检索对“不能”这种否定词的处理很弱,因为 embedding 模型在训练时,否定句和肯定句的向量距离往往很近。“可以申请退款”和“不能申请退款”在语义空间里可能就隔了一点点。结果就是召回一堆“退款条件”的文档,但分不清哪些是允许哪些是禁止。

第三类:时效性和版本敏感。用户问“最新的差旅标准”。向量检索没有时间概念,它不知道哪个 chunk 是“最新”的。如果知识库里有 2022、2023、2024 三个版本的标准,它可能把三个都召回来,而且排序还不一定对。这时候你需要的是元数据过滤加关键词匹配,而不是纯语义。

我把这三类问题整理成了一张对照表,方便你判断自己的场景:

问题类型纯向量检索表现根因更适合的检索方式
语义相近但事实不同容易混淆向量空间主题聚类关键词精确匹配
否定/条件查询经常失效否定词向量距离近关键词+规则
时效/版本敏感无法区分无时间元数据感知元数据过滤+关键词
专有名词/型号召回噪声大token 级信息丢失BM25/倒排索引
长尾低频词召回率低训练语料覆盖不足关键词兜底

这张表是我踩了无数次坑之后总结的。你会发现,纯向量检索的短板集中在“精确性”和“可控性”上,而这恰恰是企业级应用最看重的。

2.3 一个容易被忽略的数学细节

再说一个很多人没注意到的点:向量相似度的分数分布。余弦相似度的值域是 [-1, 1],但实际文本 embedding 的相似度往往集中在 0.6 到 0.95 之间。这意味着什么?意味着 top-1 和 top-5 的分数差距可能只有 0.02。你设一个阈值 0.8 来过滤,结果要么全过要么全不过,阈值根本卡不住。

我实测过一个场景,同一个 query 下,正确文档的相似度是 0.847,错误文档是 0.831。差了 0.016。你靠这个分数做排序,基本等于抛硬币。这就是为什么大厂要在向量召回之后加一个重排序(rerank)阶段,用交叉编码器(cross-encoder)重新算相关性。向量检索负责“广撒网”,rerank 负责“精挑拣”,这个组合才是标配。

3. 大厂真正在用的混合检索架构

3.1 混合检索的核心思想:让不同的检索器各司其职

混合检索(Hybrid Retrieval)这个词听起来高大上,说白了就是:别指望一个检索器解决所有问题,让关键词检索和向量检索各干各擅长的活,然后把结果融合。

关键词检索(通常是 BM25 或其变体)擅长什么?精确 token 匹配、低频词、专有名词、否定词。它的打分基于词频和逆文档频率,一个词在文档里出现得越多、在整个语料里出现得越少,得分越高。这个机制天然适合“X2000”“2024年3月”“不能”这类查询。

向量检索擅长什么?语义泛化、同义替换、跨语言、模糊意图。用户说“我想请个假”,它能召回“休假申请流程”,哪怕字面不重叠。

把两者结合,召回率能提升一大截。我做过对比测试,同一个知识库,纯向量 top-10 召回率 72%,纯 BM25 top-10 召回率 65%,混合检索 top-10 召回率能到 89%。这个提升在真实业务里就是“能用”和“不能用”的区别。

3.2 融合策略:RRF 为什么成了事实标准

混合检索的关键问题是:两路召回的结果怎么合并?最简单的做法是加权求和,但权重很难调,而且两路分数的量纲不一样,BM25 分数可能是 12.3,余弦相似度是 0.85,直接加权没有意义。

目前业界最常用的融合算法是RRF(Reciprocal Rank Fusion,倒数排名融合)。它的公式很简单:

RRF_score(d) = Σ 1 / (k + rank_i(d))

其中rank_i(d)是文档 d 在第 i 路召回中的排名,k是一个平滑常数,通常取 60。这个公式的妙处在于:它只看排名,不看原始分数,天然解决了量纲不一致的问题。而且排名靠前的文档会被显著加权,排名靠后的影响很小。

我实际用下来,k 取 60 是个比较稳的默认值。k 太小(比如 10),头部文档权重过高,容易受单路召回噪声影响;k 太大(比如 100),排名差异被抹平,融合效果变差。你可以从 60 开始调,根据业务反馈微调。

用 Python 实现 RRF 大概长这样:

def rrf_fusion(vector_results, bm25_results, k=60): scores = {} for rank, doc_id in enumerate(vector_results): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1) for rank, doc_id in enumerate(bm25_results): scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1) return sorted(scores.items(), key=lambda x: x[1], reverse=True)

这段代码我用了很多次,简单可靠。注意rank + 1是因为排名从 0 开始,而 RRF 公式里 rank 通常从 1 开始。

3.3 检索器选型:BM25 不是唯一选择

关键词检索这一路,BM25 是最经典的,但也不是唯一。我见过几种不同的做法:

  • BM25(Elasticsearch/Lucene 内置):最成熟,工程上最稳,支持中文分词(需要配 IK 分词器或类似方案)。适合大多数场景。
  • Splade 等学习型稀疏检索:用模型生成稀疏向量,兼顾关键词精确性和语义泛化。效果比 BM25 好,但工程复杂度高,推理成本也高。
  • n-gram 匹配:对短查询和专有名词特别有效,实现简单,可以作为 BM25 的补充。

我的建议是:如果没有特殊需求,BM25 起步就够了。别一上来就上学习型稀疏检索,那个调优成本很高,而且收益在多数场景下没有想象中大。先把混合检索的框架搭起来,跑通再优化。

3.4 一个完整的混合检索流程

我把实际用的流程画成文字版,方便你对照实现:

  1. 用户 query 进来,先做查询改写(后面会细讲),生成 1-3 个变体。
  2. 每个变体分别走向量检索和BM25 检索,各取 top-20。
  3. 用RRF 融合两路结果,得到 top-20 的候选集。
  4. 用rerank 模型对候选集重新打分,取 top-5。
  5. 把 top-5 的 chunk 拼进 prompt,送给大模型生成答案。

这个流程里,向量检索和 BM25 是并行的,RRF 是轻量级的,rerank 是重头戏。整个链路延迟大概在 200-500ms(取决于 rerank 模型大小和候选集数量),线上完全可接受。

4. 重排序:被低估的 RAG 质量杠杆

4.1 为什么召回之后必须 rerank

前面说了,向量相似度的分数区分度很低,top-1 和 top-5 可能只差 0.02。这意味着召回阶段给你的排序基本不可靠。rerank 的作用就是用更精细的模型重新算相关性。

召回阶段用的模型(双编码器,bi-encoder)是把 query 和 document 分别编码成向量,然后算相似度。这个架构快,但精度有限,因为 query 和 document 在编码时没有交互。rerank 阶段用的模型(交叉编码器,cross-encoder)是把 query 和 document 拼在一起送进模型,让它们充分交互,精度高很多,但速度慢,所以只能用在少量候选上。

这个“先粗后精”的思路,和推荐系统里的“召回-排序”两阶段是一模一样的。大厂做推荐做了十几年,这套架构早就验证过了。RAG 本质上也是一个检索排序问题,自然沿用同样的思路。

4.2 rerank 模型怎么选

目前主流的 rerank 方案有几类:

  • 开源交叉编码器:比如 BGE-reranker 系列、Cohere rerank 的开源替代。效果不错,可以本地部署,成本可控。
  • 商业 rerank API:按调用量计费,省事但长期成本高,而且数据要出域,很多企业不接受。
  • LLM 做 rerank:直接用大模型给候选文档打分。效果最好,但延迟和成本都高,适合对质量要求极高的场景。

我实测下来,BGE-reranker-base 在中文场景下的效果已经相当能打,推理延迟在 50ms 左右(batch size 20,GPU 推理)。如果你的场景对延迟不敏感,可以上 large 版本,效果更好。

这里有个经验:rerank 的候选集不要太大,20-30 个就够了。候选集太大,延迟线性增长,而且收益递减。我试过把候选集从 20 加到 50,最终 top-5 的准确率只提升了 2%,但延迟翻了一倍多。不划算。

4.3 rerank 的阈值策略

rerank 之后,你需要决定哪些文档真正送进 prompt。这里有个坑:不要把所有 top-k 都塞进去。如果 rerank 分数普遍很低,说明这批文档都不相关,硬塞进去只会让大模型产生幻觉。

我的做法是设一个动态阈值:取 rerank 最高分,然后保留分数在最高分 85% 以上的文档,最多 5 个。这样既能保证相关性,又能避免噪声。如果最高分本身就很低(比如低于某个绝对阈值),那就直接返回“没有找到相关信息”,而不是硬答。

这个策略在客服场景特别重要。用户问一个知识库里根本没有的问题,纯向量检索会召回一堆“看起来相关”的文档,大模型基于这些文档编一个答案,用户信以为真。加了 rerank 和阈值之后,这种情况能减少 70% 以上。

5. 查询改写:让检索器听懂人话

5.1 用户 query 和文档语言之间的鸿沟

用户提问的方式和文档写作的方式往往不一样。用户问“这个月工资怎么还没到”,文档写的是“薪资发放时间及异常处理流程”。向量检索能处理一部分这种差异,但不够。查询改写(Query Rewriting)就是在这中间搭桥。

查询改写有几个层次:

  • 同义扩展:把“工资”扩展成“薪资、薪酬、薪水”。
  • 意图澄清:把模糊 query 改写成明确的检索 query。
  • 多查询生成:生成多个不同角度的 query,分别检索后融合。
  • 对话历史融合:多轮对话里,把历史上下文融进当前 query。

5.2 多查询生成的实际效果

我重点说说多查询生成(Multi-Query),这是我觉得性价比最高的改写策略。做法很简单:用一个小模型(或者大模型的一次调用)把用户 query 改写成 3 个不同表述的 query,分别检索,然后 RRF 融合。

比如用户问“报销要多久到账”,生成的三个 query 可能是:

  1. “报销到账时间”
  2. “费用报销处理周期”
  3. “报销款项发放时效”

这三个 query 分别检索,能覆盖到不同表述的文档。实测下来,多查询生成能把召回率再提升 10-15 个百分点。代价是检索次数变成 3 倍,延迟增加,但如果你用并行检索,延迟增加其实很有限。

代码上大概是这样:

def generate_queries(original_query, llm_client): prompt = f"""请将以下问题改写成3个不同表述的检索查询,每行一个: 问题:{original_query} 查询:""" response = llm_client.generate(prompt) queries = [q.strip() for q in response.split("\n") if q.strip()] return [original_query] + queries[:3]

注意,改写用的模型不需要太大,7B 级别的模型就够用。我用过一个 3B 的模型做改写,效果和 70B 的差别不大,但延迟低了一个数量级。

5.3 查询改写的注意事项

有几个坑要避开:

  • 别改得偏离原意。改写模型有时候会“过度发挥”,把“退款”改成“退货”,这就跑偏了。建议在 prompt 里强调“保持原意”。
  • 别生成太多 query。3-5 个就够了,再多收益递减,延迟线性增长。
  • 保留原始 query。改写后的 query 是补充,不是替代。原始 query 一定要参与检索,否则可能丢掉最直接的匹配。

6. 工程落地:从 Demo 到线上的那些坑

6.1 分块策略比 embedding 模型更重要

很多人花大量时间选 embedding 模型,却忽略了分块(chunking)策略。我的经验是:分块策略对最终效果的影响,比 embedding 模型选型大得多。

固定长度分块(比如 512 token)是最简单的,但会切断语义单元。一个完整的条款被切成两半,检索到一半也没用。更好的做法是按语义边界分块:按段落、按标题、按列表项。如果文档有结构(Markdown、HTML),优先按结构分。

我实际用的策略是:先按标题层级切分,如果某个 section 超过 800 token,再按段落切;如果段落还超,才按固定长度切。每个 chunk 前面加上所属标题的路径,比如“差旅管理 > 报销标准 > 国内差旅”。这个路径信息在检索时很有用,能提供额外的上下文。

另外,chunk 之间要有重叠。我一般设 10-15% 的重叠,防止关键信息正好落在边界上被切断。

6.2 元数据过滤:被低估的精确性武器

前面提到时效性和版本敏感的问题,解法就是元数据过滤。每个 chunk 存的时候,带上时间、版本、部门、文档类型等元数据。检索时先按元数据过滤,再做向量和关键词检索。

比如用户问“最新的差旅标准”,你可以先过滤出version = latest的 chunk,再在里面检索。这样就不会召回旧版本了。

元数据过滤在 Elasticsearch 里就是 filter 查询,在向量库里通常也支持。关键是在检索前过滤,而不是检索后过滤。检索后过滤会导致召回数量不足,因为过滤掉的文档已经占用了 top-k 名额。

6.3 缓存和降级:线上稳定的保障

线上系统必须考虑缓存和降级。RAG 的链路很长,任何一环出问题都会导致整个请求失败。

  • 查询缓存:相同的 query 直接返回缓存结果,命中率在客服场景能到 30% 以上。
  • 检索结果缓存:query 改写后的检索结果可以缓存,因为改写是确定性的。
  • 降级策略:如果 rerank 服务挂了,直接跳过 rerank,用 RRF 融合结果;如果向量库挂了,降级到纯 BM25。保证系统始终能返回结果,哪怕质量下降。

我踩过最大的坑就是没做降级。有一次向量库集群扩容,短暂不可用,整个问答系统直接 500。后来加了降级逻辑,向量库挂了自动切 BM25,用户基本无感知。

6.4 评估:没有评估就没有优化

最后说评估。RAG 系统最怕的就是“感觉还行”,没有量化指标就没法优化。

我建议至少跟踪这几个指标:

指标含义目标值
召回率@ktop-k 里包含正确文档的比例>85%
精确率@ktop-k 里相关文档的比例>60%
MRR正确文档的平均倒数排名>0.7
答案准确率人工评估答案正确的比例>80%
拒答率正确拒答(无相关信息时)的比例>90%

评估集要覆盖各种问题类型:语义类、精确类、否定类、时效类。每类至少 50 条。每次改动检索策略,都跑一遍评估集,看指标变化。别凭感觉调参。

7. 几个常见问题的排查思路

7.1 召回不准怎么办

先定位是召回问题还是排序问题。把 top-20 的召回结果打出来看,如果正确文档根本不在里面,是召回问题;如果在里面但排名靠后,是排序问题。

召回问题:检查分块策略、embedding 模型、查询改写。排序问题:加 rerank,调 RRF 的 k 值。

7.2 大模型幻觉严重怎么办

幻觉通常是因为 prompt 里塞了不相关的文档。检查 rerank 阈值,把低分文档过滤掉。另外在 prompt 里明确要求“只基于提供的文档回答,不知道就说不知道”。

7.3 延迟太高怎么办

先测各阶段耗时。通常是 rerank 和 LLM 生成占大头。rerank 可以换小模型、减候选集;LLM 生成可以换小模型、流式输出。检索阶段一般不是瓶颈。

7.4 多轮对话效果差怎么办

多轮对话的关键是 query 改写,把历史上下文融进当前 query。比如用户先问“差旅标准”,再问“那住宿呢”,第二个 query 要改写成“差旅住宿标准”。这个用 LLM 做很简单,但一定要做。

8. 我个人在实际操作中的体会

说了这么多,最后分享几点个人体会。

第一,别迷信单一技术。向量检索、BM25、rerank、查询改写,每个都有它的位置。大厂的 RAG 不是用了什么黑科技,而是把每个环节都做扎实了,组合起来效果就好。

第二,工程细节决定成败。分块策略、元数据设计、缓存降级,这些看起来不“AI”的东西,恰恰是线上稳定的关键。我见过太多团队模型选得很 fancy,但分块一塌糊涂,效果还不如老老实实做 BM25。

第三,评估要趁早。别等系统上线了才想起来做评估。从第一天就建评估集,每次改动都跑一遍。这样你才知道自己的优化到底有没有用。

第四,从简单开始。如果你刚开始做 RAG,别一上来就搞多查询生成加 rerank 加混合检索。先用 BM25 加向量做混合检索,跑通再说。效果不够再加 rerank,还不够再加查询改写。每一步都验证收益,别盲目堆组件。

这个领域变化很快,新的 embedding 模型、新的 rerank 方案、新的检索架构层出不穷。但底层的检索原理和工程思路是相对稳定的。把原理搞透,把工程做扎实,比追新更重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询