
ReAct 循环设计的最核心动机——让模型有权在观察到失败后自主纠偏而不是一条路走到黑。这背后是一个从预编排到反应式的范式转变。传统的预编排模式一次赌命在没有 ReAct 循环的早期 Agent 设计中常见做法是一次性规划所有步骤然后批量执行。python# ❌ 预编排模式无循环一次决定所有工具没有回头路 plan llm.plan(提取用户数据) # 计划: [工具A, 工具B, 工具C] results [] for tool in plan: results.append(execute(tool)) # 按计划傻傻执行不看结果 final_answer llm.summarize(results) # 可能拿到空数据但木已成舟只能硬编在这种模式下如果工具A失败了Agent 是看不见的。它只能按既定计划走完全程最后拿着一堆空或错误的结果去硬着头皮回答。这就好比蒙着眼走迷宫撞墙了也不知道回头。ReAct 循环模式每步观察随时纠偏你给的例子完美展示了循环的必要性python# ✅ ReAct 循环模式观察每一步随时调整策略 messages [{role: user, content: 提取用户数据}] # 第一步 # 模型想我先从主数据库查 response llm.chat(messages, tools[工具A_主数据库, 工具B_备份库, 工具C_缓存]) # 模型决定调用 工具A_主数据库 result_A execute(工具A_主数据库) # → 连接超时无数据 messages.append(工具A 的失败结果) # 第二步关键 # 模型看到失败重新推理 # 工具A连不上。可能主库挂了我试试备份库。 response llm.chat(messages, tools[工具A, 工具B, 工具C]) # 模型决定调用 工具B_备份库 result_B execute(工具B_备份库) # → 错误权限不足 messages.append(工具B 的错误结果) # 第三步再次纠偏 # 模型看到错误再次推理 # 备份库也没权限。可能是配置问题。我看看缓存里有没有旧数据。 response llm.chat(messages, tools[工具A, 工具B, 工具C]) # 模型决定调用 工具C_缓存 result_C execute(工具C_缓存) # → 找到用户数据{...} messages.append(工具C 的成功结果) # 第四步 # 模型看到数据了整理输出 response llm.chat(messages, tools[...]) # 最终回复为您找到数据如下...如果没有循环故事在第一步就结束了——Agent 拿到一个空结果只能对你说没找到。有了循环Agent 展现了三种关键能力错误识别它知道空和权限不足是两种不同性质的失败策略调整根据错误类型自主选择不同的备选方案目标坚持不会因为一两次失败就放弃直到真正无计可施循环结构赋予模型的本质上是试错权你的例子揭示了一个更深层的设计哲学ReAct 循环的本质是将控制流从代码中剥离交给模型本身。在传统编程中if-else分支是我们开发者提前写死的pythonresult_A 工具A() if not result_A: result_B 工具B() # 这个fallback是程序员硬编码的在 ReAct 循环中if-else逻辑是模型实时推理出来的。这意味着面对从未预见的错误模型也可能想出合理的下一步工具集可以动态变化模型能在运行时决定用哪个错误恢复策略不是写死的而是模型基于对错误信息的理解即时生成的你这个问题问到了 ReAct 循环的灵魂——不是为了让 Agent 多走几步而是为了赋予它在行动中观察、在失败后调整、在不确定中寻找确定性的能力。循环只是这种能力的物理载体。从最简实现到接近 OpenClaw 风格的工程化版本逐步递进。一、极简核心ReAct 的本质Agent 不是什么魔法本质就是一个while循环python# 最简 ReAct 实现伪代码级别的核心逻辑 messages [{role: user, content: user_input}] while True: response llm.chat(messages, toolsavailable_tools) if not response.has_tool_calls(): # 没有工具调用 → 任务完成返回最终回答 return response.content # 有工具调用 → 执行工具 → 结果追加到消息历史 → 继续循环 for tool_call in response.tool_calls: result execute_tool(tool_call.name, tool_call.arguments) messages.append({ role: tool, tool_call_id: tool_call.id, content: result })二、TypeScript 实现接近 OpenClaw 源码风格OpenClaw 的核心 Agent Loop 用 TypeScript 实现来自runEmbeddedPiAgent函数typescript// 接近 OpenClaw 源码的 Agent 主循环 async function runAgentLoop(params: AgentLoopParams): Promisestring { const messages: MessageParam[] [ { role: user, content: params.userInput } ]; while (true) { // 1. THINK调用 LLM 进行推理 const response await client.messages.create({ model: claude-sonnet-4-20250514, max_tokens: 8096, tools: toolDefinitions, // 所有可用工具的 JSON Schema messages, }); // 2. REFLECT判断模型是否要调工具 if (response.stop_reason tool_use) { // 3. ACT OBSERVE执行工具并收集结果 const toolResults await Promise.all( response.content .filter((block) block.type tool_use) .map(async (block) ({ type: tool_result as const, tool_use_id: block.id, content: await executeTool(block.name, block.input), })) ); // 将助手消息和工具结果追加到历史 messages.push({ role: assistant, content: response.content }); messages.push({ role: user, content: toolResults }); // 继续循环——让 LLM 根据工具结果再想一轮 continue; } // 4. 无工具调用 → 任务完成 return response.content.find((b) b.type text)?.text ?? ; } }关键的终止条件来自stop_reason字段当模型返回end_turn而不是tool_use时说明它认为任务已经完成循环退出。三、Python 实现可运行的多工具示例下面是一个可直接运行的 Python 版本模拟了两个工具pythonimport json from openai import OpenAI client OpenAI() # 工具定义 tools [ { type: function, function: { name: get_weather, description: 获取指定城市的当前天气信息, parameters: { type: object, properties: { city: {type: string, description: 城市名称} }, required: [city] } } }, { type: function, function: { name: read_file, description: 读取本地文件的文本内容, parameters: { type: object, properties: { path: {type: string, description: 文件绝对路径} }, required: [path] } } } ] # 工具实现 def execute_tool(name: str, args: dict) - str: 实际执行工具调用生产环境中这里可能执行 shell 命令、数据库查询等 if name get_weather: return f{args[city]}今天晴气温 18-26°C适合外出 elif name read_file: try: with open(args[path], r, encodingutf-8) as f: return f.read() except FileNotFoundError: return f错误文件 {args[path]} 不存在 return 未知工具 # ReAct 循环核心 def run_agent_loop(user_message: str, max_iterations: int 15): Agent 主循环——ReAct 范式的直接实现。 循环逻辑 1. ThinkLLM 推理下一步该做什么 2. Reflect判断是否需要调用工具 3. Act Observe执行工具并收集结果 4. 结果追加到消息历史回到步骤 1 5. 当 LLM 不再请求工具调用时返回最终回答 messages [{role: user, content: user_message}] for step in range(1, max_iterations 1): print(f\n{*50}) print(f Step {step}: Think思考) print(f{*50}) # ① Think调用 LLM response client.chat.completions.create( modelgpt-4o, messagesmessages, toolstools, tool_choiceauto ) assistant_msg response.choices[0].message messages.append(assistant_msg) # ② Reflect是否有工具调用 if not assistant_msg.tool_calls: print(f\n{*50}) print(f 任务完成) print(f{*50}) return assistant_msg.content print(fLLM 决定调用 {len(assistant_msg.tool_calls)} 个工具) # ③ Act Observe执行每个工具调用 for tool_call in assistant_msg.tool_calls: func_name tool_call.function.name func_args json.loads(tool_call.function.arguments) print(f\n Act: 调用 {func_name}({func_args})) result execute_tool(func_name, func_args) print(f Observe: {result[:100]}...) # 将工具结果追加到消息历史 messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) # 循环继续让 LLM 根据新的工具结果再次推理 return 达到最大迭代次数任务未完成 # 运行示例 if __name__ __main__: result run_agent_loop( 北京今天天气怎么样同时帮我读取 /tmp/notes.txt 文件 ) print(f\n最终回答{result})这段代码的运行过程清晰展示了 ReAct 的每个轮次LLM 先决定调用工具 → 执行工具得到结果 → 结果追加到上下文 → LLM 再次推理决定下一步 → 最终输出完整答案。四、OpenClaw 源码中的真实结构OpenClaw 的实际源码比上面复杂得多但核心循环体始终保持简洁。额外能力不是塞进循环内部而是叠加在循环外部textrunEmbeddedPiAgent() ← 外层重试、容错、故障转移 └── while (true) { ← 主重试循环 ├── 检查重试次数限制 ├── 调用 runEmbeddedAttempt() ← 单次推理尝试 ├── 处理 context overflow → 自动压缩 ├── 处理 auth failure → profile 轮换 ├── 处理 timeout → 重试或报错 └── 成功则返回结果 }而runEmbeddedAttempt()内部的 ReAct 循环本质就是前面那 20 行代码的模式——一次 LLM 调用有工具调工具没工具就返回。扩展能力子 Agent、上下文压缩、Skills 加载、多模型轮换全部通过三种方式接入扩展工具集和 handler、调整系统提示结构、把状态外化到文件或数据库循环体本身基本不动。五、关键设计要点总结要素设计决策目的while True循环让 LLM 自主决定何时结束不预设步骤数模型自己判断任务完成度stop_reason判断依赖 API 的停止原因字段区分我要调工具vs我回答完了工具结果追加结果作为tool_result角色追加到 messages让 LLM 在下一轮看到执行反馈最大迭代限制硬性上限默认 15-40 次防止无限循环烧 token工具并行调用Promise.all同时执行不相关的工具调用可以并行提高效率如果你需要更具体的实现细节——比如工具定义怎么写 LLM 才不容易用错、上下文压缩的触发逻辑、或者子 Agent 的 spawn 机制——可以继续展开。