🐸 青蛙编程网
首页 / AI / 智能体 Agent / 4. ReAct 模式

4. ReAct 模式:从理论到实现

这是什么:ReAct = Reasoning + Acting(推理 + 行动),让大模型交替地思考和行动,2022 年提出,至今仍是几乎所有 Agent 框架的底座。

为什么用:第 3 章我们手写了一个「思考 → 行动 → 观察」的循环——ReAct 就是这个循环的正式名字和完整形态。知道它的出处和标准写法,才知道自己在写的东西是什么。

如何用:本章先摆出 ReAct 的三个关键词,再用经典文本格式手写一个 ReAct Agent,最后对比它和 function calling 的差别。

4.1 ReAct 是什么

ReAct = Reasoning + Acting(推理 + 行动),出自论文 ReAct: Synergizing Reasoning and Acting in Language Models(Yao 等人,2022,arXiv:2210.03629)。

它解决的问题是:大模型要么光说不做(纯推理,容易空想),要么光做不想(纯行动,容易乱来)。ReAct 让两者交替进行:

┌──────────────────────────────────────────────┐
│  Thought:我需要知道北京的天气才能给建议        │  ← 推理
│  Action:get_weather                          │  ← 行动
│  Action Input:{"city": "北京"}                │
│  Observation:北京:晴,26℃                    │  ← 观察
│  Thought:现在我知道该建议穿什么了              │  ← 回到推理
│  Action:get_tip                              │
│  Action Input:{"weather": "晴,26℃"}          │
│  Observation:穿薄外套就行                    │
│  Thought:信息齐了,可以回答了                 │
│  Final Answer:北京现在晴,26℃,穿薄外套就行。    │
└──────────────────────────────────────────────┘

三个关键词:

关键词 作用
Thought 模型的推理过程,写出来让模型自己想清楚,也让你看得见
Action 决定调哪个工具、传什么参数
Observation 工具的真实返回结果,回填进上下文

4.2 为什么它比「直接让模型回答」强

好处 说明
接上外部知识 模型不知道的事,工具能查(天气、数据库、网页)
减少幻觉 有 Observation 撑着,答案有据可依,不是凭空编
可解释 Thought 写出来,出错时你知道它哪一步想歪了
自我纠错 拿到 Observation 后能修正之前的判断,比如「原来下雨了,那要带伞」

代价也很实在:上下文涨得快、错误会沿着链条传播、模型可能绕圈打转。

4.3 经典 ReAct 的文本格式

原始论文的 ReAct 不靠 function calling,而是让模型输出纯文本,再用正则解析:

import json
import os
import re
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["LLM_API_KEY"],
    base_url=os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1"),
)
CHAT_MODEL = os.environ.get("CHAT_MODEL", "gpt-4o-mini")

REACT_PROMPT = """你可以使用这些工具:

{tools}

请严格按下面的格式回答:

Thought: 你对当前情况的思考
Action: 要调用的工具名
Action Input: 调用工具的 JSON 参数
Observation: 工具返回的结果
(Thought / Action / Action Input / Observation 可以重复多轮)
Final Answer: 给用户的最终答案

用户问题:{question}
"""


def get_weather(city: str) -> str:
    fake = {"北京": "晴,26℃", "上海": "多云,29℃"}
    return fake.get(city, f"没查到 {city} 的天气")


def get_tip(weather: str) -> str:
    if "雨" in weather:
        return "记得带伞"
    return "穿薄外套就行"


TOOL_MAP = {"get_weather": get_weather, "get_tip": get_tip}


def parse_action(text: str):
    """从模型输出里解析出工具名和参数"""
    action = re.search(r"Action:\s*(\w+)", text)
    if not action:
        return None, None
    name = action.group(1)
    raw = re.search(r"Action Input:\s*(\{.*?\})", text, re.S)
    if not raw:
        return name, {}
    try:
        return name, json.loads(raw.group(1))
    except json.JSONDecodeError:
        return name, {}


