04 - Claude Code Plan System

一、概念解释

为什么需要规划模式

复杂软件任务通常涉及多个文件的修改、架构层面的决策,以及多种可行方案的权衡。如果 LLM 直接开始编码,可能会:

  • 方向错误:选择了不合适的架构方案,导致大量返工
  • 遗漏关键上下文:没有充分理解现有代码模式就开始修改
  • 用户期望不一致:实现结果与用户心目中的方案存在偏差
  • 影响面失控:修改波及范围超出预期

设计哲学

Plan 模式的核心思想是 “先想后做”(Think Before You Act)

直接执行模式:  用户请求 → 立即编码 → 可能返工
规划模式:      用户请求 → 探索理解 → 方案设计 → 用户审批 → 编码实施

它将 “理解问题”“实施方案” 解耦为两个独立阶段,中间加入了用户审批这一安全阀。


二、核心流程图

规划模式的生命周期

PLAN 模式(权限降级为只读)

拒绝

批准

编辑后批准

拒绝

批准

用户发起复杂任务
如: 重构认证系统

LLM 判断需要规划
调用 EnterPlanMode

用户审批:
进入规划?

继续直接执行

Phase 1: 探索理解
启动 Explore 子代理
并行搜索代码库

Phase 2: 方案设计
启动 Plan 子代理
设计多种可行方案

Phase 3: 审查对齐
阅读关键文件
确认与用户意图一致

Phase 4: 撰写计划
写入计划文件(.md)
唯一允许写入的文件

Phase 5: 提交审批
调用 ExitPlanMode

用户审批:
计划可行?
可查看/编辑计划

恢复原有权限模式
LLM 按计划编码实施

状态流转

toolPermissionContext.mode 中,模式值有以下几种:

模式含义允许的操作
default正常交互模式所有操作(需用户逐步确认)
plan规划模式只读操作 + 计划文件编辑
auto自动模式分类器自动审批操作
bypassPermissions绕过权限所有操作自动通过

规划模式 vs 直接执行模式的权衡

维度规划模式直接执行模式
安全性高 – 用户在编码前审批方案中 – 逐步确认每个操作
效率低 – 需要额外的探索和审批时间高 – 直接开始工作
返工风险低 – 方案已对齐中高 – 方向错误时需要重来
适用任务新功能、重构、架构决策、多文件变更简单修复、明确的小改动
token 消耗较高 – 需要探索和设计阶段较低 – 直接编码
用户体验更可控但更慢更快但更依赖信任
代码质量更好 – 经过充分思考取决于任务复杂度

决策流程图

用户请求

任务是否简单明确?

直接执行

是否有多种可行方案?

进入规划模式

是否涉及 3+ 个文件?

进入规划模式

需求是否清晰?

直接执行

进入规划模式


三、解决什么问题?

Plan 模式解决的核心问题:如何避免 LLM 在复杂任务中盲目编码导致的返工和方向错误

具体来说,它解决了以下子问题:

  • 理解不足:LLM 在开始编码前没有充分探索代码库,遗漏关键的现有模式和可复用代码
  • 方案缺失:面对多种可行方案时缺少系统性的比较和设计过程
  • 期望偏差:实现结果与用户真实意图不一致,缺少审批和反馈环节
  • 影响面失控:修改波及范围超出预期,需要提前评估和规划
  • 权限安全:规划阶段需要限制为只读权限,防止在方案未确认时就执行破坏性操作

四、核心代码详解

4.1 EnterPlanModeTool – 进入规划模式

文件路径:src/tools/EnterPlanModeTool/EnterPlanModeTool.ts

