你正在做一个 AI Agent好不容易把工具调用、任务规划、对话记忆都调通了结果发现一个尴尬的事你问它“咱们上季度的退款率是多少”它一本正经地编了个数字。你质问他它说“我是基于公开知识推理的”。这个场景我见过太多次了。模型的参数知识是“截止日期”式的过去式而你的业务知识、私有文档、产品手册、售后记录它一样都没见过。这就是知识割裂。模型不是不聪明是它脑子里的“题库”里根本没有你公司的题。解决这个问题的标准姿势就是给 Agent 装一条外部知识管道也就是 RAG——Retrieval-Augmented Generation检索增强生成。这一篇我会把 RAG 最底层的原理、最小可用的实现、以及那些不跑一遍绝对踩不到的坑完整讲清楚。适合正在从 0 搭 Agent、准备把私有知识库接进 AI 应用、以及在 RAG 的“能跑”和“好用”之间反复挣扎的开发者。我默认你了解大模型的基础调用方式但如果你对 LangChain 不熟也没关系我会把每一步为什么这么做讲明白。1. 先搞明白AI Agent 为什么要靠 RAG 喂知识1.1 知识割裂模型记忆力再强也不等于懂你的业务大模型和搜索引擎最大的区别在于搜索引擎是“帮你找”大模型是“替你记”。但“替你记”有个硬伤它的知识在训练完成那一刻就冻结了。你的业务文档、内部 FAQ、商品目录、工单记录大概率根本不在它训练的数据里。就算在也是几个月甚至一年前的旧版本。你可以把模型理解成一个能力很强但没做过你们公司业务的新员工。它能写代码、能做推理、能写文案但你问它“我们公司 API 的鉴权流程是什么”它只能从泛化的经验里猜。这个“猜”就是 AI Agent 落地时最大的风险来源。模型一本正经地输出错误信息比它直接说“不知道”可怕得多因为前者会让下游流程拿假数据当真。RAG 解决的就是这个“知识割裂”问题。它的核心思路特别朴素模型不知道没关系你先帮它查查回来之后把相关资料塞进它的上下文让它“看着资料回答”。听起来像作弊但这就是目前业界让大模型在专业领域可用、可控、可追溯的最务实手段。1.2 RAG 在 Agent 架构里的位置不是外挂是记忆系统我见过不少人在给 Agent 写“记忆”功能时只在 Session 里存聊天记录这远远不够。聊天记录是“短期记忆”RAG 负责的是“长期记忆”加“工作记忆”的结合体。当 Agent 收到一个问题它先决定“这个问题需要哪些资料”然后通过 RAG 从知识库里检索把结果放到上下文窗口里再开始生成回答。在 Agent 的决策循环里RAG 扮演的是“信息供给方”。它不直接生成答案它只负责把相关的片段捞出来。Agent 的主流程负责判断“该信谁”“该用哪段”“证据够不够”。如果你把 Agent 比作一个分析师那 RAG 就是他的资料员。资料员的作用不是替他做判断而是保证他手边有对的材料。这决定了一个很关键的设计原则RAG 管道的输出必须可以被 Agent 的结构化逻辑信任比如分数、来源、时间戳。你不能随便返回一堆不相关的文本让 Agent 自己去大海捞针。后面我会详细讲为什么检索质量的评估比单纯“能聊”重要得多。1.3 什么时候该上 RAG什么时候该微调很多朋友一上来就问我是做 RAG 还是微调我的个人经验是如果一个知识是“查得到”的就用 RAG如果是一种“能力”或“风格”才考虑微调。你要让 Agent 学会回答“你们产品的退货政策是什么”这属于查资料RAG 完美胜任。但你要让 Agent 模仿某个作家的文风、或者学会一种特定的数据抽取格式且必须 100% 严格遵守那 RAG 做不到——它不是改变模型的“肌肉记忆”只是临时喂料。微调才是把某种行为模式刻进模型权重里。现实中绝大多数企业场景是 RAG 为主、微调为辅。先跑通 RAG把真实数据拉进来发现问题确实是模型理解能力不足再考虑针对性微调。一上来就微调成本和坑都比 RAG 高一个数量级而且微调后的模型依然不知道你仓库里最新的 PDF 写了什么。2. 从零搭建一条基础 RAG 管道关键四步2.1 文档加载不是所有 PDF 都能被干净地读取RAG 的第一公里是文档加载。这步看起来是最没技术含量的但在实际项目里坑几乎全在这。我有一次做企业知识库对方丢过来几十个 PDF结果一加载里面全是扫描图片文字全是碎的。你要先想清楚你手上有什么PDF、Word、Markdown、HTML、数据库记录还是网页我的推荐的顺序是结构化数据 半结构化数据 纯文本。能用数据库查的就直接让 Agent 调工具查别塞进 RAG。只有那些非结构化文档才需要走加载、切分、向量化这条路。对于 PDF我建议用能保留版面结构的加载器比如 PyMuPDFLoader 处理可选中文字的电子版 PDF对于扫描版老老实实先过一道 OCR。语言上中文场景用 PyMuPDFLoader 通常比某些英文为主的解析库表现稳定得多。from langchain_community.document_loaders import PyMuPDFLoader loader PyMuPDFLoader(docs/产品手册.pdf) documents loader.load() for doc in documents[:2]: print(f页码: {doc.metadata.get(page)}) print(doc.page_content[:300]) print(---)2.2 文档切分chunk_size 和 chunk_overlap 的取舍加载完文档之后你不能把整本手册丢给模型上下文窗口装不下检索精度也会急剧下降。所以你要把长文本切成小块每块叫一个 chunk。chunk_size 代表每块的最大字数chunk_overlap 代表相邻块之间的重叠字符数。这个参数为什么重要因为如果切太粗一块里混合了多个主题检索时匹配到的碎片不精准如果切太细一块里缺上下文模型看到了“退款”两个字却看不到“退款”是针对哪个商品、哪个时段的。chunk_overlap 就是为了避免把一个句子从中间切断导致语义断裂。经验值方面中文文档我通常先用 RecursiveCharacterTextSplitter按中文标点和段落切分chunk_size 设在 300-500 之间overlap 设在 50-100 之间。注意我说的是“字符数”英文是 token 数中文一个字往往对应 1-2 个 token直接用英文社区的 1000 token 参数会太大。文学性、逻辑连贯性强的文本可以适当调大一些关键词密集的说明书稍微调小一点效果更好。from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size400, chunk_overlap80, separators[\n\n, \n, 。, , , , ], ) chunks text_splitter.split_documents(documents) print(f切分后共 {len(chunks)} 个文档块) for i, chunk in enumerate(chunks[:3]): print(fChunk {i} 字数: {len(chunk.page_content)}内容: {chunk.page_content[:50]}……)2.3 向量化选对嵌入模型比调参更重要切分后的文本需要转换成向量也就是一串浮点数让计算机可以计算“语义距离”。这里最关键的是嵌入模型的选择。文本越长、领域越专业嵌入模型的表现差异就越大。我个人建议中文场景优先考虑国产开源嵌入模型比如 BAAI 的 bge-large-zh-v1.5或者后来出的 bge-m3。它们对中文长文本的支持比很多英文模型好。OpenAI 的 text-embedding-3-small 当然也能用质量也不错但对于中文企业级应用批量调用成本、数据安全、私有化部署这些问题都要纳入考虑。提示别把 embedding 模型和生成模型混为一谈。embedding 模型只负责把文本变成向量训练目标和生成模型完全不同。你换了生成模型embedding 未必需要换但你换了语言范围比如从纯中文换到中英混合embedding 很可能要重选。向量化之后自然要把向量存起来。Chroma 是本地开发最省事的选择跑起来快、接口简单。生产环境可以用 Milvus、Qdrant 这类专业的向量数据库。对于绝大多数中小项目Chroma 足够撑到几十万条 chunk 的规模。from langchain_huggingface import HuggingFaceEmbeddings from langchain_chroma import Chroma embedding_model HuggingFaceEmbeddings(model_nameBAAI/bge-large-zh-v1.5) vectorstore Chroma.from_documents(chunks, embedding_model, persist_directory./data/chroma_db)2.4 检索与生成第一次跑通你的最小 RAG检索这一步本质上是把你手里的向量数据库变成一个问题匹配器。你输入一个问题它把问题向量化和库里的每一个 chunk 向量做相似度计算返回最接近的几个 chunk 块。这之后把“问题 检索到的 chunk”拼成 prompt交给大模型生成最终回答。这条链路从问题到答案就是最小可用的 RAG。我用 LangChain 的接口把整条链串起来方便你理解和复现。from langchain_openai import ChatOpenAI from langchain.chains.combine_documents.stuff import create_stuff_documents_chain from langchain.chains.retrieval import create_retrieval_chain from langchain_core.prompts import ChatPromptTemplate retriever vectorstore.as_retriever(search_kwargs{k: 4}) prompt ChatPromptTemplate.from_messages([ (system, 你是企业知识库助手。请只根据以下资料回答用户问题如果资料中没有答案请直接说资料库中未找到相关内容不要自行编造。\n\n资料\n{context}), (human, {input}), ]) llm ChatOpenAI(modelgpt-4o-mini, temperature0) combine_docs_chain create_stuff_documents_chain(llm, prompt) rag_chain create_retrieval_chain(retriever, combine_docs_chain) result rag_chain.invoke({input: 我们的退款政策是什么样的}) print(result[answer])跑通之后你可能发现两个极端要么答案非常接近要么答非所问。别急着调 prompt大概率问题出在检索端我们接着往下看。3. 检索质量才是 RAG 的灵魂命中率与实用性调优3.1 相似度搜索的局限向量接近不等于语义正确向量检索的底层是“语义相似度”两个句子字面完全不同但意思接近向量也会接近。听起来很美好但实战里你会发现很多反直觉的坑。比如用户问“怎么申请退货”你库里的资料写的是“退换货流程”字面上只有“退货”两个字重合逻辑上其实高度相关。这种场景好的向量模型能抓住一般的模型就抓瞎。更大的坑是向量检索只关心“哪个 chunk 和问题像”却完全不关心“哪个 chunk 是错的答案”。你的文档有 100 份里面如果有两份内容自相矛盾向量检索会同时把它们捞出来。这时候模型就会逻辑错乱一会儿说可以退一会儿说不可以退。所以检索召回之后还要有一步“证据选择”这一步我们放到第四章讲。3.2 用 hit rate 量化评估你的检索器很多人调 RAG 纯靠感觉问几个问题看一眼答案觉得差不多就上线了。这是大忌。检索器的好坏是可以量化评估的最基础的指标是hit rate也就是“命中率”针对一批测试问题看检索器返回的前 k 个结果里是否包含一个标注过的正确文档块。我给你一个实操路径从你的知识库里人工挑 50-100 个真实高频问题为每个问题标注出“正确答案应该来自哪几个 chunk”。然后写一个小脚本用同样的检索器逐个查询统计 top-k 结果的命中情况。这个过程不复杂但能帮你发现到底是切分问题、嵌入问题还是 top-k 数量不够。test_questions [ {q: 你们支持哪些支付方式, gold_chunk_ids: [12, 34]}, {q: 如何修改账号绑定的手机号, gold_chunk_ids: [56]}, ] hits 0 total len(test_questions) for item in test_questions: retrieved vectorstore.similarity_search(item[q], k4) retrieved_ids [r.metadata.get(chunk_id) for r in retrieved] if any(g_id in retrieved_ids for g_id in item[gold_chunk_ids]): hits 1 print(fhit rate: {hits / total:.2%})我实测下来一个检索器 hit rate 如果低于 70%那你生成端再漂亮也是白搭。先提升命中率再谈别的。3.3 混合检索与 RRF 融合BM25 和向量检索互补向量检索擅长语义相似但它有个天然短板对专有名词、精确 ID、产品型号非常不敏感。用户问“RTX 4090 的功耗”如果你的文档里写的是“GeForce RTX 4090 最大功耗 450W”向量模型可能还能关联上但如果用户直接搜“450W”语义模型就抓瞎了。这时候就要把传统的关键词检索BM25拉回来。BM25 是搜索引擎的老技术本质上是看文档里有多少词和查询词重合重合越多分越高。它不懂语义但对精确匹配极其可靠。把 BM25 的结果和向量检索的结果合并用 RRFReciprocal Rank Fusion做融合排序效果通常会好一个档次。from langchain.retrievers import BM25Retriever, EnsembleRetriever doc_list [chunk.page_content for chunk in chunks] bm25_retriever BM25Retriever.from_texts(doc_list, k4) vector_retriever vectorstore.as_retriever(search_kwargs{k: 4}) ensemble_retriever EnsembleRetriever( retrievers[bm25_retriever, vector_retriever], weights[0.3, 0.7], )RRF 的核心逻辑很粗暴每个检索器给出一个排序一个文档在第 n 名就给它 1/(kn) 分k 通常取 60最后把各检索器的分数加起来重新排序。这么做的好处是不需要关心两个检索器的分数范围不同直接排名融合哪边的靠前结果都有机会被选上。我通常把 BM25 权重放 0.3向量检索放 0.7。如果你发现你的场景专有名词非常多比如问型号、问工号、问地址就该把 BM25 权重提到 0.5 甚至更高。这里的权重本质上是在回答一个问题你的用户更依赖“猜意思”还是“找原词”4. 知识割裂场景的工程化处理多源文档实战4.1 多知识源去重与结构化真实企业环境里知识库不会只有一份手册。它往往是多个系统的聚合产品文档、工单沉淀、内部 Wiki、销售话术、竞品分析。这些文档格式不一、质量不一甚至内容互相冲突。直接全部扔进向量库就是灾难的开始。所以第一步永远是清洗与分层。技术类文档、流程类文档、产品介绍类文档我建议分开建索引而且每类文档的 metadata 里一定要写入 source、doc_type、last_updated。这样后续在做 Agent 回答时可以根据问题类型快速限定搜索范围比如只搜“退款政策”结果就不要混入“销售话术”。这比你用一个巨无霸索引然后祈祷向量检索自己分得清要靠谱得多。去重这块也常被忽略。同一份文档如果被多个人上传过不同版本向量库里会出现大量冗余 chunk检索时返回的结果可能一半都在复读同一个观点严重挤压上下文空间。我建议在入库前做一个简单的清洗用文档的标题加内容哈希或者直接对 chunk 内容做 embedding然后计算互相之间的相似度超过阈值就标记为重复并保留时间戳最新的一份。4.2 元数据过滤与权限隔离接上一条metadata 不只是给你自己看的它是检索时最重要的绳。尤其是给企业做 Agent权限隔离是逃不掉的。销售角色不能看到财务数据实习生角色不能看到内部薪酬讨论。这些不能靠模型自觉必须在检索层就把数据挡掉。实现方式就是在每个 chunk 的 metadata 里写清楚允许访问的角色或权限组。检索时先从查询里解析出当前用户的权限标识然后带进向量库的过滤条件。Chroma 支持 metadata 过滤Qdrant 和 Milvus 也有类似能力。对辅助 Agent 决策的上下文在进 prompt 之前就要完成筛选这是最后一道闸门。vectorstore.as_retriever( search_kwargs{ k: 4, filter: {doc_type: {$eq: policy}, permission: {$in: [finance, admin]}}, } )这道工序做好了你还可以做“引文可追溯”。给模型设定规则每条结论必须标注来源 chunk 的编号。Agent 最后输出答案时直接列出引用的文档名和页码。这大大增强可信度用户在业务例会里拿 AI 的答复去汇报也有据可查。这一步是我在所有企业级项目里都强烈要求加上的。4.3 把 RAG 接到 Agent 的工具调用里到了这里RAG 已经不只是一个“问答脚本”它是 Agent 工具箱里的一把扳手。更合理的架构是Agent 主流程先判断问题属性如果是一个事实型问题就调用 retriever 工具把检索结果交给 Agent 分析如果是一个计算型问题就调用数据库工具而不是硬走 RAG。我见过一个非常典型的反例有人把财务数据全部灌进向量库然后问 Agent “上季度营收多少”结果它从一段描述里猜了个数字。这不是 RAG 的失败是工具选型的失败。结构化财务数据就该走 SQL 查询RAG 只负责那些非结构化的定性内容。所以你的 Agent 工具集里RAG 应该是“检索非结构化知识”这个动作的专用工具它回答的是“资料里怎么说的”不是“事实是什么”。当你把“查询文档”和“查询数据库”分开成两个工具Agent 的任务规划才会有正确的抓手。5. 从 RAG 走向 Agentic RAG管道要变成决策者5.1 基础 RAG 的瓶颈做完了上面这些你已经有一个相当能打的基础 RAG 了。但如果你真把它接到复杂的 Agent 场景里很快会撞到天花板。基础 RAG 的思路是“一次检索一次生成”问题来了就检索检索完就回答。但现实里的提问往往不是这样问题很模糊、问题需要多个知识片段拼起来、问题需要多轮澄清。最典型的例子“对比 A 方案和 B 方案哪个更适合我们现在的业务”这个问题你要先知道业务背景再分别找到 A、B 的详细资料最后还要做对比。基础 RAG 会把这三步揉在一起检索结果必然是残缺的。这就是基础 RAG 和 Agentic RAG 的分水岭基础 RAG 是一个无状态函数你把问题丢进去就出答案Agentic RAG 是一个有状态的流程Agent 决定先查哪个、查完之后要不要再查、以及是否先把查询改写得更精确。5.2 改写查询、多跳检索、父文档检索改造的第一步是给检索前加一层“查询改写”。用户的问题是“那玩意儿能退货吗”你得先识别出“那玩意儿”指代的是上文的“电饭煲”然后改写成“电饭煲能否退货”再去做检索。这个改写动作可以交给大模型来做本质上是在检索之前先用 LLM 做一轮意图澄清。第二步是多跳检索。第一次检索只搜“电饭煲退货政策”返回的 chunk 里提到了“本政策适用于所有已激活保修的个人用户”但没解释什么叫“已激活保修”。Agent 发现证据不足就应该在地图上继续延伸再检索一次“保修激活条件”然后汇总两次检索的结果。这种不停追问、不停补资料的动作就是从 RAG 到 Agentic RAG 的核心跃迁。第三步是父文档检索Parent Document Retriever。你有几个大块段落切小 chunk 之后检索时命中的只是其中一小段信息不完整。父文档检索的方案是用小 chunk 做精确检索但命中后返回它所属的完整大段落。这样既保住了检索的命中率又保住了生成时的上下文完整性。它对那种“一段话里塞了大量信息”的技术文档特别有效。5.3 一条进阶路线图如果你准备把 RAG 做成 Agent 的核心基础设施我建议按这个顺序进阶先把基础管道跑稳把所有环节加上评估注意不是只评答案而是拆开评检索、评切分、评 prompt然后接上混合检索和你选好的向量数据库把权限和元数据做好再之后给 Agent 加上查询改写和多跳检索能力让流程从“一步到位”变成“按需补充”。等到数据和流程都稳定了你可以开始看 GraphRAG。GraphRAG 跟普通 RAG 的区别在于它先构建知识图谱把实体之间的关系显式地存起来。当你问“和 A 公司有合作关系的供应商里谁同时是竞争对手 B 的股东”向量检索要靠运气才能碰出这种关系图谱检索却可以沿着关系直接跳过去。这就是热词里的“知识割裂”的终极解法——知识不再是一堆孤立的 chunk而是一张互相连接的网络。我个人强烈建议不要把 GraphRAG 当起点。没有干净的文档入库流程和检索评估体系图谱只会把你的脏数据变成更高级的脏数据。基础不牢图谱跑几天就会淹没在错误关系里。回到开头那个不完全精确但足够实用的比喻Agent 是大脑RAG 是喂养大脑的资料库。基础 RAG 是资料员一趟一趟帮你搬书Agentic RAG 是资料员听你问完自己判断先翻哪本书、再看哪一章、如果资料不够还知道换个问法再去查。我自己走过的最曲折的一段路就是花了大把时间调生成端 prompt结果发现答案差 80% 的原因在检索端检索召回的 chunk 压根不含关键信息。后来我把 hit rate 跑出来才发现不到五成。从那以后我的规矩变成了“先量化检索再调生成”。这条规矩我建议你也记住它能让你的 RAG 少走我看过的那些弯路。