前几篇把 AI Agent 的骨架、记忆和规划聊得差不多,这篇来说说一个容易被低估、但真正决定 Agent 是“聪明助手”还是“一本正经胡说八道”的部分——知识获取管道,也就是 RAG(检索增强生成)。RAG 从 2023 年火到现在,依然是让大语言模型拿到外部知识最主流的方式,没有之一。它是“走进 AI Agent”系列的第四篇,我会从“为什么 Agent 非要有这条管道”讲起,再拆解从文档到回答的完整链路,最后给出一套可以直接复制的落地流程和排查清单,目标是让想给自己的 Agent 接上私有知识库、又不想只停留在调 API 层面的朋友,看完能直接上手。
1. 为什么 Agent 需要一条“知识获取管道”
1.1 Agent 的困境:大模型不是百科全书
先说一个很基础但经常被忽略的事实:大模型的知识是“冻结”在参数里的,它的知识截止时间、训练语料范围决定了它天生不知道你的私有文档、内部系统、最新产品动态。你可以把大模型理解成一个读过很多书的实习生,基础能力很强,但如果你不问它“我们公司最新的返修政策”,它只能靠猜。
Agent 和普通聊天机器人最大的区别在于它会“做事”:拆解任务、调用工具、执行计划。可一旦做事的依据本身是错误或者过时的,规划得再漂亮也是空中楼阁。我见过不少团队花大力气做 Agent 的意图识别和任务编排,最后发现回答质量上不去,症结就是知识源没打通。Agent 缺的不是推理能力,而是一条稳定、可维护的知识获取管道。
1.2 RAG 解决的是哪三个问题
RAG 的思路其实特别朴素:不把知识硬塞进模型大脑,而是给模型配一个随时能查的资料库。回答之前先检索,把检索到的内容连同问题一起交给模型去生成答案。这套机制直接对症下药解决三个问题:
- 幻觉问题:模型不知道的事情会编,RAG 用真实检索内容压制编造空间。
- 时效性问题:模型知识有截止日期,RAG 可以接入最新文档、实时工单、线上数据。
- 私有知识问题:企业内部知识散落在 Wiki、数据库、PDF、Excel 和聊天记录里,RAG 把这些碎片统一变成模型可消费的上下文。
生活里好理解的做法是:给新人一沓公司历史资料,让他干活前先查资料再动手。RAG 就是这个“先查资料再动手”的自动化版本。它不改变模型本身,只改变输入给模型的内容。
1.3 从单次问答到 Agent 化:知识管道的进化
早期的 RAG 只是“检索一次、生成一次”的固定流程,用户问一句,系统检索一段,拼进提示词里让模型回答。这种方式在 FAQ 场景够用,但放到 Agent 里就太死板了。Agent 的对话是多轮的,任务是多步的,知识需求是动态变化的——第一轮需要查产品规格,第二轮可能需要查报价规则,第三轮还要结合订单状态做判断。
所以这一两年“Agentic RAG”被反复提及。它的核心变化是:把检索不再是主流程里的一步,而是一个可以被 Agent 按需调用的能力。Agent 自己决定“我现在缺哪部分知识”“应该去哪个知识库查”“这次检索结果够不够,要不要换个方式再查一次”。知识获取管道从“自来水龙头”变成了“智能调度系统”,这也是我后面实操部分重点想展开的内容。
2. RAG 管道核心解剖:从文档到向量再到上下文
2.1 完整链路:加载、切分、嵌入、存储、检索、融合、生成
一个标准 RAG 管道可以拆成七个环节:
- 文档加载(Loader):从 PDF、Word、Markdown、HTML、数据库等源头读取内容。
- 文本切分(Splitter):把长文档切成适合检索的块。
- 向量化(Embedding):将文本块转成向量。
- 向量存储(Vector Store):建索引,方便相似度检索。
- 检索(Retrieval):根据用户问题取回最相关的文本块。
- 上下文融合(Fusion):把检索结果和问题组装成提示词。
- 生成(Generation):交给大模型产出最终答案。
前四步属于“离线索引阶段”,一般预先完成;后三步属于“在线推理阶段”,每次用户提问都会执行。绝大多数 RAG 工程的坑,都出在离线阶段的数据处理质量和在线阶段的检索策略上,而不是模型本身。
2.2 切分策略:chunk 大小为什么这么纠结
切分是整个 RAG 里最容易被轻视、但影响最大的一个环节。切太大,一个 chunk 塞进太多无关信息,检索时噪声大,而且浪费上下文窗口;切太小,语义碎片化,模型拿到的是残缺信息,很难组织出完整答案。
业界常用的几种切分方式各有适用场景:
| 切分方式 | 原理 | 适合场景 | 缺点 |
|---|---|---|---|
| 固定长度切分 | 按字符或 token 数硬切 | 快速验证、结构简单 | 容易切断句子和语义 |
| 递归字符切分 | 按段落、句子、子句层级回退切分(如 LangChain 的 RecursiveCharacterTextSplitter) | 通用文档 | 对语义边界不敏感 |
| 结构化切分 | 按 Markdown 标题、PDF 章节、HTML 标签切分 | 有清晰结构的文档 | 文档结构混乱时效果差 |
| 语义切分 | 利用嵌入模型判断句子间语义连贯性再合并 | 内容跳跃性强、长文 | 计算开销大、参数敏感 |
我在实际项目里的默认做法是:中文场景优先用按段落和句子的递归切分,chunk_size 设在 400~600 token,chunk_overlap 设在 50~100 token。overlap 的作用是避免某个关键句子恰好在边界处被割裂,给前后两段各留一点“重叠记忆”。
有一个很容易踩的坑:中文不像英文天然按空格分词,如果直接按字符数切,很可能把一个完整句子切成两半。我之前做某产品手册时,按 500 字符硬切,结果不少 chunk 结尾是半句话,检索召回率掉了近两成。后来改成按句子边界做初步切分,再用 token 数限制块大小,效果立刻正常了。
2.3 嵌入模型选型:向量质量决定检索上限
嵌入模型的任务是把文本映射成向量,向量之间的“距离”代表了语义相关性。选型上要考虑三件事:语言适配、向量维度、检索效果。
中文场景我实测下来,国产开源的 bge-m3、bge-large-zh 表现不错,阿里、百度的商用嵌入模型也稳。英文场景 OpenAI 的 text-embedding-3-small 性价比高,3-large 精度更高但有额外成本。如果领域特别垂直(比如法律、医疗、代码),强烈建议拿真实数据测试几个模型,别只看榜单。
一个重要的细节:查询(query)和文档(document)往往不是同一类文本,前者短、后者长。很多嵌入模型提供了专门为 query 和 document 分别优化的模式,用对模式能明显提高召回。这属于“低保优化”,效果立竿见影。
向量维度影响存储和计算成本。1536 维和 1024 维在几千条数据上差别不大,但到百万级文档时,维度会让索引体积和检索延迟显著上升。先小规模验证效果,再考虑要不要蒸馏降维,是更稳的路径。
2.4 向量库选型:FAISS、Chroma、Milvus、pgvector、Elasticsearch
向量存储方案的选择,取决于数据规模、团队技术栈和部署环境:
| 方案 | 优势 | 适合规模 | 备注 |
|---|---|---|---|
| FAISS | 轻量、纯本地、部署简单 | 百万级以下 | Meta 开源,适合学习和原型 |
| Chroma | API 简单、上手快 | 十万级以下 | 适合快速实验和小型应用 |
| Milvus / Zilliz | 分布式、功能完整 | 千万级以上 | 生产级,运维成本高 |
| pgvector | 直接复用 PostgreSQL,业务数据与向量同库 | 百万级以下 | 适合已有 PG 的团队 |
| Elasticsearch | 支持全文检索与向量混合 | 百万级以上 | 适合已有 ES 基础设施 |
如果是个人练手、公司内网小知识库,Chroma 或 pgvector 最省心。如果是大型生产系统,且已经有 ES,我会优先考虑在 ES 上扩展向量能力,这样文本检索和向量检索可以统一在同一套基础设施里,不用额外维护一套服务。
3. 实操过程:从 0 到 1 搭一个最小可用的 RAG 管道
3.1 准备阶段:先定场景再选工具
动手之前先问自己三个问题:我的知识源是什么格式?用户问题长什么样?回答质量用什么标准验收?这决定了后续每一步的走向。
假设一个常见案例:把一批产品说明文档做成客服问答。文档是 Markdown 和 PDF,用户问题类似“某某型号支持无线充电吗”。验收标准就是检索出的文档片段能不能支撑正确回答。
技术栈我选用 LangChain 加 FAISS 起步,原因是上手快、资料多、方便替换组件。嵌入模型用 bge-m3,生成模型用常见国产大模型 API 或本地部署模型。这套组合在大多数场景下足够稳定,成本也可控。
3.2 落地步骤:加载、切分、嵌入、存储、检索、生成
下面按步骤给出可直接参考的简化实现。这里用 Python 和 LangChain 生态,但核心思路不绑死任何框架,换成 LlamaIndex 或自研代码也一样。
第一步:文档加载。
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader # 加载目录下的 Markdown 和 PDF md_loader = DirectoryLoader("./docs", glob="**/*.md", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"}) pdf_loader = DirectoryLoader("./docs", glob="**/*.pdf", loader_cls=PyPDFLoader) docs = md_loader.load() + pdf_loader.load() # 这一步务必检查 docs 数量和内容是否完整,常见问题是编码和扫描版 PDF 抽不出文字第二步:切分。
from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 按 token 数估算 chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " "] ) chunks = splitter.split_documents(docs) print(f"切分后共 {len(chunks)} 个块")这里把中文句号、感叹号、问号加进分隔符,就是为了避免前面说的“半句话”问题。
第三步:嵌入并存入向量库。
from langchain_community.embeddings import HuggingFaceBgeEmbeddings from langchain_community.vectorstores import FAISS embedding = HuggingFaceBgeEmbeddings( model_name="BAAI/bge-m3", query_instruction="为这个句子生成表示以用于检索相关文章:", encode_kwargs={"normalize_embeddings": True}, ) vectorstore = FAISS.from_documents(chunks, embedding) vectorstore.save_local("./faiss_index")bge 模型建议开启归一化,这样后面用余弦相似度时距离计算更规整。
第四步:检索。
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) hits = retriever.invoke("某某型号支持无线充电吗") for i, h in enumerate(hits): print(f"召回 {i+1},得分约 {h.metadata.get('score')}") print(h.page_content[:100])第五步:组装上下文和提示词。
def build_prompt(query, contexts): context_text = "\n\n---\n\n".join([c.page_content for c in contexts]) return f"""请基于以下资料回答问题,如果资料中没有相关信息,请明确说“资料中未找到相关信息”。 资料: {context_text} 问题:{query} 回答:""" prompt = build_prompt("某某型号支持无线充电吗", hits)这个提示词虽然简单,但“无中生有则明说”这个限制很关键,能显著降低幻觉。
第六步:调用大模型生成。
from openai import OpenAI # 兼容接口的国产模型也可以这样调 client = OpenAI(base_url="https://your-model-endpoint", api_key="your-key") resp = client.chat.completions.create( model="your-model-name", messages=[{"role": "user", "content": prompt}], temperature=0.2, # 生成与检索强相关场景,温度低更稳 ) print(resp.choices[0].message.content)这样一个最小可用的 RAG 管道就跑通了。
3.3 参数调优:chunk_size、overlap、top_k、score 阈值
跑通不代表跑好,参数必须基于真实问题调。建议按这个顺序调:
- 先固定 top_k=5,调 chunk_size 和 overlap,看召回内容是否覆盖答案。
- 再调 top_k,观察增加数量后答案是否变好,以及上下文是否变得冗余。
- 最后设定 score 阈值。低于阈值的检索结果宁可不要,也别硬塞给模型。这就是“宁可不知道,不能乱回答”。
关于 score 阈值,有个容易被忽略的点:不同嵌入模型的相似度分布差异很大,bge 的结果和 OpenAI 的结果不能套同一个阈值。我一般先抽样打印一批检索得分,找到“正确召回”和“错误召回”的分数分界,再定阈值。这个操作叫“分数摸底”,虽然土但非常有效。
另外一个经验是,不要只调向量检索,把关键词检索加进来做混合检索,往往效果提升更明显。尤其产品名、型号这类强标识信息,关键词匹配比语义向量更精准。
4. RAG 管道在 AI Agent 中的进阶用法
4.1 从一次问答到多轮:不要让 Agent 每轮都“失忆”
基础 RAG 是无状态的:用户问什么就查什么,对话上下文不参与检索。但在 Agent 场景里,用户经常先说“我想买个手环”,下一句说“续航长一点的”。如果第二轮不知道前文,检索出的内容可能完全偏离。
解决方案是给 RAG 管道加上“对话状态”:把历史对话中的关键信息提取出来,改写成独立的检索 query,再进行向量检索。这一步在实践中往往用一个小模型或者规则实现,并不复杂,但对体验的提升是决定性的。我习惯在每轮构建 prompt 时,把最近两到三轮对话的核心实体和意图拼进检索 query,例如“手环 续航长 长续航”,效果远好于只用最后一句话去检索。
4.2 Agentic RAG:让检索成为 Agent 的一个“工具”
更进阶的玩法是让 RAG 变成 Agent 工具箱里的一项能力,而不是固定在主流程里。Agent 根据当前任务自行决定:要不要检索、查哪个库、检索结果够不够、不够就改写 query 再查。
实现思路大致有三步:
- 声明检索工具:为每个知识库定义一个工具,设定名称、描述、入参格式。
- 让 Agent 自行调用:Agent 在规划阶段决定调用哪个检索工具,并决定如何利用返回内容。
- 支持循环检索:如果 Agent 发现返回内容不足,可以改写 query,更换知识库,再次检索。
这样做的好处是知识获取不再是“一刀切”,而是跟着任务走。比如用户问“这个订单能不能退款”,Agent 可能需要先查订单系统、再查售后政策、最后查商品详情,才能给出准确建议。每一步查什么,由 Agent 基于当前掌握的信息动态决定。
4.3 解决“知识割裂”:GraphRAG、本体 RAG、多知识库路由
企业内部的知识往往不是一堆独立文档,而是有关系的:产品、订单、客户、政策互相引用,分布在不同的系统里。普通向量检索难以表达这种关系,就会出现所谓“知识割裂”问题——答案里查到了产品参数,但没查到与该产品绑定的售后政策。
最近很热的 GraphRAG、Ontology RAG,本质上都是在向量检索之外,引入实体关系和图谱结构。GraphRAG 会先构建实体、关系、社区的图谱,检索时既看文本又看关系,适合“谁和谁有关联”这类问题。本体 RAG 则用预定义的知识模型约束知识表示,适合领域体系稳定的场景,比如医疗症状与药品的关联、法律条款与案例的关联。
另外,多个知识库并存时,先路由再检索比统一检索更高效。Agent 先做一层意图判断,把问题分派到对应的知识库,再执行检索,最后汇总答案。这个模式和 4.2 是配套的,多知识库路由本质上就是 Agentic RAG 的一种具体实现。
5. 常见问题与排查技巧实录
5.1 问题速查与排查思路
下面这张表是我自己项目里最常遇到的问题和排查方向,基本覆盖了 RAG 落地 80% 的坑:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 检索结果与问题完全无关 | 文档切分错误、嵌入模型语言不匹配、query 太模糊 | 打印 top_k 召回片段,检查切分边界;换嵌入模型测试 |
| 检索到了但答案不对 | top_k 太小、上下文里相关信息和无关信息混杂 | 增大 top_k 并加入重排序;检查 prompt 是否要求“只依据资料” |
| 模型编造资料中没有的内容 | prompt 约束不够、生成温度过高、上下文中根本没有答案 | 降低 temperature,强化“不确定就说不确定”的指令 |
| 同样问题不同答案效果差别大 | 知识库版本混乱、文档更新后索引未重建 | 检查索引时间戳,建立知识库版本管理机制 |
| PDF 加载出来是乱码或空内容 | 扫描版 PDF、字体编码异常、OCR 缺失 | 先做文字抽取质量检查,必要时接 OCR |
| 中文问题召回率明显偏低 | 按字符切分导致句子被切断、中文分词未处理 | 切分时加入中文标点分隔,测试不同嵌入模型 |
| 响应时间越来越长 | 向量库数据量增长、top_k 过大连带生成输入过长 | 查看检索耗时,考虑精简向量维度或换更强的索引 |
排查的第一原则:把检索质量和生成质量分开测。先看召回片段里到底有没有正确答案,如果没有,问题在索引侧;如果有但模型还是答错,问题在生成侧。这一步能避免在错误方向上调半天。
5.2 踩过的坑与避坑技巧
第一个坑:过度相信“top_k 越大越好”。我早期做售后问答,top_k 从 3 调到 10,结果答案不仅没好,反而因为上下文塞进太多弱相关内容,模型开始混乱,偶尔还引用错误片段。后来给召回结果加了一个简单的重排序环节,先把 top 20 按相关性和答案完备性粗排,再取前 5 进入生成,效果好很多。重排序模型(Reranker)是 RAG 里回报很高的投入,强烈建议加上。
第二个坑:知识更新了,向量索引没更新。有两个团队同事同时改同一份文档,索引重建任务覆盖了旧版本,结果线上回答一半新的、一半旧的。现在我会给文档加版本号和更新时间,重建索引时带 meta 信息,并且在检索结果里把来源和更新时间透出给用户。知识管理首先要讲“可追溯”,RAG 也一样。
第三个坑:忽略了检索结果的“出处引用”。最初我的 prompt 只给资料不给来源,模型生成的回答用户无法验证。后来在每段资料前加一个 id,要求模型引用时标注 [1]、[2],最后再展示对应文档标题和链接。这么做之后用户信任度提升非常明显,很多质疑根本不会出现。
5.3 质量评估:不要只靠“感觉”
RAG 做久了会发现,没有量化指标就没法持续优化。最常看的三个指标:
- Hit Rate(命中率):检索出的 top_k 片段中,有多少包含正确答案的关键实体。
- MRR(平均倒数排名):正确答案在检索结果中排得越靠前,得分越高。
- Faithfulness(忠实度):生成答案中有多少内容能在检索到的资料里找到依据。
手工建一个 100 条左右的评测集,把真实用户问题、期望回答要点、所需文档位置都记下来,每次改动后批量跑一遍,看指标变化。这比“多试几条看起来不错”可靠得多。我个人经验是,先把 Hit Rate 拉到 80% 以上,再谈生成优化;Hit Rate 太低时,盲目调 prompt 和模型只是在浪费时间。
最后再分享一个小技巧:做演示或小项目时,可以先把检索到的原文片段原样展示给用户,让用户看到“依据就是这段文字”。这既是信任工具,也是排查工具。等整个管道指标稳定了,再决定要不要加更多抽象和加工。RAG 从来不是一个模型问题,而是数据处理、检索策略、生成约束、评估闭环共同作用的结果。把每个环节的“为什么”想清楚,这条知识获取管道才算真正落地。