Agent 的 ReAct、Plan、Execute、Reflection 模式
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. 三种校验源,强弱分明
- 硬信号:测试通过、schema 校验通过、接口返回 200。最强,能直接判定。
- 证据比对:答案里的每个断言都能在检索结果里找到出处。中,能发现编造,发现不了推理错。
- 模型评审:让模型逐条判断,严格一点可以换个模型来判。最弱——它和生成器共享同一批盲区,还容易给出"整体不错,建议完善细节"这种不可执行的结论。
用第三种时必须强制结构化输出(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 步 |
| 开放式探索、代码修 bug | ReAct 单环 + 步数上限 | 8~20 步 |
| 多步业务流水线 | Plan → Execute(步骤内按需 ReAct)+ 步骤级 Reflection | 步数按计划,每步 ≤ 3 次调用 |
| 交付物有质量门槛(报告、SQL、配置) | 上面那条 + Reflection 一轮 | 多一次评审调用 |
三档够用了:轻任务别上框架,探索任务用 ReAct 加护栏,可枚举的多步任务才值得写规划器和执行器。Reflection 只加在有硬判据的环节——没有判据的反思,就是让模型自己给自己打分。
最后留一句判断:这四个模式都在回答"错了怎么知道"。如果你说不出一个任务的成功判据,那四种模式套上去都不会变好,只会变贵。
更多推荐

所有评论(0)