说实话,我最早接触 RAG 的时候,对“纯向量检索”是有滤镜的:把文本往 embedding 模型里一丢,算出向量,往向量数据库里一存,查询的时候算个相似度取 TopK,听起来又简洁又“智能”。直到我们把这套方案真正接到业务里,才发现翻车的地方远比想象中多。团队内部复盘时聊到一个很扎心的问题:为什么大厂做 RAG 几乎从来不用纯向量检索?答案不是大厂保守,而是纯向量检索这条路,在真实场景里真的走不通。
这篇文章不聊玄乎的“架构先进性”,就从一个实际做 RAG 项目的人视角,把纯向量检索的毛病、混合检索为什么成为标配、重排到底在解决什么问题,以及完整链路里有哪些容易踩的坑,一次性讲透。
1. 纯向量检索为什么在 RAG 里不够用
1.1 信息瓶颈:一段话压成一个向量,多少细节丢了
向量检索的基本思路:用双塔模型把查询和文档分别编码成一个稠密向量,再算余弦相似度。表面看是“语义匹配”,但本质上是信息压缩。一段几百字的文档,最终只保留几百维的浮点数向量。这个压缩过程必然面临信息瓶颈——尤其对精确信息极其不友好。
举个例子。你检索“ABN 公司去年营收是多少”,文档里写着“ABN 公司 2024 年营收为 735 亿元”。这句话经过 embedding 模型后,关键词“ABN”“2024”“735”都会被混进一个向量里。“公司”“营收”“年”这些高频词可能在注意力机制里占了主导,而“735”这个数字在整段话里只出现一次,很容易被“平均”掉。你检索出来的结果可能是“ABN 公司近五年营收趋势分析”,话题相关,但里面根本不含“735 亿”这个答案。
还有一个更经典的坑:否定词。文档写“该功能不支持多用户同时编辑”,查询是“支持多人编辑吗”。在向量空间里,这两句话的表示可能非常接近,因为除了“不”字之外所有 token 都一样。模型根本分不清“支持”和“不支持”在语义上的相反关系。这种问题在关键词检索里反而简单——分词之后,“不支持”作为一个整体词项直接就命中了,不会搞混。
老实说,embedding 模型擅长的是“话题级别”的联想,比如“怎么提高效率”能联想到“时间管理技巧”。但 RAG 里大量真实问题是“事实级别”的,要的是具体数字、编号、日期、版本号、是否定词。这些恰恰是稠密向量最不擅长、稀疏检索最擅长的地方。
1.2 “语义相似”不等于“有答案”
很多人对向量检索有一个误解:相似度高的文本,就是包含答案的文本。这个假设在 RAG 场景里常常不成立。向量相似度衡量的是“文本在语义空间里靠得近”,而 RAG 需要的是“文档片段能直接回答问题”,这两件事不是一回事。
做一个知识库问答时,用户问“台式机的保修政策是什么”,向量检索引擎很可能把“笔记本的保修政策”“平板电脑的保修政策”全捞上来,因为它们的主题词高度重叠、向量表示非常接近。但这些文档回答了用户的疑问吗?没有。用户要的是台式机那一页,不是笔记本那一页。真正的答案可能恰恰因为文档里少了“台式机”这个词,排名掉到了后面。
还有一个所有做过 RAG 的人都见过的现象:纯向量召回出来的 TopK 结果高度冗余。一篇长文档切成 20 个 chunk,向量检索经常把连续的中段全都召回来,因为它们上下文高度相似。结果前 5 条内容近乎重复,真正的答案被挤到了第 20 名开外。这也是为什么很多 RAG 系统早期表现很差——不是大模型不行,是检索阶段压根没把正确答案送到模型面前。
概括一下,纯向量检索三大毛病:一、对精确词项不敏感,数字、型号、否定词容易丢;二、相似度得分无法区分“话题相关”和“答案蕴含”;三、召回结果冗余严重,多样性差。这正是大厂 RAG 不在检索阶段只押注向量那一侧的原因。
2. 大厂 RAG 的混合检索究竟怎么搭
2.1 召回策略要“宽进”:两路缺一不可
大厂做 RAG 的第一原则,是召回阶段宁可多召回一些不相关的候选,也不能把正确答案漏掉。召回率一低,后面重排也好、大模型也罢,都没有补救空间。要做到高召回,最稳妥的方案就是混合检索——向量检索和稀疏检索并行,两边各自召回一批候选,再合并。
这两路的分工非常清晰:
| 对比维度 | 向量检索(稠密) | 稀疏检索(BM25/全文检索) |
|---|---|---|
| 擅长场景 | 语义改写、同义表达、概念相关 | 专有名词、编号、数字、精确短语 |
| 典型弱点 | 精确词项不敏感、结果冗余 | 不支持同义改写、词汇鸿沟 |
| 排序量纲 | 连续向量相似度 | 词频/逆文档频率统计 |
| 可解释性 | 弱,很难解释为什么召回 | 强,能看到命中哪些关键词 |
| 资源开销 | 需要向量索引常驻内存 | 索引轻量,更新快 |
实际项目中,两边都不可替代。比如用户提问“怎么让协同软件登录更快”,如果文档里写的是“减少登录延迟的优化方法”,纯 BM25 可能因为词面不匹配而漏掉,这时候向量检索能补上,因为“更快”和“延迟优化”语义相近。反过来,用户提问“版本 2.3.1 的导出 bug 修复了吗”,文档里写“2.3.1 修复了导出功能异常”,向量检索可能把 2.3.0 和 2.3.2 的文档也捞出来,但 BM25 能精确锁定“2.3.1”这个版本号。
说白了,混合检索不是炫技,是让两个互补的召回器各干各的活,二路齐发,把召回率拉上去。大厂的 RAG 管线里,这一步基本是标配,区别只在于融合策略是简单粗暴还是精细调优。
2.2 结果融合:RRF 为什么比加权平均稳定
两路召回集合合到一起之后,就要面临融合排序的问题。很多人第一反应是做加权平均:BM25 分数乘以 0.3,向量相似度乘以 0.7,再加起来。这个思路听着合理,实际一调参就想骂人,因为两种分数根本不在同一个量纲上。
BM25 的分数是一个没有上界的统计值,文档越长、词频越高,分数可能越大;向量余弦相似度则是 -1 到 1 之间的值。你把两个分布完全不同的分数强行加权,那个权重系数到底怎么解释?0.7 的向量权重意味着什么?没人说得清。每次调权重都是在碰运气,而且换一个数据集,最佳权重就变了,根本没有通用性。
业界更常用的方案是 RRF(Reciprocal Rank Fusion,倒数排名融合),核心思想是:不看分数,只看排名。公式非常简单:
score(d) = Σ 1 / (k + rank_i(d))其中 rank_i(d) 是文档 d 在第 i 路检索结果里的排名,k 是平滑系数,通常取 60。这个公式的直觉是:排在第一名得 1/61,排第十名得 1/70,排名越靠前贡献越大,而且两路之间的分数差异根本不参与计算,天然避免了量纲问题。
为什么 k 取 60 而不是 10 或 100?当年提出 RRF 的那篇工作里做了大量实验,发现 k=60 在多个数据集上表现稳定,取值大一点小一点影响都不大,是一个非常鲁棒的默认值。我自己实测也是,这个参数基本不用调。RRF 还有另一个好处——完全不依赖两路召回器的绝对分数校准,哪怕某一路的分数因为文档长度波动剧烈,也不影响融合结果。
2.3 元数据过滤:容易忽略但提升最大的一层
不少团队做混合检索时,只盯着两路算法怎么融合,却漏了最基础的一个操作:元数据过滤。向量数据库和搜索引擎里都能存结构化字段,比如文档来源、发布时间、文档类型、所属产品线、权限级别。检索之前先按这些字段把候选集过滤掉一批,再做相似度排序,效果往往比模型本身提分还明显。
我之前做过一个内部 IT 支持问答系统,知识库按产品线分成了办公软件、协同工具、数据库三类。用户提问“数据库连接报错怎么办”,如果不加产品线过滤,向量检索会把办公软件的“无法连接打印机”也捞出来,因为“连接”“报错”这些词太接近了,语义空间上它们确实很靠近。加了“产品线 = 数据库”这个过滤条件之后,无关文档直接进不了候选集,检索结果干净很多。
元数据过滤在执行上有一个关键点:chunk 切分时必须同步保留每段对应的元数据,不能只在原始文档上打标签。很多项目用现成框架切文本时,忘了把上级文档的字段透传到每个 chunk,导致过滤条件一执行就筛掉全部结果。
3. 重排(Rerank):把召回质量再提一档
3.1 双塔召回的排序能力是有限的
混合检索解决了“正确答案在不在候选集里”的问题,但候选集里面的排序依然不够准。原因在于,双塔模型计算相似度时,查询和文档在编码阶段是完全独立的,直到最后算余弦相似度才碰一次面,交互非常浅。
打个比方,双塔模型像提前给文档拍了一张“缩略图”存在库里,查询来了也拍一张缩略图,两张图放一起比对。缩略图能看出大概风格、大概色调,但看不出细节。可 RAG 要求的偏偏是细节判断:这份文档是不是真的回答了这个问题。
所以大厂 RAG 几乎都会在召回之后加一层重排。重排阶段不再用双塔,而是用交叉编码器。交叉编码器把查询和文档完整拼接成一长串文本,一起喂进 Transformer,让每个 token 在 attention 机制里充分交互,再输出一个匹配分数。这个分数比向量相似度可靠得多,因为它真正看到了“查询里的问题”和“文档里的句子”之间有没有直接的语义呼应。
有人会问,既然交叉编码器更准,为什么不直接用交叉编码器做召回?因为做不了。交叉编码器不能离线预计算文档向量,每个查询来了都得把所有候选文档实时过一遍模型。如果文档有几百万条,这个计算量直接爆炸。所以现实方案只能是:召回阶段用双塔粗筛出几十上百个候选,重排阶段再用交叉编码器精排一遍。
3.2 重排模型怎么用、参数怎么调
实际落地时,重排这层没有那么玄。业界的做法是把混合召回的结果合并、去重,候选集控制在 50 到 100 条左右,然后批量过重排模型,最终取 Top 5 到 Top 10 进大模型上下文。
这里有几个参数经验可以参考:
- max_length 一般设 512,不宜更长。交叉编码器要同时处理查询和文档,输入太长推理很慢,而且超过 512 的文本大多也是冗余内容。
- batch_size 从 16 起步,显存紧张就降到 8。重排阶段是纯在线计算,延迟敏感,批量大小要测一下再定。
- 候选集不是越大越好。我试过把候选扩到 200 条,重排成绩反而下降,因为大量不相关的文档会干扰模型的排序分布。50 到 100 条是甜点区间。
- 重排分数只在同一查询内部比大小,不能跨查询比较。这个分数天然没有归一化,不要拿它当置信度或阈值用,除非你对当前场景做过专门校准。
有一个容易被忽略的细节:重排之后的 TopK 里,如果分数相差很大,说明只有前一两篇文档是真正相关的;如果分数都挤在一起,说明候选集质量整体一般。这时候宁可根据分数断层截断,而不是机械地取 K 条。宁可少喂给大模型几条,也不要硬凑长度把噪音塞进上下文。
4. 完整 RAG 链路:从查询改写、混合检索到上下文组装
4.1 查询改写:先让“人话”变成“能检索的话”
用户真正输入的问题,往往不是理想的检索语句。RAG 系统上线之后你会发现,用户问的最多的是“那后来呢”“第二个方案有啥问题”“和前面那个比哪个好”这类指代不清的口语。这些查询直接走检索,向量模型再强也白搭,因为信息本身就不完整。
所以大厂 RAG 管线里,查询进入检索之前通常会先过一个“查询改写”环节。改写要干两件事:
- 补全省略和指代。结合对话历史,把“那后来呢”改写成“上一轮提到的某某方案的实施结果是什么”。
- 分解复合问题。用户问“A 方案和 B 方案的区别”,如果只拿这一句话去检索,结果可能两边都沾边但都不够。拆成“A 方案介绍”“B 方案介绍”“A 与 B 对比”三个子查询,分别检索再合并结果,效果会好很多。
这个改写层用大模型做比较灵活,但要注意延迟。一个查询改写指令跑几百毫秒,检索再几百毫秒,重排再几百毫秒,叠加起来用户就感觉慢了。优化办法是只在必要场景触发改写,比如检测到代词、指代或“对比”“区别”这类关键词时才走改写,其余情况直接检索。
4.2 上下文组装:把物料整理好了再交给模型
检索和重排做完,最后一步是组装上下文。这一步看起来简单,实际做起来有不少讲究。很多 RAG 效果不佳,问题不在检索,而在组装太粗糙。
组装阶段至少要处理四件事:
一是排序与截断。重排之后的候选按分数降序拼接,同时设一个总 token 上限。我一般控制在模型最大输入长度的 60% 左右,剩下 40% 留给回答生成和指令开销。
二是去重。混合检索里不同路召回容易产生内容重叠的 chunk,直接拼进去会浪费 token,还会让模型重复引用。简单做法是把候选之间做一次相似度计算,阈值超过 0.8 的视为重复,只保留分数更高的那条。
三是保留来源信息。给每个 chunk 前面加一个来源标签,比如“[文档 3][第 2 节]”,这样大模型回答时能引用来源,用户也能溯源。这一点在企业内部知识问答里几乎是刚需。
四是必要时做压缩。如果候选 chunk 太长,可以先让大模型对每个 chunk 生成一个一句话摘要,再把摘要拼进上下文。代价是增加一次模型调用和延迟,收益是能塞进更多不同来源的内容。具体取舍看业务对延迟的容忍度。
我自己操作时还有个习惯:chunk 切分时把文档标题拼进每个片段的最前面。比如某个 chunk 来自“4.3 节 数据迁移方案”,就在文本前面加上“[数据迁移方案]”。这样做之后,检索命中的上下文里自带标题信息,大模型回答时更清楚自己在引用哪一节,整体正确率有明显提升。
5. 落地方案与踩坑实录
5.1 一套可以直接抄的检索配置参考
如果不想从零造轮子,可以直接参考我这套配置思路。它不绑定具体技术栈,但按这个调参方向去落地,一般不会出大问题。
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 向量模型 | 开源中文嵌入模型 | 优先选面向中文、支持长文本且效果排名靠前的版本 |
| 稀疏检索 | 基于 BM25 的全文搜索引擎 | 直接把这次检索当做关键词匹配,不用额外训练 |
| chunk 大小 | 200 到 400 token,重叠 50 | 段落太大信息太杂,太小则上下文碎片化 |
| 元数据字段 | 来源、时间、产品线、文档类型、权限 | 每个 chunk 切分时都要带上 |
| 查询改写 | 有指代或复合问题时触发 | 用大模型改写,保留原查询兜底 |
| 召回数量 | 每路 Top 100 到 200 | 保证召回率,后续靠重排收割 |
| 融合策略 | RRF,k=60 | 不需要归一化分数,稳 |
| 重排候选 | 50 到 100 条 | 过交叉编码器精排 |
| 最终进入上下文 | Top 5 到 10 条 | 超过 10 条噪音大于收益 |
技术选型方面,重排模型尽量选专门训练过的中文 reranker,不要图省事复用 embedding 模型。你可以把重排看作文档级“把关人”,差一个模型,排序质量差一个档次。
5.2 常见问题排查速查表
按我的经验,RAG 检索链路出问题,大部分集中在下面这几类。列一个速查表,方便对照定位:
| 症状 | 优先排查方向 |
|---|---|
| 召回结果为空 | 先查分词结果是否合理,再看检索阈值是不是太严,最后确认元数据过滤条件是否把所有候选都筛掉了 |
| 召回结果全是无关内容 | 检查两路召回是否都接上了,尤其确认 BM25 那一路是否真的在跑,不能只靠向量一路 |
| 召回结果高度重复 | 缺少去重步骤,或者 chunk 切分粒度太大、重叠太多,增加相似度去重,调整重叠参数 |
| 召回到了但答案还是错的 | 检查重排是否生效,重排候选是否太少;再查最终上下文里有没有混入低分文档 |
| 查询稍长就效果暴跌 | 查询改写环节补全了上下文但没有控制长度,长查询会让向量模型注意力分散,可对改写结果做关键词提取 |
| 延迟偏高 | 看查询改写是否每次都触发、召回候选是否超过 200、重排 batch 是否过大,这三处按顺序排查 |
还有一个经验值得单独提:上线前一定要做回归测试集。不需要多大,三五十条真实用户问题就够了。每次换向量模型、改 chunk 参数、调重排策略,都跑一遍这个测试集,看相关文档的命中率和最终答案打分,心里才有底。没有评测集的迭代就是闭着眼睛换轮子,运气好能跑,运气不好就得回滚。
踩过几次坑之后,我最大的体会是:RAG 检索不是一个“选一个最强模型”的问题,而是“把几路不完美的检索器组合起来,让它们互相兜底”的问题。纯向量检索单看不弱,但单打独斗在真实业务里撑不住。把 BM25 请回来,把元数据过滤加上,把重排放进去,这套组合拳打下来,RAG 的效果才会有质的提升。
最后再分享一个小技巧。上线之后隔一段时间,把用户实际搜不到答案的问题捞出来,人工看一下失败原因。你会发现大部分失败不是模型不够强,而是检索链路里某一个环节的配置没跟上场景变化。定期拿真实数据回流迭代,比反复换模型带来的提升更扎实。