ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

纯向量检索撑不起生产级RAG?混合召回与重排才是正解

纯向量检索撑不起生产级RAG?混合召回与重排才是正解

前阵子在某客服知识库项目里调RAG,召回质量卡了很久。向量模型换了几版,embedding维度、距离度量、切块参数都调过一轮,badcase依然很顽固——用户问“发票号INV-2023-001231对应的金额是多少”,知识库里明明存着这条记录,向量检索却把它排在几十名开外,召回来的全是一些长得像的发票描述。当时我脑子里反复蹦出那句话:为什么大厂RAG从不用纯向量检索?

严格说,“从来不用”有点夸张,准确讲是“几乎不会把它当作唯一召回方案”。小项目、Demo、单一文档集,纯向量检索确实够用;但一旦进了生产环境,面对亿级文档、复杂query、精确数据、多租户隔离,单靠向量一定会出问题。这篇文章就围绕这件事展开:纯向量检索为什么撑不住,生产级RAG通常会把检索链路拆成哪几段,怎么落地,以及我踩过的几个坑。

1. 纯向量检索:它擅长什么,又守不住什么

1.1 向量检索的真正价值区间

先把话说公道:向量检索本身没毛病,它解决的是传统倒排索引解决不了的一类问题——“同义改写”“口语化提问”“跨语言近似”。

embedding模型把文本映射成向量后,语义相近的句子在向量空间里彼此靠近。比如用户搜“最近气温降了,孩子要不要加件外套”,知识库答案是“温差变化时的儿童穿衣建议”,字面上几乎没有重合词,但语义强相关。这种场景,BM25大概率漏召,向量检索能准确捞回来。这就是纯向量的高光时刻,也是RAG刚火起来时大家觉得“有了向量检索就能解决一切召回问题”的原因。

但这里有个容易被忽略的前提:向量检索的高光场景,集中在“模糊语义型”query。而真实业务知识库里,大量query根本不是这个类型。

1.2 精确信息面前,向量检索的先天劣势

业务知识库里的query,很多是冲着具体事实去的:合同编号、发票号、SKU条码、日期区间、版本号。这些信息讲究的是字段级精确匹配,而embedding模型恰恰不擅长建模数字和特殊字符。

为什么?向量模型记住的是“语义投影”,不是“字典条目”。你把一串混合字符当成一个整体映射到高维空间,任何轻微变动——比如INV-2023-001231和INV-2023-001232——都会让向量序列整体偏移。业务上只差一个字符就是两份不同单据,但向量表征里它们的距离可能非常近,甚至比正确答案还要近。

我做过一次统计,在客服知识库的真实query里,包含编号、金额、日期等精确型信息的占三成以上。这比例很高,你不可能当它不存在。

查询类型典型示例向量检索关键词检索
精确匹配型找合同HT-2024-0082的付款条款弱,容易召回相似但不相关单据强,分词后直接倒排命中
模糊语义型员工报销需要上传哪些附件强,能处理同义改写弱,字面不匹配就漏
长尾专有词梯度惩罚项在训练里的作用一般,对低频词向量不稳定强,词面命中就稳定
中英混排OpenAI的API限流策略弱,混合语言编码容易失真强,按词面切分更稳

这个表格我后来做技术方案时反复用过。它说明一个事:检索通道有很强的互补性,单押哪一路都会翻车。

1.3 “近似检索”的确定性损耗:ANN不是精确匹配

很多人忽略的一点是:向量检索落地到大规模场景,不可能做全量暴力扫描,基本都依赖近似最近邻索引,比如图索引、聚类倒排这类结构。

ANN的核心做法,是拿一部分召回率换查询速度。它不保证能找回数学意义上最相似的TopK。当文档量级到百万、千万以后,最相似的那条反而可能因为索引剪枝或图搜索路径限制被跳过去。这个概率不是零,而且会随着数据规模增大而积累。

关键词索引没有这个问题。倒排索引是确定性匹配:分词器分出来的词在倒排表里有没有,就是有没有,结果可复现、可解释。对业务方来说,如果我要“必须提到某合同编号”的召回结果,只有倒排索引能给你确定的答案。

这个不确定性放到线上就是一个非常头痛的问题:偶发漏召回,而且漏得没规律。做技术的都懂,最怕的不是永远错,而是时对时错——你没法定位,用户又偶尔被恶心一次。

