做 AI Agent 开发的老朋友应该都有体会:真正让 Agent 从“玩具”变成“工具”的,往往不是模型本身多聪明,而是它能不能在关键时刻拿到对的那条知识。这里说的知识获取管道,说白了就是 RAG(Retrieval-Augmented Generation,检索增强生成)。这篇文章是我“走进 AI Agent”系列的第四篇,专门讲 RAG 基础。适合正在折腾 Agent 项目的开发者、想给大模型接私有数据的产品经理,以及准备面试时想把 RAG 链路讲明白的同学。
RAG 解决的问题非常具体:大模型训练完就冻结了,你问它半年前的内部文档、最新产品规格、某位同事写的技术规范,它要么胡编要么说不知道。RAG 的思路就是在生成前先检索相关文档,把检索结果拼进上下文,让模型“开卷考试”。整个管道涉及文档加载、切分、向量化、索引、检索、重排、生成,每一步都有坑。这篇我先把 RAG 在 Agent 架构里的位置讲清楚,再逐环节拆解核心细节,给一套可以直接跑的 Python 代码模板,最后聊几个我实际踩过的坑,顺带说说 Agentic RAG、GraphRAG 这些进阶方向。
1. RAG 在 AI Agent 里的真实地位
1.1 为什么 Agent 必须有知识获取管道
现在聊 AI Agent,基本达成共识的架构是:一个大模型做“大脑”,配上工具(Tools),通过规划(Planning)和记忆(Memory)来完成任务。但有一个问题经常被一笔带过——工具返回的数据、用户上传的文件、系统里存的历史记录,这些“非结构化知识”到底怎么进到大模型脑子里?
你当然可以把所有文档都怼进上下文,但 token 上限不允许,成本更不允许。而且很多知识是动态的,今天的版本明天就过期。RAG 的价值就在于:它像一个知识库的管理中枢,离线阶段把文档处理成可检索的形式,在线阶段只捞取和当前问题最相关的一小部分片段,喂给模型。就像给刚入职的新人配了个资深检索员,新人不需要背下全公司的制度手册,遇到问题喊一声,检索员把相关章节递过来,现场翻书回答。RAG 就是那个检索员,而 Agent 里的规划器则决定什么时候该喊检索员。
这一点我觉得在 Agent 项目里再怎么强调都不过分。很多人一开始做 Agent,挖空心思设计 prompt、调模型参数,结果效果一直上不去,最后发现是知识根本没进到模型视野里。RAG 建得好不好,直接影响 Agent 回答的准确率、可信度和可解释性。尤其是企业级应用,用户问的是“我们公司项目 X 的验收标准”,你不能让模型去自由发挥——必须让它基于真实数据库里的文本作答。
1.2 RAG 和 Agent 的两种配合形态
在 Agent 架构里,RAG 通常以两种形态存在。
第一种是 Tool 形态:把检索封装成一个函数,比如search_knowledge_base(query),Agent 的规划器根据用户问题决定是否调用。这种形态逻辑清晰、可控性好,适合问答、客服、售前支持这类场景。第二种是 Memory 形态:把对话历史、用户画像、业务数据都存入向量库,Agent 在每次决策前自动带上相关记忆。这种形态更像“内化”的知识,适合需要长期记忆的助手型 Agent。
我做项目时的经验是:前期做原型优先用 Tool 形态,因为它能让你清楚地看到“模型什么时候在检索、检索到了什么、为什么这么回答”,排查问题非常方便。等需求复杂了,再逐步引入混合检索、多路召回、路由机制。另外要澄清一个常见误区——很多人觉得“写了 RAG 就等于有了知识库”,其实 RAG 只是知识获取的下游管道,真正决定上限的是知识源的质量。文档本身烂,检索再花哨也白搭。
1.3 什么时候不该上 RAG
不是所有场景都需要 RAG。如果你的知识是高度结构化、量又不大的,比如几百条规则、几十个配置项,直接写进 system prompt 或者用传统的规则匹配,反而更稳定、开销更低。RAG 适合的是文档量大、持续更新、语义模糊、需要“泛读”的场景。过度设计是 AI 项目最常见的浪费,搞清这个边界能帮你省下不少不必要的麻烦。
2. RAG 管道的核心环节拆解
2.1 文档加载与解析:别小看这一步
RAG 管道的第一步,是把 PDF、Word、Markdown、HTML 等格式的文档变成纯文本。这一步看着简单,实际坑最多。PDF 里的表格、图片、页眉页脚、双栏排版,还有扫描版的 OCR,都是常规解析器容易翻车的地方。
我用过 PyMuPDF、pdfplumber,也试过各种商业解析服务。经验是:不要指望一个解析器通吃所有文档。如果文档结构复杂,优先考虑按版面解析,而不是单纯抽取文本流。比如一个产品手册,标题、正文、表格、图注如果不区分,切分出来的块质量会大打折扣。更讲究一点的做法是保留结构信息,把“标题层级”“是否在表格里”“属于哪个章节”这些元数据一并存到向量库里,检索时可以做条件过滤。这一步前期投入越足,后面检索精度越高。
2.2 切分策略与参数:检索粒度的关键
切分决定了检索的粒度。很多人图省事,直接按固定字符数切,比如 512 字符一刀,这是可行的,但效果通常很平庸。更好的做法是按语义结构切:Markdown 按标题切,代码按函数切,法律文书按条款切,产品文档按章节切。语义完整的片段天然更利于向量检索——因为向量模型编码的是整段的语义,而不是截断的内容。
切分参数里有两个关键:块大小(chunk size)和重叠(overlap)。块大小决定每个片段包含多少信息量,一般 200~800 tokens 比较合理。太小,语义不完整;太大,检索结果不够聚焦,而且拼接进上下文后很快把 token 预算吃光。重叠的作用是防止一个完整语义恰好被切断在边界上。我常用的基准值是块大小 500 tokens、重叠 100 tokens,但这不是死数,必须根据文档特性调。代码文档、技术规范、客服问答,合适的参数差别很大。
2.3 向量化与索引:选对模型、别过度设计
向量化就是把文本转成固定长度的数组,让语义相近的文本在向量空间里距离更近。选 embedding 模型时,重点看三个指标:中文效果、维度、推理成本。中文场景里,社区常见的选择有 OpenAI 的 text-embedding-3-small/large,也有开源的 BGE、M3E 系列。注意 embedding 模型对语言的敏感度差别很大,一个英文上表现优异的模型,中文可能直接拉胯。一定拿你自己的真实语料跑一跑,用检索召回率说话,别只看模型榜单。
索引方面,如果数据量在几十万条以下,用 Chroma、FAISS 这些本地向量库就足够。到了千万级,才需要考虑专门的向量数据库(比如 Milvus、Qdrant)或全文检索引擎。索引不是越重越好,它必须和你的部署环境、运维成本、数据增长预期匹配。很多团队一上来就搭分布式向量库,结果几条数据跑得飞起,白白增加维护负担。
2.4 检索与重排:纯向量是不够的
检索阶段最朴素的方案是向量相似度召回 Top-K。但纯向量检索的缺陷非常典型:对专有名词、缩写、精确编号不敏感。举个例子,用户问“PV-304 的备件清单”,文档里写的是“型号 PV-304 配套配件”——这种表述差异向量模型能处理,但如果文档里还有“PV-301 备件”“PV-304 使用说明”等大量相似文本,相似度一挤,正确答案很容易被挤出 Top-K。
我建议即使做基础版本,也要尽早引入混合检索:向量召回 + BM25 关键词召回,再用倒数排名融合(RRF)合并结果。向量擅长语义相似,BM25 擅长精确匹配,两者互补性很强。重排(Rerank)更是提精度的关键。用专门的 rerank 模型把 Top-20 的结果重新排序,再取 Top-5 喂给模型,命中率能提升一截。重排模型的参数一般不大,单机 CPU 也能跑,性价比极高。
3. 实操记录:搭一条最简可用的 RAG 管道
3.1 环境准备
我用 Python 3.10 写示例,需要的库如下:langchain、chromadb、fastapi、sentence-transformers。为了照顾不同网络环境和成本,代码里我用本地 embedding 模型(BAAI/bge-small-zh-v1.5),完全不需要外部 API,下载一次权重后离线可用。
pip install langchain chromadb sentence-transformers fastapi uvicorn如果你的环境下载 HuggingFace 模型不方便,可以手动下载权重后指定本地路径。小提示:先确认机器上有足够内存,bge-small-zh 大约要 400MB。要是内存紧张,换更小的bge-small-zh也可以。
3.2 完整代码:构建知识库与检索问答
下面这个模板把流程拆成两段:第一段负责加载文档、切分、写入向量库;第二段封装检索问答接口。建议初学先把这三步流程单独跑通,不要一上来就套高级封装。
import os from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载文档 loader = DirectoryLoader("./docs", glob="**/*.txt", loader_cls=TextLoader) docs = loader.load() print(f"加载文档数: {len(docs)}") # 2. 切分 splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?", ".", " ", ""] ) chunks = splitter.split_documents(docs) print(f"切分块数: {len(chunks)}") # 3. 向量化并写入 Chroma embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding, persist_directory="./chroma_db" ) print("向量库构建完成")这段代码里我用了RecursiveCharacterTextSplitter,它按优先级去匹配分割符。这里我给separators加入了中文的句号、感叹号等,避免一处分隔符都不匹配时整个块被切得稀碎,这个细节比默认参数更适合中文。
检索问答的封装如下。我额外加了retriever_kwargs里的search_kwargs={"k": 5},先召回 5 个块。如果后续加重排,这段代码可以扩展成“检索 20 个再重排取 5 个”。
from langchain.chains import RetrievalQA from langchain_community.llms import OpenAI # 请替换为实际可用的模型 def create_qa_chain(): vectorstore = Chroma( persist_directory="./chroma_db", embedding_function=embedding ) retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, return_source_documents=True, chain_type="stuff" ) return qa_chain if __name__ == "__main__": qa = create_qa_chain() result = qa("PV-304 的备件清单包含哪些内容?") print("回答:", result["result"]) print("依据文档:") for doc in result["source_documents"]: print("-", doc.page_content[:80])这里chain_type="stuff"意思是把检索到的所有块直接拼接后交给模型。数据量小、块少的时候最合适,不会有信息丢失。如果检索块很多导致上下文太长,再考虑map_reduce、refine等策略。
3.3 参数选择和调优记录
我在自己项目里测过不同参数组合,简单列一张表:
| chunk_size | overlap | 效果观察 |
|---|---|---|
| 200 | 50 | 检索精确,但上下文碎片多,答案容易缺背景 |
| 500 | 100 | 综合表现最稳,语义完整度和召回率均衡 |
| 1000 | 200 | 大块语义完整,但检索不够聚焦,多主题段落会混淆 |
这只是我拿内部产品文档测出来的规律,具体项目必须自己重测。调参方法论比参数值更重要:先选几个有代表性的测试问题,人工标注“正确答案应该来自哪一段”,然后跑完看召回率有没有提升。没有所谓最优参数,只有适合你数据分布的那组参数。
4. 常见的坑与排查心得
4.1 检索质量差:先查“切分”而不是“向量模型”
遇到回答得牛头不对马嘴,很多人第一反应是换更强的 embedding 模型。但以我的经验,大概率问题出在切分上。比如,产品规格和售后政策写在同一段里,切分没切开,检索“售后政策”时把整个规格段落召回,模型再聪明也会被带偏。排查方法很简单:把检索返回的source_documents打出来看看,是不是目标内容;如果不是,八成是切分太粗或太碎,先调切分再考虑换模型。
另一个常见问题是知识更新后,旧版本文档还在向量库里。检索时新旧内容同时召回,模型无法判断时效性。解决办法是给每个片段加元数据(如文档版本号、更新时间),检索时按时间过滤。这是很多教程不会提的生产级细节。
4.2 检索到了但模型答错:上下文组织有问题
有些时候检索完全正确,但模型还是答错。我踩过的坑主要有三个。第一,拼接顺序不对,把不相关的块插到了相关块前面,模型注意力被分散。第二,没有告诉模型“必须依据给定材料作答”,模型就自由发挥了。第三,把多篇文档的内容平铺拼在一起,没有标识来源,模型分不清哪个信息属于哪个文档。解决办法是在 prompt 里明确要求“仅根据以下文档片段回答,若无法从片段中获得答案,请直接说不确定”,并且给每个片段加序号和来源标记。这个小改动比换模型管用得多。
4.3 性能瓶颈与优化方向
RAG 管道的性能瓶颈通常不在模型,而在检索环节。数据量大之后,每次查询都全量做向量相似度计算,延迟会显著上升。常用优化手段有三个:一是给向量库建索引(IVF、HNSW),大幅减少计算量;二是先做粗召回再做重排,避免把计算资源花在无关数据上;三是加缓存,把高频问题与其答案缓存起来,完全绕过检索管道。这些手段按需采用,前期不用追求极致性能,先把正确率提上去。
5. 从 RAG 到 Agentic RAG 的进阶方向
5.1 Agentic RAG:把检索变成决策
基础 RAG 的问题是“问题是什么,就检什么”,缺少主动推理和工具调用能力。Agentic RAG 把它升级为:Agent 先判断需要什么信息,决定调“内部文档检索”还是“搜索 API”,甚至先检索再根据结果二次检索。比如用户问“对比 A 型号和 B 型号的售后政策”,普通 RAG 从单一块里找信息会漏一半;Agentic 可以拆解成两次检索,分别找 A、B 的政策文档,再合并对比。
从技术实现上,LangChain 的create_agent已经能方便地把检索器注册成 Agent 的工具,让大模型自己决定何时调用。但注意,Agent 的自主要求对模型的推理能力更高。如果你的模型比较弱,加了工具反而容易瞎调用,不如老老实实做固定管道。
5.2 GraphRAG 与更远的方向
基础 RAG 把文档切块存向量,对“属于同一实体但分散在不同文档里的信息”召回效果很差。GraphRAG 的思路是先把文档里的实体和关系抽取出来,构建知识图谱,再在图结构上检索。比如“A 项目依赖 B 项目,B 项目依赖于 C 服务”,这种跨文档关系在普通向量库里很难召回,但在图谱上是一跳就能找到的。Microsoft 的 GraphRAG 是这类方案的代表。
不过 GraphRAG 的构建成本比较高,需要额外的实体抽取、关系抽取和消歧调度。以我的项目经验,数据量没到几万篇、关系没到频繁跨文档查询的程度,老老实实用基础 RAG 加混合检索更划算。技术选型永远是为了解决问题本身服务的,不必为了追新而追新。
回到 RAG 本身,这个领域真正有趣的地方在于:它看似是一条固定的管道,但每个环节都有充分的定制空间。文档解析、切分策略、检索融合、重排模型,任何一个环节做到极致,都能让整体效果上一个台阶。而把这些环节和 Agent 的规划、工具调用结合起来之后,就不再是简简单单的问答机器人了——那是一个真正有“工作能力”的系统。
最后分享一点我自己的体会:做 RAG 项目,不要一开始就追求把所有先进技术都塞进来。先用最简单的链路跑通端到端,让用户拿真实问题去测,记录失败案例,再沿着失败案例去优化某个环节。你需要的不是更复杂的架构,而是对“数据到底长什么样、用户到底怎么问”这两个问题更清晰的理解。把这一步做扎实了,后面不管是上重排、上 Agentic RAG 还是上图谱增强,都会顺畅很多。