AI Agent 设计模式:ReAct 深度解析

一、ReAct 的定义与起源

Reasoning + Acting,2022 年 10 月由普林斯顿大学与 Google 联合提出(arXiv: 2210.03629)。

核心思想一句话:强迫大模型交替进行推理与行动,不允许一步给出最终答案。

为什么需要 ReAct?

在 ReAct 之前有两条路线,各有缺陷:

路线 做法 缺陷
纯推理(Chain of Thought) “一步步想,然后给答案” 只能靠模型参数里的知识,遇到训练截止后的信息、需要实时查询的问题 → 产生幻觉
纯行动(Action-only) “直接干,不行再说” 没有事前规划,方向偏差后无法纠偏

ReAct 的破局点:推理为行动提供方向,行动结果修正推理 — “交错才能纠错”


二、核心机制:三步闭环循环

         ┌──────────────────────────┐
         │        Thought            │
         │   "当前已知什么?          │
         │    下一步该做什么?"        │
         └───────────┬───────────────┘
                     │ 决定调用哪个工具
                     ▼
         ┌──────────────────────────┐
         │        Action             │
         │   调用工具,传入参数        │
         │   如 search_album("西游记") │
         └───────────┬───────────────┘
                     │ 工具在真实环境中执行
                     ▼
         ┌──────────────────────────┐
         │      Observation          │
         │   工具返回的真实结果        │
         │   "找到 358 个专辑..."     │
         └───────────┬───────────────┘
                     │ 结果喂回大模型
                     ▼
              (下一轮 Thought)
                     │
                     ▼
              最终无法/不需要调工具时
                     │
                     ▼
         ┌──────────────────────────┐
         │     Final Answer          │
         │    "已经在播放第1集了~"    │
         └──────────────────────────┘

四要素

标签 谁产生 本质
Thought 大模型推理 “我该干什么?先分析现状”
Action 大模型决策 “调用工具X,参数Y”
Observation 真实环境 不是模型生成的,是工具实实在在跑出来的
Final Answer 大模型总结 基于所有 Observation 的综合回答

三、不靠训练,靠系统提示词

ReAct 不是通过微调模型实现的,而是靠一段精心设计的 System Prompt(系统提示词)

系统提示词模板

你是一个智能助手。你必须严格按照以下格式回复:

可用工具:
- search_album: 搜索有声书专辑
  参数: keyword (字符串), page (整数, 默认1)
- get_album_detail: 获取专辑详情和集数列表
  参数: album_id (字符串), keyword (字符串, 可选)
- search_and_play: 搜索并播放有声书
  参数: keyword (字符串), section (整数, 默认1)

回复格式(严格遵守!):

Thought: <你的思考过程>
Action: <工具名>
Action Input: <JSON格式参数>
Observation: <工具返回结果>

...(可多轮循环)...

Final Answer: <最终回复用户的内容>

规则:
1. 每步只调一个工具
2. Action 必须来自可用工具列表
3. Action Input 必须是合法 JSON
4. 有足够信息回答时就输出 Final Answer
5. 绝对不要编造 Observation,它由系统填入

大模型读到这段 Prompt 后,就会按格式输出。框架代码负责

  1. 解析模型输出的 ActionAction Input
  2. 真正执行工具
  3. 把结果作为 Observation 追加回对话
  4. 判断是继续循环还是输出 Final Answer

四、最小可运行实现

4.1 Python 实现(70 行)

import json
import re

