ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

从纯向量检索到多路召回:生产级RAG检索链路改造指南

从纯向量检索到多路召回:生产级RAG检索链路改造指南

如果你现在去搜RAG入门教程,十篇里有八篇会告诉你同样的三步走:把文档切片,丢进embedding模型转成向量,存进向量数据库;用户提问时同样向量化,算余弦相似度,取top-k拼进prompt。这套方案在demo阶段确实漂亮,语义理解有模有样,回答也像回事。但凡是把RAG真正推到生产环境、接了真实业务数据的人,几乎都会在一个月内开始动手改检索链路。原因只有一个:纯向量检索的缺陷,在演示数据上根本看不出来。

我见过不少团队从"向量数据库加embedding"起步,最后无一例外地补上了关键词通道、规则通道和重排模型。这不是他们闲得慌,而是生产数据会逼着你承认一件事——向量检索擅长理解大意,但不擅长精确定位。这篇文章就把这件事讲透:纯向量检索到底输在哪里,大厂实际在用的检索链路又长什么样,以及你照着改造时最容易踩的坑都在什么地方。

1. 纯向量检索的"童话剧本":Demo能跑通,上线就翻车

1.1 教程里最常见的搭建方式

先还原一下最经典的纯向量检索RAG长什么样。流程极简:离线把文档切成几百字的块,每个块过一遍embedding模型得到向量,写进向量数据库;线上用户提问,也是同一个embedding模型向量化,然后在向量数据库里做近似最近邻搜索,取相似度最高的三五个块,连同问题一起丢给大模型生成答案。

这个链路最诱人的地方是"语义"。你说"苹果手机电池不经用",它能召回"iPhone续航衰减怎么办"这种字面上毫无重叠的文档,这是传统关键词检索给不了的。所以很多人一开始天然觉得,只要embedding模型够强、向量数据库够快,RAG这事就算成了。我在早期做知识库问答时也是这么想的,demo跑起来那叫一个顺,问什么答什么。

1.2 真实数据下的三类翻车现场

换到真实业务数据后,情况就开始不对劲。我归纳过三类最常见的翻车,几乎每个团队都会撞上其中至少一个。

第一类是精确标识符查不到。用户问"订单SO-20240715-0038的状态是什么",或者"PRD-8842这个需求什么时候上线",这类问题本质是精确匹配,但向量检索是按语义相似度找的。它会把"订单"和"状态"这些词当成主要信号,中间的编号反而被当成噪音。结果top-k里全是讲订单流程的通稿,真正的订单记录排在几十名开外,甚至根本进不了召回窗口。

第二类是分数全线飘高、没有区分度。embedding模型产出的向量做过归一化之后,余弦相似度的分布通常非常集中。我在一个内部知识库上实测过,相关文档和不相关文档的分数差距常常只有0.02到0.05。也就是说,向量数据库告诉你"前五条都是0.82左右",但这个0.82既可能是真相关,也可能只是"聊的话题相近"。你把top-5直接拼给大模型,模型就得从一堆似是而非的材料里硬找答案,幻觉率自然压不住。

第三类是数据规模上来之后召回质量明显下降。几百篇文档时向量检索表现尚可,等到了几十万、上百万量级,相似的文档越来越多,向量空间里"近邻"密集得像早高峰地铁。这时候top-k的结果会被大量同类文档挤占,真正包含答案的那一条,可能恰好因为分块边界切得不好而跟查询向量离得远了那么一点,就被挤出去了。

这些问题单独看都像是"调参能解决",但合在一起你就明白了:纯向量检索的本质是把"检索"这件事简化成了一个纯粹的几何问题。而真实检索场景里,精确匹配、结构化约束、分块粒度这些信号,根本不是单纯靠向量距离能表达出来的。

2. 纯向量检索失灵的底层原因:不是模型不够强,是任务被简化错了

2.1 一个向量装不下一个分块里的"多重身份"

很多人对embedding有个误解,以为它把一段文本的"全部含义"都压缩进了向量。实际上,一个固定维度的稠密向量(常见的是768维、1024维、1536维)能承载的信息量极其有限,它只能保留文本最主导的语义方向。

