# 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

因此,计划至少同时承担五项职责:

  1. 目标对齐:把含糊需求改写为可测试的验收条件。
  2. 问题分解:按能力、文件、风险或依赖拆成有限子任务。
  3. 依赖建模:用 DAG 表示 blockedBy/blocks,只调度 ready 节点。
  4. 资源分配:决定由主 Agent、子 Agent、人还是外部系统执行。
  5. 动态重规划:工具失败、事实更新或预算变化后,局部修订而不是机械坚持旧计划。

计划不等于暴露模型的隐式思维链。生产系统应保存简洁的决策依据、任务依赖、证据和结果;不依赖不可验证的长篇内部推理。

# 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 有两条互补路径:

  1. Plan Mode 是权限状态机EnterPlanModeTool 禁止在子 Agent 上下文调用,并把 toolPermissionContext.mode 切到 plan;返回指令明确只读探索、提出方案、再退出计划模式:EnterPlanModeTool.ts:77EnterPlanModeTool.ts:85
  2. 计划是持久化的执行产物。每个 Session 使用唯一短名称(slug)对应一个计划 Markdown 文件,主线程与子 Agent 的文件彼此隔离;恢复时优先从计划文件读取,远端文件丢失时可从快照或消息历史恢复:plans.ts:32plans.ts:119plans.ts:164
  3. 退出计划模式是审批门ExitPlanModeV2Tool 校验当前模式;普通主线程向用户请求确认,要求审批的 teammate 则把计划内容和 request id 写入 team lead mailbox:ExitPlanModeV2Tool.ts:195ExitPlanModeV2Tool.ts:221ExitPlanModeV2Tool.ts:263
  4. Task 是可调度 DAG。任务包含 owner/status/blocks/blockedBy/metadata;创建任务使用文件锁和单调递增的 ID 计数器,避免多个 Agent 并发创建时冲突:tasks.ts:69tasks.ts:76tasks.ts:279TaskUpdateTool 在 owner 变化时发送分配消息,并双向维护依赖:TaskUpdateTool.ts:276TaskUpdateTool.ts:300

这里可以得出一个重要结论:计划产物负责“方案审批”,任务依赖图负责“执行调度”,二者不能用同一张待办事项表替代。

# 16.4 Codex 源码映射

Codex 明确注释 update_plan 是 Todo/checklist 工具,不是 Plan mode。参数只有可选 explanation 与 pending/in_progress/completed 步骤:plan_tool.rs:6plan_tool.rs:24。工具描述约束最多一个 in_progressplan_spec.rs:42

PlanHandler 在真正的 Plan mode 中反而拒绝 update_plan,解析参数后发送 EventMsg::PlanUpdate 给客户端,再给模型返回固定的 Plan updatedplan.rs:62plan.rs:84plan.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;正在执行的节点先取消或等待安全点。审批只覆盖被批准版本,若新版本扩大文件、网络或部署范围,必须重新审批。否则模型可以先提交一个低风险计划获批,再在执行中悄悄改成高风险动作。

最后更新: 2026/8/5 22:05:21