1. 纯向量检索到底哪里不够用1.1 从一个真实翻车案例说起去年我帮一个团队调他们的知识库问答系统场景是内部技术文档检索。他们用的是当时最流行的方案文档切块、embedding 模型编码、灌进向量库、查询时做余弦相似度召回 Top-K。Demo 阶段效果惊艳问什么都能答上来。上线两周后投诉开始集中爆发。最典型的一个 case有人搜“接口超时重试策略”系统返回了一堆讲“网络协议分层”的文档片段因为这两者在向量空间里距离很近——都涉及网络、都涉及通信。但用户要的是具体的重试次数配置和退避算法不是 OSI 七层模型科普。另一个 case 更离谱搜一个具体的错误码“ERR_4032”向量检索返回的是“ERR_4030”“ERR_4035”相关的文档因为这几个错误码的 embedding 几乎重合模型根本分不清数字后缀的差异。这两个问题暴露了纯向量检索的两个致命伤语义相似不等于精确匹配以及向量模型对细粒度差异不敏感。大厂之所以不用纯向量方案不是因为它不好而是因为它在真实业务场景下的召回质量撑不住。1.2 向量检索的本质局限要理解为什么纯向量不够得先搞清楚向量检索到底在做什么。Embedding 模型把一段文本映射成一个高维空间中的点语义相近的文本在空间中距离近。检索时把查询也映射成点找最近的 K 个邻居。这个过程本质上是一次模糊的语义聚类。问题在于这个“模糊”是有代价的。向量模型在训练时做的是对比学习它学到的是“这两段话在讲同一件事”而不是“这两段话包含同一个关键实体”。所以当你的查询里有一个必须精确匹配的元素——产品型号、错误码、人名、特定术语——向量检索没法保证这个元素被正确匹配。我做过一个粗略的统计在一个包含 5 万条技术文档的知识库里纯向量检索对于包含专有名词的查询Top-5 召回率只有 62% 左右。而加上关键词过滤后这个数字能拉到 89%。这 27 个百分点的差距就是大厂不敢用纯向量的直接原因。1.3 大厂的真实检索链路长什么样大厂的 RAG 系统检索环节从来不是单点方案而是一条多路召回 融合排序的流水线。典型的结构是这样的第一路稀疏检索BM25/SPLADE。负责精确匹配关键词、短语、数字、代码片段。BM25 虽然老但它对 term 的精确匹配能力是向量替代不了的。第二路稠密检索向量。负责语义召回处理那些“换了说法但意思一样”的查询。第三路结构化过滤。按时间、来源、权限、文档类型做硬过滤把不该出现的结果直接排除。融合层RRF 或加权融合。把多路结果合并成一个统一排序。重排层Cross-Encoder 或 LLM 重排。对融合后的 Top-N 做精细打分输出最终 Top-K。这套链路的核心思想是不同检索方式有互补的盲区用多路召回互相兜底。纯向量只解决了其中一路的问题把它当成全部等于把其他几路的盲区全暴露了。2. 多路召回的核心技术拆解2.1 BM25 为什么还没被淘汰很多人觉得 BM25 是上个时代的产物有了向量就该扔掉。这个想法很危险。BM25 的核心价值在于它做的是词项级别的精确匹配而且它的打分函数有明确的数学解释。BM25 的打分公式大致是这样的对于查询中的每个词计算它在文档中的出现频率TF乘以一个逆文档频率IDF的权重再除以一个文档长度归一化因子。直观理解就是一个词在文档里出现得越多、在整个语料库里越稀有这个文档就越相关。这个机制让 BM25 在以下场景里碾压向量检索精确术语匹配查询“Kafka 消费者组 rebalance”BM25 能精确命中包含这些词的文档向量检索可能返回一堆讲“消息队列负载均衡”的泛泛之谈。数字和代码错误码、版本号、配置参数BM25 按 token 匹配不会把“v2.3.1”和“v2.3.2”混为一谈。低频专有名词某个内部系统的代号、某个特定 API 的名字这些词在 embedding 模型训练时可能根本没出现过向量表示质量很差但 BM25 照样能精确匹配。当然 BM25 也有明显短板它不理解同义词。用户搜“怎么调优”文档里写的是“性能优化”BM25 匹配不上。这就是为什么需要向量检索来补位。2.2 向量检索的正确打开方式向量检索不是没用而是要用对地方。我的经验是向量检索最适合处理以下几类查询自然语言问句用户用完整句子提问比如“为什么我的服务在高峰期响应变慢”这种查询的关键信息分散在语义里BM25 很难匹配。跨语言/跨表述检索查询和文档用不同的说法表达同一个意思向量能桥接这个 gap。模糊概念召回用户自己也不太清楚要搜什么只有一个模糊的方向向量能帮你找到语义相近的内容。但即便在这些场景里向量检索也需要配合其他手段。比如做查询改写把用户的自然语言问句先拆解成关键词 语义两部分关键词部分走 BM25语义部分走向量。再比如做混合检索把 BM25 分数和向量相似度分数做加权融合权重根据查询类型动态调整。2.3 RRF 融合让多路结果和平共处多路召回的结果怎么合并最简单的方法是加权求和但 BM25 分数和余弦相似度的量纲完全不同直接加权需要归一化而归一化又会引入新的偏差。大厂更常用的是RRFReciprocal Rank Fusion。RRF 的思路很巧妙不看具体分数只看排名。对于每个文档它在每一路召回中的排名是 r那么它的 RRF 分数就是 sum(1/(kr))其中 k 是一个平滑常数通常取 60。最后按 RRF 分数排序。这样做的好处是不需要处理不同检索方式的分数归一化问题而且对异常分数有天然的鲁棒性。一个文档如果在 BM25 里排第 1、在向量里排第 50它的 RRF 分数仍然不错如果一个文档只在某一路里排第 1、另一路里根本没出现它的分数也不会被过度放大。我实测下来RRF 在大多数场景下比加权求和更稳尤其是当两路召回的质量差异较大时。唯一需要注意的是 k 值的选择k 太小排名靠前的结果权重过大k 太大排名差异被抹平。60 是一个经过大量实验验证的经验值但具体场景还是得调。2.4 重排层最后一道质量闸门多路召回 融合之后通常会得到一个 Top-50 到 Top-100 的候选集。这个规模直接送给 LLM 做生成太浪费 token而且噪声太多。所以需要重排层来精筛。重排有两种主流方案Cross-Encoder 重排把查询和每个候选文档拼在一起送进一个专门的交叉编码器打分。优点是精度高因为它能建模查询和文档之间的细粒度交互缺点是慢因为每个候选都要单独跑一次模型。LLM 重排直接用大模型对候选文档做相关性打分或排序。优点是灵活可以通过 prompt 控制排序标准缺点是成本高而且 LLM 的打分有时不太稳定。我的建议是如果候选集在 50 以内用 Cross-Encoder如果对成本敏感且候选集较大可以先用轻量级模型粗筛到 20再用 LLM 精排。重排层的存在让前面多路召回的“宁滥勿缺”策略成为可能——召回阶段可以放宽标准反正后面有重排兜底。3. 实操搭建一套多路召回 RAG 检索链路3.1 环境准备与工具选型先说一下我用的技术栈这套组合在多个项目里验证过稳定性和效果都不错稀疏检索Elasticsearch 或 OpenSearch自带 BM25 打分支持丰富的过滤条件。稠密检索Milvus 或 Qdrant 做向量库embedding 模型用 BGE-M3 或类似的多语言模型。融合与重排Python 脚本做 RRF 融合Cross-Encoder 用 BGE-Reranker 系列。编排LangChain 或 LlamaIndex 做流程串联但核心检索逻辑建议自己写框架的抽象有时会挡住调优空间。安装依赖的部分就不展开了重点说配置。Elasticsearch 的索引 mapping 需要把文档的文本字段设成text类型用于 BM25同时把需要精确过滤的字段如文档 ID、来源、时间设成keyword或date类型。向量库那边需要确认 embedding 的维度与模型输出一致距离度量用余弦相似度。3.2 文档切块策略比检索算法更重要很多人把精力全花在检索算法上却忽略了切块策略。我踩过的最大的坑就是切块没切好后面怎么检索都是白搭。切块的核心矛盾是块太小语义不完整向量表示质量差块太大噪声多检索精度下降。我的经验值是 256 到 512 个 token 之间具体取决于文档类型。技术文档可以小一点因为信息密度高叙述性文档可以大一点因为需要更多上下文才能理解。更重要的是切块方式。不要简单地按固定长度切那样会把一个完整的段落从中间截断。我通常用递归切块先按段落切如果段落太长再按句子切如果句子还太长才按固定长度切。同时保留一定的重叠overlap通常 10% 到 20%防止关键信息刚好落在切块边界上。还有一个技巧给每个块加上元数据。比如它来自哪个文档、在文档中的位置、所属章节标题。这些元数据在检索时可以用来做过滤也可以拼在块内容前面一起做 embedding提升向量表示的质量。3.3 多路召回的实现细节先看稀疏检索这一路。Elasticsearch 的查询构造很直接{ query: { bool: { must: [ { match: { content: 接口超时重试策略 } } ], filter: [ { term: { doc_type: technical } }, { range: { updated_at: { gte: 2024-01-01 } } } ] } }, size: 50 }这里的关键是filter部分用结构化条件做硬过滤把不符合要求的文档直接排除。这一步能大幅减少后续融合和重排的计算量。向量检索这一路先把查询用同一个 embedding 模型编码然后在向量库里做 ANN 搜索query_vector embedding_model.encode(query) results vector_store.search( query_vectorquery_vector, top_k50, metric_typecosine )注意top_k要设得比最终需要的数量大因为后面还要融合和重排。我通常设 50 到 100。两路都拿到结果后做 RRF 融合def rrf_fusion(bm25_results, vector_results, k60): scores {} for rank, doc in enumerate(bm25_results): scores[doc.id] scores.get(doc.id, 0) 1 / (k rank 1) for rank, doc in enumerate(vector_results): scores[doc.id] scores.get(doc.id, 0) 1 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue)这段代码很简单但效果很实在。融合后的排序通常比任何单路都好。3.4 重排与最终输出融合后的 Top-50 送给 Cross-Encoder 重排pairs [(query, doc.content) for doc in fused_results[:50]] rerank_scores reranker.predict(pairs) reranked sorted(zip(fused_results, rerank_scores), keylambda x: x[1], reverseTrue) final_results [doc for doc, score in reranked[:5]]最终取 Top-5 送给 LLM 做生成。这里有个细节重排的输入不一定是原始块内容可以把块的元数据章节标题、来源也拼进去给重排模型更多判断依据。整个链路跑下来单次查询的延迟大概在 200 到 500 毫秒之间取决于候选集大小和重排模型的大小。对于大多数在线场景这个延迟是可以接受的。如果要求更高可以把重排模型换成更轻量的版本或者做异步重排。4. 常见问题与排查技巧实录4.1 召回率突然下降怎么排查这是最常见的问题。我的排查顺序是这样的先看查询本身。是不是查询里包含了新的专有名词而 embedding 模型没见过是不是查询太短语义信息不足再看召回结果。把 BM25 和向量两路的结果分别打印出来看是哪一路出了问题。如果 BM25 召回正常但向量召回跑偏可能是 embedding 模型不适合这个领域如果两路都差可能是切块或索引出了问题。检查过滤条件。有时候是 filter 写得太严把相关文档误杀了。可以先把 filter 去掉看召回是否恢复。检查索引更新。新文档有没有及时灌进索引向量库和 ES 的数据是否一致我遇到过一次诡异的问题召回率突然从 85% 掉到 40%。排查了半天发现是 ES 的索引在某个时间点被重建了但向量库没有同步重建导致两路召回的数据源不一致。这种问题只能靠监控和定期一致性检查来预防。4.2 融合后排序反而变差怎么办RRF 虽然稳但也不是万能的。如果融合后排序比单路还差通常是以下原因某一路召回质量太差。如果向量检索的 Top-50 里大部分是噪声融合后会把 BM25 的好结果也拉低。解决办法是给两路设置不同的权重或者对质量差的那一路做截断。k 值不合适。k 太小会让排名靠前的结果权重过大k 太大会让排名差异被抹平。可以试试 k20、k60、k100 几个值看哪个效果最好。两路召回的结果重叠度太低。如果 BM25 和向量召回的结果几乎不重叠说明两路捕捉的是完全不同的信号融合时需要考虑是否真的应该融合还是应该分别处理。4.3 重排模型太慢怎么优化Cross-Encoder 的延迟和候选集大小成正比。如果 Top-50 重排太慢可以先粗筛再精排。用一个小模型或简单的规则先把 Top-50 筛到 Top-20再用 Cross-Encoder 精排。用 ONNX 或 TensorRT 加速推理。把重排模型导出成优化后的格式推理速度能提升 2 到 3 倍。批处理。如果查询量不大可以把多个查询的重排请求攒在一起做批处理提高 GPU 利用率。换更小的模型。BGE-Reranker 有 base 和 large 两个版本base 版本速度更快效果差距在大多数场景下可以接受。4.4 常见问题速查表问题现象可能原因排查方向解决方案专有名词搜不到向量模型对低频词表示差检查 BM25 是否命中确保 BM25 路正常提高其权重同义词搜不到BM25 无法匹配语义检查向量路召回确保向量路正常或加查询改写召回结果重复切块重叠过大检查块间重叠比例降低 overlap 到 10% 左右排序靠前的结果不相关重排模型不适配领域人工评估重排分数换领域适配的重排模型或微调延迟过高候选集太大或模型太重分段计时缩小候选集换轻量模型新文档搜不到索引未更新检查索引时间戳建立实时索引更新机制4.5 几个我踩过的坑坑一embedding 模型和向量库的维度不匹配。换模型时忘了重建索引查询时报维度错误。这个坑很低级但很容易犯建议在配置里加一个维度校验。坑二BM25 的 analyzer 没配好。中文文档如果没配中文分词器BM25 会把整句话当成一个 token完全没法匹配。Elasticsearch 需要装 IK 分词器或者用内置的 smartcn。坑三RRF 的 k 值用了默认的 60 但没调。不同场景下最优 k 值差异很大建议把 k 作为超参数在验证集上调。坑四重排模型的输入长度超限。Cross-Encoder 通常有最大输入长度限制如 512 token如果查询加文档超过这个长度会被截断影响打分。解决办法是控制块大小或者在重排前对文档做摘要。坑五忽略了权限过滤。多路召回时如果忘了加权限过滤可能会把用户无权访问的文档召回出来。这个在内部系统里是严重问题一定要在每一路召回里都加上权限过滤条件。5. 不同规模团队的落地建议5.1 小团队从混合检索起步如果你是一个人或者两三个人的小团队没必要一上来就搭全套多路召回。我的建议是先从BM25 向量混合检索开始用最简单的加权融合先把基础效果跑通。等业务量上来了再逐步加重排和更复杂的融合策略。小团队的优势是决策快、迭代快。不要被大厂的复杂架构吓到他们的架构是为了支撑海量数据和超高并发你的场景可能根本不需要那么复杂。一个 ES 加一个向量库配上简单的 RRF就能解决 80% 的问题。5.2 中型团队重视评估和监控当你的 RAG 系统开始有真实用户、开始承载业务指标时评估和监控就变得比算法选型更重要。你需要一套自动化的评估流程定期跑一批标注好的查询-文档对计算召回率、MRR、NDCG 等指标。同时要有线上监控跟踪查询延迟、召回数量、用户点击率等信号。我见过太多团队把精力全花在调模型上却没有一套像样的评估体系结果改了半天不知道到底变好了还是变差了。评估集不需要很大几百条高质量标注就够用但一定要持续维护。5.3 大团队关注一致性和可扩展性大团队面临的挑战不是单点效果而是系统的一致性和可扩展性。多路召回意味着多个数据源如何保证它们之间的数据一致性如何在不影响线上服务的情况下更新索引如何水平扩展以支撑更高的并发这些问题没有标准答案但有几个原则可以参考索引更新走双写或影子索引确保切换时无感知召回服务做无状态化方便水平扩展融合和重排层做异步化避免阻塞主链路。这些工程上的考量往往比算法本身更能决定系统的成败。6. 一些个人体会这套多路召回的方案我在几个项目里反复迭代过最大的感受是检索质量的上限取决于数据质量而不是算法复杂度。我见过太多团队在算法上疯狂堆料却不愿意花时间清洗文档、优化切块、维护评估集。结果就是算法越堆越复杂效果提升却越来越小。另一个体会是没有银弹。BM25 有 BM25 的盲区向量有向量的盲区重排也有重排的盲区。多路召回的本质是用工程手段互相兜底而不是找到一个完美的单点方案。接受这个现实你就能把精力放在如何让各路召回更好地协作上而不是追求某个算法的极致。最后说一个实操小技巧在查询入口做意图分类。不同类型的查询适合的召回策略不同。事实型查询“XX 的配置参数是什么”应该偏重 BM25探索型查询“怎么优化系统性能”应该偏重向量。用一个轻量级分类器先判断查询类型再动态调整各路召回的权重效果比固定权重好很多。这个分类器不需要很准哪怕只有 70% 的准确率也能带来明显的提升。