问题就出在"最主导"这三个字上。一个分块里往往同时包含多个实体、多个事实、多个主题。举个最常见的合同类文档:"甲乙双方于2024年3月签订服务协议,合同金额500万元,服务期限12个月,付款方式为分期付款,其中违约金条款约定为合同总额的20%。"这一整段压缩成一个向量后,相当于把"签约时间、金额、期限、付款、违约金"五件事各自的方向叠在一起,最后取了个平均方向。用户问"这笔合同违约金怎么算",和问"合同什么时候签的",在这个平均向量上的区分度极小。向量检索根本分不清该把哪条排前面。

这不是embedding模型的错,而是"一段文本一个向量"这个范式天然有信息瓶颈。想要让向量检索真正精准,就得把分块切到足够细,让每个块只有一个核心信息。可切细了又带来另一个问题:上下文割裂,大模型拿到的信息不完整。这个矛盾恰恰是纯向量检索绕不开的死结。

2.2 语义匹配解决不了精确匹配

embedding模型训练时优化的目标是语义相似度,模型学到的能力是"这两句话是不是在说同一件事"。但生产环境里的检索需求,有很大一部分是精确匹配:订单号、手机型号、版本号、报错码、人名、地名、产品SKU。

这类文本的致命特征是"差一个字符就完全不是同一个东西"。比如用户搜"K8s 1.28版本升级注意事项",和"K8s 1.26版本升级注意事项",语义几乎一样,但答案完全不同。向量模型很难对这种细微差异保持敏感,因为训练语料里不会有多少样本专门区分"K8s 1.28"和"K8s 1.26"这两个高度相似但答案互斥的查询。结果就是,向量检索经常把两个版本混在一起,召回的文档看着相关,实际用起来一点都不相关。

还有一类更隐蔽的问题是词粒度损失。中文embedding模型在切词时可能把"降龙十八掌"这种固定搭配切成"降龙"和"十八掌",如果语料里恰好是另一种写法,向量就匹配不上。而关键词检索完全没有这个问题,倒排索引天生就是按词精确匹配的,你写的是什么,它就找什么。

2.3 相似度分数的"头部"不够尖,Top-K的选择进退两难

纯向量检索召回质量差,还有一层数学上的原因:余弦相似度的区分度不足。向量经过归一化后,所有向量都落在高维单位球面上,维度越高,任意两个向量之间的夹角就越趋于稳定,相似度分数会大量集中在某个小区间里。我见过一份实测数据,不相关文档的平均相似度也能到0.78,相关文档是0.83,中间只隔了0.05。这种分布下,top-5和top-50之间拉的差距很小,你根本没法靠分数来信任排序结果。

Top-K参数也很尴尬。K设小了,容易漏召回;K设大了,噪声全进来了,大模型面对一堆低质量材料反而更容易被带偏。纯向量检索没有任何机制能告诉你"这个问题应该扩大召回"还是"这个问题只需要一条精确答案"。它只会机械地每次都返回K条。生产环境里查询类型千变万化,一个固定K值永远不可能对所有场景都合适。

3. 大厂的解法之一:多路召回,把一个问题拆给多个"选手"做

3.1 典型召回通道怎么搭配

既然纯向量检索覆盖不了所有查询类型,行业的共识做法是"多路召回":让不同检索机制各跑各的,最后把结果合并。每一路只负责自己擅长的查询类型,不追求一路通吃。

实际工程里最常见的是三到四路并行:

  • 向量通道:负责语义泛化。用户换了个说法、用了同义词、口语化提问,靠它兜底。召回量一般给50条左右。
  • 关键词通道(BM25或倒排索引):负责精确词汇、型号、编号、人名。只要用户问题里出现了明确的高信息量词汇,它基本都能命中。召回量同样50条左右,但原理和向量完全不同,两者互补性很强。
  • 规则/字典通道:负责强规范的数据。比如"订单号"就直接走正则匹配订单表;"报错码"就走数据库精确查询。这一路召回量小,但精度极高,命中即用。
  • 元数据/结构化通道:负责"时间、作者、类目、部门"这类过滤条件。用户问"上个月某某部门发布的安全规范",没有这一路,前两路都会在时间维度上失明。

