
1. 先想明白一个前提RAG的检索决定生成的下限1.1 检索环节不是锦上添花而是答案质量的天花板最近好几个做RAG的朋友问我同一个问题为什么我们内部的知识助手从来不用纯向量检索我一开始也以为是性能问题后来把检索链路翻来覆去拆了几遍才想明白这根本不是快慢的事而是纯向量这条路在真实业务里根本走不通。RAG的完整链路是先找文档再让大模型根据文档写答案。检索环节决定了大模型能看到什么而大模型只能基于你喂给它的上下文作答。如果正确答案没有被召回生成模型要么假装知道、硬着头皮编要么直接说不知道——在知识库场景下前者比后者危险得多。业内常说一句话RAG的上限由检索决定生成只是把检索结果包装成通顺的答案。这句话虽然不够严谨方向是对的。我做过的几个知识库项目里对比测试结果惊人地一致检索召回率相差20个百分点时最终答案正确率的差距远不止20个点因为错误召回还会把模型带偏。这也是为什么大厂在生产环境不用纯向量检索。纯向量检索在demo里跑得通几万个chunk的小规模、几百条测试query效果似乎还行。但一旦进入百万级chunk、高频更新、强时效性、错误成本高的生产环境纯向量的问题就会被成倍放大。1.2 语义相似不等于业务相关这两件事必须分开要理解这个问题先得知道向量检索是怎么工作的。embedding模型会把一段文本映射成一个几百维的向量语义相近的文本在向量空间里距离更近。这听起来很理想但语义相近和业务相关是完全两码事。举个我在实际项目中遇到的例子。用户问的是这笔订单能不能退款知识库里有两条文档一条是退款政策与流程另一条是订单状态说明与售后联系方式。从语义上看两条都跟订单、退款沾边向量距离也差不多。但第一条文档里的具体政策条款才是用户真正需要的第二条只是泛泛而谈。纯向量检索经常把这类泛相关但不解决问题的内容排在前面。更典型的是精确匹配场景。用户搜零件型号FRU-0710X这个型号在维修手册里可能出现的位置很隐蔽关键词精确命中才是关键。纯向量检索会召回一堆风扇转速异常更换风扇注意事项等图文并茂的段落但偏偏没有FRU编号——因为embedding模型把文本压在语义空间后对字面token的精确匹配完全不敏感。而BM25这类稀疏检索天然对单词命中非常敏感。1.3 生产环境的检索不是跑通一个demo大厂里的RAG系统面对的不是精心挑选的几十条测试问题而是真实用户随口说的一句话、一个残缺的型号、一个拼错的单词、一条夹带口语的复杂提问。检索系统要在这种噪声下仍然稳定工作。Deme阶段你可能感受不到纯向量的痛点几百条chunk一条query进来top-5就算全不相关肉眼也能看出来手动调一下query继续测就行。生产环境不一样用户不会帮你debug错误答案一旦被大模型包装成通顺的自然语言用户只会觉得系统不行。我后来跟几个做搜索和知识库的朋友交流大家的结论是一致的大厂生产环境的RAG基本没有人敢只依赖单路向量召回。通用做法是混合检索——至少两条召回路线再加一层重排序。具体怎么搭、怎么调、踩过哪些坑后面逐个讲。2. 拆解纯向量检索的4个致命伤2.1 精确信息完全抓不住型号、编号、人名、条款先说最要命的硬伤。纯向量检索对精确文本匹配几乎无感而业务知识库里偏偏充斥着必须精确命中的信息设备型号、合同编号、人员工号、产品SKU、法律条文序号。我做过一个供应链场景的知识库里面存的是供应商资质文档。用户经常直接搜供应商编号 SUP-88923这种查询。纯向量检索会召回到供应商资质审核标准供应商年度评估流程这类内容因为它们在语义上跟供应商紧密相关但不会优先召回到那个明确写了SUP-88923的文档。embedding模型把SUP-88923当成了一个普通token在语义空间里它可能落在供应商附近却无法保证精确匹配。BM25则完全不同。它基于词频和逆文档频率至少能保证包含SUP-88923这个词的文档被高分召回哪怕这条文档里其它部分和用户问的都对不上。混合检索的核心价值之一就是用BM25的路由来兜住这类精确匹配需求。2.2 长文档被向量平均化关键信息被稀释embedding模型要把不定长文本压缩成一个固定维度的向量。一段500字的技术描述和一段50字的简讯最终都变成一个768维的向量。在这个过程中长文本里的信息密度会被平均化——关键词分布在整个段落里每个token的贡献被摊薄。实际表现就是当你把chunk切得比较大比如1500个token纯向量检索经常把关键实体给丢了。举个具体数字我在某产品手册项目里做过对比统一用同一款embedding模型1500 token整段切分时recall5只有52%改成600 token 150 token重叠并保留标题层级后recall5提升到71%。差距相当明显。BM25没有这个问题。它天然关注词频一个词在文档里出现次数多分数就高不会被文档长度稀释。所以混合检索里BM25能很大程度补上向量检索在长文本、高词频、强关键词场景下的短板。2.3 伪相似陷阱看着像实际意思完全相反Embedding模型的另一个老问题是语义敏感度不够。它能把相关但不相同的文本拉近但经常抓不住否定、逻辑对比、数值边界这些关键语义差异。我自己踩过最深的坑用户问哪些渠道不支持退款知识库里有一句话以下渠道支持退款。从向量相似度看这两句话有大量重叠词支持退款和不支持退款里面的退款都出现了embedding模型认为它们距离很近。但语义上两者恰恰是反的。如果大模型拿这句话当上下文就会给出完全错误的答案。这类情况在条款类、规则类知识库中尤其常见。合同有效期两年和合同有效期已超过两年是两种完全相反的状态但向量距离可能只有0.05的差别。Rerank模型交叉编码器能够在一定程度上纠正这类误判但前提是候选集里要有正确的文档。如果纯向量召回在top-20里压根没把正确答案放进来rerank也无能为力。2.4 增量更新与一致性向量索引不是想改就能改纯向量检索用的多是HNSW这类近似近邻索引。新增向量时HNSW会在图结构里为新节点寻找邻居并插入连接这个过程比想象中要重删除和更新就更麻烦了需要找到节点并断开连接处理不好还会留下幽灵引用。倒排索引BM25底层的结构则完全不同。新增一篇文档本质上就是往倒排列表里插入若干项秒级生效更新和删除也很直接。大厂知识库每天都在变化新文档上线、旧文档下线、政策修订、产品参数更新。纯向量索引在处理这种高频增量时容易造成旧数据删不干净、新数据迟迟不生效。我遇到过的真实案例某团队归档了一批已过期的合同协议向量数据已经从索引里删掉了但线上检索依然偶尔召回其中一条用户在问答结果里看到一份已经失效的合同。排查之后定位到问题出在HNSW图的连接残留——删除向量时没有清理所有相关邻接关系。这也是一个很现实的工程教训。3. 生产环境我是怎么做的混合检索 Rerank3.1 完整链路并发召回 RRF融合 交叉编码器精排我自己在生产环境里搭的检索链路核心是三条路并行召回再融合再精排用户query进来先做一层轻量改写。拼写纠错、同义词扩展、口语转书面语。例如手机充电口松了会被改写成手机Type-C接口松动。这一步同时服务BM25和向量检索能明显提升两条路的命中率。并行走两路召回。路A是BM25全文检索专注精确匹配和关键词命中路B是向量语义检索专注找说得不像但意思相近的内容。如果领域有强规则还可以加路C做词典/正则召回。两路结果用RRFReciprocal Rank Fusion融合而不是简单加分数。融合后的top-50到top-100条交给一个cross-encoder模型做精排取top-5。最后把重排后的文本块拼进上下文送LLM生成。为什么坚持走这条链路因为我对比过几种方案纯向量、纯BM25、BM25向量直接加权、BM25向量RRF、再加Rerank。在这组实验里每往上加一层离线评测的答案正确率都在涨但时延也在涨。最后我权衡下来采用双路召回 RRF 100条候选精排这个组合效果和成本最平衡。3.2 RRF融合为什么比直接加权更稳两路召回的结果怎么合并最直白的做法是把BM25分数和向量余弦相似度各自做归一化然后按权重相加。我一开始也是这么干的结果发现根本调不稳向量分数分布和BM25分数分布完全不是一个量级今天压到同一尺度明天换一批文档又漂移了。RRF的思路完全不同。它不看分数绝对值只看排名。公式很简单score(d) Σ 1 / (k rank_i(d))其中rank_i(d)是文档d在第i路召回里的排名。k是平滑参数通常取60。举个例子。某文档A在BM25路排第2、在向量路排第5文档B在BM25路排第8、在向量路排第1。取k60A1/(602) 1/(605) 1/62 1/65 ≈ 0.0315B1/(608) 1/(601) 1/68 1/61 ≈ 0.0311两条文档融合后分数非常接近A略高。也就是说即使某一路召回里某个文档排名不高只要另一路排名靠前RRF依然能把它捞回来。这个特性在实际场景里很有用因为BM25和向量检索的路数完全不同一个文档经常只在其中一路上表现好。直接加分会因为量纲问题把另一路的贡献吞掉RRF则天然抗住了这种问题。k值我建议直接用60这是我测试下来最稳的默认值。k越小融合结果越偏向单路高排名的文档k越大两路的排名影响越趋于平均。如果发现某一路召回质量明显更差可以稍微调大那一路的排名权重但我不建议一开始就做这种精细调整先跑通默认参数再慢慢调。3.3 Rerank为什么不可或缺混合召回只能保证候选集里大概率有正确答案但排序未必合理。两路召回的结果跑到top-100真正需要的那个文档可能排在第37位。如果直接把top-5丢给LLM等于又把它晾在外面。所以最后一层必须用交叉编码器cross-encoder精排。双塔模型bi-encoder在召回阶段把query和文档各自编码成向量算相似度很快适合海量候选。但cross-encoder是直接把这个query和文档拼接成一个长序列一起送进模型交互计算得到的是更精细的相关性分数。时延高得多但精度也高得多。我的实操经验是精排候选集控制在50-100条。这个规模下用常见的开源中文reranker模型跑一遍单批大约200-400毫秒完全可接受。如果候选集扩大到500条时延会起飞到1.5秒以上对线上问答场景很不友好。系统上线之后我通常会把两路召回的top-100作为固定上限既给精排足够的候选空间又不会拖垮整体时延。4. 工程落地中的参数与避坑4.1 切分策略直接影响检索效果并不是切得越小越好做RAG的人总在纠结chunk大小。我的经验是没有一个万能值但可以从一个安全起点开始。我常用的安全起点是主文本chunk为600-800 token重叠150-200 token同时按文档原有的标题层级切分并且在每个chunk前面带上父标题作为上下文前缀。这样做的好处是每个chunk既不会因为过长导致embedding被平均化也不会因为过短而失去结构性上下文。切分太短的问题在于语义断裂。我在一个项目里把产品手册按句子切分单个chunk只剩30-50 token结果用户问保修期内运费谁承担包含这个答案的chunk是单独的一句话运费由消费者承担没有上文说明保修期内导致大模型完全不知道这条规则适用的条件。后来改成按二级标题合并段落chunk提到200-400 token问题才消失。实际操作中我建议用离线评测先测chunk切分。拿100条带标准答案的query跑一轮命中率把切分长度从300到1500都试一遍画一条曲线出来。多数项目的最优点在500-900之间但具体选哪里要以自己的数据和评测结果为准。4.2 向量索引HNSW参数先保召回再压时延如果你用向量检索大概率会碰到HNSW的三个参数M、ef_construction和ef_search。M每个节点的最大邻居数。M越大图越密召回精度越高内存占用越大。推荐16-32默认16起步。ef_construction建索引时动态候选队列长度。越大索引质量越高但构建时间越长。推荐200-400。ef_search查询时动态候选队列长度。越大召回越准但查询越慢。推荐50-100。我常用的做法是先用ef_search100做离线评测确认recall5达标后再逐步下调到80、60、50观察时延和召回率的变化。大部分落地项目最后会停在ef_search 50-80这个区间。不要一上来就为了追求快把ef_search设成20那样精确率掉得厉害反而浪费精排算力去补救。4.3 Embedding模型怎么选先跑分再换模型选embedding模型的时候不要只看公开榜单。公开数据集上的分数只能证明一个模型的通用能力和你业务里的文档风格、问题类型不一定匹配。我见过一个医疗知识库项目公开榜单上表现很好的通用模型在自己的病历语料上recall5还不如一个专门在医学语料上训练过的小模型。所以我的流程固定是这样的从线上日志里抽100-200条真实用户query人工标注正确答案对应的文档id。用当前候选的embedding模型离线跑一遍召回计算recall5和MRR。用评测结果做决策而不是凭感觉。如果业务领域非常垂直比如法律、医疗、金融可能还需要微调embedding模型。但这个成本较高我一般先试试通用模型如果recall5低于50%再考虑训练。4.4 缓存设计高频query别每次都重跑完整链路生产环境的RAG尤其对内的知识库问答用户提问有很大重复性。报销流程是什么年假怎么算这类问题每天可能被问几十次。每次都跑一遍完整链路改写召回融合精排LLM既浪费算力又拉高时延。我的做法是把query做一层归一化之后缓存对query做分词、去停用词、统一大小写和全半角然后计算哈希同时用向量相似度做一个近似query匹配相似度超过阈值直接命中缓存。命中时直接返回历史答案TTL设24小时。实测效果热门query缓存命中率能到30-50%整体问答时延从2秒左右降到300毫秒。不过要注意失效机制——知识库里的文档更新之后必须让相关query的缓存失效否则用户会一直看到旧答案。这一点特别容易被忽略。5. 常见问题与排查实录5.1 为什么我的混合检索效果反而不如纯向量这是我自己被问过最多的问题。加了BM25加了RRF加了Rerank离线一测效果还倒退了。出现这种情况通常有四个原因两路分数没归一化就直接相加。BM25分数和向量相似度不在一个量级直接加权等于在污染结果。解决方案是改用RRF。Rerank模型和领域不匹配。通用领域训出来的reranker到食品配方或金融条款这类垂直领域可能会把正确答案的分数压得很低。建议先用小测试集跑一遍看看top-1是不是正确答案。Chunk切分不合理。BM25对chunk粒度非常敏感chunk切太大一个chunk里塞了太多主题BM25会给你一堆提关键词正好对上、但整体内容答非所问的片段。Query改写过度。改写后的query跟原始query意思产生偏移反而把正确结果带丢了。我遇到过一次用户说屏幕闪屏改写系统给改成显示器闪烁故障排查向量检索把内容拉偏了。后来给改写环节加了仅做同义扩展、不改变核心实体的约束。排查的正确姿势是把检索链路的每个节点都可视化——先看BM25单独召回的top-10再看向量单独召回的top-10再看融合后的top-10最后看精排后的top-5。哪一步不对劲问题范围就被迅速缩小了。5.2 线上排序经常偏移怎么办排序偏移的原因很多。我整理了几个最常见的现象和排查思路做成了一张速查表异常现象可能原因排查方向向量分数普遍偏高/偏低两路分数没归一化就直接相加改RRF或先做min-max归一化Rerank后所有doc分数都很接近候选集太小或rerank模型领域不匹配扩大候选集换领域更匹配的rerankerBM25召回的片段重复度极高同一文档重复命中多个chunk按标题去重同一文档下只保留最高分chunk新更新的文档永远排不上去BM25和向量索引的增量同步有延迟检查更新管道看是否有文档状态字段没刷新我踩过最典型的一次某天线上突然大量query命中错文档查了半天最后定位到是embedding模型被人误换成了新版本新模型和老文档的向量空间不兼容导致相似度分数整体漂移。从那以后我把embedding模型和rerank模型的版本都固定在了配置中心升级必须走完整的离线评测流程。5.3 时延超预算怎么压RAG系统的时延大头通常是三块召回、精排、LLM生成。LLM生成没法省太多但检索链路可以优化。我的优化顺序是先压召回再压精排。召回方面BM25和向量检索可以并行跑整体耗时取两者最大值而不是相加。向量检索的ef_search从100降到50单次查询耗时能减少一半。实测在1000万级向量规模下HNSW M16、ef_search50单次向量检索p95约15毫秒完全可接受。精排方面候选集从200降到50时延几乎线性下降。如果单条query需要精排的文档实在太多还可以把精排改成并发处理开4-8个线程100条候选的批处理时延能压到300毫秒以内。整体调下来问答链路不含LLM可以做到200-400毫秒加上LLM生成也基本能控制在2秒内。对大多数知识库场景来说这个时延能接受。5.4 关于评测不要只看一两次手感最后说一个我坚持了很久的习惯每个检索项目上线前我都会建一个坏case池。每次评测发现的错误答案、错误召回、或者用户投诉都往池子里塞。隔两周就回归一次看哪些坏case被修复了哪些还在哪些是新出现的。这个习惯救了我好几次。有一次性模型升级离线评测指标全线提升我以为稳了结果一跑坏case池发现5个历史问题全回来了。后来查清楚是模型对特定类型query的行为发生偏移。如果只盯着平均指标这种回归问题根本不会被发现。混合检索本身不是多新鲜的技术BM25、向量召回、交叉编码器精排这些组件单拎出来都是成熟方案。真正拉开差距的地方在于你清理过多少坏case你对自家数据的脾气了解多少你的参数是根据评测曲线调出来的还是拍脑袋定的。这些细节才是生产RAG和demo RAG的本质区别。