---
title: "Agent_02_ReAct模式"
source: https://www.zbhgis.com/blog/blog-20260106-1
author: zbhgis 浩瀚地学
created: 2026-01-05
tags: [Agent, agent, Python]
---

> 本文来自浩瀚地学，规范页面（含代码高亮与图片灯箱）：https://www.zbhgis.com/blog/blog-20260106-1
# Agent_02_ReAct模式

## ReAct 是什么

ReAct（Reasoning + Acting）是 Agent 领域被引用最多的模式之一。本质上就是在每一步行动之前，让 LLM 先输出一段思考过程。

没有这个思考步骤时，LLM 选工具的决策是黑盒的：

```python
# 反例：没有 Thought，直接调工具
for step in steps:
    tool_name = llm.choose_tool(step)  # LLM 直接选工具
    result = tools.execute(tool_name)  # 执行
```

它为什么选这个工具而不是那个？不知道。选错了也不知道它当时怎么想的。

加上 Thought 之后：

```
Thought: 用户想分析销售数据，但首先需要读取文件。我应该用 read_csv 工具。
Action: read_csv(path="sales.csv")
Observation: 读取成功，共 1000 行，5 列...
Thought: 数据已经读取完毕，接下来需要分析趋势。用 analyze 工具。
Action: analyze(data=...)
Observation: Q4 增长 15%...
Thought: 分析完成了，可以生成最终报告了。
Final Answer: 根据分析，Q4 销售增长 15%...
```

每一行 Thought 都是一个可检查的决策点。回头看日志，能清楚知道 Agent 为什么在这一步选择了这个工具、基于什么信息做了判断。

ReAct 的循环就是三步：**Thought → Action → Observation**，不断循环直到输出 Final Answer。

---

## Python 代码示例

### 示例一：ReAct Agent 完整实现

完整的 ReAct 循环，核心逻辑不到 50 行：

```python
"""
ReAct Agent —— Thought → Action → Observation 循环
"""

from dataclasses import dataclass
from typing import Any


@dataclass
class ReActStep:
    """ReAct 单步记录"""
    thought: str = ""
    action_name: str = ""
    action_args: dict = None
    observation: str = ""
    is_final: bool = False


class ReActAgent:
    """ReAct Agent 主循环"""

    def __init__(self, llm, tool_registry, max_steps: int = 10, verbose: bool = True):
        self.llm = llm
        self.tools = tool_registry
        self.max_steps = max_steps
        self.verbose = verbose

    def run(self, goal: str) -> str:
        system_prompt = self._build_system_prompt()
        context = self._build_initial_context(goal)

        steps: list[ReActStep] = []

        for step_num in range(1, self.max_steps + 1):
            if self.verbose:
                print(f"\n{'='*50}")
                print(f"  第 {step_num} 步")
                print(f"{'='*50}")

            # 1. LLM 输出 Thought + Action
            response = self.llm.generate_with_react_format(
                context=context,
                system=system_prompt,
                steps=steps,
            )

            step = ReActStep(
                thought=response.get("thought", ""),
                action_name=response.get("action", ""),
                action_args=response.get("action_args", {}),
                is_final=response.get("is_final", False),
            )

            if self.verbose:
                print(f"  Thought: {step.thought}")

            # 2. 判断是否是最终答案
            if step.is_final:
                step.observation = step.action_name
                steps.append(step)
                if self.verbose:
                    print(f"  Final Answer: {step.observation}")
                return step.observation

            # 3. 执行工具
            if self.verbose:
                print(f"  Action: {step.action_name}({step.action_args})")

            result = self.tools.execute(step.action_name, **step.action_args)

            step.observation = result.output if result.success else f"错误: {result.error}"
            steps.append(step)

            if self.verbose:
                status = "成功" if result.success else f"失败: {result.error}"
                print(f"  Observation: {status}")

            # 4. 追加到上下文
            context = self._append_step(context, step)

        return "达到最大步数限制，未能完成。最后的结果:\n" + self._format_steps(steps)

    def _build_system_prompt(self) -> str:
        tool_list = "\n".join(
            f"- {t['name']}: {t['description']}"
            for t in self.tools.get_schemas()
        )
        return f"""你是一个 ReAct Agent。你必须严格遵守以下格式输出：

Thought: <你的思考过程>
Action: <工具名称>
Action Input: <工具的 JSON 参数>

或者当你有了最终答案时：

Thought: 我已经有了答案
Final Answer: <你的最终回复>

可用工具：
{tool_list}

注意：
1. 每一步必须先 Thought，再 Action
2. 不要编造 Observation，Observation 由我提供
3. 一旦有了最终答案，输出 Final Answer 并结束"""

    def _build_initial_context(self, goal: str) -> str:
        return f"目标: {goal}\n\n开始执行。\n"

    def _append_step(self, context: str, step: ReActStep) -> str:
        context += f"Thought: {step.thought}\n"
        if step.is_final:
            context += f"Final Answer: {step.observation}\n"
        else:
            context += f"Action: {step.action_name}\n"
            context += f"Action Input: {step.action_args}\n"
            context += f"Observation: {step.observation}\n"
        return context

    def _format_steps(self, steps: list[ReActStep]) -> str:
        lines = []
        for i, s in enumerate(steps, 1):
            lines.append(f"Step {i}:")
            lines.append(f"  Thought: {s.thought}")
            if not s.is_final:
                lines.append(f"  Action: {s.action_name}({s.action_args})")
            lines.append(f"  Observation: {s.observation}")
        return "\n".join(lines)
```