export const EnterPlanModeTool: Tool = buildTool({
  name: ENTER_PLAN_MODE_TOOL_NAME,

  // 搜索提示:帮助 LLM 在合适场景选择此工具
  searchHint: 'switch to plan mode to design an approach before coding',
  maxResultSizeChars: 100_000,

  // ⑥ 启用性检查:KAIROS 渠道模式下禁用
  //    原因:--channels 激活时(Telegram/Discord),审批对话框无法显示
  //    同时 ExitPlanMode 也被禁用,避免进入后无法退出的陷阱
  isEnabled() {
    if (feature('KAIROS') && getAllowedChannels().length > 0) {
      return false
    }
    return true
  },

  async call(_input, context) {
    // ① 安全检查:子代理中禁止使用规划模式
    //    原因:规划模式需要用户交互(审批对话框),
    //    子代理运行在后台,无法与用户直接交互
    if (context.agentId) {
      throw new Error('EnterPlanMode tool cannot be used in agent contexts')
    }

    // ② 记录模式转换(用于生成系统提示附件)
    //    当 fromMode !== 'plan' 且 toMode === 'plan' 时,
    //    清除之前可能残留的 plan_mode_exit 附件
    const appState = context.getAppState()
    handlePlanModeTransition(appState.toolPermissionContext.mode, 'plan')

    // ③ 更新权限上下文 -- 降级为只读
    //    prepareContextForPlanMode() 会:
    //    - 保存当前模式到 prePlanMode(退出时恢复用)
    //    - 处理 auto 模式的特殊逻辑(见第 4 节)
    context.setAppState(prev => ({
      ...prev,
      toolPermissionContext: applyPermissionUpdate(
        prepareContextForPlanMode(prev.toolPermissionContext),
        { type: 'setMode', mode: 'plan', destination: 'session' },
      ),
    }))

    return {
      data: {
        message:
          'Entered plan mode. You should now focus on exploring ' +
          'the codebase and designing an implementation approach.',
      },
    }
  },

  // ⑦ 工具结果映射:根据工作流类型注入不同指令
  mapToolResultToToolResultBlockParam({ message }, toolUseID) {
    const instructions = isPlanModeInterviewPhaseEnabled()
      // 迭代式工作流:简短提示,详细指令通过 plan_mode 附件注入
      ? `${message}\n\nDO NOT write or edit any files except the plan file.
         Detailed workflow instructions will follow.`
      // 五阶段工作流:内联完整指令
      : `${message}\n\nIn plan mode, you should:
        1. Thoroughly explore the codebase to understand existing patterns
        2. Identify similar features and architectural approaches
        3. Consider multiple approaches and their trade-offs
        4. Use AskUserQuestion if you need to clarify the approach
        5. Design a concrete implementation strategy
        6. When ready, use ExitPlanMode to present your plan for approval

        Remember: DO NOT write or edit any files yet. This is a read-only
        exploration and planning phase.`
    return { type: 'tool_result', content: instructions, tool_use_id: toolUseID }
  },

  // 标记为只读工具和并发安全
  isConcurrencySafe() { return true },
  isReadOnly() { return true },

  // 需要延迟处理(shouldDefer: true)-- 触发用户审批对话框
  shouldDefer: true,
})

关键设计要点:

  1. shouldDefer: true – 此工具的调用不会立即执行,而是弹出用户确认对话框。用户必须明确同意才能进入规划模式。
  2. 子代理限制context.agentId 检查确保只有在主会话中才能进入规划模式,防止后台代理陷入无法退出的状态。
  3. 不可变状态更新 – 使用 setAppState(prev => ...) 函数式更新,而非直接修改状态。

4.2 ExitPlanModeV2Tool – 退出规划模式

文件路径:src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts

