前几天帮一个团队做技术评审,他们搭了一套挺复杂的RAG系统,向量库、重排序、混合检索全上了,结果一测,用户问“把上季度所有客户投诉里跟物流相关的问题汇总一下,按严重程度排序”,系统直接懵了——拆出的子问题全是散文式搜索,检索结果七零八落。这不是个例,传统RAG的瓶颈早就不是检索精度,而是“智能决策”的缺失。这也是我写下这篇Agent系列第7篇的原因:AgenticRAG,或者说Agent化的RAG,正在把检索增强生成从“查字典”升级成“做研究”。
AgenticRAG的核心不是再换一个更强的embedding模型,而是把LLM从“生成器”变成“调度者”——让模型自己决定要不要检索、分几步检索、查完怎么判断够不够、不够要怎么办。这篇文章我会结合最近在几个工业级项目里的实操经验,把这套机制的主干脉络、路由与计划、查询改写、反思循环等关键组件全部拆开,附上可落地的代码片段和调参细节,希望对正在从传统RAG往Agent方向迁移的工程师有帮助。
1. 整体设计思路:从“检索-生成”到“规划-执行-反思”
1.1 传统RAG的三个死穴,痛点到了非改不可的地步
先别急着上Agent,得先认清传统RAG为什么在复杂的真实业务上会失灵。我把它总结成三个死穴。
第一是“一次检索定生死”。传统流程因为要控制延迟,基本是单轮检索、一次性取回Top-K文档,然后塞给LLM生成。一旦问题本身是复合型的,比如“帮我对比A产品今年和去年的退货率,再看下B区域的客诉原因有没有变化”,单次检索很难同时召回两个时间维度、两个实体维度的上下文,结果就是答案只覆盖了问题的一半。
第二是“没有判断力”。传统RAG对检索结果的质量是零感知的——哪怕召回了一堆完全不相关的文本,系统也会硬着头皮让LLM基于这些噪声生成答案。Lilian Weng的博客和大量实验都验证过,检索质量差的时候,RAG的答案甚至不如纯靠LLM内部知识生成来得准。
第三是“查询意图单一”。用户真实输入往往是模糊的:可能是口语化表达,可能是隐含指代,可能一个问题里混着事实查询和统计计算。传统RAG没有能力对查询做改写、分解、补全,直接拿原始query去检索,命中率自然上不去。
1.2 AgenticRAG的核心理念:把LLM放回控制回路
AgenticRAG的核心思路其实不复杂,就是一句话:让LLM成为整个检索过程的大脑,而不是末端的话筒。它不再把“检索”当成一个固定的前置步骤,而是把它变成模型可以自主调用的工具。
在这套架构里,LLM可以决定:
- 根本不需要检索,直接凭内部知识回答(比如常识性问题);
- 需要检索,但先要把用户问题拆成两个子问题,各自检索再汇总;
- 一次检索结果不够,需要换关键词、换过滤器再检索第二轮;
- 当前检索到的文档互相矛盾,需要把多个来源交叉验证;
- 最终答案要引用哪些文档、以什么结构组织。
这种机制在学术界对应的是Self-RAG、CRAG这些概念,工业界的LangGraph、LlamaIndex、Haystack也都有了比较成熟的Agentic RAG实现。用大白话说,传统RAG是“司机按固定路线开车”,AgenticRAG是“导航系统实时看路况自己改路线”。
1.3 一个“高层决策”对比案例,直观感受差异
还是用前阵子一个电商客户的实际需求举例。用户问:“各个仓库的库存周转率变化大吗?主要是哪些SKU导致的?”
传统RAG的做法:把问题整体embedding化,送到向量库,召回Top-K片段。结果大概率是召回了几段“库存周转率定义”和“某SKU某月周转率数字”。LLM只能把这些零散素材拼在一起,输出一大堆含糊其辞的车轱辘话。
AgenticRAG的做法:先由规划模块判断,这问题需要三步——第一步找出有周转率数据的仓库列表,第二步检索各仓库各SKU的周转率变化趋势,第三步把数据汇总成结构性对比。规划模块生成三个子任务,分别执行检索,甚至调用一个写好的计算工具去算周转变动率,最后把三块信息揉成一张对比表,还得给出“主要是哪些SKU导致”的归因结论。
这个例子能明显看出来,AgenticRAG不是简单的“多检索几轮”,而是任务分解、工具调用、结果再加工的组合拳。下面我按核心机制逐层拆解。
2. 核心机制拆解:路由、改写、反思是三大支柱
2.1 路由机制:先判断“要不要检索”和“去哪检索”
路由(Routing)是AgenticRAG区别于传统RAG最直接的一层。它做两件事:判断查询是否需要检索,以及判断应该走哪条检索路径。
“要不要检索”听起来多余,但在真实业务里非常关键。用户可能问“RAG的全称是什么”这种模型本来就会的题,也可能问“你觉得我们该不该上微调”这种开放性咨询。这时候检索反而引入噪声,直接让LLM基于内部知识作答效果更好。实现上,用一个轻量LLM调用做意图分类,一般不超过300ms,也不会显著增加成本。
“去哪检索”解决的是多数据源问题。一个正经的Agent系统通常会同时挂多个检索器:一个向量库存文档,一个倒排索引存工单记录,一个SQL接口存经营数据,甚至还能调用外部搜索API。路由模块根据查询内容,决定走哪个或哪几个检索器。
这里有个容易踩的坑:路由不要做太“硬”。我见过有些团队直接用分类模型决定只走一路,发现分类错了就没法补救。更稳妥的做法是给每个检索器打分,而非选唯一,允许多路并行,再交给后面的融合排序模块去综合。这样容错率会高很多。
2.2 查询改写与任务分解:检索质量的第一道闸门
路由决定了方向,但这还不够。用户的原始query直接拿去检索往往效果很差,因为口语化问题跟文档里书面化表述之间存在词汇鸿沟。所以AgenticRAG里通常有一个查询改写模块(Query Rewriting),常见的几种改写策略包括:
- 指代消解:“它们的退货率分别是多少” -> “A产品和B产品在2024年的退货率分别是多少”;
- 同义扩展:“怎么把货退掉” -> “退货流程 退货申请 退款政策”;
- 多子句拆分:“帮我总结Q2的销售报告,并和Q1对比” -> “Q2销售报告总结”、“Q1销售报告总结”、“Q2与Q1对比分析”三个子查询。
这块的实现可以参考Self-RAG里的工作:让LLM先生成多个查询变体,再用LLM或规则挑出最合理的组合。工业界更常用的是一个低温度(temperature调到0.1左右)的LLM调用,配合few-shot示例。
任务分解再往后走一步,就进入了“规划”的范畴。简单查询不需要规划,但如果Agent接的是一个“对多个文档做横向对比”类任务,就需要把任务树建出来:根任务是最终回答,叶子任务是单个检索单元,内节点是汇总加工。LangGraph里用图结构表达这种规划非常顺手,后文实操部分我会给出示例。
2.3 反思与自校正循环:AgenticRAG的灵魂所在
如果AgenticRAG只有路由和改写,那它跟“加了意图分类的普通RAG”没什么本质区别。真正拉开差距的是反思(Reflection)和自校正(Self-Correction)机制。
说白了,就是让Agent在拿到检索结果后,先别急着生成答案,而是自己先评判一下“这批文档够不够用”。评判方式通常是给LLM一个Prompt,要求它对检索结果做三件事:相关性打分、信息充足度判断、冲突检测。
如果发现检索结果不相关或不足,Agent可以触发第二轮检索,这次改写成另一组关键词,或者换一个检索器。如果发现两个文档之间的数据互相冲突,Agent需要决定是找第三个来源做交叉验证,还是在答案里如实说明冲突。
这个“反思-再行动”的循环,让RAG从固定流程变成有闭环反馈的控制系统。它的代价是增加了调用次数和时延,所以设计时一定要给循环次数设上限,防止检索风暴。我一般在代码里把max_retrieval_rounds设成2~3,超过就强制进入生成阶段。
3. 工具选型与架构形态:先想清楚你的场景要哪种
3.1 三种主流架构形态,按需选择而非盲目上最强
不是所有场景都需要最复杂的AgenticRAG。我把它分成三个层级,方便你对号入座。
轻量级:路由式RAG(Router Pattern)。核心就是一个路由判断,用户问题进来先分类,命中“需要检索”就按条件走不同检索器,否则直接LLM回答。适合数据源多但单源查询简单的中台系统。优点是改动小、容易上线,缺点是基本没有多轮迭代能力。
中量级:规划式RAG(Planner Pattern)。在路由基础上增加了一个规划步骤:把复杂查询分解成多个子任务,按依赖顺序执行,每个子任务可以对应不同检索器或工具。合适的问题能一口气拆成三四个子问题,分别检索完汇总成一个答案。这是目前性价比最高的形态,也是下文实操部分要实现的。
重量级:反思式RAG(Reflective Pattern)。在规划式基础上增加了反思和自校正闭环。每一步检索完,Agent都要评判结果质量,决定是继续补充检索还是推进到下一步。适合问答质量要求极高、数据噪声大的场景,比如医疗、金融合规。缺点是延迟高、token成本大,需要做严格的预算控制。
选型原则很简单:你的问题复杂度决定了你要不要上规划,你的数据噪声程度决定了你要不要上反思。别一步到位,先轻后重,让评估数据说话。
3.2 主流框架对比:LangGraph、LlamaIndex、Haystack怎么选
我在不同项目里轮番用过LangGraph、LlamaIndex和Haystack,简单说下体验和取舍,仅供参考。
LangGraph是最贴近“手动挡”的选择。它的核心抽象是图结构,节点就是“调用LLM”或“调用检索器”,边就是条件跳转。你完全掌控流程,适合对控制流有强需求的场景,也方便给业务方画流程图讲清楚Agent在干嘛。代价是样板代码多一些,需要自己管理状态。
LlamaIndex的Agentic RAG能力更偏“开箱即用”。它把QueryPlanningTool、RouterQueryEngine都封装好了,最适合快速搭原型。但它比较吃版本迭代,API变动快,上生产环境要锁定版本,否则升级容易踩坑。
Haystack在这三者里最“规范”,pipeline的概念很清晰,加了Agent功能后也支持工具循环调用。它在传统RAG领域积累了丰富的集成组件。如果你团队已经用了Haystack,平滑升级到Agentic模式会比较容易。
我的建议是:团队熟悉图编程就选LangGraph,想快速验证想法就LlamaIndex,已有Haystack存量系统就继续用Haystack。框架只是工具,真正拉开效果差距的是Prompt设计和流程编排,这部分没有任何框架能替你完成。
4. 实操落地:从规划到代码,搭一个最小可用的AgenticRAG
4.1 定义场景和模块划分
为了让例子不飘,我以一个内部知识库问答场景为例:知识库里有产品文档、工单记录、Q2销售报告,用户可能会问跨文档对比类问题。需求很简单——让它能自主把复杂问题拆解,检索不同数据源,最后汇总成结构清晰的答案。
模块上我分成四块:路由模块、改写与规划模块、检索工具集、反思与生成模块。整个流程可以用状态机来描述,LangGraph的StateGraph就是天然为此设计的。
4.2 核心代码实现:LangGraph版最小闭环
下面是我在一个项目里验证过的最小实现,去掉了一些业务细节,保留核心链路,方便你跑通后再扩展。
先定义整体的状态结构,这是Agent的“工作记忆”,所有节点都往里面读写:
from typing import TypedDict, List, Dict, Any import operator class AgentState(TypedDict): question: str # 原始问题 sub_queries: List[str] # 分解出的子查询 retrieved_docs: Dict[str, List[str]] # 每个子查询对应的检索结果 generation: str # 最终生成的答案 rounds: int # 检索轮次计数,防止无限循环 can_generate: bool # 是否满足生成条件 messages: List[Dict[str, str]] # 便于调试与追溯的对话记录接下来定义工具集。这里的检索函数建议封装统一接口,后面在反思节点里调用时完全屏蔽底层的向量库、倒排索引差异:
# 一个简单的检索器抽象,实际项目中可替换成向量库或ES def search_product_docs(query: str) -> List[str]: return ["产品文档片段1", "产品文档片段2"] def search_ticket_records(query: str) -> List[str]: return ["工单记录片段1", "工单记录片段2"] def search_sales_report(query: str) -> List[str]: return ["销售报告片段1", "销售报告片段2"]然后写路由与规划节点。这里的核心是用Prompt让LLM决定要不要分解任务、怎么分解。我试过几个Prompt模板,效果最稳的是给LLM一个明确的JSON输出格式约束,能显著减少解析失败率:
from langchain_openai import ChatOpenAI import json llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2) router_prompt = """ 你是查询规划和路由引擎。你的任务是分析用户的查询,决定: 1. query_type: 可选值为 "simple" 或 "complex" - simple: 单一事实性问题,只需要一次检索 - complex: 需要多源检索、对比、汇总的复合问题 2. sub_queries: 如果query_type为complex,将原问题分解为2-4个具体子查询;否则为原问题 3. data_sources: 每个子查询应使用的数据源,从以下选项选择:product_docs, tickets, sales_report 用户查询:{question} 请严格以JSON格式输出,不要包含其他文本,格式如下: {{"query_type": "...", "sub_queries": ["..."], "data_sources": ["..."]}} """ def plan_and_route(state: AgentState) -> AgentState: response = llm.invoke(router_prompt.format(question=state["question"])) try: parsed = json.loads(response.content) except json.JSONDecodeError: # 解析失败时降级为简单查询,避免整个流程崩溃 parsed = {"query_type": "simple", "sub_queries": [state["question"]], "data_sources": ["product_docs"]} state["sub_queries"] = parsed["sub_queries"] # 这里把data_sources存进state,实际工程中可以加一个字段 state["messages"].append({"role": "assistant", "content": f"规划结果: {parsed}"}) return state检索节点负责把子查询分配给对应工具。需要注意,子查询和检索器之间不是一一对应的,一个查询可能需要在多个数据源里各查一遍,这里用简单映射就能覆盖大多数场景:
def retrieve(state: AgentState) -> AgentState: retrieved = {} for query in state["sub_queries"]: # 简化版检索:三个源都查,后续融合 combined = [] combined += search_product_docs(query) combined += search_ticket_records(query) combined += search_sales_report(query) retrieved[query] = combined state["retrieved_docs"] = retrieved state["rounds"] += 1 return state最后是关键的反思节点。这里我让LLM判断检索结果是否足以生成最终答案。判断逻辑用Prompt引导LLM从“相关性”和“信息充足度”两个维度给分,只要有一个维度不过关就触发重规划。实测下来,这个二元判断比让LLM写长篇评估更稳定,也更容易微调:
reflection_prompt = """ 你是一个信息评估专家。请评估检索到的信息是否足以回答用户的问题。 用户问题:{question} 检索到的信息: {context} 请从以下两个维度评估,直接输出两行,每行只输出一个数字,范围0-10: 相关性分数: 信息充足度分数: 只有当你认为相关性和信息充足度都在7分以上时,才可以在最后一行输出"可以生成答案"。 否则,输出"需要补充检索",并简要说明缺什么信息。 """ def reflect_and_generate(state: AgentState) -> AgentState: if state["rounds"] > 3: # 超出最大轮次,强制生成,防止无限循环 state["can_generate"] = True else: context = "\n\n".join( f"【子查询: {q}】\n" + "\n".join(docs) for q, docs in state["retrieved_docs"].items() ) response = llm.invoke(reflection_prompt.format(question=state["question"], context=context)) result_text = response.content state["can_generate"] = "可以生成答案" in result_text if state["can_generate"]: # 常规生成,把检索结果交给LLM输出最终答案 generate_agent_prompt = "基于以下材料回答问题…请务必标注答案中每个部分引用的信息来源。" state["generation"] = llm.invoke(...) return state把节点组装到LangGraph里,可以这样写:
from langgraph.graph import StateGraph, START, END graph = StateGraph(AgentState) graph.add_node("plan", plan_and_route) graph.add_node("retrieve", retrieve) graph.add_node("reflect", reflect_and_generate) graph.add_edge(START, "plan") graph.add_edge("plan", "retrieve") graph.add_edge("retrieve", "reflect") # 条件边:反思后决定是再检索一次还是直接生成 def should_continue(state: AgentState): if state["can_generate"]: return "done" else: return "retry" graph.add_conditional_edges( "reflect", should_continue, {"done": END, "retry": "plan"} # 重规划,重新拆分子查询 ) app = graph.compile()跑一遍之后会看到类似这样的中间状态流:“plan”节点输出了2个子查询,分别对应产品文档和销售报告;“retrieve”节点把两组文档拉回来;“reflect”节点判断信息不足,原因是缺乏工单数据;“plan”节点第二次规划时调整了数据源映射;“retrieve”节点补齐工单检索;“reflect”节点这次打出了“可以生成答案”的结论。
4.3 关键参数调优经验:温度、轮次、上限
我见过不少团队把AgenticRAG跑起来之后折腾半天,最后发现是几个参数没调对。这里分享几个最影响稳定性的数值建议,都是我踩过坑后总结出来的,未必适用于所有场景,但至少是一个靠谱的起点。
规划节点temperature设在0.2以下。规划是结构化决策,太高的temperature会让LLM频繁给出不同的拆分方案,同一问题前后两次执行可能拆成完全不同的子查询,线上排查问题会非常痛苦。建议直接在代码里硬编码低温度。
检索轮次上限2到3轮。反思循环虽然强大,但每多一轮就意味着多一次LLM调用和检索调用。我见过有Agent在失败案例里连续循环了7次,生成结果质量没有提高,token成本却翻了四倍。上限设成2到3轮后,结合“把缺失信息写进生成Prompt”的兜底策略,效果反而更稳定。
每个节点前加超时和重试。LLM接口偶尔会抽风或超时,Agent链路比传统RAG更长,任何一个环节出问题都会导致整个流程失败。建议每个LLM调用点都包一层超时控制,超时后自动把该节点的判断降级为默认值,保底流程至少要能输出一个“不知道”的答案。
对最终答案强制要求有引文标注。这一点非常反常识,但效果出奇地好:当你要求“每个回答段落必须附加来源”,LLM会倾向于更谨慎地组织表述,而且在答案和检索文档不一致时更容易暴露问题。排查线上问题的时候,有引文和没引文的追踪难度完全是两个量级。
5. 常见问题与排查技巧实录
5.1 问题一:Agent陷入检索死循环,就是不生成答案
这是我遇到最多的问题,症状是反思节点一直打“需要补充检索”,Agent在多轮检索里反复横跳,最后要么超时要么token爆炸。
排查思路:先定位是“LLM真的觉得信息不足”还是“评分标准太严”。用langsmith或LangGraph自带的trace功能,查看每一轮反思节点的输入输出。如果发现LLM一直在要求补充“更细节的数据”,但检索结果里其实已经有了,说明是Prompt措辞让LLM产生了“我应该再多查一点”的幻觉。
解决办法有两个方向。第一,把反思Prompt里的判断标准从“信息是否完美”改成“信息是否足够给出部分回答”,同时加上“如果信息不完美但足够合理推测,继续生成”的句式。第二,从工程上强制设置最大轮次,在第3轮之后无条件进入生成阶段,并在Prompt里告诉LLM“这是最后一轮,请尽可能基于现有信息作答”。
5.2 问题二:子查询拆分太碎,结果互相之间对不齐
规划模块把问题拆得过于碎之后,每个子查询只拿到很小的一块上下文,汇总时LLM很难把碎片信息拼成连贯的答案。典型症状是回答看起来像几个段落硬凑在一起,逻辑断裂。
解决办法是给规划模块加上“汇总粒度”约束。比如Prompt里明确要求“每个子查询的期望返回内容应该是完整的一段观点,而不是一个事实碎片”。同时在检索阶段,除了精确匹配子查询,还可以把原问题也一起检索一遍,把原问题的检索结果作为额外上下文提供给生成阶段。这个技巧叫“query expansion”,实际效果非常明显。
5.3 问题三:多数据源结果冲突,Agent无法取舍
当一个数据源说“退货率下降”,另一个数据源说“退货率上升”时,简单的拼接式RAG会给出自相矛盾的回答。AgenticRAG本来应该解决这个问题,但如果反思节点没有覆盖“冲突检测”,照样会翻车。
我的方案是在反思节点里显式增加一句:如果发现多个文档的数据存在矛盾,请指出矛盾点,并优先采用时效性更新的数据源,若无法判断则如实输出冲突说明。这一个简单的Prompt改动,让答案质量从“互相打架”变成了“明确告知差异”,用户满意度反而更高,因为这符合人类研究者展示信息的方式。
5.4 常见问题速查表,收藏备用
| 症状 | 可能原因 | 推荐排查动作 |
|---|---|---|
| Agent反复检索不生成 | 反思Prompt标准太严格 | 降低评分阈值,增加最大轮次硬限制 |
| 子查询碎片化、答案逻辑断裂 | 规划粒度过细 | 要求子查询输出完整观点块,同时检索原问题 |
| 多来源数据冲突未标注 | 反思节点缺少冲突检测 | 在反思Prompt显式增加冲突处理指令 |
| 解析JSON频繁失败 | 规划输出格式不稳定 | 降低temperature,尝试函数调用(function calling)方式输出结构化对象 |
| 某一路检索一直不命中 | 数据源映射配置错误 | 检查路由模块的data_sources分配逻辑,必要时增加默认检索兜底规则 |
| 最终回答没有引出处 | 生成Prompt未约束引用格式 | 强制要求按段落附“来源文档ID”,做好后端校验 |
6. 一点经验之谈:别急着上全套机制
最后聊一点个人体会。现在大家看到Agent、AgenticRAG这类词,很容易犯一个毛病——恨不得把所有的规划、反思、多智能体协作全塞进项目里。我之前也这么干过,结果就是流程长得自己都调试不动,线上出问题了根本定位不到是哪一环。
我的建议是:先搭“路由+改写”这一层,跑一个月看数据。如果发现效果已经提升明显,再逐步加规划、加反思。每加一层机制之前,都要先想清楚:这层机制到底在解决哪个可量化的痛点?是检索命中率上不去,还是多轮问题答不全,还是跨源信息冲突?没有明确回答之前,别急着加复杂度。
AgenticRAG真正厉害的地方不在于它用了多少新潮技术,而在于它把“决策能力”还给了模型——让检索这件事从固定的流水线变成灵活的对话。这种思路放到任何一个用到RAG的场景里,都会有迁移价值。希望这篇文章能帮你在实际落地时少走弯路。