几个要点：

- **格式由系统 prompt 约束**，不是代码强制的。LLM 必须按 `Thought → Action → Action Input` 输出
- **Final Answer 是退出条件**：LLM 判断信息够了就输出 Final Answer，循环终止
- **每步结果追加到上下文**：LLM 下一轮能看到所有历史信息，这是 ReAct 的记忆机制
- **max_steps 是安全阀**：防止 LLM 陷入无限循环

---

### 示例二：LLM 的 ReAct 输出解析

这是最容易搞砸的部分。LLM 不一定乖乖遵守格式，解析逻辑必须有容错：

```python
import re
import json


class ReActOutputParser:
    """ReAct 输出解析器 —— 把 LLM 的文本输出解析为结构化数据"""

    def parse(self, text: str) -> dict:
        """
        解析 LLM 输出

        返回格式：
        {
            "thought": "思考内容",
            "action": "工具名" 或 "",
            "action_args": {"key": "value"},
            "is_final": False,
            "final_answer": "",
        }
        """

        # 策略一：精确匹配 Thought/Action/Observation 格式
        thought_match = re.search(r"Thought:\s*(.+?)(?=\nAction:|\nFinal Answer:|$)", text, re.DOTALL)
        final_match = re.search(r"Final Answer:\s*(.+)$", text, re.DOTALL)
        action_match = re.search(r"Action:\s*(.+?)(?=\n|$)", text)
        args_match = re.search(r"Action Input:\s*(.+?)(?=\n|$)", text, re.DOTALL)

        thought = thought_match.group(1).strip() if thought_match else ""

        if final_match:
            return {
                "thought": thought,
                "action": "",
                "action_args": {},
                "is_final": True,
                "final_answer": final_match.group(1).strip(),
            }

        if action_match:
            action_name = action_match.group(1).strip()

            action_args = {}
            if args_match:
                args_text = args_match.group(1).strip()
                action_args = self._safe_parse_json(args_text)

            return {
                "thought": thought,
                "action": action_name,
                "action_args": action_args,
                "is_final": False,
                "final_answer": "",
            }

        # 策略二：模糊匹配
        return self._fuzzy_parse(text)

    def _safe_parse_json(self, text: str) -> dict:
        """安全解析 JSON，失败返回空字典"""
        try:
            return json.loads(text)
        except json.JSONDecodeError:
            try:
                fixed = re.sub(r'(\w+):', r'"\1":', text)
                return json.loads(fixed)
            except json.JSONDecodeError:
                brace_match = re.search(r'\{(.+?)\}', text, re.DOTALL)
                if brace_match:
                    try:
                        fixed = re.sub(r'(\w+):', r'"\1":', brace_match.group(1))
                        return json.loads("{" + fixed + "}")
                    except json.JSONDecodeError:
                        pass
                return {}

    def _fuzzy_parse(self, text: str) -> dict:
        """模糊解析——LLM 不按格式输出时的兜底"""
        conclusion_keywords = ["结论", "总结", "最终", "因此", "所以", "综上所述"]
        if any(kw in text for kw in conclusion_keywords):
            return {
                "thought": "",
                "action": "",
                "action_args": {},
                "is_final": True,
                "final_answer": text,
            }

        return {
            "thought": "",
            "action": "",
            "action_args": {},
            "is_final": True,
            "final_answer": text,
        }
```

