Agent 工程化(二):RAG 不是搜索——让 Agent 真正"懂"你的数据
检索是找到文档,RAG 是让模型基于文档生成正确的回答——这两者之间的距离,就是一整套工程系统。
你让一个 Agent 去查公司的退款政策,它可能会给你两个结果:要么编一个听起来很合理的规则——“7 天无理由退款,需保持商品完好”——但它其实是从训练数据里某个电商 FAQ 学来的,跟你公司一点关系都没有;要么它确实找到了你们公司的退款政策,但那是 2023 年 3 月的旧版本,早在去年 9 月就已经改过了。
这就是 LLM 最根本的两个知识缺陷:知识有截止日期,而且它不知道你公司内部发生了什么。训练数据再丰富,也覆盖不了你企业知识库里的那几十万份文档、不是昨天的产品变更、不是只有内部员工才看得懂的术语表。
上一篇我们聊了 Agent 的骨架——LLM + Tools + Memory + Harness。骨架搭好了,但光有骨架的 Agent 就像一个什么都不知道的新员工:它能用工具、能执行流程,但问它任何业务相关的问题,它要么说”我不知道”,要么更危险——假装知道。
所以怎么让 Agent 真正”懂”你的数据?答案指向一个被说烂了但很少被说清楚的词:RAG。问题在于,大多数人理解的 RAG 就是”接个向量数据库”,这距离真正能用的 RAG pipeline 差了十万八千里。
先回到最基本的。RAG 这四个字母是 Retrieval-Augmented Generation 的缩写,直译就是”检索增强生成”。直觉上其实很朴素:用户问了一个问题,你先从知识库里把相关的内容找出来,然后把这些内容连同问题一起交给 LLM,让它基于这些内容生成回答。四个步骤——Query、Retrieve、Augment、Generate——说白了就是:提问、检索、拼上下文、生成回答。
但这和”搜索”有什么区别?
区别大了。搜索是”找到相关文档,你自己看”。RAG 是”找到相关文档,让 LLM 读完之后替你总结、推理、回答”。搜索结果是一堆链接,RAG 的产出是一段经过理解之后的回答。这个区别听起来微妙,但在工程上意味着完全不同的要求和复杂度——搜索只需要召回率,RAG 还需要检索结果的质量足够好,好到 LLM 能基于它生成准确的回答,而不是被噪声带偏。
可惜现实中,人们对 RAG 的误解比理解多。最常见的一个:把 RAG 等同于向量搜索。这当然可以理解——大部分 RAG 教程的第一步都是”把文档切成 chunk 然后塞进向量数据库”,很容易让人觉得向量搜索就是 RAG 的全部。但向量搜索只是检索环节的一种手段,检索本身又只是 RAG 四步中的一步。Query 怎么理解、检索结果怎么排序和过滤、喂给 LLM 之前怎么组装上下文、生成之后怎么验证回答质量——每一个环节都能决定最终效果。把向量搜索等同于 RAG,就好比把”去超市买食材”等同于”做了一桌菜”。
还有一些更微妙的认知偏差。比如觉得 chunk 切得越细检索精度就越高——这听起来有道理,但粒度太细会切断语义完整性。你把一段文字切成三块,检索命中的可能只是其中一块,LLM 看到的信息是残缺的,回答自然也是残缺的。粒度选择本质上是个工程权衡,要看数据类型和查询模式,不是越细越好。
更隐蔽的一个误解是觉得 RAG 能解决所有知识问题。其实如果你想让模型学会某种风格或格式——比如用你们公司的文档风格来写东西——few-shot 或者 fine-tuning 可能更直接。RAG 解决的是”模型不知道某个事实”的问题,不是”模型不知道怎么做”的问题,这两件事的解法不该混为一谈。
搞清楚了 RAG 不是什么,接下来聊聊 RAG 怎么从”能跑”变成”能用”。这部分会比较技术,但我觉得理解每个环节的动机比记住具体方案更重要。
最朴素的 RAG——通常叫 Naive RAG——流程很简单:把文档切成固定大小的 chunk,用 embedding 模型转成向量,存进向量数据库。用户提问的时候,把问题也转成向量,用余弦相似度在向量库里找最接近的 top-K 个 chunk,拼在一起塞给 LLM,让它生成回答。
这个流程跑通不难。但跑通之后你会发现几个很具体的问题。
先说检索噪声。余弦相似度找出来的 top-10 个 chunk 里,可能只有两三个是真正相关的,剩下的只是”向量空间上离得比较近”而已。LLM 拿到一堆半相关不相关的上下文,很容易被带偏——它可能从某个噪声 chunk 里抓了一个看起来相关但已经过时的数据,然后信心满满地写进了回答。
再说 chunk 边界的问题。你按 500 个 token 切了一段文字,一个完整的论述可能被切成两半——前半段说”我们的产品分为三个版本”,后半段才展开每个版本的定价。检索只命中了前半段,LLM 根本看不到完整的信息。
更致命的是多跳问题。用户问”2024 年 Q3 我们在欧洲市场的退货率是多少”,这个回答可能需要先找到 Q3 的销售数据,再找到欧洲市场的退货统计,然后交叉计算。但 Naive RAG 只做一次检索,它大概率找到的是一份笼统的市场报告,里面的数据既不够具体也不是你要的季度。
这几个问题不是孤立的,它们指向同一个结论:Naive RAG 的瓶颈不在 LLM 的生成能力,而在检索的质量。检索垃圾进去,生成的也是垃圾。
所以从 Naive RAG 到 Advanced RAG 的演进,核心就是围绕检索的三个阶段做工程优化。
检索前的优化目标是让”问题”本身变得更适合检索。用户的原始 query 往往不是最优的检索词——用户问”怎么退货”,但你们的知识库里写的是”退换货流程与售后指引”,关键词完全不匹配。Query 改写就是用 LLM 把用户的自然语言问题转成更适合检索的形式,比如把”怎么退货”改成”退换货流程 售后指引 步骤”。
如果问题比较复杂,Query 分解会更有效——把一个复杂问题拆成多个子问题分别检索再合并。比如”对比我们三个产品的定价策略”可以拆成三个独立的检索请求。还有一个我觉得挺巧妙的技巧叫 HyDE(Hypothetical Document Embedding):先让 LLM 根据问题”猜”一个答案,然后用这个假答案去做检索。原理很直觉——LLM 猜的答案在语义上和真实文档更接近,用答案去检索比用问题去检索效果更好。
检索环节的改进核心思路是”不要只靠一种检索方式”。向量检索擅长捕捉语义相似性——用户说”怎么退款”能匹配到”退换货流程”,因为语义上接近。但它对精确匹配不太行——用户搜一个特定的产品编号”SKU-28473”,向量检索可能完全找不到。BM25 关键词检索刚好反过来,精确匹配很强但语义理解弱。所以生产环境里最常见的做法是混合检索:向量检索和 BM25 各召回一批候选,然后合并去重。效果比单独用任何一种都好。
光召回还不够。混合检索可能召回了 50 个候选 chunk,但 LLM 的上下文窗口有限,你不可能全塞进去。所以需要重排序(Reranking):先用一个轻量模型做粗筛,保留 20-30 个候选,再用一个更精确但更慢的 cross-encoder 模型精排,最终只保留最相关的 5-8 个。cross-encoder 的原理是同时看 query 和文档内容做注意力计算,精度比向量相似度那种”各自编码再比较”的方式高一个档次——类似招聘,先海选简历,再面试细筛。另外,元数据过滤在这一步也很关键:如果用户问的是 2024 年的政策,那 2023 年的文档直接过滤掉,不需要进入排序环节。
检索之后的事同样重要。不是所有检索结果都值得放进 LLM 的上下文。上下文压缩(Context Compression)会去掉检索结果中的冗余信息——同一件事被三份文档重复描述了,只需要保留最完整的一份。相关性校验会更进一步:用一个小模型判断每个检索结果是否真的和用户问题相关,不相关的直接丢弃。最后是引用溯源——让 LLM 在生成回答时标注每句话的来源,这样用户可以验证回答的可靠性。这一步在知识密集型场景里几乎是必须的,你不能让 Agent 给出一段无法溯源的回答,那跟幻觉没什么区别。
说到检索质量,有一个经常被低估但直接影响效果下限的决策:chunk 怎么切。
最简单的方案是固定长度切割——每 500 个 token 一刀,不管内容结构。实现起来最快,但语义很容易被切断。一个完整的操作步骤可能横跨两个 chunk,检索命中了其中一个,LLM 看到的就是不完整的指令。
更自然的做法是利用文档本身的结构信息——标题、段落、换行符——来决定切分边界。这样每个 chunk 至少是一个完整的意思,语义完整性好得多。代价是 chunk 大小不均匀,有的段落可能就两句话,有的章节可能上千字,给检索的均匀性带来一些挑战。语义切割(Semantic Chunking)的思路更精细:用 embedding 计算相邻句子的相似度,相似度突然下降的地方就说明话题切换了,那就是天然的边界。效果最好,但计算成本也最大——每句话都要算一遍 embedding。
实践中用得最多的是 LangChain 的递归切割——先按章节切,太长就按段落切,还长就按句子切,逐层往下直到大小合理。说白了就是前几种方案的组合,兼顾结构完整性和大小均匀性。
没有银弹。技术文档结构清晰,按章节切就够了;客服对话记录结构松散,可能需要语义切割;产品描述短而多,固定长度反而最简单有效。务实地说,先按段落切、跑通第一版、看检索效果,遇到具体问题再针对性优化——比一上来就追求最优解靠谱得多。
把上面这些串起来,看一个生产级的 RAG pipeline 长什么样。
场景是企业知识库问答系统。数据来源包括产品文档、内部 Wiki、技术规范、历史工单记录。用户是公司内部的客服和运营,他们需要快速查到准确的产品政策和技术参数。
整个系统分两个阶段运行。离线索引阶段:先把各种格式的文档解析成纯文本——PDF 要 OCR,Wiki 要爬取,表格要转成结构化的文字描述。然后按”段落优先、递归兜底”的策略切块。切好的 chunk 经过 embedding 模型转成向量,写入向量数据库(比如 Milvus 或 Pinecone),同时构建一份全文索引用于 BM25 检索。每个 chunk 还会附带元数据:文档标题、来源、更新时间、所属部门、权限级别。这些元数据在后面的检索过滤里会派上大用场。
在线查询阶段的流程大致是这样:
1 | def rag_pipeline(query: str, user_context: dict) -> Answer: |
这里有几个值得说的工程决策。为什么选混合检索而不是纯向量?因为企业知识库里大量查询是精确匹配的——员工搜一个具体的产品型号或政策编号,BM25 比向量检索靠谱得多。为什么用 cross-encoder 重排序而不是直接用向量相似度?因为 cross-encoder 是交叉编码,会同时看 query 和文档内容做注意力计算,精度比”各自编码再比较”高一个档次。代价是慢,所以只对粗筛后的少量候选做精排,把延迟控制在可接受的范围。至于为什么按段落切而不是语义切割——其实产品文档结构清晰的时候,段落本身就是很好的语义边界,没必要引入额外的 embedding 计算成本。
性能和成本的权衡贯穿始终。Query 改写和 HyDE 都要额外调一次 LLM,重排序要跑 cross-encoder——每一层都增加延迟和成本。如果查询量不大、对延迟不敏感,全上没问题。但如果是高并发的在线服务,就得做取舍:Query 改写保留,HyDE 砍掉;粗筛用向量相似度代替 cross-encoder 的第一轮;上下文压缩用简单的去重代替 LLM 摘要。说白了,不是所有环节都需要最贵的方案,够用就行。
回到 Agent 的视角来看 RAG。
RAG 不是一个独立系统,它是 Agent 的知识基础设施。上一篇我们聊了 Agent 的骨架——LLM 负责推理,Tools 负责执行,Memory 负责记忆,Harness 负责安全。RAG 解决的是另一个维度的问题:Agent 的推理需要事实依据,而这些事实不可能全靠训练数据来提供。
可以用一个简单的分工来理解它们的关系。Agent 是执行者,决定做什么、怎么做。RAG 是知识源,在 Agent 需要事实依据的时候提供弹药。Harness 是安全网,确保 Agent 的行为在可控范围内。三层缺一不可:没有 RAG 的 Agent 是一个不知道任何业务事实的工具执行器,没有 Harness 的 Agent 是一个不可控的风险源。
那 Agent 什么时候该用 RAG?简单说,当任务需要”知道某个具体事实”的时候——查政策、找参数、定位文档。需要”执行某个操作”的时候该用工具——发邮件、查数据库、调用 API。需要”回忆之前做过什么”的时候该用记忆——追踪任务进度、延续对话上下文。三种信息来源各有边界,搞混了效果就差。
单个 Agent 配上 RAG 和 Harness,已经能做不少事了。但现实世界里的任务往往不是单个 Agent 能扛住的——一个复杂的企业流程可能涉及多个系统的交互、多种角色的协作、多个阶段的串并联。单个 Agent 再强,也有天花板。下一篇我们就聊聊,当天花板真的撞到了,怎么通过多 Agent 编排来突破它。
RAG 的核心从来不是接个向量数据库。从 Query 理解到检索策略、从 Chunk 切分到重排序、从上下文组装到引用溯源——每一个环节都在做同一件事:让 LLM 拿到最好的素材,然后生成最准确的回答。素材质量决定了回答质量的上限,而每个工程环节都在往上推这个上限。
当然,并不是每个场景都需要全套组件。先跑通 Naive RAG,有了基线数据,再根据具体问题逐个优化。检索噪声大就加重排序,语义切断就调 Chunk 策略,多跳问题就加 Query 分解。不要一上来就把所有高级组件全堆上去——每一层都有成本,每一层都可能引入新的 bug。先让它跑起来,再让它跑得好。
这说到底是个工程问题,不是什么魔法。


