Agent 的 ReAct、Plan、Execute、Reflection 模式

这四个不是四选一,是同一个控制流的四段:ReAct 决定下一步干什么,Plan 决定整条路怎么走,Execute 决定路怎么落地,Reflection 决定落地之后怎么知道对不对。

拆开看,它们只回答两个问题:下一步谁定、错了怎么发现。其余差别都是这两个问题的实现细节。

一、四张形状

在这里插入图片描述

四个形状放在一起看,区别一目了然:ReAct 每一步都重新决策,Plan 只决策一次,Execute 不决策只执行,Reflection 不改流程只改产物。

决策时机循环在哪擅长致命伤
ReAct每一步思考与行动之间环境会变的探索步数失控、原地打转
Plan动手前一次无步骤可枚举的批量活计划错了全盘错
Execute不决策步骤内部把计划落成调用判据缺失等于没有执行器
Reflection产出之后产出与校验之间质量敏感的单点任务没有外部信号就空转

我在《Agent》那篇里给过 ReAct 和 Plan & Execute 的对比,这里往下挖一层:每种具体怎么写、坏在哪、怎么拼成一台能用的机器。

二、ReAct

1. 一条轨迹长什么样

ReAct 是 2022 年的 arXiv:2210.03629,主张推理轨迹和动作交替出现,观察结果回填到下一轮推理。落到代码里就是三段循环:

Thought: 用户要退 A103 的款,我得先看订单状态和金额
Action:  get_order("A103")
Observation: {"status": "已发货", "amount": 1000, "paid": true}

Thought: 已发货的订单退款要走售后流程,不能直接退;先查售后规则
Action:  get_policy("已发货退款")
Observation: {"need_return": true, "window_days": 7}

Thought: 需要先退货再退款,直接告诉用户流程,不要自己发起退款
Final:   A103 已发货,退款需要先申请退货(7 天内),我可以帮你起一个退货单。

中间两轮 Thought 是全部价值所在。只调工具不推理,模型在拿到"已发货"之后照样可能直接发起退款;只推理不调工具,它会编一个"7 天无理由"。

2. 三个部件的硬要求

  • 思考必须是可读的输出(或推理模型的内部推理),不能是空字符串占位。省掉思考的 ReAct 退化成"根据工具名瞎试"。
  • 动作必须结构化:函数名和参数走 schema,不要让它输出"帮我查一下 A103"这种自然语言句子再去正则解析。
  • 观察必须原样回填。你替它总结过的观察,等于把它推理的依据换掉了;要压缩就在工具内部压,别在执行器里压。

3. 四种失败模式

症状现场长什么样对策
重复动作同样的参数连调三次在上下文里列出已调用记录,参数相同就提示"这步刚做过"
观察被忽略拿到了"余额不足",下一轮还在下单关键观察前置(工具结果放最后一条消息),或在提示里点名"先确认再行动"
局部最优一直在优化第 2 步的措辞,忘了目标是出报告每轮把原始目标重贴一次,别指望它记住第一条消息
格式漂移thought 里写 Python 代码,action 不按 schema用强约束的结构化输出,错了就重试解析,别把脏字符串塞给工具

四种里最难查的是"观察被忽略"——日志里工具明明返回了正确数据,模型就是没按它行动,看上去像模型能力问题,实际是上下文位置问题。

三、Plan

1. 计划不是待办清单

一份能被执行的计划,每一步至少要有五个字段:

class Step(BaseModel):
    id: int
    task: str                                  # 这一步要干什么
    inputs: list[str]                          # 依赖哪几步的输出
    done_when: str                             # 完成判据,可判定
    on_fail: Literal["retry", "skip", "replan", "abort"]

class Plan(BaseModel):
    goal: str
    steps: list[Step]

done_when 是命门。没有它,执行器不知道这一步算不算完成,重规划也没有终点——模型永远可以说"还不够,再研究一轮"。

判据要能被代码检查,而不是"分析得足够深入"这种话。写成 {payable: 800}、tests_pass: true、rows > 0 才有用。

2. 粒度

太细:计划变成伪代码,你等于用自然语言重写了一遍程序,还多付了模型调用。

太粗:一步里塞整个"完成数据分析",那一步内部又成了黑箱 Agent,计划的约束等于没有。

我的经验是一步对应一个可验证的交付物:一个查询结果、一段草稿、一个通过测试的文件。这样每步结束都有东西可以检查,出错也能定位到步。

3. 什么时候别用 Plan

三条失效条件,中一条就别规划,用 ReAct:

  • 目标里有"合适"“有意思”"差不多"这类词,完成判据写不出来
  • 环境会在执行中变化(库存、价格、别人正在改同一份文件)
  • 步骤数取决于中间结果(先查有几章,才知道派几个人)

第三条其实是协调者-工作者那个模式,别混进 Plan 里硬撑。

四、Execute

执行器经常被写成一个 for 循环,然后所有问题都从这里冒出来。它实际要干三件事。

1. 路由与串联

按步骤声明的工具去调,参数从上一步的产出里取。我习惯用 @1.amount 这种引用语法,比让模型重新生成一遍参数稳得多——重生成就有编的可能。

inputs 为空的步骤互相独立,可以并发跑;有依赖的按拓扑序串行。并发时注意结果的写入:多路同时写同一个字段,得有 reducer 或显式合并,否则后完成的覆盖先完成的。

2. 失败策略

失败之后怎么办,必须写在计划里,不能让执行器临时决定:

策略用在哪
retry幂等且可能是瞬时错误(超时、限流)
skip可选的补充步骤,缺了不影响主交付
replan前置假设被推翻(查不到这个订单不是网络问题)
abort涉及写操作且状态不明,交人工

