1. 为什么Agent需要知识获取管道:先认清LLM的“知识天花板”
前几篇我们聊了Agent的骨架、规划和工具调用,这套机制让模型能拆解任务、调用外部API、执行动作。但如果你真正上手跑过几个Agent,大概率会遇到同一个尴尬场面:Agent逻辑很顺、工具调用也很溜,可一问到具体的业务细节、内部文档、某个产品的参数规格,它就开始一本正经地胡说八道。这不是模型笨,而是它脑子里的知识有边界。
大模型在训练时把互联网上的公开知识压缩进了参数里,但训练语料有截止日期,而且不可能覆盖你团队内部的私有资料。你不可能让一个通用模型天然知道你们公司的报销制度、某个设备的维修手册、上个月的运营数据。这时候就需要一条从外部知识源把信息“喂”给模型的管道,这就是RAG(Retrieval-Augmented Generation,检索增强生成)要做的事。RAG不是一个独立的模型,而是一个架构模式:先根据用户的问题去外部知识库里检索相关内容,再把检索结果和问题一起塞进提示词,让模型基于这些材料回答。
所以我把这个系列里RAG这篇定位成“知识获取管道”。它是Agent的眼睛和耳朵,负责在模型开口之前,先把有用的背景材料捞过来。这篇文章面向两类人:一类是刚开始搭Agent、不知道知识库这块怎么下手的小白;另一类是已经跑了裸模型接口、但发现回答总是不够准、不够新的开发者。看完你应该能理解RAG的完整链路,并且能照着后面的代码搭出一个能用的最小管道。
1.1 Agent的“记忆缺陷”与RAG的补位
先别急着写代码,我们把问题掰开揉碎。LLM本身有没有记忆?有,但那是“参数记忆”,知识被以概率分布的形式存在几亿几百亿个权重里。这种记忆有三个硬伤:静态、模糊、通用。静态是说它不会因为你昨天更新的文档而变化,除非重新训练或微调,但那成本高得吓人;模糊是说它记不住精确的编号、日期、价格,因为它学的是“规律”,不是“数据库”;通用是说它只擅长大众化的知识,你问它某个冷门框架的底层实现,它只能靠“猜”。
Agent在执行任务时,最怕的就是这种模糊性。比如一个客服Agent,用户问“我上个月订单为什么还没发货”,模型只能瞎编一个原因。但如果给它检索到该用户的订单状态、物流轨迹、仓库备注,它就能基于事实回答。RAG就是在模型推理之前,先做一次精准的“开卷考试”:不是让模型背全书,而是把相关章节撕下来递给它。
这里要区分RAG和微调(Fine-tuning)。很多人一开始都会纠结:要不要微调一个垂直模型?我的建议是,如果你的知识是事实型、更新频繁、需要精确引用的内容,优先上RAG。微调更适合改变模型的“行为风格”,比如让它按固定格式输出、学会某种语气,不适合塞大量事实细节。RAG还有一个好处是每次回答都能引用来源,出了问题你可以去查检索到了什么,这在新规合规场景下非常重要。
1.2 RAG在整个Agent架构中的位置
在Agent架构里,RAG通常不是独立模块,而是作为“工具”或“上下文构建器”嵌入到工作流里。最朴素的做法是:Agent接收到用户问题后,先调用一个“知识检索工具”,把检索到的文本片段作为上下文,再交给主模型生成最终回复。稍微进阶一点,Agent可以判断这个问题是否需要检索、需要检索几次、检索后用哪个工具的结果,这就是后面要讲的Agentic RAG。
很多人会混淆RAG和“长上下文窗口”。现在模型动辄支持100万token上下文,那是不是把整个文档库都塞进去就行?从理论上说可行,但成本高、速度慢,而且实验发现过长的无关上下文反而会稀释模型的注意力,导致回答质量下降。RAG的本质是做“精挑细选”:从海量文档里挑出最相关的几千token作为上下文,让模型把注意力集中在关键信息上。就好比你给员工开卷考试,直接把整本辞海放他桌上,他未必能找到答案;但如果事先帮他折好页码,效率立刻上来了。
所以你在搭建Agent时,不要把知识库设计成“一次性把所有资料导入上下文”,而要把知识获取设计成“按需检索的管道”。管道这个词很关键:它意味着数据从源头到模型之间要经过清洗、切分、索引、召回等关卡,每一关都会影响最终效果。下面我按管道顺序拆开讲。
2. 构建RAG管道的四个核心环节:从文档到召回
一次标准的RAG流程可以分成四步:加载与清洗、切分、向量化、存储与召回。这四步环环相扣,前面一步做不好,后面再怎么调模型也没用。很多人在第二步切分上踩坑最多,但第一步的清洗反而最容易被忽略。
2.1 文档加载与清洗:别让垃圾数据进管道
文档加载听起来最简单,不就是读文件嘛。实际上坑很多。PDF有扫描版、表格版、三栏排版;Word里有页眉页脚;Markdown里有HTML标签;PPT和Excel导出成文本后往往留下一堆无意义的编号。如果你的知识库是面向业务的,通常还有各种格式的导出文件。我们要做的清洗包括:去除页眉页脚、统一换行符、去掉图片OCR产生的乱码、合并断行、处理表格转换为结构化文本。
我的经验是:在加载阶段多花半小时,比在后续调参节省十小时。比如PDF里的文字经常因为排版被拆成半个单词,直接切分后会出现大量“chun”和“ked”这样的碎片。清洗阶段可以先把所有文档统一转成纯文本或Markdown格式,再用正则清理掉连续空行、特殊符号。如果你用的是现成加载器,比如LangChain的PyPDFLoader,也要记得用text_splitter之前先自己预览几次输出内容。
还有一个常被忽视的点:数据粒度。知识库里的文档长短差异巨大,有的是一整本书,有的是一句话FAQ。不要混在一起直接处理。建议先按文档类型或业务板块分目录管理,再对不同类型配置不同的清洗和切分策略。管道设计上要让“清洗”这一步独立出来,方便后续单独调整。
2.2 文本切分:决定检索质量的“隐藏杠杆”
切分是RAG里最容易被低估的一环。选什么模型、用什么向量库固然重要,但文本怎么切,直接决定了检索命中的质量。如果切得太碎,一个完整的概念被拆到两个片段里,模型看到的信息是残缺的;如果切得太粗,一个片段里塞了太多无关内容,向量相似度会被稀释,召回结果飘忽不定。
常见的切分方式有两种:固定长度切分和语义切分。固定长度切分是按字符或者token数量无脑切,比如每300个字符一段,段与段之间重叠50个字符。这种方式简单、稳定,适合大多数情况,尤其是中英文混合、代码、日志这类文本。语义切分则是按段落、标题、句子边界来切,比如Markdown格式的文档按标题层级切分,效果更符合人类的阅读逻辑,但实现复杂度高一些。
关于切分大小,我建议你以“检索后拼给模型之后,模型能否独立理解这一段”为标准来调整。如果你们公司准备上一套RAG,拿业务文档先试一把,多用几个chunk size跑一遍,看哪个尺寸命中率最高。经验值说说:中文场景下,chunk size在300到500字之间通常比较稳妥,overlap设置在50到100字。太长会导致多个观点挤在一起,向量方向被“平均化”;太短则上下文不足。表格数据尽量保持完整行组合成块,不要拆断字段。
2.3 向量化嵌入:把文字变成坐标
文本切完之后,要转成计算机能计算的向量。这一步的核心是嵌入模型(Embedding Model),它把一段文字映射成一个高维向量,语义相近的文本在向量空间里的距离更近。你对嵌入模型的选择,会影响整个管道的召回上限。
目前常用的开源嵌入模型有BGE、M3E、GTE、text-embedding-adapter等,闭源的如OpenAI的text-embedding-3-small、阿里云的文本向量模型。选型时要考虑:中英文双语能力、向量维度、支持的最大token长度、是否本地部署、成本。如果你处理的是纯中文业务文档,直接用支持中文的模型比用通用英文模型效果好得多。最怕的是中英文混排的专业术语,比如“RAG pipeline”,很多模型会把它们分开处理,导致向量偏移。
这里要补充一个细节:嵌入模型不是越大越好。大模型生成的向量质量更高,但推理成本也高。小模型在垂直领域经过微调后,效果可以超过通用大模型。所以不要盲目迷信热门,建议抽200条有代表性的测试问题,分别用两三个模型跑一下检索命中率,再做决定。
向量维度也需要关注。高维度(比如1024维或1536维)表达能力强,但占用存储多,检索计算量大;低维度(如256维或384维)更轻量,适合快速实验。有些向量库支持量化压缩,把浮点向量转成二值或8位整型,大幅减少存储,代价是少量精度损失。不是极端场景,我建议先用原始精度跑通,再考虑量化。
2.4 存储与召回:向量数据库怎么挑怎么用
向量化之后要存起来,这就轮到向量数据库登场。市面上的选择很多:FAISS、Milvus、Qdrant、Chroma、Weaviate、Elasticsearch(向量插件)、pgvector等。我的看法是:阶段不同,选型不同。
如果你在做原型验证、个人项目、不超过几万条文档片段,直接用FAISS或者Chroma就够了。FAISS是Meta开源的内存索引库,没有服务器,轻巧灵活;Chroma更偏向RAG一站式体验,自带数据持久化。如果数据量到了百万级以上、需要分布式部署、要支持过滤和复杂查询,就上Milvus或Qdrant,或者直接用PostgreSQL加pgvector扩展,让业务数据和向量数据在同一个库里,减少维护成本。
日常使用中,我把向量数据库更多看成“召回服务”,而不只是存储。关键参数有这几个:
top_k:返回多少个候选片段。太小容易漏,太大会把不相关的东西塞进上下文,增加token消耗。我一般初始设置为5,然后根据问题复杂度调整。score阈值:相似度分数低于某个值的结果应该被丢弃。很多RAG管道不加阈值,导致明明没检索到相关内容,硬把低相似度的片段塞给模型,模型就只能瞎编。建议在召回后加一道过滤:分数过低就返回“知识库中未找到相关内容”。- 元数据过滤:向量数据库不只存向量,还能存来源、章节、时间、类型等标签。检索时可以先用元数据缩小范围,再做向量相似度计算,这样既提升速度,也提升准确率。比如用户问的是“2024年售后政策”,你可以先过滤时间为2024年的文档,再在剩余文档里做语义检索。
存储和召回这块,我强烈建议你把“召回日志”记录下来:每个问题召回了哪些片段、分数是多少、最终答案引用了哪些来源。没有日志,出问题时你只能抓瞎。日志是后面调优和排查的重要依据。
3. 实操:手把手搭一个最小可用RAG管道
理论说了一堆,下面来做点能跑的东西。我用Python,选型尽量简单:langchain负责文档加载和文本切分,OpenAI或textembedding模型做嵌入,FAISS做向量索引。这一步的目标不是做一个生产系统,而是让你看到一个问题从输入到输出经过了哪些环节,以及每一环节的参数到底影响什么。
3.1 技术选型:先跑通要紧
技术选型的原则是:能少依赖就少依赖,能看清内部逻辑就看清。我见过很多新手一上来就部署Milvus、上K8s、接Elasticsearch,结果调了一周环境问题,连RAG本身的原理都没摸到。正确路径是:先用单机内存版向量库跑通一条数据,再考虑扩展。
依赖安装很简单:
pip install langchain langchain-community langchain-openai faiss-cpu openai如果你不想用LangChain,也可以自己写加载逻辑。但在没有明显痛点之前,用LangChain能省很多时间。需要注意版本兼容问题,LangChain更新很快,我下面的代码以2025年的稳定版为基准,实际运行时如果遇到API变动,优先查对应版的文档。
3.2 代码实现:从文档导入到问答一回
假设我们有一个docs/目录,里面放了几篇Markdown格式的公司产品说明。我们把它加载、切分、向量化、存入FAISS,然后写一个最简单的查询函数。
from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS from langchain_openai import ChatOpenAI from langchain.schema import HumanMessage, SystemMessage # 1. 加载文档 loader = DirectoryLoader( "./docs", glob="**/*.md", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"} ) docs = loader.load() # 2. 切分 splitter = RecursiveCharacterTextSplitter( chunk_size=400, chunk_overlap=80, separators=["\n\n", "\n", "。", ".", " ", ""], ) chunks = splitter.split_documents(docs) # 3. 向量化 + 存储 embeddings = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = FAISS.from_documents(chunks, embeddings) vectorstore.save_local("faiss_index") # 4. 检索 + 生成 def rag_answer(question: str, top_k: int = 5): db = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True) hits = db.similarity_search_with_score(question, k=top_k) context = "\n\n".join([doc.page_content for doc, score in hits]) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1) messages = [ SystemMessage(content="你是一个知识问答助手,只能根据给定的上下文回答问题。" "如果上下文中没有答案,请明确说‘知识库中没有相关信息’,不要编造。"), HumanMessage(content=f"问题:{question}\n\n上下文:\n{context}") ] return llm.invoke(messages).content, hits这段代码里有两个容易被忽略的点。第一,temperature=0.1,生成任务里不要把温度调太高,否则模型容易在不确定时发挥想象力。第二,SystemMessage里明确限制模型“只能根据上下文回答,没有就直说”,这是抑制幻觉的第一道防线。
跑一次查询:
answer, hits = rag_answer("我们公司支持哪些支付方式?") print(answer) print("召回片段数:", len(hits)) for doc, score in hits: print(f"分数={score:.4f}, 来源={doc.metadata.get('source')}, 内容前50字={doc.page_content[:50]}")你会看到输出包含答案和召回的原文片段。检验RAG是否跑通,不是只看答案漂不漂亮,而是先看召回片段是否相关。如果召回片段本身牛头不对马嘴,那再好的生成模型也救不回来。
3.3 评估召回效果:别只凭感觉
“看起来还行”“好像回答得挺准”,这种主观感受不能作为上线依据。我建议你用一个最简单的评估方式:准备10到20个业务问题,每个问题注明它对应哪份文档的哪个片段,然后写脚本批量跑检索,统计召回片段中是否包含对应原文。
评估指标用两个就行:
hit rate:被测试问题中,正确答案片段是否出现在召回前k条里。比如20个问题,有16个的正确片段被召回到前5,那hit@5就是80%。MRR (Mean Reciprocal Rank):正确答案片段在召回列表中的排名倒数,取平均。如果排名越靠前,MRR越高。
手动构造测试集虽然要花一点时间,但它能让你快速发现切分粒度和检索参数的问题。我在实操中遇到过这样一个案例:我原本以为问题是embedding模型不够好,反复换模型都提升不大。后来一查日志,发现大量查询命中的都是同一份文件里的低质量片段,而正确答案排在十几位。问题出在切分时把表格拆散了,字段和数值都被分到不同chunk。修正切分逻辑后,hit@5从52%直接提到86%。
所以,第2章里强调的“切分是隐藏杠杆”就在这里。光调向量库参数不解决内容结构问题。评估工具也不用复杂,一个脚本就够了。生产环境可以接LangSmith或自建记录系统,但现在我们先把度量能力养起来。
4. 从基础RAG到Agentic RAG:知识管道的升级路径
基础RAG能解决60%的“查资料”问题,但它不够聪明。它的问题是:无论用户问什么,都做一次相似度检索,然后无脑用料生成。真实场景里,用户的问题可能是多跳的、模糊的、需要推理的。这时候就需要让Agent参与知识获取的过程,也就是所谓的Agentic RAG。
4.1 朴素RAG的痛点
我用三个真实场景说明朴素RAG的局限。
第一,问题表述与文档风格差异大。用户问“最近能不能申请居家办公”,但文档里写的是“员工弹性办公管理办法”,关键词完全不重叠,向量相似度得分很低,召回结果自然差。如果Agent先把问题改写成更规范的检索查询,比如“弹性办公申请条件”,效果立刻不同。
第二,需要多跳检索。用户问“谁负责审批我们的外读申请,他平时在哪个办公室?”这个问题要拆成两步:先找到制度文档里关于外读审批的章节,再从组织架构文档里找到审批人的办公室信息。朴素RAG一次检索不可能同时命中两个文档片段。Agent可以把这个问题拆成两个子查询,分别检索,再汇总。
第三,需要判断什么时候不检索。用户跟Agent闲聊“今天天气怎么样”,这时候去知识库检索是浪费。又比如用户问的是数学模型题,应该让模型直接推理而不是检索。Agent可以先用意图识别判断需要调用知识库还是直接生成。
4.2 何时引入Agent判断
我不建议一上来就做复杂的Agent编排,而是先给检索增加一层“路由控制”。用一个较弱的分类模型或规则,判断用户问题是否需要检索;如果需要,是检索整个知识库,还是指定某一类文档。这一步能过滤掉大量无效召回。
接下来增加“查询改写”。对原始问题做扩充或改写,让它更适合向量检索。比如原问题是“上次说的那个报销规则”,改写为“差旅报销规则及报销时限规定”,能明显提升命中率。这时候可以用一次LLM调用完成改写,成本不高,收益很大。
再往后就是“多轮检索”。对复杂问题,用Agent的工作流循环:执行检索,评估召回片段是否足够回答;如果不够,再看缺什么条件,重新生成查询再检索。这个“评估-再检索”的循环就是Agentic RAG和普通RAG的分水岭。比较轻量的方案是LangGraph里构建一个带条件分支的图,节点包括“检索”“评判”“生成”,评判节点决定是否回到检索节点。
我建议的落地节奏是:先把基础RAG跑稳,评估指标达标了,再加查询改写,再上路由,最后做循环评判。中间每一步都加日志和评估,不要一口气堆出个黑盒系统。
4.3 可落地的小项目练手
如果你看完这篇想动手,我推荐三个练手项目,由易到难。
第一个是“文档问答机器人”:用公司或自己的笔记库搭建一个本地RAG,要求答案必须引用来源,做一个简单的命令行或网页交互。重点练习文档清洗和切分。
第二个是“多知识库路由Agent”:把文档分成技术、人事、财务三个库,先用小型分类器识别问题的归属域,再路由到对应库检索。重点体会元数据过滤和路由对准确率的提升。
第三个是“多跳问答工作流”:用LangGraph构建一个三步循环,第一步检索、第二步判断信息是否足够、第三步决定再检索或生成。数据集可以用一些公开的开放域QA数据,或者干脆用你自己的行业文档构造多跳问题。重点体会Agent编排对复杂问题的增益。
这三个项目做完,你对RAG的理解会比单纯读十篇文章深刻得多。而且每一步都能积累可复用的工具函数,后面做生产系统心里会有底。
5. 常见问题与排查技巧实录
最后写点实操中踩过的坑。这里整理成一张速查表,然后挑几个重点展开。
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 回答里出现知识库没有的内容 | 检索到了低相关片段,或System Prompt约束不足 | 提高分数阈值,增加来源引用,强制模型“不知道就说不知道” |
| 答案正确但引用的片段不对 | 切分时上下文断裂,语义不完整 | 增加chunk_size或overlap,保留段落边界 |
| 检索结果与问题无关,但模型还硬答 | 查询用词与文档用词差异大 | 加一层查询改写,或使用混合检索 |
| 特定文档永远召回不到 | 切分时破坏了表格或结构,或文档未清洗 | 针对该文档单独设计清洗和切分规则 |
| 召回速度慢 | 向量库未加索引,或元数据过滤缺失 | 建立HNSW/IVF索引,先过滤再检索 |
| 成本过高 | embedding和LLM调用频繁 | 增加缓存,相似查询复用结果 |
5.1 查不到或查到乱:切分与检索参数排查
“查不到”通常分两种情况:一是完全没召回相关内容,二是召回了但排名太低。先确认切分后的片段本身是否包含完整答案。一个简单的方法是直接读几个切分片段,人工判断内容是否可读。如果一段话被从中间腰斩,比如把一个产品参数表拆成了上下两半,那基本就能确定问题在切分。
接下来检查检索参数。top_k是否太小?试试10。相似度计算方式是否合适?有的向量库默认用余弦相似度,有的用点积,如果你之前用OpenAI嵌入,注意OpenAI官方文档明确建议用余弦相似度。FAISS的IndexFlatIP是内积,需要把向量归一化后才等价于余弦。别小看这个细节,我见过有人纠结半天,最后发现相似度都算反了。
还有一个很隐蔽的坑:中文标点与英文标点统一问题。文本切分时,如果原文档里用的是英文逗号,而查询输入用的是中文逗号,某些embedding模型对这类细节比较敏感。清洗时尽量把标点统一成中文或英文一种形式。
5.2 幻觉依旧存在:召回增强与答案生成的边界
有人以为RAG可以彻底消除幻觉,这是误解。RAG只是降低幻觉概率,模型依然可能因为上下文里的矛盾信息而产生错误推理。比如知识库有新旧两版政策,召回时同时命中,模型可能各取一半,拼出一个不存在的规则。
解决办法分两边。召回侧:在知识入库时给文档打上版本号、生效日期等元数据,并把这些元数据拼进片段文本中,比如在每段前加一行“【来源:2025年新版员工手册】”。这样模型更容易识别冲突。生成侧:让Prompt要求模型“如果你发现上下文中存在相互矛盾的信息,请明确指出并优先采用最新版本”,这能在一定程度上减少误导。
另外要给答案加上引用标记。让模型在回答里标注“(来源1)”“(来源2)”,对应召回的片段编号。这样用户和开发者都能快速核验。我一般在SystemMessage里加一句“每个关键信息后面用方括号标注来源编号”,然后在返回结果时把编号映射到元数据。
如果还是频繁幻觉,我的排查顺序是:先看召回片段是不是真的包含正确答案,再看Prompt是否约束到位,最后才考虑换更强的模型。很多团队一遇到幻觉就换大模型,纯属浪费钱。
5.3 性能与成本平衡
RAG管道的成本大头通常不在向量化,而在生成阶段的LLM调用。每次检索生成都要消耗几千token,如果Agent还做多轮改写,成本翻倍。优化思路有几种。
第一,做检索缓存。把问题和对应答案存进缓存,同一个问题只计算一次。第二,对生成模型做分级:简单问题用迷你模型,复杂问题才用强力模型。可以通过召回到的片段数量、长度、置信度来触发模型升级。第三,减少无效上下文:召回的top_k不等于必须全部塞给模型。如果前3个片段已经很相关,后面2个明显弱相关,就只送前3个,既省token又减少干扰。
向量化成本也别忽略。如果每天有大量新文档入库,每次都全量重算嵌入会非常慢。建议做成增量流程:记录每个文档的哈希值,内容没变就直接跳过,只处理新增或修改的文件。知识库做到后面,维护成本和构建成本一样重要,管道要把这两件事都考虑进去。
我个人在实际操作中的一点体会是:RAG调试最忌“头痛医头”。经常有人问为什么召回不准,第一反应是换embedding模型;问了半天发现是切分有问题,切分改好了,又发现是查询表达有问题。整个管道是一条链路,要带着测试集和日志去定位,而不是靠感觉调参。我建议你从现在开始,每做一个RAG项目,先写一个简单的评估脚本,把hit@k和MRR跑出来,再动手优化。有了数字,很多争论会自然消失。
最后分享一个小技巧:在做完最小管道之后,把召回结果打印出来,去读一读那几段被召回的文本。很多时候你一眼就能看出哪里不对劲,这比任何指标都直观。RAG的门槛不高,但它是一门需要耐心打磨的实践手艺。希望这篇能把你的第一根知识管道顺利铺出去,让它真正成为Agent的稳定“外接大脑”。