做 AI Agent 项目做得多了,你会发现一个很拧巴的现象:模型能力越强,用户越喜欢拿它当知识库用,问的全是你岗位里那点内部文档、历史项目、数据口径。可模型自己根本没见过这些东西,它只能靠训练时学来的"常识"硬撑,结果就是答得流利,却错得离谱。
这一篇我们专门聊 AI Agent 的"知识获取管道",也就是 RAG(检索增强生成)基础。标题里的"管道"两个字,我要重点强调一下:很多人把 RAG 理解成"接个向量数据库就完事",但一个真正能在 Agent 里长期运转的知识获取链路,远比"搜到就返回"要复杂。前几篇我们把 Agent 的工具调用、任务编排讲了一遍,这次把知识这块补上,属于整个系列里最偏"地基"的一篇。适合已经写过简单 Agent、准备给它接私有知识的同学。
1. 为什么 Agent 不能只靠"模型自己知道的内容"
先从我实际接手过的一个项目说起:客户做企业内部知识助手,知识库里有产品手册、售后流程、财务制度、架构成档,总共几百篇文档。最开始没人想过做 RAG,方案是"把文档全塞进提示词里让模型读",结果第一条上线反馈就把我钉在耻辱柱上——用户问"退货周期是多久",文档里写的是"售后退款时限",模型翻遍提示词找不到"退货"两个字,愣是答出了"一般情况下 1-2 周",实际业务标准是 3 个工作日内必须出审批结果。
这就是典型的知识割裂:词面不匹配,语义却完全是一回事。模型的能力再强,它也做不了"自己不知道的知识"的检索,因为它在生成回答时根本没有办法判断该去哪里取证据。这不是提示词能解决的问题,而是需要一条管道,把"用户的问法"转成"知识库里能被定位的证据"。
1.1 模型权重里存的是能力,不是你要的答案
这里要先给一个基础认知:模型训练学到的知识,和业务系统里的知识,是两个完全不同的东西。前者是"语言规律+公共知识"的统计压缩,后者是"你所在组织独有的、动态变化的、需要交叉验证的事实"。你可以通过微调让模型记住一部分内部术语,但你没法通过微调让它实时跟上文档更新,更没法在每次回答时告诉你"这个答案出自哪一篇、第几节"。
我自己做过一次对照组测试:同一批公司制度文档,A 组只靠微调,B 组用 RAG。结果在涉及具体数字、流程节点、责任人这类"低容错"问题上,A 组的正确率只有 51%,B 组可以达到 88% 以上。微调适合让模型"变专业",RAG 适合让模型"查得到"。Agent 要落地,二者不是二选一,而是各管一段。本文的核心场景,是把 RAG 这段做到可维护、可观察、可升级。
1.2 Agent 需要的不是搜索框,而是一条管道
你可能会说:企业内部早就有全文搜索,直接"搜索+复制给模型"不就行了?这是个很容易踩的误区。关键词搜索返回的是网页列表,它不知道哪一段是回答问题的关键证据,更不会做语义改写。用户问"订单被拒了怎么处理",文档里写的是"支付状态异常时的申诉流程",词面匹配大概率漏掉。
管道的价值在于,它把"提问"到"证据"到"回答"之间的所有环节都拆成了可替换的组件:
用户提问 → 查询改写/理解 → 知识库切块召回 → 候选结果重排 → 组装上下文 → 交给生成模型 → 返回回答。
这里面每一个箭头,都是一个可以通过测试来优化的环节。搜索框只有一个步骤,而管道是一整条流水线。RAG 基础阶段,你不用把每个环节都做到极致,但你要先知道这条线上有哪些"阀门"。
1.3 一个最小管道里,每段分别解决什么问题
按我的经验,给零基础同学讲 RAG,最好先画清楚四个模块:
- 文档预处理:把 PDF、Word、Markdown 清洗成干净的文本块,去掉页眉页脚、水印干扰;
- 索引构建:把文本块切分成合适的片段,再用嵌入模型转成向量,存进向量库;
- 召回阶段:用户提问也转成向量,去向量库里找最相似的若干片段;
- 重排与生成:取回 Top N 片段,用重排器精排,最后拼进提示词,让模型只依据这些片段作答。
下面几节,我会把每一段为什么这么做、不这么做会怎样讲透,再给一个能在你本机直接跑通的最小实现。
2. 拆开 RAG:嵌入、切分、召回、重排,每一环都有自己的脾气
很多人跑通第一个 RAG demo 只需要一小时,但跑通之后一测真实问题,命中率马上就露馅。原因通常是:他们把所有希望押在了"嵌入模型"上,以为向量相似度高就等于回答正确。实际上,四段里任何一段偷懒,整条管道都会变傻。
2.1 嵌入模型:把语言变成空间里的位置
嵌入模型做的事,是把一段文本映射成一个高维向量,让语义相近的句子在向量空间里距离更近。比如"退货周期"和"售后退款时限",如果嵌入模型够好,两个向量的距离应该很近。
选嵌入模型时,我建议你直接做一个小实验:拿 20 条用户真实问题,分别用两三个候选模型编码,再去知识库里跑一遍召回,看谁先召回正确片段。不要只看榜单分数,因为榜单是在通用语料上评的,你的语料有自己的行业黑话。中文场景下,我现在常用的是bge-m3这类多语言或者中文优化模型,效果稳定,部署也不重。
还有一个常被忽略的点:嵌入模型对输入长度有窗口限制。一般 512 token 左右,超过之后会截断。这直接决定了你的知识块不能无限长——块太长,语义被稀释,向量全往中间靠,检索结果跟随机差不多。
2.2 切分策略:召回上限在切分那一步就决定了
切分是 RAG 基础里最容易被低估的环节。你切出来的块,就是将来模型能看到的"最小证据单元"。块太小,证据不完整,比如一个流程的三个步骤被拆到三个块里,模型只拿到第一步;块太大,一个问题对应十几个块,向量相似度被无关文字拉低,精确答案反而不突出。
我的起步参数一般是chunk_size=600字符、chunk_overlap=100字符,然后根据文档类型调整。普通企业制度文档、项目文档,这个范围基本够用;如果文档里有大量表格,我会把它转成 Markdown 表格句式再切,避免表格结构被拦腰斩断。
更好的做法是"结构感知切分":Markdown 文档按标题层级切,PDF 按章节切,代码文档按函数边界切。一句话原则:宁可让一个块稍长一点,也不要让一个语义单元被切散。
2.3 召回与重排:宽召回,精排序
召回阶段最常见的错误,是k值设得太小——只取 Top 3 就交给模型。向量检索的召回率本身不是百分百,正确证据有时排在第五、第八位。你只取前三,等于把正确答案直接扔了。
我的习惯是:召回 10 到 20 个候选,然后用一个重排模型(Cross Encoder)逐对打分,再取 Top 3 到 5 个放进提示词。为什么不能一开始就用重排模型扫全库?因为重排模型要对每一对"问题+文档"做一次完整推理,几千个文档下来耗时不可接受。向量检索是快速粗筛,重排是精判,两段分工,成本和效果都友好。
这里把两个阶段的定位整理成一张表,方便你对照排查:
| 阶段 | 常用工具/模型 | 目标 | 常见翻车点 |
|---|---|---|---|
| 嵌入 | bge-m3、text-embedding 类 | 语义向量化 | 窗口截断、行业词不敏感 |
| 切分 | RecursiveCharacterTextSplitter 等 | 语义单元完整 | 表格被切开、标题与正文分离 |
| 召回 | 向量库 TopK、MMR | 提高召回率 | k 值太小、相似度阈值形同虚设 |
| 重排 | CrossEncoder、bge-reranker | 提高精确率 | 跳过重排,直接塞 TopK |
| 生成 | 任意 LLM | 依据证据作答 | 提示词未限制"只依据资料" |
有的项目为了省事,直接用向量距离当最终排序,效果也能用,但一旦知识量上来、文档间相似度变高,你就知道重排这一环省不得。
3. 从零搭一个最小 RAG:索引、检索、重排和生成,跑通一遍
说再多理论,不如直接跑一个能用的 demo。下面这套是我在自己笔记本上验证过的流程,依赖少、不烧钱,适合作为你后续扩展的骨架。我用的是 LangChain 做胶水,FAISS 做本地向量库,嵌入和重排都用本地模型,整个索引过程不依赖任何外部 API。
3.1 环境准备和选型原因
先说明为什么这样选:本地嵌入模型可以离线跑,避免你每一轮实验都花 API 费用;FAISS 是单机向量库,不需要启动服务,最适合起步阶段;LangChain 在这里只是把加载、切分、检索串起来,你完全可以不用它,但用了能少写很多样板代码。
需要安装的依赖:
pip install langchain langchain-community langchain-huggingface faiss-cpu sentence-transformers如果你机器上没有 GPU,faiss-cpu也够用,我测过几十万条向量的检索,毫秒级返回,单机起步完全没压力。
3.2 构建索引:加载、切分、嵌入、入库
假设你有一个docs目录,里面是整理好的 Markdown 或文本文件。下面这段代码会读进来、切块、向量化、存成本地索引:
import os from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS loader = DirectoryLoader( "./docs", glob="**/*.md", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"}, ) docs = loader.load() splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=100) chunks = splitter.split_documents(docs) embedding = HuggingFaceEmbeddings(model_name="BAAI/bge-m3") index_path = "./faiss_index" if os.path.exists(index_path): vectorstore = FAISS.load_local(index_path, embedding, allow_dangerous_deserialization=True) else: vectorstore = FAISS.from_documents(chunks, embedding) vectorstore.save_local(index_path)注意allow_dangerous_deserialization=True这个参数:FAISS 本地文件可能包含序列化的对象,只在你完全信任本地文件来源时才开。别人的索引文件不要乱加载。
3.3 检索、重排与答案生成
索引建好后,核心的检索逻辑如下:
query = "智能体在什么情况下应该调用外部工具?" hits = vectorstore.similarity_search_with_score(query, k=10) for doc, score in hits[:5]: print(f"向量距离: {score:.4f} | 来源: {doc.metadata.get('source', 'unknown')}") print(doc.page_content[:80]) print("---")向量距离越接近 0 代表越相似,但具体阈值因嵌入模型而异,不要拿一个固定值套所有模型。接下来用重排器把候选重新排序:
from sentence_transformers import CrossEncoder reranker = CrossEncoder("BAAI/bge-reranker-base") pairs = [(query, doc.page_content) for doc, _ in hits] scores = reranker.predict(pairs) ranked = sorted(zip(scores, hits), key=lambda x: x[0], reverse=True) top_docs = [doc for _, (doc, _) in ranked[:4]]重排分数越高代表越相关。把重排后的 Top 4 拼进提示词:
context = "\n\n".join([doc.page_content for doc in top_docs]) prompt = f"""你是企业内部知识问答助手。 请只依据下面的“参考资料”回答问题,不要编造资料中没有的信息。 如果资料不足以回答,直接回复“知识库中未找到相关信息”。 参考资料: {context} 问题: {query} """最后接一个兼容 OpenAI 接口的模型。我本地用的是 Ollama 跑 qwen2.5,代码同样适用 OpenAI 的接口:
from openai import OpenAI client = OpenAI(base_url="http://localhost:11434/v1", api_key="EMPTY") resp = client.chat.completions.create( model="qwen2.5:14b", messages=[{"role": "user", "content": prompt}], ) print(resp.choices[0].message.content)到这里,一个"文档问答"的最小 RAG 已经通了。注意:整套流程里,真正需要调模型的只有检索后的重排和最终生成两步,其他环节都是确定性的。这条特性很重要,后面讲 Agent 集成时会反复用到。
3.4 用一个"命中率"指标给自己打分
demo 跑通不等于有效。我习惯在起步阶段就建立一个小型评测集:准备 20 到 30 个你业务里真实会出现的问题,每个问题人工标注"正确证据出现在哪一段"。然后跑一遍检索,看正确证据是否出现在 Top 5 返回结果里,统计命中比例,这就是检索命中率(Hit Rate)。
我见过太多团队把全部精力花在调生成模型上,结果其实是召回环节根本没把证据找回来。先测检索命中率,如果连这一步都不及格,后面无论换什么模型都救不回来。评测集不用多,每个迭代周期维护二三十条就够,关键是这些问题是真实的,不是你自己编出来的顺风问题。
4. RAG 文献踩坑记录:四个问题,完整排查链路
这一节我不想干巴巴地列注意事项,直接把我遇到过的几个真实故障和排查思路写出来,你照着排查手法走一遍,比背结论管用。
4.1 症状一:同一段知识,换个问法就查不到
项目初期,用户问"结算周期"查得到,问"多久结算一次"就空手而归。第一反应是换嵌入模型,换完之后依旧不行。后来我冷静下来,没有继续在模型上使劲,而是去翻索引文本本身:原文档里把"结算周期"定义在一篇很长的合同模板中,而这份模板 60% 的篇幅是法律条款和公司抬头。
切块策略就是根因:chunk_size是 1000,整段模板被切成四五块,真正的定义句只有十几个字,被埋进了一大堆无关文本里,向量特征被稀释。
排查链路是:先确认其他问题能否正常召回,排除向量库损坏;再打印召回的前十条,发现正确块并非没被召回,而是排在第八位;然后检查该块的完整内容,发现大量噪音。修复方式是退回 600 字切分,把切分逻辑改成"按章节先切一次,再对超长章节按 600 字二次切分"。换模型之前,先看看你的证据块干不干净,这是最省钱的排查顺序。
4.2 症状二:答案答得流利,但引用来源是错的
这个问题更隐蔽:生成答案看着通顺,可你去看它引用的原文片段,会发现跟答案对不上。我一直强调,RAG 不是"模型自己会找证据",而是"你给它什么它用什么"。模型擅长顺着上下文把话圆过去,所以当 Top 1 结果里有一段语义相近但并非答案的文本时,它可能就顺着编了。
排查时我用了最笨但最有效的方法:把提示词里的"只依据参考资料作答"换成"你必须在答案末尾列出依据片段编号,如果参考中没有依据,只能回答不知道"。模型行为立刻发生改变,很多幻觉其实是提示词给了它发挥空间,而不是检索出了问题。
另一个修复手段是"引用回溯":要求模型在答案里用编号标注用了哪段参考,程序端再做一次校验,看答案中的关键数字或结论是否真的出现在标号片段里。即使做不到全自动校验,至少人工 review 时有据可查。
4.3 症状三:知识库更新后,老答案依旧阴魂不散
有一次我们整体替换了产品手册版本,删掉了旧文档,也重建了索引,但测试时模型依然会提到已经废弃的功能名。查了一圈,原因出在更新流程上:我们的旧索引并没有被清空,新索引是"追加写入"的,旧块还躺在向量库里,检索时偶尔被捞回来。
排查链路:先确认检索返回文档的元数据,发现另一份文件名;去docs目录确认该文件已删除;代码里定位到from_documents每次都是全量重建,但因为我的save_local多次执行,没有覆盖旧目录。修复方式:每次重建索引前先删除旧索引目录,并且在加载时给索引加上一个"构建批次号"元数据,检索结果里过滤掉非当前批次。
4.4 症状四:PDF 导进来不少,但检索时永远找不到几页的内容
这个坑最基础也最容易被新人忽略:有些 PDF 是扫描件,没有文字层。DirectoryLoader读进来的文本是空白或乱码,嵌入进去的向量全是噪音。推荐做法是起步阶段统一用PyPDFLoader或pypdf先校验每页是否提取到足够文本,低于阈值就标记出来人工处理或接入 OCR。
这几类问题的通用排查顺序我整理成了这张表:
| 现象 | 先查什么 | 再查什么 | 最后考虑 |
|---|---|---|---|
| 差得远,完全无关 | 文档加载是否正常 | 切分是否把语义切碎 | 嵌入模型是否适配行业 |
| 相关但排位靠后 | 召回 k 值是否太小 | 块内噪音是否太多 | 是否缺少重排环节 |
| 生成答案引用不准 | 提示词是否有"只能依据资料" | 参考片段是否包含答案 | 是否缺失引用校验 |
| 更新后无效 | 索引是否全量重建 | 是否存在旧文件残留 | 检索是否有批次过滤 |
5. 把 RAG 接进 Agent:它不是后台脚本,而是要变成一件"工具"
基础 RAG 跑通后,你要把它嵌进 Agent。很多人的第一反应是把 RAG 当作一个retrieve(query)函数直接丢给 Agent 调用,但这样做的效果并不好。问题在于:Agent 在编排层,它本身不擅长判断"哪些问题该走向量检索"。
5.1 给 Agent 一个"检索工具",而不是一个后台函数
正确的做法是把 RAG 封装成一个带明确描述的工具,让 Agent 在需要外部知识时主动调用。工具描述里要写清楚三个信息:这个工具解决什么问题、什么情况下不要调用它、返回结果长什么样。比如:
工具名:企业内部知识检索 用途:查询公司制度、产品手册、项目文档中的事实信息 禁忌:不处理代码运行问题、不处理需要登录系统的实时数据 返回格式:JSON,包含证据片段、来源文件名、相关度分数Agent 收到用户问题时,会先判断"这要不要查知识库",再决定调用。这一步的价值,是让 Agent 把"我该不该查"和"如何作答"两件事拆开,避免所有问题都先查一遍再自由发挥。
5.2 检索上下文要设定边界,不能让 Agent 无限取块
把检索结果直接交给 Agent 时,一个常见失控场景是:Agent 越权决定"再查十个块",结果把上下文塞满,还混进大量无关片段,生成质量反而下降。
你应该在封装工具时就固定上游配置:召回 20 条、重排取 3 到 4 条、证据总长度限制在 1500 字以内。Agent 只负责决定"查或不查""怎么改写提问",至于取多少证据、如何拼接,这些要由工具内部决定。把不确定性留在可控范围内,是 Agent 工程稳健性的关键。
5.3 权限、更新与缓存,是一套知识服务的标配
当多个部门共用一套 Agent 时,权限问题会迅速浮出水面。最简单的做法是给每个知识库分配一个"检索域",在工具描述中带上workspace_id,检索时按这个字段过滤。别指望模型自己"自觉不越权",你要在索引元数据里就隔离干净。
更新方面,我见过最稳妥的方案是"文档级版本号":每次知识库重建,给所有文档打上批次号;Agent 检索时只看最新批次。缓存则要小心:同一问题短期内可以命中缓存,但知识库一旦更新,缓存必须同步失效,否则旧答案会比新知识还活跃。
5.4 观测你的 Agent:RAG 出问题到底是哪一段的锅
基础 RAG 的日志,至少要包含这几项:原始问题、改写后的问题(如果有)、召回的候选片段 ID、重排后的最终片段、生成答案、耗时。我排查线上幻觉问题,几乎全靠这六项日志。看到"答案错误",先定位是候选片段压根没有证据,还是证据在但生成阶段歪曲了它,这两种情况的修复手段完全不同。
这其实是把 RAG 当"服务"来运营的思维。基础 RAG 是一本流水账,而 RAG 服务是一套可观测、可控制、可独立的系统。等你的 Agent 开始对外服务时,才会发现这个抽象有多救命。
6. 基础 RAG 的上限:遇到哪些情况,该考虑升级方案了
RAG 基础并不复杂,但它在某些问题上确实有天然短板。我自己的判断标准很简单:如果 20 条评测问题的命中率和正确率连续两个迭代周期没有提升,往往不是调参的问题,而是这个方案本身就触碰了上限。
6.1 基础 RAG 的典型失灵场景
- 跨文档综合题:需要把三个文档里的信息拼成一个结论,"A 文档定义流程,B 文档定义负责人,C 文档定义时限",基础 RAG 很难把三块证据稳定凑齐;
- 同义词/多源冲突:两份文档口径不一致,基础 RAG 只会取相似度最高的一段,不会主动发现矛盾;
- 需要全局理解:知识库整体讲了一个"为什么",但答案需要站在全局总结,只靠局部片段回答,必然以偏概全;
- 强调 schema 一致性:比如需要严格按某个字段结构输出,向量检索本身不保证这一点。
6.2 升级方向:Agentic RAG、GraphRAG、Ontology RAG 怎么选
你很可能在热搜词里看到过这几个名词。简单说:
- Agentic RAG:让 Agent 自己决定检索策略,比如先查一次,发现证据不足再换关键词查第二次,或者把问题拆成多个子问题分开检索。适合跨文档综合题;
- GraphRAG:把文档里的实体和关系抽成图,再做全局或社区级别的检索。适合"知识之间有网状联系、需要全局总结"的场景;
- Ontology RAG:先用一套本体约束实体的类型和关系,再基于图谱检索。适合对输出结构要求严格的领域,比如医疗、制造、法规;
- LLM Wiki:把知识持续沉淀成"渐进式条目",类似人工维护的 Wiki,由模型不断整理合并。适合需要长期积累、持续修正的知识体系。
我的建议很明确:如果你现在只是"单点事实问答"还没做好,就不要上任何升级方案。先把基础 RAG 的切分、召回、评测、日志做到位,你会发现它已经解决了 Agent 知识获取的七成问题。剩下的三成,等你真正遇到具体瓶颈时,再按需升级,而不是提前架构。
这套基础管道,你在本地用几百行代码就能完整跑通。我个人的体会是:别急着把 Agent 编排得花团锦簇,先让知识获取这件最朴素的事变得可靠。等你哪天深夜被用户叫起来改 bug,发现能快速定位到"是召回没召到、还是生成没用好",你就知道今天这五千字没有白写。