# 16. Planning
# 16.1 原理:Planning 不是展示一张待办清单
Planning 的本质是把开放目标转成可执行、可验证、可调整的约束系统。面试时应区分四类经常被混在一起的对象:
| 对象 | 解决的问题 | 是否直接执行 | 典型状态 |
|---|---|---|---|
| Goal | 最终成功标准是什么 | 否 | active/completed/blocked |
| 计划产物(Plan Artifact) | 采用什么方案、有哪些风险 | 否,通常需审批 | draft/approved/rejected/superseded |
| Task graph | 工作如何拆分、依赖和分配 | 被调度器消费 | pending/ready/running/completed/failed |
| Step | 当前一次模型采样或工具动作 | 是 | proposed/running/succeeded/failed |
因此,计划至少同时承担五项职责:
- 目标对齐:把含糊需求改写为可测试的验收条件。
- 问题分解:按能力、文件、风险或依赖拆成有限子任务。
- 依赖建模:用 DAG 表示
blockedBy/blocks,只调度 ready 节点。 - 资源分配:决定由主 Agent、子 Agent、人还是外部系统执行。
- 动态重规划:工具失败、事实更新或预算变化后,局部修订而不是机械坚持旧计划。
计划不等于暴露模型的隐式思维链。生产系统应保存简洁的决策依据、任务依赖、证据和结果;不依赖不可验证的长篇内部推理。
# 16.2 计划模式、执行模式与审批边界
复杂 Coding Agent 常把计划做成一种受限运行模式:计划阶段允许读代码、搜索和提问,但禁止写业务文件;计划被批准后,Runtime 才切换到可执行模式。这样做的价值不是 UI 上多一个标签,而是把安全边界编码进工具集合和权限状态。
DISCOVER // 只读探索代码与环境,收集制定计划所需的事实和约束
→ DRAFT_PLAN // 根据已发现事实生成包含步骤、依赖和验收标准的计划草案
→ CHECK(feasibility, dependencies, acceptance criteria) // 检查方案可行性、任务依赖完整性以及验收条件是否可验证
→ HUMAN_OR_POLICY_APPROVAL // 将带版本的计划交给用户或自动策略进行执行前审批
├─ reject → REVISE // 审批被拒绝时保留反馈并修订计划,不能进入有副作用的执行阶段
└─ approve → EXECUTE_TASK_GRAPH // 审批通过后按依赖关系调度任务图中的就绪节点
├─ evidence insufficient → REPLAN // 执行证据不足或关键假设失效时,只重规划受影响部分
└─ all verified → COMPLETE // 所有任务均通过验收证据验证后,才把目标标记为完成
关键不变量:
- 计划模式不能调用会产生业务副作用的工具。
- 被批准的计划是一份带版本和哈希的执行产物,执行时不能悄悄替换。
- 同一任务最多一个执行者进入
running,任务认领需原子更新。 - 任务完成必须绑定证据,例如测试、Diff、查询结果,而不是只由模型声明。
- 重规划要保留旧版本和变更原因,便于回放失败归因。
# 16.3 Claude Code 源码映射
Claude Code 的 Planning 有两条互补路径:
- Plan Mode 是权限状态机。
EnterPlanModeTool禁止在子 Agent 上下文调用,并把toolPermissionContext.mode切到plan;返回指令明确只读探索、提出方案、再退出计划模式:EnterPlanModeTool.ts:77、EnterPlanModeTool.ts:85。 - 计划是持久化的执行产物。每个 Session 使用唯一短名称(slug)对应一个计划 Markdown 文件,主线程与子 Agent 的文件彼此隔离;恢复时优先从计划文件读取,远端文件丢失时可从快照或消息历史恢复:
plans.ts:32、plans.ts:119、plans.ts:164。 - 退出计划模式是审批门。
ExitPlanModeV2Tool校验当前模式;普通主线程向用户请求确认,要求审批的 teammate 则把计划内容和 request id 写入 team lead mailbox:ExitPlanModeV2Tool.ts:195、ExitPlanModeV2Tool.ts:221、ExitPlanModeV2Tool.ts:263。 - Task 是可调度 DAG。任务包含
owner/status/blocks/blockedBy/metadata;创建任务使用文件锁和单调递增的 ID 计数器,避免多个 Agent 并发创建时冲突:tasks.ts:69、tasks.ts:76、tasks.ts:279。TaskUpdateTool在 owner 变化时发送分配消息,并双向维护依赖:TaskUpdateTool.ts:276、TaskUpdateTool.ts:300。
这里可以得出一个重要结论:计划产物负责“方案审批”,任务依赖图负责“执行调度”,二者不能用同一张待办事项表替代。
# 16.4 Codex 源码映射
Codex 明确注释 update_plan 是 Todo/checklist 工具,不是 Plan mode。参数只有可选 explanation 与 pending/in_progress/completed 步骤:plan_tool.rs:6、plan_tool.rs:24。工具描述约束最多一个 in_progress:plan_spec.rs:42。
PlanHandler 在真正的 Plan mode 中反而拒绝 update_plan,解析参数后发送 EventMsg::PlanUpdate 给客户端,再给模型返回固定的 Plan updated:plan.rs:62、plan.rs:84、plan.rs:90。这说明 checklist 是用户可见的进度投影,不是完整的内部调度器。
# 16.5 可恢复 Planner 伪代码
function makePlan(goal, snapshot): // 定义从用户目标和当前世界快照生成可审批计划的入口
criteria = deriveAcceptanceCriteria(goal) // 把自然语言目标转换成可由测试或证据判断的验收条件
facts = boundedResearch(snapshot, goal) // 在时间和 Token 预算内读取代码与环境,补齐规划所需事实
tasks = decompose(goal, facts, criteria) // 依据事实和验收条件把目标拆成粒度可执行的任务
graph = validateDAG(tasks) // 校验任务依赖无环、引用有效,并产出可调度的有向无环图
plan = persistVersioned({goal, facts, graph, criteria}) // 持久化不可变计划版本,便于审批绑定、恢复和变更审计
return requestApproval(plan.id, plan.hash) // 用计划 ID 与内容哈希发起审批,防止审批后内容被静默替换
function schedulingTick(planId): // 定义调度器的一次周期性扫描与任务派发过程
plan = loadApprovedPlan(planId) // 只加载已经审批且哈希匹配的计划版本,避免执行草案
ready = plan.tasks.filter(t => // 从任务图中筛选当前满足运行条件的候选任务
t.status == PENDING && all(t.blockedBy, status == COMPLETED)) // 要求任务尚未认领,并且所有前置依赖已经成功完成
for task in capacityAwareSelect(ready): // 按执行者容量、优先级和能力,从就绪集合选择本轮任务
if compareAndSet(task.status, PENDING, RUNNING): // 原子地认领任务,确保并发调度器中只有一个执行者成功
dispatch(task, leastPrivilegeCapability(task)) // 派发任务并仅签发完成该任务所需的最小权限能力
function onTaskResult(task, result): // 定义任务执行结果到达后的验证、重试与重规划逻辑
persistEvidence(task.id, result) // 先持久化输出、测试结果和变更引用,防止状态更新后证据丢失
if result.success && verifier.accept(result.evidence): // 同时要求执行成功且独立验证器认可证据,不能只信模型声明
transition(task, COMPLETED) // 验证通过后将任务原子迁移为完成,从而解锁下游依赖
else if result.retriable && task.attempt < task.retryBudget: // 若故障可重试且没有耗尽预算,则避免昂贵的全局重规划
transition(task, PENDING) // 把任务重新放回待调度状态,由后续调度周期再次认领
else: // 不可重试、预算耗尽或验证失败时进入局部重规划分支
revised = replanAffectedSubgraph(task, result) // 仅修订失败任务及其依赖子图,保留其他已验证工作
persistPlanVersion(revised) // 保存新的不可变计划版本及变更原因,支持审计和恢复
# 16.6 高频面试题
问:什么时候应该重规划?
当关键假设被工具证据推翻、依赖不可用、预算明显偏离、权限无法获得或验收条件变化时重规划。普通的可重试网络错误不应触发全局重规划;优先做局部子图修订。
问:为什么不能让模型自己说“任务完成”就结束?
模型输出是提议,不是事实。完成条件应由 Harness 根据测试退出码、文件 Diff、结构化工具结果、人工批准或独立 verifier 判断。
# 16.7 原理深化:Plan、Task DAG 与审批是三份不同契约
计划产物是人与 Agent 之间的方案契约,描述目标、假设、风险和验收;任务有向无环图(Task DAG)是调度器的执行契约,描述节点、依赖、负责人、重试和状态;Approval 是安全系统的授权契约,绑定某个 Plan 版本或具体动作范围。三者可以互相引用,但不能合并为一个可随意改写的待办事项数组。
Claude Code 将 Plan 写成 Session 关联的文件,并在退出 Plan Mode 时向用户或 team lead 发审批请求;Task Store 则另外用锁、单调递增的 ID 计数器和双向依赖维护并发任务。Codex 又明确把 update_plan 定义为 checklist,并限制最多一个 in_progress,而不是把它当成真正的 Plan Mode。这些实现共同说明 UI 清单只是根据任务事件生成的视图:即使清单显示完成,Harness 仍应从 Task terminal event 和 verifier evidence 判断目标是否完成。
执行中重规划应创建新版本并计算受影响子图:已验证且前置假设未变的节点继续有效;依赖已失效假设的节点回到 pending/invalidated;正在执行的节点先取消或等待安全点。审批只覆盖被批准版本,若新版本扩大文件、网络或部署范围,必须重新审批。否则模型可以先提交一个低风险计划获批,再在执行中悄悄改成高风险动作。