从零搭建 RAG 管道:让 Agent 拥有可靠的外部知识获取能力
1. 为什么 Agent 光靠大模型“内置知识”不够先把 LLM 和 Agent 的关系说清这段时间后台收到的高频问题里有一个特别有意思“DeepSeek 到底算大模型还是算 Agent我拿它写提示词是不是就等于在搭 Agent”我的回答通常很短DeepSeek、Qwen 这些产品本质上都是 LLM它们负责“理解语言、生成语言”而 Agent 是构建在 LLM 之上的一个完整闭环包含目标拆解、工具调用、记忆管理、自我纠错等动作。你拿 LangChain 写了几行链式调用严格来说还是在“用 LLM API”离真正的 Agent 还差一个决策循环。RAG 在这套体系里的角色就是给 Agent 装上“外部知识获取”这根管子——让它在回答问题时不是只靠模型参数里存的旧知识而是能主动翻资料。把这个问题拆开还有个好处很多同学纠结“我到底应该学 RAG 还是 Agent”其实这两件事根本不在同一层。RAG 是一个能力组件Agent 是一个系统架构。Agent 可以用 RAG 来补知识也可以不用但每一个需要回答业务问题的 Agent最终都绕不开“知识从哪来”这个坎。这就是我把这一篇定位成“知识获取管道”的原因——它是 Agent 工程化落地时最实在、最基础的一个模块。1.1 大模型的三块天生短板先看 LLM 自己有哪些解决不了的事。第一知识是静态的。模型训练好之后它的参数就冻结了。你问它公司上个月刚调整的产品价格它大概率会给你编一个合理但错误的数字。预训练数据截止时间决定了它的知识上限任何在这个时间之后发生的事情它都不知道。第二幻觉是无解的。它不是“偶尔犯错”而是在面对不确定信息时会用概率最高的词“填满”答案。说得难听一点你问它一个它没见过的型号参数它宁可编一个也不愿意承认不会。第三私有数据它是完全接触不到的。你的个人笔记、公司 ERP 里的库存数据、售后工单、合同条款这些东西根本不在公开训练集里模型再大也没用。这三个短板加起来正好指向同一个问题的三个层面知识不是“永远在线”的。而 Agent 区别于普通聊天机器人的核心价值恰恰是要处理具体的、动态的、私有的任务。所以一个 Agent 光接一个 LLM API 是不够的它还需要一条能够持续从外部取知识的管道。1.2 补知识的两种主流思路微调与 RAG知识缺失的解决方案行业内基本就两条路。一条是微调Fine-tuning把知识直接写进模型权重。听起来很优雅但实操时会遇到一系列问题标注数据从哪来、一次微调要花多少训练资源、知识更新后要不要重新训、会不会把模型原本的能力“学坏”。我见过不少团队把微调当万能药最后光维护训练数据就累得够呛。另一条就是 RAGRetrieval-Augmented Generation检索增强生成把知识放在模型外部回答问题时先检索相关内容再把检索结果拼进上下文提示词让模型基于这些材料作答。用一个不太严谨但很好懂的类比微调相当于考前熬夜把整本教材背进脑子里RAG 则是带着参考书进考场遇到哪题翻哪页。对 Agent 来说知识更新是常态业务数据是动态的RAG 这种“按需取用”的方式明显更划算成本更低、更新更快、还能给答案标来源。这篇后面讲的整个管道就是围绕“如何让这套开卷考试机制跑得又快又准”展开的。1.3 Agent 的知识来源其实是一个组合件顺便把 Agent 的知识结构梳理一下否则容易把 RAG 当成唯一知识来源。Agent 的知识大致分四类参数记忆也就是 LLM 权重里存的语言知识和常识工作记忆就是当前对话上下文几轮内的事长时记忆指外部存储包括向量数据库、普通数据库、知识图谱工具调用结果比如调 API 查到的实时天气、库存数量、订单状态。RAG 管的是长时记忆里的“显性知识”尤其是非结构化文档——PDF、Markdown、手册、邮件、聊天记录。它不适合管那些强结构化、需要精确计算的场景那种场景交给 SQL 查询或 API 调用更可靠。所以你看RAG 不是“包治百病”而是 Agent 知识体系里的一块重要拼图。理解了这块拼图的边界下面再讲实现细节你就能明白为什么要这样设计。2. RAG 的完整工作链路一次查询从入库到出答案中间到底发生了什么RAG 不是一个单独算法而是一条管道。很多人一上来就写代码结果检索不准也不知道该调哪里。先把链条画清楚后面所有调优都会变得有方向。整条链路可以分成三阶段索引Indexing、检索Retrieval、生成Generation。2.1 索引阶段把杂乱文档变成可查询的向量索引阶段解决的是“让机器提前知道有哪些知识”。这个过程通常有四道工序第一是文档加载与清洗。你的原始文档可能是 PDF、Word、Markdown、HTML里面掺杂页眉、页脚、表格、图片说明、乱码换行。加载之后不能直接拿去切块要先做清洗把和正文无关的内容去掉。这一步看起来不起眼实际影响非常大你后面会觉得“这答案怎么一股页码味儿”多半就是清洗没做到位。第二是切块Chunking。长文档不可能整篇向量化——一方面 embedding 模型有输入长度上限另一方面检索时粒度过大相似度会被稀释。切块就是把长文档切成一段一段语义相对完整的片段。切多长、按什么边界切是 RAG 调优里最关键的活儿我后面专门用一小节来讲。第三是向量化Embedding。把每个文本块用 embedding 模型转成一个固定维度的向量比如 1024 维。这个向量的含义是“这段文本在语义空间里的位置”语义相近的文本向量距离也应该相近。这个环节的质量直接决定了“系统能不能理解相似”。第四是入库。向量库Vector Database负责存储这些向量并提供按相似度检索的能力。Chroma、FAISS、Milvus、pgvector、Elasticsearch 都是常见选择各有各的适用场景。索引阶段做完你的知识就变成了一份“可查询的语义地图”。2.2 检索阶段把用户问题变成一次“语义搜索”当用户问出“A500 最大功率是多少”时系统先把这个 query 用同一个 embedding 模型转成向量然后在向量库里计算它和所有文档块的相似度按分数从高到低取 top-k 个候选片段。这一步的核心是“召回”目标不是给答案而是把最相关的材料捞上来。很多新手会在这里犯一个认知错误认为向量检索是全能的。实际上向量检索擅长“语义相近”但不擅长“关键词精确命中”。比如产品型号有特殊符号或者文档里用“峰值功率”而你问“最大出力”纯向量检索就可能漏。所以线上环境通常会做混合检索向量检索加上 BM25 这类关键词检索再把两路结果合并排序。先别急着上一堆复杂配置等你把基础管道跑通之后再根据失败案例决定要不要加这一层。2.3 生成阶段让模型只看资料说话检索出来的 top-k 片段会被拼装进提示词作为参考上下文。同时 System Prompt 里通常要写死一条规则只能依据提供的资料回答资料里没有就要明确说“不知道”而不是自由发挥。然后把用户问题一起交给 LLM生成最终答案。这里有个细节值得留意检索出的片段可能互相矛盾也可能都不相关LLM 得有能力“拒绝回答”。所以提示词不能只写“请回答”要写“请先判断材料是否足以回答再组织语言”。一个好的生成阶段还应该在答案后面附上来源片段编号或文档位置方便用户核对。这个习惯在 Agent 场景里尤其重要——Agent 一旦把查询结果回传给用户用户无法验证对错来源就变成了信任基础。为了让你更直观地感受整条链路随便举一个例子假设库里有一份产品手册文本块是“A500 系列最大输出功率为 1200W适配电压 220V”。用户 query 是“A500 能带多大功率的电器”系统检索时发现语义相近召回这个片段LLM 基于它生成“A500 系列最大输出功率为 1200W”。你会发现答案里的信息全部来自外部资料模型只是负责组织和润色。这就是 RAG 的本质。3. 从0到1搭一个最小可复现的 RAG 管道代码与验证理论基础说完了下面动手。我不会给你一个非常重的企业级架构而是先搭一个最小可复现的管道让你亲手把整条链路跑通。逻辑通了后面换组件、加功能都是水到渠成的事。3.1 技术选型先用最轻的组件跑通RAG 相关的组件选型非常多新手特别容易被工具绕晕。我先给出一套适合“本地练手”的组合再给一张替换对照表。环节轻量入门方案生产环境可选方案文档加载TextLoader 读 Markdown/文本pdfplumber、Unstructured、OCR 服务文本切块RecursiveCharacterTextSplitter按 Markdown 标题结构切、语义切块向量化HuggingFaceEmbeddings 本地模型公司自建 embedding 服务、领域微调模型向量库Chroma纯本地、零部署Milvus、pgvector、Elasticsearch生成模型DeepSeek / Qwen 兼容接口按预算、合规和延迟需求选择这套组合的好处是除了模型调用之外其他环节全都落在本地不需要额外搭建服务。Chroma 一条命令就能持久化到本地目录特别适合先把原理跑明白。3.2 核心代码流程我这里以 Python LangChain 生态为例。如果你在 Java 技术栈LangChain4j 或 Spring AI 也有对应的 RAG 模块核心概念完全一致只是 API 不同。from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate # 1. 加载文档 loader TextLoader(./product_manual.md) raw_docs loader.load() # 2. 切块块大小 500重叠 80优先按段落切 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , ] ) docs splitter.split_documents(raw_docs) # 3. 向量化并入库 embedding HuggingFaceEmbeddings(model_nameBAAI/bge-m3) vectorstore Chroma.from_documents( docs, embeddingembedding, persist_directory./chroma_db ) # 4. 检索返回最相关的 4 个片段 retriever vectorstore.as_retriever(search_kwargs{k: 4}) query A500 最大功率是多少 hits retriever.invoke(query) # 5. 把检索结果拼成上下文交给大模型生成 context_text \n\n.join( f[片段{i1}] {doc.page_content}\n(来源: {doc.metadata.get(source, 未知)}) for i, doc in enumerate(hits) ) prompt ChatPromptTemplate.from_messages([ (system, 你只能依据以下资料回答问题资料里没有的信息要明确说不知道。\n\n{context}), (human, {question}) ]) llm ChatOpenAI( modeldeepseek-chat, api_key你手头的模型服务密钥, base_url你的模型服务地址 ) chain prompt | llm answer chain.invoke({context: context_text, question: query}) print(answer)这段代码里前面四步基本上就是索引和检索的全部内容第五步才是生成。这里有个容易被忽略的点不同版本 LangChain 的包名经常调整比如新版把 HuggingFaceEmbeddings 挪到了 langchain_huggingface 里。遇到 ImportError 不要慌先看你安装的版本然后按报错信息找对应包名。代码这种东西能跑通比“写得优雅”重要。3.3 怎么判断管道真的跑通了跑通代码不算完你要用几个测试问题来验证效果。我的习惯是准备三组问题第一组是“文档里原话能直接命中”的问题比如“A500 最大功率是多少”第二组是“文档里换了种说法”的问题比如“这个型号能带多大负载”第三组是“文档里根本没有答案”的问题比如“这个产品的研发团队有谁”。前两组看检索和生成是否正常第三组看它是否老老实实说不知道。如果第三组问题它开始编答案说明你的 System Prompt 约束还不够硬。如果第一组能命中但第二组召不回说明 embedding 或切块策略还没到位。我建议你把每次检索返回的片段先打印出来看一遍而不是只看最终答案。这一步能帮你快速判断问题出在检索环节还是生成环节调优就有一半的把握了。4. 检索质量的四个命门切块、Embedding、召回策略和重排序同样一份文档有人搭出来的 RAG 准确率很高有人搭出来像人工智障差别往往不在代码而在四个关键抉择。这四件事决定了“检索到底能不能捞到正确答案”也决定了生成质量的上限。4.1 切块块的大小和边界决定召回上限切块是整个管道里最“微妙”的环节。块太小一个语义单元被截断比如“A500 最大功率为 1200W适配电压 220V”被切成两半检索时各自语义都不完整块太大一个片段里混了很多主题向量表达被稀释相似度也不准。我的经验参数是一般文档从 300 到 800 字符起步重叠区设 10% 到 20%。切分边界优先用自然段落“\n\n” 或句号、问号这些语义断点。对于有层级结构的 Markdown 或 HTML按标题切块往往比纯字符数切更合理因为标题天然定义了一个语义区块。结构化表格就更特殊普通切块会把一行表格切开导致语义错乱最好单独做表格解析。切块没有一个“最正确”的参数只有“最适合你文档”的参数。你把文档类型、块大小、重叠度、检索结果质量放到一起对比调几轮就有手感了。4.2 Embedding 模型决定相似度是否可信Embedding 模型决定了“语义相似”这件事靠不靠谱。通用模型在普通文本上表现不错但遇到专业术语、产品型号、中英文混杂时相似度计算往往会失灵。比如“R200”和“R2OO”人眼一眼看出是同一批货embedding 可能完全分不出来。选型时重点看三个方面中文效果是否达标、支持的最大输入长度、向量维度带来的存储和计算成本。开源方案里像 BAAI/bge-m3 这类模型在中文场景表现比较稳定而且支持本地运行不依赖外部接口。如果你的语料有很强的领域属性后续可以考虑用领域数据微调一个专用 embedding 模型。不过我建议不要一上来就折腾 embedding 替换它属于“底层换血”影响面最大、成本最高。先把切块、召回、重排这几种更便宜的手段用尽还不够再考虑换 embedding。4.3 召回策略与重排序让正确答案浮出水面单纯用向量检索召回 top-4遇到复杂问题很容易把相关片段漏掉。改进方向有两个一是混合召回二是重排序。混合召回就是向量检索和关键词检索并行然后把两路结果做合并去重。这套方案特别适合“产品型号、部门缩写、专业代码”这类场景因为这些词往往是精确匹配比语义匹配更可靠。重排序则是把召回范围先放大比如先取 top-50再用一个重排模型Reranker对这些候选做精细打分挑出真正相关的 top-5 送给 LLM。重排模型和 embedding 模型不同它是“给定问题和片段直接预测相关性”准确性通常更高代价是多一次模型推理。从工程成本看重排序是性价比极高的优化项。很多RAG项目在加了重排之后答案可用率明显提升。但要记住重排也不能拯救极度糟糕的召回——如果前 50 个候选里根本没有正确答案重排再准也没用。4.4 查询改写把用户口语变成可检索的 query最后一个命门最简单也最容易被忽略用户问的问题往往不适合直接拿去检索。举个例子用户问“那这个型号能带多大功率呢”这个 query 语义不完整“这个型号”指代不明。直接向量化检索效果自然差。Agent 场景里更常见多轮对话之后用户说“它呢”系统若无其事地拿“它”去做检索。正确处理办法是在检索之前加一个 Query Rewrite 步骤让 LLM 先把用户口语结合对话历史改写成独立的、完整的检索 query比如“A500 系列在 220V 电压下的最大输出功率是多少”然后再进入检索环节。这一步不复杂但对检索质量的影响是立竿见影的。5. 我实际踩过的五个坑检索不到、上下文拼接失控、多轮对话失效理论看着都清楚一进实战还是会翻车。下面这几个坑是我在不同项目里实打实遇到过的每个都附上排查思路你以后遇到可以直接照着走。5.1 明明文档里有答案却检索不到第一次跑 RAG 时最容易崩溃的场景我把答案写在文档里了问它它就是答不上来。排查链路基本是这样先把检索返回的 top-k 片段全部打印出来肉眼确认相关片段在不在里面。如果片段里没有说明召回阶段就没捞到如果片段里有但答案不对问题在生成阶段。召回阶段捞不到的常见原因切块把完整语义切断了文档里用的是“峰值功率”问题里用的是“最大功率”embedding 没把它们关联起来特殊符号“R200-A”被切块切成两截或者是纯向量检索对关键词不敏感。解决办法就是前面讲的调切块边界、上混合检索、加查询改写。我的经验是先加混合检索往往能解决一大半问题因为它能在语义检索失效时兜底。5.2 材料检索对了生成结果还是答非所问有一种更隐蔽的情况检索返回的片段确实是相关的但 LLM 生成时还是乱编。我在一个项目里遇到过System Prompt 写的是“请基于以下资料回答”没写负面约束模型就自由发挥补齐了很多资料里没有的内容。解决这件事分两手。第一手System Prompt 要写得很硬只允许使用资料中的信息资料不足以回答时明确回复“资料中未找到相关信息”不得推测、不得编造。第二手把检索片段编号并在生成时要求模型引用例如“根据片段3的信息A500 最大功率为 1200W”。这样即使答案出错了你也知道它是从哪个片段推理出来的定位问题会快很多。5.3 多轮对话里的指代问题Agent 和普通问答的最大区别就是多轮对话。用户说“那个呢”、“价格呢”、“和上一款比呢”这些“那个”“上一款”如果不处理检索出来的结果全是噪声。我踩过的坑是把对话历史原封不动也塞进上下文让 LLM 自己“感受”指代关系。结果模型确实能感受但在检索阶段query 还是“那个呢”向量检索完全找不到北。正确做法还是在进入检索之前先做查询改写把整段对话历史喂给 LLM让它生成一个不依赖上下文的独立 query。这一步可以在 Agent 里做成一个小 skill每次调用 RAG 工具前自动执行。5.4 增量更新与旧版本数据污染当一个 RAG 系统上线后文档会持续更新。最粗鲁的做法是每次全量重建索引——数据量小的时候问题不大但积累到几百万片段时全量重建的成本和耗时都不可接受。更现实的做法是增量更新新增文档直接加进去修改后的文档用新的 chunk 覆盖旧的删除的文档要主动清理。向量数据库基本都支持按 ID 删除和更新但前提是你在入库时就给每个片段规划好稳定的文档 ID 和版本号。我在实际项目里吃过一次亏旧版产品手册入库后没有及时清理结果同一个问题同时检索到新旧两版参数模型直接混乱。后来我加了一张“文档版本表”每次更新都记录版本号和生效时间这个问题才彻底解决。5.5 把相似度阈值当成万能参数很多教程会告诉你“设置相似度阈值 0.7低于这个分就判定为无关”。但我要泼一盆冷水embedding 相似度的分布和模型、文档类型、切块方式都有关系0.7 在一个系统里是好阈值在另一个系统里可能把所有正确答案全部过滤掉。我的建议是不要把相似度阈值当成固定的安全网而是把它当成最后一道防线。先用 top-k 召回再用人工抽查返回片段来感受分数的分布最后根据你业务能接受的“漏报率”来定阈值。上线后还要持续监控因为文档更新后分数分布可能会漂移。6. 从基础 RAG 到 Agentic RAG知识获取管道还会怎么演进把基础 RAG 跑通之后你会发现它本质上是一条“固定管道”——用户一提问就检索一次生成一次。但 Agent 场景里知识获取往往需要更灵活的姿态。这一节聊几个和热词密切相关的话题RAG 和 MCP 的区别、Agentic RAG 是什么以及还能往哪个方向练手。6.1 RAG 与 MCP知识管道和工具协议的分工最近问“RAG 和 MCP 有什么区别”的人特别多因为这俩都跟“让模型获取外部信息”有关。我的理解是RAG 解决的是“知识如何进入上下文”核心是检索MCPModel Context Protocol解决的是“模型如何标准化地调用外部工具和数据源”核心是协议。RAG 关心的对象通常是文档非结构化的知识比如 PDF、手册、笔记MCP 关心的对象通常是服务和数据源比如数据库、ERP 系统、内部 API、外部平台。两者不是替代关系而是互补关系。你完全可以把一个 RAG 服务封装成 MCP 工具让 Agent 通过 MCP 协议按需调用这个知识管道。在 Agent 系统里RAG 是“知识存储与检索层”MCP 是“工具接入层”它们各司其职。6.2 Agentic RAG把“要不要查、怎么查”交给 Agent 判断传统 RAG 的问题是“被动”不管问题需不需要外部知识它都会检索一轮检索结果不好也不会重试。Agentic RAG 则把知识获取变成一个决策循环Agent 自己决定要不要检索、去哪个知识库检索、当前检索结果够不够、要不要换个 query 再试一次甚至多轮检索后把不同来源的信息做交叉验证。举个例子。用户问“我们上个月的退货率为什么升高”传统 RAG 可能直接检索文档给了个泛泛答案。Agentic RAG 的做法是先生成一个问题推理判断“退货率”这个数字在文档里没有可能需要查数据系统于是 Agent 决定调用一个数据查询工具拿到数字后再结合知识库里的售后政策文档综合分析后才给出结论。这种“检索只是其中一个步骤”的形态才是 Agent 场景下 RAG 的完整形态。但是要提醒一句不要一上来就追新概念。Agentic RAG 的前提是基础 RAG 已经稳定可靠否则 Agent 只是在更花哨地犯错。6.3 几个可以直接上手的练手方向如果你想把这套知识继续深化我列几个我亲测有学习价值的方向个人笔记知识库问答把你日常写的 Markdown 笔记全部入库做一个能按主题检索、按笔记生成回答的小助手。这是成本最低、反馈最直接的练手项目。本地 ERP RAG LLM 产品检索很多企业内部都有 ERP 系统里面有产品主数据、库存、价格。你可以先把产品说明文档做成 RAG再把结构化数据通过 MCP 工具暴露给 Agent做一个“既能回答参数、又能查实时库存”的助手。结合 Agent Skill把“检索知识库”封装成一个可复用的 Skill让 Agent 在多步任务中按需调用。这个方向能帮你理解 RAG 从“管道”变“组件”的过程。结构化知识与 Ontology RAG普通 RAG 把文档看成一段段独立的文本但很多知识是有关系网络的。引入知识图谱或本体Ontology后检索可以沿着实体关系扩展这属于 RAG 的高阶进化方向适合想往深处钻研的同学。我自己的体会是RAG 这套东西真正难的不是代码而是“用户问法、文档形态、检索策略”这三者的匹配。工具会不断变——今天是 LangChain明天可能是新的框架——但只要你能把“索引、检索、生成”这条链路里的每个环节都亲手调过一遍后面任何新框架对你来说都只是换一层皮。先把最小管道跑通再拿你自己的真实文档去折磨它踩坑踩得够多你自然就有手感了。