跳转到内容

第 22 课:Planner 与任务分解

用 task graph、step planning、checkpoint、replan、stop condition 和失败回退控制多步骤工程任务。

Planner 的价值不是生成一段看起来很完整的 TODO。它要把模糊工程任务拆成可执行、可验证、可恢复的 task graph。

本课的核心模型是:每个 step 都要有依赖、输入、执行者、验证方式和停止条件。执行过程中每完成一个 checkpoint 就记录状态;失败时根据失败类型选择 replan、retry、fallback 或 stop。这样 agent 才能处理跨文件、跨测试、跨审查的工程任务,而不是在长对话里凭感觉继续。

第 21 课已经把角色拆出来了。现在的问题是:主 agent 怎么知道下一步派谁?什么时候可以并行?什么时候必须等待测试?什么时候应该重做计划?

例如用户输入:

实现 trace replay,并补测试和文档。

这不是一步任务。它至少包含源码调查、接口设计、实现、测试、文档、审查。Planner 要把这些动作变成图,而不是只输出“先看代码,再修改,再测试”。

本课从 /Users/ryanchen/codespace/pi-agent-course-lab/src/12-planner-task-graph.ts 出发,把 Step[] 扩展成带 checkpoint、replan 和 stop condition 的任务系统。

Planner 可以分成三层:

  • TaskGraph:描述任务之间的依赖关系,回答“哪些 step 可以执行”。
  • ExecutionState:记录哪些 step 完成、失败、跳过、等待确认。
  • ControlPolicy:决定失败后是 retry、replan、fallback 还是 stop。

最重要的不是 plan 文本,而是 step 的字段:

type Step = {
id: string;
title: string;
dependsOn: string[];
owner: "researcher" | "planner" | "implementer" | "tester" | "reviewer";
verification: string;
checkpoint: string;
stopCondition?: string;
};

done 集合变化时,nextReadySteps(steps, done) 就能算出下一批可执行 step。失败时,planner 读 failedStepevidenceremainingSteps,决定是否改图。

starter 的 createPlan(feature) 返回四步:

inspect -> implement -> test -> review

这是最小可用链。把它升级成 graph 后,可以表达并行:

inspect
-> design
-> implement
-> test
-> review
docs
depends on design

docs 不一定要等 test 完成,但必须等 design 稳定。task graph 让 planner 明确这种关系。

一个 step 如果没有验证方式,就不能自动判断是否完成。verification 不是泛泛写“检查通过”,而要写可收集的证据:

  • source anchors listed
  • diff touches only src/runtime and tests
  • npm test -- trace passes
  • review findings are empty or acknowledged
  • docs page contains usage example

验证方式决定 tester/reviewer 后续要收什么证据。

3. Checkpoint 是恢复点,不是装饰字段

Section titled “3. Checkpoint 是恢复点,不是装饰字段”

checkpoint 记录的是“完成到哪里可以恢复”。例如:

inspect.done: 已列出源码锚点
implement.done: 已产生 diff
test.failed: 测试失败日志已保存
review.blocked: 需要用户确认 API 取舍

有 checkpoint,长任务被中断后就不用从头读全部上下文。主 agent 可以从 checkpoint 恢复,或者把 checkpoint 交给另一个 subagent。

不要每失败一次就重写全计划。replan 的触发条件应该明确:

  • 源码调查发现原假设不成立。
  • 实现 step 发现需要新增依赖或修改公共 API。
  • 测试失败暴露缺少前置 step。
  • reviewer 发现安全或架构问题。
  • 用户改变范围。

replan 的输出也应该是 graph patch:新增 step、删除 step、修改依赖、修改验证方式,而不是重新输出一整段计划。

Planner 必须知道什么时候停止:

  • 所有 required steps 完成且验证通过。
  • 同一 step 连续失败达到上限。
  • 失败原因需要用户输入。
  • 权限不足,例如需要写文件但当前是 readonly。
  • 任务范围变大,超过当前执行预算。

stop condition 是 agent 可靠性的关键。没有 stop condition,replan 会变成无限“再试一次”。

失败不是只有 retry。常见回退策略:

  • retry:同一步重跑,例如网络抖动或测试偶发失败。
  • replan:改任务图,例如发现需要先改类型定义。
  • fallback:换更小实现,例如先做只读 trace viewer,不做 replay。
  • stop:需要用户确认或权限不足。

每次失败都要把命令、输出、diff、上下文摘要写进 evidence,供下一轮 planner 使用。