1.4 相似度分数漂移,导致阈值根本没法定

再讲一个和“召回质量”同样重要的细节:阈值。

向量检索输出的相似度分数是相对的,同一个模型在不同业务语料上的分布可以完全不同。我见过一类query的top10相似度全在0.9以上,另一类query的top1只有0.5。你如果设一个全局相似度阈值来过滤“是否相关”,0.8的阈值会掐死第二类query,0.5的阈值会让第一类混进来大量噪声。

更麻烦的是,不同embedding模型产出的分数根本不在同一量纲上。你换一个模型,之前调好的阈值全部作废,线上效果直接波动。这个问题直接连锁到一个关键缺陷:纯向量检索的“无命中判定”很难做——系统不知道什么时候该承认自己没找到,于是只能硬着头皮把弱相关结果送给大模型。

这一节归纳起来就是:纯向量检索擅长处理“模糊问题找模糊答案”,但生产级RAG大量面对的是“精确问题找精确答案”,同时还需要系统具备确定性和可控性。这些恰恰是纯向量检索不擅长的事。

2. 大厂生产链路:多路召回加精排

2.1 三路典型的召回通道

真正到生产环境,常用的方案不会在“选哪个retriever”上纠结,而是让多种retriever并行召回,再合并排序。最典型的是三路:

第一路,稀疏关键词检索,基于倒排索引。适合精确token匹配,比如专有名词、编号、日期、代码片段。分词之后直接在倒排表里查命中,好处是可解释、可控,query里的关键信息在哪里被命中,一查便知;坏处是对同义改写、语义近似基本无感。

第二路,稠密向量检索。用embedding捕获语义相似,适合“换个说法但其实意思一样”的query。缺点前面已经说了:精确匹配弱、分数分布漂移、ANN带来不确定性。

第三路是规则和过滤通道。这一路经常被忽略,但非常关键。包括业务线过滤(文档属于哪个业务域)、权限过滤(这个用户有没有权限看这篇文档)、时间范围过滤、标签过滤,甚至布尔条件(必须包含词A且不能出现词B)。几十行配置,能解决前两路都解决不了的问题,而且它不会“悄悄答错”:既不会把无权访问的内容吐出去,也不会把过期版本当最新版本召回。在大厂多租户体系里,这一路几乎是刚性需求,没有它,前两路做得再好也上不了线。

多路召回的设计思想很朴素:每个通道都有自己的盲区,彼此重叠的部分可以互相验证,各自独有的部分可以补位。检索通道之间的能力是正交的,不是互相替代的关系。

2.2 为什么“一个混合模型”替代不了多路策略

现在有些开源模型能同时输出稠密向量和稀疏权重,方便你用一个模型同时兼顾语义和词面匹配。这个思路很好,也确实简化了工程。但从系统设计角度,一个模型产出两路特征,并不等于“两条独立通道”。

原因是:这两路特征仍然共享同一个模型权重,领域迁移如果没做好,两条腿会一起变形。比如业务知识库是某精密仪器售后文档,垂直度高,通用embedding处理不了,那你稠密路和稀疏路同时失效,问题反而更难隔离。

多路召回的另一个价值在排错。线上效果变差时,我可以单独看每一路的Recall@k,看是哪路拖后腿,就调哪一路:词典、过滤器、向量模型、权重,各自优化互不干扰。组件化、可替换带来的长期收益,远比“少维护一个组件”高得多。这也是我后来理解“大厂不用纯向量”的切入点之一——系统设计要的不是某一个时刻的惊艳效果,而是持续可控的稳定性。

2.3 重排阶段:从“召回”到“精排”

召回追求“全”,重排追求“准”。为什么要分成两段?核心原因是计算资源的性价比。

如果对全库几千万条文档都用交叉编码器精排,成本完全不可接受。但只对召回得到的Top50到Top100做精排,性价比就非常合适了。

交叉编码器的原理是:把query和每条候选文档拼成一个完整序列,过一个完整的Transformer,输出一个相关性分数。它和双塔embedding的关键差别在于:双塔是query和doc分别编码成向量再算相似度,信息交互发生在最后一层;交叉编码器在输入端就把两边拼在一起,token之间互相做注意力,所以能捕捉到非常细粒度的交互信号。