LLM 经常：漏写 `Action:` 前缀、JSON key 不带引号、Thought 和 Action 写同一行、直接给答案不写格式。好的解析器必须做精确匹配到模糊匹配到兜底解析的三级容错。

---

### 示例三：ReAct 循环的三种变体

根据任务不同，有不同的循环策略：

```python
from dataclasses import dataclass


@dataclass
class ReactResponse:
    """ReAct 响应"""
    thought: str = ""
    action: str = ""
    args: dict = None
    is_final: bool = False
    answer: str = ""
    needs_replan: bool = False


class ReActStrategy:
    """ReAct 循环的三种策略"""

    @staticmethod
    def standard(llm, tools, goal, max_steps=10):
        """
        标准 ReAct：每步 Thought → Action → Observation

        适用场景：工具不多、步骤不长的任务。
        优点：简单直接。
        缺点：容易陷入长循环，每步都要 LLM 输出。
        """
        context = f"目标: {goal}\n"
        for _ in range(max_steps):
            response = llm.generate_react(context)
            if response.is_final:
                return response.answer
            result = tools.execute(response.action, **response.args)
            context += f"\nThought: {response.thought}\nAction: {response.action}\nObservation: {result}"
        return "达到最大步数"

    @staticmethod
    def plan_then_act(llm, tools, goal, max_steps=10):
        """
        先规划再执行：先让 LLM 生成完整计划，然后按 ReAct 循环执行

        适用场景：步骤较多、逻辑复杂的任务。
        优点：LLM 先想好全局计划，不容易跑偏。
        缺点：规划可能不准确，执行中需要动态调整。
        """
        # Phase 1：规划
        plan = llm.generate_plan(goal)
        context = f"目标: {goal}\n计划: {plan}\n"

        # Phase 2：按 ReAct 执行，但可以偏离计划
        for step in range(max_steps):
            response = llm.generate_react_with_plan(context, plan)
            if response.is_final:
                return response.answer
            result = tools.execute(response.action, **response.args)
            context += f"\nObservation: {result}"
            if response.needs_replan:
                plan = llm.update_plan(context, plan)
        return "达到最大步数"

    @staticmethod
    def reflexion(llm, tools, goal, max_steps=10, max_retries=2):
        """
        Reflexion ReAct：执行后自我反思，结果不好就重试

        适用场景：对输出质量要求高、允许重试的任务。
        优点：自我纠错，结果质量高。
        缺点：成本高，每轮重试都要重新调 LLM。
        """
        context = f"目标: {goal}\n"
        for attempt in range(max_retries):
            result, steps = ReActStrategy._run_react_loop(
                llm, tools, context, max_steps
            )

            critique = llm.critique(result, goal)
            if critique.is_good:
                return result

            context += f"\n\n上次尝试结果: {result}\n反思: {critique.feedback}\n请重新执行，注意避免以上问题。"

        return result

    @staticmethod
    def _run_react_loop(llm, tools, context, max_steps):
        """ReAct 循环的内部实现"""
        steps = []
        for _ in range(max_steps):
            response = llm.generate_react(context)
            if response.is_final:
                return ReactResponse(is_final=True, answer=response.answer), steps
            result = tools.execute(response.action, **response.args)
            context += f"\nObservation: {result}"
            steps.append(response)
        return ReactResponse(is_final=True, answer="达到最大步数"), steps
```

