LOONG / JOURNAL

返回列表

JOURNAL / Agent / 文章

ReAct 与 Plan-and-Execute 在代码任务中的适用阶段分析

  • Agent
  • 工程实践

从代码任务的不同阶段,理解两种 Agent 执行机制的适用场景。

ReAct 与 Plan-and-Execute 在代码任务中的适用阶段分析
本文目录回到顶部

一、问题的起点

当我们谈论"Agent 写代码"时,其实在谈的是一个差异极大的任务光谱:

  • 让 Agent 修一个 TypeError,和让它从零实现一个完整页面,是完全不同的事情
  • 让 Agent 在已有代码库里加一个字段,和让它重构一个 1000 行的单体文件,对"记忆"和"规划"的要求天差地别

现有的大多数对比停留在"任务复杂度"这个维度,但这不够精准。复杂度是一个模糊的连续量,我们无法在接到任务时直接判断"这个任务有多复杂"。

更实用的判断维度是:这个任务目前处于哪个开发阶段?

开发阶段是离散的、可识别的,而且不同阶段对 Agent 能力的核心要求截然不同。


二、两种机制的本质差异(一句话版本)

  • ReAct:没有全局计划,每一步"看到什么 → 想下一步 → 做下一步",依赖即时反馈驱动推进
  • Plan-and-Execute:先把任务拆成子任务列表,再逐一执行;全局结构在执行前已经确定

这两句话背后隐藏着一个关键差异:ReAct 的决策成本分散在每一步,Plan-and-Execute 的决策成本集中在开始时。

这意味着:

  • 当任务的"正确路径"事先无法确定时,ReAct 的逐步决策是优势
  • 当任务的子任务之间有明确依赖、需要全局一致性时,ReAct 的逐步决策会产生冲突和遗忘

三、代码任务的四个核心阶段

软件开发的过程,无论任务大小,都可以拆解为四个阶段。Agent 在每个阶段面对的核心挑战不同。

阶段一:探索与理解(Exploration)

场景举例:

  • 接手一个陌生代码库,需要理解其架构
  • 复现一个用户反馈的 bug,路径未知
  • 调查某个性能问题的根因

这个阶段的核心特征: 目标模糊,路径未知,每一步的发现都会改变下一步的方向。Agent 需要读文件 → 发现线索 → 读另一个文件 → 调整假设,这是一个典型的"观察驱动"过程。

适合的机制:ReAct

原因是直接的:你无法在开始时制定一个有效的计划,因为你不知道问题在哪里。Plan-and-Execute 在这里会产生一个虚假的计划——"先看 main.py,再看 utils.py,再看 config.py"——这个计划没有任何信息含量,只是文件的罗列。

ReAct 的每一步 Observation 都在缩小搜索空间,这正是探索阶段需要的。

示例执行轨迹:

任务:找出为什么用户提交表单后收不到确认邮件

Thought 1:应该先找邮件发送的入口函数
Action 1:search_code("send_email")
Obs 1:找到 email_service.py 里的 send_confirmation()

Thought 2:看看这个函数的实现,特别是错误处理
Action 2:read_file("email_service.py", lines=45-89)
Obs 2:发现函数 catch 了所有异常但没有记录日志,异常被吞掉了

Thought 3:需要确认异常是否真的被触发,查看最近的错误日志
Action 3:run_command("tail -n 100 logs/error.log | grep email")
Obs 3:发现 SMTP 连接超时错误,频率很高

Thought 4:SMTP 超时,可能是环境变量没配置
Action 4:run_command("echo $SMTP_HOST")
Obs 4:返回空字符串——确认了根因

在这个过程里,第 3 步和第 4 步在任务开始时完全无法预测。如果用 Plan-and-Execute,Planner 不可能提前知道要检查环境变量。


阶段二:设计与规划(Design)

场景举例:

  • 设计一个新功能的数据库 Schema 和 API 接口
  • 规划一次重构的改动范围和文件列表
  • 拆解一个大需求为可独立实现的子任务

这个阶段的核心特征: 目标明确,需要全局视角,输出的是结构而非代码。这个阶段不需要频繁调用工具,核心是 LLM 的规划和推理能力。

适合的机制:Plan-and-Execute(但只用 Planner,不执行)

设计阶段本身就是在"做计划",Plan-and-Execute 的 Planner 角色天然契合。但注意:这里的重点是输出一个显式的、可被检查的计划,而不是立刻开始执行。

显式计划的价值在于:人类可以在执行前介入审查,发现设计错误的成本远低于实现后再改。

示例:

任务:为现有的 Todo API 增加团队协作功能(多用户共享列表)

Planner 输出:
子任务列表:
1. 数据库层:新增 teams 表和 team_members 表,修改 todos 表增加 team_id 外键
2. Model 层:创建 Team 和 TeamMember model,修改 Todo model 增加团队关联
3. API 层:新增 /teams CRUD 接口,修改 /todos 接口支持团队过滤
4. 权限层:增加"团队成员才能操作团队 Todo"的权限检查中间件
5. 测试层:为以上每个改动编写对应的单元测试和集成测试
6. 迁移层:编写数据库迁移脚本,保证已有数据不受影响

