1. 从一个被问烂了的问题说起
“为什么大厂 RAG 从不用纯向量检索?”
这个问题我在不同场合被问过不下十次。有刚入行的朋友,也有做了两三年搜索推荐的老手。问法大同小异,核心困惑都一样:向量数据库这几年这么火,Embedding 模型越来越强,按道理说,把文档切块、转向量、存库、检索,一条链路干干净净,为什么真正落到生产环境的 RAG 系统,检索层往往搞得特别复杂?
我自己的经历是,早期做知识库问答的时候,确实用过纯向量检索的方案。文档切完,调个 Embedding 接口,塞进向量库,用户提问也转向量,算余弦相似度,取 Top-K,拼进 Prompt,结束。Demo 阶段效果惊艳,随便问几个问题都能找到相关内容,当时觉得这东西简直是银弹。
然后真实用户来了。
有人问“XX 功能的报错码 4021 是什么意思”,向量检索返回了一堆讲“错误处理机制”的文档,语义确实相关,但就是没有 4021 这个具体条目。有人问“A 产品和 B 产品的区别”,检索回来的全是 A 产品的介绍,因为 B 产品在文档里只提了一嘴。还有人问一个特别具体的时间点,比如“去年第三季度更新的那个策略”,向量检索完全抓瞎,它根本不知道“去年第三季度”对应文档里的哪个时间戳。
这些问题不是调调参数就能解决的。它们暴露的是纯向量检索的结构性缺陷,不是工程优化能绕过去的。
所以这篇内容,我想把这件事掰开揉碎讲清楚。不是泛泛地说“向量检索有局限”,而是具体到:局限在哪、为什么会产生这些局限、大厂实际用什么方案补、每种补法的原理和代价是什么、你自己做系统的时候该怎么选。适合正在做 RAG 系统、被检索效果折磨、或者准备选型的技术同学参考。不管你是刚接触 RAG,还是已经踩过一些坑,应该都能从中找到对自己有用的部分。
2. 纯向量检索到底在做什么
2.1 向量检索的本质:语义空间的近邻搜索
先把基础说清楚。向量检索的核心逻辑是:把文本通过 Embedding 模型映射到一个高维向量空间,语义相近的文本在这个空间里的距离就近。用户提问也映射成向量,然后在这个空间里找距离最近的 K 个文档向量。
这个过程用生活化的类比就是:你把所有文档撒在一个巨大的地图上,意思相近的文档会被撒在相近的位置。用户提问也是地图上的一个点,你以这个点为中心画个圈,圈里的文档就是检索结果。
这个思路的美妙之处在于语义泛化。用户问“怎么退款”,文档里写的是“申请退货流程”,字面完全不重叠,但向量空间里这两个表达距离很近,所以能召回。这是关键词检索做不到的,也是向量检索最大的价值。
但问题恰恰出在“语义相近”这四个字上。什么叫语义相近?Embedding 模型说了算。而 Embedding 模型对“相近”的理解,和真实业务场景需要的“相关”,往往不是一回事。
2.2 一个向量检索的典型实现
为了后面讨论具体,这里给一个最朴素的纯向量检索实现。用 Python 写,逻辑很直白:
import numpy as np from sentence_transformers import SentenceTransformer # 加载 Embedding 模型 model = SentenceTransformer('all-MiniLM-L6-v2') # 文档库:假设已经切好块 documents = [ "产品A支持七天无理由退货,需保持商品完好", "错误码4021表示账户余额不足,请充值后重试", "2023年Q3更新了会员积分规则,积分有效期改为12个月", # ... 更多文档 ] # 建索引:把所有文档转向量 doc_vectors = model.encode(documents, normalize_embeddings=True) def vector_search(query, top_k=3): query_vector = model.encode([query], normalize_embeddings=True)[0] # 余弦相似度 scores = np.dot(doc_vectors, query_vector) top_indices = np.argsort(scores)[::-1][:top_k] return [(documents[i], scores[i]) for i in top_indices] # 测试 results = vector_search("4021错误怎么解决") for doc, score in results: print(f"{score:.4f} | {doc}")跑一下你会发现,对于“4021错误怎么解决”这个问题,返回的第一条很可能是“产品A支持七天无理由退货”,因为“解决”“支持”这些词在语义空间里可能比较近,而“4021”这个关键标识符,Embedding 模型根本没当回事——它可能被 tokenize 成几个无意义的子词,在向量里几乎不占权重。
这就是纯向量检索的第一个大坑:对精确标识符、数字、代码、专有名词不敏感。
2.3 为什么 Embedding 模型会“忽略”关键信息
要理解这个问题,得稍微看一下 Embedding 模型是怎么训练的。主流 Embedding 模型(比如基于 BERT 的那些)通常用对比学习训练:给模型看大量“相似句对”和“不相似句对”,让相似句对的向量靠近,不相似的推远。
训练数据里,“4021”这种数字串出现的频率极低,而且它和“错误码”的关联在训练语料里可能只出现几次。模型学到的更多是“错误”“解决”“问题”这些高频词的语义关系,对于低频的、具体的标识符,它没有足够的信号去赋予它们区分性的向量表示。
更麻烦的是,Embedding 模型有维度瓶颈。一个 768 维的向量要编码一段文本的所有语义信息,必然要做取舍。高频的、泛化的语义会被保留,低频的、具体的细节会被压缩掉。这就像你用一张低分辨率图片去保存一幅画,整体轮廓在,但细节糊了。
所以纯向量检索在处理以下类型查询时,表现会明显下降:
| 查询类型 | 示例 | 纯向量检索的问题 |
|---|---|---|
| 精确标识符 | “错误码4021” | 数字被忽略,召回语义相关但非目标文档 |
| 专有名词 | “XX型号的接口定义” | 型号名在向量中权重低,容易混淆 |
| 否定查询 | “不支持的功能有哪些” | 向量空间对否定不敏感,“支持”和“不支持”可能很近 |
| 多条件组合 | “A产品在B场景下的C问题” | 多个条件在向量中被平均,丢失组合精确性 |
| 时间/版本限定 | “2023年Q3的规则” | 时间信息在向量中几乎不可区分 |
这张表里的每一行,都是我在实际项目中真实遇到过的。不是理论推演,是用户 query 日志里活生生的问题。
3. 大厂为什么“不敢”只用向量检索
3.1 召回率与精确率的根本矛盾
检索系统有两个核心指标:召回率和精确率。召回率是“该找的有没有找到”,精确率是“找到的有没有用”。纯向量检索在这两个指标上存在结构性矛盾。
提高召回率,通常要增大 Top-K,或者降低相似度阈值。但向量检索的相似度分数分布往往很“平”——第一名 0.85,第十名 0.82,差距很小。你很难找到一个自然的截断点。增大 K 值,精确率就掉;收紧阈值,召回率就掉。
这个矛盾在关键词检索里反而好处理,因为关键词匹配是布尔逻辑,匹配就是匹配,不匹配就是不匹配,边界清晰。向量检索的连续分数空间,让“相关”和“不相关”之间没有明确界限。
大厂面对的是海量用户和多样化的 query 分布,不可能用一个固定的 K 值和阈值打天下。他们需要的是可控的、可解释的、可调优的检索行为,而纯向量检索在这方面天然弱势。
3.2 业务对“可解释性”的硬需求
这一点很多技术同学容易忽略。在实验室里,你只需要关心最终答案对不对。但在生产环境,尤其是企业级应用,检索结果需要可解释。
什么叫可解释?用户问“为什么给我返回这个文档”,你得能说出理由。是关键词匹配了?是语义相似?是某个字段精确命中了?纯向量检索给出的理由是“向量相似度 0.83”,这个理由对业务方、对用户、对排查问题的工程师来说,几乎等于没说。
我经历过一次线上问题排查:用户反馈某个问题搜不到正确答案。如果是关键词检索,我可以直接看倒排索引,发现是分词问题还是同义词问题。但纯向量检索,我只能看到“这个 query 的向量和正确答案的向量距离是 0.79,和错误答案的距离是 0.81”,然后呢?我能调什么?换模型?重新训练?成本高且不确定。
大厂的系统往往要支撑多个业务线,每个业务线对检索行为有不同的预期。没有可解释性,就没法做精细化的业务调优。
3.3 数据规模带来的性能与成本压力
纯向量检索在数据量小的时候很优雅。但数据量上去之后,问题就来了。
向量检索通常用近似最近邻(ANN)算法来加速,比如 HNSW、IVF。这些算法用精度换速度,本身就会引入召回损失。数据量越大,为了保持可接受的延迟,往往要牺牲更多精度。
而且,向量索引的内存占用很可观。一个 768 维的 float32 向量是 3KB,一百万条文档就是 3GB,这还只是原始向量,没算索引结构的开销。大厂的知识库动辄千万级、亿级文档,纯向量方案的成本会非常夸张。
相比之下,倒排索引的压缩率高得多,而且关键词检索的延迟天然就低。大厂在成本敏感的场景下,不可能把所有检索压力都压在向量上。
3.4 一个真实的翻车场景
说一个我印象深刻的案例。某团队做一个内部技术文档问答系统,初期用纯向量检索,效果还行。后来文档量从几千涨到几万,用户开始反馈“搜不到”。
排查发现,很多 query 是“XX接口的返回值格式”“XX配置项默认值”这类。这些 query 的核心信息是“接口名”“配置项名”这些精确标识符,而向量检索返回的往往是“接口设计原则”“配置管理最佳实践”这类语义相关但完全不解决问题的文档。
更致命的是,当文档量增大后,语义相近的文档变多,向量空间的“分辨率”不够用了。原本几千条文档时,相似文档少,Top-K 里总能挤进正确答案。几万条文档时,相似文档一大堆,正确答案被淹没在语义相近但实际无关的结果里。
这个团队最后的解决方案是:向量检索 + 关键词检索双路召回,再用重排序模型融合。上线后准确率提升了将近一倍。这不是个例,几乎是大厂 RAG 系统的标配。
4. 大厂实际在用的混合检索架构
4.1 双路召回:向量 + 关键词
大厂 RAG 检索层最常见的架构是双路召回:一路向量检索负责语义泛化,一路关键词检索(通常是 BM25 或类似的稀疏检索)负责精确匹配。两路各自取 Top-K,然后合并、去重、重排序。
为什么是这两路?因为它们的能力互补:
- 向量检索擅长:语义改写、同义表达、模糊意图
- 关键词检索擅长:精确标识符、专有名词、数字、代码、布尔逻辑
用户 query 里既有“怎么退款”这种语义型,也有“错误码4021”这种精确型。双路召回能同时覆盖,比任何单路都稳。
实现上,关键词检索通常用 Elasticsearch 或 OpenSearch 的 BM25,向量检索用 Milvus、Qdrant、Weaviate 之类的向量库。两路并行查询,延迟增加不多,但召回质量提升明显。
4.2 融合策略:RRF 与加权分数
两路召回的结果怎么合并?常见的有两种策略。
RRF(Reciprocal Rank Fusion)是最常用的,因为它不需要两路分数可比。公式很简单:
RRF_score(d) = Σ 1 / (k + rank_i(d))其中 rank_i(d) 是文档 d 在第 i 路召回中的排名,k 是平滑常数,通常取 60。这个方法的妙处在于只依赖排名,不依赖原始分数,避免了向量相似度和 BM25 分数不可比的问题。
加权分数融合则是把两路分数归一化后加权求和。这个方法更灵活,但需要调权重,而且归一化方式对结果影响很大。我一般建议先用 RRF 跑通,有精力再尝试加权融合。
| 融合策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| RRF | 无需分数归一化,鲁棒 | 丢失分数信息 | 快速上线,两路分数不可比 |
| 加权分数 | 可精细调优 | 需归一化,权重难调 | 有标注数据,可做调参 |
| 学习排序 | 效果上限高 | 需要训练数据和工程投入 | 大厂有资源团队 |
4.3 重排序:把对的排到前面
双路召回解决了“找得到”的问题,但“排得准”还得靠重排序。重排序模型(Reranker)通常是 Cross-Encoder 结构,把 query 和文档拼在一起输入模型,输出相关性分数。
和 Embedding 模型(双塔结构)不同,Cross-Encoder 能让 query 和文档在模型内部充分交互,对精确匹配、否定、多条件组合的判别能力强得多。代价是计算量大,不能预先建索引,只能对召回结果做精排。
所以典型流程是:双路召回各取 Top-50 到 Top-100,合并去重后可能有两三百条,然后用 Reranker 精排,取 Top-5 到 Top-10 送给大模型生成答案。
这个架构的每一层都在解决特定问题:召回层保证不遗漏,融合层保证两路互补,重排序层保证精度。纯向量检索只相当于召回层的一路,而且是最容易出问题的那一路。
4.4 查询理解与改写:在检索之前做文章
大厂 RAG 还有一个纯向量检索方案不会涉及的环节:查询理解。用户输入的 query 往往不是检索友好的,需要先做处理。
常见的处理包括:
- 意图识别:判断 query 是事实型、对比型、操作型还是闲聊型,不同类型走不同检索策略
- 查询改写:把口语化 query 改写成更适合检索的形式,比如“那个退款咋弄”改成“退货流程”
- 关键词提取:从 query 中抽出关键标识符,用于关键词检索路
- 查询扩展:补充同义词、相关术语,提升召回
- 时间/版本解析:把“去年Q3”解析成具体时间范围,用于过滤
这些处理在纯向量检索方案里往往被忽略,因为大家觉得“向量检索能理解语义”。但实际是,Embedding 模型的理解能力有限,提前做查询理解能大幅提升后续检索的准确性。
5. 自己动手:搭一个混合检索 RAG
5.1 环境与工具选型
如果你要自己搭一个混合检索 RAG,工具选型上我建议:
- 向量检索:Qdrant 或 Milvus。Qdrant 轻量易用,Milvus 适合大规模。本地开发用 Chroma 也行。
- 关键词检索:Elasticsearch 或 OpenSearch。如果不想太重,可以用 rank_bm25 这个 Python 库做轻量 BM25。
- Embedding 模型:中文场景推荐 BGE 系列或 M3E,英文场景 all-MiniLM 系列够用。
- 重排序模型:BGE-Reranker 系列,中文效果好,部署简单。
- 框架:LangChain 或 LlamaIndex 可以快速搭原型,但生产环境建议自己控制检索逻辑。
下面给一个用 rank_bm25 + Qdrant + BGE-Reranker 的最小实现。这个方案不需要额外部署 ES,适合中小规模快速验证。
5.2 双路召回的实现
from rank_bm25 import BM25Okapi from qdrant_client import QdrantClient from sentence_transformers import SentenceTransformer, CrossEncoder import jieba # 中文分词 # 初始化 embedding_model = SentenceTransformer('BAAI/bge-base-zh-v1.5') reranker = CrossEncoder('BAAI/bge-reranker-base') qdrant = QdrantClient(":memory:") # 演示用内存模式 documents = [ "产品A支持七天无理由退货,需保持商品完好", "错误码4021表示账户余额不足,请充值后重试", "2023年Q3更新了会员积分规则,积分有效期改为12个月", "产品B不支持退货,仅支持换货", # ... 更多文档 ] # 关键词检索索引 tokenized_docs = [list(jieba.cut(doc)) for doc in documents] bm25 = BM25Okapi(tokenized_docs) def keyword_search(query, top_k=10): tokenized_query = list(jieba.cut(query)) scores = bm25.get_scores(tokenized_query) top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k] return [(documents[i], scores[i]) for i in top_indices if scores[i] > 0] # 向量检索索引(简化示意,实际用 Qdrant 的 collection) doc_vectors = embedding_model.encode(documents, normalize_embeddings=True) def vector_search(query, top_k=10): query_vector = embedding_model.encode([query], normalize_embeddings=True)[0] scores = doc_vectors @ query_vector top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k] return [(documents[i], float(scores[i])) for i in top_indices] # RRF 融合 def rrf_fusion(results_list, k=60): score_map = {} for results in results_list: for rank, (doc, _) in enumerate(results): score_map[doc] = score_map.get(doc, 0) + 1 / (k + rank + 1) return sorted(score_map.items(), key=lambda x: x[1], reverse=True) def hybrid_search(query, top_k=5): vec_results = vector_search(query, top_k=20) kw_results = keyword_search(query, top_k=20) fused = rrf_fusion([vec_results, kw_results]) # 重排序 candidates = [doc for doc, _ in fused[:20]] pairs = [[query, doc] for doc in candidates] rerank_scores = reranker.predict(pairs) reranked = sorted(zip(candidates, rerank_scores), key=lambda x: x[1], reverse=True) return reranked[:top_k] # 测试 results = hybrid_search("4021错误怎么解决") for doc, score in results: print(f"{score:.4f} | {doc}")跑这个代码,你会发现“错误码4021”那条文档会被正确召回并排在前面。关键词路抓到了“4021”,向量路提供了语义兜底,重排序做了最终精排。
5.3 参数调优的实操经验
这套架构里,有几个参数对效果影响很大,我把自己调参的经验列一下:
BM25 的 k1 和 b:k1 控制词频饱和度,b 控制文档长度归一化。默认 k1=1.5、b=0.75 对大多数场景够用。如果文档长度差异大,可以适当调高 b。如果短查询多,可以调低 k1。
向量检索的 Top-K:召回阶段建议取大一点,20 到 50。因为后面有重排序兜底,召回阶段宁可多召回,不要漏。但也不能太大,否则重排序计算量爆炸。
RRF 的 k 值:默认 60 是经验值。k 越小,排名靠前的结果优势越大;k 越大,排名差异被平滑。如果两路召回质量差异大,可以给高质量那路更大的权重(通过调整 k 或直接加权)。
重排序的候选数:一般取 20 到 50。太少可能漏掉正确答案,太多延迟高。我实测下来,20 到 30 是性价比比较高的区间。
注意:重排序模型的计算量和候选数成正比,如果延迟敏感,优先控制候选数,而不是换更小的模型。小模型的效果下降往往比重排序候选数减少更明显。
5.4 一个容易忽略的细节:文档切块策略
检索效果不好,很多时候问题不在检索算法,而在文档切块。切块太大,一个块里混了多个主题,向量表示被平均,检索精度下降。切块太小,上下文丢失,大模型生成答案时信息不足。
我的经验是:
- 按语义边界切,不要按固定字数硬切。段落、章节、列表项是自然的切分点。
- 块大小控制在 200 到 500 字之间,根据文档类型调整。技术文档可以小一点,叙述性文档可以大一点。
- 块之间保留一定重叠(比如 10% 到 20%),避免边界信息丢失。
- 给每个块加上元数据:来源文档、章节标题、时间戳。这些元数据在检索时可以做过滤,在生成时可以做引用。
元数据过滤是混合检索的另一个优势。纯向量检索很难做精确过滤,但混合检索可以先用元数据缩小范围,再做向量和关键词检索。比如用户问“2023年的退货政策”,可以先用时间元数据过滤出 2023 年的文档,再检索。
6. 常见问题与排查技巧实录
6.1 检索效果不好的排查顺序
遇到检索效果问题,我一般按这个顺序排查:
- 看 query 日志:用户到底在问什么?是语义型还是精确型?有没有大量特殊标识符?
- 看召回结果:正确答案在不在召回列表里?如果不在,是召回问题;如果在但排名靠后,是排序问题。
- 分路排查:向量路和关键词路各自的表现如何?哪一路漏了正确答案?
- 看切块:正确答案所在的块,内容是否完整?有没有被切碎?
- 看 Embedding 模型:模型是否适合当前领域?中文场景用英文模型效果会差很多。
- 看重排序:重排序模型是否适合当前语言和领域?
这个顺序的核心逻辑是:从数据到算法,从粗到细。很多问题在第一步和第二步就能定位,不需要一上来就调模型。
6.2 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 精确标识符搜不到 | 向量模型忽略低频词 | 看关键词路是否召回 | 加强关键词路权重,或加元数据过滤 |
| 语义相近但答案不对 | 向量空间分辨率不足 | 看 Top-K 中正确答案排名 | 加重排序,或换更强 Embedding 模型 |
| 多条件查询效果差 | 条件在向量中被平均 | 拆分 query 分别检索 | 查询理解 + 多路召回 |
| 否定查询失效 | 向量对否定不敏感 | 测试“支持”和“不支持”的向量距离 | 关键词路兜底,或查询改写 |
| 新文档搜不到 | 索引未更新 | 检查索引更新时间 | 建立增量索引机制 |
| 延迟太高 | 重排序候选太多 | 看各阶段耗时 | 减少候选数,或换更快的重排序模型 |
| 结果重复 | 切块重叠过多 | 看召回结果相似度 | 调整切块重叠比例,或去重 |
6.3 几个踩过的坑
坑一:过度依赖 Embedding 模型的语义能力。我早期总觉得“向量能理解语义”,所以把什么 query 都往向量路扔。后来发现,对于“XX是什么”“XX怎么用”这类 query,向量路确实好;但对于“XX错误码”“XX配置项”这类,关键词路才是主力。不要指望一个模型解决所有问题。
坑二:忽略查询改写。用户 query 往往很口语化,直接拿去检索效果差。加一层轻量的查询改写(比如用大模型把 query 改写成检索友好的形式),效果提升很明显。这个改写的成本不高,但收益很大。
坑三:重排序模型选太大。重排序模型的效果和参数量正相关,但延迟也正相关。我试过用 large 级别的重排序模型,效果确实好一点,但延迟翻倍。后来换成 base 级别,候选数从 50 降到 30,整体延迟降了一半,效果只掉了不到 2%。性价比高得多。
坑四:不做 A/B 测试。检索系统调优很容易陷入“我觉得这样更好”的陷阱。一定要有评测集,一定要做 A/B 测试。我见过太多“拍脑袋调参”导致线上效果反而下降的案例。
6.4 评测集怎么建
没有评测集的检索调优就是盲人摸象。评测集不需要很大,但要有代表性。我的做法是:
- 从真实 query 日志里采样,覆盖不同类型(语义型、精确型、多条件型)
- 每个 query 标注正确答案(可以人工标,也可以用大模型辅助标再人工校验)
- 至少 100 到 200 条,少了不具统计意义
- 定期更新,因为用户 query 分布会变化
评测指标用 Recall@K 和 MRR(Mean Reciprocal Rank)。Recall@K 看正确答案有没有被召回,MRR 看正确答案排得靠不靠前。这两个指标结合,能比较全面地反映检索质量。
7. 不同规模团队的选型建议
7.1 小团队:先跑通,再优化
如果你是一个人或者小团队,资源有限,我的建议是:
- 先用纯向量检索跑通 MVP,验证需求
- 遇到精确匹配问题,加一路轻量 BM25(rank_bm25 库就够)
- 用 RRF 融合,不要纠结权重调优
- 重排序可以用 API 服务,不用自己部署
- 评测集先建 50 条,够用就行
这个阶段的核心是快速验证,不要过度工程化。很多问题在用户量上来之前不会暴露,提前优化是浪费。
7.2 中型团队:混合检索 + 重排序
有一定用户量之后,检索质量开始影响核心指标,这时候需要:
- 上完整的双路召回(ES + 向量库)
- 加查询理解层(意图识别、查询改写)
- 部署自己的重排序模型
- 建立评测集和 A/B 测试流程
- 监控检索各阶段的延迟和召回率
这个阶段的关键是可观测性。你要能知道每一路召回的表现,能定位问题出在哪个环节。
7.3 大团队:多路召回 + 学习排序
大厂的做法通常是:
- 多路召回:向量、关键词、知识图谱、协同过滤等
- 学习排序(LTR):用标注数据训练排序模型,融合多路特征
- 在线学习:根据用户反馈实时调整
- 个性化:根据用户历史和行为调整检索策略
这个阶段的复杂度很高,不是一般团队需要追求的。但理解这个演进路径,有助于你在自己的阶段做正确的取舍。
8. 一些底层思考
8.1 检索的本质是“匹配”,不是“理解”
很多人把向量检索的语义能力神化了,觉得它能“理解” query。但严格来说,Embedding 模型做的是表示学习,它把文本映射到一个空间,让语义相近的文本距离近。它没有真正的“理解”,只是在做模式匹配。
真正的理解需要推理、需要世界知识、需要上下文。这些是生成模型(大模型)擅长的,不是检索模型擅长的。检索层的职责是高效地缩小候选范围,把可能相关的文档找出来,而不是做最终判断。
想清楚这个分工,你就不会指望纯向量检索解决所有问题。检索层要的是高召回、可解释、可控,精排和生成交给后面的环节。
8.2 没有银弹,只有权衡
纯向量检索之所以流行,是因为它简单、优雅、Demo 效果好。但生产环境是复杂的,用户 query 是多样的,数据是脏的,需求是变化的。任何单一方案都有其适用边界。
大厂不用纯向量检索,不是因为向量检索不好,而是因为大厂的场景太复杂,单一方案覆盖不了。他们用混合检索,是用工程复杂度换效果稳定性。
对你自己来说,选型的关键是匹配当前阶段的需求和资源。小团队用纯向量检索跑 MVP 完全没问题,但要有心理准备,用户量上来后需要演进。大团队直接上混合检索,是因为他们踩过坑,知道纯向量的边界在哪。
8.3 检索质量的上限在数据,不在算法
最后说一个容易被忽略的点:检索效果的上限,往往取决于数据质量,而不是算法。文档切块是否合理、元数据是否完整、标注是否准确、索引是否及时更新,这些“脏活累活”对效果的影响,往往比换一个更强的 Embedding 模型更大。
我见过太多团队花大量时间调模型、调参数,但文档切块一塌糊涂,元数据缺失,索引更新滞后。这种情况下,再好的检索算法也救不回来。
所以如果你正在做 RAG 系统,我的建议是:先把数据层做扎实,再考虑算法优化。数据层的投入,回报周期更长,但上限更高。
这个内容后续还可以这样扩展:具体讲查询理解层的实现、重排序模型的微调、多路召回的工程架构、以及如何用大模型做检索结果的自动评测。每一个方向都值得单独展开,有机会再聊。