第03章 ReAct循环:思考-行动-观察的迭代

第03章 ReAct循环:思考-行动-观察的迭代

“真正的智慧不在于一次做对,而在于思考、尝试、观察,然后修正。这是科学方法的本质,也是Agent规划的核心。” —— Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, 2022


ReAct(Reasoning + Acting)是目前最广泛使用的 Agent 规划范式。它的核心思想极其简洁:Think → Act → Observe,循环往复,直到任务完成

这个循环看起来朴素,却解决了 LLM 面临的核心难题:如何在不确定的环境中,通过与外部世界的交互,逐步推进目标。


3.1 ReAct 的三元组:Thought-Action-Observation

# ====================================================
# ReAct 核心结构:Thought-Action-Observation 三元组
# ====================================================

from dataclasses import dataclass
from typing import Optional, Any
from enum import Enum
import json

class StepType(Enum):
    THOUGHT = "thought"       # 思考:Agent的推理过程
    ACTION = "action"         # 行动:调用工具或执行操作
    OBSERVATION = "observation"  # 观察:工具返回的结果


@dataclass
class ReActStep:
    """ReAct循环中的一个步骤"""
    step_type: StepType
    content: str              # 步骤内容
    tool_name: Optional[str] = None    # ACTION步骤使用的工具
    tool_input: Optional[dict] = None  # ACTION步骤的工具输入
    tool_output: Optional[Any] = None  # OBSERVATION步骤的工具输出
    step_index: int = 0


@dataclass 
class ReActTrace:
    """完整的ReAct执行轨迹"""
    goal: str
    steps: list  # List[ReActStep]
    final_answer: Optional[str] = None
    success: bool = False
    
    def to_prompt_context(self) -> str:
        """将轨迹格式化为LLM的上下文"""
        lines = [f"目标:{self.goal}\n"]
        
        for step in self.steps:
            if step.step_type == StepType.THOUGHT:
                lines.append(f"思考:{step.content}")
            elif step.step_type == StepType.ACTION:
                tool_input_str = json.dumps(step.tool_input, ensure_ascii=False)
                lines.append(f"行动:{step.tool_name}({tool_input_str})")
            elif step.step_type == StepType.OBSERVATION:
                lines.append(f"观察:{step.content}")
        
        return "\n".join(lines)


# ---- 一个完整的 ReAct 执行轨迹示例 ----

def create_example_trace() -> ReActTrace:
    """展示ReAct处理'查询今天上海天气'的完整轨迹"""
    
    trace = ReActTrace(goal="告诉我今天上海的天气情况,以及是否适合户外运动")
    
    trace.steps = [
        ReActStep(
            step_type=StepType.THOUGHT,
            content="我需要查询今天上海的天气。我应该使用天气查询工具获取当前温度、天气状况和风力等信息,然后根据这些数据判断是否适合户外运动。",
            step_index=1,
        ),
        ReActStep(
            step_type=StepType.ACTION,
            content="调用天气查询工具",
            tool_name="weather_search",
            tool_input={"city": "上海", "date": "today"},
            step_index=2,
        ),
        ReActStep(
            step_type=StepType.OBSERVATION,
            content='{"city": "上海", "date": "2026-04-30", "temp_high": 22, "temp_low": 15, "condition": "多云转晴", "wind": "东南风3级", "humidity": 65, "aqi": 48}',
            tool_output={"city": "上海", "temp_high": 22, "condition": "多云转晴"},
            step_index=3,
        ),
        ReActStep(
            step_type=StepType.THOUGHT,
            content="天气数据已获取:22℃,多云转晴,东南风3级,AQI 48(优)。温度适宜,空气质量好,风力不大,非常适合户外运动。我可以直接给出答案了。",
            step_index=4,
        ),
    ]
    
    trace.final_answer = (
        "今天上海天气:多云转晴,最高22℃,最低15℃,东南风3级,空气质量优(AQI 48)。\n"
        "✅ **非常适合户外运动**:温度舒适,空气质量好,风力适中。建议穿一件薄外套,早晚注意保暖。"
    )
    trace.success = True
    
    return trace


trace = create_example_trace()
print(trace.to_prompt_context())
print(f"\n最终答案:{trace.final_answer}")

3.2 实现完整的 ReAct Agent

# ====================================================
# 完整 ReAct Agent 实现
# ====================================================

import asyncio
import json
import re
from typing import Dict, Callable, Awaitable
from openai import AsyncOpenAI

client = AsyncOpenAI()