export const ExitPlanModeV2Tool: Tool = buildTool({
  name: EXIT_PLAN_MODE_V2_TOOL_NAME,
  searchHint: 'present plan for approval and start coding (plan mode only)',
  maxResultSizeChars: 100_000,

  // ⑥ 启用性检查(与 EnterPlanMode 相同的 KAIROS 渠道限制)
  isEnabled() {
    if (feature('KAIROS') && getAllowedChannels().length > 0) return false
    return true
  },

  // ⑦ 非只读 — 会将用户编辑的计划同步写回磁盘
  isReadOnly() { return false },

  // ⑧ 用户交互需求:子代理无需本地用户交互
  requiresUserInteraction() {
    if (isTeammate()) return false  // 队友通过 mailbox 审批
    return true                     // 非队友需要用户确认对话框
  },

  // ⑨ 输入校验:拒绝在非 plan 模式下调用
  async validateInput(_input, { getAppState, options }) {
    if (isTeammate()) return { result: true }
    const mode = getAppState().toolPermissionContext.mode
    if (mode !== 'plan') {
      // 防止 compact/clear 后模型重复调用 ExitPlanMode
      return {
        result: false,
        message: 'You are not in plan mode. This tool is only for exiting plan mode after writing a plan.',
        errorCode: 1,
      }
    }
    return { result: true }
  },

  // ⑩ 权限检查:子代理直接放行,非队友弹出确认
  async checkPermissions(input, context) {
    if (isTeammate()) return { behavior: 'allow', updatedInput: input }
    return { behavior: 'ask', message: 'Exit plan mode?', updatedInput: input }
  },

  async call(input, context) {
    // ① 获取计划文件路径和内容
    const filePath = getPlanFilePath(context.agentId)

    // CCR (Web UI) 用户可能编辑了计划内容
    // 优先使用编辑后的版本,否则从磁盘读取
    const inputPlan =
      'plan' in input && typeof input.plan === 'string'
        ? input.plan
        : undefined
    const plan = inputPlan ?? getPlan(context.agentId)

    // ② 如果用户编辑了计划,同步写回磁盘并持久化快照
    if (inputPlan !== undefined && filePath) {
      await writeFile(filePath, inputPlan, 'utf-8').catch(e => logError(e))
      void persistFileSnapshotIfRemote()
    }

    // ③ 团队模式:将计划提交给队长审批
    if (isTeammate() && isPlanModeRequired()) {
      if (!plan) throw new Error(`No plan file found at ${filePath}`)
      const requestId = generateRequestId('plan_approval', ...)
      await writeToMailbox('team-lead', {
        from: agentName,
        text: jsonStringify({
          type: 'plan_approval_request',
          from: agentName,
          planFilePath: filePath,
          planContent: plan,
          requestId,
        }),
      }, teamName)

      // 更新任务状态为等待审批
      setAwaitingPlanApproval(agentTaskId, context.setAppState, true)
      return { data: { plan, isAgent: true, filePath, awaitingLeaderApproval: true, requestId } }
    }

    // ④ 熔断器保护:auto 模式在规划期间可能被禁用
    let gateFallbackNotification: string | null = null
    if (feature('TRANSCRIPT_CLASSIFIER')) {
      const prePlanRaw = appState.toolPermissionContext.prePlanMode ?? 'default'
      if (prePlanRaw === 'auto' && !isAutoModeGateEnabled()) {
        gateFallbackNotification = getAutoModeUnavailableNotification(reason)
        // 通知用户 auto 模式不可用,将降级到 default
        context.addNotification?.({ text: `plan exit → default · ${gateFallbackNotification}` })
      }
    }

    // ⑤ 恢复之前的权限模式
    context.setAppState(prev => {
      if (prev.toolPermissionContext.mode !== 'plan') return prev

      setHasExitedPlanMode(true)
      setNeedsPlanModeExitAttachment(true)

      // 从 prePlanMode 恢复
      let restoreMode = prev.toolPermissionContext.prePlanMode ?? 'default'

      // 熔断器:如果 auto 模式在规划期间被禁用,降级到 default
      if (restoreMode === 'auto' && !isAutoModeGateEnabled()) {
        restoreMode = 'default'
      }

      // 追踪规划期间是否实际使用了 auto 模式
      const autoWasUsedDuringPlan = isAutoModeActive()
      setAutoModeActive(finalRestoringAuto)  // 激活或停用分类器
      if (autoWasUsedDuringPlan && !finalRestoringAuto) {
        setNeedsAutoModeExitAttachment(true)  // 触发 auto 退出附件
      }

      // 如果恢复到非 auto 模式,还原被剥离的危险权限
      let baseContext = prev.toolPermissionContext
      if (restoringToAuto) {
        baseContext = stripDangerousPermissionsForAutoMode(baseContext)
      } else if (prev.toolPermissionContext.strippedDangerousRules) {
        baseContext = restoreDangerousPermissions(baseContext)
      }

      return {
        ...prev,
        toolPermissionContext: {
          ...baseContext,
          mode: restoreMode,
          prePlanMode: undefined,
        },
      }
    })

    // ⑥ 检查是否支持团队创建(用于输出中的团队提示)
    const hasTaskTool = isAgentSwarmsEnabled() &&
      context.options.tools.some(t => toolMatchesName(t, AGENT_TOOL_NAME))

    return {
      data: {
        plan,
        isAgent: !!context.agentId,
        filePath,
        hasTaskTool: hasTaskTool || undefined,
        planWasEdited: inputPlan !== undefined || undefined,
      },
    }
  },

  // ⑪ 工具结果映射:根据场景生成不同的输出消息
  mapToolResultToToolResultBlockParam(
    { isAgent, plan, filePath, hasTaskTool, planWasEdited, awaitingLeaderApproval, requestId },
    toolUseID,
  ) {
    // 团队队友等待审批
    if (awaitingLeaderApproval) {
      return { content: `Your plan has been submitted to the team lead for approval.
        Request ID: ${requestId}. Do NOT proceed until you receive approval.` }
    }
    // 子代理审批通过
    if (isAgent) {
      return { content: 'User has approved the plan. Please respond with "ok"' }
    }
    // 空计划
    if (!plan || plan.trim() === '') {
      return { content: 'User has approved exiting plan mode. You can now proceed.' }
    }
    // 正常输出:包含完整计划 + 团队提示
    const teamHint = hasTaskTool
      ? `\n\nIf this plan can be broken down into multiple independent tasks,
         consider using the TeamCreate tool to create a team.`
      : ''
    const planLabel = planWasEdited ? 'Approved Plan (edited by user)' : 'Approved Plan'
    return { content: `User has approved your plan. You can now start coding.
      Your plan has been saved to: ${filePath}${teamHint}
      ## ${planLabel}:\n${plan}` }
  },

  shouldDefer: true,
  isConcurrencySafe() { return true },
})