一句话总结多路召回的价值:它不是锦上添花,而是把"检索"从单打独斗变成团队协作,每路选手都用自己最擅长的方式捞一遍,然后统一汇总。这样即使某一路完全失灵,另外几路还能把结果带回来。

3.2 分数不能直接相加,得用RRF融合

多路召回之后立刻面临一个头痛的问题:各路分数怎么合并?向量通道输出的是0到1的余弦相似度,BM25输出的是理论上无上界的词频权重分,规则通道可能根本没有分数只有命中标记。把它们直接加在一起,等于让不同量纲的分数打架,谁的量级大谁就赢。用归一化?也难,因为BM25的分数分布跟语料长度、词频高度相关,归一化后依然不稳定。

工业界最常用的解法是RRF(Reciprocal Rank Fusion,倒数排名融合)。它的核心逻辑很朴素:不看具体分数,只看排名。每条文档在各路召回里的名次,用这个公式折算一个融合分:

score(d) = ∑ 1 / ( k + rank_i(d) )

其中k是平滑常数,常见取60。假设某条文档在向量通道排第3,在BM25通道排第10,那么它的融合分就是1/(60+3) + 1/(60+10)。最终按融合分从高到低排序。

这个思路妙在它是纯排名驱动的,完全不依赖各路分数是否可比。语义相关但表述换了说法的文档,向量通道会给它高名次;精确命中关键型号的文档,BM25会给它高名次;两种信号同时命中的文档,自然在两个通道都有高名次,融合后稳稳地排到前面。RRF实现起来就几十行代码,不需要训练任何模型,也不需要调权重,是性价比最高的融合手段。

3.3 各通道的召回量怎么设置

多路召回不等于每路都拉到一样的数量。我的经验是:高精度的通道可以少拉,高召回的通道必须多拉。规则通道和元数据通道本来命中就少,返回5到10条够了;向量和BM25这两个主力通道,建议各拉50到100条。这样在融合阶段,两个通道的重叠部分有机会被放大,单通道独有的结果也不至于被直接丢掉。

融合后一般还要再做一轮截断,只保留融合分最高的20到50条进入下一阶段。注意,这个阶段的目标是"别漏",不是"选准"。把候选集从几万压到几十,靠的是多路召回和RRF共同完成。真正决定最终答案质量的,是接下来要说的一步——重排。

4. 大厂的解法之二:召回之后必须接重排,别把召回结果直接喂给大模型

4.1 召回只看"大概齐",精排看"准不准"

如果现在你还想省掉重排这一步,建议先记住一个原则:召回阶段用的双塔/向量模型,和最终判定"这条文档到底能不能回答这个问题"是两种不同的任务。双塔模型因为查询和文档被编码成两个独立向量,交互信息只能在最后的相似度计算里碰一次面,它能做的是粗粒度筛选。想要精确判断"这段话里是否包含问题的答案",需要的是细粒度的词级、甚至跨度级的交互匹配。

所以生产级RAG的标准流程是两段式:第一阶段多路召回,把候选从全库压到几十条,要求是查全;第二阶段用重排模型对候选逐条打分,把最相关的5到10条挑出来,要求是精准。这两个阶段一粗一细,各司其职,谁也别想替代谁。

4.2 为什么重排要用交叉编码器

重排阶段业界普遍用的是交叉编码器(cross-encoder)。它的做法是把查询和文档拼接成一段文本,一起塞进模型,让模型同时看到两侧的每一个token,然后直接输出一个相关度分数。听起来跟embedding没什么区别,但关键在于交互方式:交叉编码器让查询里的每个词都有机会"盯着"文档里的每个词做注意力计算,比如查询里有个"违约金",文档里恰好有"违约金条款",这个精确的词级对齐信息会被模型捕捉到并用在打分上。双塔模型做不到这一点,因为它在编码查询时根本看不到文档,只能靠向量空间里的"近似"。

代价自然也有。交叉编码器要对每个查询-文档对单独算一次前向传播,假设候选集有100条,就要算100次,看一次查询的成本比双塔模型高出两个数量级。所以它只适合在候选集已经缩小到几十条时使用。我见过有些团队一上来就把全库文档都过一遍交叉编码器,检索延迟直接爆炸,这是典型的把重排当召回用的错误姿势。

