欢迎光临
我们一直在努力

ReAct 和 Plan-and-Solve,两种 Agent 模式你选哪个?

写这篇东西的起因是上个月在群里和人吵了一架。吵的是 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+ 在实际落地时,触发条件需要仔细设计。


正面比比看

聊完各自的原理和代码,摆在一起看一下。

一个表说清差异

维度ReActPlan-and-Solve
决策方式 实时决定,下个动作取决于前一步结果 提前定好全部步骤,照着执行
全局视野 弱,容易走失 强,一开始就有完整规划
灵活性 高,随时能拐弯 低,计划定了就不好改
错误容错 高,下步可以修正上步的错误 低,一步错可能步步错
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 开发的基本功。


参考:

  • Yao et al. “ReAct: Synergizing Reasoning and Acting in Language Models”, 2022 — https://arxiv.org/abs/2210.03629
  • Wang et al. “Plan-and-Solve Prompting”, 2023
  • LangChain Agents — https://python.langchain.com/docs/modules/agents/
  • LangGraph ReAct from Scratch — https://langchain-ai.github.io/langgraph/how-tos/react-agent-from-scratch/

  • 代码在 Python 3.10+ 下都能跑,装个 openai 库就行。

    赞(0)
    未经允许不得转载:171主机测评 » ReAct 和 Plan-and-Solve,两种 Agent 模式你选哪个?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址