关键设计要点:

  1. 模式恢复机制prePlanMode 字段暂存进入规划前的模式,退出时精确恢复。
  2. 熔断器保护 – 如果 auto 模式在规划期间被禁用(通过 GrowthBook 配置),退出时不会恢复到 auto,而是降级到 default,并通过 addNotification 通知用户。
  3. 计划内容双通道 – 支持从磁盘读取和从用户编辑结果(CCR Web UI)两个来源获取计划内容。
  4. 团队协作 – 在多代理团队中,计划需要通过 writeToMailbox 发送给队长审批,包含 requestId 用于跟踪。
  5. auto 使用追踪 – 通过 autoWasUsedDuringPlan 追踪规划期间是否实际使用了 auto 模式,决定是否触发 auto 退出附件。
  6. 输入校验validateInput() 防止 compact/clear 后模型在非 plan 模式下重复调用此工具。
  7. 非只读isReadOnly() 返回 false,因为需要将用户编辑的计划同步写回磁盘。

4.3 handlePlanModeTransition – 模式转换副作用管理

文件路径:src/bootstrap/state.ts

export function handlePlanModeTransition(fromMode: string, toMode: string): void {
  // 进入规划模式:清除之前可能残留的退出附件
  // 防止用户快速切换时发送矛盾的 plan_mode + plan_mode_exit
  if (toMode === 'plan' && fromMode !== 'plan') {
    STATE.needsPlanModeExitAttachment = false
  }

  // 离开规划模式:标记需要发送退出附件
  // 退出附件会在下一轮对话中作为系统提示注入
  if (fromMode === 'plan' && toMode !== 'plan') {
    STATE.needsPlanModeExitAttachment = true
  }
}

这个函数管理的是 系统提示附件(System Prompt Attachments) 的状态。附件机制确保 LLM 在每轮对话中都收到正确的上下文提示,即使在 context clear(上下文压缩)之后也不会丢失关键信息。

4.4 prepareContextForPlanMode – 进入时的权限准备

文件路径:src/utils/permissions/permissionSetup.ts

export function prepareContextForPlanMode(
  context: ToolPermissionContext,
): ToolPermissionContext {
  const currentMode = context.mode
  if (currentMode === 'plan') return context  // 已在 plan 中,无需操作

  // 场景 A:从 auto 模式进入 plan
  if (feature('TRANSCRIPT_CLASSIFIER') && currentMode === 'auto') {
    const planAutoMode = shouldPlanUseAutoMode()  // 用户是否选择在规划中使用 auto

    if (planAutoMode) {
      // auto 语义保持激活,仅暂存当前模式
      return { ...context, prePlanMode: 'auto' }
    }

    // 不使用 auto:停用分类器,恢复危险权限
    autoModeStateModule?.setAutoModeActive(false)
    setNeedsAutoModeExitAttachment(true)
    return {
      ...restoreDangerousPermissions(context),
      prePlanMode: 'auto',
    }
  }

  // 场景 B:从 default 等模式进入,且启用了规划中的 auto 模式
  if (feature('TRANSCRIPT_CLASSIFIER')) {
    const planAutoMode = shouldPlanUseAutoMode()
    if (planAutoMode && currentMode !== 'bypassPermissions') {
      autoModeStateModule?.setAutoModeActive(true)
      return {
        ...stripDangerousPermissionsForAutoMode(context),
        prePlanMode: currentMode,
      }
    }
  }

  // 场景 C:普通进入(最常见)
  // 仅暂存当前模式,不做其他处理
  return { ...context, prePlanMode: currentMode }
}