class ReactAgent:
    """ReAct 引擎:Thought → Action → Observation 循环"""

    def __init__(self, tools: dict, system_prompt: str):
        self.tools = tools            # {"工具名": 函数}
        self.system_prompt = system_prompt
        self.max_rounds = 10          # 防止死循环

    def run(self, user_task: str) -> str:
        """
        核心循环:
        1. 构建 messages(系统提示词 + 用户任务)
        2. 调大模型 → 解析输出
        3. 如果是 Final Answer → 返回
        4. 如果是 Action → 执行工具 → Observation → 回到第 2 步
        """
        messages = [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": user_task},
        ]

        for _ in range(self.max_rounds):
            # ====== Thought 阶段:调大模型 ======
            response = self._call_llm(messages)
            text = response.strip()

            # ====== 判断终止 ======
            if "Final Answer:" in text:
                print(f"[ReAct] ✅ 循环结束")
                return self._extract_tag(text, "Final Answer")

            # ====== 解析 Action ======
            action_name = self._extract_tag(text, "Action")
            action_input_str = self._extract_tag(text, "Action Input")

            if not action_name or action_name not in self.tools:
                # 模型输出格式不对,提示重试
                messages.append({"role": "assistant", "content": text})
                messages.append({
                    "role": "user",
                    "content": "Observation: 格式错误!请检查 Action 名称和 Action Input"
                })
                continue

            print(f"[ReAct] 🔧 执行工具: {action_name}({action_input_str})")

            # ====== Action 阶段:真正执行工具 ======
            try:
                action_input = json.loads(action_input_str)
                tool_result = self.tools[action_name](**action_input)
            except Exception as e:
                tool_result = f"工具执行失败: {str(e)}"

            # ====== Observation 阶段:结果写回对话 ======
            observation = f"Observation: {tool_result}"
            print(f"[ReAct] 👁 Observation: {tool_result[:100]}...")

            messages.append({"role": "assistant", "content": text})
            messages.append({"role": "user", "content": observation})
            # → 回到循环顶部,大模型看到 Observation 后继续 Thought

        return "超过最大循环次数,任务未完成"

    def _call_llm(self, messages: list) -> str:
        """调大模型(这里用伪代码代替,实际调 OpenAI/DeepSeek API)"""
        # 实际代码:
        # import openai
        # response = openai.ChatCompletion.create(
        #     model="gpt-4o", messages=messages, temperature=0
        # )
        # return response.choices[0].message.content
        #
        # 这里用简化模拟:
        last_user_msg = messages[-1]["content"] if messages else ""
        if "Final Answer" in last_user_msg:
            return last_user_msg  # 模拟:用户提示中已含答案就停止
        # 实际场景中这里会真正调用 LLM API
        raise NotImplementedError("替换为真实的 LLM API 调用")

    def _extract_tag(self, text: str, tag: str) -> str:
        """从模型输出中提取标签内容"""
        pattern = rf"{tag}:\s*(.*?)(?:\n|$)"
        match = re.search(pattern, text, re.DOTALL)
        return match.group(1).strip() if match else ""

4.2 接入懒人听书工具

# ====== 懒人听书工具(从 xiaozhi Player 中简化) ======

# 模拟工具(实际中这些是真实的 API 调用)
def search_album(keyword: str, page: int = 1) -> str:
    """搜索有声书专辑"""
    # 实际调用: 52api.cn API
    result = {
        "西游记": "找到3个专辑: 1.米小圈快乐西游记(主播:米小圈) 2.西游记|甄齐播讲(主播:甄齐) 3.西游记少儿版(主播:阅耳亲子故事)",
        "三体": "找到5个专辑: 1.三体|广播剧(主播:729声工场) 2.三体全集(主播:青雪) ...",
    }
    return result.get(keyword, f"未找到与'{keyword}'相关的有声书")

def get_album_detail(album_id: str, keyword: str = "") -> str:
    """获取专辑详情,返回集数列表"""
    # 实际调用: 52api.cn API type=detail
    return f"专辑ID:{album_id}, 共100集: 第1集:猴王出世, 第2集:拜师学艺, ..."

def search_and_play(keyword: str, section: int = 1) -> str:
    """搜索并播放(优先本地缓存)"""
    # 实际调用: Player.search_and_play()
    # 1. 查本地缓存
    # 2. 缓存命中 → 直接播放
    # 3. 缓存未命中 → 后台下载 + 立即返回
    return f"正在播放(本地): {keyword} - 第{section}集"


# ====== 注册工具 ======
tools = {
    "search_album": search_album,
    "get_album_detail": get_album_detail,
    "search_and_play": search_and_play,
}

# ====== 系统提示词 ======
SYSTEM_PROMPT = """
你是一个懒人听书助手。你必须按以下格式回复:

可用工具:
- search_album: 搜索有声书,参数 {"keyword": "西游记", "page": 1}
- get_album_detail: 查看专辑详情,参数 {"album_id": "123", "keyword": ""}
- search_and_play: 搜索并播放,参数 {"keyword": "西游记", "section": 1}

回复格式:
Thought: <思考过程>
Action: <工具名>
Action Input: <JSON参数>
...(可循环多轮)...
Final Answer: <最终回答>
"""