选择标准：工具少步骤短用 Standard；步骤多逻辑复杂用 Plan-Then-Act；对质量要求高且允许重试用 Reflexion。

---

### 示例四：Few-shot ReAct Prompt——好示例 vs 坏示例

ReAct 的成败，一半取决于 system prompt 写得好不好。下面对比三种写法：

```python
"""
Few-shot ReAct Prompt 设计
"""


class ReActPromptBuilder:
    """ReAct 系统 prompt 构建器"""

    @staticmethod
    def bad(tools: list[dict]) -> str:
        """坏 prompt —— 只有规则描述，没有示例"""
        tool_desc = "\n".join(f"- {t['name']}: {t['description']}" for t in tools)
        return f"""你是一个智能助手。请使用以下格式回答：

Thought: 你的思考
Action: 工具名
Action Input: 参数
Observation: 结果

可用工具：
{tool_desc}

请先思考再行动。"""

    @staticmethod
    def good(tools: list[dict]) -> str:
        """好 prompt —— 一个完整的 few-shot 示例"""
        tool_desc = "\n".join(f"- {t['name']}: {t['description']}" for t in tools)
        return f"""你是一个数据分析助手。请严格遵守以下格式：

Thought: <思考为什么需要这个步骤>
Action: <工具名称>
Action Input: <JSON 格式的参数>
Observation: <工具执行结果（由我提供）>

或者当你有最终答案时：

Thought: <思考为什么可以结束了>
Final Answer: <你的最终回复>

可用工具：
{tool_desc}

---
示例：

用户：北京今天天气怎么样？

Thought: 用户想知道北京的天气，我需要调用天气查询工具。
Action: get_weather
Action Input: {{"city": "北京", "unit": "celsius"}}
Observation: 北京今天晴，气温 15-22°C，微风。

Thought: 已经获取了天气信息，可以回答用户了。
Final Answer: 北京今天晴天，气温 15-22°C，风力较小。
---

现在开始执行。遵守上面的格式，不要跳过 Thought。"""

    @staticmethod
    def production(tools: list[dict], preferences: dict = None) -> str:
        """生产级 prompt —— 多个示例 + 边界情况处理 + 偏好设置"""
        tool_desc = "\n".join(f"- {t['name']}: {t['description']}" for t in tools)
        tool_names = ", ".join(t["name"] for t in tools)

        prefs = ""
        if preferences:
            prefs = "\n用户偏好：\n" + "\n".join(f"- {k}: {v}" for k, v in preferences.items())

        return f"""你是一个 ReAct Agent。你必须严格遵守以下格式输出。

## 输出格式

每一步必须按以下顺序输出（不要跳过任何一步）：

Thought: <解释你为什么要做这个动作，1-2 句话>
Action: <工具名称，必须是以下之一：{tool_names}>
Action Input: <严格的 JSON 格式参数，key 用双引号>

Observation: <我会在下轮提供执行结果>

当你有了足够的信息来回答用户时：

Thought: <解释为什么可以结束了>
Final Answer: <你的最终回复>

## 禁止行为

1. 不要编造 Observation——Observation 由我提供
2. 不要使用不在工具列表中的工具
3. 不要在 Action Input 中使用单引号（必须用双引号）
4. 不要连续两次调用同一个工具（除非参数不同）
5. 如果已经尝试了 5 步还没有进展，请总结当前信息并输出 Final Answer

## 示例

示例一：工具调用

用户：比较北京和上海的人口

Thought: 用户需要比较两个城市的人口，我需要分别查询北京和上海的人口数据。先查北京。
Action: get_city_population
Action Input: {{"city": "北京"}}
Observation: 北京人口约 2189 万。

Thought: 已获取北京的数据，接下来查询上海的人口。
Action: get_city_population
Action Input: {{"city": "上海"}}
Observation: 上海人口约 2487 万。

Thought: 两个城市的数据都获取到了，可以比较并回答用户。
Final Answer: 北京人口约 2189 万，上海人口约 2487 万，上海比北京多约 298 万人。

示例二：信息不足时直接说明

用户：火星上有水吗？

Thought: 这个问题涉及天文学知识，我不需要调用任何工具，可以直接回答。但需要注意我掌握的信息可能不是最新的。
Final Answer: 根据目前的研究，火星上存在水冰形式的，主要分布在两极地区和地下。

{prefs}

现在开始执行。"""
```

