Plan-and-Execute 与 Reflexion:给 ReAct 装上全局锚和经验回放
系列导航:00 系列导航 · 01 单 Agent 总论 · 02 ReAct 原理 · 03 手写内核 · 04 ACI 工具设计 · 05 上下文工程 ·06 两个增强变体· 07 上线前清单
小提一句: 由于平台每日发布笔记数量限制,目前系列笔记还没全部上传。后续内容会持续更新,大家可以先点个关注、收藏,以免错过~
引子:原生 ReAct 的两个结构性问题
第 2 篇结尾我们推导了 ReAct 的六个失效模式。其中两个不是靠写好工具或压好上下文能解决的,它们是循环结构本身的缺陷:
- 目标漂移:Thought 只做局部计划,每一步都在"对上一步做出反应"。10 步之后,模型可能在解决一个与原任务沾边但不同的问题。没有全局锚。
- 单步失败无法恢复:某一步走岔了(比如查错了服务名),后续全部基于错误前提推进。ReAct 只能在步内修正,没有任务级的"这次不算,重来"机制。
这两个缺陷各自催生了一个增强变体:
| ReAct 的结构性缺陷 | 对应的增强变体 |
|---|---|
| 缺全局锚(问题 1) | Plan-and-Execute:把计划显式化,作为执行期的锚点 |
| 缺任务级恢复(问题 2) | Reflexion:失败后生成文字反思,下一次尝试时注入 |
两者都不是 ReAct 的替代品,而是套在 ReAct 外面的控制层。看清这一点,"我该用 ReAct 还是 Reflexion"这种伪选择题就不会再出现了。
一、Plan-and-Execute:把全局结构显式化
1.1 架构
三个角色可以是同一个模型配三套 prompt,也可以是不同规模的模型(小模型规划、大模型执行,或者反过来)。工程上最常见的配置是同一个模型、三套 system prompt。别为了"分工"就引入第二个模型,那是多 Agent 的活儿,代价完全不同。
1.2 计划的 schema:不要让它自由发挥
计划必须是结构化的,否则它只是一段漂亮的文字,无法被程序校验、无法部分重排、无法做进度记账。一个够用的 schema:
frompydanticimportBaseModelclassStep(BaseModel):id:intgoal:str# 这一步要达成的子目标(不是要调用的工具)tool_hint:str|None# 建议工具(允许执行时偏离)depends_on:list[int]# 依赖的步骤 iddone_criteria:str# 什么证据出现,才算这一步完成status:str="pending"# pending | running | done | failed | skippedclassPlan(BaseModel):objective:str# 原始目标,锁定后不可被重规划修改steps:list[Step]三个设计要点,每一条都是从具体事故里换来的:
goal写子目标而不是工具名。写"调用 search_logs"会让执行器变成无脑执行器,失去对异常的适应能力;写"确认 5xx 的起始时间点",执行器才能自己选工具。done_criteria必须有。没有完成判据的步骤,执行器不知道该停在哪,会一直"再查一次",这就是漂移的微观机制。objective单独字段且不可修改。这是防漂移的最后防线:重规划只允许改steps,不允许改objective。我见过太多"重规划把原始目标也一起改了"的系统,它永远能自圆其说,因为它连题目都改了。
1.3 实现
asyncdefplan_and_execute(task:str,tools:ToolRegistry,max_steps:int=20):plan:Plan=awaitplanner(task)# 一次性规划h:list[Turn]=[]reflect_every=4# 每 4 步强制重规划一次forstepinrange(max_steps):cur=next_pending(plan)ifcurisNone:break# 周期性重规划:只在没有正在进行的步骤时触发,避免打断ifstep%reflect_every==0andstep>0:plan=awaitreplanner(task,plan,h)# objective 保持不变t,a=awaitllm_exec(cur,h,tools)# 单步 ReAct:T → Ah.append(Turn(thought=t,action=a))obs=awaittools.dispatch(a)h[-1].observation=obs cur.status=judge(cur.done_criteria,obs,h)# done / failed / 保持 runningifcur.status=="failed":plan=awaitreplanner(task,plan,h)# 异常立即重规划returnsynthesize(plan,h)# 汇总,而不是让最后一步直接 finish1.4 什么时候重规划:三种触发策略
| 策略 | 触发时机 | 优点 | 缺点 |
|---|---|---|---|
| 异常触发 | 步骤失败 / 工具连续报错 | 最省 | 无法发现"没失败但走偏了" |
| 周期触发 | 每 N 步(如 N=4) | 能抓到漂移 | 多花 token;N 太小会抖动 |
| 偏差检测触发 | 用 LLM 或规则判断"当前证据是否仍支持原计划" | 最准 | 需要额外的判断器 |
生产建议:异常触发(必开)+ 周期触发(N=4~6)。偏差检测留给任务价值足够高的场景。
1.5 失败模式
Plan-and-Execute 不是银弹,它有自己专属的四个坑:
- 计划错误带偏整段执行。这是它与 ReAct 最本质的权衡:ReAct 是"局部最优的累积",P&E 是"全局结构的约束"。当计划本身错了,P&E 会比 ReAct 错得更彻底、更一致,因为每一步都在忠实执行一个错误的计划。缓解:Planner 阶段允许少量探索(先查一下再定计划)、周期重规划、以及"执行器有权偏离"(
tool_hint只是 hint)。 - 计划过细 → 脆弱。把每一步拆到工具调用粒度,等于把工作流硬塞进模型,丧失了 Agent 的全部意义。经验粒度:一个 step ≈ 一个可验证的子目标 ≈ 1~5 次工具调用。
- 计划过粗 → 等于没有。
["1. 分析 2. 定位 3. 修复"]这种计划提供不了任何锚定作用。 - 重规划抖动。每次重规划都大幅改写剩余计划,导致执行器反复改变方向。缓解:限制重规划的改动幅度(如"每次最多重排后 50% 的步骤"),并要求重规划输出"为什么改"的理由。
1.6 适用边界
适合 Plan-and-Execute: ✓ 步骤数 > 15 且结构清晰(能大致预判需要哪些阶段) ✓ 子目标之间存在依赖关系(必须先 A 后 B) ✓ 任务失败代价高,需要可审计的阶段划分(计划本身就是审计材料) 不适合: ✗ 短任务(< 5 步)——规划开销占比过高,直接 ReAct ✗ 高度探索性任务(连阶段都无法预判)——计划会变成瞎猜,反成枷锁 ✗ 环境变化极快(计划生成时已过期)二、Reflexion:语言化的强化学习
2.1 架构
最多尝试 K 次;K 次仍未通过,就显式返回失败。
四个组件:Actor(就是 ReAct 本身)、Evaluator(判定成败)、Self-Reflection(生成文字反思)、Memory(存反思,下次注入)。
2.2 与强化学习的形式对照
这是 Reflexion 最漂亮的地方,理解了这张表,你就理解了它为什么被称作"verbal reinforcement learning":
| RL 概念 | Reflexion 的对应物 | 关键差异 |
|---|---|---|
状态s_t | 当前轨迹 + 任务描述 | 同 |
动作a_t | 模型生成的工具调用 | 同 |
奖励r | Evaluator 的成败信号(常退化为二值) | RL 通常是稠密标量,Reflexion 几乎只能是稀疏二值 |
| 策略更新 | 不改权重,改为向记忆追加一条文字反思 | 这是全部差异所在 |
| 经验回放 | 把历史反思检索注入 prompt | 回放的是自然语言,不是 (s,a,r,s’) 四元组 |
| 泛化方式 | 通过上下文的 in-context 泛化 | RL 通过参数泛化 |
| 遗忘 | 受上下文窗口限制,塞不下就"忘" | RL 是灾难性遗忘 |
Reflexion 把梯度下降换成了写日记。
它的优缺点都由这一个替换决定:
- 优点:反思人类可读可编辑(你可以人工修正一条错误反思);零训练成本;可以跨任务迁移(把反思库复用到同类任务);不会灾难性遗忘已有能力。
- 缺点:不压缩、不泛化到权重,每一条经验都要占 token 才能生效;受窗口硬约束;反思质量完全依赖生成它的模型。
2.3 实现
@dataclassclassAttempt:trajectory:list[Turn]success:boolverdict:str# Evaluator 给出的判定理由reflection:str# 模型生成的文字反思asyncdefreflexion(task:str,tools:ToolRegistry,evaluator:Evaluator,k:int=3)->Result:memory:list[str]=[]forattempt_noinrange(k):# 1) Actor:跑一个完整 ReAct,把历史反思注入 systemtraj=awaitreact(task,tools,extra_context=render(memory))# 2) Evaluator:必须是外部信号,不是模型的自我感觉ok,verdict=awaitevaluator(task,traj)ifok:returnResult.ok(extract_answer(traj),attempts=attempt_no+1,trace=traj)# 3) Self-Reflection:只许归因,不许空话refl=awaitllm(REFLECT_PROMPT.format(task=task,trajectory=render_trajectory(traj),verdict=verdict,# 关键:把外部判定理由喂给反思器))a=Attempt(traj,ok,verdict,refl)memory.append(a.reflection)# 4) Memory:累积,下次注入returnResult.fail("reflexion_exhausted",memory=memory)反思提示词的关键约束(这一段比架构更重要):
REFLECT_PROMPT=""" 下面是上一次尝试的完整轨迹,以及环境给出的失败判定。 轨迹: {trajectory} 失败判定: {verdict} 请写一条反思,必须包含: 1. 失败发生在第几步,该步的哪个具体决策是错的(引用该步的 Observation); 2. 为什么这个决策在当时看起来是对的(避免事后诸葛); 3. 下一次应该改的**具体动作**(工具名 + 参数层面的改动,不是"要更仔细")。 禁止输出以下类型的句子: - "下次要更仔细 / 更全面 / 更谨慎" - "我应该更早注意到……" - 任何不能翻译成一次具体工具调用的结论 """2.4 好反思 vs 坏反思
用 OpsAgent 举例,一眼就能看出差别:
| 内容 | |
|---|---|
| ❌坏反思 | “上一次我没有成功定位根因。下次我应该更仔细地分析日志,全面考虑各种可能性。” |
| ✅好反思 | “失败在第 3 步。我用search_logs(service="order")过滤日志,但 5xx 是在网关层产生的,网关日志里service字段是gateway,导致 order 服务日志中只有下游超时、没有根因。下次应先用query_metrics(service="gateway")确认错误产生于哪一层,再按层下钻。” |
判别标准极其简单:这条反思能否翻译成一次具体的工具调用改动?能,就是好反思;不能,就是模型在说废话,而废话反思比不反思更糟,它占了 token、给了虚假的进步感。
2.5 三个前提条件(缺一不可)
条件一:必须有可信的成败信号。
这是最重要的前提。Reflexion 的 Evaluator 必须是环境给出的,而不能是模型的自我评估:
| 场景 | Evaluator | 可信度 |
|---|---|---|
| 代码修复 | 单元测试 / 编译 | 高 |
| 数据分析 | 结果表 schema 校验 + 断言 | 高 |
| 游戏 / 具身任务 | 环境奖励 | 高 |
| 故障排查 | 验证脚本命中预注入根因 | 中高 |
| 开放式写作 / 分析报告 | ❌ 只能靠 LLM-judge 或人工 | 低——此时不要用 Reflexion |
没有外部信号时的反思是危险的:模型会生成"我觉得上次不够全面"之类的自我说服,然后带着一条错误的经验进入下一次尝试,把错误固化。这不是反思,这是自我强化的偏见。
因此第 1 篇的决策树里有一条硬规则:无确定性成败信号 → 用 ReAct + 严格预算,不要加 Reflexion。
条件二:任务可重试且副作用可控。
重试会放大副作用。一个会写文件、发消息、下单的 Agent,跑三次意味着三次副作用。幂等设计或 dry-run + 人工确认是前提。
条件三:反思必须被选择性注入,且总量受限。
反思会越积越多。三条工程约束:
- 最多保留最近 K=3~5 条;
- 按与当前任务的相关度检索注入,而不是全量塞入(第 5 篇的检索回放策略同样适用于记忆);
- 反思库允许人工清洗,这是它相对 RL 的最大优势,别浪费。
2.6 成本
Reflexion 的成本是乘性的:总成本 ≈ 单次 ReAct 成本 × 尝试次数 K。且由于每次尝试都要注入历史反思,后续尝试的上下文更长,实际是超线性的。设 K=3 时大致是 3.5~4 倍,而不是 3 倍。
所以 K 不要设大。经验区间 K=2~3;超过 3 次的边际收益在多数任务上已经很薄,不如把预算花在改 ACI 上。
三、选型决策树
把第 1 篇的流程图补全:
补两条边界说明:
- P&E 与 Reflexion 不互斥:可以是"先规划 → 执行 → 失败 → 反思计划而非反思步骤"。在长程任务上,反思计划的收益通常高于反思某一步,因为计划错了是最贵的错。
- 都不需要时,就别加。原生 ReAct 在 <10 步的任务上往往就是最优解。每加一层控制,就多一层成本、多一类 bug、多一处需要观测的地方。
四、组合:三层控制架构
当任务真的复杂时(长程 + 有信号 + 高价值),三层可以同时上。这是我推荐的组合方式:
三层的频率差是这个架构能工作的关键:计划层低频(贵,但提供结构)、执行层高频(便宜,提供适应)、反思层任务级(最贵,但只在失败时付费)。频率分层,就是成本分层。
五、一个诚实的争议:Reflexion 的边际收益
最后必须讲清楚一件事,否则这篇文章会误导你。
设单次尝试成功率为p,朴素重试 K 次(各次独立、不注入反思)的成功率是:
P(朴素重试 K 次) = 1 - (1 - p)^KReflexion 的全部价值,在于让各次尝试不独立且逐次变好,即p_1 < p_2 < p_3。如果p_2 = p_1(反思没带来任何改进),那么 Reflexion 就等价于更贵的朴素重试。
什么时候p_2 ≈ p_1?两种情况:
- 失败是随机噪声:模型这次手滑写错了参数,下次大概率不会犯同样的错。此时反思是在总结经验一个不存在的模式。
- 模型已经足够强,
p_1已经 0.9,剩下的失败是长尾个案,反思提炼不出可复用的规律。
2024 年之后的一个普遍观察是:随着基础模型变强,Reflexion 相对"朴素重试"的增量在缩小。这是合理的,反思能修正的,主要是"系统性的、模型自己意识不到的错误";而强模型犯的系统性错误本来就少。
⚠️ 需要说明:这个观察目前主要来自工程实践与部分复现,缺少严格的控制变量第三方研究。社区对此有真实分歧,本系列把它标注为存疑项。
我的实践判断(不是实验结论):
| 情况 | 建议 |
|---|---|
| 单次成功率已经 > 0.8 | 先测"朴素重试 K 次",大概率够用,且便宜 |
| 失败呈现可归类的模式(同一类错误反复出现) | 上 Reflexion,收益明确 |
| 无外部成败信号 | 不要上,用严格预算的原生 ReAct |
| 任务价值极高、信号可信、成本不敏感 | 上,且配合人工清洗反思库 |
Reflexion 治的是"系统性错误",不是"随机失误"。先确认你的失败是前者,再付这笔钱。
六、小结
ReAct 的两个结构性缺陷,各自催生了一个挂在外面的控制层。Plan-and-Execute 用显式计划当全局锚,治的是漂移;Reflexion 用文字反思治任务级失败。两个都不是替代品。
工程上我最想强调的反倒是"别急着加层"。短任务用原生 ReAct 往往就够了;只有长程、高价值、又有可信信号的任务,才配得上那套三层频率分层的组合——计划低频、执行高频、反思任务级,把成本压住。
最后留一个存疑项。强模型时代 Reflexion 相对朴素重试的边际收益在收窄,目前缺第三方控制变量复现。所以动手之前先确认你的失败是系统性的还是随机的,再决定要不要付这笔钱。
上一篇:05 长程 Agent 的上下文工程:截断、摘要压缩与检索回放
下一篇:07 上线前清单:单 Agent 的五类陷阱、六项指标与评测方法
参考
- Shinn et al.,Reflexion: Language Agents with Verbal Reinforcement Learning, NeurIPS 2023, arXiv:2303.11366.
- Yao et al.,ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023, arXiv:2210.03629.
- Wang et al.,Plan-and-Solve Prompting, ACL 2023, arXiv:2305.04091.
- Anthropic,Building Effective Agents, 2024-12(augmented LLM 与工作流/Agent 切分)。