class ReActAgent:
    """
    基于 Thought-Action-Observation 循环的 Agent
    """
    
    def __init__(
        self, 
        tools: Dict[str, Callable],
        model: str = "gpt-4o",
        max_iterations: int = 10,
    ):
        self.tools = tools        # 工具字典:工具名 → 异步函数
        self.model = model
        self.max_iterations = max_iterations
        self.system_prompt = self._build_system_prompt()
    
    def _build_system_prompt(self) -> str:
        """构建 ReAct 系统提示词"""
        
        tools_desc = "\n".join([
            f"- {name}: {func.__doc__ or '无描述'}"
            for name, func in self.tools.items()
        ])
        
        return f"""你是一个使用ReAct模式工作的智能助手。

可用工具:
{tools_desc}

工作模式:
每次响应时,你必须按照以下格式之一输出:

格式A(还需要继续):
思考:[你对当前情况的分析和下一步计划]
行动:tool_name
输入:{{"param1": "value1", "param2": "value2"}}

格式B(任务完成):
思考:[为什么任务已经完成]
最终答案:[给用户的完整回答]

规则:
1. 每次只能执行一个工具调用
2. 必须先思考,再行动
3. 行动后等待观察结果,再决定下一步
4. 如果某个工具调用失败,思考原因并尝试不同的方法
5. 最多执行{self.max_iterations}次行动后必须给出最终答案
"""
    
    async def run(self, goal: str) -> ReActTrace:
        """
        执行 ReAct 循环直到任务完成
        
        Returns:
            完整的执行轨迹
        """
        trace = ReActTrace(goal=goal)
        messages = [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": f"请帮我完成以下目标:{goal}"},
        ]
        
        for iteration in range(self.max_iterations):
            print(f"\n--- 迭代 {iteration + 1} ---")
            
            # 调用 LLM 获取下一步
            response = await client.chat.completions.create(
                model=self.model,
                messages=messages,
                temperature=0.1,
            )
            
            llm_output = response.choices[0].message.content
            print(f"LLM输出:\n{llm_output}")
            
            # 解析 LLM 输出
            parsed = self._parse_llm_output(llm_output)
            
            if parsed["type"] == "final_answer":
                # 任务完成
                thought_step = ReActStep(
                    step_type=StepType.THOUGHT,
                    content=parsed["thought"],
                    step_index=len(trace.steps) + 1,
                )
                trace.steps.append(thought_step)
                trace.final_answer = parsed["answer"]
                trace.success = True
                print(f"\n✅ 任务完成:{parsed['answer']}")
                return trace
            
            elif parsed["type"] == "action":
                # 执行工具调用
                thought_step = ReActStep(
                    step_type=StepType.THOUGHT,
                    content=parsed["thought"],
                    step_index=len(trace.steps) + 1,
                )
                trace.steps.append(thought_step)
                
                action_step = ReActStep(
                    step_type=StepType.ACTION,
                    content=f"调用 {parsed['tool_name']}",
                    tool_name=parsed["tool_name"],
                    tool_input=parsed["tool_input"],
                    step_index=len(trace.steps) + 1,
                )
                trace.steps.append(action_step)
                
                # 执行工具
                observation = await self._execute_tool(
                    parsed["tool_name"], 
                    parsed["tool_input"]
                )
                
                obs_step = ReActStep(
                    step_type=StepType.OBSERVATION,
                    content=str(observation),
                    tool_output=observation,
                    step_index=len(trace.steps) + 1,
                )
                trace.steps.append(obs_step)
                
                # 将工具结果添加到对话历史
                messages.append({"role": "assistant", "content": llm_output})
                messages.append({
                    "role": "user", 
                    "content": f"观察:{str(observation)}\n\n请继续。"
                })
            
            else:
                # 解析失败,让LLM重试
                messages.append({"role": "assistant", "content": llm_output})
                messages.append({
                    "role": "user",
                    "content": "我无法解析你的输出格式。请严格按照'思考→行动→输入'或'思考→最终答案'的格式输出。"
                })
        
        # 达到最大迭代次数
        trace.final_answer = "已达到最大迭代次数,任务可能未完全完成。"
        trace.success = False
        return trace
    
    def _parse_llm_output(self, output: str) -> dict:
        """解析LLM输出,提取思考、行动或最终答案"""
        
        # 检查是否是最终答案
        final_match = re.search(r"最终答案[::]\s*(.+)", output, re.DOTALL)
        if final_match:
            thought_match = re.search(r"思考[::]\s*(.+?)(?=最终答案)", output, re.DOTALL)
            return {
                "type": "final_answer",
                "thought": thought_match.group(1).strip() if thought_match else "",
                "answer": final_match.group(1).strip(),
            }
        
        # 解析行动
        thought_match = re.search(r"思考[::]\s*(.+?)(?=行动[::])", output, re.DOTALL)
        action_match = re.search(r"行动[::]\s*(\w+)", output)
        input_match = re.search(r"输入[::]\s*(\{.+?\})", output, re.DOTALL)
        
        if action_match and input_match:
            try:
                tool_input = json.loads(input_match.group(1))
            except json.JSONDecodeError:
                tool_input = {}
            
            return {
                "type": "action",
                "thought": thought_match.group(1).strip() if thought_match else "",
                "tool_name": action_match.group(1),
                "tool_input": tool_input,
            }
        
        return {"type": "unknown", "raw": output}
    
    async def _execute_tool(self, tool_name: str, tool_input: dict) -> Any:
        """执行工具调用"""
        
        if tool_name not in self.tools:
            return f"错误:工具 '{tool_name}' 不存在"
        
        try:
            tool_func = self.tools[tool_name]
            result = await tool_func(**tool_input)
            return result
        except Exception as e:
            return f"工具执行失败:{str(e)}"