本课实践基于 /Users/ryanchen/codespace/pi-agent-course-lab/src/12-planner-task-graph.ts。starter 里已经有 createPlan(feature)nextReadySteps(steps, done)

先用最小图理解 ready step:

const plan = createPlan("trace replay");
const done = new Set<string>();
nextReadySteps(plan, done); // inspect
done.add("inspect");
nextReadySteps(plan, done); // implement

然后扩展成可执行状态机:

type StepState = "pending" | "running" | "done" | "failed" | "blocked";
type ExecutionState = {
done: Set<string>;
failed: Map<string, string>;
checkpoints: Record<string, string>;
};
function completeStep(state, step, evidence) {
state.done.add(step.id);
state.checkpoints[step.id] = evidence;
}

变量流:steps 是静态任务图;done 是动态执行状态;nextReadySteps() 用依赖关系和 done 集合计算下一步;checkpoints 保存为什么可以认为某步完成。

课堂上从 starter 的四步计划开始:

const steps = createPlan("trace replay");
const done = new Set<string>();
while (true) {
const ready = nextReadySteps(steps, done);
if (ready.length === 0) break;
for (const step of ready) {
const result = await runStep(step);
if (result.ok) {
done.add(step.id);
saveCheckpoint(step.id, result.evidence);
continue;
}
const decision = decideFailure(step, result);
if (decision.type === "retry") await retry(step);
if (decision.type === "replan") steps = applyGraphPatch(steps, decision.patch);
if (decision.type === "stop") return summarizeBlocked(step, result);
}
}

这段代码要让学生看到三个控制点:

  1. nextReadySteps() 是调度控制点,防止依赖没完成就执行。
  2. saveCheckpoint() 是恢复控制点,防止中断后丢证据。
  3. decideFailure() 是 replan/stop 控制点,防止失败后盲目继续。

再看一个 replan 例子:

// test 失败:缺少 trace fixture
const patch = {
add: [
{
id: "fixture",
title: "Add trace replay fixture",
dependsOn: ["inspect"],
verification: "fixture is loaded by test",
},
],
update: [
{ id: "test", dependsOn: ["implement", "fixture"] },
],
};

这里 planner 没有推翻整个计划,只是在图里补了一个前置 step,并让 test 依赖它。学生要带走的模型是:replan 是对 task graph 的补丁,不是把历史抹掉重来。

  • /Users/ryanchen/codespace/pi-agent-course-lab/src/12-planner-task-graph.ts:本课 lab starter,展示 StepcreatePlan()nextReadySteps()
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/plan-mode/README.md at 61babc2:Plan Mode 示例说明只读规划、计划提取、进度追踪、[DONE:n] 标记和持久化。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/plan-mode/index.ts at 61babc2:Plan Mode extension 的主实现,包含 plan/execution 模式切换、工具限制、上下文注入和完成追踪。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/plan-mode/utils.ts at 61babc2extractTodoItems()extractDoneSteps()markCompletedSteps()isSafeCommand() 等纯函数。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/plan-mode-utils.test.ts at 61babc2:plan mode 工具函数的测试入口,用于验证计划提取和完成标记逻辑。
  • /Users/ryanchen/codespace/external-sources/pi/packages/agent/src/agent-loop.ts at 61babc2:低层 agent loop 的停止与继续边界,可对照 planner 的 step loop。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session.ts at 61babc2:session 层处理 retry、abort、compaction 和事件,是把 step 执行结果落到长期会话状态的参考。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/subagent/index.ts at 61babc2:subagent chain/parallel 模式可作为 task graph 执行器的参考。

学完本课后,你应该能做到:

  • 把一个模糊 feature spec 拆成包含依赖关系的 task graph。
  • 为每个 step 写出 owner、verification、checkpoint 和 stop condition。
  • done 集合和依赖关系计算下一批 ready steps。
  • 解释 replan、retry、fallback、stop 的区别,并知道什么时候触发。
  • 设计失败证据格式,让后续 planner 能基于真实信息改图。
  1. 实践题:扩展 /Users/ryanchen/codespace/pi-agent-course-lab/src/12-planner-task-graph.ts,给 Step 增加 ownercheckpointstopCondition,并实现一个 applyGraphPatch() 支持新增 step 和更新依赖。
  2. 思考题:为什么 planner 失败后不应该总是重新生成一整份计划?什么时候应该用 graph patch,什么时候应该停止等待用户确认?