Few-shot 比纯指令有效，因为 LLM 本质是模式匹配。给它看一个 JSON 格式示例，它大概率模仿。示例是最好的指令。

---

### 示例五：上下文窗口耗尽的应对策略

ReAct 每步追加 Thought + Action + Observation，上下文增长很快。下面是三种应对策略：

```python
"""
ReAct 上下文管理
"""


class ContextStrategy:
    """上下文管理策略"""

    @staticmethod
    def sliding_window(messages: list[dict], keep: int = 8, system: str = "") -> list[dict]:
        """
        策略一：滑动窗口

        只保留最近的 N 条消息。
        """
        system_msgs = [m for m in messages if m.get("role") == "system"]
        non_system = [m for m in messages if m.get("role") != "system"]

        return system_msgs + non_system[-keep:]

    @staticmethod
    def summary_compression(llm, messages: list[dict], keep: int = 5, system: str = "") -> list[dict]:
        """
        策略二：摘要压缩

        用 LLM 把早期步骤压缩成一条摘要消息。
        """
        system_msgs = [m for m in messages if m.get("role") == "system"]
        non_system = [m for m in messages if m.get("role") != "system"]

        if len(non_system) <= keep:
            return messages

        early = non_system[:-keep]
        early_text = "\n".join(f"{m['role']}: {m['content']}" for m in early)

        prompt = f"""请将以下对话历史压缩为一段摘要，只保留：
1. 用户的原始目标
2. 已完成的工具调用和关键结果
3. 当前的进度和发现
不要保留 Thought 和 Action 的细节。

{early_text}
"""
        summary = llm.generate(prompt, system="你是对话摘要专家。输出要简洁。")

        compressed = system_msgs + [
            {"role": "system", "content": f"[早期对话摘要]\n{summary}"},
        ] + non_system[-keep:]

        return compressed

    @staticmethod
    def tool_output_truncation(messages: list[dict], max_observation_len: int = 300) -> list[dict]:
        """
        策略三：工具输出截断

        从源头控制 Observation 的长度。
        """
        result = []
        for msg in messages:
            if msg.get("role") == "tool" and len(msg["content"]) > max_observation_len:
                truncated = msg["content"][:max_observation_len] + "...（已截断）"
                result.append({**msg, "content": truncated})
            else:
                result.append(msg)
        return result

    @staticmethod
    def hybrid(llm, messages: list[dict], token_threshold: float = 0.7) -> list[dict]:
        """
        混合策略：截断 + 滑动窗口 + 摘要压缩
        """
        messages = ContextStrategy.tool_output_truncation(messages, max_observation_len=500)
        messages = ContextStrategy.sliding_window(messages, keep=12)

        estimated_tokens = sum(len(m.get("content", "")) // 3 for m in messages)
        if estimated_tokens > token_threshold * 4096:
            messages = ContextStrategy.summary_compression(llm, messages, keep=5)

        return messages
```