# ---- 定义工具并测试 ReAct Agent ----

async def weather_search(city: str, date: str = "today") -> dict:
    """查询指定城市的天气信息"""
    # 模拟天气API(实际使用真实API)
    mock_data = {
        "上海": {"temp_high": 22, "temp_low": 15, "condition": "多云转晴", "aqi": 48},
        "北京": {"temp_high": 18, "temp_low": 8, "condition": "晴", "aqi": 62},
    }
    return mock_data.get(city, {"error": f"找不到城市 {city} 的天气数据"})


async def web_search(query: str) -> str:
    """搜索互联网获取信息"""
    # 模拟搜索结果
    return f"搜索'{query}'的结果:找到3篇相关文章,主要内容为..."


async def calculator(expression: str) -> float:
    """计算数学表达式"""
    try:
        # 注意:生产环境应使用安全的表达式求值,而非eval
        import ast
        return float(ast.literal_eval(expression))
    except Exception as e:
        return f"计算错误:{e}"


async def main():
    agent = ReActAgent(
        tools={
            "weather_search": weather_search,
            "web_search": web_search,
            "calculator": calculator,
        },
        max_iterations=5,
    )
    
    goal = "查询上海今天的天气,并计算如果最高气温22℃换算成华氏度是多少"
    trace = await agent.run(goal)
    
    print("\n=== 执行轨迹摘要 ===")
    print(f"总步骤数: {len(trace.steps)}")
    print(f"执行成功: {trace.success}")
    print(f"最终答案: {trace.final_answer}")


asyncio.run(main())

3.3 ReAct 的终止条件与循环控制

# ====================================================
# 终止条件设计:防止无限循环
# ====================================================

from typing import Optional
import time


class TerminationController:
    """
    ReAct循环的终止条件控制器
    
    多种终止条件并行检测,任一满足即停止循环
    """
    
    def __init__(
        self,
        max_iterations: int = 15,
        max_time_seconds: float = 120,
        max_tool_calls: int = 20,
        repetition_window: int = 3,  # 检测最近N步是否有重复行为
    ):
        self.max_iterations = max_iterations
        self.max_time_seconds = max_time_seconds
        self.max_tool_calls = max_tool_calls
        self.repetition_window = repetition_window
        
        self.start_time = time.time()
        self.iteration_count = 0
        self.tool_call_count = 0
        self.recent_actions: list = []  # 记录最近的行动
    
    def should_terminate(self, trace: ReActTrace) -> tuple:
        """
        检查是否应该终止循环
        
        Returns:
            (应该终止, 终止原因)
        """
        self.iteration_count += 1
        elapsed = time.time() - self.start_time
        
        # 条件1:达到最大迭代次数
        if self.iteration_count >= self.max_iterations:
            return True, f"达到最大迭代次数 ({self.max_iterations})"
        
        # 条件2:超时
        if elapsed >= self.max_time_seconds:
            return True, f"执行超时 ({elapsed:.1f}秒)"
        
        # 条件3:工具调用次数过多
        action_steps = [s for s in trace.steps if s.step_type == StepType.ACTION]
        if len(action_steps) >= self.max_tool_calls:
            return True, f"工具调用次数过多 ({len(action_steps)}次)"
        
        # 条件4:检测循环行为(相同工具+相同输入重复出现)
        if len(action_steps) >= self.repetition_window:
            recent = action_steps[-self.repetition_window:]
            recent_signatures = [
                f"{s.tool_name}:{json.dumps(s.tool_input, sort_keys=True)}"
                for s in recent
            ]
            if len(set(recent_signatures)) < len(recent_signatures):
                return True, "检测到重复行为(Agent陷入循环)"
        
        return False, None
    
    def get_summary(self) -> dict:
        """获取执行统计摘要"""
        return {
            "iterations": self.iteration_count,
            "elapsed_seconds": round(time.time() - self.start_time, 2),
            "tool_calls": self.tool_call_count,
        }