依赖关系:
- 任务 2 依赖任务 1(Model 依赖 Schema)
- 任务 3 依赖任务 2
- 任务 4 依赖任务 3
- 任务 5 可以与 2/3/4 并行推进
- 任务 6 独立,可最后执行

这个计划在执行前就暴露了一个问题:如果 Planner 漏掉了"任务 6"(数据库迁移),执行阶段在生产环境部署时会出现严重问题。显式计划让这个遗漏在设计阶段就被发现。


阶段三:实现(Implementation)

场景举例:

  • 按照设计文档实现一个功能
  • 完成 Planner 分配的某个子任务
  • 从零写一个有明确输入输出规格的函数

这个阶段的核心特征: 目标高度明确(有规格、有测试用例),步骤之间有严格的依赖顺序,需要全局一致性(文件 A 的接口要和文件 B 的调用匹配)。

适合的机制:Plan-and-Execute

实现阶段是 Plan-and-Execute 的主场。原因有三:

  1. 一致性保证:Plan-and-Execute 在开始时就确定了所有文件的接口约定,不会出现 ReAct 里"写 A 文件时忘了 B 文件已经在等这个接口"的问题
  2. 并行可能:无依赖关系的子任务可以并行执行,提升效率
  3. 进度可见:每个子任务的完成状态是明确的,出错时容易定位是哪个子任务失败了

ReAct 在实现阶段的典型失败案例:

任务:实现用户注册功能(包含 controller、service、repository、model 四层)

Step 1: 写 UserController,定义了接口 /api/users POST,接受 {email, password, name}
Step 2: 写 UserService,实现 createUser(email, password)  ← 丢失了 name 参数
Step 3: 写 UserRepository,实现 save(user)
Step 4: 写 User model,定义了 id, email, password_hash 字段  ← 丢失了 name 字段
Step 5: 运行测试,失败
Step 6: 修复 UserService,加回 name 参数
Step 7: 修复 User model,加回 name 字段
Step 8: 发现 UserController 的验证逻辑和 UserService 的参数校验重复,不确定要不要改
...

ReAct 在多文件实现中容易产生"局部最优"问题:每一步单独看都是合理的,但全局缺乏一致性。步骤越多,接口漂移越严重。


阶段四:调试与修复(Debug & Fix)

场景举例:

  • 修复一个已知的 bug(有错误信息,有复现步骤)
  • 让一组失败的测试通过
  • 解决 CI 流水线中的构建错误

这个阶段的核心特征: 有明确的"失败信号"(报错信息、失败的测试),修复路径需要根据报错信息动态调整,每次修改后都需要验证是否解决了问题。

适合的机制:ReAct

调试是一个假设-验证的循环。每次运行测试的结果都是新的 Observation,会更新对 bug 根因的判断。Plan-and-Execute 在这里的问题是:bug 的真正原因往往和第一印象不同,一个固定的计划会把 Agent 锁定在错误的假设上。

示例执行轨迹:

任务:修复 test_user_login 测试失败,错误信息:AssertionError: 401 != 200

Thought 1:401 是鉴权失败,可能是测试数据里的密码哈希有问题
Action 1:read_file("tests/test_user.py", lines=1-30)
Obs 1:测试用 "password123" 创建用户,然后用同样密码登录

Thought 2:看看密码哈希和验证的逻辑
Action 2:read_file("services/auth_service.py")
Obs 2:发现哈希用的是 bcrypt,verify 用的也是 bcrypt,逻辑看起来没问题

Thought 3:可能是测试环境的 SECRET_KEY 和生产不同,导致 token 验证失败
Action 3:run_test("test_user_login", verbose=True)
Obs 3:详细日志显示"Invalid password"——根本没到 token 阶段,就是密码错了

Thought 4:重新看哈希逻辑,bcrypt 有个坑:rounds 参数在测试和生产配置不同会影响结果吗
Action 4:search_code("bcrypt", "config")
Obs 4:发现 config/test.py 里 BCRYPT_ROUNDS=4,config/prod.py 里 BCRYPT_ROUNDS=12
        但用户创建时用的是哪个 config?

Thought 5:检查测试的 fixture 是怎么创建用户的
Action 5:read_file("tests/conftest.py")
Obs 5:fixture 直接把明文密码存进了数据库,没有哈希!但 login 时做了哈希验证——根因找到了

这个调试过程里,第 4 步的"bcrypt rounds"假设是错误的,ReAct 能够在 Observation 反馈后及时纠正方向。如果用 Plan-and-Execute,Planner 一旦生成"检查 bcrypt 配置"这个子任务,Executor 就会在一个错误方向上浪费很多步骤。


四、判断框架:四象限模型

基于以上四个阶段的分析,可以归纳出一个判断框架:

                    目标明确
                       │
                       │
     Plan-and-Execute  │  Plan-and-Execute
      (设计阶段)      │   (实现阶段)
                       │