重排模型的数量可以先从几十条起步:多路召回融合后取50条,重排后取前10,再拼进prompt。这个"50进10"的比例在大多数场景下都能在效果和延迟之间取得平衡。

4.3 重排模型不是随便找个通用模型就行

常见的做法是拿开源的排序模型直接用,但通用排序模型在专业领域效果往往一般。原因很简单:排序模型在训练时见过的查询-文档对都是通用领域的,你的业务里有大量专属术语、专属文档结构,它判断不出来。

要提升重排效果,至少需要做两件事。第一,在业务数据上微调。用"用户的真实查询"和"被确认有用的文档"拼成正样本,再从召回结果里随机捞一些没被用户点开、或者被明确反馈无用的文档当负样本。正负样本的比例控制在1比3到1比5比较合理。第二,做困难负样本挖掘。如果负样本太容易区分(比如完全不相关),模型学不到精细的判断力,要专门挑那些"看着相关但实际回答不了问题"的文档做负样本,逼着模型学会区分。

我自己的经验是,重排模型上线后,RAG最终答案的引用准确率能提升10到15个百分点。这个环节是整条链路里投入产出比最高的一步,强烈不建议省。

5. 一套能落地的生产级检索链路:从查询改写开始

5.1 先处理查询,再走多路召回

很多人做RAG时习惯把用户原话直接丢给检索器,这一步在生产环境是很浪费的。用户的问题往往带口语、带错别字、带指代,直接检索等于让检索器在最差的条件下去工作。生产级链路通常在查询进入检索之前加一个"查询改写"模块。

改写包括三类操作。第一类是纠错,把用户打错的专有名词修正回来,比如"密玛"改"密码"、"myql"改"mysql"。第二类是补全指代,用户问"这个功能怎么配置",需要结合对话上下文补成"XX系统中的XX功能怎么配置"。第三类是扩展同义表达,把"怎么退钱"扩展成"退款、退货、退费流程",让各路召回通道都有更大的命中面。改写可以用大模型来做,也可以先跑规则再做大模型兜底。注意控制改写本身的延迟,我一般把改写模块的耗时压在100毫秒以内。

5.2 分块策略和索引设计要一起考虑

纯向量检索对分块粒度极其敏感,但多路召回链路给了你"拆得细、捞回来再拼"的自由。比较稳妥的方案是分层分块:把文档切成较小的语义块用于检索(比如128到256个token),同时保留这些小块所在的父级段落。检索命中小块时,把父级段落一起作为上下文交给大模型。这样既保证了检索的精度,又不会因为块太小而丢失上下文。

索引层面,向量索引和倒排索引可以部署在同一个搜索引擎里,也可以用两套独立的系统。关键是两边要共享同一套文档ID和分块ID,这样RRF融合时才能对齐。元数据(时间、作者、类目)建议单独建字段,方便在召回前或召回后做过滤,而不要混进embedding里。

5.3 多路召回到生成之间的完整顺序

把前面所有环节串起来,一条生产级检索链路的完整顺序是:

  1. 查询改写:纠错、补全、扩展,耗时控制在100毫秒内。
  2. 多路召回:向量、BM25、规则、元数据过滤并行执行,每路各自返回带排名的候选。
  3. RRF融合:把各路候选按公式合并排序,截断到50条。
  4. 重排:交叉编码器逐对打分,截断到5到10条。
  5. 拼装上下文:把命中的分块连同父级段落一起,按重排分数拼进prompt,交给大模型生成。

每一层都在压缩候选范围、提升精度。这样做之后,大模型拿到的材料质量和纯向量检索时代完全不同,幻觉率下降是必然的。

5.4 评估链路不能只看"答得对不对"

搭建完链路,评估方式也要跟着升级。很多团队只用"大模型回答是否正确"来做判断,这是远远不够的,因为回答错了你根本不知道错在检索还是生成。