# ---- 带终止控制的增强版 ReAct ----

async def react_with_termination_control(
    agent: ReActAgent,
    goal: str,
    termination_config: Optional[dict] = None
) -> ReActTrace:
    """
    带完善终止控制的 ReAct 执行
    """
    config = termination_config or {}
    controller = TerminationController(
        max_iterations=config.get("max_iterations", 10),
        max_time_seconds=config.get("max_time", 60),
        max_tool_calls=config.get("max_tools", 15),
    )
    
    trace = await agent.run(goal)  # 内部已有基本控制
    
    summary = controller.get_summary()
    print(f"\n执行统计:{summary}")
    
    return trace


# ---- ReAct vs. 简单LLM对比实验 ----

async def compare_react_vs_simple():
    """对比ReAct和直接LLM调用的效果"""
    
    goal = "找出Python中最快的JSON解析库,并比较它们的性能差异"
    
    # 方法1:直接LLM(一次性回答)
    print("=== 方法1:直接LLM(一次调用)===")
    direct_response = await client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": goal}],
    )
    print(f"直接回答:\n{direct_response.choices[0].message.content[:300]}...")
    print("(无法查询最新数据,答案可能过时)")
    
    # 方法2:ReAct(搜索+分析)
    print("\n=== 方法2:ReAct(工具增强)===")
    agent = ReActAgent(
        tools={"web_search": web_search},
        max_iterations=4,
    )
    trace = await agent.run(goal)
    print(f"ReAct答案:{trace.final_answer}")
    print(f"使用了 {len([s for s in trace.steps if s.step_type == StepType.ACTION])} 次工具调用")
    
    # 对比:ReAct的答案基于实际搜索,更可靠


asyncio.run(compare_react_vs_simple())

本章小结

  1. ReAct 的核心是 Thought-Action-Observation 三元组:思考给出方向,行动获取信息,观察更新认知。这个循环让 Agent 能够在不确定环境中逐步逼近目标。

  2. 与直接 LLM 调用的本质区别:ReAct 允许 Agent 在执行中途获取新信息,每一步的"观察"都可以改变后续决策,而不是基于训练时的静态知识一次性回答。

  3. 终止条件是不可或缺的安全机制:最大迭代次数、超时、重复行为检测,三重保障确保 Agent 不会进入无限循环。忽略这一点是初学者最常犯的错误。

  4. 解析 LLM 输出是工程关键:格式化的系统提示(Thought/Action/Observation 格式)加上健壮的正则解析,决定了 ReAct 的可靠性。

  5. ReAct 是后续所有架构的基础:第4章的 Plan-and-Execute、第5章的依赖图、第6章的动态重规划,都建立在 ReAct 的思想基础上。

# 核心行动:在你的项目中实现一个最小可用的 ReAct Agent
import asyncio

async def minimal_react_demo():
    """最小可用的 ReAct 实现,5分钟可以跑起来"""
    
    # 1. 定义你的工具(替换为真实工具)
    async def my_tool(query: str) -> str:
        """你的工具描述"""
        return f"工具返回结果: {query}"
    
    # 2. 创建 Agent
    agent = ReActAgent(
        tools={"my_tool": my_tool},
        max_iterations=5,
    )
    
    # 3. 运行
    trace = await agent.run("你的任务目标")
    print(f"结果: {trace.final_answer}")

asyncio.run(minimal_react_demo())

本章提示词模板

【模板1:ReAct系统提示词(可直接使用)】
你是一个使用ReAct(推理+行动)模式的智能助手。

可用工具:
{tools_list}

你必须按照以下格式输出(每次响应只能是其中一种):

需要使用工具时:
思考:[分析当前情况,说明为什么需要这个工具]
行动:[工具名称]
输入:{"参数名": "参数值"}

任务完成时:
思考:[总结已获得的所有信息,确认任务已完成]
最终答案:[给用户的完整、清晰的回答]

重要规则:
- 每次只调用一个工具
- 必须先思考,再行动
- 根据观察结果调整你的思考方向
- 如果工具失败,思考原因并尝试其他方法
【模板2:ReAct调试提示词】
以下ReAct执行轨迹出现了问题,请分析原因:

执行轨迹:
{trace_content}

问题症状:{problem_description}

请分析:
1. Agent在第几步开始偏离正确方向?原因是什么?
2. 哪个思考步骤出现了逻辑错误?
3. 工具调用的参数是否正确?
4. 应该如何修改系统提示词或工具设计来避免此类问题?

给出具体的改进建议。

→ 第04章:Plan-and-Execute:先规划再执行