这个系列写到第四篇,前面聊了 Agent 的基本结构、记忆系统怎么设计、工具调用怎么做,今天终于要碰一个很多人绕不过去的坎:Agent 的知识从哪里来。
先说背景。常见 Agent 做聪明,但一到企业内部场景就开始露馅——模型训练完的那一刻,它的知识就冻结了。你问它产品手册里最新的参数、公司制度里某条审批流程、某个系统的操作文档,它要么一本正经编答案,要么就说不知道。RAG(Retrieval-Augmented Generation,检索增强生成)是目前最主流也是最务实的外部知识方案:它把大模型不掌握的私域信息切碎、向量化、存起来,等到用户提问时先检索相关内容,再带着查到的材料去生成回答。说白了,就是给 Agent 做一个“先查资料再回答”的管道。
本篇是 RAG 的基础篇,适合正在从 0 到 1 搭 Agent、想把手头知识库接进系统的朋友。不需要你有很强的机器学习背景,但需要你写过一点 Python,了解大模型 API 的基本用法。我会从“RAG 在 Agent 体系里到底站在哪个位置”讲起,再把索引、检索、生成三个环节逐个拆开,最后用 LangChain 搭一个能跑起来的最小 RAG,并把实际操作中踩过的坑一并列出来。
1. RAG 在 Agent 体系中到底扮演什么角色
1.1 为什么 Agent 不能只靠大模型自己的知识
很多人第一次接触大模型应用时会产生一个错觉:模型什么都知道。实际上,大模型的训练数据有截止日期,训练过程也决定了它只能记住训练语料里出现过的信息。你给它喂一个 PDF 让它现场学习,和它能不能理解无关,而是它的上下文窗口就那么大,旧的东西很快被新的覆盖,聊几句就“失忆”了。
这不是模型能力的问题,而是架构问题。模型是个“推理引擎”,不是“硬盘”。在一套完整的 Agent 系统里,推理、规划、工具调用、记忆、知识获取,各司其职。知识获取管道(Knowledge Acquisition Pipeline)要解决的需求很明确:模型回答问题时需要事实信息,但这些事实信息在模型参数里不存在,得有一个快速、可靠、可更新的渠道,把“外部事实”传递给“推理引擎”。RAG 就是这个渠道的工程化产物。
在原生的 LLM 应用里,RAG 可以只是一个提问增强工具;但放到 Agent 体系里,它的角色更下沉——它往往是 Agent 的“工具之一”。Agent 决定要不要查知识库、查什么、查到什么程度,然后把检索到的资料交给生成链路做总结。这也是为什么最近社区里一直在聊 Agentic RAG:核心不再是“用户问了就查”,而是“Agent 自主判断该不该查,如何把查到的信息用于下一步决策”。要理解 Agentic RAG,得先把基础知识这一层打牢,这也是本篇存在的意义。
1.2 为什么不用微调来注入知识
这里必须把 RAG 和微调(Fine-tuning)的选择逻辑说清楚。很多人一上来就问:我有一堆私域文档,是不是直接微调模型更快?其实大多数场景下微调不是最优解,原因是它解决的方向和“知识获取”根本不一致。
微调整改的是模型的参数分布,让它学到某种行为风格、指令遵循方式或者领域表达习惯。但微调之后,模型仍然无法做到“精确记住”某条具体的、随时会变的记录——比如某个客户的合同编号、某产品今天的最新库存。就算你反复训练,模型也可能把相近的记录混淆,这是一种典型的记忆不可控。更现实的是,微调成本高:要数据标注、要算力、要反复实验,而业务文档一旦更新,你又得重新训练。
RAG 的设计目标恰恰相反:知识存在外部、更新走管道、生成只负责“读”。文档变了,重新跑一遍索引就完成更新,模型本身岿然不动。这带来了几个实战中的关键好处:第一,检索到的内容可追溯,用户能知道回答依据是哪一份文档;第二,权限可以控制,不同 Agent 角色的知识颗粒度不一样;第三,回答过程的幻觉风险更容易通过提示词约束来压制。当然 RAG 并不完美,它和微调其实是互补关系,不是一个替代另一个——如果你发现模型总是答非所问、表达风格完全不对,那才需要考虑微调;如果你只是缺资料、缺上下文,先上 RAG。
1.3 RAG 的工作流程:图书馆管理员模式
理解 RAG 最简单的方式,是想象图书馆里的管理员。第一步是“上架”:新书到馆,管理员给每本书贴标签、编索引,放到对应书架;第二步是“查书”:读者来问“有没有讲推荐系统冷启动的书”,管理员快速定位到几本;第三步是“整理答复”:管理员把这几本书翻到相关页,把内容摘抄组织成一段话交给读者。
对应到技术实现,RAG 就是三个环节:Ingestion(索引)、Retrieval(检索)、Generation(生成)。知识入库阶段,把源文档清洗、切分成小块(chunk),用 embedding 模型转成向量,写入向量数据库;查询阶段,用户问题也转成向量,到库里做相似度搜索,取回最相关的若干片段;生成阶段,把片段作为上下文塞进 prompt,让大模型基于这些材料回答。
这个模型的精妙之处在于,每一步都能替换或优化,互不干扰。检索可以换成混合检索,生成可以换成更强的模型,索引可以用不同的切分策略。做 Agent 应用,最忌讳把 RAG 当成一个黑盒接进去,理解了这层“管道”思维,才能在大模型基础能力波动时,准确判断是哪一环出了问题。
2. 核心细节解析:索引、检索、生成三个环节的工程要点
2.1 知识入库:切分策略是 RAG 成败的第一道关口
知识入库的输入往往是杂乱的:PDF、Word、Markdown、网页抓取、数据库导出的表格。第一步是清洗。我个人强烈建议,在切分之前先人工或程序化处理掉页眉页脚、重复段落、多余空白,必要时用 PyMuPDF 或 unstructured 这类工具把 PDF 表格结构识别出来。很多人犯的错是文档一拿到就切,结果向量库里存了一堆噪声,后面检索质量怎么修都修不回来。
切分(chunking)是最容易踩坑、也最影响效果的一环。最简单的策略是固定长度切分,比如每 500 个字符一刀,但这会拦腰截断语义完整的段落。工程上更推荐递归结构切分,比如 LangChain 的 RecursiveCharacterTextSplitter,它先按段落分隔符切,再按句子、最后按字符兜底,尽量保住语义边界。chunk_size 和 chunk_overlap 这两个参数要配合调:chunk_size 决定每块多大,chunk_overlap 决定相邻块之间保留多少重叠文字,目的是避免某段关键信息恰好被切在边界处丢失。
选 chunk_size 没有标准答案,要看下游生成模型对上下文长度的容忍度以及文档自身的特点。我的经验是,中文通用文档在 500 到 800 字左右比较平衡,overlap 设成 chunk_size 的 10% 到 20%。太大,检索时会把不相关内容拉进来稀释精度;太小,知识碎片化,检索找回的片段缺少上下文,回答就会显得断断续续。实际操作中,最好做一个“迷你实验集”,用 20 到 30 个典型问题测试不同参数下哪个组合能召回相关内容,再用真实用户问题做人工验收。这一步做好了,后面的工作会轻松不少。
2.2 向量化与向量数据库选型:不是越贵越好
切分完的文本片段要变成向量,需要选 embedding 模型。市面上选择很多:OpenAI 的 text-embedding-3 系列效果稳定,阿里百炼的 text-embedding-v4 的中文效果也不错,开源里 BAAI/bge-m3、bge-small-zh-v1.5 都有大量中文社区验证。选择的关键是“领域匹配度”:通用模型对法律、医疗、编程等垂直领域的效果不一定好,有条件的话应该用小批真实文档测试相似度排序是否符合直觉。
这里必须强调一个常见误区:embedding 的维度不是越高越好。向量维度过高,存储开销大、检索速度慢,而且低维的 bge-small 在某些中文场景下表现并不输给大模型级别的 embedding。对于刚起步的项目,优先用开源小模型挡住成本,等指标真上不去了再换商业 API 做对比。相似度度量也就是老生常谈——一般用余弦相似度(cosine)就够了,中文文本用 dot product 的效果差别不大,别在此纠结太久。
向量数据库选型可以按规模来定:个人项目和小型 Demo,Chroma、FAISS 都很合适;有一定规模、需要持久化和过滤功能,Milvus、Qdrant 更稳;如果团队已经在用 Elasticsearch,它的 knn 搜索也能直接当向量库用,不用额外引入新组件。选型不是越重型越好,一个几百条文档的项目用 Milvus 纯粹是给自己找运维负担。记住一个原则:RAG 的核心不是向量数据库,而是“检索得好不好”。基础设施只是载体,别本末倒置。
2.3 检索环节:向量检索只是起点
检索环节同样有讲究。标准的做法是把用户 query 也做 embedding,然后用向量数据库做 Top-K 相似度检索,K 通常在 3 到 6 之间。但这套“原生向量检索”在实际业务里远远不够。问题经常出在“用词不对”上:用户说的是“我上个月订单退款怎么还没到”,文档里写的是“售后时效:退款将在申请后 3 个工作日内原路退回”,两者的字面差异很大,向量相似度不高,结果就是召回不到。
对付这类问题有两招。一招是 query 改写(query rewrite):在检索前先用大模型把用户口语化的问题转成几组适合检索的关键词,甚至扩充成多个角度的子问题,分别去检索再合并结果,这叫多路召回(multi-query)。另一招是混合检索:向量检索负责语义相关性,BM25 这类传统关键词检索负责精确匹配术语,两边召回再融合,效果往往明显提升。我在做本地 ERP 产品检索时体会特别深——产品型号“SE-K-300”这种写法,向量模型很容易被无关内容干扰,但 BM25 对精确字符串的命中非常准,混合之后命中率立刻上了一个台阶。
再往上走一步是重排(rerank)。向量库 Top-100 先粗排召回候选,然后用更强的排序模型(比如 bge-reranker 系列)对候选片段和 query 做细粒度相关性打分,把最相关的 3 到 5 个放到最前面。这种“粗召回+精重排”的结构在业务中几乎是标配。有人一上来就把 K 设成几十个块全扔给大模型,以为上下文多就是好,结果反而把模型注意力带偏了。正确的思路是:召回宽一点,重排选准一点,最终进入生成环节的上下文越精炼越好。
2.4 生成环节:prompt 里的硬约束
生成环节不再需要堆技术,但细节决定回答质量。核心是把检索到的片段按顺序拼接成上下文,塞进 prompt,然后明确告诉模型:“以下内容来自知识库,请基于这些内容回答;如果内容不足以回答问题,请直接说不知道,不要编造。”这行话不是可有可无的——没有这个约束,大模型看到相关知识只会顺手“扩展”细节,编一个像模像样的答案,这在知识问答里是不可接受的。
另一个实战技巧是给每个片段标注来源。比如在拼接上下文时,在每个片段前加一行“文档:xxx.docx,第 3 页”之类的来源信息,并要求模型在回答末尾附上引用。这样用户能自查,排查问题时也能快速定位是哪一份文档、哪个段落导致了错误内容。经验上,“可溯源”这一设计带来的信任价值,往往比模型能力本身还高。
最后一个注意点是上下文长度控制。检索回来的片段拼接后动辄两三千字,一次问答可能没问题,但 Agent 场景里这只是一次工具调用的结果,后面可能还有多轮对话、多步工具操作。上下文窗口是有限的,塞得越多,Agent 留给规划和推理的空间就越小。所以在生成链路里,要养成“精确引用而不全文摘抄”的习惯——只把真正相关的片段传给模型,而不是把一整块知识库 dump 进去。
3. 实操走一遍:用 LangChain 搭一个最小 RAG
3.1 技术选型与运行环境
这一节我用 LangChain 演示,因为它链条清晰、适合教学,而且社区资料多。实际生产项目里,你也可以用 LlamaIndex、Spring AI,或者直接裸写调用 embedding API 和向量库,原理完全相同。为了这次演示,我选的是纯本地最小可行链路:本地文档、本地 embedding 模型、Chroma 持久化存储、OpenAI 兼容的模型 API 做生成。
环境准备很简单,装几个库就行:
pip install langchain langchain-community langchain-chroma chromadb sentence-transformers pypdf我这边用的 Python 3.10 跑下来没有版本冲突。如果你用的是更新版本的 LangChain,注意 langchain-community 里的某些模块名可能会改,这里我以 0.2 系列的 API 为准。embedding 模型用的是 BAAI/bge-small-zh-v1.5,512 维,中文效果稳定,资源占用也不高,本地跑完全没压力。生成模型我暂时用 OpenAI 兼容接口,你换成 DeepSeek、通义或者其他模型的 base_url 就能适配。DeepSeek 的中文效果在同等价位里很能打,日常实验足够了。
3.2 从文档到向量库:三步完成索引
先准备一份测试文档,比如一个假的《员工考勤管理制度》,里面要有请假流程、加班规则、罚款细则等内容。路径放在本地,然后用递归切分器入库。注意,我故意在切分参数上用了比较常规但不特别激进的值,方便你观察实际效果后再调:
from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_chroma import Chroma # 1. 加载 PDF(也可以换成 DirectoryLoader 批量加载) loader = PyPDFLoader("./employee_attendance.pdf") documents = loader.load() # 2. 切分成 chunk text_splitter = RecursiveCharacterTextSplitter( chunk_size=600, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""], length_function=len, ) chunks = text_splitter.split_documents(documents) print(f"共切出 {len(chunks)} 个文本块") # 3. 向量化并持久化存入 Chroma(首次运行会下载 bge-small 模型) embedding_model = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5") vector_store = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )切分器里 separators 的作用是定义逐级切割的优先级。中文场景里,我排过经验顺序:段落 > 换行 > 句号 > 感叹号问号 > 分号 > 空格 > 兜底字符。“。!”这些中文标点能比较自然地保住句子完整性;如果你不写“。”,很多句子会被从中间截断成无意义的碎片。这也是很多人说切分效果差的一大原因——默认的 separators 对英文友好,对中文不一定好。
persist_directory 参数让向量库落盘到本地目录,第二次运行就不需要重新构建。这是个很省心的小细节:项目重启后,直接 Chroma 加载已有目录,省掉重复向量化的时间。我用一个 30 页的管理制度 PDF 实测,整个索引过程不到 15 秒,embedding 模型下载一次后本地加载更快。
3.3 检索器和完整问答链
知识库建好后,核心就是检索器和问答链的组合。下面这段是完整可运行的最小链路:
from langchain.chains import create_retrieval_chain from langchain.chains.combine_documents import create_stuff_documents_chain from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI # 1. 加载已有向量库 vector_store = Chroma( persist_directory="./chroma_db", embedding_function=HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5"), ) # 2. 构造检索器 retriever = vector_store.as_retriever(search_kwargs={"k": 4}) # 3. 定义提示词 system_prompt = """ 你是企业内部知识助手。请基于以下知识库片段回答问题。 规则: 1. 只能使用提供的片段作为事实依据。 2. 如果片段中没有足够信息,明确回答“知识库中未找到相关内容”,不要编造。 3. 回答末尾标注依据的片段来源。 <context> {context} </context> """ prompt = ChatPromptTemplate.from_messages([ ("system", system_prompt), ("human", "{input}"), ]) # 4. 构建问答链 llm = ChatOpenAI( model="deepseek-chat", openai_api_base="https://api.deepseek.com/v1", openai_api_key="YOUR_API_KEY", temperature=0.2, ) combine_docs_chain = create_stuff_documents_chain(llm, prompt) rag_chain = create_retrieval_chain(retriever, combine_docs_chain) # 5. 提问 answer = rag_chain.invoke({"input": "请假的审批流程是什么?"}) print(answer["answer"]) for doc in answer["context"]: print("---来源---", doc.metadata.get("source"))几个参数值得说清楚。k=4 表示从知识库召回 4 个片段交给生成模型。太小可能漏内容,太大可能让回答变得啰嗦,4 到 6 之间通常是入门好起点。temperature=0.2 把生成随机性压得非常低,知识问答这种场景切忌高温度,否则同一个问题两次回答可能细节都对不上。
create_stuff_documents_chain 的含义是把所有召回片段“堆”到一个 prompt 里一次性让模型生成。这在数据量小时最省事。数据量大以后,你就要考虑 map-reduce 或 refine 这类别的链式策略,先逐段总结再合成最终答案,不过那就是进阶优化的范畴了,基础篇先跑通这条链路再说。
3.4 跑起来之后先验证这几件事
第一次跑通最小 RAG 后,别急着接业务,先用一个小型评估集做冒烟测试。我习惯准备 10 到 20 个问题,覆盖三类:能从文档直接回答的事实类问题、需要综合多个段落的问题、文档里根本没有的问题。逐条观察回答是否准确、是否引用了正确来源、遇到未知问题有没有老实说不清楚。
这一步能快速暴露 pipeline 的问题。比如我发现“综合多段落”类问题通常失败在召回环节——单条 query 只能召回某一段落,另一段相关知识点根本没进入上下文。解决办法就是前面提到的 multi-query 扩展,把一个问题拆成几个不同角度的子问题分别召回,最后合并去重再加进 context。此类的验证方法越早做越好,等部署上线再补评估,成本就高了。
4. 常见问题与排查技巧实录
4.1 检索命中率低:先看切分和数据质量,再换模型
大多数人遇到“回答胡说”第一反应是换更强的模型,但实际上 80% 的问题出在检索返回的内容不够相关。排查思路应该是:先单独把检索器拎出来看,用户问题进来后,Top-4 召回片段到底是什么。这一步用 vector_store.similarity_search_with_score(query) 就能看到,别直接看最终答案,先看召回。
如果召回相关的文档完全没出现,大概率三件事:embedding 模型和领域不匹配;chunk 切分把关键信息打散了;文档本身术语表达和用户问题差异过大。按顺序试:调 chunk_overlap、换领域更贴近的 embedding(比如代码文档用 code embedding,法律文本用中文法律语料微调过的模型)、引入混合检索,最后考虑 rerank。这里特别提醒,查询和文档的“称呼不一致”是中文场景的高发问题。你文档里写“考勤异常”,用户问的是“迟到怎么办”,字面距离很远,纯向量检索很容易漏,混合检索结合关键词后会有明显改善。
4.2 回答幻觉:问题可能不在模型,而在 prompt 和上下文
如果召回没问题,上下文里有正确内容,但模型还是答偏了,这时候才轮到生成环节。第一步是检查 prompt 里有没有“只依据上下文回答”的硬约束;第二步检查上下文中正确内容和杂质内容的占比——如果 Top-4 里只有一条和问题相关,其他三条是无关联片段,模型会被带偏。这不是模型笨,而是信息噪声太大。
我在实践中常用的一个小技巧是把召回的片段按相似度分数排序后,只选分数超过阈值的片段;同时把“不可回答”的兜底回答写进 system prompt,比如“如果上下文不足,回复:根据现有知识库无法确认”。这其实是一种省事的幻觉抑制手段。真正严格的场景,你还可以让模型在生成时同步输出引用索引,再程序化校验每个断言都能对应到片段文本,这项技术叫 groundedness checking,基础篇先了解有这个方向,后面再展开。
4.3 和 Agent 协作时的典型坑:工具描述与上下文管理
RAG 接入 Agent 后,头号问题不是知识库本身,而是 Agent 对工具的调用很随意。我希望它只在用户问内部制度时才调用知识库工具,结果它连“帮我写一句宣传语”也要先去查一遍,动不动把几百个 token 的上下文填满。排查下来往往发现是 Agent 工具描述写得太模糊,比如就叫“knowledge_search”,模型根本没法判断适用场景。
正确写法是把工具描述写得像个规则引擎:“适用于查询公司内部管理制度、产品说明、操作手册。当问题涉及公共常识时请直接回答,不要调用此工具。”这样模型才能做准确路由。另一个问题是多轮对话里的记忆错位:Agent 上一轮查过某文档,下一轮可能不查了,直接拿旧上下文作答,导致信息过期。这种情况下,建议在每次调用 RAG 工具后,让 Agent 把“检索到的事实摘要”重新写回工作记忆,并删除旧的检索内容,不要跨轮保留原始片段。细节很多,但核心只有一条:RAG 是 Agent 的外部知识来源,不是它的永久记忆,不能替代记忆系统。
4.4 效果优化清单:从基础到进阶的实战顺序
最后给一份我自己的优化顺序清单,遇到问题可以按这个顺序逐项尝试,而不是一上来就换架构:
| 优化项 | 操作内容 | 适用场景 |
|---|---|---|
| 调整切分参数 | chunk_size 在 400~800 之间调,overlap 调 10%~20% | 召回内容不完整或碎片化 |
| 数据清洗 | 去掉页眉页脚、表格乱码、重复内容 | 检索结果噪声大 |
| 多路召回 | 把 query 拆成多个子问题分别检索 | 单条 query 难以覆盖多维度 |
| 混合检索 | 向量检索 + BM25 关键词检索融合 | 精确术语和口语化表达混合的场景 |
| 重排 | 用 bge-reranker 对粗排结果精排 | 召回分布宽、上下文只能放少量片段 |
| 引用约束 | prompt 强制要求标注来源 | 回答可信度要求高 |
| 权限过滤 | 按用户角色过滤可检索的命名空间 | 多角色系统,一条知识库不能全员共享 |
这份清单适用于绝大多数中小型项目。等你把这些都做了一遍,RAG 本身的剩余优化空间就很有限了,这时再考虑向 Agentic RAG 演进:让 Agent 动态决定检索策略、子问题拆解、多轮检索甚至检索后反思。这正好是下一篇可以聊的话题。
最后说点个人感受。我从纯 Demo 到真正上线一个内部知识问答 Agent,中间走的最大弯路,就是一开始把精力都花在选“最好的 embedding 模型”和“最强的向量库”上,后来才发现,最影响体验的其实是数据清洗和切分策略。先把一手烂数据管好,比盲目换模型有效得多。还有一件事——任何 RAG 系统都要尽早建立小规模评估集,哪怕只有 20 条问题。没有评估,你就永远不知道一次改动到底把系统变好了还是变坏了。RAG 是个工程味道很重的技术,它的天花板往往不取决于你用了多贵的模型,而是你有没有耐心把细节打磨到位。