
Agentic RAG这个名字最近一年几乎成了RAG领域的显学。不管你是用LangChain、LlamaIndex还是自己从零搭流水线讨论的焦点都已经从怎么切分文档、怎么调embedding转移到了怎么让检索过程具备自主规划与决策能力。作为这个系列的第三篇我默认你已经对基础RAG有实操经验——向量库、召回、重排这些概念不再赘述这篇文章聚焦一件事把Agent的能力真正嵌入到RAG链路中讲清楚架构怎么设计、代码怎么写、生产环境怎么调。这个系列前两篇分别讲了RAG的基线架构和检索质量优化这一篇想聊的是进阶形态Agentic RAG。它适合谁看适合那些已经跑通了基础RAG、但发现复杂问题答不好、多跳问题答不了、检索失败时系统还硬着头皮编答案的团队。你不需要是Agent方向的专家但最好对LangGraph或类似的状态机框架有一点概念。文章里所有代码都是基于LangGraph的Python实现我会把每一步为什么这么写讲透而不是丢一堆demo了事。1. Agentic RAG到底在解决什么问题先说个我自己的观察。很多团队做RAG的方式本质上还是召回-重排-生成三段式流水线。用户问一个问题系统去向量库捞top-k文档塞进prompt里让大模型回答。这种架构在单点事实型问题上表现不错但一旦问题涉及多步推理、跨文档对比、或者检索结果本身不充分整个系统就会暴露出非常明显的短板。1.1 传统RAG的两堵墙第一堵墙叫单次检索的天花板。用户问张三在A公司期间参与的项目后来被B公司收购后发生了哪些组织调整这个问题至少要拆成三跳先查张三在A公司的项目再查B公司收购的细节最后查组织调整。传统RAG只能做一次向量检索它拿到的top-k文档大概率只覆盖其中一跳后面的内容全靠大模型脑补。我见过太多项目卡在这一层不断调chunk size、调embedding模型但问题是结构性的调参解决不了。第二堵墙叫无反馈的盲答。向量检索这个环节太偷懒了——它不知道自己的结果够不够用也不会对结果做任何质量判断。更麻烦的是生成阶段的大模型也不判断检索到的东西能不能支撑回答它只会顺着已有的材料编下去。结果就是你问一个库里有明确答案的问题系统给出的回答却和库里的事实对不上而且它自己毫无察觉。这两堵墙的本质是检索与生成之间缺少一个反馈闭环。传统RAG假设检索一次就够但这个假设在真实业务里几乎不成立。用户问题千奇百怪检索的召回率再高也总有漏网之鱼模型自己对证据是否充分这件事的判断能力比我们想象中弱得多。1.2 Agentic RAG的核心转变检索不再是终点Agentic RAG做的事情简单说就是让大模型在一个循环里自主决定什么时候检索、检索什么、要不要继续检索、什么时候停止、最终怎么回答。它不再把检索当成一个固定步骤而是把一个检索器封装成Agent手里的工具让Agent围绕问题展开多轮思考-行动-观察循环。这个转变带来三个直接好处多跳问题被拆解Agent先把问题拆成若干子问题每个子问题单独检索检索结果组合起来再生成答案。检索失败有兜底Agent检完一轮之后会自我检查发现信息不够就换一种查询方式再检一轮而不是硬着头皮乱答。工具组合成为可能检索只是工具之一Agent还可以调用计算器、数据库查询、API接口等其他工具检索结果和这些工具的输出一起参与推理。我用一个生活化的类比帮你理解传统RAG像一个只有查词典一个动作的实习生你问这个季度比上季度增长了多少他直接翻开一页词典找答案。Agentic RAG则是那个会先看你是要哪个季度、什么口径、增长率还是增长量、发现词典缺页会主动去翻另一本参考书、最后还会自己核对一遍数字的成熟员工。1.3 什么场景真正需要Agentic RAG这里得泼一盆冷水不是所有RAG场景都需要Agentic。如果你的业务问题80%以上是XX产品的退货政策是什么XX平台的客服电话是多少这种单点事实查询传统RAG配合好的rerank完全够用硬上Agent只会增加延迟和成本。根据我这大半年的落地经验以下四类场景才真正需要Agentic化场景类型典型问题传统RAG表现Agentic RAG优势多跳推理某业务的负责人是否参与过收购案只命中其中一跳答错拆解子问题逐跳验证对比分析对比两款产品的退换货与售后政策只覆盖一个产品多路检索组合对比条件不足用户问题缺少关键限定条件按默认假设乱答主动追问或按条件枚举跨工具协作算某个指标并解释计算依据无法结合计算检索计算器协同判断标准其实很简单你拿10个真实用户问题去跑现有RAG如果5个以上因为问题太复杂或检索结果不全导致答错那就是该上Agentic RAG的时候了。如果错误主要来自chunk切分和embedding质量先把基础优化做完再考虑Agent别急着追热词。2. 系统架构Agentic RAG的核心模块怎么设计想落地Agentic RAG第一步不是写代码而是想清楚架构。我见过不少人直接把Agent简单理解成让大模型决定调几个函数结果写出来的东西既没有状态管理也没有退出条件跑起来一团乱麻。下面按我实际验证过的方案拆开讲。2.1 规划器把大问题拆成检索任务Agentic RAG的第一个核心模块是规划器Planner。它的职责是接收用户原始问题输出一个检索执行计划。这个计划可以是一系列子问题也可以是一系列检索指令关键要满足三个条件子问题之间不重叠、每个子问题能独立检索、子问题的组合能覆盖原问题。这里有个容易踩的坑很多人让规划器一次拆很多个子问题结果拆出来的东西相互纠缠检索时上下文搅在一起。我的经验是硬性限制子问题数量。一个复杂问题拆2-3个子问题就够了超过3个建议改为多轮交互——先回答第一层再根据结果决定要不要追问。因为拆得越细单次检索的质量越难保证组合时的误差也被放大。规划器的输出格式建议用JSON而不是自由文本。原因很实际后续节点要解析子问题并逐一检索结构化输出能省掉大量解析错误。我在生产环境里会要求模型严格输出{sub_questions: [..., ...]}并在prompt里给两个few-shot示例实测能把格式错误率压到1%以下。2.2 验证与反思让Agent知道自己没查到这是Agentic RAG和传统RAG最本质的区别系统必须有能力判断当前检索到的信息够不够用。这个模块我一般叫验证器Verifier也有些人叫反思器Reflector。验证器的实现有两种路线。一种是把检索结果和问题一起交给大模型让它判断基于这些资料能否回答用户问题不能回答就返回一个特定标记。另一种是做更细的逐条证据检验——把关键断言从待回答问题里抽出来逐条核对检索结果中是否有对应事实。前者实现简单、速度更快后者更严谨、能精确指出缺什么。我实际用的是折中方案第一轮验证用能否回答的粗粒度判断如果判断为不能再进入缺什么信息的细粒度分析把缺失信息作为下一轮检索的查询词。这样既避免了每轮都做细粒度分析的延迟开销又给第二轮检索指明了方向。验证器的prompt里有一个特别重要的细节必须让模型区分资料里明确说了没有和资料里根本没提到。前者是负证据后者是缺失证据处理方式完全不同——前者可能说明问题本身有问题比如问了不存在的东西后者才应该触发补充检索。2.3 记忆管理短期状态与上下文压缩Agent在循环里会产生大量中间信息子问题列表、每轮检索到的文档、验证结果、已经回答过的部分。这些信息如果全部堆在上下文里很快会把大模型的context window撑爆。所以要有一个状态管理的意识。我用LangGraph实现时会把状态分为两类短期工作记忆当前子问题、当前轮检索结果、验证结论这部分在每轮循环结束后就清空或压缩。长期累积信息已经确认的事实、已经排除的方向、最终需要合成的证据列表这部分贯穿整个Agent循环。一个非常实用的技巧是每轮检索后做一次提炼——不是把原始文档塞进状态而是让模型把文档里和当前子问题相关的部分提炼成两三句话存入记忆中。这样多轮循环下来context增长是线性的而不是爆炸式的。我见过很多人忽略这个步骤跑到第三轮就把context跑到接近上限导致后面的生成质量急剧下降。2.4 工具集设计检索器不是唯一的工具Agentic RAG虽然名字里带RAG但真正生产化之后你会发现检索器只是Agent工具集里的一把刀。以我做的企业知识库问答系统为例最终给Agent配了四个工具向量检索器处理语义相似度召回适合开放式问题。关键词检索器处理专有名词、编号、精确术语比如工单编号为CS-2024-011的任务向量检索经常翻车BM25反而一查一个准。SQL查询器处理结构化数据比如上季度各产品线的投诉率这不该走向量库。计算器/代码解释器处理需要精确计算的数值问题。工具选择本身也是一个Agent决策点。我在规划器之后加了一个工具路由节点让模型判断当前子问题更适合哪个工具。这个节点的prompt会给每个工具附上适用场景示例并明确规则不确定时就选向量检索器因为它容错性最好。工具设计的核心原则是每个工具都要有清晰的边界描述。我曾经犯过的错是把工具描述写得太宽泛导致Agent把查一个专有编号的问题错误地路由给了向量检索结果一路跑偏。后来我重新改了工具描述每个都加上适用XX、不适用XX的否定式描述路由准确率提升非常明显。3. 实操基于LangGraph落地一个Agentic RAG讲完架构进入代码环节。我用LangGraph来演示因为它把Agent循环建模成一张状态图节点、边、条件路由都显式声明比纯ReAct的prompt循环更容易控制和调试。这个项目我跑在Python 3.11 LangChain 0.2环境上如果你用的是旧版本API细节可能略有差异。3.1 技术选型与目录结构先说明为什么选LangGraph而不是直接手写while循环调LLM。手写循环的问题是状态全靠全局变量维护没有清晰的执行轨迹一旦循环次数多了或者某个节点出错你很难定位问题。LangGraph把一切变成显式的图每个节点是纯函数状态在节点之间传递图本身可以可视化、可以断点调试。生产环境里这个优势非常值钱。项目目录结构参考agentic_rag/ ├── graph/ │ ├── __init__.py │ ├── state.py # 状态类型定义 │ ├── nodes.py # 各节点实现 │ ├── router.py # 条件路由函数 │ └── build_graph.py # 图编排入口 ├── tools/ │ ├── vector_store.py # 向量检索器 │ ├── keyword_store.py # 关键词检索器 │ ├── sql_tool.py # SQL查询工具 │ └── calculator.py # 计算工具 ├── llm.py # LLM统一封装 └── main.py # 对外调用入口3.2 状态定义与节点实现状态是整个Agent循环的共享工作台我定义如下# graph/state.py from typing import Annotated, TypedDict from langgraph.graph import END, StateGraph import operator class AgentState(TypedDict): question: str # 用户原始问题 sub_questions: list[str] # 规划器拆解的子问题 current_index: int # 当前执行到第几个子问题 contexts: Annotated[list, operator.add] # 已确认的证据列表 memory: list[str] # 长期记忆事实摘要 need_more: bool # 是否需要补充检索 missing_info: str # 缺失信息的描述 final_answer: str # 最终答案 max_rounds: int # 最大循环轮数防止死循环注意contexts用operator.add做累加器——这意味着每次有新证据进来都追加到列表而不是覆盖。这非常重要因为多轮循环中各轮检索到的证据都需要保留到最终生成环节。规划器节点的实现# graph/nodes.py import json from llm import llm PLANNER_PROMPT 你是检索规划器。请将用户问题拆解为1-3个独立的子问题。 硬性要求 1. 子问题之间不得重叠 2. 每个子问题必须能独立进行信息检索 3. 简单问题只输出1个子问题不要过度拆分 4. 输出必须是JSON格式{sub_questions: [子问题1, 子问题2]} 示例1 用户问题张三在A公司期间负责的项目后来怎么样了 输出{sub_questions: [张三在A公司期间负责了哪些项目, 这些项目后续的发展或结局是什么]} 用户问题{question} def planner_node(state: AgentState): resp llm.invoke(PLANNER_PROMPT.format(questionstate[question])) try: plan json.loads(resp.content) sub_questions plan[sub_questions][:3] except Exception: # 解析失败时降级把原问题当成唯一子问题 sub_questions [state[question]] return { sub_questions: sub_questions, current_index: 0, }这里加了一个降级逻辑解析失败就把原问题当唯一子问题。我在这上面吃过亏一开始解析失败会直接抛异常结果整个Agent流程挂掉。生产环境里任何解析逻辑都要有降级路径宁可退化成传统RAG也不能让流程中断。检索节点def retrieve_node(state: AgentState): sub_q state[sub_questions][state[current_index]] # 1. 先走向量检索 vec_docs vector_retriever.invoke(sub_q, top_k6) # 2. 再走关键词检索补充精确匹配结果 kw_docs keyword_retriever.invoke(sub_q, top_k3) # 3. 合并去重后用cross-encoder重排 merged deduplicate(vec_docs kw_docs) pairs [[sub_q, doc.page_content] for doc in merged] scores reranker.predict(pairs) ranked [doc for _, doc in sorted(zip(scores, merged), reverseTrue)] best_docs ranked[:3] # 4. 提炼为短期记忆 content \n.join(d.page_content for d in best_docs) summary llm.invoke(f从以下资料中提炼与问题{sub_q}直接相关的要点不超过100字\n{content}) return { contexts: [d.page_content for d in best_docs], memory: [f【{sub_q}】{summary.content}], }我这个检索节点里其实藏了一个关键设计向量关键词双路召回。为什么这么做因为向量检索对同义改写容忍度高但对精确编号、人名、产品型号经常失手关键词检索恰好相反。两条路召回的结果合并后重排能显著提升召回质量。这个技巧只增加几十毫秒开销收益却很实在。验证节点VERIFY_PROMPT 基于以下资料判断能否回答用户问题 资料 {contexts} 用户问题{question} 判断规则 - 如果资料中包含回答问题的充分事实输出 ANSWERABLE - 如果资料部分相关但缺少关键信息输出 UNDERSPECIFIED并在下一行用一句话说明缺少什么 - 如果资料与问题完全无关输出 IRRELEVANT 只输出以上三种判断之一不要输出其他内容。 def verify_node(state: AgentState): contexts \n.join(state[contexts]) resp llm.invoke(VERIFY_PROMPT.format( contextscontexts, questionstate[sub_questions][state[current_index]] )) content resp.content.strip() if content.startswith(ANSWERABLE): return {need_more: False} elif content.startswith(UNDERSPECIFIED): missing content.split(\n)[1] if \n in content else 需要补充更多细节信息 return {need_more: True, missing_info: missing} else: # IRRELEVANT return {need_more: True, missing_info: 检索结果与问题不相关需要更换检索词}这里有个细节值得强调验证节点的判断对象是当前子问题不是用户的完整问题。因为子问题已经拆得很细了资料能否支撑这个子问题的判断会容易得多。如果你拿完整问题去验证模型会被长问题干扰经常误判。3.3 图编排与条件路由节点写完之后用StateGraph把它们串起来。关键是定义什么时候该走哪条边的条件路由# graph/build_graph.py from graph.state import AgentState from graph.nodes import planner_node, retrieve_node, verify_node from graph.router import route_after_verify, route_after_planner def build_agentic_rag_graph(): g StateGraph(AgentState) g.add_node(planner, planner_node) g.add_node(retrieve, retrieve_node) g.add_node(verify, verify_node) g.add_node(generate, generate_node) g.set_entry_point(planner) # 规划器执行完后进入第一个子问题的检索 g.add_edge(planner, retrieve) # 检索后进入验证 g.add_edge(retrieve, verify) # 验证后的路由决定继续检索、还是进入下一个子问题、还是直接生成 g.add_conditional_edges( verify, route_after_verify, { retry: retrieve, # 补充检索 next_sub: retrieve, # 进入下一个子问题先检索 generate: generate, # 所有子问题处理完生成最终答案 } ) g.add_edge(generate, END) return g.compile()路由函数是整张图的决策中枢# graph/router.py def route_after_verify(state: AgentState): sub_qs state[sub_questions] idx state[current_index] # 轮数保护超过最大循环就强制生成 if idx len(sub_qs): return generate # 当前子问题需要补充检索 if state[need_more]: if state.get(retry_count, 0) 2: # 重试次数过多跳过当前子问题 return next_sub return retry # 当前子问题完成进入下一个 return next_sub注意我在路由里加了一个重试上限。这是个容易忽略的点如果不设上限某个子问题如果一直检索不到合适资料Agent会陷入无限循环。我设了2次重试上限超过就让Agent带病前进——去处理下一个子问题最终生成时模型自己会知道某些部分证据不足。这比卡死整个流程划算得多。最终生成节点GENERATE_PROMPT 你是知识库问答助手。请基于以下证据回答用户问题。 证据 {memory} 用户问题 {question} 要求 1. 严格基于证据回答不要编造 2. 如果证据不足明确说明根据现有资料无法完整回答... 3. 综合所有子问题的证据给出连贯回答 def generate_node(state: AgentState): memory \n.join(state[memory]) resp llm.invoke(GENERATE_PROMPT.format( memorymemory, questionstate[question] )) return {final_answer: resp.content}3.4 关键参数与调优心得这节全是真金白银的踩坑经验我按重要性排序模型参数。规划器和验证器节点用温度0.1以下的采样原因很简单这些节点做的是解析和判断不是创意生成温度一高就容易发挥输出格式开始漂。生成节点可以用温度0.3左右稍微给一点自由度但别超过0.5否则容易脱离证据自由发挥。另外如果有预算规划器用强一点的模型比如更大的参数或更擅长指令遵循的那个检索器和生成器用中等模型就行。检索参数。top_k的设置我试过从3到10最佳区间是5-8。太小了召回不够验证阶段容易误判资料不足太大了噪声增多重排的精度下降。另外一定要给检索器加上分数阈值——低于阈值的文档直接丢弃宁缺毋滥。原因是低分文档通常是语义不相关的噪声留它们在后边只会干扰验证和生成。循环上限。我建议把整个Agent循环的步数上限设成子问题数乘以3。比如拆了3个子问题最多允许9步。这个上限写在状态里每个节点执行时都检查一次。别嫌这种保护多余我在demo阶段就因为忘记设上限让一个特别刁钻的问题跑了20多轮烧了几十块钱的token才反应过来。4. 生产环境的评测、成本与体验优化写demo和上生产完全是两码事。Agentic RAG引入了一个传统RAG没有的问题循环次数不确定导致成本和延迟不确定。这一节聊怎么把这两个变量重新摁住。4.1 评测用回溯验证集兜底很多团队对Agentic RAG的评测还在用找几个样例跑一下看看的方式这在生产化阶段是远远不够的。因为Agent的行为是发散性的——同一个问题今天跑和明天跑可能中间步骤都不一样。你需要一个结构化评测集来兜底。我推荐回溯验证集方案从线上日志里收集最近一周的真实用户问题人工标注标准答案组成一个200-500条的问题集。每次改代码、调prompt都在这个集合上回归。评测指标我用四类指标计算方式关注什么答案准确率人工打分或LLM-as-judge评分最终答案质量关键点覆盖率标准答案里的要点是否全部出现回答完整性平均步数所有问题的Agent循环步数均值系统效率无效循环率达到步数上限才结束的问题占比路由与验证质量这里有个重要经验把无效循环率单列出来。传统RAG不存在这个问题但Agentic RAG一旦验证器判断不准确就会出现大量检索-验证-再检索的空转。我在早期版本里无效循环率一度高达30%后来把验证prompt改严谨、加了重试上限才压到5%以下。这个指标是判断系统健康度的晴雨表。4.2 控制成本与延迟Agentic RAG的延迟比传统RAG高多少我测过一组平均数据传统RAG端到端延迟1.2秒Agentic RAG平均3.8秒复杂问题甚至到8秒。成本大约是2到5倍。如果你的产品对延迟敏感比如在线客服、实时助手必须做三件事第一设置延迟预算。在Agent循环里记录每个节点耗时如果某一步耗时超过设定值比如检索超过800ms触发降级路径——不再重试直接进入下一步。这和数据库的超时机制是一个思路。第二使用快速通道。在进入Agent循环之前先做一次轻量判断这个问题是简单问题还是复杂问题简单问题单点事实、关键词明确直接走传统RAG快路径复杂问题才进Agent循环。这个判断本身也是一次LLM调用但用的是小模型、短prompt成本很低。我上线这个快速通道之后平均延迟从3.8秒降到了2.1秒因为约60%的问题都被分流到了快路径。第三缓存命中的问题。把用户问题和Agent执行计划做缓存。同一问题再次出现时直接复用上次的执行计划跳过大半Agent循环。这里要注意缓存键的设计——不是缓存最终答案而是缓存问题→执行计划因为相同问题在不同时间可能需要不同答案比如价格、政策会变。执行计划复用后检索是新的但规划成本全省了。4.3 与并行检索的混合策略Agentic RAG处理多跳问题是串行的——一个一个子问题来。但实际场景里很多子问题之间并没有依赖关系。比如对比A和B的售后政策两个子问题完全独立串行白白浪费了时间。我现在的做法是给每个子问题打一个依赖标签规划器在输出子问题时同时标注每个子问题是否依赖前序子问题的结果。独立的子问题放到一个批次里并行检索有依赖的才串行等待。LangGraph的fan-out/fan-in机制支持这个实现起来大概多花半天时间但延迟能再降30%左右。不过并行也有代价多个检索请求同时打向量库数据库QPS会翻倍返回结果合并时上下文顺序需要重新编排。我建议先跑串行版本验证效果再在问题量上来之后优化成并行别一上来就并行增加排查复杂度。5. 常见问题与排查实录最后这部分整理我在生产环境里被问得最多的问题以及对应的排查思路。5.1 高频问题速查表现象根本原因排查方式解决方案Agent陷入无限循环验证器持续判不足看每轮验证输出确认判断逻辑加重试上限改进验证prompt答案前后矛盾各子问题证据冲突检查各子问题检索到的文档领域生成时增加证据一致性核对指令规划拆解过于碎片规划prompt缺少硬性限制查看规划输出统计子问题数量限制最多3个子问题强约束JSON检索结果偏题工具路由错误查看工具路由节点日志改写工具描述增加否定式边界延迟突然飙升并行检索打爆向量库监控检索节点P99耗时增加向量库连接池限制并发数格式化错误Few-shot示例不足查看原始模型输出增加2-3个反例降温度5.2 三个独家避坑技巧避坑一不要让Agent直接操作真实生产数据库。我在最初版本里让Agent可以直接执行SQL查询结果它拿到一个含糊的问题后生成了一个没带WHERE条件的查询把整张表拉出来算了一遍接口直接超时。后来我改成参数化查询模板——Agent只能填参数不能写完整SQL。这个约束把风险降了一个数量级。凡是Agent的工具都要考虑异常输入时的破坏力能限制就限制。避坑二一定要给验证器加记忆偏差防护。这是我从一次线上事故里学到的。用户连续问了好几个关于同一产品的问题Agent的长期记忆里积累了关于这个产品的信息。到第五个问题时检索器其实没召回相关内容但验证器因为看到记忆里有相关事实错误地判断为答案可回答最终生成了大量基于过时记忆的错误信息。修复方式验证器判断能否回答时只允许看当前轮检索到的新证据不能看长期记忆。长期记忆只能用于最终生成不能用于有效性判断。避坑三把Agent的思考过程写进日志而不是只记最终答案。传统RAG的日志一行就够问题、检索文档、回答。Agentic RAG必须记录完整的轨迹每个子问题、每轮检索的查询词、验证结论、重试原因、内存摘要、每步耗时。这不仅是排查问题的依据更是复盘这个Agent为什么会这么想的唯一线索。我建议用JSON Lines格式按步骤追加每步一行方便用jq之类的工具分析。有一回线上用户反馈系统回答得莫名其妙我翻开日志一看发现Agent在第三轮检索时把子问题的查询词改了个离谱的方向检索结果完全跑偏后面的验证器居然还通过了。如果只有最终日志这种问题根本查不出来。写在最后的个人体会Agentic RAG这半年给我的最大感受是它不是一个装上就能变强的模块而是一套需要持续调校的运行时。比写Agent节点更难的是让这个系统稳定、可控、可预测。我个人在实际操作中最常在验证器上花时间——它就像是Agent的方向盘调好了系统指哪打哪调不好就是花钱跑偏。如果你是从基础RAG迁移过来我强烈建议先不要动现有架构而是把Agentic RAG作为一个独立增强通路接到旁边简单问题走老路复杂问题才分流到Agent通路。跑两周对数、对比准确率后再决定是否全量切。最后再分享一个扩展方向——我现在正在把多模态检索也塞进Agent的工具箱里让它能同时查文本、表格和图片。这个系列如果有下一篇我想专门聊聊这个话题。