
“走进 AI Agent”写到第四篇前面三篇我们把 Agent 的基本结构、记忆模块、规划能力都过了一遍。今天这篇要补上最容易被低估的一块知识获取管道也就是 RAG。先说个判断我经手过的企业级 Agent 项目九成以上最后跑起来靠的都是 RAG而不是什么更花哨的 Agent 框架。RAG 这套东西听起来不复杂但真正上手调过的人都知道它才是决定一个 Agent “像不像样”的分水岭。这篇我打算从原理讲到最小实现再讲我在真实项目里踩过的一堆坑适合已经写过一个简单 Agent、正准备往里面喂真实业务数据的开发者。1. 为什么 AI Agent 离了知识获取管道就跑不动1.1 模型天生存在“知识盲区”大模型的知识来自训练阶段见过的数据这带来两个天然短板。第一知识的截止时间固定在了训练数据采集的那一刻之后发生的事情它完全不知道。第二训练数据里绝大多数是公开网页、论文、代码企业内部的私有文档、SOP、产品手册、客户反馈模型基本上没见过。你问一个 Agent 自家产品的某个参数配置它要么一本正经地胡编要么直接说“我不知道”这两种表现在真实业务里都不可接受。很多人第一反应是“那我微调一个私有模型总行了吧”。我先泼一盆冷水微调模型的成本、周期和更新频率对于高频变化的知识来说完全跟不上。比如产品上线一个新版本操作手册从 200 页变成 220 页你不可能每周都重新微调一次。微调更适合改变模型的表达风格、推理模式不适合当知识库来用。知识的更新和知识的存储应该放在模型外面而不是塞进模型参数里。RAG 解决的就是这件事。它的核心思路是“不背答案只查答案”把外部知识提前切开、做成索引模型在回答问题之前先去索引里检索出可能相关的片段再把片段连同问题一起交给大模型做最终回答。你可以把它理解成给 Agent 配了一个随取随用的资料室而不是指望它把整个资料室的档案都背下来。对应到 Agent 的整体架构里RAG 的位置非常清晰它负责“知道什么”不负责“做到什么”——执行动作的部分靠的是工具调用和 Skills两者不要混。1.2 为什么这条管道要用“管道”的思维来设计我见过不少初学者把 RAG 简单当成“把文档丢给向量数据库然后查一下”这种做法在小 demo 里能跑到真实业务里几乎必挂。为什么因为真实的知识获取场景是一条完整链路原文档加载、文本清洗、切块、向量化、索引存储、检索召回、重排、拼装 Prompt、最终生成。每个环节单独看都不难但它们之间是强耦合的。切块大小直接影响检索命中率Embedding 模型的选择直接影响语义相似度的质量重排模型又决定 Top 5 里到底哪个片段能被真正用上。“管道”这个说法强调的是数据像水流经过一系列独立工序每道工序只做一个事情并向后传递标准化的结果。这种做法最大的好处是可调试。线上召回效果变差了你可以逐段检查是切块的问题、Embedding 的问题还是召回 Top-K 设置的问题。如果所有逻辑都写在一个函数里一把梭排查的时候就会非常痛苦。另外要区分一个概念RAG 是知识管道记忆模块记录的是 Agent 和用户交互过程中产生的历史信息两者获取数据的来源不同。RAG 管的是百科式、组织化的资料记忆管的是对话上下文和用户偏好。这篇文章把重心放在 RAG 这条知识管道上因为它是企业落地 Agent 时第一个要解决的基础设施。不管你是 Python 技术栈用 LangChain还是 Java 技术栈用 LangChain4j 或者 Spring AI管道设计的本质都是一样的。2. 经典 RAG 五段链路逐个拆开看2.1 文档加载从歧义百出的原始格式到干净文本RAG 的第一步是把各种奇奇怪怪的源数据变成模型能处理的文本。真实业务里见过的源格式包括 Word、PDF、Markdown、HTML 网页、Confluence 页面、数据库里的备注字段甚至音视频转写文本。这一步看着是“体力活”其实是坑最多的环节。PDF 是最容易出问题的格式。文本型 PDF 可以直接抽取文字但扫描件是图片必须走 OCROCR 的中文识别准确率直接决定后续检索质量。我做过一批扫描版产品手册第一次直接导入检索出来的结果全是乱码Hit Rate 惨不忍睹后来不得不加了 OCR 预处理才恢复正常。Word 文档则要提防表格和复杂排版。表格内容如果被转换成一行行的纯文本原有的行列语义就丢了检索时容易把“A 型号的功率”和“B 型号的功率”混在一起。在实践中建议先把文档统一转成 Markdown 或者结构化 JSON再做切片。HTML 要记得去掉导航栏、页脚这些噪音信息否则检索引擎会频繁召回一些“首页”“联系我们”之类毫无价值的片段。加载环节还要做一层基础的清洗去掉多余换行、统一特殊字符、过滤掉明显的乱码块。这一步不需要多高深的算法但它能显著减少下游切块和检索的干扰。我个人的标准是清洗后的文本要能被人类通读如果人类读起来觉得零碎那模型检索出来也一定觉得零碎。2.2 切块策略chunk 尺寸不是拍脑袋决定的切块是整个 RAG 里最需要反复试验的环节它同时影响检索的召回率和生成的质量。切得太小一个语义完整的事情被拦腰截断检索时可能只找到了半句话信息不完整切得太大一个 chunk 里塞了太多无关内容向量表示会被平均掉检索回来的片段“相关性还行”但真正能用到的细节反而被稀释。我见过有人把整个文档作为一个 chunk 丢进去结果检索出来一堆废料一次生成塞进几千 token既浪费上下文又严重拉低准确率。切块的基本单位不要直接用“字符数”而是用模型的 token 数。以中文场景为例常见的经验值在 200 到 800 token 之间。200 token 适合问答对、FAQ、条款类数据语义本身就足够独立400 到 600 适合业务文档、操作手册更长的文本块适合需要完整上下文的政策原文。同时一定要加重叠overlap也就是相邻 chunk 之间留出一段交叉文本。重叠的目的很简单如果某个关键信息恰好落在切分边界上重叠部分能把这段信息保留在前后两个 chunk 里避免信息被切出局。我的默认配置是 chunk_size 500overlap 80然后针对实际效果再调。固定尺寸切块实现简单但有时不贴合文本结构。更聪明的做法是语义切块按标题、段落、列表结构来切。比如一个操作手册中每个步骤天然是一个小段落那就不该机械地卡 token 数而是把结构作为切分边界。再进一步还有“父子切块”策略父块负责语义粗召回子块负责细节检索时先定位到父块、再把子块喂给模型。这套做法的原理很好理解先用大块找准范围再用小块提供证据适合文档结构性强的场景。2.3 向量化把文本变成可计算的坐标切好的文本块需要变成机器能比对的向量。这里有一个核心概念Embedding也就是把一个文本块映射成一个高维向量使得语义相近的文本在向量空间中的距离更近。打个比方文本是一批货物Embedding 模型是把货物分拣到仓库货架上的工人每个语义相近的货物被放到邻近的位置检索时只要找到离“提问”最近的那些货架位置就行了。Embedding 模型直接决定“语义相似度靠不靠谱”。你在中文场景下直接用某些英文优化过的模型效果会明显打折。这几年中文场景里比较稳的选择包括 BGE 系列、M3E 系列还有 Open AI 的 text-embedding-3 系列英文场景更强。如果考虑部署成本和数据隐私本地模型通常是企业优先方案BGE-M3 在中文语义匹配上已经做得很成熟。Embedding 模型的维度也有讲究模型不同向量维度不同从几百到上千维都有。维度越高通常表征能力越强但存储和计算成本也越高。比如 BGE-M3 输出 1024 维几百个 chunk 感觉不到差别但到了几百万级维度对资源的影响就需要提前评估了。有了向量之后检索本质上就是在向量集合里做“找邻居”。向量数据库就是干这个的常见的有 Chroma、Milvus、Qdrant、Weaviate、Elasticsearch它也内置了向量检索能力。它们一般都支持 HNSW 这类近似最近邻索引原理是构建一张多层图先快速定位到大致区域再在局部精细查找。这样做的目的是用少量精度损失换取巨大的速度提升让几百万甚至上千万级别的向量检索也能在毫秒级完成。向量化完之后还要选择相似度算法大多数场景默认用余弦相似度因为它只关心方向不关心向量长度对文本长度变化不敏感。在实际代码里Chroma 默认的空间就是 cosine这也是我推荐大多数初学者直接沿用默认的原因。2.4 检索与重排从“找得到”提升到“找得准”检索环节的核心目标是把和用户问题最相关的 top-K 个片段找出来。最简单的方式是纯向量检索把用户问题也编码成向量然后找余弦相似度最高的 K 个。这种方式的优点是快缺点是对关键词敏感度不高。比如用户问“怎么退款”文档里写的是“退货流程”语义上相关但字面差异很大纯向量检索有时能找到有时找不到。行业里更稳的做法是混合检索把向量检索和传统的关键词检索BM25结合起来再做一个加权融合。向量负责语义泛化关键词负责精确匹配品牌名、型号、编号这类硬信息。比如用户问“A300 型设备如何校准”BM25 能够精准命中“A300”这个型号向量检索则负责理解“校准”这个动作词。两类结果合并后需要去重和排序LightRAG 或 Rerank 模型在这里派上用场。召回之后别急着把结果全丢给大模型我建议加一个重排步骤。向量检索返回的 Top-K 是基于“粗略语义相关”的其中往往混着一些看似相关、实则没用的片段。重排模型Cross Encoder会把提问和每个候选片段成对送入模型做一个更精细的相关性打分然后把真正有用的片段排到前面。重排的效果非常明显很多项目在加上重排之后最终答案准确率能提升十个百分点以上。代价是需要额外推理但换取的是生成质量的巨大提升这笔账非常划算。2.5 Prompt 注入与生成如何让模型“只答范围内的题”检索到候选片段之后最后一步是拼装 Prompt 并调用大模型。这一步的核心在于限制和约束系统提示词需要明确要求模型只能依据提供的上下文回答不能额外编造上下文不足时要么明确说不知道要么给出部分信息并告知局限。同时在 Prompt 里把用户问题和检索片段分隔清楚通常会用“以下是参考资料”和“请基于资料回答”这样的语义界限来组织。如果按原文逐字拼接片段模型很难区分哪些是资料、哪些是问题。更实用的做法是给每个片段附上来源和编号要求模型在回答中引用对应编号。这样做一方面让答案可溯源另一方面也能减少模型把不同片段张冠李戴的问题。我一直坚持在 Prompt 里加一句“如果资料中没有相关信息请直接说明资料不足”这一句话能大幅降低幻觉率。3. 从 0 到 1 搭建最小可用的 RAG 管道3.1 技术选型这次用“能看懂的组件”而不是“高度封装的框架”市面上的 RAG 框架很多LangChain、LlamaIndex 等等。这里我故意不用重量级框架而是用原生 Python 组件搭一个最小实现。原因有两个第一框架封装太厚出了问题你很难知道内部发生了什么第二RAG 的核心链路并不复杂直接调组件能让你对每一环节都有掌控感。等这套逻辑跑通了再迁移到 LangChain 或者 Spring AI 都很轻松因为你已经明白它内部在干什么。组件选型上Embedding 用 Sentence-Transformer 加载本地 BGE-M3向量库用 Chroma 的持久化模式LLM 部分用一个兼容 OpenAI 接口的封装方便接任意厂商的 API。这套组合的特点是依赖少、可本地运行、便于调试。3.2 环境准备需要安装的依赖并不多核心就四个chromadb、sentence-transformers、openai、以及一个 docx 解析库 python-docx。我用pdfplumber处理文本型 PDF。如果你要做中文文档解析建议再装一个pypinyin不是必须但方便做关键词分析。安装命令如下pip install chromadb sentence-transformers openai pdfplumber python-docxBGE-M3 模型在首次编码时会从 Hugging Face 下载权重。如果所在网络访问 Hugging Face 比较麻烦可以从 ModelScope 下载模型文件后加载本地目录加载方式相同只是路径换成模型所在目录。这个细节在离线环境尤其重要。from sentence_transformers import SentenceTransformer embedding_model SentenceTransformer(BAAI/bge-m3)3.3 核心代码实现与逐段讲解我按管道顺序写一个最小实现。第一步读取文档并切块。为了方便演示这里只处理纯文本和 Markdown真实项目中 PDF 解析只需要在 docs 列表里多加一个加载函数。import os def load_documents(file_path): if file_path.endswith(.txt) or file_path.endswith(.md): with open(file_path, r, encodingutf-8) as f: return f.read() if file_path.endswith(.pdf): import pdfplumber text [] with pdfplumber.open(file_path) as pdf: for page in pdf.pages: text.append(page.extract_text() or ) return \n.join(text) return 第二步切块。我用递归字符切分优先按段落和换行切再按 token 限制。一个简单的切法如下def split_text(text, chunk_size500, overlap80): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end start chunk_size chunk text[start:end] chunks.append(chunk) start end - overlap return chunks这个版本严格按字符切没有考虑句末边界。稍微进阶一点可以用正则优先找最近的换行作为切分点。实战中文本块如果总是从句子中间开始向量质量会肉眼可见地下降。更合理的写法是import re def split_text(text, chunk_size500, overlap80): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end min(start chunk_size, len(text)) # 从 end 往前找最近的换行符 if end len(text): last_newline text.rfind(\n, start, end) if last_newline ! -1 and last_newline start: end last_newline chunks.append(text[start:end]) start max(start 1, end - overlap) return chunks第三步向量化和入库。这里把文本块编码成向量然后写入 Chroma 持久化集合。import chromadb chunks split_text(doc_text) vectors embedding_model.encode(chunks).tolist() client chromadb.PersistentClient(path./rag_db) collection client.get_or_create_collection( nameagent_kb, metadata{hnsw:space: cosine} ) ids [str(i) for i in range(len(chunks))] collection.add( idsids, documentschunks, embeddingsvectors, metadatas[{source: manual.pdf, chunk: i} for i in range(len(chunks))] )第四步检索。提问先编码再去集合里查 Top-Kdef retrieve(query, top_k5): query_vec embedding_model.encode([query]).tolist() results collection.query( query_embeddingsquery_vec, n_resultstop_k, ) return results[documents][0], results[metadatas][0]这一步返回的是原始文本块和元数据。注意真实场景里要返回distances来观察相似度分数如果最高的相似度都特别低那基本可以判断资料库里没有相关内容别硬答。第五步组装 Prompt 并让 LLM 生成答案。这里我使用 OpenAI 兼容接口封装具体 base_url 按你的模型服务配置from openai import OpenAI llm OpenAI( api_key你的API密钥, base_url你的模型服务地址/v1 ) def generate(query, chunks): context \n\n.join( f[片段来源{meta.get(source, unknown)}]\n{doc} for doc, meta in chunks ) prompt ( 请基于下面的参考资料回答问题。\n 如果资料中没有足够信息请明确回答“资料不足”。\n 回答中请标注参考片段编号。\n\n f参考资料\n{context}\n\n f问题{query}\n ) response llm.chat.completions.create( model你的模型名称, messages[{role: user, content: prompt}], temperature0.2 ) return response.choices[0].message.content到这里一个最小可用的 RAG 管道就通了。整个流程不到 100 行代码但是每个环节你都能看到原始数据经过什么处理。接下来要做的就是把它放进一个循环里对一批真实问题测试效果。3.4 效果怎么度量Hit Rate、MRR 与最终答案质量很多做 RAG 的人第一版跑通了就以为完事结果上线后效果一言难尽。所以我建议一开始就建立评测集准备 30 到 50 个用户大概率会问的真实问题每个问题标注“答案应该在哪个文档块中”。这两个数字是硬指标Hit Rate命中率问题检索到的 Top-K 结果里是否包含标注的文档块。它衡量的是“找不找得到”是 RAG 最重要的基础指标。计算公式很简单命中问题数除以总问题数。MRR平均倒数排名如果命中的文档块排在第一位那这个问题的得分是 1排在第二位得分是 0.5排第三位是 0.33。把所有问题的这个分数取平均。它衡量的是“找得准不准”和“排名质量”。Hit Rate 的分母是整个检索能力重排模型主要改善的是 MRR 以及最终生成质量。我建议先跑一轮 Hit Rate如果低于 70%别急着调 Prompt先去回头改切块和 Embedding。如果 Hit Rate 已经不错但答案仍然不好那大概率是重排或 Prompt 组装的问题。4. 从 RAG 走向 Agentic RAG把检索决策交给 Agent 自己4.1 静态 RAG 的瓶颈在哪里上面搭的管道是“一问一检”的静态流程用户提问系统检索一次生成答案。这种模式解决“知识盲区”够用但碰到复杂的业务问题就有明显局限。典型场景有两类一类是用户的问题本身信息不足比如上来就问“这个产品怎么配置”但没说是哪个型号、哪种场景直接拿这句话去检索召回结果必然发散另一类是答案分散在多个文档里比如要对比两款产品的差异需要从产品说明书、技术参数表、售后 FAQ 三个来源同时取证据。静态 RAG 一次检索搞不定这种需要多步推断的问题。还有一个容易被忽略的瓶颈静态 RAG 不会判断“自己有没有检索到有效信息”。它每次都是盲目自信哪怕召回的片段相似度低得离谱也照样把它们拼进 Prompt 里让模型硬答。这就是为什么很多 RAG 应用在资料库内容不足时依然会输出看似合理的错误答案。4.2 Agentic RAG查什么、怎么查、要不要换一种查法交给模型决策Agentic RAG 的本质是把“检索”从一次性的函数调用变成 Agent 可以反复使用的工具。模型在回答过程中可以自主决定要不要查、先查什么、查不到怎么办。业界最常见的实现模式有这么几种第一种是查询改写。模型先把用户原始问题拆解和改写去掉歧义补充约束再拿改写后的内容去检索。比如“这个怎么配置”会被改写成“请列出 A300 型设备网络参数配置步骤”。查询改写能显著提升召回精度实现也不复杂本质是让模型先做一轮语义澄清。第二种是检索路由。Agent 面对多个知识库先判断当前问题应该去哪个库查。这个做法在企业环境里尤其有效产品知识、规章制度、故障工单三者放在同一个向量库里检索结果互相污染按路由分库之后每个库的检索结果质量都明显提升。路由可以基于分类模型也可以让 Agent 用函数调用的方式选择工具。第三种是循环检索与自我校验。模型先执行一次检索把答案和召回片段一起检查如果有证据不足或矛盾就再次改写查询、继续检索直到满足停止条件。这就是把“搜索-阅读-判断”循环交给 Agent 自动化。实现上需要给模型增加一个检索工具并设计一个拒绝停止的条件比如“最多检索三次”或“证据充分才停止”。第四种是多路证据融合。当答案分散在多个来源时Agent 可以同时检索多个知识库把多路证据合并到上下文里再统一做归纳式回答。这能解决跨文档、跨类型知识的割裂问题也是企业知识库最值钱的场景。Agentic RAG 并不是要替代经典 RAG而是在经典 RAG 的每一环之上加“自主决策”。如果你的场景是高频的标准问答经典 RAG 就够用引入 Agent 反而增加延迟和成本。如果面对的是长尾的、复杂的、多跳的问题那 Agentic RAG 才能发挥真正的价值。5. 实测中高频翻车的场景与排查思路5.1 检索到了但模型就是答不对先看一个最让人迷惑的现象从向量库里看召回的片段确实包含了答案但模型最终输出的答案还是错的。我排查这类问题一般按三个顺序查第一查 Prompt 约束是不是没有强调“只能依据资料回答”导致模型把已有的参数知识和新给的资料揉在一起第二查上下文长度有没有因为 Top-K 太大把太多噪声片段塞了进来正确答案被淹没第三查片段本身是不是答案跨了多个 chunk单个片段信息不完整模型看到半截话只能半截猜。前两者改配置就行如果是第三方就得回头调整切块策略或改用父子切块。5.2 检索不到但答案明明在库里面资料库里明明有内容却检索不出来问题一般出在三个地方。一是切块切得不对一个核心概念被切开向量表示被稀释导致这个 chunk 和任何查询的相似度都不高二是 Embedding 模型不适合当前语言或领域在中文和英文混合的场景下尤其明显三是查询本身的表达方式和文档差异太大比如用户问口语化的“怎么退货”文档写的是正式术语“退款流程申请条件”纯向量检索容易失配。针对第三种混合检索或查询改写能解决很大一部分针对前两种优先换 Embedding 模型和切块参数。5.3 Top-K 设成多少合适Top-K 没有标准答案但经验范围是 3 到 10。K 值太小漏召回的概率增大K 值太大噪声增加上下文窗口被浪费。这里我建议做一次“召回率随 K 变化”的小实验在评测集上分别计算 K3、5、8、10 的 Hit Rate找到提升开始变平的拐点。很多知识库里 K5 就够用但如果你加了重排可以把向量检索阶段 K 放大到 20重排后再截断到 Top 5。这套“粗召回再精排”的两段式结构效果最稳。5.4 中文 PDF 和扫描件是重灾区扫描版 PDF 在中文场景里极其常见尤其是合同扫描件、盖章文件、旧版手册。如果没有 OCR整个管道等于在“喂乱码”。我的建议是文本型 PDF 用 pdfplumber 优先扫描件用 PaddleOCR 这类中文 OCR 工具识别后再拼回文本。要注意 OCR 输出往往带大量换行需要在切块前做一轮文本合并和段落重排否则 chunk 会被换行切成碎片。这一步花的时间值和它提升的效果成正比。5.5 换了 Embedding 模型之后没有重新建库很多人踩过这个坑把 Embedding 模型从 A 换成 B但没有重建向量集合依然在旧向量上检索效果不升反降。原因不难理解不同模型产出的向量空间完全不同旧向量和新查询向量不在同一个坐标系里比较相似度没有意义。我习惯在代码库里把 Embedding 模型的名称直接写入集合的 metadata加载集合时校验当前模型名称和库里的名称一致不一致就强制重建。下面把几个高频问题的排查要点整理成速查表现象优先排查项原因与建议召回结果看起来相关但答案错Prompt 约束、上下文噪声强调仅依据资料作答检查 Top-K 与片段完整性检索不到答案切块、模型、查询表达调整 chunk 大小考虑混合检索与查询改写Hit Rate 低但 MRR 正常Embedding 与领域匹配差换中文场景更适配的 Embedding 模型命中排在 Top 3 之外重排缺失引入 Cross Encoder 做精排中文 PDF 乱码扫描件 OCR接入 OCR并对 OCR 文本做段落合并换模型后效果下降向量库未重建将模型名写入 metadata加载时校验并重建6. 做多了 RAG 项目之后我最想留给你的一句话把 RAG 调好真正靠的不是某一个惊艳的算法而是一遍一遍围绕业务问题做评测和迭代。我见过太多团队把精力花在换框架、换向量库上最后效果没有本质提升就是因为一直没有建立起“评测集 可量化指标 逐环节可定位”的工程习惯。只要 Hit Rate 有问题就聚焦切块和 Embedding只要 Hit Rate 没问题但答案不行就聚焦重排和 Prompt。这个思路放到任何技术栈下都成立。从 RAG 再往前走一步就是把它真正嵌进 Agent 里让它变成一个可以被调用的知识工具。你可以让 Agent 自己决定什么时候查、查哪个库、查不到怎么办这也是我后面打算继续写的方向。如果你现在正准备搭建自己的知识管道我建议先拿 30 个真实问题跑通这个最小实现把 Hit Rate 调到 80% 以上再考虑上 GraphRAG、本体 RAG 这些进阶方案——它们解决的是复杂关系知识不适合一上来就铺开。先把管道建得干净剩下的都是优化问题。