# ====== 启动 Agent ======
agent = ReactAgent(tools=tools, system_prompt=SYSTEM_PROMPT)

# 用户任务
result = agent.run("帮我播放西游记第1集")
print(f"\n用户看到: {result}")

4.3 一次完整的执行轨迹

用户: "帮我播放西游记第1集"

━━━ 第 1 轮 ━━━
大模型输出:
  Thought: 用户想播放西游记第1集。search_and_play 可以一步完成搜索和播放,直接用这个。
  Action: search_and_play
  Action Input: {"keyword": "西游记", "section": 1}

框架执行:
  tools["search_and_play"](keyword="西游记", section=1)
  → 查本地缓存 / 调API / 后台下载
  → 返回: "正在播放(本地): 西游记 - 第1集"

框架追加到 messages:
  Observation: 正在播放(本地): 西游记 - 第1集

━━━ 第 2 轮 ━━━
大模型收到 Observation 后输出:
  Thought: 播放成功了,任务完成。
  Final Answer: 已经在播放西游记第1集了~

框架判断: "Final Answer:" 存在 → 循环结束 → 返回给用户

五、ReAct 循环在xiaozhi 项目中的实际对应

5.1 架构映射

┌──────────────────────────────────────────────────────────────┐
│                     ReAct 引擎(xiaozhi)                      │
│                                                              │
│   远程大模型                    MCP Server                     │
│   (DeepSeek/混元)                (localhost)                  │
│  ┌────────────┐           ┌──────────────────────────┐       │
│  │ Thought    │           │                          │       │
│  │   "搜一下  │           │                          │       │
│  │    西游记"  │           │                          │       │
│  │   ↓        │           │                          │       │
│  │ Action     │ ────────→ │ tools/call               │       │
│  │ "lanrentingshu         │   ↓                      │       │
│  │  .search_album"        │ Manager.wrapper()        │       │
│  │   ↓        │           │   ↓                      │       │
│  │ Action     │           │ Player.search_album()    │       │
│  │ Input      │           │   ↓                      │       │
│  │ {keyword:  │           │ HTTP POST → 52api.cn     │       │
│  │  "西游记"}  │           │   ↓                      │       │
│  │            │           │ 返回 JSON → 格式化文本    │       │
│  │            │           │   ↓                      │       │
│  │Observation│ ←──────── │ "找到 358 个有声书..."    │       │
│  │   ↓        │           │                          │       │
│  │ Thought    │           │                          │       │
│  │   "选第一个 │           │                          │       │
│  │    播放"    │           │                          │       │
│  │   ↓        │           │                          │       │
│  │ Action     │ ────────→ │ tools/call               │       │
│  │ "lanrentingshu         │   ↓                      │       │
│  │  .search_and_play"     │ 搜索→下载→FFmpeg解码     │       │
│  │            │           │   ↓                      │       │
│  │Observation│ ←──────── │ "正在播放..."            │       │
│  │   ↓        │           │                          │       │
│  │ Final      │           │                          │       │
│  │ Answer     │           │                          │       │
│  └────────────┘           └──────────────────────────┘       │
└──────────────────────────────────────────────────────────────┘

5.2 懒人听书 7 个 Tool 在 ReAct 中的角色

MCP Tool ReAct 标签 触发场景
search_album Action “帮我搜一下西游记”
get_album_detail Action “看看西游记有多少集”
search_and_play Action “播放西游记第1集”
pause Action “暂停”
resume Action “继续”
stop Action “别放了”
get_status Action “现在在播什么?”

每个 Tool 的返回值 = Observation,直接喂回大模型作为下一轮 Thought 的输入。

5.3 后台异步任务:避免阻塞 ReAct 循环

你的 search_and_play 有个关键优化:

async def search_and_play(self, keyword: str, stream_section: int = 1) -> dict:
    # 第一步:查本地缓存 — 快路径
    local_file = await self._find_local_episode(keyword, stream_section)
    if local_file:
        await self._play_local_file(local_file, ...)
        return {"status": "success", "message": f"正在播放(本地): {keyword} - 第{stream_section}集"}
        # ← Observation 瞬间返回,ReAct 循环不卡

    # 第二步:后台下载 — 慢路径
    asyncio.create_task(self._background_search_and_play(keyword, stream_section))
    return {"status": "success", "message": "正在搜索下载中,请稍等一下~", "background": True}
    # ← Observation 也瞬间返回!