以 20 步 ReAct 为例，不做处理直接超限制报错。滑动窗口省到约 1600 token 但丢失前 12 步。摘要压缩约 1200 token 但多一次 LLM 调用。混合策略最实用——从源头截断加事后压缩。

---

### 示例六：幻觉传播链——ReAct 中最隐蔽的 bug

某一步的幻觉会传播到后续所有步骤，因为每轮的 Observation 会追加到上下文，LLM 下一轮把它当事实：

```python
"""
幻觉传播链分析
"""


class HallucinationTracker:
    """
    幻觉传播追踪器

    标记哪些信息是工具返回的，哪些是 LLM 推断的。
    如果最终答案引用了未被工具验证的信息，发出警告。
    """

    def __init__(self):
        self.steps: list[dict] = []
        self.facts: dict[str, str] = {}
        self.unverified_claims: list[str] = []

    def record_step(self, thought: str, action: str, observation: str) -> None:
        """记录一步，区分事实和推断"""
        self.steps.append({
            "thought": thought,
            "action": action,
            "observation": observation,
        })

        if observation:
            self._extract_facts(observation, source="observation")

        if thought:
            self._extract_claims(thought)

    def _extract_facts(self, text: str, source: str) -> None:
        """从 Observation 中提取事实"""
        import re
        patterns = [
            r"(\S+)\s+(?:是|有|为|约)\s+(.+?)[\.\,，。]",
            r"(\S+):\s*(.+?)[\.\,，。]",
        ]
        for pattern in patterns:
            for match in re.finditer(pattern, text):
                key = match.group(1).strip()
                value = match.group(2).strip()
                self.facts[f"{key}: {value}"] = source

    def _extract_claims(self, thought: str) -> None:
        """从 Thought 中提取声明"""
        import re
        patterns = [
            r"(?:因此|所以|说明|意味着|看来).+?[\.\,，。]",
            r"(?:应该|可能|大概|似乎).+?[\.\,，。]",
        ]
        for pattern in patterns:
            for match in re.finditer(pattern, thought):
                self.unverified_claims.append(match.group().strip())

    def verify_final_answer(self, answer: str) -> list[str]:
        """验证最终答案——检查是否引用了未被验证的信息"""
        import re
        unverified = []

        for claim in self.unverified_claims:
            if any(word in answer for word in re.findall(r'\w{2,}', claim)):
                supported = any(
                    fact_key in claim or fact_val in claim
                    for fact_key, fact_val in (f.split(": ") for f in self.facts)
                )
                if not supported:
                    unverified.append(claim)

        return unverified

    def report(self) -> str:
        """生成追踪报告"""
        lines = [
            f"共记录 {len(self.steps)} 步",
            f"已验证事实: {len(self.facts)} 条",
            f"未验证声明: {len(self.unverified_claims)} 条",
        ]
        if self.unverified_claims:
            lines.append("\n未验证声明:")
            for claim in self.unverified_claims:
                lines.append(f"  - {claim}")
        return "\n".join(lines)


# 使用示例
tracker = HallucinationTracker()

tracker.record_step(
    thought="数据已读取，接下来分析趋势",
    action="analyze(data)",
    observation="Q4 增长 15%，总销售额 500 万",
)
tracker.record_step(
    thought="看来 Q3 表现不佳，可能是季节性因素",
    action="search('Q3 销售下降原因')",
    observation="未找到相关信息",
)
tracker.record_step(
    thought="因此 Q3 下降主要是因为市场竞争加剧",
    action="",
    observation="",
)

report = tracker.report()
print(report)
```

ReAct 中每一步的 Thought 可以包含推断，但这些推断不一定是事实。如果后续步骤没有通过工具调用验证，它们就作为伪事实传播到最终答案。我个人的做法是定期让 LLM 自我审查——哪些结论有工具数据支持，哪些只是推断。

