ReAct 循环:Agent 实现自主决策的核心引擎

    |     2026年8月17日   |   AI大模型应用, 原生agent   |     0 条评论   |    2

上篇你给 Agent 加上了记忆——消息链让它记住整个会话。但记忆只是基础,真正的 Agent 需要反复思考、反复行动,直到任务完成。这篇讲 Agent 的灵魂——ReAct 循环。


小明发现 Agent “不想多干活”

小明把记忆、工具注册表、MCP 都接上了。Agent 能聊天、能查天气、能搜新闻,看起来很完美。

但他发现一个问题:

你:帮我查北京和上海的天气,然后告诉我哪个城市更适合出去玩 Agent:(调用 get_weather(city="北京")) Agent:北京晴,15°C。我只能告诉你北京的天气。

“它只查了北京!上海呢?” 小明很困惑。

他去问老张。老张看了一眼代码:”你写的逻辑是 if tool_calls → 执行 → 喂回去 → 结束。也就是说,每次只能调一次工具。”

“那我让它查两个城市,它应该先查北京,再查上海,然后对比——这不就是多做几步的事吗?”

“对,你需要的是——循环。”


什么是 ReAct

老张在白板上画了一个三角形:

Thought(思考) ↓ Action(行动) ↓ Observation(观察) ↓ Thought(再思考) ↓ ...

“这个模式叫 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 行:”

class ReactAgent: def __init__(self, max_steps: int = 5) -> None: self.max_steps = max_steps self.tools = get_tools_schema() def run(self, user_task: str) -> str: memory = ShortTermMemory(max_messages=60) memory.add_system(SYSTEM_PROMPT) memory.add({"role": "user", "content": f"请帮我完成这个任务:{user_task}"}) for step in range(1, self.max_steps + 1): # 1. 调用 LLM message = chat(memory.to_api_format(), tools=self.tools) # 2. 无 tool_calls → 任务完成 if not message.tool_calls: return message.content or "" # 3. 有 tool_calls → 执行 memory.add(assistant_to_dict(message)) for tool_call in message.tool_calls: result = execute_tool(tool_call.function.name, tool_call.function.arguments) memory.add({ "role": "tool", "tool_call_id": tool_call.id, "content": result, }) # 4. 达到上限 → 强制收尾 memory.add({"role": "user", "content": "你已用完工具调用次数,请基于已有信息给出最终答案。"}) return chat(memory.to_api_format(), tools=None).content

“一行行拆解:”

第一步:初始化

def __init__(self, max_steps: int = 5): self.max_steps = max_steps self.tools = get_tools_schema()

“两个配置:max_steps 是安全阀,防止无限循环;tools 是工具 Schema,每次循环都要传给 LLM。”

第二步:准备记忆

memory = ShortTermMemory(max_messages=60) memory.add_system(SYSTEM_PROMPT) memory.add({"role": "user", "content": f"请帮我完成这个任务:{user_task}"})

“每次 run() 都新建一个 ShortTermMemory,灌入 system 指令和用户任务。这和上篇讲的记忆机制完全一致。”

“这里的关键是 system prompt 的设计:”

你是一个能完成复杂任务的智能助手,可以反复使用工具直到信息足够。 规则: - 需要信息时调用工具(可多次、可换工具) - 收集到足够信息后,直接用自然语言给出最终答案(不再调用工具) - 不要用相同参数重复调用同一个工具 - 回答用中文,简洁清晰

“注意这条:’不要用相同参数重复调用同一个工具’。如果没有这条,模型可能在某个问题上死循环——比如查天气失败,它反复用同样的参数重试。”

第三步:主循环

for step in range(1, self.max_steps + 1): message = chat(memory.to_api_format(), tools=self.tools)

“每步做三件事:调 LLM → 判断 → 执行或返回。”

“注意这里 tools=self.tools——每一步都把完整的工具 Schema 传进去。因为模型需要在每一轮评估’我还能用什么工具’。”

# 无 tool_calls → 任务完成 if not message.tool_calls: return message.content or ""

“这是循环唯一的正常出口。模型判断信息够了,直接返回自然语言回答。注意:此时不追加任何 user 消息,因为模型已经在 assistant 消息里说话了。”

# 有 tool_calls → 执行 memory.add(assistant_to_dict(message)) for tool_call in message.tool_calls: result = execute_tool(name, args) memory.add({ "role": "tool", "tool_call_id": tool_call.id, "content": result, })

“关键动作:

  1. assistant(tool_calls) 写入记忆(完整保留 tool_call.id)
  2. 逐个执行工具
  3. 把每个工具结果以 role=tool 写入记忆,tool_call_id 必须和上面的 assistant 对应

continue 是隐式的——Python for 循环自动进入下一步。”

“上篇讲记忆的时候强调过:assistant(tool_calls) 和 tool 消息必须成对,ID 必须对应。这里严格执行了这个规则。”

第四步:强制收尾

memory.add({"role": "user", "content": "你已用完工具调用次数,请基于已有信息给出最终答案。"}) return chat(memory.to_api_format(), tools=None).content

“当循环跑满 max_steps 还没结束——说明模型太’贪心’了,一直在调工具。这时强制追加一条 user 消息让它收尾,同时 tools=None 不让它再调任何工具。”

“两个保险手段组合使用:

  1. 追加 user 消息——’你已经不能再调工具了’
  2. tools=None——从协议层面禁止调工具

