
用 ReAct 循环实现 Agentic RAG:检索、自评、改写与终止条件
用 ReAct 循环实现 Agentic RAG:检索、自评、改写与终止条件
固定流程的 RAG 只能检索一次,遇到比较、追问、需要计算的提问就力不从心。本文讲清如何把检索的控制权交给 LLM:用 ReAct 的观察—思考—行动循环组织检索、自评与查询改写,用步骤上限和兜底出口防止死循环,并给出可运行的 LangGraph 风格示例与选型判断标准。
固定流程的 RAG(常说的流水线型 RAG)在提问到达之前就把处理顺序写死了:切块、检索、拼接、生成,检索通常只做一次。如果第一次检索拿回来的片段与问题关系不大,系统没有机会换一组检索词重来。Agentic RAG 要解决的就是这个问题——把「下一步做什么」的决定权从程序移到 LLM 手里,让检索、改写、计算这些动作按需执行若干次。它适合那些一次检索答不完的提问:新旧制度对比、看到结果后想换检索词的追问、需要从文档里取数再计算的问题。本文按 ReAct 循环讲清实现路径、终止条件和选型边界,适合已经搭过基础 RAG、准备引入多步检索的开发者阅读。
准备工作
在动手之前,需要先确认几件事已经就位。Agentic RAG 不是从零起步的方案,它建立在一条能跑通的基础检索链之上。
- 一条可用的基础检索链:文档已经完成切块与向量化,能按查询返回 top-k 片段。检索工具本身的质量决定了 Agentic 层的上限,检索召回很差时,再多的改写轮次也救不回来。
- 一个支持结构化输出或工具调用的 LLM 接口:循环中的「思考」步骤需要模型输出可解析的判断结果,例如是否足够、下一个动作是什么、改写后的查询是什么。具体支持哪些能力、上下文长度与计费方式,以所用服务商的官网当前信息为准。
- 一个编排框架或自建状态机:示例使用 LangGraph 风格的图结构,也可以用任意能表达「节点 + 条件边」的框架,甚至手写 while 循环加状态字典。框架不是必需的,状态与路由的清晰表达才是。
- 可观测性:每一步的输入、输出、耗时、token 消耗都要能记录下来。多步循环出问题时,没有轨迹几乎无法定位。
- 预算与超时配置:最大步数、单步超时、总 token 上限,这些参数要在写第一行代码前就定下来,而不是等线上跑飞了再补。
另外要明确一点:Agentic RAG 不是流水线型 RAG 的升级替代品,两者擅长的提问类型不同。如果现有系统在简单 FAQ 上表现已经稳定,引入自主循环只会增加延迟和成本。
操作步骤
第一步:分清控制流由谁决定
流水线型 RAG 的控制流在提问到来之前就固定了:顺序确定、检索次数确定、失败时的行为也确定。代价是僵化,收益是延迟和成本容易估算、结果可复现、便于审计。
Agentic RAG 的控制流在运行时由 LLM 决定:检索几次、按什么顺序、要不要换工具,都随情况变化。收益是灵活,代价是延迟与成本随步数线性增长,判断本身也带波动。理解这条权衡,后面的所有设计决策才有依据。
第二步:搭起观察—思考—行动的循环
ReAct 模式把一次迭代拆成三段:
- 观察(Observation):收集当前已知的信息,包括原始提问、工具返回的结果、错误信息。
- 思考(Thought):判断已经知道什么、还缺什么、下一步该调用哪个工具、是否已经可以作答。
- 行动(Action):带参数调用某个工具,或者直接给出最终回答。
这里有一个必须坚持的职责划分:LLM 只负责「提议」动作,真正执行工具的是运行时。运行时负责工具名匹配、参数校验、权限检查、超时控制。这个分离是全部护栏的基础——模型可以幻觉出一个不存在的工具名,但运行时能把它拦下来。
循环的终止条件也要在第一步就设计好,至少覆盖四类:模型给出最终回答、达到步数上限、超出预算、遇到不可恢复的错误。
第三步:加入自评与查询改写
Agentic RAG 价值最集中的地方,是检索之后先评估结果、不够就重来。第一次检索只拿到相关性很弱的片段时,思考阶段应判断「证据不足」,然后改写查询再检索一次。
改写的方向取决于提问类型:
- 模糊提问要具体化。例如「年假怎么算」应改写为「年休假 结转天数 享受条件」这类带关键实体的查询。
- 比较类提问要拆开。例如问新旧规定在年假结转天数上的差异,应分别用两个查询去检索现行规定和旧规定,而不是把整句丢进检索器。
- 证据已经齐备时,不再追加检索,直接进入生成。
「先自评再前进」这一步是流水线型 RAG 没有的。但要注意成本:如果把检索结果全文都塞进对话历史,token 消耗会迅速膨胀。更现实的做法是观察里只保留摘要和来源 ID,正文按需再取。
第四步:让工具不止于检索
Agent 能调用的工具不限于检索。常见组合是检索加计算:先从文档里取出数值和条件,再用计算工具求和或求差。例如差旅费问题,先在制度文档里确认日补贴与住宿标准,再调用计算工具算出应发金额。
工具越多,工具描述的质量越关键。每个工具的名称、用途、参数含义、返回结构都要写清楚,因为模型只能依据这些描述做选择。参数校验放在运行时,不要指望模型自己保证格式正确。
第五步:复杂任务考虑先计划后执行
任务步骤较多时,可以采用 Plan-and-Execute 模式:先让模型产出一份计划,再逐步执行。计划作为数据留存下来,执行前就能检查有没有漏掉的步骤。风险在于计划一旦出错会波及全程,所以这种模式必须配合验证与预算管理使用,不能裸跑。
第六步:用图结构把循环写出来
下面是一个带自评的循环的骨架,用 LangGraph 风格表达。核心是四个节点加一个条件路由,以及一个明确的步数上限。
from langgraph.graph import StateGraph, END
MAX_STEPS = 6 # 步数上限,属于护栏的一部分
def retrieve(state):
"""用检索工具取回片段。"""
docs = search_tool(state["query"], top_k=5)
state["docs"] = docs
state["steps"] += 1
return state
def grade(state):
"""自评:这些片段是否足以作为回答依据。"""
state["sufficient"] = llm.judge(state["question"], state["docs"])
return state
def rewrite(state):
"""证据不足时,把查询改写得更具体。"""
state["query"] = llm.rewrite(state["question"], state["query"])
return state
def generate(state):
"""基于已收集的证据,带来源作答。"""
state["answer"] = llm.answer(state["question"], state["docs"])
return state
def route(state):
if state["sufficient"]:
return "generate"
if state["steps"] >= MAX_STEPS:
return "generate" # 到达上限,用已知信息作答后结束
return "rewrite"
graph = StateGraph(dict)
graph.add_node("retrieve", retrieve)
graph.add_node("grade", grade)
graph.add_node("rewrite", rewrite)
graph.add_node("generate", generate)
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "grade")
graph.add_conditional_edges("grade", route)
graph.add_edge("rewrite", "retrieve")
graph.add_edge("generate", END)
app = graph.compile()
这段代码有两个关键点。第一,MAX_STEPS 是硬性护栏,防止改写—检索之间无限往返。第二,到达上限时不是抛错,而是走「用已掌握的信息作答并结束」这条出口路径。自主性只有和这类护栏配套,才具备上生产的条件。
一个完整示例
下面用「现行规定与旧规定在年假结转天数上有什么变化」这个问题,把整条链路走一遍。示例只描述流程与数据结构,具体函数实现依赖各自的检索与模型服务。
初始状态
state = {
"question": "现行规定与旧规定在年假结转天数上有什么变化?",
"query": "年假 结转 天数",
"docs": [],
"sufficient": False,
"steps": 0,
"answer": None,
}
第 1 轮:检索
用初始查询调用检索工具,取回 5 个片段,steps 变为 1。结果里大多是现行规定的条款,旧规定只出现一条且与结转无关。
第 1 轮:自评
自评节点判断证据不足——问题要求对比两个版本,而当前片段只覆盖了一个版本。此时 sufficient 保持为 false,路由进入改写。
第 1 轮:改写
改写节点把单一查询拆成两个方向,例如先检索「现行规定 年假 结转 天数」,再检索「旧规定 年假 结转 天数」。实现上可以让改写节点输出查询列表,由检索节点依次执行;也可以让循环多跑一轮,每轮聚焦一个版本。
第 2 轮:检索与自评
两个版本的条款都拿到了,自评判断证据充分,sufficient 变为 true,路由直接进入生成。
生成
生成节点基于两组片段作答,并在答案中标注每个结论对应的来源 ID。如果中途 steps 达到 MAX_STEPS 而证据仍不充分,同样进入生成,但答案需要说明哪些部分缺少依据。
整个过程中,运行时负责校验每次工具调用的参数、执行超时和权限;模型只负责提出「检索什么」「够不够」「要不要改写」这些判断。把这条边界守住,循环才可控。
注意事项
不要把所有提问都送进 Agent
Agentic RAG 适合需要多次判断才能作答的提问:看到首轮结果后要换检索词的多步推理、多来源对比、需要用到检索数值的计算、按情况切换工具。这类业务通常提问量有限、单条价值较高,效果也更容易衡量。
反过来,简单 FAQ 和单一事实确认用自主循环,只会白白增加延迟和成本。对响应时间要求严格的场景,以及为了复现和审计需要固定步骤的场景,自主性都是减分项。
用路由做渐进式落地
比较务实的路径是分阶段推进:先用流水线型 RAG 建立基线,收集答错的提问;分析这些失败是否确实源于固定的检索流程;只把这类提问交给 Agent 处理。简单问题走流水线,复杂问题走 Agent,这种路由方式比全量切换更稳。
评估要对比基线
评估时至少要看任务完成率、工具选择准确率、平均步数、延迟和成本,并与流水线型基线逐项对比。只看最终答案质量,会漏掉多花的那些步数和时间。
观察内容要压缩
检索结果全文堆进历史会让 token 消耗快速上升。保留摘要与来源 ID、正文按需回取,是控制成本的基本手段。同时,步数上限、单步超时、总预算这三道闸门都要有,缺一道都可能让循环在线上失控。
易变信息以官网为准
模型服务的上下文长度、计费方式、工具调用能力,以及编排框架的接口细节都可能变化。本文示例中的参数(如 top_k=5、MAX_STEPS=6)只是示意值,实际取值需要结合自己的检索质量、延迟预算和成本上限来定,相关规格请以所用服务与框架的官网当前信息为准。