Agent 做到第四篇,终于要聊知识获取了。前面几篇我们搭过工具调用、聊过记忆机制,但很多人跑完 Demo 之后会卡在一个很现实的问题上:模型会的知识都是训练时的,让它回答一个发生在今天、或者只存在于你们内部系统里的事实,它就哑火了。这就是 AI Agent 的知识获取管道问题,也是 RAG 基础要解决的核心矛盾——让 Agent 在推理时能实时“查资料”,而不是凭记忆硬编答案。
这一篇我会按自己做项目的顺序来讲:先解释 Agent 为什么必须接外部知识,再拆 RAG 的完整链路,然后把切分、向量化、检索这三个环节逐个拆开,最后聊怎么把它真正接进 Agent 的交互循环里。适合已经跑通 Agent 基础流程、想把知识库能力接进去的开发者,也适合刚入门 RAG 但被各种文章绕晕的朋友。
1. 先搞清楚一件事:Agent 为什么需要外部知识
1.1 大模型的“认知天花板”在哪里
先说一个很多人没意识到的点:预训练模型本身是个“死”的知识体。它的参数权重在训练完成那一刻就固定了,之后你不管怎么对话,模型内部的知识边界都不会变。你问它公司最新的规章制度、你们产品的当前版本号、某个系统的接口文档,它要么编一个像模像样的答案,要么用“截止到我训练数据”来搪塞。
这不是模型笨,而是它的知识获取机制决定了它只能“回忆”不能“查阅”。这里有个很关键的类比:把大模型当成一个读书很多但是毕业后就不再翻书的人。你问他教科书上的概念他能答得很好,但你要问他上周的会议纪要,他只能靠猜。
这正是 Agent 场景下的致命伤。因为 Agent 跟普通 ChatBot 最大的区别就是它会“做事”——做事的依据如果不是来自真实世界,那这个 Agent 就是空中楼阁。我在前几篇里提过工具调用,工具解决的是“行动”问题,但行动之前还需要一个“依据”问题。这个依据从哪来?就是知识获取管道。
1.2 “管道”这个比喻到底在说什么
标题里“知识获取管道”这个说法,不是随便起的。管道意味着两个特点:一是有明确的输入输出,二是有固定的路径。RAG 就是给 Agent 装了一条从外部知识源到模型上下文的水管。
水流方向是这样的:外部文档先被切碎、变成向量、存进向量库;用户提问时,问题也被变成向量,去向量库里捞最相关的片段;捞出来的片段和问题一起拼进 Prompt 发给大模型;大模型基于这些片段组织答案。整个过程看起来是两段式——索引和检索生成,但放在 Agent 里还有一个更上层的问题:Agent 要知道什么时候该“开水龙头”。
这个“什么时候该查”的开关,在传统 RAG 流程里是固定的——每次提问都查。但在 Agent 场景里你会慢慢发现,不加判断地查会导致很多问题:查回来的内容不相关、打断了 Agent 的推理节奏、甚至让模型被噪声片段带偏。这就是为什么现在大家都在聊 Agentic RAG,后面第五章我会专门展开。
2. RAG 全链路拆解:从一份文档到一次回答
2.1 按需拆文档:切分质量决定“粮食”颗粒度
RAG 管线第一步是文档加载和切分。这一步听起来平平无奇,但它决定了后续所有环节的上限。你想想,给模型喂进去的知识是碎片化的,如果切分出来的片段是语义残缺的,再强的检索算法也捞不出完整信息。
切分的核心矛盾是颗粒度:切得太碎,单一片段信息量不够,模型看不到上下文;切得太粗,片段太长,向量化之后语义被稀释了,而且拼进 Prompt 会浪费大量 token。我见过很多新手直接按固定字符数切——这样省事,但中英文混排文档会切得乱七八糟。
一个比较稳妥的起步方案是递归字符切分:先按段落分,段落太长再按句子分,句子还太长再按固定长度分。这种递归策略保证了“能完整就完整、不能完整才硬切”。具体实现上 LangChain 里有现成的RecursiveCharacterTextSplitter,但我不建议你直接默认参数跑,后面第三章我讲怎么调。
这里只强调一个观念:切分不只是一个预处理步骤,它是你知识库的“农田规划”。你打算让模型以什么粒度去理解这份文档,完全取决于切分策略。
2.2 向量化:让计算机理解“意思相近”
文档切好之后,需要把它们变成计算机能比较的形式。文本本身没法算相似度,所以要把每段文本映射成一个高维向量——“意思相近的文本,向量之间的距离也近”。这就是 embedding 模型干的事情。
做个不严谨但很好懂的类比:你把每个段落压缩成一个几百维的“语义坐标”,在这个坐标空间里,“我今天吃了苹果”和“我啃了一个红富士”应该靠得很近,而和“苹果发布了新手机”稍微远一点。向量化就是把语言变成几何问题,把“相不相似”变成“距不距离”。
实际项目里 embedding 模型的选择非常重要。常见的有 OpenAI 的text-embedding-3-small、text-embedding-3-large,开源的 BGE 系列、M3E,还有 Cohere 的 embed 系列。选型要考虑语言支持、维度大小、收费模式。中文场景我实测下来 BGE-M3 在开源模型里表现不错,兼顾多语言和检索效果;如果预算允许,OpenAI 的 embedding 接口胜在稳定,但要注意它返回的向量维度动辄 1536,存储和计算成本要心里有数。
向量化之后还有一个容易漏掉的细节:embedding 模型要和后续检索部署对齐。训练向量库时候用什么模型,线上查询时候就必须用同一个模型,否则坐标空间都不一样,检索结果会魔幻到怀疑人生。
2.3 检索与生成:召回不是终点,拼进 Prompt 才是
索引完成后,线上就进入检索生成阶段。用户问题进来,先向量化,然后去向量库做近似搜索,找出 TopK 个最相近的文本片段,最后把片段和原问题拼成一个带上下文的 Prompt 丢给大模型。
这里有个新手最容易误解的地方:检索结果不是直接返回给用户的。RAG 整个流程里,最终答案是“生成”出来的,而不是“查”出来的。检索只是给生成提供材料,生成才是最终的答案来源。
有人会问:那为什么不把检索到的片段直接展示给用户?原因很简单——用户的自然语言问题跟文档片段之间往往存在说法差异,直接贴片段给用户体验很差。生成这一步可以让模型用更自然、更贴合问题的口吻来组织答案,同时把多个片段的信息融合起来。
所以完整的链路是:
# 伪代码,演示 RAG 检索生成主流程 def rag_answer(question: str, top_k: int = 5) -> str: # 1. 问题向量化 q_embedding = embed(question) # 2. 向量库检索 retrieved = vector_store.search(q_embedding, top_k=top_k) # 3. 拼装上下文 context = "\n\n".join([f"[片段{i+1}]: {doc.page_content}" for i, doc in enumerate(retrieved)]) prompt = f"基于以下资料回答问题:\n\n{context}\n\n问题:{question}\n回答:" # 4. 生成最终答案 return llm(prompt)这个流程看着简单,但每个环节都有不少坑。接下来的内容,我挑三个影响最大的环节展开讲:切分策略、embedding 选型、检索质量控制。
3. 把“喂”给 Agent 的知识整理成合格的索引
3.1 切分策略:固定长度、递归分块与父子分块
切分这件事,我建议直接从三个层次去选策略,从快到慢、从粗到精。
固定长度切分是最简单的起步方案。指定一个 chunk_size(比如 512 字符)和一个 chunk_overlap(比如 50),从头切到尾。优势是稳定可控,劣势是完全无视语义边界,可能把一个完整故事拦腰截断。如果文档结构非常规整——比如每行一条日志记录,那这种方案反而最合适。
递归分块是适用范围最广的方案。思想是维护一组分隔符,按优先级从高到低尝试切分:段落级分隔符\n\n优先,然后是\n、句号、空格、字符。切出来的块尽量在分隔符处断开,保证语义相对完整。LangChain 的RecursiveCharacterTextSplitter就是这个思路。中文场景里我习惯把separators设成["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],这样长句不会从中间被硬切。
父子分块是相对进阶的方案,适合文档结构嵌套复杂的场景——比如手册里既有大章节又有层级条目。思路是维护两套块:父块(较大的语义单元)用于提供上下文,子块(较细的片段)用于精确匹配。检索时候先命中子块,但把其所属的父块一起拼进 Prompt。这样既保证了检索精度,又不丢失上下文完整性。代价是实现复杂度高一些,一般等简单方案到瓶颈了再上。
不讨论模型就用不了生产环境——所有切分参数都应该由实测反馈来调。我给个经验值范围供起点:中文通用文档chunk_size=500、overlap=50起步,技术手册类可以放大到1000,代码类建议300-400、overlap 在30-50。跑一轮评测看检索命中率,再逐步调整。
3.2 Embedding 选型:决定语义精度的一层
Embedding 模型的差距,在文档少的时候感觉不明显,文档量一上来差距立刻放大。我之前做过一个对比:同一批中文技术文档,分别用 OpenAI 的 embedding 接口和开源的 BGE-M3 建索引,在 20 个测试问题上对比检索的 Top1 命中率。结果 OpenAI 略高,但差距其实不大,反而是 BGE-M3 在本地部署的成本优势十分明显。适合离线场景或数据敏感型项目。
选 embedding 模型时我建议盯五个指标:语言覆盖度、向量维度、最大输入长度、推理速度、成本。语言覆盖度解决“文档是不是纯中文/纯英文”的问题,中英混排的文档不要选纯中文模型。向量维度影响存储和计算量,1536 维的向量在百万级文档量下的索引开销不是开玩笑的。最大输入长度决定了你切块能切多大——embedding 模型吞不下超大 chunk。
这里插一个经验教训:embedding 模型上线后尽量不要随便更换。换模型 = 旧库全部重建 + 线上检索效果重新评测。我见过有人中途把 OpenAI 的 embedding 模型从小换到大,结果相似度分布整个变了,相关性阈值全部失效,花了一周时间重新调参。如果非换不可,务必做一轮全量回归测试。
3.3 向量库选型:从单机到分布式的代价
向量库是 RAG 的存储底座。市面选择很多,但别一上来就上分布式集群,绝大多数项目的真实瓶颈不在向量库,而在数据质量和检索策略。
个人项目或原型阶段,用Chroma或者FAISS就够。Chroma 胜在轻量、Python 直接调、支持持久化,几百兆的数据毫无压力。FAISS 是 Meta 开源的纯向量检索引擎,不支持文档存储,需要自己维护 ID 映射关系,但速度快、可控性强。
到了需要跟现有业务系统打交道的阶段,我会优先考虑pgvector——直接在 PostgreSQL 里加一个向量类型和索引,复用现有数据库的权限管理、备份恢复机制,减少一个中间件,对团队运维是很大的减负。
当数据量到千万级以上,或者 QPS 要求很高,再考虑Milvus、Weaviate、Qdrant这类专用向量数据库。它们提供了分布式能力、混合检索、丰富的过滤条件,但也要付出部署运维成本。有个原则供参考:当你的向量库成为运维负担之前,它不是瓶颈。别过度设计。
4. 检索质量控制的几个硬指标
4.1 TopK 到底设多少
检索召回的数量直接影响答案质量。TopK 设得太少,可能漏掉关键信息;设得太多,噪声片段混进来,模型反而无所适从,而且 prompt 长度暴涨。
TopK 没有标准答案,但有一个决策区间。一般文档场景下,TopK=3~5是起点。如果你的知识库有严格的分类体系、文档切分质量也很高,可以缩小到2~3。但如果文档碎片化严重,单片段信息密度低,就要加到5~8来补足信息量。我实测下来,超过 8 个片段之后,答案质量的提升就很微弱了,而 token 开销却线性上涨。所以与其盲目调大 TopK,不如优化切分质量。
有个技巧是动态 TopK:根据检索分数的分布来决定返回多少。如果前 5 个片段的分数都很高,说明知识高度密集,返回 3 个足够;如果分数普遍偏低,说明问题跟资料的相关性本来就不高,返回再多也是凑数。这样既节省 token,又能提升准确率。
4.2 相似度阈值:宁缺毋滥还是宁滥毋缺
检索接口返回的相似度分数,必须设一个阈值来兜底。不设阈值的结果是:即使用户问的问题跟知识库完全没关系,系统也会硬挤几个“最像”的片段出来。模型拿到这些不相关材料,又不想承认自己不知道,就会开始一本正经地胡说八道。
阈值的高低跟 embedding 模型的分数分布有关,没有统一的 0.7 或 0.8。你需要在真实数据上先跑一批查询,统计“相关”和“不相关”的分数区间,然后在两个分布之间找一个分界点。这个工作看着繁琐但非常值得做,它决定了你的 RAG 系统是“知之为知之”还是“不知也硬答”。
实际操作里我建议双阈值策略:一个高阈值用于直接答,一个低阈值用于“不确定但给线索”。高于高阈值,模型基于资料自信作答;介于两个阈值之间,模型在回答中标注信息来源的置信度;低于低阈值,直接让 Agent 承认知识库不足,转交给其他工具或人工。
4.3 混合检索:让关键字和语义互补
纯向量检索有一个天生盲区:它擅长语义匹配,但不擅长精确匹配。你问“BUG-1024 的状态”,向量检索会把“BUG-1024”这个专有名词的语义权重分散到整个句子里,匹配效果未必好。这种场景下,BM25 这种传统的关键词检索反而更可靠。
混合检索就是把两条路都走一遍:BM25 负责精确匹配,向量负责语义扩展,两边召回的结果做融合排序,最后统一去重。融合排序最简单的办法是加权求和,BM25 的得分和向量相似度分别做归一化,然后按权重合并。经验上关键词类问题给 BM25 高一点权重,语义类问题给向量高一点权重。
很多向量库原生支持混合检索,比如 Milvus 支持 BM25 + 稠密向量混合查询,Qdrant 也有类似能力。如果用的是 pgvector,可以自己用 PostgreSQL 的全文检索配合向量检索完成融合。混合检索不是必须一开始就上的,但它能把 RAG 系统的鲁棒性往前推一大截,尤其是处理用户问法跟文档表述差异很大的情况。
5. 把 RAG 真正接进 Agent 的交互链路
5.1 查询改写:Agent 的“嘴”和 RAG 的“耳朵”对齐
用户提问是口语化的、指代模糊的,而向量检索是字面匹配的。这里有个理解差。比如用户说“就是上次你跟我说的那个配置,再帮我确认一下”,这句话里没有任何可检索的关键信息,直接拿去查知识库必挂。但 Agent 在前面几轮对话里已经提到过“RabbitMQ 的连接超时配置”,完全有能力把这句话改写成可检索的查询。
这就是查询改写。在 Agent 里,RAG 不应该直接消费用户的原始输入,而应该消费 Agent 理解后的改写结果。可以把改写看成一个小的LLM调用:
def rewrite_query(history: list[str], raw_input: str) -> str: # 把历史上下文和当前问题一起送给 LLM,让它输出用于检索的查询语句 prompt = f"""根据对话历史和用户最新提问,生成一个适合检索知识库的独立查询。 只输出查询语句本身,不要解释。 历史:{' '.join(history)} 用户最新问题:{raw_input} 改写后的查询:""" return llm(prompt)改写后的查询,可以干很多事情:补全缺失信息、把口语变成书面语、把模糊指代换成明确的实体名称、甚至拆成多个子查询分别检索再合并结果。很多 Agent 项目里 RAG 效果不好,不是因为向量库不行,而是因为“问题”本身没有整理干净就送进去检索了。
5.2 知识库路由:先想清楚去哪找,再去找
当 Agent 接入了多个知识库时——比如一个接产品文档库、一个接内部运维手册、一个接入工单系统——你不会希望每次提问都把三个库全查一遍。原因不只是浪费,更关键的是混合结果会互相干扰。产品相关的问题里混进运维手册的片段,模型就会被带偏。
知识库路由就是先决定“该查哪个库”,再执行检索。路由本质上是一个小分类任务,可以简单到用关键词规则,也可以用一个轻量 LLM 来做决策。实际项目中我更推荐用 LLM 做路由,因为规则维护起来太痛苦了——知识库数量一多,关键词规则之间就开始打架。
打个比方:Agent 是接待员,知识库是各个办公室。没有路由的时候,接待员逮住哪个办公室就问哪个,效率极低还经常问错人。有路由之后,接待员先判断“这事归谁管”,然后直奔目标办公室,效率天差地别。
路由的决策结果还可以带上结构化参数,比如“检测到用户咨询的是内存溢出问题,优先查故障排查手册,检索 TopK=3”。这样知识库路由和检索参数联动,整个 RAG 调用就更加精细了。
5.3 Agentic RAG 与 ReAct 模式的融合
最后聊一下 Agentic RAG——这是当前社区里热度极高的方向,也是“知识获取管道”从单向管道进化为智能管道的下一步。
传统 RAG 是一次性的:检索一次,生成一次,结束。它的局限在于如果第一次检索结果不好,没有人会去补救。Agentic RAG 打破了这种单次模式:Agent 把检索当成一个可交互的工具,先查一次,看结果够不够好,不够就改查询再查,或者查另一个库,或者把结果反馈给推理链继续思考。
这就跟 ReAct 模式天然契合了。ReAct 是“推理 + 行动 + 观察”的循环——Agent 先想想现在缺什么信息,然后决定调用检索工具,观察检索回来的结果,再决定下一步是继续检索还是生成答案。在这个模式下,RAG 不再是流水线上固定的一道工序,而是 Agent 手里的一个可自主支配的“情报员”。
举个例子。用户问“我们的支付服务最近有没有异常的监控告警?”Agent 的思考链路可能是:先查监控知识库了解告警规则,再查告警记录库拉最近告警,发现数据不足,再改写查询扩大时间范围,最后结合两边的资料生成结论。整个过程涉及两次以上的检索、多个知识库的切换,这是传统 RAG 完全做不到的。
不过要泼一盆冷水:Agentic RAG 不是银弹。多轮检索不可避免地带来更高的 token 消耗和更长的响应时间,而且 Agent 的推理链路越复杂,出错概率也越高。我的建议是先从传统的单次 RAG 跑通业务闭环,确认知识库和切分质量没问题,再逐步加 Agentic 能力。如果基础 RAG 都做得稀烂,上 Agentic RAG 只会让错误更复杂。
我自己的实践体会是,RAG 的调优过程很像“炖汤”——材料(文档)、刀工(切分)、火候(检索参数)都影响最后的口味,缺一环都不行。很多人一上来就追最新的向量库、最贵的模型,结果忽略了最基础的文档清洗和切分。等你把基础链路跑稳、把检索质量调到位,再回头看那些花哨的概念,会发现其实每一步都只是把这一篇讲的内容做深做细而已。