def run_react(question: str, max_steps=5):
    tools_desc = (
        "get_weather(city) —— 查询指定城市的当前天气\n"
        "get_tip(weather) —— 根据天气描述给出穿衣建议"
    )
    history = REACT_PROMPT.format(tools=tools_desc, question=question)

    for step in range(max_steps):
        resp = client.chat.completions.create(
            model=CHAT_MODEL,
            messages=[{"role": "user", "content": history}],
        )
        text = resp.choices[0].message.content
        print(f"--- 第 {step + 1} 轮 ---\n{text}\n")

        if "Final Answer:" in text:
            return text.split("Final Answer:")[-1].strip()

        name, args = parse_action(text)
        if not name:
            return text.strip()

        try:
            observation = TOOL_MAP[name](**args)
        except Exception as e:
            observation = f"工具执行出错:{e}"

        # 把这一轮的思考和工具结果都追加进历史
        history += f"\n{text}\nObservation: {observation}\n"

    return f"超过 {max_steps} 步仍未给出最终答案。"

运行:

python react_agent.py

你会看到(示意):

--- 第 1 轮 ---
Thought: 我需要先查北京的天气。
Action: get_weather
Action Input: {"city": "北京"}
--- 第 2 轮 ---
Thought: 已知是晴天,可以直接给建议。
Action: get_tip
Action Input: {"weather": "晴,26℃"}
--- 第 3 轮 ---
Thought: 信息齐了。
Final Answer: 北京现在晴,26℃,穿薄外套就行。

4.4 现代等价物:function calling

上面这种「让模型输出文本、再用正则解析」的做法现在有点旧了。第 2 章学的 function calling 其实是 ReAct 的工程化版本:

经典 ReAct(文本) function calling(现代)
模型怎么表达「我要调工具」 输出 Action: xxx 文本 输出结构化的 tool_calls 字段
怎么解析 正则匹配,格式不对就崩 SDK 给你结构化对象,不用解析
参数怎么传 模型写 JSON 字符串,你 json.loads SDK 直接给你 dict
怎么终止 匹配 Final Answer: 看 msg.tool_calls 是不是 None

所以真正写生产代码,用 function calling 更好。 理解 ReAct 的价值在于:知道循环的骨架长什么样、每一步该往上下文里写什么——这两件事用哪种实现都一样。

4.5 两种变体

变体 做法 适合
ReAct 提示词 在提示词里给几个「Thought/Action/Observation」示例,让模型模仿 快速上手、模型本来就聪明
ReAct 微调 用「思考 + 行动 + 观察」的完整轨迹当训练数据微调模型 有标注数据、要极致效果(见 微调栈)

4.6 常见翻车现场

报错 / 现象 原因 修法
parse_action 返回空 模型没按格式输出 提示词里强调「严格按格式」,并给示例
json.loads 报错 模型写的 JSON 带解释文字 解析时只取 {} 之间那段,并 try/except
调了不存在的工具 模型幻觉出工具名 解析后校验名字在白名单里,不在就回一句「没有这个工具」
上下文越来越长 每轮都把全量历史重发 压缩历史(第 9 章)
反复调同一个工具 Observation 没告诉它「查过了」 在 Observation 里写清已查到什么
绕圈停不下来 没有步数上限 一定要设 max_steps

4.7 动手改

  1. 给 run_react 加一个「同一个工具连续调两次就拒绝」的规则,看模型怎么反应
  2. 把提示词里的工具列表加一个 get_time(),问「北京几点,该穿什么」,看它会不会两个都调
  3. 把 REACT_PROMPT 改写成 function calling 风格(参考第 2 章),对比代码量和稳定性

4.8 小结

  • ReAct = Reasoning + Acting,交替进行,2022 年提出至今仍是 Agent 的底座
  • 三个关键词:Thought(推理)、Action(行动)、Observation(观察)
  • 好处:接外部知识、少幻觉、可解释、能自我纠错;代价:上下文涨、错误传播
  • 经典实现是纯文本 + 正则解析;现代写法用 function calling 更稳
  • 无论哪种实现,骨架都一样:思考 → 行动 → 观察 → 写回上下文 → 再思考

下一章:用 LangGraph 构建 Agent,把这套循环交给框架编排 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。