4.5 权限流转图

退出规划模式时的权限恢复

restoreMode=default
恢复原始权限

restoreMode=auto
保持危险权限剥离

restoreMode=default
熔断器阻止恢复auto
危险权限恢复

plan
prePlanMode=default

default
正常权限

plan
prePlanMode=auto

auto
分类器权限

plan
prePlanMode=auto
熔断器触发

default
正常权限

进入规划模式时的权限变化

prepareContextForPlanMode()
prePlanMode=default
危险权限保持不变

场景A1: planAutoMode=true
prePlanMode=auto
危险权限仍被剥离

场景A2: planAutoMode=false
prePlanMode=auto
危险权限已恢复

default
正常权限

plan
只读权限

auto
分类器权限

plan
auto语义保持
分类器仍运行

auto
分类器权限

plan
纯只读
分类器停用

4.6 危险权限的剥离与恢复

在 auto 模式下,某些权限规则会绕过安全分类器(如 Bash(*)Bash(python:*)Agent(*)),因此在进入规划模式时需要处理这些"危险权限":

// 剥离危险权限:从上下文中移除,暂存到 strippedDangerousRules
stripDangerousPermissionsForAutoMode(context)

// 恢复危险权限:从 strippedDangerousRules 中还原
restoreDangerousPermissions(context)

剥离的权限被保存在 toolPermissionContext.strippedDangerousRules 中,是一个以来源(source)为键的映射表。退出规划模式时,如果恢复到非 auto 模式,这些权限会被原样还原。


五、五阶段规划工作流

5.1 标准五阶段工作流(V2)

isPlanModeInterviewPhaseEnabled() 返回 false 时(外部用户默认),系统使用五阶段工作流。这些指令通过 plan_mode 附件注入到 LLM 的上下文中。

Phase 1: Initial Understanding(初始理解)
目标:全面理解用户请求和关联代码

行动:
- 启动最多 3 个 Explore 子代理(并行)
- 每个代理聚焦不同的代码区域
- 搜索可复用的现有函数、工具和模式
- 避免提出已有实现的新代码

子代理数量决策:

任务规模代理数量适用场景
1 个简单任务集中在已知文件,用户提供了具体路径
2-3 个中等范围不确定,涉及多个模块,需要理解现有模式
Phase 2: Design(方案设计)
目标:基于 Phase 1 的理解设计实现方案

行动:
- 启动 Plan 子代理设计实现方案
- 默认启动至少 1 个 Plan 代理
- 复杂任务可启动最多 3 个代理提供不同视角

不同视角示例:
- 新功能:简洁性 vs 性能 vs 可维护性
- Bug 修复:根因 vs 绕过 vs 预防
- 重构:最小改动 vs 清洁架构
Phase 3: Review(审查对齐)
目标:审查方案并确保与用户意图一致

行动:
1. 阅读 Phase 2 中标识的关键文件
2. 确认方案与用户原始请求对齐
3. 使用 AskUserQuestion 澄清剩余问题
Phase 4: Final Plan(最终计划)
目标:将最终方案写入计划文件

行动:
- 以 Context 节开头:解释变更原因和预期结果
- 仅包含推荐方案,不列出所有备选
- 包含需要修改的文件路径
- 引用可复用的现有函数及其文件位置
- 包含验证方案:如何端到端测试变更
Phase 5: Call ExitPlanMode(提交审批)
目标:通知用户计划已就绪,等待审批

行动:
- 调用 ExitPlanModeV2Tool
- 用户的每一轮只能以以下两种方式之一结束:
  1. AskUserQuestion -- 需要更多信息
  2. ExitPlanMode -- 计划已就绪,请求审批
- 禁止用文字询问"这个计划可以吗?"-- 必须用 ExitPlanMode