判据和失败策略必须分开:done_when 说"什么算成功",on_fail 说"不成功怎么办"。混在一起写,两者都会写歪。

3. 自由度旋钮

这是 Execute 最容易被忽视的设计点:一步内部给模型多大自由度。

给到最大(步骤内部随便跑一个 ReAct),计划就只是建议;给到最小(一步只许调一次声明好的工具),执行器就退化成一段写死的流水线,灵活性归零。

我的分法是:查询类步骤允许内部 ReAct(要几次查几次),写操作步骤只允许一次调用且参数固定。这条线划在"有没有副作用"上,比划在"步骤大小"上稳。

五、Reflection

1. 形状

产出一版,校验一遍,不合格就带着具体问题重做。

ReAct 是"想-做-看",Reflection 是"做-看-改",区别在于改的对象是产物而不是下一步。

2. Reflexion 加的那一层

2023 年的 Reflexion(arXiv:2303.11366)多做了一件事:把失败原因用自然语言写下来,存进记忆,下一次尝试时带进上下文。注意它改的不是模型权重,而是上下文——所谓的"言语强化"。

这个细节有实际意义:反思要留痕。这次为什么失败、改了什么、结果如何,都该落盘。只反思不记录,下一轮还会犯同样的错。

3. 三种校验源,强弱分明

  1. 硬信号:测试通过、schema 校验通过、接口返回 200。最强,能直接判定。
  2. 证据比对:答案里的每个断言都能在检索结果里找到出处。中,能发现编造,发现不了推理错。
  3. 模型评审:让模型逐条判断,严格一点可以换个模型来判。最弱——它和生成器共享同一批盲区,还容易给出"整体不错,建议完善细节"这种不可执行的结论。

用第三种时必须强制结构化输出(pass: bool、problems: [str]),并把 problems 逐条喂回去;不接受"建议"这种词。

4. 只改一轮

第二轮开始,收益断崖下跌,成本线性上升。要更多轮,那已经不是 Reflection,而是工作流里的评估器-优化器模式——区别在于它有明确的通过标准和退出条件,而 Reflection 是环内的一次自纠。

六、拼起来

单独用哪个都不够。真实的形状是:Plan 在外层,Execute 逐步跑,每步内部按需跑 ReAct,步骤后过一遍 Reflection,连续失败才重规划。

下面这段不联网,工具是三个普通函数,计划写死,只验证控制流:判据怎么挡、反思怎么改、什么时候停。

TOOLS = {
    "get_amount": lambda order: {"order": order, "amount": 1000},
    "apply_discount": lambda amount, rate: {"payable": round(amount * rate)},
    "write_draft": lambda order, payable: {"draft": f"订单 {order} 应付 {payable} 元"},
}

PLAN = [
    {"id": 1, "tool": "get_amount", "args": {"order": "A103"},
     "check": lambda ctx: ctx[1]["amount"] > 0},
    {"id": 2, "tool": "apply_discount", "args": {"amount": "@1.amount", "rate": 1.0},
     "check": lambda ctx: ctx[2]["payable"] == ctx[1]["amount"] * 0.8},
    {"id": 3, "tool": "write_draft", "args": {"order": "A103", "payable": "@2.payable"},
     "check": lambda ctx: ctx[3]["draft"].endswith("元")},
]


def run(plan, max_replan=1):
    ctx, replans = {}, 0
    for step in plan:
        args = {k: (ctx[int(v[1:].split(".")[0])][v.split(".")[1]]
                    if isinstance(v, str) and v.startswith("@") else v)
                for k, v in step["args"].items()}
        ctx[step["id"]] = TOOLS[step["tool"]](**args)
        print(f"第{step['id']}步 {step['tool']}({args}) -> {ctx[step['id']]}")
        if not step["check"](ctx):                      # 外部校验挡着,不靠模型自评
            if replans >= max_replan:
                return "中止,交人工"
            replans += 1
            print("  ✗ 判据不满足,带着反馈改这一步")
            ctx[step["id"]] = TOOLS[step["tool"]](**{**args, "rate": 0.8})
            print(f"  重试 -> {ctx[step['id']]}")
    return ctx[3]["draft"]


print("最终结果:", run(PLAN))

实际输出(照抄运行结果):

第1步 get_amount({'order': 'A103'}) -> {'order': 'A103', 'amount': 1000}
第2步 apply_discount({'amount': 1000, 'rate': 1.0}) -> {'payable': 1000}
  ✗ 判据不满足,带着反馈改这一步
  重试 -> {'payable': 800}
第3步 write_draft({'order': 'A103', 'payable': 800}) -> {'draft': '订单 A103 应付 800 元'}
最终结果: 订单 A103 应付 800 元

这段代码里没有一行"让模型反思一下"。第 2 步之所以能改对,是因为判据写成了 payable == amount * 0.8,执行器拿着这个硬条件去补了 rate。反思的价值不在反思本身,在于它有一个不依赖模型的自检依据。

七、选型

任务组合预算
单轮问答、轻量查询不套模式,一次调用带工具1~3 步
开放式探索、代码修 bugReAct 单环 + 步数上限8~20 步
多步业务流水线Plan → Execute(步骤内按需 ReAct)+ 步骤级 Reflection步数按计划,每步 ≤ 3 次调用
交付物有质量门槛(报告、SQL、配置)上面那条 + Reflection 一轮多一次评审调用

三档够用了:轻任务别上框架,探索任务用 ReAct 加护栏,可枚举的多步任务才值得写规划器和执行器。Reflection 只加在有硬判据的环节——没有判据的反思,就是让模型自己给自己打分。

最后留一句判断:这四个模式都在回答"错了怎么知道"。如果你说不出一个任务的成功判据,那四种模式套上去都不会变好,只会变贵。

Logo

更多推荐