做 AI Agent 这段时间,一个特别强烈的感受是:Agent 跑得起来不难,跑得好不好,几乎全看它"知道什么"。模型本身只会背书——你把它训练时见过的知识摊开摆在哪,它就只认哪些。可真实业务里,你的文档、你的流程、你客户上周刚提的工单,模型全都没见过。于是 RAG 就成了 Agent 的"知识获取管道":Agent 在回答之前,先从一个外部知识库把相关内容捞出来,再交给模型推理生成。这篇是以"走进 AI Agent"系列第四篇的身份,把 RAG 的基础原理、工程实现、调优方法和踩坑经验一次讲透,适合已经跑通 LLM API、开始给 Agent 接真实数据的人参考。
1. 模型记不住的东西,RAG 替它记
这一节我想先放下代码,聊聊 RAG 到底在 Agent 里扮演什么角色。很多人一上来就查"LangChain 怎么配 RAG",配了半天效果不对,回头抱怨检索不准,其实根子在于没想清楚:你到底在解决什么问题。
1.1 参数化记忆与非参数化记忆的裂缝
大模型的"知识"是写在权重里的,这叫参数化记忆。训练完成那一刻,它的知识截止日期就固定了。你问它去年的行业政策、你们公司内部的产品规格、某个客户的历史订单,它要么一本正经地编,要么直接说不知道。更麻烦的是,即便知识没有过期,私有数据也根本不在训练集里——模型对这部分是一张白纸。
所以 Agent 要做业务,必须引入非参数化记忆:知识放在外部存储里,用的时候再检索。RAG 是这种理念最典型的实现。检索(Retrieval)拿到相关内容,增强生成(Augmented Generation)让模型基于这些内容回答。你也可以把它理解为:模型从"闭卷考试"变成了"开卷考试",而 RAG 就是帮你在考场上翻书的那只手。
1.2 和微调相比,为什么 Agent 场景先考虑 RAG
提到给模型喂知识,总有人问:干嘛不微调?我的观点是,微调和 RAG 解决的是不同问题。微调适合改变模型的"行为方式"——语气、输出格式、特定任务的执行逻辑;RAG 适合补充"事实知识"。你可以这样记:微调改变的是怎么说话,RAG 改变的是说什么内容。
从工程角度看,RAG 的优势非常明显:
- 更新快:知识库文档改了,重新跑一遍索引就行,不需要重新训练模型。
- 可追溯:模型回答时可以附上引用的原文片段,这在企业场景里是刚需——出了问题能倒查。
- 成本可控:不用为每一个知识域训练一个模型,知识通过检索来复用。
我见过不少团队一上来就微调,结果训了个"一本正经的胡说八道增强版"——因为微调并不会让模型凭空多出事实记忆,只会让它把没见过的东西编得更流畅。正确姿势是先 RAG 跑通,确有必要再考虑微调做补充。
1.3 RAG 在 Agent 架构里的坐标
一个完整 Agent 通常有四块:模型(大脑)、工具(手脚)、记忆(短期+长期)、规划(行动策略)。RAG 属于记忆层里最常被读取的那条通道。它能喂给 Agent 三类东西:静态知识(文档手册)、动态数据(实时查询结果)、以及对话过程中的积累(历史记录向量化)。
所以当你设计 Agent 时,不要把它当成一个"检索插件",而要当成"知识获取管道"来设计:Agent 什么时候去查、查什么、查不到怎么办、查到的信息如何拼给模型。这些决策,才是从"能跑"到"好用"的分水岭。后面第 5 节会展开讲 Agentic RAG,就是让这条管道具有一定的自主决策能力。
2. 拆开看:索引、检索、生成三者各管一段
把 RAG 拆开,其实就三个阶段:索引(Indexing)、检索(Retrieval)、生成(Generation)。它们分工完全不同,很多人调不出效果,就是因为把注意力全放在生成那一步,却不知道真正出问题的地方七八成都在前面。
2.1 三个阶段的职责与典型流程
- 索引(离线):把文档吃进来——加载、清洗、切块、向量化,写进向量数据库。这一步决定知识库的"底子"。
- 检索(在线):收到用户问题,转成向量,去向量库找最相近的内容块,可以配合关键词过滤、重排等策略。
- 生成(在线):把检索到的内容块作为上下文,拼进 Prompt 里,让模型基于这些内容作答。
一个最小闭环可以写成这样的流程:
用户问题 ↓ 问题向量化(用同一个 Embedding 模型) ↓ 向量库相似度检索(TopK) ↓ 得到若干 chunk ↓ 拼装 Prompt:System + Retrieved Context + Question ↓ LLM 生成回答你会发现,真正跟"AI"关系最密切的其实是模型,但决定你这个问题问出去能不能有答案的,是前面那段管道。生成环节反而是最简单的:只要检索回来的上下文是对的,模型基本上能给出像样的回答。
2.2 一个最小可跑的检索闭环示例
为了让你对代码体积有个概念,我用最朴素的原生写法给你展示个最小实现,不依赖任何重框架。假设你已经有一个向量库,里面存好了文档切块后的向量。
import numpy as np from openai import OpenAI client = OpenAI() def embed(texts): resp = client.embeddings.create(model="text-embedding-3-small", input=texts) return [item.embedding for item in resp.data] def retrieve(query, vectors, chunks, top_k=3): q_vec = np.array(embed([query])[0]) # 简化版余弦相似度 scores = [np.dot(q_vec, np.array(v)) for v in vectors] top_idx = np.argsort(scores)[::-1][:top_k] return [chunks[i] for i in top_idx] def generate(query, context_chunks): context = "\n\n---\n\n".join(context_chunks) prompt = f"""请仅依据以下资料回答问题。如果资料中没有相关信息,请直接说明“资料中未找到”。\n\n资料:\n{context}\n\n问题:{query}""" resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], temperature=0.2, ) return resp.choices[0].message.content这段代码糙,但骨架是对的。你平时用 LangChain 或 LlamaIndex,不过是把这里面每一步替换成更成熟的组件。先把原生流程跑通了,再上框架,你才不会被框架的黑魔法绕晕。
2.3 为什么说"生成"反而是最简单的一环
因为要说"人话",模型是专业的。难点从来都是:你到底喂了什么。如果检索回来的 chunk 牛头不对马嘴,再强的模型也答不对;如果检索回来的内容是零碎半句话,模型就只能脑补补齐信息,幻觉就从这里冒出来。
所以 Prompt 里那句"请仅依据资料回答,没有就直说"不是客套话,是防幻觉的第一道闸门。但这道闸门只能防"模型自己发挥",防不住"检索资料本来就错"的情况。逻辑顺序一定要摆正:生成是最后一道工序,别指望它兜住前面所有问题。
3. 从文档到向量:索引环节全是细节
很多人第一次做 RAG,把文档一股脑喂进去,向量化存起来就算完事。这个阶段看起来越简单,后面检索阶段就越吃亏。索引阶段的每个决定,都在检索阶段十倍百倍地放大。所以我愿意把这一节写细一点。
3.1 文档加载与清洗:垃圾进,垃圾出
现实中的知识库文档格式五花八门:PDF、Word、Markdown、HTML、扫描件,有些甚至是复制粘贴到后台的富文本。PDF 尤其麻烦——有些是文字层可提取,有些是扫描图片需要 OCR,还有些是表格和图文混排,直接按顺序抽取会把正文和注释搅在一起。
我的建议是,按内容类型建立不同的加载策略:
- 纯文本 / Markdown:直接读,注意保留标题层级。
- PDF:先用工具检测是否有文字层,没有就 OCR;之后尽量按阅读顺序抽取段落和表格。
- HTML:去标签、去导航和页脚噪声。
- 表格类:不要把它拆散成纯文本,尽量保留"表头+行"的结构,检索回来后模型才知道哪个数字对应的哪个字段。
这块工具选择很多,unstructured、Marker、PyMuPDF 我都用过。讲师自己图省事,可以直接先用 langchain 的加载器凑合上,但你要清楚:索引是给后续所有阶段打底的事,文档没清洗干净,检索再厉害也白搭。
3.2 Chunk 切分:粒度与语义完整性的拉锯战
切分是整个索引环节里最讲究的一步。切太大,单个 chunk 里噪声多,召回不精准,还会浪费模型的上下文窗口;切太小,语义被拦腰截断,一个完整的结论散落在两个 chunk 里,检索时哪个都不完整。
常见的切分策略我给你列个表格:
| 策略 | 做法 | 适合场景 | 注意点 |
|---|---|---|---|
| 固定长度切分 | 按 token 数切,比如 512 token,overlap 50 | 大多数问答类场景的默认起跑线 | 容易切断句子,需要 overlap 兜底 |
| 结构感知切分 | 按 Markdown/HTML 标题层级切,代码块、表格整体保留 | 文档结构良好的技术手册、教程 | 标题附近的信息要跟正文一起归并 |
| 父子切分 | 最外层是大块,内部再切成小块;检索用小块,上下文用大块 | 既要精准定位又要上下文完整的场景 | 存储和检索链路会复杂一些 |
| 递归字符切分 | 按段落、句子、单词逐级往下切,碰到阈值为止 | 通用场景,LangChain 默认方案 | 依赖分隔符质量 |
固定长度切分时,overlap 一般取 10%~20%。比如 chunk_size=500,overlap=50,意思是相邻两个 chunk 之间有 50 个 token 是重叠的。这样做的目的,是防止一段完整语义恰好被边界切开——边界附近的上下文在相邻 chunk 里还能"续上"。
我自己的经验是:先按结构切,越细越好;结构不清晰的再落到固定长度兜底。如果你的文档本身是那种一屏一段的手册,直接固定长度切也不会太差;但如果文档里有大量项目符号、嵌套列表、表格,结构感知切分几乎是必须的。
3.3 Embedding 模型选型:别迷信一个模型走天下
向量化就是把文本变成一串数字,让语义相近的句子在向量空间里离得近。这一步选择什么 Embedding 模型,直接决定"语义相近"对你来说意味着什么。
中文场景下我比较常用的几个:
bge-m3(BAAI):中英文效果均衡,支持 8k 长文本,适合中文知识库起步。text-embedding-3-small/large(OpenAI):生态成熟,API 调用方便,但中文效果不一定比得过专门的中文模型。m3e/jina-embeddings-v2等:不同场景各有千秋,建议拿自己的数据小批量测一下。
一个核心原则:检索用的 Embedding 模型,最好和后续语义匹配的调性一致。比如你做的是数学公式检索,通用模型就明显不够,得找对数学符号友好的模型。另外,查询短句和文档长段落之间的语义鸿沟是真实存在的,有些模型支持为 query 和 passage 分别加前缀指令,用的时候别省略这一步。
还有两个工程细节容易踩:
- Batch 嵌入时,注意最大输入长度,超长文本要预先截断或分段。
- 如果自己部署 Embedding 模型,加个缓存:文档 hash 映射到向量,避免重复计算。全量重算几万份文档的向量,时间和钱都是成本。
3.4 元数据与向量库:过滤条件决定检索上限
向量检索不是只能算相似度,你完全可以在向量库的字段上叠加过滤条件。比如知识库按部门、时间、文档类型打标签,检索时先过滤再排序,既快又准。这个能力叫元数据过滤(Metadata Filtering),很多初学者会忽略,但它在真实业务里几乎必用。
举个例子:客户问"去年你们退款政策是什么",如果库里混着 2022 版、2023 版、2024 版的政策文档,纯向量检索可能把三版都捞出来。给文档打上version、effective_date字段,检索时限定effective_date <= 2025-01-01,再按日期倒序,才能保证模型看到的是最新政策。
向量数据库选择上,我按项目规模给个参考:
| 需求 | 推荐 | 理由 |
|---|---|---|
| 个人项目/小 Demo | FAISS / Chroma | 轻量,本地跑,不引入额外服务 |
| 企业应用,需要高并发 | Milvus / Qdrant | 分布式、支持复杂过滤、生态成熟 |
| 已有 PostgreSQL | pgvector | 复用现有数据库,减少基建成本 |
| 需要在 Elasticsearch 里统一检索 | ES + dense vector | 关键词和向量统一入口 |
老实说,我先是从 FAISS 起步的,后来项目里同时用了 Qdrant 和 pgvector,各有各的舒服和不舒服。选型别纠结,先跑通业务,再按瓶颈换,成本比想象低。
4. 检索环节:搜到不难,搜得准才难
索引建好了,向量库塞满了,接下来就是用户提问的瞬间:怎么从几十万条 chunk 里捞出最该给模型看的那几条。这一步是 RAG 的命门,也是优化空间最大的地方。
4.1 TopK 的甜区:5~20 之间,别贪
TopK 指检索返回的 chunk 数量。太少了,上下文不够模型发挥;太多了,无关信息掺杂进来,模型容易被噪声带偏,还白烧 token。
我的习惯是:起始设为 5~10,根据答案质量微调。如果某个知识域 chunk 本身信息量大且切得准,TopK 甚至 3 就够了;如果 chunk 切得碎、信息分散,可能要加到的 15~20。结合上下文窗口来看,一般给模型的上下文总长度控制在 2000~4000 token 比较舒服,太长了模型反而抓不住重点。
这里有个经验规律:TopK 增大,召回变好但精确率下降——捞回来的相关材料多了,同时不相干的东西也多了。你要在"有没有捞到"和"捞到的是不是干净"之间找平衡。最好的办法是拿一批真实提问跑一遍,看返回的 chunk 到底长什么样,再决定 K 值。别拍脑袋。
4.2 混合检索:用 BM25 补上语义检索的盲区
纯向量检索有个天然盲区:对精确词汇、专有名词、型号编号、人名不敏感。比如用户输入"AYB-200 型传感器故障码 E04",Embedding 模型很可能把重点放在"传感器故障"上,把关键的型号和故障码稀释掉了。这种场景,传统的关键词检索反而更准。
所以实践中我会做混合检索(Hybrid Search):向量检索负责语义相似,BM25 负责精确匹配,最后把两者结果合并排序。合并时可以用 RRF(Reciprocal Rank Fusion)——给每个文档在两路结果里的排名取倒数相加,简单有效:
score(doc) = 1/(60 + rank_vector) + 1/(60 + rank_bm25)这个 60 是常见平滑常数,你可以调。合并后按总分排序取 TopK。这一步在 LangChain 里也有现成实现(EnsembleRetriever),但你要理解背后的逻辑,才知道什么时候该调权重。
实测效果:我做过一个客服知识库,纯向量检索的命中率大概 0.6,混合检索后能到 0.8 左右。最明显的提升出现在"用户报出精确产品型号"这类问题上。
4.3 Rerank:从"相关"到"可用"之间还有一道槛
混合检索拿到的 TopK,相关性是粗糙的。向量相似度高,不代表它真的能直接回答用户问题。这时候就需要重排(Rerank)模型登场。
重排模型一般用交叉编码器(Cross-Encoder),把用户问题和候选 chunk 一起放进模型打分,比双塔结构的向量检索精度更高,但速度慢。工程上通常在混合检索之后,对 Top 50 或 Top 100 的结果做一次重排,只保留 Top 5 给模型。这样兼顾了效率和精度。
# 以 bge-reranker-v2-m3 为例 from FlagEmbedding import FlagReranker reranker = FlagReranker("BAAI/bge-reranker-v2-m3") pairs = [[query, chunk] for chunk in candidate_chunks] scores = reranker.compute_score(pairs)部署重排模型需要一点显存,但换来的是回答质量肉眼可见的提升。尤其你的知识库大、文档杂的时候,重排往往是性价比最高的一步优化。
4.4 量化检索质量:Hit Rate、MRR 和你的评估集
优化要建立在数字上,不然就是玄学调参。RAG 检索质量最常用的指标有三个:
- Hit Rate(命中率):对 N 个测试问题,检索结果里包含正确答案的比例。这个指标回答的是"有没有捞到"。
- MRR(Mean Reciprocal Rank):正确答案在检索结果中的排名倒数取平均。回答的是"捞到的位置靠不靠谱"。
- NDCG:考虑排名位置加权的指标,适合评估多段相关内容的相关性排序。
建一个 50~100 个问题的评估集,每个问题标注"应该命中哪个 chunk"。然后每次改动切分策略、Embedding 模型、TopK、重排逻辑,都跑一遍评估集,看 Hit Rate 和 MRR 的变化。这样调优不再是凭手感,而是有数据支撑的迭代。
我在实际项目里的收尾标准大概是:Hit Rate 0.85 以上,MRR 0.7 以上,线上效果基本能看。如果 Hit Rate 低于 0.7,先别急着调生成,回头检查索引和检索。
5. 从朴素 RAG 到 Agentic RAG:知识管道的自主进化
前面几节讲的都是"朴素 RAG"(Naive RAG):一次检索,固定 TopK,拼装 Prompt,完成。这个模式已经能解决很多问题,但真实 Agent 场景会暴露它的局限——最典型的一个就是:用户的问题它一次检索没搜索到,它就会继续硬着头皮答,哪怕答错了。
5.1 朴素 RAG 的瓶颈:一锤子买卖
朴素 RAG 的问题是"问一遍就交卷"。它不会根据第一次检索的结果判断"信息够不够",不会调整检索词,更不会把多轮检索的结果进行综合。碰到复杂问题就抓瞎:
- 问题里含有指代词("刚才那个方案""第二页的表格"),第一次检索根本定位不到。
- 需要多个知识源联合推理("对比 A 和 B 产品在 2026 年的性能差异"),一次检索只从一个角度捞,信息自然不全。
- 用户问题表述模糊("你们那个东西怎么不行"),不做改写检索就是死路一条。
刚开始用朴素 RAG 就是这么天真,我以为模型足够聪明能把这些补全,实际效果满是坑。所以后来我发现:要让 RAG 真正融入 Agent,必须给它加上"策略"——让模型决定怎么检索、何时停止。
5.2 让 Agent 决定"怎么查":查询改写与迭代检索
最简单的 Agent 化改造是查询改写(Query Rewriting)。在检索之前,让模型把用户问题改写得更适合检索。比如"你们那个东西怎么不行" 改写为 "产品故障排查指南中关于 XXX 无法正常工作的原因与解决方案"。改写后的 query 往往比原文更容易命中。
再进一步是迭代检索(Iterative Retrieval):第一次检索后,如果模型判断上下文不足,可以继续生成新的检索词,或者基于已有结果提出后续问题,再次检索,直到信息足够。这种"检索-评估-再检索"闭环,其实就是 Agent 规划的雏形。用 LangChain/LlamaIndex 的自定义 Agent 或者直接写循环都能实现。
用户问题 ↓ Query 改写 ↓ 检索 → 拼装上下文 ↓ LLM 判断:上下文够不够?不够 → 生成新检索词,回到检索 ↓ 够了 → 生成最终回答这个循环的退出条件要写清楚,别让 Agent 无限查下去。我一般限制最多检索 3~4 轮,超过就让它基于已有内容回答,并说明检索到的信息有限。
5.3 多路召回与工具路由:按需选择知识源
到了复杂一点的企业系统里,知识源往往不是一个向量库,而是多个:文档知识库、关系数据库、知识图谱、外部 API,甚至另一个 Agent。这时候"知识获取管道"的含义就从单条管道变成了一张网。
Agentic RAG 的思路是让模型做路由(Routing):根据问题类型决定访问哪个知识源。比如:
- 事实型问题("退款政策是什么")→ 文档向量库
- 结构化查询("上季度各区域销售额")→ 数据库 / API
- 关系型问题("A 项目涉及哪些供应商")→ 知识图谱
- 需要实时数据的问题("现在服务器状态如何")→ 监控 API
路由可以通过配置化的工具定义,也可以直接用模型的 Function Calling 实现。你在给 Agent 设计工具时,把"知识获取"也定义成一组工具:search_docs(query)、query_database(sql)、search_graph(entity)。这样 Agent 就在用统一的决策逻辑调度不同知识源了。
这种设计有一个额外好处:每个知识源可以各自优化。文档库调重排,数据库写 SQL,图谱走图查询,互不干扰,整体还能统一审计。这是我从单一 RAG 向量库切到 Agent 式多路召回后,觉得收益最大的地方。
5.4 演进路线图:按项目节奏走,别一口吃成胖子
如果你现在的项目还处于"能跑"阶段,我建议按这个顺序演进:
- 朴素 RAG 跑通全文:文档加载、切块、向量化、检索、生成,先让答案是"有出处"的。
- 查询改写 + 混合检索 + 重排:把检索质量提上来,这是性价比最高的组合拳。
- 迭代检索 / 多轮验证:让 Agent 自己判断上下文够不够,不够就再查。
- 多路路由:接数据库、图谱、API,形成更完整的知识网络。
- Graph RAG / Ontology RAG:当你的知识库实体关系密集时(比如几十个产品线互相依赖),可以考虑这类升级。Graph RAG 在社区里讨论很多,但务必先确认你的数据适合实体抽取和图建模,不要为了追新而上。
这个过程是渐进的,每步都有可量化的指标进步。我见过不少人从第 1 步直接跳到第 5 步,结果被图索引的复杂度拖垮,最后连基础检索都没做好。老老实实把地基打好,再谈上层建筑。
6. 实操中的五个坑,以及我是怎么绕过去的
最后分享一些实战里踩过的坑和绕行方案。这些内容不常出现在官方文档里,但几乎每个做 RAG 的团队都会撞上。
6.1 坑一:文档切碎后,模型"看不到全局"
早期我做过一个产品手册的 RAG,手册里有大量"前文定义、后文引用"的结构。纯固定长度切块后,同一个概念的定义和引用被切到不同 chunk 里,模型看到了关于某个参数的一句话,但不知道这个参数整体怎么用。
后来我改成"父子切分":把整章节作为父块,章节内的小节作为子块;检索命中子块后,把父块一并塞进上下文。这样模型既能定位精确内容,又能看到上下文语境。如果你不想做得太复杂,至少可以用结构感知切分,尽量让一个 chunk 内部是一个完整语义单元。
6.2 坑二:Embedding 模型不匹配领域
有段时间我在做法律文档的 Agent,发现通用的中文 Embedding 模型对法条编号和"主体-行为-结果"这类结构的理解很弱。检索"违约责任计算方式"总是捞回一堆相关但不对口的法条。
解决方式很朴素:准备一个小规模的领域评估集,把 3~5 个候选 Embedding 模型各跑一遍,看 Hit Rate。无非花半天时间,换来的是长期稳定的检索质量。领域越是专业,越不能迷信排行榜,要拿自己的数据说话。
6.3 坑三:重排前先看召回,否则重排是花架子
重排模型救不了召回。如果相关 chunk 根本没进入候选集,重排再强也排不出去。所以性能差时,优先检查的是召回:是不是 TopK 太小?是不是混合检索少了关键词路?是不是切分把答案切丢了?重排是在好的召回复地上做锦上添花,而不是雪中送炭。
6.4 坑四:成本与延迟失算,线上跑不起来
RAG 链路长,每个环节都耗时耗钱:Embedding 计算、向量检索、重排推理、LLM 生成。我见过一个团队把 TopK 调到 30,重排候选 100 条,结果单次请求延迟从 800ms 飙到 4s。LLM 上下文又是 1 万 token 起步,费用翻了几倍,效果却没有明显提升。
给个务实建议:Embedding 结果一定要缓存;重排候选集控制在 50 条以内;TopK 不要超过 20;LLM 上下文裁剪到只包含检索段落和当前问题。一次请求全链路延迟控制在 2 秒内,用户才不会有明显卡顿感。
6.5 坑五:把 RAG 当成"万能知识注入器"
这个误区最普遍,也最危险。RAG 只能回答"库里有的知识"。如果文档本身没写、信息过时、或者压根没有相关知识,RAG 模型再强也只能说"资料中未找到"。它不是让模型变聪明,而是让模型能查到聪明人整理好的资料。
所以做 Agent 时,你要同时设计一条"知识回流"的路径:用户问了一个库里没有的问题,记录下来,定期整理成新文档,重新索引。这样知识管道才能越用越宽。我现在的项目里就挂了一个"未命中问题"的采集表,每两周整理一批,把高频缺口补进知识库。
RAG 这个主题,越深入越觉得它其实不是一个组件,而是一整套关于"Agent 怎么获取知识"的设计哲学。基础扎实了,后面无论接图谱、接数据库、接多 Agent 协同,都是在同一根管道上加插口。这篇先把底子打牢,下一篇我会挑一个实际的 Agent 项目,把 RAG 放进完整流程里,看看它和工具调用、记忆管理之间是怎么配合着工作的。