只做其中一项不够。光靠文字提示,模型可能忽略;光靠 tools=None,模型可能返回空内容。两项一起才稳妥。”


完整运行过程

老张拿一个真实任务来演示:

你:帮我搜索最近 AI 领域的新闻,总结出 3 条最重要的

运行过程:

[步骤 1/5] [调用工具]: web_search [参数]: {"query": "AI 新闻 2026年8月"} [工具结果]: 1. OpenAI 发布新模型 | ... 2. AI 芯片新突破 | ... 3. ... [步骤 2/5] [调用工具]: web_search [参数]: {"query": "AI 行业动态 2026"} [工具结果]: 1. 国产大模型进展 | ... 2. AI 投资趋势 | ... [步骤 3/5] [任务完成,共 3 步] Agent:根据搜索结果,近期 AI 领域最重要的 3 条新闻是: 1. OpenAI 发布了新一代推理模型... 2. 国内多款大模型通过国家备案... 3. AI 芯片供应的地缘政治影响持续升温...

“模型做了 3 步:

  1. 搜第一轮——拿到一些结果
  2. 觉得信息不够全面,换了个关键词再搜一轮
  3. 两轮信息够了,开始总结

每一步都是模型自己做的决策——要不要继续、换什么工具、用什么参数。你只写了循环框架,策略全在模型侧。”


对比:单步 FC vs ReAct

小明想起之前的代码:”我们之前写的也是调工具,跟现在有什么区别?”

老张把两段代码并排:

单步(之前)

message = chat(messages, tools=tools) if message.tool_calls: # 执行工具,喂结果 messages.append(tool_result) # 第二轮,不带 tools,强制回答 final = chat(messages, tools=None) return final.content else: return message.content

多步(现在)

for step in range(1, max_steps + 1): message = chat(messages, tools=tools) if not message.tool_calls: return message.content # 执行工具,写结果,继续循环

“差异一目了然:

  1. 单步用 if——最多调一次工具;多步用 for——一直调到够
  2. 单步第二轮 tools=None固定行为;多步只在兜底时才 tools=None
  3. 多步每轮都传 tools,让模型有持续的行动能力
  4. 多步需要 max_steps 兜底,单步不需要”

两个常见坑

坑 1:无限循环

“如果不设 max_steps,模型可能在某个问题上死循环。比如:

你:帮我找一个永远不会下雨的城市 Agent:查北京天气 → 有雨 Agent:查上海天气 → 有雨 Agent:查广州天气 → 有雨 Agent:查深圳天气 → 有雨 ...

“模型永远找不到’永远不会下雨的城市’,但每次工具都正常返回——它就会一直循环下去。max_steps 是唯一的硬停机制。”

“另一个防御手段是 system prompt 里的’不要用相同参数重复调用’——但这是软的,不能 100% 依赖。”

坑 2:工具结果格式不对导致循环乱掉

“如果工具返回的不是纯文本(比如返回了一个 JSON 对象、或者报错栈),模型可能:

  • 把它当成操作指令来执行
  • 识别不了里面的关键信息
  • 反复调同一个工具试图获得’对的格式’

所以工具函数在注册表里统一 return str(...),确保返回的是文本字符串。”


旧版 ReAct vs 官方 FC 版

小明查了资料:”ReAct 不是新东西吧?我好像见过很早就有文章讲。”

“对。ReAct 论文是 2022 年的,但在 OpenAI 推出 Function Calling 之前,大家怎么做 ReAct?”

# 旧版:用 JSON 格式区分"调工具"和"最终答案" response = chat(messages) # 模型返回普通文本 if '"type": "tool_call"' in response: # 解析 JSON,调工具 result = call_tool(json.loads(response)) messages.append(f"Observation: {result}") elif '"type": "final_answer"' in response: return json.loads(response)["content"]

“问题多:

  1. 模型输出格式不稳定——JSON 偶尔少个引号,整个流程就崩了
  2. 需要自定义协议——你得告诉模型’输出格式必须是 JSON,type 字段必须是什么’
  3. 没有 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 从’能调工具’变成了’能自主决策’。”

“三个核心理解:

  1. ReAct 就是 while 循环——while tool_calls → 执行 → 继续,直到模型觉得够了
  2. tool_calls 是信号——有就继续,没有就结束。不需要任何额外的判断逻辑
  3. max_steps + tools=None 兜底——防无限循环,强制收尾

跟之前的文章串起来:

Function Calling → 让模型"会用工具" 工具注册表 → 让模型"方便用很多工具" MCP → 让模型"用全世界的工具" 记忆 → 让模型"记住对话历史" ReAct 循环 → 让模型"自己决定用几次、用什么"

每一步都在让 Agent 更自主。到了 ReAct 循环这一步,Agent 已经具备了’接到任务 → 分解步骤 → 调用工具 → 综合分析 → 给出答案’ 的完整能力。”

小明说:”那我理解了。ReAct 不是另一个功能,是把前面所有能力串起来的运行引擎。”

“对。你以前写的代码,模型是被动的——你说调什么它调什么。现在模型是主动的——它自己决定下一步做什么。这个从被动到主动的转变,就是 Agent 的灵魂。”

Agent 的灵魂不是能调多少个工具,而是能自己决定下一步做什么。

转载请注明来源:ReAct 循环:Agent 实现自主决策的核心引擎
本文链接地址:https://ai.zhousir.top/?p=3772
回复 取消