这一篇接着聊“走进 AI Agent”系列的第四部分知识获取管道也就是 RAGRetrieval-Augmented Generation检索增强生成的基础。前面几篇我们把 Agent 的规划、记忆、工具调用都盘了一遍但有个问题始终绕不开模型自己的知识截止日期和私有业务知识之间的巨大缺口。RAG 就是补上这个缺口最实用的一条管道。这一篇我尽量不写教科书定义而是把“RAG 为什么在 Agent 架构里那么重要”“最小可用实现长什么样”“从基础 RAG 往 Agentic RAG 升级要过哪几道坎”一次讲透。先给结论RAG 不是“给模型接一个数据库”那么简单它本质上是一条负责知识获取、清洗、组织、检索与合成的完整管道。你在日志里看到的效果差异绝大多数不是模型能力造成的而是管道设计造成的。接下来的内容按“为什么—是什么—怎么做—怎么优化—踩坑记录”来展开适合刚学完 Agent 基础、准备给 Agent 接入私域知识的同学也适合已经在跑 RAG、但被“召回不准”“上下文污染”折磨过的人。1. 为什么 AI Agent 需要一条“知识获取管道”1.1 Agent 的“知识饥饿”不是小问题大语言模型的参数化记忆有一个天花板训练数据截止日期、对私域数据天然不可见、对细粒度的版本差异不敏感。你在 Agent 里直接问“我们公司最新的报销制度是什么”模型只会把 2022 年甚至更早的通用常识翻出来更麻烦的是它还会一本正经地编一套规则。这就是知识饥饿模型不是不想告诉你是它的“记忆”里根本没有这份资料。有人会想那我直接把几千页文档塞进上下文不就行了对不起上下文窗口再大也扛不住两个问题。一是真实业务资料动辄几十上百份 PDF、表格、钉钉文档、旧版本文档混在一起token 开销和响应延迟直接失控二是“塞进去”不等于“抓得准”模型在超长上下文里的注意力会很散经常漏掉关键段落。RAG 的思路则是换个方式不把整库知识塞给模型而是每来一次提问只把最相关的几段资料捞出来拼成一个精简的上下文再交给模型。这个“按需捞取”的动作就是知识获取管道。把 RAG 放进 Agent 的场景里它的价值会更明显。Agent 的规划器负责“做什么”工具调度负责“调用什么”但这两者都得建立在“知道什么”的基础上。如果你把一个没有知识管道的 Agent 放去处理企业内部的运营问答它表现出来的不是笨而是“自信地胡说”。有了 RAG它至少知道哪些话是资料里有依据的、哪些话资料库里没有、需要坦白说不知道。这一步直接决定了 Agent 能不能从玩具变成可用工具。1.2 RAG 在 Agent 架构里的准确位置很多人画 Agent 架构图时习惯把 RAG 画成一个孤立的“知识库组件”其实这是不准确的。RAG 在 Agent 体系里至少有两种存在形态。第一种是“隐式管道”也就是查询一进来系统自动做检索、拼上下文、再交给大模型生成用户完全感知不到检索过程。第二种是“显式工具”Agent 的规划器把“知识检索”本身当成一个可调用工具模型根据问题判断要不要检索、检索哪个知识库、检索结果怎么用。第二种就是我们常说的 Agentic RAG它更像是一个会自己决定要不要查资料的助手而不是被动等待查资料的接口。两种形态没有绝对的优劣取决于你的使用场景。如果只是做一个固定的问答机器人隐式管道足够稳定且好调试如果你的 Agent 需要处理多种类型的跨领域问题比如用户问一句“这个客户之前的工单和报价分别是什么”让它显式地决定先查工单库再查报价库效果会明显更好。本篇先讲基础 RAG 这个“根”把根扎稳了Agentic RAG 才能搭得起来。我个人的理解是RAG 之于 Agent不是外挂而是一套“接到问题先想清楚该读什么资料”的机制。它让 Agent 的知识来源从“模型脑内”扩展到“组织沉淀”这也是企业愿意让 Agent 落地到业务里的前提。后面讲的所有参数、步骤、坑都是在围绕这一点服务。2. RAG 基础流程拆解从文档到答案的四段式管道2.1 离线索引加载、切分、嵌入、入库RAG 的完整流程可以拆成离线索引和在线检索两段。离线索引是“造管道”的阶段核心四步是文档加载、文本切分、向量化、写入向量库。很多新手把精力全花在最后一步选什么向量数据库上其实前两步才是决定效果的上限来源。文档加载这一步注意不要只盯着 PDF。真实业务资料里Markdown、Word、HTML、表格、扫描件混在一起非常常见。加载器需要按文档类型分别处理纯文本用目录加载器PDF 至少要试一下能否提取出干净的文本层扫描件就需要 OCR 前置表格类内容建议保留结构信息比如转成 Markdown 表格后再切分纯文本拍平容易把单元格关系丢掉。加载完还没完要顺带洗数据去掉页眉页脚、统一换行符、过滤空段落。这一步不做后面切出来的 chunk 会带着大量噪声。切分是离线阶段最该花心思的环节。常用的方式是递归字符切分器RecursiveCharacterTextSplitter它先从段落级分隔符往下切切不干净再逐级细化到句子这样能尽量避免把语义完整的段落拦腰截断。切分要考虑两个核心参数chunk_size 和 chunk_overlap。chunk_size 决定每一块有多大chunk_overlap 让相邻块之间保留重合区域防止一句话正好被切到边界上导致语义断裂。具体取值没有银弹英文资料和中文资料、对话文本和技术文档的最优值都不一样后面第三部分我会给一套我常用的起步参数和调整思路。向量化就是把文本块变成稠密向量。选 Embedding 模型时不要只看排行榜分数要看你语料的语言分布和领域词是否在它的训练范围里。通用场景可以直接用商业 API数据敏感的场景建议用可私有化部署的本地模型。向量入库后就完成了所谓“向量数据库”真正要做的事情是相似度检索而不是存储本身选型时可以按数据量、并发量、是否要混合检索来判断不要盲目上重组件。2.2 在线检索召回、重排、合成在线检索是“走管道”的阶段。用户问题进来先把问题用同一个 Embedding 模型编码成向量然后在向量库里做相似度搜索取回 top-k 个文本块。这一步的关键在于检索用的 Embedding 模型必须和索引阶段完全一致否则查询向量和文档向量不在同一个语义空间里召回质量会直接崩掉。这个低级错误我见过不止一次值得先提。向量召回只是初筛。真实业务里向量相似度和“真的有用”之间并不完全等价一段文本可能关键词重合度很高但不是用户真正关心的答案也可能一句话有歧义向量层面把它带偏了。所以召回之后通常还需要重排Rerank。重排是拿一个更精细的模型把查询和候选文档两两打分重新排序后再取 top-n 进入上下文。重排的成本比向量检索高所以实践上都是“向量粗召回一批重排精筛一小批”两层漏斗配合使用。最后一步是合成。把选出来的文本块拼成 context和用户问题一起组装进 Prompt交给生成模型作答。这里有个细节容易忽视拼 context 的顺序、每个块前标注来源、以及“找不到就直说”的指令都会显著影响生成质量。不要把所有块一股脑倒进去适当截断最不相关的尾部反而能降低模型被噪声带偏的概率。2.3 关键参数到底怎么定参数调优是 RAG 最磨人的地方也是效果差异最大的来源。我把最常用的参数列成一个速查表再逐个解释背后的取舍逻辑参数作用我常用的起步值调整思路chunk_size单个文本块大小500-800 字符语料本身句子长、段落完整可以偏大口语化碎片文本用偏小值chunk_overlap相邻块重叠大小50-200 字符取决于是否常出现跨块语义断裂断裂多就加大top_k向量召回数量20-30取决于库里文档噪声比例噪声高先加大再配合重排top_n重排后进入上下文的块数3-5上下文窗口小就减小但不能小于能覆盖答案的块数similarity_threshold相似度过滤阈值不设或设很低设太高容易漏召回设太低等于没设先看分布再定chunk_size 是最容易让人纠结的参数。设大了一个块包含的信息完整但噪声多、向量被稀释相似度区分度下降设小了语义片段干净、检索精准但跨句信息被切碎模型可能看不到完整的上下文。我的经验是先用 800 字符跑一版然后拿一批真实问题去测 hit rate如果发现很多答案分布在两个相邻 chunk 里就调大 size 或者调大 overlap如果出现“一个块里混了好几个主题导致检索不准”就调小。top_k 和 top_n 的区别很多人搞混前者是初召回的数量后者是最终进 Prompt 的数量。两者之间隔着重排器。如果没加重排器建议直接把 top_k 和 top_n 设成同一个值别给自己制造一种“好像筛过了”的错觉。加了重排器初召回可以放宽到 30 左右重排后再砍到 3 到 5效果会比单纯调向量相似度阈值稳定得多。3. 动手搭建一个最小可用的 RAG 管道3.1 工具选型原型阶段我为什么选 LangChain FAISS工具选型这事我建议按“原型速度优先生产环境再换重组件”的思路来。原型阶段我用的是 LangChain 做调度编排、FAISS 做向量库、OpenAI 或本地 Embedding 模型做编码。选 LangChain 不是因为它在生产环境一定最优秀而是它把文档加载、切分、向量存储、QA 链这些样板代码都封装好了能用最短时间跑通整条管道方便你理解流程。FAISS 则胜在轻量单机文件级存储不需要额外起服务非常适合调试和验证。如果你在 Java 技术栈里做 AgentLangChain4j 和 Spring AI 都是值得关注的方案它们对 RAG 的抽象思路和 LangChain 类似但不建议同时学好几套先把一条链路跑通再说。向量库以后要换 Qdrant、Milvus 还是 Elasticsearch在 LangChain 里基本只需要替换两层接口这属于低成本切换不用在原型阶段纠结。还有一点建议数据量小于几十万条文本块时别急着上分布式向量库。单机的 FAISS、Chroma 完全够用先把检索质量调好。检索质量不好换来再高性能的存储也是白搭。等到你的命中率测试稳定了、数据量上来了再迁移不迟。3.2 一份可以直接跑通的基础代码这里给一份我常用的最小实现采用离线索引加在线检索分开的结构。先看离线索引部分from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS # 1. 加载把 docs 目录下所有 PDF 读进来 loader DirectoryLoader( ./docs, glob**/*.pdf, loader_clsPyPDFLoader, ) docs loader.load() # 2. 切分按段落优先中文场景把句号分号也纳入分隔符 splitter RecursiveCharacterTextSplitter( chunk_size800, chunk_overlap150, separators[\n\n, \n, 。, , , ], ) chunks splitter.split_documents(docs) print(f加载 {len(docs)} 个文档切分为 {len(chunks)} 个块) # 3. 嵌入 入库用同一个 embedding 模型做向量化写入 FAISS embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vectorstore FAISS.from_documents(chunks, embeddings) vectorstore.save_local(./faiss_index)在线检索和问答部分from langchain_openai import ChatOpenAI from langchain_community.vectorstores import FAISS from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate # 4. 加载索引构建检索器 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vectorstore FAISS.load_local( ./faiss_index, embeddings, allow_dangerous_deserializationTrue, ) retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4}, ) # 5. 组装 Prompt强调“有依据、不编造、无资料要明说” template 你是企业内部知识助手。请严格根据以下资料回答问题。 资料 {context} 问题{question} 如果资料中没有答案请直接回答“资料库中没有相关信息”不要编造。 prompt PromptTemplate(templatetemplate, input_variables[context, question]) # 6. QA 链stuff 模式适合块数少的场景块多了换 map_reduce 或 refine llm ChatOpenAI(modelgpt-4o-mini, temperature0) qa RetrievalQA.from_chain_type( llmllm, retrieverretriever, chain_typestuff, return_source_documentsTrue, ) result qa.invoke(我们的报销流程是什么) print(result[result]) for doc in result[source_documents]: print(来源:, doc.metadata.get(source))这份代码的核心思路是“先建索引、后查索引”两者拆开跑。第一次执行先把所有文档灌进 FAISS之后每次问答都不需要重新加载原始文档直接走第 4 到第 6 步。注意第 5 步的 Prompt 设计我在生成阶段专门加了“资料中没有就直说”的约束这个约束比很多复杂调参都管用能明显减少一本正经的编造。需要提醒一个坑FAISS 本地序列化加载时LangChain 出于安全考虑要求显式声明allow_dangerous_deserializationTrue。如果加载的是你自己生成的索引文件这个开关可以用但如果索引文件来源不可信一定要改用FAISS.load_local之外的安全校验方案不要盲目信任外部文件。3.3 检索效果怎么评hit rate 与 MRR 计算很多项目跑通了 RAG 之后凭感觉说“效果还行”这是最危险的状态。检索质量必须用指标量化。我最常用的两个指标是 hit rate命中率和 MRR平均倒数排名。hit rate 衡量的是对于一组问题答案所在的文档块是否出现在召回结果的前 k 名里。MRR 则更严格一些它看正确答案排在第几位排名越靠前得分越高。一份可用的评测代码是这样def hit_rate(questions, retriever, k3): hits 0 for question, gold_content in questions: docs retriever.invoke(question)[:k] if any(gold_content in doc.page_content for doc in docs): hits 1 return hits / len(questions) def mrr(questions, retriever, k3): total 0.0 for question, gold_content in questions: docs retriever.invoke(question)[:k] rank next( (i 1 for i, doc in enumerate(docs) if gold_content in doc.page_content), None, ) total 1.0 / rank if rank else 0.0 return total / len(questions)这里的questions是一组手工标注的测试集每个元素包含一个问题和一个“黄金答案片段”。注意黄金片段不能是一整篇文档而是你期望被召回的具体句子或段落这样才能真正反映检索器有没有把最相关的那段捞出来。测试集至少准备二三十个问题覆盖不同提问方式和知识点的分散程度否则指标好看没有意义。我第一次搭完管道时hit rate 只有四成左右原因就是测试问题里包含很多口语化问法比如“报销要怎么做”和文档里的“报销操作流程”字面差异很大纯向量召回很难对齐。后来在检索前加了查询改写hit rate 立刻涨到七成以上。这正好引出下一部分基础 RAG 的天花板在哪以及如何往上走。4. 从“基础 RAG”到“Agentic RAG”的升级路径4.1 基础 RAG 的三个典型瓶颈基础 RAG 能解决“单轮问题、资料结构规整、知识相对静态”的场景但一旦遇到真实业务三个瓶颈就会冒出来。第一个瓶颈是查询表达与知识表达不一致。用户问“这个月我还能请几天假”资料里写的是“年度带薪年假按自然年计算每月累计不超过 2 天”。两者词汇完全不同向量相似度不高召回自然失败。基础 RAG 不会去理解用户意图只会傻傻地把问题原样转成向量去匹配。第二个瓶颈是信息碎片化。答案往往分散在多个文档甚至多个知识库里比如“某客户的历史采购记录”在 ERP 系统“他的合同条款”在另一个系统单跳检索只能拿到其中一半。第三个瓶颈是知识时效与多轮上下文。基础 RAG 每次检索都是独立的用户第一句问“上个月销售额”第二句说“和它去年同期比呢”如果第二句不结合第一句做改写检索器根本不知道“它”指什么。这也就是为什么社区里开始出现“Agentic RAG”的概念它不是某个新算法而是把 Agent 的规划能力应用到检索流程上让模型参与“决定怎么查、查什么、查完怎么用”的决策。个人理解这里是 RAG 和 AI Agent 结合最有价值的交界点。4.2 查询改写、HyDE 与多路召回针对第一个瓶颈最直接的升级是查询改写。做法很简单在检索之前先用一个轻量模型把用户原始问题转成更适合检索的形式。比如用户问“我还能休几天”改写器输出“查询员工剩余年假天数及扣除规则”用户在多轮对话里说“它呢”改写器结合上文补全为完整问题。别小看这一步它相当于把“用户表达空间”映射到“文档表达空间”通常能把 hit rate 提升十个百分点以上。更进一层的做法是 HyDE。思路是先让模型针对用户问题生成一个假想的答案文档然后拿这个假想文档去做向量检索而不是拿原始问题去检索。原理是问题往往很短、信息量少而假想答案的语义更接近目标文档检索匹配更准。HyDE 不适合所有场景但它对“用户问得简洁、资料写得详细”的场景非常有效值得放进工具箱。多路召回解决的是“一种检索方式不够”的问题。最常见的是“向量召回 关键词召回”混合向量负责语义相似BM25 负责字面/专业名词匹配两边结果合并去重后再重排。也可以做多知识库分路召回比如针对工单库和报价库分别检索再把结果汇总。多路召回的核心不是路数多而是每路之间确实有互补性否则只是增加延迟和噪声。4.3 Agent 化检索让模型决定“何时查、怎么查、怎么信”到了这一步RAG 就不再是一条被动的“查了之后生成”的管道了而是变成 Agent 手里的一个工具。Agent 会根据问题类型决定是先查知识库再回答还是先调用别的工具拿到上下文再查知识库甚至是多次检索、每轮检索结果逐步缩小范围。这就是我在前面提到的显式工具形态。具体实现里你可以把knowledge_search封装成 Agent 的一个 function calling 工具。模型收到用户问题后如果判断“这个问题需要私域资料支持”就会调用这个工具并传入检索关键词如果判断“这是通用常识或数学计算”就直接回答不调用检索。这个“知道什么时候不检索”的能力往往比检索本身更值钱因为它大幅减少了不必要的延迟和上下文污染。再往上就是多跳检索multi-hop RAG。比如用户问“对比一下 A 客户和 B 客户的项目周期”Agent 先检索引出 A 客户的合同信息再根据结果构建第二跳查询去找 B 客户最后把两者结果汇总对比。这个过程中GraphRAG 和知识图谱类方案也会登场通过实体关系图把分散的信息片段连接起来让多跳路径更清晰。但要提醒的是GraphRAG 的构建成本远高于普通 RAG起步阶段先把查询改写、重排、混合检索做好性价比要高出很多不要一上来就奔着最复杂的方案去。5. 常见问题与排查技巧实录5.1 三种高频故障的定位思路跑 RAG 最常遇到的故障是“召回不到答案”。定位时先别急着调模型按层排查先确认测试问题对应的“黄金片段”是否真的在原始文档里然后单独打印检索器返回的前几块手工判断这些块和问题是否语义相关再确认 Embedding 模型是否前后一致。如果检索结果里确实有相关内容但答案还是不对问题多半出在生成阶段比如 Prompt 里没约束“只能根据资料回答”模型跑偏了。第二个故障是“召回结果噪声太多”。表现为检索回来的块里混着大量无关内容污染了上下文。这时要检查 chunk_size 是不是设太大导致一个块里装了好几个主题或者相似度阈值太低把低相关也捞进来了。解决方法是调小 chunk、提高重排门槛、严格限制进入 Prompt 的 top_n。第三个故障是“上下文溢出或截断”。在线阶段拼进去的块太多token 超出限制框架默默截断了后面的内容而正确答案恰好被截掉。排查方法是打开 Debug 日志看进入 Prompt 的文本块列表以及每个块的长度。解决方法是减少 top_n、在切分阶段把 chunk_size 调小或者换用 map_reduce 之类的链式处理方式。5.2 我踩过的几个真坑第一个坑是表格资料直接拍平。我最早处理公司的 Excel 报价单时直接把每一行文本连起来切块结果检索时召回的是零散单元格根本拼不出完整产品信息。后来我把表格转成 Markdown 表格的形式保留表头和结构切分时再按表结构保留效果立刻不一样。遇到表格时先想“尽量保住结构信息”别轻易拍平成纯文本。第二个坑是切分器把 Markdown 标题和章节结构切碎了。技术文档通常很长一个主标题下面有若干副标题递归切分器如果没把 Markdown 标题符加入分隔符就会把章节标题和正文内容离散成两块。后来我把分隔符调成[\n\n, \n, ## , # , 。, ]并尽量让每个 chunk 从章节标题开始检索命中率明显提升。经验是切分的边界最好天然对齐文档自身的语义边界而不是对齐字符数。第三个坑是只测了离线索引没测在线检索。很多人的流程是“索引跑完然后问一个问题觉得还行”就完事了。这远远不够。没有标注测试集你就无法知道昨天的调参到底进步了还是退步了。我后来养成了一个习惯每次改动索引或检索逻辑都先重跑一遍 hit rate 和 MRR用数据说话。5.3 避坑速查表把常见现象和对应处理整理成一张表方便排查时直接对照现象可能原因优先排查项检索结果和问题完全不相关Embedding 模型前后不一致检查索引和查询是否用的是同一 Embedding答案对但引用来源不对上下文过短答案跨了多个块调大 chunk_overlap 或 chunk_size答案明显编造Prompt 没有约束“仅根据资料回答”在生成 Prompt 里加反幻觉约束上下文被截断导致答案丢进入 Prompt 的块太多或太长减小 top_n调小 chunk_size检索命中但生成质量差噪声块污染上下文加重排器或提高相似度阈值专业名词查不到向量模型不熟悉领域术语混合 BM25 关键词召回这张表只覆盖了高频问题。更复杂的场景比如同名文档、版本覆盖、权限隔离导致的知识冲突已经不是基础 RAG 能解决的需要在管道上游加文档管理、版本控制和权限过滤。这些话题后面可以单独写一篇但前提是先把这个基础管道调到稳定。最后分享一个我自己的习惯每搭一条 RAG 管道都会先用手工标注的二十个问题压一遍最坏情况而不是拿几个“设计得好”的例子自嗨。RAG 的难点从来不在接口调用而在你是否真正理解自己的语料结构。资料是按章节组织还是按卡片组织、是长段落还是短句、有没有大量重复和过期内容这些都直接决定参数怎么调。你在调试上花的时间有一大半会花在“读懂你的知识库”而不是“改模型”。把这一步做好后续往 Agentic RAG、GraphRAG 升级时你会少走很多弯路。
阅读完成 · 觉得有帮助?