5.2 迭代式规划工作流(Interview Phase)

isPlanModeInterviewPhaseEnabled() 返回 true 时(Ant 内部用户默认启用),系统使用更灵活的迭代工作流。

核心理念:结对规划(Pair Planning)

The Loop — 迭代循环

探索代码

更新计划文件

询问用户
如有必要

首轮行为
1. 快速扫描几个关键文件,形成初步理解
2. 写一个骨架计划(标题 + 粗略笔记)
3. 向用户提出第一轮问题
4. 不要在接触用户之前做详尽探索
提问原则
- 永远不要问通过阅读代码就能找到答案的问题
- 将相关问题批量提出(使用多问题 AskUserQuestion 调用)
- 聚焦只有用户才能回答的问题:需求、偏好、权衡、边界情况
- 根据任务规模调整深度:
  - 模糊的功能请求需要多轮提问
  - 聚焦的 bug 修复可能只需一轮或不需要
收敛条件
计划就绪的标志:
- 所有歧义都已解决
- 计划覆盖:改什么、改哪些文件、复用什么现有代码(含路径)、如何验证
- 此时调用 ExitPlanMode 提交审批

六、深入讲解

6.1 计划文件的存储与管理

文件路径:src/utils/plans.ts

存储位置
默认位置:~/.claude/plans/{slug}.md
自定义位置:通过 settings.json 的 plansDirectory 配置
子代理计划:~/.claude/plans/{slug}-agent-{agentId}.md
Slug 生成

每个会话的计划文件使用一个随机单词 slug 命名:

export function getPlanSlug(sessionId?: SessionId): string {
  const id = sessionId ?? getSessionId()
  const cache = getPlanSlugCache()
  let slug = cache.get(id)

  if (!slug) {
    const plansDir = getPlansDirectory()
    // 最多重试 10 次以避免文件名冲突
    for (let i = 0; i < MAX_SLUG_RETRIES; i++) {
      slug = generateWordSlug()  // 生成随机单词组合
      const filePath = join(plansDir, `${slug}.md`)
      if (!getFsImplementation().existsSync(filePath)) {
        break
      }
    }
    cache.set(id, slug!)
  }
  return slug!
}
路径安全
// 验证自定义路径不会逃逸项目根目录
if (!resolved.startsWith(cwd + sep) && resolved !== cwd) {
  logError(new Error(`plansDirectory must be within project root: ${settingsDir}`))
  plansPath = join(getClaudeConfigHomeDir(), 'plans')  // 降级到默认路径
}
会话恢复

当恢复一个之前的会话时,系统会尝试从以下来源恢复计划:

优先级:
1. 直接读取计划文件(如果文件系统上仍存在)
2. 文件快照(File Snapshot,CCR 远程会话中增量写入)
3. 消息历史(从 ExitPlanMode tool_use、planContent 字段、plan_file_reference 附件中提取)

恢复逻辑在 copyPlanForResume 中实现:

export async function copyPlanForResume(
  log: LogOption,
  targetSessionId?: SessionId,
): Promise<boolean> {
  // ① 从日志中提取 slug(写入缓存)
  const slug = getSlugFromLog(log)
  setPlanSlug(sessionId, slug)

  // ② 尝试直接读取文件
  try {
    await readFile(planPath)
    return true  // 文件存在,恢复成功
  } catch (e) {
    if (!isENOENT(e)) return false
    // 仅远程会话(CCR)需要恢复(本地文件持久存在)
    if (getEnvironmentKind() === null) return false

    // ③ 尝试文件快照恢复
    const snapshotPlan = findFileSnapshotEntry(log.messages, 'plan')
    if (snapshotPlan) {
      await writeFile(planPath, snapshotPlan.content)
      return true
    }

    // ④ 回退到消息历史恢复
    const recovered = recoverPlanFromMessages(log)
    if (recovered) {
      await writeFile(planPath, recovered)
      return true
    }
  }
}
分叉会话的计划管理
// 分叉会话时生成新 slug,防止与原始会话的计划文件冲突
export async function copyPlanForFork(
  log: LogOption,
  targetSessionId: SessionId,
): Promise<boolean> {
  const originalSlug = getSlugFromLog(log)
  // 生成新的 slug(不复用原始 slug)
  const newSlug = getPlanSlug(targetSessionId)
  await copyFile(originalPlanPath, newPlanPath)
}
Slug 生命周期管理
// 设置特定会话的 slug(用于恢复会话)
export function setPlanSlug(sessionId: SessionId, slug: string): void