我的建议是分三层评估。第一层是检索层指标,包括recall@k、precision@k、MRR、NDCG,直接衡量"正确答案是否在候选里、排得够不够靠前"。第二层是重排层指标,看重排后的top-10里正确答案的占比有没有提升。第三层才是端到端指标,把最终答案给人工或者用大模型自动打分,评估答案的完整性和引用命中率。线上还要盯延迟:从查询进入到答案输出,多路召回加RRF加重排,整体预算通常压在1秒以内,其中重排环节是大头,候选集数量直接决定它的耗时。

6. 几个反复踩过、但几乎没人写进文档的坑

6.1 元数据过滤的先后顺序,直接影响结果对不对

元数据过滤放召回前还是召回后,是个经典的翻车点。先过滤再向量检索,意味着过滤后只剩很少的向量参与搜索,如果是按时间过滤,某个类目下可能只剩几十条向量,近似最近邻搜索在高维空间里样本太少,结果会非常不稳定。先向量检索再过滤,风险则是召回阶段根本没把该过滤的那条捞进来。比如用户要求"只看上个月的文档",而那条满足条件的文档在向量排序里排到了120名,你top-50召回根本没包含它,后面再怎么过滤都找不回来。

我验证下来比较可靠的做法是:过滤条件能提前就提前,但过滤后的候选集如果太小,要自动放宽过滤条件重新召回;同时向量通道的召回量要适当放大,给后置过滤留足余量。另一个思路是把关键的元数据条件拼进查询向量里参与检索,但这个方法对embedding模型要求高,不是所有场景都好用。

6.2 文档更新后,向量索引里的"旧幽灵"

文档被修改或删除后,向量数据库里的旧向量不会自动消失,这会导致用户搜到一个已经不存在的版本。纯向量检索时代大家忽视这个问题,因为demo数据根本不变。生产环境里文档每天都在变,我吃过一次大亏:一份政策文件改了条款,旧版本还在向量索引里,用户检索时新旧两个版本同时被召回,大模型把矛盾的条款都编进了答案里。

解决思路是给每个分块维护版本号和生效状态,检索时对结果做状态过滤;同时在更新流程里保证"先写新向量、再删旧向量、最后更新倒排索引",顺序反了就会出现窗口期查不到新内容。如果改动频繁,建议不要做实时逐条更新,而是定时批量重建索引,重建期间让检索服务平滑切到上一版本索引。

6.3 分块边界切出来的"半句话"

最后说一个很难排查的坑。文档在切分时如果正好从一句话中间断开,前半块和后半块各自都成了"残句"。向量检索时,残句跟查询的相似度会异常低,即使内容本身高度相关也捞不回来;就算捞回来了,大模型拿到残句也补不全信息。表现就是"系统明明有这份文档,但怎么都答不对"。

这类问题很难通过调检索参数解决,更有效的办法是改进切分策略:设定最小的句子完整性约束,切分点必须落在句号、问号、感叹号等完整句边界上;同时让分块之间保留10%到20%的重叠作为缓冲。检索命中小块后,把相邻的块也一起带上,给大模型足够的上下文重建空间。

6.4 缓存和降级:链路越复杂,越要想好"挂了怎么办"

多路召回加重排的链路组件多,任何一个环节抖动都会影响整体。检索结果可以按查询做缓存,同样的热门问题直接命中缓存,连大模型生成都可以省掉。向量通道如果因为索引重建或者服务异常暂时不可用,至少要保证BM25通道还能顶上,输出质量会降一档,但服务不至于完全挂掉。

这套链路改完以后,我从不敢说"纯向量检索完全没用"。在语义泛化、跨语言检索、模糊提问这些场景里,它依然是最能打的一路召回通道。但指望它单独扛住生产RAG的全部检索压力,就真是低估了真实数据的复杂程度。我的建议很直接:先按"向量加关键词加规则"把多路召回搭起来,再用RRF融合,最后补一个重排模型,哪怕第一版用的全是默认参数,效果也一定会比纯向量检索高一个台阶。等跑一段时间拿到线上数据,再针对具体场景调每一路的权重和召回量。检索链路这个东西,从来不是选一个最优方案,而是让每个环节都留出可观测、可替换、可退化的空间,因为真实数据永远比你预想的更刁钻。

RELATED READING

延伸阅读

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