这一篇是“走进 AI Agent”系列的第四篇,前几篇我们聊了 Agent 的整体框架、任务规划、工具调用和记忆机制。今天把话头接上,专门拆一个容易被低估但绕不开的模块——知识获取管道,也就是 RAG。很多人以为 RAG 就是把文档切碎塞进向量库再搜一搜,自己动手搭一遍才发现,切块大小、检索策略、重排序、索引更新,每一个环节都能让效果天上地下。这篇我会从零讲清楚 RAG 的原理、完整搭建流程、评估指标,以及我在实际项目里踩过的坑,适合正在学 AI Agent、或者想把知识库能力接进自己产品的人。
1. 为什么 Agent 需要一条“知识获取管道”
1.1 大模型的“记忆瓶颈”与 Agent 的知识缺口
先聊一个最基础的事实:大模型的知识是“训练时定死”的。你问它今天的新闻、你公司内部的产品手册、某个客户项目的私有资料,它大概率答不上来,或者只能给出一段看似正确实则编造的推测。更麻烦的是,即使是训练过的事实,模型也可能记错细节,因为参数里的知识是压缩和泛化过的,不是精确的数据库记录。
Agent 不一样,Agent 被设计成能“行动”的系统。但它行动的起点同样受制于模型的知识边界。一个没有知识获取能力的 Agent,本质上是个记忆力很差、只会凭刻板印象做事的实习生。它知道怎么调用工具、怎么拆解任务,但一旦遇到需要具体事实支撑的场景,比如“根据这份 2026 版报价表给客户算总价”“查一下我们答过这个客户哪些问题”,它就只能靠猜。
所以 Agent 需要一条管道,把“外部知识”变成“模型可用的上下文”。这条管道不能是一次性的,而应该是持续的、可配置的、能感知知识变化的。这就是我们说的知识获取管道。RAG 是目前最主流的实现方式,它的核心思路很朴素:先检索,再生成——在模型开口之前,先把相关资料拉回来拼进提示词里。
1.2 RAG 在 Agent 里的位置:知识层、记忆层和工具层
在搭 Agent 的时候,我会把它的能力拆成三层来看。
第一层是工具层,负责“能做什么”,比如调用搜索、执行代码、操作数据库,对应 Function Calling 那套机制。
第二层是记忆层,负责“记得什么”,通常包括短期记忆(多轮对话的上下文窗口)和长期记忆(把过去的对话摘要、用户偏好存下来,下次再取用)。
第三层是知识层,负责“知道什么”,解决的是事实性、时效性、私域性的信息获取问题。RAG 就属于这一层。
这三层很容易混在一起讲,但在工程上一定要分清楚。记忆层存的是“这个用户上次说了什么”,知识层存的是“这个领域的客观资料是什么”。如果你把知识库当过手账用,把对话历史全塞进去,检索出来的结果会非常脏;反过来,如果你把知识库当长期记忆用,知识的版本更新和权限控制就完全没法做。
知识获取管道在 Agent 里的位置,是介于“任务规划”和“回答生成”之间的一个接口。Agent 在做计划时,可能决定“我需要先查知识库再回答”,然后把查询请求交给管道,管道返回一组相关资料,Agent 再结合这些资料组织答案。在更复杂的 Agentic RAG 架构里,Agent 甚至可以自己决定“要不要查、查几次、查完要不要再追查”,这是后话,后面会展开。
1.3 先分清几个高频词:RAG、Agentic RAG 和 GraphRAG
最近热词里经常看到 Rag、Agentic RAG、GraphRAG、Ontology RAG 混在一起,新手很容易被绕晕。简单理一下:
RAG 是最基础的一条管道,叫法区分主要体现在“管道是否由 Agent 动态编排”上。传统 RAG 是固定的流程:用户问题进来,检索,拼接提示词,生成答案。每次的流程一模一样。
Agentic RAG 则是在这个流程外面加了一个“决策层”。Agent 接到问题后,先判断这个问题需不需要查知识库,如果需要,它再决定查哪几个库、关键词要不要改写、第一次检索结果不够的话要不要换策略重查、要不要把多路结果合并后总结。流程不再是写死的,而是根据问题动态调整。这个架构的优点是灵活,代价是更容易出错、更难调试。
GraphRAG 是微软开源项目带火的一个概念。它的思路不是把所有资料切成小块求相似度,而是先把文档里的实体和关系抽出来,构建成一张知识图谱,再把相似或相关的实体聚类成社群,离线生成社区摘要。查询时,针对全局性问题检索社区摘要,针对具体问题检索实体关联详情。适合做“这个文档集合里整体讲了什么”这类全局问答,但建图成本高、更新难,不是所有场景都需要一上来就上。
还有一个 Ontology RAG,本质是在知识库里先定义一份本体,也就是领域概念和关系的明确 schema,比如“客户”“订单”“产品”以及它们之间的关联,再做受控检索和推理。它适合知识结构很稳定的企业场景,但前期建模成本不低。
一句话总结:RAG 是地基,Agentic RAG 是给它装了大脑,GraphRAG 和 Ontology RAG 是两种面向特定问题的建材方案。
2. 一条 RAG 管道的完整设计与拆分
2.1 标准五段式:加载、切块、向量化、检索、生成
不管是用 LangChain、LlamaIndex,还是自己手搓,RAG 管道的骨架基本是固定的,我把它们拆成五段:
第一段是文档加载。把 PDF、Word、Markdown、HTML、数据库里的文本捞出来,转成纯文本。这一步往往被忽略,实际上格式解析的干净程度直接决定后续切块和检索质量。PDF 里的表格、多栏排版、页眉页脚,处理不好就会把语义切得七零八落。
第二段是切块。长文本不能整个塞进 embedding 模型和上下文窗口,必须切成有独立语义的小块。切多大、块与块之间要不要重叠、按什么边界切,这一段是 RAG 效果的分水岭之一。
第三段是向量化。把每个文本块丢给 embedding 模型,转成一个高维向量,存进向量数据库,同时保留原始文本和元数据。这个向量表示的是语义,语义相近的文本在向量空间里距离就近。
第四段是检索。用户问题来了之后,先给问题做向量化,然后到向量库做相似度检索,取回最相关的 top-k 个文本块。这一步还可以叠加关键词检索、重排序、元数据过滤等手段。
第五段是生成。把检索到的文本块作为上下文,连同原始问题一起拼给大模型,让它基于这些材料作答。这里要尤其注意提示词的写法,不能允许模型忽略上下文自由发挥。
整个管道再进一步抽象,就是两条链路:离线索引链路负责前三个环节,先把知识变成可检索的结构;在线查询链路负责后两个环节,把问题变成答案。很多项目出问题,都是因为在“离线”和“在线”之间没做好衔接,后面会细说。
2.2 离线索引与在线查询:两条时间线要分清
RAG 项目里最经典的错误认知,就是把索引和查询当成同一件事。实际上它们是两条完全不同的时间线。
离线索引发生在“知识入库”的时候。你把一批新文档导进来,程序读取、清洗、切块、embedding、写入向量库。这个过程可以很慢,因为不涉及用户等待。重要的是可重复性:同一份文档重新索引,结果应该一致;文档有更新,要能定位到旧版本并覆盖或标记失效。
在线查询发生在“用户提问”的时候。一条请求进来,程序要在几百毫秒到几秒内完成 embedding、检索、重排序,然后把结果交给模型生成答案。这个阶段最关心的是延迟和召回质量。
这两条时间线一旦被混淆,就会产生连锁问题。最常见的例子:有人把“向量化整个文档”放在查询请求里做,用户问一句,程序现场把全部资料重新处理一遍,延迟立刻爆炸;也有人把“索引更新”做成完全没有版本号的东西,知识库里的旧数据没删干净,新数据又叠进来,检索结果被过时信息污染。
我的习惯是在索引端给每个文档或切片打上版本号、入库时间和来源标记,查询端严格只用“已发布”版本的数据。这样知识更新是平滑的,出了问题也能回溯是哪一批数据引入的。
2.3 索引设计里的三个关键参数:chunk_size、overlap 和 embedding 模型
切块大小这个参数,网上说法很多,有人说 256,有人说 512,有人说 800,其实脱离场景谈数字没意义。文本的语义粒度是不一样的:产品 FAQ 一个问答就是一个完整语义单元,适合按条目切,一个条目几百字;长文分析报告可能一个大段落才是一个完整论点,切小了反而把论证过程截断。
我的经验是:chunk_size 的选择首先要看下游任务需要多完整的上下文。如果答案通常依赖一个段落内的逻辑,512 到 800 字是比较稳的区间;如果是代码、SQL、JSON 这类结构化内容,按结构块切往往比按字数切更好。另一个容易被忽略的参数是 overlap。块与块之间重叠一部分,可以缓解“关键句恰好被切在边界上”的问题。比如 chunk_size=512、overlap=64,意味着每个块末尾的 64 字会和下一个块开头重复,这些重复内容在检索时可能同时命中两个块,但能明显降低关键信息被切断的概率。
embedding 模型的选择,比很多人想象的更影响效果。不是维度越高越好,也不是模型越大越好,而是模型要和你的语料类型匹配。中文场景下,现在开源社区有很多好用的中文向量模型,比如 BGE 系列、M3E 系列,英文场景也有相应的选择。判断标准只有一个:在你的真实语料里跑检索,看 top-5 命中率,而不是看公开榜单分数。
还有一个容易忽略的决策:要不要同时保留关键词索引。纯向量检索擅长“语义相似”,但不擅长“精确匹配”。比如型号编号“PLC-S7-1200”、订单号“SO-2026-0081”,这类字符串特征,向量检索不一定能精准命中,BM25 这类关键词检索反而很擅长。成熟的方案是做混合检索:向量召回一批,关键词召回一批,合并去重后再统一打分。
2.4 检索增强:查询改写、混合检索、重排序
基础 RAG 的问题在于“一次检索、一次生成”,用户问题里如果有指代、有隐含前提,或者描述方式和知识库里的原文差距很大,第一次检索往往召不回好东西。这时候就需要给检索环节加装增强组件。
查询改写是我最喜欢先做的一步。用户说“那台设备还能修吗”,如果不改写,检索系统不知道“那台设备”是什么。改写的方式有两种:一种是在对话历史里做指代消解,把“那台设备”替换成前文里出现过的“西门子 1200 PLC”;另一种是把原问题扩展成多个子查询,从不同角度同时检索。改写后的查询可以直接调用一个轻量模型来完成,成本不高,但对命中率的提升非常明显。
重排序则是把检索结果重新排座次。向量检索给出的 top-k 是按向量相似度排的,但向量相似度不完全等价于“对回答问题的有用程度”。一个文本块和问题语义相近,可能只是都提到了同一个词,并不包含真正需要的答案;另一个文本块关联度没那么高,但包含了关键结论。重排序的目的是用更强的交叉编码模型,把“问题-文档块”成对打分,筛掉低质量的召回结果,再把高价值文档顶到前面。
重排序不是必须的,但它带来的收益很直接。没加重排序之前,top-5 命中率可能在 60% 上下;加上之后,很多场景能稳定到 85% 以上。代价是多一次模型推理的延迟,通常可以接受。
3. 从 0 到 1 搭一条可用的 RAG 管道
3.1 技术选型:先用最朴素的组合跑通闭环
开始动手之前,先劝一句:不要在第一天就上全家桶。你不需要一开始就铺 Kubernetes、分布式向量数据库、多路召回、图谱构建。我的习惯是先找一套最朴素的组合,把从文档到答案的完整闭环跑通,验证业务可行性,然后再逐步替换组件。
最朴素的选型可以是这样:
文档解析用现成的工具库,PDF、HTML 都能处理,不强求 100% 完美;切块用递归字符切分器,先按章节标题切,再按段落切,最后按字符数兜底;embedding 用一个中文效果不错且开源可下载的向量模型,本地运行没有额外调用成本;向量库用一个轻量级的即可,数据量在百万级以内完全够用;LLM 调用则对接一个商用大模型 API,或者本地部署的开源模型,按你现有的基础设施决定。
这套组合的特点是每个环节都可以替换,但整体能很快跑起来。等你确认了业务数据、评估了真实效果,再决定要不要引入重排序、要不要换专业向量库、要不要上混合检索。
3.2 最小可运行代码:以 Python 为例
下面给一套最小实现骨架,代码做了简化,但流程是完整的,你在本地换上自己的数据和模型就能跑。
# 1. 文档加载与切块 from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter loader = DirectoryLoader("./docs", glob="**/*.md", loader_cls=TextLoader) documents = loader.load() splitter = RecursiveCharacterTextSplitter( chunk_size=512, chunk_overlap=64, separators=["\n## ", "\n### ", "\n\n", "\n", "。", ";", ",", ""], ) chunks = splitter.split_documents(documents) print(f"共切分 {len(chunks)} 个文本块")这里的 separators 顺序值得注意:优先按 Markdown 二级标题切,然后是三级标题、空行、换行、句号、分号、逗号,最后才是兜底。这样切出来的块尽量保留完整语义,而不是硬按字符截断。
# 2. 向量化与入库 from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma embedding_model = HuggingFaceEmbeddings(model_name="BAAI/bge-m3") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", collection_name="agent_knowledge", ) print("向量库写入完成")BGE-M3 是我在中文语料里用得比较多的一款向量模型,对长文本和中文语义的支持都比较稳。如果你只需要纯中文场景,也可以换更小的模型来降低部署成本。
# 3. 查询、检索与生成 from langchain_community.vectorstores import Chroma vectorstore = Chroma( embedding_function=embedding_model, persist_directory="./chroma_db", collection_name="agent_knowledge", ) retriever = vectorstore.as_retriever(search_kwargs={"k": 8}) docs = retriever.invoke("我们公司的退换货政策是什么?") context = "\n\n".join([doc.page_content for doc in docs]) prompt = f"""请基于以下资料回答问题。如果资料中没有足够信息,请直接说明未知,不要编造。 资料: {context} 问题:{query} """ # 这里的 llm 替换成你实际调用的模型接口,比如本地部署的开源模型或商用 API response = llm.invoke(prompt) print(response)这个骨架虽然短,但已经包含了 RAG 的全部核心环节。你拿真实业务文档跑一遍,很快就能发现瓶颈集中在哪个环节。
3.3 如何评估管道好不好:Hit Rate、MRR 和忠实度
评估 RAG 不能只看回答顺不顺口,必须拆开看指标。我在实际项目里主要盯三个数:
第一个是检索命中率,简单说,就是给定一组测试问题,正确答案对应的文本是否出现在检索返回的前 k 个结果里。衡量的是“好东西有没有被召回来”。召回都没召回,生成端再强也白搭。
第二个是平均倒数排名,它衡量的是正确答案在结果列表里排得多靠前。如果正确答案排在第一位,这个问题的 MRR 就是 1;排在第二就是 0.5。它比 Hit Rate 更严格,因为不仅要求召回,还要求排位靠前。
第三个是生成忠实度。这是单独评“生成”环节的指标,检查模型生成的答案有多少内容是来自检索上下文的。忠实度低的典型表现是:答案里出现了资料里根本没有的数据或结论,也就是模型又忍不住编了。
这三个指标要一起看。如果 Hit Rate 高但 MRR 低,说明相关文档被召回了但排得靠后,重点优化重排序;如果 Hit Rate 低,重点优化切块和 embedding;如果前两者都正常但忠实度低,问题出在提示词或模型本身的指令遵循能力上。
3.4 把 RAG 接进 Agent 的决策循环
RAG 管道本身是一条“查询-返回”的流水线,但接入 Agent 之后,它就不再是简单的一问一答了。Agent 会在决策循环里反复调用它。这里有几个我总结的接入要点。
第一,把管道封装成一个工具函数。输入是查询字符串,输出是结构化结果列表。Agent 完全可以像调用其他工具一样调用它:决定要不要查、用什么关键词查、查出结果后怎么用。
第二,给查询加“意图判断”。不是每个问题都需要走 RAG。Agent 可以先做一次轻量判断,是否需要外部知识。比如简单闲聊、数学计算、代码生成,就不需要检索;涉及事实、时效、私域资料的问题,才走管道。这一步能省掉大量无效检索。
第三,支持多轮追问。用户问“那台设备还能修吗”,Agent 要能结合历史把实体补全后再调用管道。这个其实就是前面讲的查询改写,在 Agent 场景里它不是可选优化,而是必需品。
第四,检索结果要允许“二次追问”。Agent 读完检索结果后发现信息不足,可以选择换关键词再查一次,或者查多个知识库再合并。这种让 Agent 自己决定查几次的架构,就是前面说的 Agentic RAG。它的好处是灵活,坏处是增加了不可预测性,所以我建议在初期把搜索策略的边界写清楚,限制最大检索轮数,避免 Agent 在知识库里绕圈。
4. 常见问题与排查技巧实录
4.1 检索命中率低:先看切块,再看 embedding
如果你发现用户的问题在知识库里明明有答案,但检索结果却不相关,第一步不要急着换模型,先检查切块。
我遇到最多的问题是切块粒度与查询粒度不匹配。文档里一个问题的答案往往横跨三四个段落,但你把文档切成 200 字的小块,每个块只含答案的一部分,检索时每块的相关性得分都不高,自然排不到前面。这种情况下,把 chunk_size 从 256 调整到 512 或者 800,命中率往往立刻改善。
还有一种情况是切块边界把关键信息切断了。比如“退换货政策”的适用条件写了一整段,断句切分却把它截成两半,前半段只有条件没有结论,后半段只有结论没有条件。检索时用户问“什么情况不能退”,只命中了后半段,回答就缺少了前提。解决思路是检查重叠区,或者换用递归切分,优先保语义段。
排除了切块问题,再看 embedding 模型。如果你的语料高度垂直,通用 embedding 模型可能分不清细微的领域差异。比如“内存”在 IT 行业指存储,在建筑行业指混凝土间隙,通用模型可能把它们混在一起。这时候优先考虑在垂直语料上微调 embedding 模型,或者换一个更专业的领域模型。
还有一个经常被忽略的细节:问题和文档的措辞差异。用户在搜索时用的词,和文档里的用词可能完全不同。比如文档里写“质保期内”,用户问“坏了能免费修吗”。向量模型如果不够强,这两个表述的语义距离可能很远。这个问题的解法就是查询改写,把口语化问题改写成文档常用语义表述后再检索。
4.2 回答“答非所问”:问题出在上下文拼错或提示词没约束
如果检索结果明明相关,但最终回答还是跑偏,问题大概率出在生成环节。我踩过两个典型的坑。
第一个坑是把太多不相关的内容塞进了上下文。检索返回 top-10,但真正和问题相关的可能只有两三个块,剩下的全是低相关噪音。模型被噪音干扰,反而忽略了正确答案。处理办法是严格控制 top-k 数量,同时借助重排序把可信度低的结果过滤掉。我一般习惯检索 top-20,重排序后取前 5 或前 8 进上下文,保留密度高、噪音小。
第二个坑是提示词里没有写清约束。如果不明确告诉模型“只能根据资料回答”,它就很容易顺着自己的训练记忆开始补充常识,甚至胡编细节。在 Agent 场景里,这个约束还要更严格一点,因为 Agent 有很强的“完成任务”倾向,它会倾向于给出一个看似完整的答案,而不是承认资料缺失。
给一个我常用的提示词骨架:
请基于提供的资料回答问题。回答时遵守三条规则: 第一,只使用资料中出现的信息; 第二,如果资料不足以回答问题,明确回答“资料不足”,不要猜测; 第三,在回答末尾用小字形式列出引用了哪些资料编号。
第三条的引用跟踪非常有用,它能让你在评估时快速定位答案来源,也方便排查是哪一份资料导致了错误回答。
4.3 知识更新后效果不变:索引生命周期与缓存问题
这种问题通常不是代码逻辑错了,而是知识库的“版本管理”没跟上。你导入了新文档,但向量库里旧文档还没清除,于是新旧内容同时被检索出来,模型被过时信息带偏。
解决办法是给数据入库时打上明确的元数据标记,比如文档的更新时间、状态字段。查询时强制带上过滤条件,只检索 status=active 的块。这样每次更新都先标记旧版失效、再写入新版,而不是让新旧并存。
另一种常见情况是向量库的缓存策略太激进。有些组件在内存里缓存了旧的 embedding 结果,导致即使索引已经更新,查询命中的还是缓存里的旧数据。排查方法很简单:在更新索引后,用一个已知在旧版本存在、但新版本已修改的问题去测试,如果答案还是旧的,基本可以确定是缓存问题,清掉查询链路的缓存即可。
版本管理这件事,往往是在你第一次知识更新失败之后才会重视起来的,不如一开始就设计好。
4.4 和现有系统割裂:元数据、权限和“知识门面”
最后一个问题是整合层面的。很多企业不是没有知识库,而是知识散落在 OA、Wiki、ERP、售后系统、产品手册里。你单独搭一个 RAG 管道,如果只把文档一股脑抓下来切碎入库,不做任何结构规划,得到的就是一个“知识大杂烩”,检索时跨领域内容互相干扰,权限也没法控制。
我的建议是提前做好知识库的划分,安排合理的 Collection/Schema 结构。不同的知识类型放进不同的集合,元数据里明确来源系统、业务领域、可见权限。查询时先根据业务上下文选定集合,再执行检索。这样做,一是检索精度更高,二是权限控制可以落到查询链路里,三是后续对接现有系统时改动最小。
这个思路其实就是热词里“本地 ERP + RAG + LLM 产品检索”这类项目的基础形态。你不需要一开始就上知识图谱或本体建模,但至少要把元数据体系建好。等到某一天你发现跨文档的关联问答、全局统计问答做不了时,再往 GraphRAG 或 Ontology RAG 的方向演进,元数据体系依然不会白建。
最后说一点我的个人体会。RAG 这个方向,入门门槛看起来很低,跑通 demo 只需要半天,但真正把它做进生产系统、接进 Agent,你会发现每个环节都是细节堆出来的。切块要有讲究,检索要有多路保障,重排序不能省,版本和元数据更不能乱。我见过太多项目在一套漂亮的 demo 面前兴奋不已,最后却倒在知识更新的运维和跨库检索的噪音上。希望这篇把基础打牢,能帮你少走我走过的弯路。你搭建的时候遇到什么奇怪的现象,欢迎按这篇的思路逐段排查,大部分问题到最后都藏在最不起眼的参数里。