1. 从"能查到"到"查得准":Agent 检索质量的分水岭在哪
做过 RAG 项目的人大概都有过这种体验:Demo 阶段效果惊艳,一旦接入真实业务数据,回答质量就断崖式下跌。用户问"上季度华东区的退货政策有没有调整",Agent 检索回来一堆华南区的旧文档、一份无关的产品说明书,甚至还有一段两年前的内部通知。表面上看它"查到了资料",但查到的全是噪音。
这就是当前 Agent 开发中最普遍也最致命的瓶颈——检索精度问题。大多数团队把精力花在模型选型、Prompt 调优、Agent 编排框架上,却忽略了最底层的一件事:你喂给模型的知识到底对不对。RAG 的核心价值从来不是"能检索",而是"检索得准"。一个检索命中率只有 40% 的知识库,后面接再强的模型也是白搭。
这篇内容面向的是正在做或准备做 RAG 知识库、AI Agent 检索增强的开发者。不管你是用 LangChain 搭原型,还是已经在生产环境跑 Agent 项目,我都会围绕"如何让 Agent 从能查资料变成查对资料"这条主线,把混合搜索、向量数据库选型、检索评估、Agentic RAG 这些关键环节拆开讲透。涉及到的技术点包括 BM25、向量检索、混合搜索策略、分块策略、重排序等,每一个我都会说清楚"为什么这么做"而不只是"怎么做"。
先抛一个我自己的真实数据:在一个约 12 万条文档片段的知识库里,纯向量检索的 Top-5 命中率大约是 62%,加入 BM25 混合搜索后提升到 81%,再加一层重排序模型后达到 89%。这三个数字之间的差距,就是"能查资料"和"查对资料"之间的距离。
2. 纯向量检索为什么不够用:一个被低估的精度陷阱
2.1 向量检索的语义漂移问题
向量检索的原理是把文本通过 Embedding 模型映射到高维空间,然后通过余弦相似度或内积来找到语义相近的片段。听起来很美好,但实际操作中有一个很容易被忽视的问题:语义相似不等于答案相关。
举个例子。用户问"RAG 知识库能存储图片吗",纯向量检索可能返回一段讲"向量数据库支持多模态数据存储"的文档,语义上确实很接近,但用户真正想知道的是"图片怎么存、存了之后怎么检索、检索出来怎么展示"。向量检索捕捉到了"存储"和"图片"的语义关联,但丢失了"操作方式"这个意图维度。
更麻烦的是,Embedding 模型对专有名词、产品型号、代码标识符的处理往往很差。比如你搜"langchain4j easy rag",向量检索可能给你返回一堆关于 LangChain 的通用介绍,因为"langchain4j"这个特定库的名称在 Embedding 空间里被稀释了。类似的情况还有版本号、错误码、API 名称——这些恰恰是技术文档检索中最常见的关键词。
2.2 分块策略对检索质量的隐性影响
很多人搭 RAG 的时候,分块策略就是随手设一个 chunk_size=500、overlap=50 就完事了。但分块方式直接决定了检索的上限。
我踩过的一个坑:早期做技术文档知识库时,按固定字数切分,结果一个完整的配置示例被切成了三段,用户搜"如何配置连接池"时,检索到的片段只有半截配置代码,缺少上下文,模型拿到这种残缺信息自然给不出完整答案。
后来调整为按语义边界分块——优先在段落结束、标题切换、代码块结束处切分,同时保留一定的重叠窗口。具体策略是:
- 对于叙述性文本,按段落切分,单块控制在 300-600 字
- 对于代码块和配置示例,整块保留不切分
- 对于表格,转成 Markdown 格式后整块保留
- 在每块前面拼接所属章节的标题路径,比如"第 3 章 > 3.2 连接池配置 > 超时设置"
最后这一条特别关键。加上标题路径之后,即使片段本身内容不完整,检索时也能通过标题信息匹配到正确的上下文。实测下来,仅这一项改动就让命中率提升了大约 8 个百分点。
2.3 向量数据库选型:别被 benchmark 带偏
向量数据库的选型是个老生常谈的话题,但我想从 RAG 检索质量的角度来聊,而不是单纯比性能。
| 数据库 | 适合场景 | 检索质量相关特性 | 注意事项 |
|---|---|---|---|
| Milvus | 大规模生产环境 | 支持稀疏向量+稠密向量混合检索 | 部署复杂度较高,小项目杀鸡用牛刀 |
| Qdrant | 中小规模、快速迭代 | 原生支持过滤+向量混合查询 | 过滤条件多时性能下降明显 |
| Weaviate | 需要内置模块化 | 自带 BM25 和混合搜索 | 资源占用偏高 |
| Chroma | 原型验证、本地开发 | 轻量、易上手 | 生产环境能力有限 |
| pgvector | 已有 PostgreSQL 技术栈 | 与关系数据无缝结合 | 索引类型选择影响召回率 |
选型的核心原则不是"哪个最强",而是"哪个最适合你的检索模式"。如果你的场景里关键词匹配很重要(比如技术文档、法律条文、产品手册),那一定要选支持原生混合搜索的数据库,或者至少能方便地接入外部 BM25 引擎。Qdrant 和 Weaviate 在这方面做得比较自然,Milvus 需要额外配置稀疏向量字段。
3. 混合搜索的工程落地:BM25 和向量怎么配合才不打架
3.1 混合搜索的两种融合策略
混合搜索说起来简单——把 BM25 的关键词匹配能力和向量检索的语义理解能力结合起来。但具体怎么结合,直接决定了效果。
目前主流有两种融合方式:
第一种:加权求和(Weighted Sum)
把 BM25 分数和向量相似度分数分别归一化到 0-1 区间,然后按权重相加。比如final_score = 0.3 * bm25_score + 0.7 * vector_score。这种方式实现简单,但权重的设定很依赖经验,而且不同查询类型的最优权重可能完全不同。
第二种:倒数排名融合(Reciprocal Rank Fusion, RRF)
不直接比较分数,而是看排名。公式是RRF_score = sum(1 / (k + rank_i)),其中 k 通常取 60。这种方式的优势是不需要归一化,对分数尺度不敏感,鲁棒性更好。
我两种都用过,最终在生产环境选了 RRF。原因是:BM25 的分数范围受文档长度和词频影响很大,归一化之后仍然不稳定;而 RRF 只看排名,省去了调权重的麻烦。实测在技术文档场景下,RRF 的 Top-5 命中率比加权求和高出约 5 个百分点。
3.2 BM25 参数调优:k1 和 b 怎么设
BM25 有两个核心参数:k1 控制词频饱和度,b 控制文档长度归一化。默认值通常是 k1=1.2、b=0.75,但这个默认值不一定适合你的场景。
- k1 调大:词频的影响更显著,适合短查询、关键词密集的场景
- k1 调小:降低词频影响,适合长文档、主题分散的场景
- b 调大:对长文档惩罚更重,适合文档长度差异大的知识库
- b 调小:减弱长度归一化,适合文档长度均匀的场景
我的经验是:技术文档知识库通常 b 设 0.6-0.7 比较合适,因为技术文档普遍偏长,b 太大会导致长文档被过度惩罚。k1 保持默认 1.2 一般够用,如果查询普遍很短(2-3 个词),可以调到 1.5。
3.3 一个可复现的混合搜索实现
下面是一个基于 Python 的混合搜索核心逻辑,用 rank_bm25 做关键词检索,用向量数据库做语义检索,最后用 RRF 融合:
from rank_bm25 import BM25Okapi import numpy as np class HybridSearcher: def __init__(self, documents, embedding_model, vector_store): self.documents = documents self.embedding_model = embedding_model self.vector_store = vector_store # 构建 BM25 索引 tokenized_docs = [doc.split() for doc in documents] self.bm25 = BM25Okapi(tokenized_docs, k1=1.2, b=0.65) def search(self, query, top_k=10, rrf_k=60): # BM25 检索 tokenized_query = query.split() bm25_scores = self.bm25.get_scores(tokenized_query) bm25_ranking = np.argsort(bm25_scores)[::-1][:top_k * 2] # 向量检索 query_embedding = self.embedding_model.encode(query) vector_results = self.vector_store.search(query_embedding, top_k=top_k * 2) vector_ranking = [r.id for r in vector_results] # RRF 融合 rrf_scores = {} for rank, doc_id in enumerate(bm25_ranking): rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (rrf_k + rank + 1) for rank, doc_id in enumerate(vector_ranking): rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (rrf_k + rank + 1) # 排序返回 sorted_results = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True) return sorted_results[:top_k]这段代码里有两个细节值得注意:一是 BM25 和向量检索各取top_k * 2个候选,给融合阶段留出更大的池子;二是 RRF 的 k 值取 60,这是原论文推荐的默认值,实践中 40-80 之间都可以,影响不大。
4. 检索之后还有一道关:重排序与上下文压缩
4.1 为什么需要重排序
混合搜索解决了"召回"的问题,但召回回来的结果未必都是精品。Top-10 里可能混着一些边缘相关的片段,如果全部塞给模型,不仅浪费 token,还会干扰模型的判断。
重排序(Reranking)的思路是:先用混合搜索快速召回一批候选(比如 20-30 个),再用一个更精细的模型对这批次候选做精排,选出最相关的 3-5 个送给大模型。
常用的重排序方案有两种:
- Cross-Encoder 模型:把 query 和 document 拼在一起输入模型,输出相关性分数。精度高但速度慢,适合候选集不大的场景。常见的有 bge-reranker、Cohere Rerank 等。
- LLM 重排序:直接让大模型对候选片段打分或排序。灵活但成本高,适合对精度要求极高的场景。
我的建议是:如果候选集在 30 个以内,用 Cross-Encoder 重排序性价比最高。超过 50 个候选的话,先做一轮粗筛再重排,否则延迟会很明显。
4.2 上下文压缩:给模型减负
重排序之后,还有一个容易被忽略的步骤——上下文压缩。即使选出了最相关的 5 个片段,每个片段里可能仍然有大量与问题无关的内容。
上下文压缩的做法是:对每个选中的片段,提取出与 query 最相关的句子或段落,去掉冗余部分。可以用一个小模型来做抽取式压缩,也可以用规则方法(比如按句子与 query 的相似度筛选)。
实测效果:在一个平均片段长度 500 字的知识库里,经过上下文压缩后,送给模型的平均 token 数从 2500 降到了 1200 左右,而回答质量基本没有下降。这意味着你可以用更低的成本处理更多的查询,或者在同样的 token 预算下塞入更多的有效信息。
4.3 检索评估:怎么知道你的检索到底行不行
说了这么多优化手段,但如果没有一套评估机制,你根本不知道改动到底有没有效果。
我常用的评估指标有三个:
- Hit Rate@K:前 K 个结果中包含正确答案的比例。这是最直观的指标。
- MRR(Mean Reciprocal Rank):正确答案排名的倒数的均值。衡量正确答案排得够不够靠前。
- NDCG@K:考虑排序质量的综合指标,适合有多个相关文档的场景。
构建评估集的方法:从真实用户查询中采样 100-200 条,人工标注每条查询对应的正确文档片段。这个过程比较费时,但一次构建可以反复使用。每次调整检索策略后跑一遍评估集,就能量化地看到提升或退步。
注意:评估集一定要覆盖不同类型的查询——短关键词查询、长自然语言查询、包含专有名词的查询、模糊意图查询。只在单一类型查询上评估,很容易过拟合。
5. Agentic RAG:让 Agent 自己决定怎么查
5.1 从被动检索到主动检索
传统的 RAG 流程是线性的:用户提问 → 检索 → 生成回答。但真实场景中,用户的问题往往需要多步检索才能回答。比如"对比一下 Qdrant 和 Milvus 在混合搜索方面的差异",这需要先检索 Qdrant 的混合搜索能力,再检索 Milvus 的混合搜索能力,最后做对比。
Agentic RAG 的核心思想是:让 Agent 自己决定什么时候检索、检索什么、检索几次。Agent 可以根据中间结果判断信息是否充足,如果不够就换个查询词再检索,或者从不同角度检索。
实现方式通常是把检索工具注册给 Agent,让 Agent 通过 Function Calling 或 ReAct 模式自主调用。关键设计点包括:
- 查询改写:Agent 在检索前先改写查询,把模糊问题拆成具体的检索语句
- 多轮检索:允许 Agent 根据中间结果发起新的检索
- 检索结果评估:Agent 判断检索结果是否足够回答问题,不够则继续检索
- 来源引用:Agent 在最终回答中标注信息来源,方便用户验证
5.2 查询改写:检索质量的第一道杠杆
在 Agentic RAG 中,查询改写是最容易被低估但收益最大的环节。用户输入的原始查询往往不适合直接检索——可能太口语化、可能包含多个意图、可能缺少关键上下文。
常见的查询改写策略:
- HyDE(Hypothetical Document Embeddings):先让模型生成一个假设性的答案文档,然后用这个文档的 Embedding 去检索。原理是"答案和答案更像",比"问题和答案"的语义距离更近。
- 多查询生成:把一个查询扩展成多个不同角度的子查询,分别检索后合并结果。适合复杂问题。
- 查询分解:把多意图查询拆成多个单意图查询,逐个检索。比如"RAG 怎么做分块和重排序"拆成"RAG 分块策略"和"RAG 重排序方法"。
我在项目中实测,加入查询改写后,复杂查询的 Hit Rate@5 从 71% 提升到了 84%。尤其是 HyDE 策略,对于"怎么做 XX"这类操作性问题效果特别明显。
5.3 Agent 检索的并发与延迟控制
Agentic RAG 虽然效果好,但多轮检索带来的延迟问题不容忽视。一次用户查询可能需要 3-5 次检索调用,每次检索 200-500ms,加上模型推理时间,总延迟可能超过 5 秒。
控制延迟的几个手段:
- 并行检索:多查询生成后,并行执行多个检索请求,而不是串行
- 缓存:对高频查询的检索结果做缓存,相同或相似查询直接返回
- 提前终止:Agent 判断当前结果已经足够回答问题时,立即停止检索
- 分级检索:先用轻量检索快速返回粗排结果,如果 Agent 判断不够再触发精排
并行检索是最直接有效的手段。把 3 个串行检索改成并行,延迟直接从 1.5 秒降到 0.5 秒左右。实现上用 asyncio 或者线程池都可以,关键是向量数据库的客户端要支持并发请求。
6. 那些文档里不会写的实战教训
6.1 知识库更新比搭建更麻烦
搭一个 RAG 知识库可能只需要几天,但维护它是个长期工程。文档会更新、会新增、会删除,如果知识库不能及时同步,Agent 就会拿着过时的信息回答用户。
我踩过的坑:早期没有做增量更新机制,每次文档变更都全量重建索引。12 万条片段全量重建一次要 40 多分钟,期间服务不可用。后来改成了增量更新——只对变更的文档重新分块、重新向量化、更新索引,单次更新控制在 2 分钟以内。
增量更新的关键是要有一个可靠的文档变更追踪机制。简单做法是用文件的修改时间戳做比对,复杂一点可以用内容哈希。另外要注意:删除文档时不仅要删向量索引,还要删 BM25 索引,否则会出现"检索到了但内容已不存在"的幽灵结果。
6.2 检索日志是最好的优化依据
很多人优化检索靠直觉——觉得应该加个重排序、觉得应该调一下分块大小。但直觉往往不准。
我的做法是:记录每一次检索的完整日志,包括原始查询、改写后的查询、召回结果、最终送给模型的片段、用户的反馈(点赞/点踩/追问)。积累一两周之后,分析那些点踩或追问的 case,看看问题出在哪个环节。
有一次分析日志发现,大量追问集中在"具体步骤"类问题上。进一步排查发现,操作步骤类的文档片段在分块时经常被切断,导致检索到的片段缺少关键步骤。针对性地调整了分块策略后,这类追问减少了 60% 以上。
6.3 不要忽视 Embedding 模型的选择
Embedding 模型对检索质量的影响可能比向量数据库更大。同一个知识库,换一个 Embedding 模型,命中率可能差 10-15 个百分点。
选择 Embedding 模型时需要考虑:
- 语言支持:中文场景一定要选中文优化过的模型,直接用英文模型效果会打折扣
- 维度:维度越高表达能力越强,但存储和检索成本也越高。768 维和 1024 维在实际效果上差距不大,但 1024 维的存储成本高 33%
- 领域适配:通用模型在专业领域(医疗、法律、金融)的表现可能不如领域微调过的模型
- 最大输入长度:要确保模型的最大输入长度能覆盖你的分块大小
目前中文场景下,bge 系列和 m3e 系列是比较稳妥的选择。如果预算允许,可以用自己的领域数据做微调,效果提升很明显。
6.4 混合搜索不是万能药
虽然我前面花了很多篇幅讲混合搜索的好处,但也要说清楚它的局限。混合搜索在以下场景可能反而拖后腿:
- 纯语义查询:用户问"怎么做才能提高检索效果",这种没有明确关键词的查询,BM25 的贡献很有限
- 同义词密集的场景:如果用户用的词和文档里的词完全对不上,BM25 匹配不到,只能靠向量检索
- 多语言混合的知识库:BM25 对跨语言匹配基本无能为力
所以混合搜索的权重和策略需要根据你的实际数据分布来调整。我的建议是先用评估集测一下纯向量和混合搜索的效果差异,如果混合搜索提升不明显,就不要为了"技术先进性"而强行上混合。
7. 从检索质量到 Agent 整体效果:几个容易被忽略的联动因素
7.1 Prompt 里的检索结果怎么组织
检索回来的片段怎么放进 Prompt,对最终回答质量影响很大。我见过很多项目就是把片段直接拼接起来,中间加个分隔符就完事了。但更好的做法是:
- 标注来源:每个片段前面标明来源文档和章节,方便模型引用
- 按相关性排序:最相关的片段放在最前面,因为模型对开头内容的注意力更强
- 控制总长度:不要超过模型上下文窗口的 60%,留出空间给系统指令和用户问题
- 去重:不同片段之间可能有大量重复内容,去重后能塞入更多有效信息
一个实际效果对比:同样的检索结果,经过结构化组织后,回答的准确率提升了约 12%,而且模型引用来源的准确率从 55% 提升到了 88%。
7.2 Agent 记忆与 RAG 的关系
Agent 记忆和 RAG 经常被混为一谈,但它们解决的是不同问题。RAG 解决的是"外部知识"的检索,Agent 记忆解决的是"对话历史"和"经验积累"的存储与调用。
在实际项目中,两者需要配合使用。比如用户在多轮对话中先问了"Qdrant 怎么配置",又问了"那它的混合搜索呢"——第二个问题里的"它"需要从对话记忆中解析出指的是 Qdrant,然后再去 RAG 知识库里检索混合搜索相关内容。
设计时要注意:对话记忆的检索和知识库的检索应该走不同的索引和不同的策略。对话记忆更看重时间近因和相关性,知识库检索更看重内容匹配度。混在一起做,两边效果都会打折扣。
7.3 安全边界:Agent 不该查什么
最后说一个容易被忽略的点——检索的安全边界。不是所有知识库里的内容都适合对所有用户开放。如果 Agent 检索到了权限外的内容并展示给用户,那就是严重的安全问题。
基本的做法是在检索阶段就做权限过滤——每个文档片段打上权限标签,检索时根据当前用户的权限过滤候选集。这个过滤要在向量检索和 BM25 检索两个环节都做,不能只在最后一步过滤,否则会浪费检索配额,也可能通过排名泄露敏感信息的存在。
另外,对于涉及个人隐私、商业机密的内容,建议在入库前就做脱敏处理,而不是依赖检索时的过滤。入库前处理是"默认安全",检索时过滤是"默认开放",前者的安全级别更高。
这套东西我在几个项目中反复迭代过,核心体会就是:RAG 的检索质量不是某一个环节决定的,而是分块、Embedding、索引、检索策略、重排序、上下文组织这一整条链路共同作用的结果。任何一个环节偷懒,最终都会体现在回答质量上。而优化的时候,一定要有评估集和日志做依据,否则就是盲人摸象。