16. Planning

Harness Engineering

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:77、EnterPlanModeTool.ts:85。
  2. 计划是持久化的执行产物。每个 Session 使用唯一短名称(slug)对应一个计划 Markdown 文件,主线程与子 Agent 的文件彼此隔离;恢复时优先从计划文件读取,远端文件丢失时可从快照或消息历史恢复:plans.ts:32、plans.ts:119、plans.ts:164。
  3. 退出计划模式是审批门。ExitPlanModeV2Tool 校验当前模式;普通主线程向用户请求确认,要求审批的 teammate 则把计划内容和 request id 写入 team lead mailbox:ExitPlanModeV2Tool.ts:195、ExitPlanModeV2Tool.ts:221、ExitPlanModeV2Tool.ts:263。
  4. 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;正在执行的节点先取消或等待安全点。审批只覆盖被批准版本,若新版本扩大文件、网络或部署范围,必须重新审批。否则模型可以先提交一个低风险计划获批,再在执行中悄悄改成高风险动作。

16.8 DeepSeek Harness:Plan、Goal、Todo 和 Workflow 是四种独立状态

packages/plan/plan-mode 只管理“是否处于规划协作模式”:最后一个 plan/mode 事件决定状态,开启时插入部署方指导段,exit_plan_mode 把完整计划交给用户审阅。该工具在非 Plan Mode 也保持注册,使状态切换不改变 Tool Catalog;用户选择只在下一个已接受 agent/pre-step 边界写入,请求重试不会中途切模式。

packages/goal 将跨 Turn 的用户目标折叠为带 revision 的持久状态,并把自动续跑留在进程内;tool-todo 只写入当前 Session 的检查清单;workflow/ralph 才负责用 worker-thread 与子 Session 执行脚本化回合。这四者不共用一个“plan”布尔值,分别解决协作模式、持久意图、当前清单和可调度执行。

最后更新 8/17/2026, 6:27:24 PM