// 清除当前会话的 slug(/clear 时调用,确保使用新的计划文件)
export function clearPlanSlug(sessionId?: SessionId): void

// 清除所有会话的 slug(/clear 时释放子会话条目)
export function clearAllPlanSlugs(): void
CCR 文件快照持久化
// 在远程会话(CCR)中增量持久化会话文件快照到 transcript
// 每次计划文件变更时调用,确保远程会话恢复时能找回计划
export async function persistFileSnapshotIfRemote(): Promise<void> {
  if (getEnvironmentKind() === null) return  // 本地会话不需要
  const snapshotFiles = []
  const plan = getPlan()
  if (plan) {
    snapshotFiles.push({ key: 'plan', path: getPlanFilePath(), content: plan })
  }
  await recordTranscript([{ type: 'system', subtype: 'file_snapshot', snapshotFiles, ... }])
}

6.2 何时使用 / 何时不使用规划模式

两个版本的 Prompt 差异

源码中有两个版本的 EnterPlanMode 提示词,通过 process.env.USER_TYPE 区分:

外部用户版本(USER_TYPE !== ‘ant’)

更积极地推荐进入规划模式:

"Prefer using EnterPlanMode for implementation tasks unless they're simple."
(除非任务简单,否则优先使用规划模式)

适用条件(满足任一即进入):

  1. 新功能实现
  2. 多种可行方案
  3. 代码修改(影响现有行为)
  4. 架构决策
  5. 多文件变更(3+ 文件)
  6. 需求不明确
  7. 用户偏好重要
Ant 内部版本(USER_TYPE === ‘ant’)

更保守,只在真正必要时才进入:

"Plan mode is valuable when the implementation approach is genuinely unclear."
(只有当实现方案确实不明确时,规划模式才有价值)

不适用条件(满足任一即跳过):

  • 任务简单直接,即使涉及多个文件
  • 用户请求足够具体,实现路径清晰
  • 功能有明显的实现模式(如添加按钮、按现有约定添加端点)
  • Bug 修复(一旦理解了 bug,修复方式就明确了)
  • 用户说"我们来做 X" – 直接开始

6.3 实际场景分析

应进入规划模式的场景:

用户: "给应用添加用户认证"
→ 需要架构决策(JWT vs Session、存储方案、中间件结构)
→ 进入规划模式

用户: "优化数据库查询性能"
→ 多种方案(索引优化、查询重写、缓存策略)
→ 需要先分析瓶颈
→ 进入规划模式

用户: "实现暗黑模式"
→ 影响多个组件,需要主题系统架构决策
→ 进入规划模式

应直接执行的场景:

用户: "修复 README 中的拼写错误"
→ 明确的单行修改
→ 直接执行

用户: "添加一个 console.log 来调试这个函数"
→ 简单明确
→ 直接执行

用户: "哪些文件处理路由?"
→ 纯研究/探索任务,不是实施任务
→ 直接回答(使用 Explore 代理)

6.4 完整示例:规划模式的工作流程

场景:用户请求 “给搜索功能添加缓存”
Step 1: LLM 判断需要规划,调用 EnterPlanMode
LLM 思考:
"这个任务涉及缓存策略选择(内存 vs Redis vs 文件系统)、
缓存失效逻辑、多个文件的修改。应该进入规划模式。"

→ 调用 EnterPlanMode 工具
→ 弹出用户确认对话框:"进入规划模式?"
→ 用户点击 [批准]
Step 2: 系统注入 plan_mode 附件
系统自动在 LLM 上下文中注入以下指令:

"Plan mode is active. 你必须不能进行任何编辑(计划文件除外)、
运行任何非只读工具...

## Phase 1: Initial Understanding
启动 Explore 代理搜索代码库...

## Phase 2: Design
启动 Plan 代理设计实现方案...
..."
Step 3: Phase 1 – 探索
LLM 启动 2 个 Explore 代理(并行):

代理 A:
  搜索目标:找到搜索功能的实现位置
  → 发现 src/services/search.ts 中的 search() 函数
  → 发现调用链:routes/api.ts → searchController.ts → search.ts