如果不用后台模式

ReAct 循环:
  Action: search_and_play("西游记", 1)
    ↓
  ... 等待 30 秒下载 ...  ← 整个循环卡死!
    ↓
  Observation: (30秒后才回来)
    ↓
  AI 请求超时 ❌ 用户看不到回复

使用后台模式

ReAct 循环:
  Action: search_and_play("西游记", 1)
    ↓ (10ms)
  Observation: "正在下载中,请稍等一下~"  ← 瞬间返回
    ↓
  Thought: "下载中,告知用户等待"
  Final Answer: "正在帮你下载西游记第1集,稍等一下哦~"

  ... 30 秒后,后台下载完成,自动播放 ...

六、ReAct 的关键工程价值

6.1 白盒化(可解释性)

每一轮 Thought-Action-Observation 都保留在对话历史中:

messages = [
    {system: "你是懒人听书助手..."},
    {user: "播放西游记第1集"},
    {assistant: "Thought: 需要搜索和播放... Action: search_and_play..."},
    {user: "Observation: 正在播放(本地): 西游记 - 第1集"},
    {assistant: "Thought: 成功了... Final Answer: 已经在播放了~"},
]

开发者可以回溯每一步

  • 为什么调了这个工具?→ 看 Thought
  • 工具返回了什么?→ 看 Observation
  • 为什么决定结束?→ 看最后一轮的 Thought

6.2 可干预性

人类可以在任意 Thought 节点插入修正,不需要重跑整个任务:

原来的 Observation: "找到 358 个专辑"
人类插入: "Observation: 前5个结果不对,请只看第6到第10个"
→ 大模型收到新 Observation → 调整策略 → 继续

6.3 防止幻觉

CoT(纯推理)的问题:

用户: "今天北京天气怎么样?"
CoT: "北京今天晴天,25度" ← 幻觉!模型不知道今天天气

ReAct:

用户: "今天北京天气怎么样?"
Thought: 我不知道今天天气,需要查
Action: get_weather("北京")
Observation: "2026-05-18 北京: 多云, 22°C"  ← 真实数据
Final Answer: "北京今天多云,22°C"  ← 基于真实 Observation

七、ReAct vs Plan-And-Execute 对比

维度 ReAct Plan-And-Execute
核心循环 Thought → Action → Observation 先 Plan → Execute → Replan
模块数 1 个大模型 多个(Plan模型 + Replan模型 + 执行Agent)
规划方式 边走边看,每步即时决策 先画完整路线图,再一步步走
调整方式 每轮 Observation 后自然调整 执行完一步后 Replan 模型重新规划
适用场景 探索性任务、状态不确定 多步骤、子目标明确的任务
实现复杂度 低(一个 while 循环) 高(“Agent 套 Agent”)
懒人听书适用 ✅ 搜索→选专辑→播放,每步依赖前一步结果 ❌ 过度设计

八、总结

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   大模型 = 大脑(只能推理)                                    │
│      +                                                      │
│   工具   = 感官和四肢(读写文件、调API、执行命令)               │
│      =                                                      │
│   Agent = 能感知和改变世界的智能程序                           │
│                                                             │
│   ReAct = Agent 的调度引擎                                   │
│           Thought → Action → Observation 三步循环             │
│           推理为行动指方向,行动结果为推理纠错                   │
│                                                             │
│   实现 = System Prompt(格式约束)+ while 循环(引擎)          │
│         不靠微调,靠 Prompt Engineering                       │
│                                                             │
│   价值 = 白盒化(每步可回溯)+ 防幻觉(Observation是真实数据)   │
│           + 可干预(人类可中途插入修正)                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

自 2022 年论文至今,ReAct 的核心结构始终是 Thought-Action-Observation 三步闭环循环。大模型从"回答问题"升级为"完成真实世界任务",靠的就是这套机制。
在这里插入图片描述

然后至于它都能执行哪些操作,是由工具来决定的,而工具又怎么来的呢?可以继续看看MCP相关的!

在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