Claude Code 源码解读之 规划模式
04 - Claude Code Plan System
一、概念解释
为什么需要规划模式
复杂软件任务通常涉及多个文件的修改、架构层面的决策,以及多种可行方案的权衡。如果 LLM 直接开始编码,可能会:
- 方向错误:选择了不合适的架构方案,导致大量返工
- 遗漏关键上下文:没有充分理解现有代码模式就开始修改
- 用户期望不一致:实现结果与用户心目中的方案存在偏差
- 影响面失控:修改波及范围超出预期
设计哲学
Plan 模式的核心思想是 “先想后做”(Think Before You Act):
直接执行模式: 用户请求 → 立即编码 → 可能返工
规划模式: 用户请求 → 探索理解 → 方案设计 → 用户审批 → 编码实施
它将 “理解问题” 和 “实施方案” 解耦为两个独立阶段,中间加入了用户审批这一安全阀。
二、核心流程图
规划模式的生命周期
状态流转
在 toolPermissionContext.mode 中,模式值有以下几种:
| 模式 | 含义 | 允许的操作 |
|---|---|---|
default | 正常交互模式 | 所有操作(需用户逐步确认) |
plan | 规划模式 | 只读操作 + 计划文件编辑 |
auto | 自动模式 | 分类器自动审批操作 |
bypassPermissions | 绕过权限 | 所有操作自动通过 |
规划模式 vs 直接执行模式的权衡
| 维度 | 规划模式 | 直接执行模式 |
|---|---|---|
| 安全性 | 高 – 用户在编码前审批方案 | 中 – 逐步确认每个操作 |
| 效率 | 低 – 需要额外的探索和审批时间 | 高 – 直接开始工作 |
| 返工风险 | 低 – 方案已对齐 | 中高 – 方向错误时需要重来 |
| 适用任务 | 新功能、重构、架构决策、多文件变更 | 简单修复、明确的小改动 |
| token 消耗 | 较高 – 需要探索和设计阶段 | 较低 – 直接编码 |
| 用户体验 | 更可控但更慢 | 更快但更依赖信任 |
| 代码质量 | 更好 – 经过充分思考 | 取决于任务复杂度 |
决策流程图
三、解决什么问题?
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,
})
关键设计要点:
shouldDefer: true– 此工具的调用不会立即执行,而是弹出用户确认对话框。用户必须明确同意才能进入规划模式。- 子代理限制 –
context.agentId检查确保只有在主会话中才能进入规划模式,防止后台代理陷入无法退出的状态。 - 不可变状态更新 – 使用
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 },
})
关键设计要点:
- 模式恢复机制 –
prePlanMode字段暂存进入规划前的模式,退出时精确恢复。 - 熔断器保护 – 如果 auto 模式在规划期间被禁用(通过 GrowthBook 配置),退出时不会恢复到 auto,而是降级到 default,并通过
addNotification通知用户。 - 计划内容双通道 – 支持从磁盘读取和从用户编辑结果(CCR Web UI)两个来源获取计划内容。
- 团队协作 – 在多代理团队中,计划需要通过
writeToMailbox发送给队长审批,包含requestId用于跟踪。 - auto 使用追踪 – 通过
autoWasUsedDuringPlan追踪规划期间是否实际使用了 auto 模式,决定是否触发 auto 退出附件。 - 输入校验 –
validateInput()防止 compact/clear 后模型在非 plan 模式下重复调用此工具。 - 非只读 –
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 权限流转图
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)
首轮行为
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."
(除非任务简单,否则优先使用规划模式)
适用条件(满足任一即进入):
- 新功能实现
- 多种可行方案
- 代码修改(影响现有行为)
- 架构决策
- 多文件变更(3+ 文件)
- 需求不明确
- 用户偏好重要
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.ts | Auto 模式状态管理(激活/停用分类器) |
src/utils/messages.ts | plan_mode 附件指令(五阶段/迭代式工作流) |
src/utils/teammate.ts | 团队协作工具(isTeammate、isPlanModeRequired) |
src/utils/teammateMailbox.ts | 团队邮箱通信(writeToMailbox) |
src/bootstrap/state.ts | 全局状态管理(模式转换副作用、slug 缓存) |
本文档基于 Claude Code 源码逆向还原版本编写,部分模块使用了兼容性填充(shim),但规划模式的核心架构和行为与原版一致。
更多推荐


所有评论(0)