代理 B:
  搜索目标:找到现有的缓存模式和工具
  → 发现 src/utils/cache.ts 中的 LRUCache 类
  → 发现 Redis 配置在 config/redis.ts
  → 发现另一个模块使用了内存缓存:src/services/recommendations.ts
Step 4: Phase 2 – 设计
LLM 启动 1 个 Plan 代理:

Plan 代理接收到的上下文:
- 搜索功能位于 src/services/search.ts
- 已有 LRUCache 工具可复用
- Redis 已配置但未广泛使用
- recommendations.ts 中的缓存模式可作为参考

Plan 代理输出方案:
1. 扩展 LRUCache 以支持 TTL
2. 在 search() 函数中添加缓存层
3. 添加缓存失效钩子
Step 5: Phase 3 – 审查
LLM 阅读 src/services/search.ts 和 src/utils/cache.ts
确认方案可行且与现有代码模式一致。

使用 AskUserQuestion 询问用户:
"缓存策略偏好:
A) 内存缓存(LRU,简单,重启后失效)
B) Redis(持久化,需要额外依赖)
C) 混合方案(内存 + Redis 后备)"
Step 6: Phase 4 – 写入计划
LLM 将最终计划写入 ~/.claude/plans/quick-fox.md:

# 搜索功能缓存实现计划

## Context
搜索功能在高并发下存在性能瓶颈,需要添加缓存层
降低数据库查询压力。

## 实现方案
采用内存 LRU 缓存,复用现有 LRUCache 工具类。

### 修改文件
- src/utils/cache.ts -- 添加 TTL 支持到 LRUCache
- src/services/search.ts -- 在 search() 中添加缓存层
- src/services/search.test.ts -- 添加缓存相关测试

### 复用的现有代码
- LRUCache 类 (src/utils/cache.ts:15)
- serializeQuery() 工具函数 (src/utils/query.ts:8)

### 验证
运行 bun test src/services/search.test.ts
Step 7: Phase 5 – 提交审批
LLM 调用 ExitPlanModeV2Tool

→ 弹出用户审批对话框,显示计划内容
→ 用户可以:
  a) [批准] -- 退出规划模式,开始编码
  b) [编辑] -- 修改计划后批准
  c) [拒绝] -- 返回规划模式继续调整
Step 8: 退出规划模式,开始实施
用户批准后:

1. 权限模式从 'plan' 恢复为 'default'
2. 系统注入 plan_mode_exit 附件告知 LLM:
   "已退出规划模式。计划已保存到 ~/.claude/plans/quick-fox.md"
3. LLM 按计划开始编码实施

七、关键文件索引

文件路径职责
src/tools/EnterPlanModeTool/EnterPlanModeTool.ts进入规划模式的工具实现
src/tools/EnterPlanModeTool/constants.ts工具名称常量
src/tools/EnterPlanModeTool/prompt.ts进入规划模式的 LLM 提示词(外部版/Ant版)
src/tools/EnterPlanModeTool/UI.tsx进入规划模式的 TUI 渲染组件
src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts退出规划模式的工具实现(含团队审批、模式恢复)
src/tools/ExitPlanModeTool/constants.ts工具名称常量
src/tools/ExitPlanModeTool/prompt.ts退出规划模式的 LLM 提示词
src/tools/ExitPlanModeTool/UI.tsx退出规划模式的 TUI 渲染组件(计划审批对话框)
src/utils/planModeV2.ts规划模式增强功能(代理数量、Interview Phase 开关、PewterLedger 实验)
src/utils/plans.ts计划文件存储和管理(slug生成、会话恢复、分叉复制、快照持久化)
src/utils/permissions/permissionSetup.ts权限模式切换和上下文准备(prepareContextForPlanMode、transitionPlanAutoMode)
src/utils/permissions/autoModeState.tsAuto 模式状态管理(激活/停用分类器)
src/utils/messages.tsplan_mode 附件指令(五阶段/迭代式工作流)
src/utils/teammate.ts团队协作工具(isTeammate、isPlanModeRequired)
src/utils/teammateMailbox.ts团队邮箱通信(writeToMailbox)
src/bootstrap/state.ts全局状态管理(模式转换副作用、slug 缓存)

本文档基于 Claude Code 源码逆向还原版本编写,部分模块使用了兼容性填充(shim),但规划模式的核心架构和行为与原版一致。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