---

## 讨论

### ReAct 真的有必要吗？

不需要用的场景：

- 工具调用是固定的、线性的。比如"先读文件再分析再画图"，不需要 LLM 每步思考接下来该做什么。直接写代码按顺序调就行
- 只有 1-2 个工具，LLM 每次都会选对（因为没别的选择）。Thought 成了废话
- 对延迟要求高，多一轮 LLM 调用多 2-5 秒

需要用的场景：

- 5 个以上工具，LLM 需要根据当前信息动态选择
- 任务步骤不固定，需要根据中间结果决定下一步
- 需要调试和审查——Thought 日志能帮你理解 Agent 的决策过程
- 任务涉及搜索或探索，Agent 不知道答案在哪

### Thought 是不是废话？能不能省？

很多人觉得 Thought 就是一堆废话，想省掉来节省 token。至少我目前的情况，不能完全省掉：

1. **Thought 是 LLM 的 Chain of Thought**。模型先生成推理过程再生成答案，准确率会显著提升
2. **Thought 是调试的生命线**。没有它，只看得到 `Action: read_csv` 到 `Observation: 成功`，但不知道 Agent 为什么选 read_csv
3. **Thought 约束了 LLM 的行为**。没有 Thought 的 prompt，LLM 更可能输出不规范格式

但 Thought 的质量差异很大：

```
# 低质量 Thought（纯复述）：
Thought: 我需要读取文件。
Action: read_csv

# 高质量 Thought（有推理）：
Thought: 用户提到"销售数据"，但没有指定格式。根据文件扩展名 .csv，
        应该用 read_csv 工具而不是 read_excel。如果文件不存在，
        需要提示用户确认路径。
Action: read_csv(path="sales.csv")
```

低质量 Thought 没有提供 Action 之外的任何信息。高质量 Thought 包含：为什么选这个工具、排除了哪些其他选项、有什么前置条件需要检查。

提升方向：在 few-shot 示例中展示高质量 Thought；在 system prompt 中要求解释为什么选这个工具而不是其他；用结构化 Thought（JSON 格式的决策理由）替代自由文本。

### ReAct 的代价

| 代价 | 说明 | 缓解方案 |
|------|------|---------|
| **LLM 调用次数暴增** | 每步一次调用。10 步 = 10 次 | Plan-Then-Act 策略减少步数 |
| **延迟高** | 每次 2-5 秒。10 步就是 20-50 秒 | 流式输出 Thought |
| **格式不稳定** | LLM 可能不按格式输出 | Few-shot + 三级解析器 |
| **循环失控** | 反复调同一个工具找不到答案 | max_steps + 进度检查 prompt |
| **上下文膨胀** | 每步都追加，很快触 token 限制 | 滑动窗口 / 摘要压缩 / 截断 |
| **幻觉传播** | 未验证的推断传播到后续步骤 | HallucinationTracker + 定期自查 |
| **工具选择偏差** | LLM 倾向关键词匹配而非功能匹配 | 工具描述包含"不适合"场景 |

### 最容易被滥用的场景

**每步 Thought 都是废话：**

```
Thought: 我需要读取文件。
Action: read_csv
Observation: 成功

Thought: 文件读完了，我需要分析。
Action: analyze
Observation: 分析完成

Thought: 分析完了，我输出结果。
Action: output
Observation: 输出完成
```

这种没有任何推理价值，说明任务不需要 ReAct，直接写代码按顺序调用就行。

**循环 20 步还没结果：** max_steps 设为 10 跑不完，改成 20 还是没完。Agent 迷失了，在随机试工具。

解决：系统 prompt 加进度检查——"尝试了 5 步没有进展就总结当前信息并输出 Final Answer"。

**Thought 和 Action 脱节：**

```
Thought: 我需要分析销售数据的季度趋势
Action: read_image("photo.jpg")
```