举个例子,用户问“这个功能怎么关闭?”,三个候选:

  • 文档A:“功能开启后系统自动运行”
  • 文档B:“如需关闭功能,请在设置中操作”
  • 文档C:“功能关闭后将不再记录日志”

双塔向量和三个候选的相似度差距可能不大;但交叉编码器能准确识别出B才是真正回答了“怎么关闭”的文档,A是干扰项。这种细粒度判断能力,在Top5排序稳定性上的提升是肉眼可见的。

这也是大厂不直接用纯向量检索Top结果当作最终上下文的原因之一:向量Top5的排序并不总能保证第一个答案就是正确的那句话。但加上二阶段重排后,答案位置的稳定性会明显改善。

3. 落地实践:从查询解析到融合排序

3.1 查询解析:先把query里的“精确指令”识别出来

我第一次真正意识到这一步有多重要,是在客服知识库的badcase分析会上。当时查了一个case:用户问“采购合同HT-2024-0099的付款方式是什么”,向量模型召回的top10里只有一条是那份合同,排在第8位,最后根本没拼进大模型上下文。

后面大家达成的共识是:把query交给多路召回之前,要先做一次轻量解析,识别里面的“实体指令”。用正则和简单规则就够:

  • 识别编号模式,比如字母加数字组合、固定前缀
  • 识别日期、金额、版本号等结构化信息
  • 识别业务域关键词和术语表词汇

解析结果做两件事:一是给关键词通道加权,或者在查询里强制构造bool条件;二是给过滤通道添加业务域限制。例如识别到“HT-2024-0099”之后,直接构造一个“必须包含HT-2024-0099”的检索条件,这条基本就漏不掉了。

这个阶段还可以加一层极轻的意图分类:query是高精确定向型,还是泛语义咨询型?不同类型走不同的通道权重组合。

意图类型典型示例主召回通道辅助通道
精确指令型查合同编号、发票金额关键词向量+过滤
知识咨询型报销流程怎么走向量关键词
规则限定型某权限下的可见内容过滤关键词/向量

意不意图无所谓,关键是它提醒系统:不要用语义匹配硬扛精确匹配。

3.2 融合排序:RRF还是分数归一化加权?

多路召回之后,怎么合并成一个有序列表?两个主流的办法:RRF和归一化加权。

RRF全程是Reciprocal Rank Fusion,它的核心思路是只看排名,不看分数。公式很简单:

from collections import defaultdict def rrf_fusion(rank_lists, k=60): scores = defaultdict(float) for rank_list in rank_lists: for rank, doc_id in enumerate(rank_list, start=1): scores[doc_id] += 1.0 / (k + rank) return sorted(scores.items(), key=lambda x: x[1], reverse=True)

某条文档在向量通道排第3,在关键词通道排第12,那它的RRF分就是1/63加1/72。k是一个平滑常数,一般取60左右,它控制排名靠后文档的分数差异敏感度。

RRF的最大优点是“分数免疫”。各路召回打分的量纲不一致、分布不一致,完全无所谓,你只需要知道相对顺序。这解决了1.4节提到的分数漂移问题。

另一种做法是归一化加权:把每路分数做min-max归一化到0到1,乘以权重再加和。听上去更“科学”,但实际操作中,线上分数分布是持续漂移的,min和max不稳定,归一化后的结果也会抖动,做不好效果比单一通道还差。我的经验是:第一版先上RRF,它能用最短时间把线上效果稳定下来;等你把各通道特点摸清了,觉得非要上加权不可,再上,并且做好趋势监控。

3.3 交叉编码器重排的正确姿势

重排环节最大的成本是计算,所以要把每一分算力花在刀刃上。

候选规模控制在Top50到100。召回阶段各路合并后取并集,交给cross-encoder打分。我实测过把候选从100提到200,最终Top5评测指标几乎没有变化,延迟却翻了将近一倍,所以100已经算是比较保守的上限。

长文档要先切段落再重排。交叉编码器有token长度限制,直接拼接长文档会截断,答案在后半段时容易丢。比较好的做法是把候选文档按语义切成段落,段落级重排,再把最优段落对应的整篇文档带回来。

高频query要加缓存。相同query和几乎不变的候选集,重排分数完全可以复用。我给团队加的简单方案是30秒TTL的LRU缓存,延迟收益非常直观。

整体链路可以用一个表看清:

