ReAct 循环:Agent 实现自主决策的核心引擎
上篇你给 Agent 加上了记忆——消息链让它记住整个会话。但记忆只是基础,真正的 Agent 需要反复思考、反复行动,直到任务完成。这篇讲 Agent 的灵魂——ReAct 循环。
小明发现 Agent “不想多干活”
小明把记忆、工具注册表、MCP 都接上了。Agent 能聊天、能查天气、能搜新闻,看起来很完美。
但他发现一个问题:
“它只查了北京!上海呢?” 小明很困惑。
他去问老张。老张看了一眼代码:”你写的逻辑是 if tool_calls → 执行 → 喂回去 → 结束。也就是说,每次只能调一次工具。”
“那我让它查两个城市,它应该先查北京,再查上海,然后对比——这不就是多做几步的事吗?”
“对,你需要的是——循环。”
什么是 ReAct
老张在白板上画了一个三角形:
“这个模式叫 ReAct——Reasoning + Acting 的结合。最早是 Google 在 2022 年的一篇论文里提出的,现在几乎成了所有 Agent 系统的标准循环。”
“跟人类的做事方式一样:想 → 做 → 看到结果 → 再想 → 再做。循环往复,直到问题解决。”
“那跟我们之前写的 Function Calling 有什么区别?” 小明问。
“之前是单步的:模型说有 tool_calls → 你执行 → 喂结果 → 模型回答。一次就结束了。ReAct 把这个过程放进了一个 while 循环里——只要模型还觉得信息不够,就继续调工具。”
官方 FC 协议下的 ReAct
老张把关键差异画了出来:
| 维度 | 单步 FC(之前) | ReAct(现在) |
|---|---|---|
| 控制结构 | if tool_calls |
while tool_calls |
| 工具调用次数 | 最多 1 次 | 不限,直到够了 |
| 终止条件 | 调完就回答 | 模型自己判断”信息够了” |
| 安全兜底 | 不需要 | max_steps 防无限循环 |
| 循环信号 | 无 | 有 tool_calls = 继续;无 = 结束 |
“核心思想就一句话:tool_calls 是否存在,就是 Agent 是否需要继续行动的信号。“
“有 tool_calls → 继续循环,执行工具,观察结果,再问模型。无 tool_calls → 任务完成,返回最终答案。”
小明悟了:”所以不是’最多调一次’,而是’调到模型觉得够了为止’。”
“对。而且这个判断是模型自己做的——它在每一步都会评估:我已获取的信息够不够回答用户?够了就不调工具了,直接输出文字。”
代码实战
老张打开 agent_loop.py,这是今天最核心的文件。
“先看整体结构,一共就 60 行:”
“一行行拆解:”
第一步:初始化
“两个配置:max_steps 是安全阀,防止无限循环;tools 是工具 Schema,每次循环都要传给 LLM。”
第二步:准备记忆
“每次 run() 都新建一个 ShortTermMemory,灌入 system 指令和用户任务。这和上篇讲的记忆机制完全一致。”
“这里的关键是 system prompt 的设计:”
“注意这条:’不要用相同参数重复调用同一个工具’。如果没有这条,模型可能在某个问题上死循环——比如查天气失败,它反复用同样的参数重试。”
第三步:主循环
“每步做三件事:调 LLM → 判断 → 执行或返回。”
“注意这里 tools=self.tools——每一步都把完整的工具 Schema 传进去。因为模型需要在每一轮评估’我还能用什么工具’。”
“这是循环唯一的正常出口。模型判断信息够了,直接返回自然语言回答。注意:此时不追加任何 user 消息,因为模型已经在 assistant 消息里说话了。”
“关键动作:
-
把 assistant(tool_calls)写入记忆(完整保留 tool_call.id) -
逐个执行工具 -
把每个工具结果以 role=tool写入记忆,tool_call_id 必须和上面的 assistant 对应
continue 是隐式的——Python for 循环自动进入下一步。”
“上篇讲记忆的时候强调过:assistant(tool_calls) 和 tool 消息必须成对,ID 必须对应。这里严格执行了这个规则。”
第四步:强制收尾
“当循环跑满 max_steps 还没结束——说明模型太’贪心’了,一直在调工具。这时强制追加一条 user 消息让它收尾,同时 tools=None 不让它再调任何工具。”
“两个保险手段组合使用:
-
追加 user 消息——’你已经不能再调工具了’ -
tools=None——从协议层面禁止调工具
只做其中一项不够。光靠文字提示,模型可能忽略;光靠 tools=None,模型可能返回空内容。两项一起才稳妥。”
完整运行过程
老张拿一个真实任务来演示:
运行过程:
“模型做了 3 步:
-
搜第一轮——拿到一些结果 -
觉得信息不够全面,换了个关键词再搜一轮 -
两轮信息够了,开始总结
每一步都是模型自己做的决策——要不要继续、换什么工具、用什么参数。你只写了循环框架,策略全在模型侧。”
对比:单步 FC vs ReAct
小明想起之前的代码:”我们之前写的也是调工具,跟现在有什么区别?”
老张把两段代码并排:
单步(之前):
多步(现在):
“差异一目了然:
-
单步用 if——最多调一次工具;多步用for——一直调到够 -
单步第二轮 tools=None是固定行为;多步只在兜底时才tools=None -
多步每轮都传 tools,让模型有持续的行动能力 -
多步需要 max_steps兜底,单步不需要”
两个常见坑
坑 1:无限循环
“如果不设 max_steps,模型可能在某个问题上死循环。比如:
“模型永远找不到’永远不会下雨的城市’,但每次工具都正常返回——它就会一直循环下去。max_steps 是唯一的硬停机制。”
“另一个防御手段是 system prompt 里的’不要用相同参数重复调用’——但这是软的,不能 100% 依赖。”
坑 2:工具结果格式不对导致循环乱掉
“如果工具返回的不是纯文本(比如返回了一个 JSON 对象、或者报错栈),模型可能:
-
把它当成操作指令来执行 -
识别不了里面的关键信息 -
反复调同一个工具试图获得’对的格式’
所以工具函数在注册表里统一 return str(...),确保返回的是文本字符串。”
旧版 ReAct vs 官方 FC 版
小明查了资料:”ReAct 不是新东西吧?我好像见过很早就有文章讲。”
“对。ReAct 论文是 2022 年的,但在 OpenAI 推出 Function Calling 之前,大家怎么做 ReAct?”
“问题多:
-
模型输出格式不稳定——JSON 偶尔少个引号,整个流程就崩了 -
需要自定义协议——你得告诉模型’输出格式必须是 JSON,type 字段必须是什么’ -
没有 tool_call_id——多工具并行调用时容易乱”
“官方 Function Calling 解决了所有这些问题:
-
协议标准化—— tool_calls是 SDK 原生字段,格式零出错 -
并行调用天然支持——一个消息里可以有多个 tool_calls -
ID 追踪—— tool_call_id让结果精确对应到调用”
| 旧版 ReAct | 官方 FC 版 ReAct |
|---|---|
| 自定义 JSON 协议 | 原生 tool_calls 字段 |
| 格式不稳定,需要容错 | SDK 保证格式正确 |
| Observation 写成 user 文本 | 标准 role=tool 消息 |
| 多工具并行实现复杂 | 天然支持并行 tool_calls |
| 依赖 prompt 工程 | 依赖协议,prompt 更简洁 |
“思想没变——Thought → Action → Observation 循环。变的是载体——从手写 JSON 变成了原生协议。”
总结
老张说:”这一篇,你把 Agent 从’能调工具’变成了’能自主决策’。”
“三个核心理解:
-
ReAct 就是 while 循环—— while tool_calls → 执行 → 继续,直到模型觉得够了 -
tool_calls是信号——有就继续,没有就结束。不需要任何额外的判断逻辑 -
max_steps+tools=None兜底——防无限循环,强制收尾
跟之前的文章串起来:
每一步都在让 Agent 更自主。到了 ReAct 循环这一步,Agent 已经具备了’接到任务 → 分解步骤 → 调用工具 → 综合分析 → 给出答案’ 的完整能力。”
小明说:”那我理解了。ReAct 不是另一个功能,是把前面所有能力串起来的运行引擎。”
“对。你以前写的代码,模型是被动的——你说调什么它调什么。现在模型是主动的——它自己决定下一步做什么。这个从被动到主动的转变,就是 Agent 的灵魂。”
Agent 的灵魂不是能调多少个工具,而是能自己决定下一步做什么。
转载请注明来源:ReAct 循环:Agent 实现自主决策的核心引擎







