
从 0 到 1 搭一个 Agent很多人第一反应是“选个大模型”第二反应是“配个提示词”。这些当然重要但真正让 Agent 从“玩具”变成“生产力工具”的往往是知识怎么进去、怎么被拿到。这篇聊的就是这件事——RAG也就是检索增强生成说白了就是给 Agent 外接一个可查询的记忆库。我会用做 Agent 开发的视角把 RAG 拆开揉碎为什么需要它、管道上每一环在解决什么问题、怎么落地一个小但完整的例子以及那些不实际跑几遍根本发现不了的坑。适合刚开始做 RAG、或者已经跑通 demo 但总感觉效果不稳定的读者。不需要你有很强的机器学习背景但最好写过一点 Python能看懂基本代码。1. 为什么 Agent 需要一条知识管道1.1 模型参数不是你想要的“知识”先说一个很多人忽略的事实大模型里存的知识本质上是预训练阶段从公开语料里统计出来的“印象”。它知道 2022 年以前的很多事实但不一定知道你们公司的内部制度、你上周刚更新的产品手册、或者那个只有 Excel 表格里才有的历史报价。更麻烦的是大模型在回答它不确定的问题时会非常自然地“编”一个看起来合理的答案。我见过很多团队踩这个坑老板说“把我们的知识库接入大模型”于是开发同学直接调 API然后把公司文档一股脑塞进上下文窗口。结果上下文超限、费用暴涨、回答还经常串内容。这不是模型笨而是你根本没有给它一条“查找资料”的通道。RAG 解决的就是这件事不是让模型背下所有知识而是让它在需要的时候自己去文档堆里找到相关段落再基于这些段落组织答案。换个生活类比你新入职一家公司问老员工问题。如果老员工凭记忆乱猜那就是“裸模型”如果老员工先查一下内部 wiki、翻一下聊天记录然后根据查到的内容回答你这就是 RAG。它的核心价值是把“死记硬背”变成“按需查阅”。1.2 RAG 不是什么高深魔法而是一条数据管道很多刚接触 RAG 的人以为它就是一个“搜索 提示词”的拼凑。实际上RAG 是一整条管道从文档进来到最后回答出去中间每个环节都会影响结果质量。你可以把它拆成四个阶段文档接入与处理从 PDF、网页、数据库、Wiki 等来源抽取文本清洗、去重、切成小块。向量化与索引把文本块变成向量存进向量数据库建好索引。检索拿到用户问题时先向量化问题再去库里找最相关的文本块。增强生成把检索到的文本块作为上下文连同用户问题一起交给大模型生成回答。这四个阶段每一环都有“看起来能用但其实很糙”的默认做法。比如很多人直接用 PDF 文本抽取结果表格全乱还有人把整个文档切成 500 字的小块切碎了语义。RAG 效果不好往往就坏在这些细节上而不是模型不够强。1.3 在 Agent 场景里RAG 跟传统搜索问答还不一样如果你只做一个“知识库问答机器人”那么 RAG 就是“用户问一句你查一次答一句”这个链路相对简单。但 Agent 的麻烦在于它有对话状态、会调用工具、可能需要多轮推理甚至要在一次任务里多次检索不同资料。举一个我实际做过的例子一个内部运维 Agent用户说“帮我查一下昨晚备份失败的任务然后看看存储空间是不是不够了”。这句话至少要两次检索一次查备份日志一次查存储监控。而且第二次检索要依赖第一次的结果来决定查询词——如果只是机械地把用户原话转成向量去搜索很可能搜不到。所以 Agent 场景下的 RAG 需要“检索决策”也就是让 Agent 自己判断“现在要不要查、查什么、查完怎么用”。这也是最近大家总说的 Agentic RAG 的雏形。后面我会先从最基础的管道讲起把每一步都跑通再回到 Agent 场景说说怎么把 RAG 封装成 Agent 的内部工具。地基一定要扎实因为后面所有“智能”都建立在“能查得对”上面。2. RAG 管道的核心环节拆解2.1 文档接入与清洗管道的入口最容易埋雷绝大多数团队的第一版 RAG用的都是现成的文档比如 Word、PDF、Markdown。但“能读”和“读得对”是两回事。PDF 尤其麻烦很多 PDF 内部是图片格式文字根本抽不出来还有多栏排版、页眉页脚、表格换页这些都会让提取出来的文本顺序错乱。我这里说的清洗不是简单的去空行而是要做几件很实际的事去除页眉页脚和页码避免每段文本里混入“第 3 页”“公司名称”这类噪声。处理表格要么把表格转成 Markdown 表格要么转成“列名: 值”的键值对文本之后再切块。直接按原始布局抽取的表格检索时几乎必乱。去重特别是多份文档互相引用、复制粘贴的情况同一段知识库里存三份检索时会被同一内容刷屏。我用过一款开源文档解析工具叫 unstructured它对 PDF、Docx、HTML 都有现成的 partition 函数能按元素类型输出文本和表格。社区里也有很多人直接用 LangChain 的PyPDFLoader但那个只适合干净的单栏 PDF。如果你们的文档体系比较复杂建议在加载后用一次结构清洗这比你在后面调 embedding 参数省心得多。2.2 切分策略块大小不是拍脑袋决定的把清洗后的文档切成小块是 RAG 里最容易被低估的一步。块太大了向量化后会混入太多无关信息检索精度下降块太小了单个块信息量不足号称命中了但内容回答不了问题。我见过一种很常见的做法固定 512 个字符重叠 50 个字符。听起来很标准但实际跑起来问题很多。比如技术文档里的“前提条件”“注意事项”经常跟上下文分隔很远固定切分会把它们切到不同块里检索时只找到了一半。比较稳妥的策略有两种按结构切分先识别标题层级以章节为单位切块。Markdown 文档可以用MarkdownHeaderTextSplitter它会根据#、##标题保留上下文。按语义切分利用 embedding 判断文本间的语义断点在意思变化处切分。LangChain 的SemanticChunker就是这个思路但计算成本高一些。切分时还需要设一个 overlap让相邻块之间重叠一部分内容。这个小参数很关键它避免“一句话被从中间切断”导致搜索不到。我通常的做法是块大小 500 到 800 字重叠 80 到 120 字。具体数字要根据文档类型调整原则是“一个块要能独立表达一个完整意思而不是机械地数字符”。2.3 向量化与索引Embedding 模型选择要看检索场景Embedding 模型负责把文本变成向量这个环节很多人直接选 OpenAI 的text-embedding-3-small但国内团队做企业知识库时要考虑数据出域和成本问题。如果你用国产大模型生态可以考虑智源bge-m3或者阿里的text-embedding-v3这些在中文语义检索上都不差。选 embedding 模型时不要只看 MTEB 榜单分数。要用你自己的业务文档去测看那些“同义改写”和“专业术语”能不能匹配上。我踩过的一个典型坑是直接用通用 embedding 处理我们内部的运维文档里面全是“网关”“节点”“熔断”这种词。模型一般能理解但遇到“服务雪崩”这种隐喻性说法纯向量检索就抓瞎了。所以后面我会建议你上“混合检索”把关键词匹配和向量检索结合起来。向量库这块轻量场景用 Chroma、FAISS 都行团队协作或者要上生产建议用 Milvus、Qdrant 这类独立服务。FAISS 的好处是简单一个文件就能存索引但不好做持久化管理和多副本数据量大时会比较吃力。索引结构也有讲究最简单的就是暴力扫描FLAT数据量小没问题数据量大建议用 HNSW它通过多层图结构加速搜索精度损失可控性能提升明显。2.4 检索与重排命中不是终点排对才是检索的直观逻辑是把用户问题变成向量然后在向量库里找余弦相似度 Top K 的文本块。但现实是向量相似度高并不代表语义上真的解决用户问题。两个句子可能用了完全不同的表达但意思接近也可能字面上高度重合但实际风马牛不相及。所以更靠谱的做法是“召回 精排”两步走第一步用向量检索加关键词检索比如 BM25各召回一批候选块合并去重留出一定余量比如 Top 20。第二步用 Rerank 模型对这些候选块做精细打分挑出真正对回答有贡献的 Top 5 交给大模型。Rerank 模型我这边常用的有bge-reranker-v2-m3它的思路是直接把“用户问题 候选块”拼接成对计算相关分。这一步增加了一点调用成本但对最终回答质量提升非常明显。我做过一次对比纯向量检索的 hit rate 大概 68%加上关键词召回和 rerank 后能到 85% 左右。在知识库问答里这个差距就是“能用”和“好用”的分界线。3. 实操把一个最小可用的 RAG 管道跑起来3.1 环境准备与依赖我们用一个相对轻量但完整的方案LangChain 做管道编排Chroma 做向量存储本地 embedding 模型用bge-small-zh-v1.5生成模型可以接任意 OpenAI 兼容接口。整个流程不需要昂贵硬件CPU 也能跑。pip install langchain langchain-community langchain-chroma sentence-transformers pymupdf说明一下langchain-chroma是专门适配 Chroma 的包老版本 LangChain 直接内置 Chroma新版拆出来了不装会报错。我这里用的 Python 版本是 3.10LangChain 版本为 0.2.x。如果你用的版本更高部分 API 名字可能变了但整体逻辑一致。3.2 文档加载、切分与入库我们先写一个最基础的脚本把docs目录下的 PDF 加载进来按 Markdown 结构切分然后向量化存入 Chroma。from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载 PDF loader PyPDFLoader(./docs/运维手册.pdf) documents loader.load() # 2. 切分 splitter RecursiveCharacterTextSplitter( chunk_size600, chunk_overlap100, separators[\n\n, \n, 。, , , , , , ], ) chunks splitter.split_documents(documents) print(f切分后文本块数量: {len(chunks)}) # 3. 向量化并存储 embeddings HuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5) vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directory./chroma_db )这里要提醒一下RecursiveCharacterTextSplitter的separators顺序很重要它代表切分优先级。中文我用“句号、问号、感叹号、分号、逗号”这些作为次级分隔比按纯字符硬切要人性化得多。如果你的文档是英文为主分隔符列表的顺序也要相应调整。3.3 实现检索与生成链路入库之后就可以写一个查询链路。这里把检索到的文本块用模板拼进提示词喂给大模型让模型“仅根据上下文回答”。from langchain.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI prompt ChatPromptTemplate.from_template( 你是一个知识库问答助手。请仅根据以下资料回答用户的问题。 如果资料中没有相关信息请明确回答“资料中没有找到相关信息”不要编造。 资料内容 {context} 用户问题 {question} ) llm ChatOpenAI( base_urlhttp://localhost:8000/v1, # 你的 OpenAI 兼容服务地址 api_keyEMPTY, modelqwen2.5-14b, temperature0.1, ) retriever vectorstore.as_retriever(search_kwargs{k: 5}) def answer(question: str): docs retriever.get_relevant_documents(question) context \n\n.join([doc.page_content for doc in docs]) chain prompt | llm response chain.invoke({context: context, question: question}) return response.content这段代码看着简单但有几个参数值得反复调k5是召回数量。太小容易漏太大会塞入大量无关文本反而干扰模型判断。temperature0.1对知识问答要低因为我们要的是稳定、可复现的答案而不是天马行空的发挥。另外prompt里那句“只根据资料回答不要编造”不是可有可无的废话。实测下来如果缺少这句模型会倾向于动用自身预训练知识去“补充”一旦补充错了你很难排查到底是检索的问题还是生成的问题。3.4 把 RAG 封装成 Agent 的一个工具跑通上面的链路之后就可以把它接进 Agent 了。这里我以 LangChain 的 Agent 为例思路是用tool装饰器把answer函数包装成一个工具让 Agent 自主决定何时调用。from langchain.tools import tool tool def knowledge_base_search(question: str) - str: 在内部知识库中检索相关信息回答用户关于产品手册、运维文档、FAQ 等问题。 return answer(question)然后把这个工具挂到 Agent 的工具列表里。这样做的好处是Agent 可以决定哪些问题走 RAG、哪些问题直接用自身能力回答。比如用户问“今天天气怎么样”Agent 不会去查知识库问“备份任务失败的处理流程”Agent 就知道必须调用知识库检索。集成之后有一点要特别注意Agent 是多轮对话的上一轮检索到的文本块不会自动从上下文里消失。如果你把工具返回结果原封不动丢给大模型Agent 可能会在下一轮参考到一个已经过时或跟当前问题无关的文档片段。这是 Agentic RAG 里常见的上下文污染问题后面我会专门讲怎么处理。4. 踩坑记录与效果调优4.1 常见问题速查表我把实际调 RAG 时遇到最多的问题整理成一个表按“症状-原因-解决”三列说明。症状可能原因解决思路检索结果明显不对分块切碎了语义或 embedding 不匹配调大 chunk size换成领域相关 embedding加混合检索答案答非所问上下文混入太多无关块降低 k 值加 rerank过滤掉相似度低于阈值的块模型总说“没有相关资料”检索没召回或者召回内容太泛检查向量库是否嵌入了内容手动打印检索结果观察答案编造事实prompt 没约束或检索内容不充分明确“只能根据资料回答”并补充召回数量对话到第二轮就开始乱上下文里残留了上一轮检索的文本每次工具调用后裁剪或定期清理历史记录向量库占用空间增长异常覆盖写入时旧向量没有清理根据 doc_id 先删再插或使用能更新的集合4.2 命中率为什么一直上不去很多人问我为什么我的 RAG 命中率这么低。我说你先把“命中率”定义清楚是用标准答案去比对检索结果还是只看最后回答对不对前者是 retriever 的hit rate后者是端到端的准确率。两个指标不一定一致但你首先要有一个可量化、可重复的测试集。你可以从知识库里挑 30 到 50 个典型问题每个问题标注一个或几个正确答案所在的文档片段然后统计Top 5 里有没有包含标准片段。如果 hit rate 低于 70%别急着调大模型先把检索侧调好。我常用的调试手段是打印查询向量和召回的文本块人工观察相似度高的块是否真的相关。尝试不同的 chunk size从 300 到 1000 扫一遍看 hit rate 的曲线变化。加入 BM25 关键词召回跟向量结果融合因为有些专业词向量模型根本匹配不上。别怕调参费时间。RAG 的效果上限从来不是由模型决定的而是由“你能否准确地把正确答案放到模型的眼皮底下”决定的。4.3 Agent 多轮对话中的上下文污染问题这个坑是我做 Agent 集成时最深刻的教训。最开始我把 RAG 工具接进 Agent 后第一轮效果很好第二轮突然出现“用上一轮的文档回答这一轮问题”的诡异现象。排查了很久发现问题是这样的Agent 每轮对话都会把之前的工具返回结果放进历史消息里而大模型是自回归的它会倾向于利用上下文里最近出现过的文本片段即使那些片段已经跟当前问题无关。解决办法有几个工具返回时只返回精简摘要而不是完整文档块。这样即使历史里残留影响也小。在 prompt 里明确告诉模型“仅根据当前最新检索结果回答忽略历史工具消息中的文档信息。”如果对话轮次很多定期压缩历史把旧工具消息裁剪或总结成一句“此前用户问过...答案是...”。我实际采用的做法是“摘要裁剪”。每次工具调用后把原始检索结果在工具内部转换为一段 100 字以内的摘要再返回给 Agent。这样既保留了信息又不会污染后续决策。4.4 从 RAG 到 Agentic RAG让 Agent 自己决定怎么查基础 RAG 最大的局限是“一次查询定生死”。用户问题复杂一点比如“对比 A 产品和 B 产品的规格差异”你很难用一个查询词同时检索到两个产品的信息。这时你需要把检索拆成多个子查询分别去查再汇总。Agentic RAG 的思路就是把这个“拆查询、调工具、多次检索、汇总验证”的过程交给 Agent 去编排。它不是一上来就用一个大检索而是先分析用户意图生成多个搜索计划逐步执行。举个例子用户问“我们上周的故障是不是因为磁盘满了”Agent 会先检索“上周故障记录”拿到故障时间段和现象再检索“磁盘监控告警”确认是否在同一时间有容量告警最后综合两份检索结果给出判断。如果只用一次检索往往会漏掉第二份资料。从工程实现来看Agentic RAG 并不复杂你只要把知识库检索封装成多个工具比如“故障记录查询”“监控数据查询”“容量报表查询”然后让 Agent 按需调用。但这里对 Agent 的模型能力要求更高因为 Agent 必须会做任务分解。如果模型太弱它可能只会调用一次工具就草率收场。我自己在做内部工具时会这样分级问题简单走单次 RAG问题可能涉及多个数据源走 Agentic RAG再往上才是多智能体协作。别一上来就把架构做复杂先让基础管道稳定再逐步开放决策权。最后分享一点我的个人体会RAG 这个方向入门不难做精很难。很多人以为把文档塞进向量库就完事了然后被效果打得怀疑人生。其实每一个环节都值得你花时间去测试、量化和优化。我自己的经验是先用一个小而全的测试集把管道打扎实再去追求高级编排。否则Agentic RAG 跑起来之后你根本分不清问题出在检索、排序还是决策上。从“能用”到“好用”其实就是把那些不起眼的细节一点点扣干净的过程。