链路层次核心职责工程实现典型参数
查询解析识别意图和精确指令正则/规则/术语表编号正则、术语表
多路召回先别漏关键词+向量+过滤每路取Top50
融合排序合并出最终候选RRFk=60
交叉编码器精排兜底cross-encoderTop100到Top5
供给LLM组装上下文截断/引用标注按最大窗口控制

4. 实测中遇到的坑与排查清单

4.1 分数不对齐,加权融合翻车实录

有一阵子我迷信“更精细的融合方式”,把关键词路和向量路分数做了min-max归一化再加权。上线后发现召回质量不升反降,关键词路明明命中了目标文档,融合后排名还是上不去。

排查后发现问题出在分数分布上:关键词路的分数普遍在0.1到0.3之间,向量路的分数普遍在0.7到0.9之间。二者权重即使设成五五开,向量路也实际占主导,关键词路的命中贡献被持续压制。本质上还是分数语义单位不同,强行加和很别扭。

后来改回RRF,问题立刻缓解。所以我现在的建议非常明确:在不完全掌握各通道分数分布规律之前,RRF就是那个“稳”的选择。它不是理论上最优,但它在工程上最不容易翻车。在某个知识库评测集上,纯向量召回的Recall@20大约是0.72,加上关键词通道后到0.85,再上精排后Top5正确率改善明显。这个提升路径比任何花哨技巧都实在。

4.2 重排拖慢延迟,先别急着加机器

上了cross-encoder后,链路延迟涨得很快,P95从150ms直接飙到500ms。第一反应是加机器,但后来真正做的是四步:

第一步,加缓存。发现大量高频query都在重复计算重排分数,缓存命中后延迟立刻降了一截。第二步,砍候选。把100条减到60条,延迟再降,质量基本没变。第三步,把重排服务拆成独立部署,远程调用带来的不确定性也少很多。第四步,用TensorRT这类方案做模型加速,或者换一个更轻量的重排模型。最终P95稳定在250ms左右。

这个过程中最深的体会是:链路延迟的瓶颈往往不在模型推理本身,而在无效计算和重复计算上。

4.3 索引两路不一致,新旧文档打架

混合检索最容易踩的坑是“两条索引不同步”。某次业务方上传了一批新合同,运维只重建了向量索引,关键词索引还是旧的。用户搜最新合同标题,向量路召回了,关键词路没命中,融合后排名被关键词路的缺失拖累,结果依然搜不到。

那天排查浪费了不少时间,第一反应是调融合权重,反复试也没有效果,最后检查索引状态才发现问题。后来团队立了一个规矩:每份文档统一维护一个版本号,两路索引都更新完成后版本号一致才可见;写链路里加了校验和告警,再没出过类似问题。索引一致性这种问题,平时不显眼,一出就误大事。

4.4 阈值与“无命中”判定:宁可不答,不要乱答

纯向量时代难定阈值,加了混合检索之后依然要处理这个问题。生产级RAG必须接受一个事实:知识库里可能真的没有用户想问的内容。

这时候千万不要硬给大模型塞几段弱相关文本。大模型会基于这些弱相关上下文,生成一段“看似合理但完全不对”的答案——也就是所谓幻觉。用户体验上,“抱歉没找到”比“一本正经地编”要好得多。

阈值怎么定?基于评测集统计。在你的人工标注评测集上,分别看相关case和不相关case的重排分数分布,找一个能最大化F1的切分点。初始取值偏保守一点,宁可多拒绝几次,也别给大模型喂垃圾上下文。上线后持续复盘badcase:误拒太多就往下调,误答太多就往上调。没有这个环节,混合检索就永远差最后一公里。

最后再分享一点个人体会。检索系统的设计里,“稳定”的价值永远大于“惊喜”。向量检索经常能带来惊喜,但生产环境最怕的就是不可控的惊喜。这也是我越来越理解“大厂不用纯向量检索”这句话的原因——它不是否定向量方案本身,而是成熟系统必须对所有的不确定性建立预案。你现在如果还在用纯向量检索,遇到幻觉或者漏召回,可以先翻一翻badcase,看看里面有没有大量精确编号、专有名词没命中。如果有,把关键词通道和重排能力补上,效果往往立刻上一个台阶。这是我踩了多轮坑之后,最想告诉同行的一件事。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进