写这篇东西的起因是上个月在群里和人吵了一架。吵的是 Agent 到底应该"边想边做"还是"先想后做"。两边都有实际案例撑腰,谁也说服不了谁。
后来我想通了——这根本就不是谁对谁错的问题。ReAct 和 Plan-and-Solve 这俩模式,本来就是应对不同场景的。但好多文章把这事儿讲得太学术了,动不动就搬论文。我试着换个写法,从代码入手,把这两个东西掰开揉碎聊清楚。
先说人话:侦探和建筑师
先别管论文怎么写,咱们用直觉感受一下。
ReAct 像侦探破案。
侦探走进案发现场:
地上有脚印 → 量一下尺寸(行动)→ 28cm,男性鞋码(观察)→ 门口有泥渍,和鞋印颜色一致 → 取样分析 → 泥土含红色粘土,城东工地才有……
看到了吗?每一步行动都依赖上一步的发现。他不可能提前写一份"破案计划书"按部就班执行——因为现场会冒出什么线索,只有到了才知道。
Plan-and-Solve 像建筑师盖楼。
建筑师接了个活,第一件事不是搬砖,是画图纸。地基怎么打、框架怎么搭、水电怎么走,全写在蓝图里。然后施工队进场,按图施工。
他不会一边砌墙一边想"哎要不这里改个游泳池"——蓝图定了就是定了。
所以这俩模式的核心区别就八个字:
ReAct 是走一步看一步,Plan-and-Solve 是先看完整张地图再动脚。
ReAct 到底是个什么东西
论文那点事
2022 年 Princeton 的 Yao Shunyu 他们发了篇论文,核心观点其实特别简单:
推理(Reasoning)和行动(Acting)不应该分开。
在那之前,主流做法分两派:
- Chain-of-Thought:只让模型推理,不接触外部世界。好处是逻辑连贯,坏处是容易一本正经地胡说八道——因为它没法查资料。
- Act-only:只让模型调工具,不要求它解释为什么调。效率高,但策略很破碎,每次调用都是孤立的。
ReAct 把这两个拧在一起,形成了一个循环:
思考(Thought)→ 行动(Action)→ 观察(Observation)→ 思考 → 行动 → 观察 → …… → 最终答案
每一步的"观察"结果,都是下一步"思考"的输入。这就形成了一个闭环反馈。
手写一个 ReAct Agent
说再多不如跑段代码。下面是个最精简的实现,只用了 OpenAI 的 SDK,没套任何框架。
import json
from openai import OpenAI
client = OpenAI()
# 先定义两个工具
def calculator(expression: str) –> str:
"""执行数学计算"""
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"计算错误: {e}"
def get_weather(city: str) –> str:
"""获取天气信息(模拟数据)"""
weather_data = {
"北京": "晴天,25°C",
"上海": "多云,28°C",
"深圳": "雷阵雨,30°C",
"东京": "小雨,22°C",
}
return weather_data.get(city, f"没找到 {city} 的天气数据")
TOOLS = {
"calculator": {"func": calculator, "desc": "数学计算,传表达式如 '25 * 17'"},
"get_weather": {"func": get_weather, "desc": "天气查询,传城市名"},
}
# System Prompt 规定了对话格式
SYSTEM_PROMPT = """你是一个能调用工具的助手。按这个格式回复:
Thought: 分析当前情况
Action: 工具名,必须是 [{tool_names}] 中的一个
Action Input: 工具的输入参数
拿到 Observation 后继续循环,直到可以给出 Final Answer。"""
def react_agent(query: str, max_iterations: int = 10):
messages = [
{"role": "system", "content": SYSTEM_PROMPT.format(
tool_names=", ".join(TOOLS.keys())
)},
{"role": "user", "content": query},
]
for step in range(max_iterations):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0,
)
llm_output = response.choices[0].message.content
# 如果 LLM 给出了最终答案,直接返回
if "Final Answer:" in llm_output:
return llm_output.split("Final Answer:")[–1].strip()
# 否则解析 Action 字段,调用对应工具
try:
action_line = llm_output.split("Action:")[1].split("Action Input:")[0].strip()
action_input = llm_output.split("Action Input:")[1].strip()
tool = TOOLS.get(action_line)
if tool is None:
observation = f"不认识这个工具: {action_line}"
else:
observation = tool["func"](action_input)
except (IndexError, ValueError):
break
# 把本轮推理结果和观察结果追加到对话里
messages.append({"role": "assistant", "content": llm_output})
messages.append({
"role": "user",
"content": f"Observation: {observation}"
})
return "超时了,没跑完。"
if __name__ == "__main__":
result = react_agent("算一下 25 * 17,然后告诉我这个数是不是质数")
print(result)
这段代码跑起来的流程是这样的:
第一轮:
LLM 想:先算 25*17
调 calculator("25 * 17") → 拿到 425
第二轮:
LLM 想:425 的个位数是 5,可能被 5 整除
调 calculator("425 / 5") → 拿到 85.0
第三轮:
LLM 得出结论:425 不是质数,能被 5 整除
输出 Final Answer
注意看这个过程——LLM 不是在"执行预设的步骤",而是在每一步根据上一步的结果做决策。如果 calculator 返回的是个错误,它可以选择换个方式重试。这种动态决策能力是 ReAct 最大的本钱。
但代价也很明显:
- 每一步都是一次 API 调用,token 消耗不小
- 如果 prompt 写得不好,模型可能原地打转,怎么都出不了 Final Answer
- 上下文越来越长,到后面 LLM 可能"忘"了最初的目的是什么
所以实际用 ReAct,一定要设 max_iterations 兜底,不然它能在循环里跑一宿。
Plan-and-Solve 又是什么
Plan-and-Solve(有些文章叫 Plan-and-Execute,也有人口误叫 Play and Resolve,说的都是一个东西)是 2023 年微软研究院的 Lei Wang 他们提出来的。
它的动机很直白:
ReAct 那种"边想边做",在小任务上没问题。但任务一复杂,模型很容易"只见树木不见森林"——花了半天调工具,结果忘了最初要干啥。
Plan-and-Solve 的做法是:先把脑子拎出水面,看清楚全貌,再下水。
也手写一个
同样是"算 25*17 并判断质数",Plan-and-Solve 的写法是这样:
from openai import OpenAI
client = OpenAI()
# 工具复用上面的 calculator
def calculator(expression: str) –> str:
try:
result = eval(expression, {"__builtins__": {}}, {})
return str(result)
except Exception as e:
return f"计算错误: {e}"
TOOLS = {
"calculator": {"func": calculator, "desc": "数学计算"},
}
# ——– Planner ——–
PLANNER_PROMPT = """你是任务规划师。
用户的问题是: {question}
请把这个问题拆成 2~5 个有序的子步骤,每行一个:
Step 1: <子任务>
Step 2: <子任务>
…"""
def plan(question: str) –> list[str]:
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": PLANNER_PROMPT.format(question=question)}],
temperature=0,
)
steps = []
for line in resp.choices[0].message.content.strip().split("\\n"):
line = line.strip()
if line.startswith("Step"):
steps.append(line.split(":", 1)[1].strip())
return steps
# ——– Executor ——–
EXECUTOR_PROMPT = """全局任务: {global_task}
当前子任务: {current_step}
上一步结果: {previous_result}
可用工具: {tool_names}
完成这个子任务。需要工具就写:
Action: 工具名
Action Input: 参数
Observation: 结果
不需要工具直接写:
Result: <结果>"""
def execute_step(step: str, global_task: str, previous_result: str) –> str:
messages = [{"role": "user", "content": EXECUTOR_PROMPT.format(
global_task=global_task,
current_step=step,
previous_result=previous_result,
tool_names=", ".join(TOOLS.keys()),
)}]
for _ in range(5):
resp = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, temperature=0,
)
output = resp.choices[0].message.content
if "Result:" in output:
return output.split("Result:")[–1].strip()
# 工具调用
try:
action = output.split("Action:")[1].split("Action Input:")[0].strip()
action_input = output.split("Action Input:")[1].split("Observation:")[0].strip()
obs = TOOLS[action]["func"](action_input)
messages.append({"role": "assistant", "content": output})
messages.append({"role": "user", "content": f"Observation: {obs}"})
except (IndexError, KeyError):
return output
return "执行超时"
# ——– Agent ——–
def plan_and_solve_agent(question: str):
print(">>> 第一阶段:规划")
steps = plan(question)
print(f"拆成 {len(steps)} 步: {steps}")
print("\\n>>> 第二阶段:执行")
prev_result = "还没有上一步"
for i, step in enumerate(steps, 1):
print(f"\\n— 步骤 {i}: {step} —")
result = execute_step(step, question, prev_result)
print(f"结果: {result}")
prev_result = result
# 汇总
summary = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"任务: {question}\\n各步骤结果: {prev_result}\\n请汇总回答。"}],
temperature=0,
)
return summary.choices[0].message.content
if __name__ == "__main__":
print(plan_and_solve_agent("算 25*17,告诉我是不是质数"))
跑起来的流程是这样:
>>> 第一阶段:规划
拆成 3 步: ['计算 25*17', '判断 425 是不是质数', '汇总回答']
>>> 第二阶段:执行
— 步骤 1: 计算 25*17 —
结果: 425
— 步骤 2: 判断 425 是不是质数 —
结果: 425 能被 5 整除,不是质数
— 步骤 3: 汇总回答 —
结果: 25*17=425,不是质数
看到和 ReAct 的区别了吗?规划阶段就把 3 步定死了。 执行阶段只是机械地"完成第 1 步 → 完成第 2 步 → 完成第 3 步"。
代价是灵活换来了什么
好处:
- Token 省了一大截。规划只需要一次调用,后面每步只需要上下文+上一步结果,不用每次都把全部历史塞进去。
- 不可能死循环。步数是定的,跑完拉倒。
- 全局结构清晰。Planner 能看到完整的任务图景,不会出现 ReAct 常见的那种"转了 10 轮还在原地打转"的情况。
坏处:
- 计划一旦错了,后面全错。如果 Planner 把"判断质数"写在"计算乘积"前面,Executor 会懵掉。
- 如果某一步需要临时查个什么东西(比如中间结果不确定,需要搜一下),Executor 没法灵活应对——它的 prompt 被限定在了"只完成当前子任务"。
- 错误会一路传播。不像 ReAct 那样可以通过后续的 Thought 步骤修正。
所以有人搞了个改进版叫 PS+(Plan-and-Solve+),它的核心就是在 Executor 发现某一步跑砸了的时候,触发重规划:
def ps_plus(question: str, max_replans=3):
steps = plan(question)
for attempt in range(max_replans):
prev = ""
ok = True
for i, step in enumerate(steps):
result = execute_step(step, question, prev)
if "失败" in result or "错误" in result:
steps = replan(question, steps, i, result) # 砍掉后面的,从 i 重新规划
ok = False
break
prev = result
if ok:
return summarize(question, steps, prev)
return "搞不定"
但这个"重规划"本身又是一次 LLM 调用,而且怎么判断"执行失败"也是个模糊地带——代码报错好判断,但"这个调研结果不够深入"呢?所以 PS+ 在实际落地时,触发条件需要仔细设计。
正面比比看
聊完各自的原理和代码,摆在一起看一下。
一个表说清差异
| 决策方式 | 实时决定,下个动作取决于前一步结果 | 提前定好全部步骤,照着执行 |
| 全局视野 | 弱,容易走失 | 强,一开始就有完整规划 |
| 灵活性 | 高,随时能拐弯 | 低,计划定了就不好改 |
| 错误容错 | 高,下步可以修正上步的错误 | 低,一步错可能步步错 |
| API 开销 | 高,不可预测 | 低,基本可预测 |
| 死循环风险 | 存在,必须有兜底 | 不存在 |
| 可解释性 | Thought 链完整可见 | 计划本身就是文档 |
实际场景怎么选
无脑选 ReAct 的场景:
- 需要实时信息:天气、股价、新闻、数据库查询——你没法"规划"一个还没发生的数据
- 探索性任务:代码调试、竞品分析、技术调研——你一开始不知道会发现什么
- 多工具交叉调用:搞着搞着可能需要换个工具,顺序不确定
无脑选 Plan-and-Solve 的场景:
- 步骤明确的复杂任务:比如"拉取上个月销售数据 → 计算环比增长 → 生成报表",每一步该干啥是已知的
- 长文档生成:写报告、写文章——需要全局结构,不然写到后面会跑题
- 对 token 消耗敏感的场景:预算有限,希望调用次数可控
举个具体的例子对比感受一下:
调试一段有 bug 的代码:
ReAct:
→ 跑一下,看报什么错
→ TypeError: 'int' object is not iterable
→ 哦,那看看哪行把整数当 list 用了
→ 定位到第 42 行,修掉
[每一步都在根据上一步的线索做决策]
Plan-and-Solve:
Step 1: 运行代码获取错误
Step 2: 分析错误原因
Step 3: 修复
[但如果 Step 1 拿到的错误信息出乎意料,Step 2 的 prompt 里没覆盖到,就……]
写一份行业调研报告:
ReAct:
→ 搜一下主流框架有哪些
→ 再搜一下性能对比
→ (写了两段后发现有篇新论文没看,又切回去搜)
→ 结果报告结构很散,东一块西一块
Plan-and-Solve:
Step 1: 搜框架
Step 2: 对比性能
Step 3: 分析趋势
Step 4: 写报告
→ 结构清晰,但中间如果发现"按原计划找不到需要的数据",得靠重规划兜底
混合用的才是高手
看到这儿你肯定在想:那能不能两个都用?
能。实际上生产环境里纯用某一种模式的极少。大部分是在做某种混合。
Plan → ReAct(推荐方案)
顶层用 Plan-and-Solve 定骨架,每个子步骤内部走 ReAct 的灵活循环。这是最常见的搭配。
顶层骨架(Plan):
资料调研 → 分析对比 → 撰写报告 → 审校修改
│
▼
内部执行(ReAct):
搜资料 → 看摘要 → 决定是否深入 → 再搜 → 直到满意
代码上就是把刚才两个例子拼一起——plan() 生成步骤列表,然后 execute_step() 内部跑一个完整的 ReAct 循环。
ReAct + Reflection(另一种选择)
在 ReAct 循环里每 N 步插入一次"反思":
Thought → Action → Observation → Thought → Action → Observation
→ [反思:目前进展如何?方向对吗?]
→ Thought → Action → Observation → …
相当于让模型定期跳出具体操作,从更高维度审视一下自己的工作。这种模式适合那种容易"走深走丢"的复杂任务。
总结一下
两个模式的取舍,本质上是在灵活性和结构性之间做权衡。
ReAct 把决策权分散到每一步,模型实时决定下一步干什么。代价是 token 烧得多,还有死循环风险。
Plan-and-Solve 把决策集中在规划阶段,后面就是机械执行。代价是一旦规划有误,整个任务可能白干。
所以没有"哪个更好",只有"哪个更适合你当前的任务"。如果拿不准,我的建议是:从 ReAct 开始试。 跑通了但觉得 token 开销太大、或者模型经常跑偏,再考虑往 Plan-and-Solve 的结构化方向调整。反过来就难了——Plan-and-Solve 的结构一旦固定,再想往里加灵活性,改动的成本要高得多。
最后说一句:这两个模式只是起点。现在已经有了 Self-Discovery(让模型自己选推理方式)、Multi-Agent 协作(Planner Agent + Worker Agents)、以及模型原生支持 Tool-Use 后框架层自动接管 ReAct 循环的方案。但不管怎么变,理解 ReAct 和 Plan-and-Solve 这俩底层的思维方式,始终是 Agent 开发的基本功。
参考:
代码在 Python 3.10+ 下都能跑,装个 openai 库就行。