路径未知 ──────────────┼────────────────── 路径明确
                       │
       ReAct           │      ReAct
     (探索阶段)      │    (调试阶段)
                       │
                    目标模糊

更直接的判断问题只有两个:

问题 1:任务开始时,你知道需要做什么(what)吗?

  • 知道 → 倾向 Plan-and-Execute
  • 不知道 → 倾向 ReAct

问题 2:任务开始时,你知道怎么做(how)吗?

  • 知道 → 倾向 Plan-and-Execute(路径确定)
  • 不知道 → 倾向 ReAct(需要探索路径)

五、验证设计

为了验证以上分析,可以设计以下对照实验:

5.1 实验环境

  • 基础模型:Claude Sonnet / GPT-4o(两种机制用同一模型)
  • ReAct 实现:LangGraph ReAct Agent,工具集:read_file, write_file, run_command, search_code
  • Plan-and-Execute 实现:LangGraph Plan-and-Execute,同一工具集,加 Replan 节点
  • 代码库:真实的开源项目(推荐使用 FastAPI 官方示例仓库)

5.2 测试任务集

阶段一任务(探索):

  • 任务 E1:给定一个有 bug 的 FastAPI 项目,但只告诉 Agent"用户反馈登录后权限不对",不提供错误信息,让 Agent 自行排查根因
  • 任务 E2:给定一个陌生代码库,让 Agent 生成一份架构说明文档
  • 评估指标:根因定位准确率、步骤数、无效步骤比例

阶段三任务(实现):

  • 任务 I1:按给定规格为 FastAPI 项目新增"用户收藏"功能(涉及 model、service、router、test 四层)
  • 任务 I2:为现有 API 增加 Redis 缓存层(需修改 5 个文件)
  • 评估指标:实现完整率(所有文件都被正确修改)、接口一致性(跨文件接口匹配率)、测试通过率

阶段四任务(调试):

  • 任务 D1:给定 3 个失败的测试,让 Agent 修复(bug 根因需要 2–3 步推理才能找到)
  • 任务 D2:修复一个在异步代码里的竞态条件
  • 评估指标:修复成功率、假设纠正次数(走错方向后能否及时回头)

5.3 关键指标

指标定义衡量什么
任务完成率通过预设验收条件的比例最终结果质量
接口漂移率跨文件接口不一致的次数全局一致性
无效步骤率重复/矛盾/被撤销的步骤占比决策效率
方向纠正次数推翻之前 Thought 的次数灵活适应能力
Token 消耗完成任务的总 token 数成本效率

5.4 预期结论

基于机制分析,预期数据会呈现以下模式:

  • 在探索和调试任务上,ReAct 的"方向纠正次数"会显著高于 Plan-and-Execute,这应该带来更高的任务完成率
  • 在实现任务上,Plan-and-Execute 的"接口漂移率"应显著低于 ReAct,体现为更高的测试通过率
  • 在实现任务上,ReAct 的 Token 消耗会因反复修改而高于 Plan-and-Execute

如果实验数据和预期一致,则验证了"阶段"而非"复杂度"是更好的判断维度。如果某些数据不符预期,则需要分析是机制本身的问题还是工具集和实现方式的问题。


六、混合策略:把两种机制放到正确的阶段

一个完整的代码 Agent 系统不应该只选一种机制,而应该根据当前阶段动态切换:

用户输入任务
     │
     ▼
[阶段识别器]
     │
     ├── 目标模糊?→ ReAct(探索模式)→ 产出:根因分析 / 架构理解
     │                                          │
     │                                          ▼
     ├── 目标明确,路径未知?→ Plan-and-Execute  → 产出:显式子任务计划
     │                         (仅 Planner)           │
     │                                                  ▼
     └── 目标明确,路径明确?→ Plan-and-Execute  → 产出:代码实现
                               (Planner + Executor)
                                                  │(遇到 bug)
                                                  ▼
                                              ReAct(调试模式)

这个结构的核心洞察是:Plan-and-Execute 的 Planner 和 Executor 可以分开使用。设计阶段只用 Planner,让人类审查计划后再触发 Executor,是比全自动流水线更安全、也往往更准确的工程实践。


七、结论

ReAct 和 Plan-and-Execute 的优劣不是固定的,而是随开发阶段而变化的:

  • 探索阶段用 ReAct:路径未知,需要即时反馈驱动方向
  • 设计阶段用 Plan-and-Execute 的 Planner:输出显式计划,供人类审查
  • 实现阶段用 Plan-and-Execute 的 Executor:保证全局一致性,支持并行
  • 调试阶段用 ReAct:假设-验证循环,需要动态纠正方向

判断用哪种机制,不需要估算"这个任务有多复杂",只需要问:此刻我(或 Agent)是在缩小搜索空间,还是在执行一个已经确定的计划? 前者用 ReAct,后者用 Plan-and-Execute。


下一步:基于第五节的实验设计运行验证,重点关注实现阶段的接口漂移率数据,这是支撑本文核心论点最直接的证据。