写这篇东西的起因是上个月在群里和人吵了一架。吵的是 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 开发的基本功。


参考:

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

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

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