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 动手改
- 给
run_react加一个「同一个工具连续调两次就拒绝」的规则,看模型怎么反应 - 把提示词里的工具列表加一个
get_time(),问「北京几点,该穿什么」,看它会不会两个都调 - 把
REACT_PROMPT改写成 function calling 风格(参考第 2 章),对比代码量和稳定性
4.8 小结
- ReAct = Reasoning + Acting,交替进行,2022 年提出至今仍是 Agent 的底座
- 三个关键词:Thought(推理)、Action(行动)、Observation(观察)
- 好处:接外部知识、少幻觉、可解释、能自我纠错;代价:上下文涨、错误传播
- 经典实现是纯文本 + 正则解析;现代写法用 function calling 更稳
- 无论哪种实现,骨架都一样:思考 → 行动 → 观察 → 写回上下文 → 再思考
下一章:用 LangGraph 构建 Agent,把这套循环交给框架编排 🐸