Thought 说分析数据，Action 读图片。解决：系统 prompt 里加工具使用规则，要求 Thought 和 Action 逻辑一致。

### 更简单的替代方案

| 场景 | 替代方案 | 说明 |
|------|---------|------|
| 固定步骤 | **写死代码** | `data = read_csv(); result = analyze(data)` |
| OpenAI 生态 | **Function Calling** | SDK 自动处理 Action/Observation 循环 |
| 需要高质量输出 | **Few-shot 示例** | prompt 里给 2-3 个正确示例，LLM 模仿格式 |
| 需要规划 | **先输计划再执行** | 比 ReAct 少一半 LLM 调用 |

```python
# 最简单的替代方案：不需要 ReAct 循环
# 如果任务步骤是固定的，直接写代码

def analyze_sales(file_path: str) -> str:
    data = read_csv(file_path)
    cleaned = clean_data(data)
    summary = analyze(cleaned)
    chart = plot_bar_chart(summary)
    return generate_report(summary, chart)

# 只有需要"动态决策"的环节才交给 LLM
chart_type = llm.decide("根据数据特征，应该用柱状图还是折线图？")
```

---

## 和其他模式的关系

ReAct 是边想边做，Plan-Execute 是先想好再做。ReAct 更灵活但不稳定，Plan-Execute 更稳定但不够灵活。

Reflexion = ReAct + 自我评估。ReAct 只管执行，Reflexion 执行完还要判断好不好。

ReAct 的循环也可以用状态模式实现：ThoughtState 到 ActionState 到 ObservationState 循环。多工具串联时，每个工具是责任链节点，ReAct 决定走哪条链。

---

## 框架中的实际应用

| 框架 | ReAct 实现 | 说明 |
|------|-----------|------|
| **LangChain** | `create_react_agent()` | 最经典的实现，内置 output parser 和 system prompt |
| **LlamaIndex** | `ReActAgent` | 与检索增强结合，工具中包含向量检索 |
| **OpenAI** | Function Calling + 循环 | 本质一样：模型输出函数调用到执行到返回结果 |
| **Anthropic** | Tool Use + 循环 | Claude 的原生工具调用，同样遵循 Thought 到 Action 到 Observation |

---

## 演进路线

ReAct 不是起点，是逐步演进出来的。

最初可能只是直接调工具，步骤固定不需要 LLM 决策：

```python
data = read_csv("sales.csv")
result = analyze(data)
```

后来发现需要 LLM 决定用什么工具，但每步独立调用：

```python
tool_name = llm.choose_tool(user_input)
result = tools.execute(tool_name)
```

再后来发现需要多步串联，让 LLM 一次性输出所有步骤（Plan-Execute）：

```python
plan = llm.generate_plan(user_input)
for tool in plan:
    result = tools.execute(tool, result)
```

最后发现中间结果会影响后续决策，这才上了 ReAct：

```python
for _ in range(max_steps):
    thought, action = llm.think_and_act(context)
    observation = tools.execute(action)
    context += f"\nThought: {thought}\nObservation: {observation}"
```

先写最简单的代码，发现需要多步决策、中间结果影响后续，再引入 ReAct。

---

## 使用判断

**该用（至少命中两条）：**

- 3 个以上工具，LLM 需要动态选择
- 任务步骤不固定，依赖中间结果
- 需要调试日志追踪每步决策理由
- 任务涉及探索或搜索，无法预先规划全部步骤

**不该用（命中任意一条就不需要）：**

- 工具调用固定、线性
- 只有 1-2 个工具
- 对延迟要求高（小于 5 秒）
- 任务可以预先规划全部步骤

---

## 写在最后

ReAct 是 Agent 架构的核心执行模式，但不是万能的。很多场景下固定工具调用或预先规划反而更高效稳定。ReAct 的真正价值在于处理不确定性——当 Agent 不知道该做什么、需要根据中间结果动态决策时，Thought 到 Action 到 Observation 循环才是最合适的选择。
