本教程教你在 Claude Code 里用 Plan 模式(Plan Mode) 完成"先勘探、出方案、再动手"的大重构,把返工率压到接近为零。你将掌握三幕剧式工作流:只读勘探代码库 → 写方案文件并提交审批(ExitPlanMode)→ 审批通过后用 TodoWrite 逐步落地,再配 PostToolUse/Stop 钩子自动跑 lint 和测试。适合做过几个 Claude Code 小项目、想挑战多文件重构的中级读者。
ExitPlanMode 在 2025 年成熟、2026 年仍为主线机制)git 基本操作有了解(重构前先 git commit 打快照,便于回滚)Claude Code 有三种权限模式(permission mode),用 Shift+Tab 在 CLI 里循环切换,或用 --permission-mode 启动参数指定:
| 模式 | 读文件 | 改文件 / 跑命令 | 典型用途 |
|---|---|---|---|
| Default(默认) | 自由 | 每次改动前询问你 | 日常交互开发 |
| Auto-Accept(自动接受) | 自由 | 自动放行,不再问 | 你盯着看、信任度高的批量活 |
| Plan(计划) | 自由 | 一律禁止 | 勘探、分析、出方案 |
Plan 模式的核心契约是:模型只能读、搜、想,不能写、不能编辑、不能跑会改东西的命令。 它把 Claude 锁进一个"只读沙箱",逼它先把问题想透,把方案落到一段你可以否决的文字里,而不是一堆你得回滚的坏文件。
一句话记住:Plan 模式下,最便宜的 bug 还停留在"一句话"阶段——你点否决就完了,成本为零;出了 Plan 模式再发现的错,就是一堆要手工理清的烂摊子。
进入 Plan 模式有三种姿势:
Shift+Tab,直到底部状态栏显示 plan mode on。EnterPlanMode 工具把自己切进去——尤其当它原本想用 AskUserQuestion 问你方案选型时,切 Plan 模式更合适。claude -p "重构思路" --permission-mode plan,让 Claude 在 CI 或脚本里只产出方案文本、不动文件。进入 Plan 模式后,给 Claude 一个目标导向而非步骤导向的指令。差的指令把活儿规定了,好的指令把"要解决什么"说清、把决策权留给模型。
以本教程的实战案例——把项目里散落的 console.log 统一替换为结构化日志(pino)——为例:
进入 plan 模式。目标:把整个 src/ 下所有 console.log / console.error
统一迁移到 pino 结构化日志。先勘探:列出所有调用点,按 debug/info/warn/error
分类,搞清楚哪些是真正要保留的错误日志、哪些是临时调试残留。
不要改任何文件,勘探完给我一份迁移方案。
模型会动用三类只读工具:
Glob:按模式找文件,例如 src/**/*.jsGrep:内容搜索,定位 console.log 调用点,支持 -n 行号、output_mode: "count" 统计Read:读具体文件,理解上下文(这行 log 在哪个函数里、记的是请求参数还是异常)勘探阶段你应该看到类似这样的工具调用链:
Grep pattern="console\.(log|error|warn|info)"
glob="src/**/*.js" output_mode="content" -n
→ 命中 47 处,分布在 12 个文件
Glob pattern="src/**/*.js"
→ 列出 28 个源文件
Read file_path="src/services/order.js" (理解 error 分支)
Read file_path="package.json" (确认是否已装 pino)
模型最终会产出一张调用点清单,类似:
src/routes/user.js :12 console.log('login', userId) → info
src/routes/user.js :45 console.error('auth fail', err) → error(保留)
src/services/order.js :88 console.log('debug stock') → debug(疑似残留)
... 共 47 处
Shift+Tab。src/routes/,再 src/services/,分批进 Plan 模式。勘探完,模型会把方案写进一个计划文件(plan file),然后调用 ExitPlanMode 工具,把方案端到你面前审批。这一步是整个工作流的咽喉——你不点头,它一行代码都改不了。
一份合格的方案文件通常包含:
git revert / 备份分支)npm test / npm run lint)迁移规则示例表(方案里应该有):
| 原调用 | 目标 | 规则 |
|---|---|---|
console.log('xxx') 普通信息 |
logger.info(...) |
包成结构化对象 |
console.log('debug ...') 调试残留 |
删除 | 标注后删,不留 |
console.error(...) 真实错误 |
logger.error({err}) |
保留并结构化 |
console.warn(...) |
logger.warn(...) |
同上 |
方案里常会冒出需要你拍板的岔路。与其让模型瞎猜,不如在 ExitPlanMode 前先用 AskUserQuestion 问清楚。比如:
AskUserQuestion
Q: "src/legacy/ 下的 13 处 console.log 是否一并迁移?"
选项:
- 一并迁移(推荐):保持全项目一致
- 跳过 legacy:该目录即将废弃,不值得动
- 只迁移 error 级别:最小风险
经验法则:只要方案里有一个"二选一"会影响后面十几个文件改动,就先问、再出方案。Plan 模式的
EnterPlanMode触发条件之一,正是"原本要问选型问题"——把提问和方案合并到一起,最省往返。
ExitPlanMode 不只是"请批准",它还顺带声明执行阶段需要的 Bash 权限(allowedPrompts)。这样审批一过,模型跑 npm install pino、npm test 时不必再逐条问你。模型会写成类似:
{
"allowedPrompts": [
{ "tool": "Bash", "prompt": "install npm dependencies" },
{ "tool": "Bash", "prompt": "run tests" },
{ "tool": "Bash", "prompt": "run linter and formatter" }
]
}
审批弹窗里你会看到完整方案 + 权限清单。此时你有三种选择:
Bash(*))。
解决:打回收紧到具体子动作(install/test/lint),不要给空白支票。批准后,模型退出 Plan 模式,开始落地。关键纪律:用 TodoWrite 把方案拆成可见的任务清单,每改一个文件勾一项,你和模型都能看到进度。
一个 47 处调用的迁移,任务清单可能是:
☐ 安装 pino(npm i pino)
☐ 新建 src/lib/logger.js(统一出口)
☐ 迁移 src/routes/(3 文件,18 处)
☐ 迁移 src/services/(5 文件,22 处)
☐ 迁移 src/legacy/(4 文件,13 处)— 仅 error 级
☐ 全局 grep 复查残留 console.*
☐ 跑 npm test 验证
执行阶段最容易翻车的是"改完忘了跑测试"。用两个钩子把质量门焊死。在 .claude/settings.json 里加:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_FILE_PATHS\" 2>/dev/null; true"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "npm test --silent" }
]
}
]
}
}
关于
Stop钩子的退码语义:返回非零退出码会把输出回灌给模型,等于"逼它继续修";返回 0 则静默通过。想让失败自动触发修复,就让钩子在测试失败时exit 1。
执行过程会看到任务逐个从 ☐ 变 ☑,文件被 Edit 逐个改写,每个改动后 Prettier 自动格式化,阶段性停下时 npm test 自动跑。最终一次 Stop 时测试全绿,任务清单全勾。
Edit src/lib/logger.js ✅ + PostToolUse → prettier
Edit src/routes/user.js ✅ + prettier
...
TodoWrite 全部 ☑
Stop → npm test PASS (23 specs)
TodoWrite 清单就是纠偏锚点。提示"对照任务清单,继续第 4 项"。Stop 钩子命令加 --silent 或重定向,只在失败时吐详细信息。git checkout src/legacy/ 退回,回到 Plan 模式补方案。把三幕串起来。准备一个最小 Node.js 项目:
mkdir plan-refactor-demo && cd plan-refactor-demo
npm init -y
npm i express
mkdir -p src/routes src/services
造两个带"脏日志"的文件:
// src/routes/user.js
import express from 'express'
const router = express.Router()
router.post('/login', (req, res) => {
const userId = req.body.id
console.log('login', userId) // 临时调试残留
try {
auth(userId)
} catch (err) {
console.error('auth fail', err) // 真实错误,要保留
res.status(500).end()
}
res.json({ ok: true })
})
export default router
// src/services/order.js
export function placeOrder(items) {
console.log('debug stock', items) // 调试残留
// ...下单逻辑
}
第一幕:Shift+Tab 切到 plan mode,贴上文那段勘探指令。模型 grep 出 3 处,分类:1 个 error(保留)、2 个 debug 残留(删/转)。
第二幕:模型出方案——建 src/lib/logger.js、user.js 的 log 转 logger.info、error 转 logger.error({err})、order.js 的 debug 直接删;执行顺序 routes → services;验证 node -e "require('./src/routes/user.js')"。带 AskUserQuestion 问"order.js 的 debug 是删还是转?",你选"删"。ExitPlanMode 递上 allowedPrompts: [install, run]。你批准。
第三幕:模型 TodoWrite 建清单,逐个 Edit,PostToolUse 自动 Prettier,最后 Stop 钩子跑 node --check 全过。最终 src/lib/logger.js:
import pino from 'pino'
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: { target: 'pino-pretty' }
})
user.js 里 console.log('login', userId) → logger.info({ userId }, 'login');console.error('auth fail', err) → logger.error({ err }, 'auth fail')。order.js 删掉 debug 行。三处全清,测试通过,零返工。
flowchart TD
A["用户给目标指令"] --> B{"权限模式?"}
B -->|默认/自动| C["模型判断复杂度"]
C -->|"够复杂/要问选型"| D["EnterPlanMode"]
C -->|简单| Z["直接 Default 执行"]
B -->|手动 Shift+Tab| D
D --> E["第一幕:勘探只读"]
subgraph ACT1["第一幕 勘探(只读沙箱)"]
E --> E1["Glob 找文件"]
E1 --> E2["Grep 定位调用点"]
E2 --> E3["Read 理解上下文"]
E3 --> E4["输出调用点清单"]
end
E4 --> F["第二幕:提案"]
subgraph ACT2["第二幕 提案 + 审批"]
F --> F1["写 plan 文件<br/>规则/顺序/回滚/验证"]
F1 --> F2{"有分叉?"}
F2 -->|是| F3["AskUserQuestion 问选型"]
F2 -->|否| F4["ExitPlanMode<br/>+ allowedPrompts"]
F3 --> F4
F4 --> F5{"用户审批"}
end
F5 -->|打回| E
F5 -->|批准| G["第三幕:执行"]
subgraph ACT3["第三幕 执行 + Hook 看门"]
G --> G1["TodoWrite 建任务清单"]
G1 --> G2["Edit 逐文件改"]
G2 --> G3["PostToolUse → Prettier"]
G3 --> G4["Stop → npm test"]
G4 --> G5{"测试绿?"}
G5 -->|红| G2
G5 -->|绿| H["全清单 ☑ 完成"]
end
Plan 模式不只是交互玩具。用 -p 把它接进 CI/脚本,让 Claude 在不碰文件的前提下产出方案,挂到 PR 评论里供人评审:
# CI 里:针对本次 diff 产出重构方案,只读、零副作用
claude -p "$(cat <<'EOF'
进入 plan 模式。分析本次 PR 的 git diff,针对 src/ 下被改动的文件,
提出进一步的可读性与性能优化方案。不要修改任何文件,只输出方案。
EOF
)" --permission-mode plan > .claude/plan.md
由于 --permission-mode plan 强制只读,即便 prompt 里误写了"请修改",模型也无法落笔——权限护栏在模型之上,是最后一道闸。产出的 plan.md 可由后续步骤贴进 PR、或交给一次 Auto-Accept 的执行轮次去落地。
把"出方案"和"动手"拆成两个独立进程:第一进程
--permission-mode plan只读产出方案,人审;第二进程拿到批准的方案再用 Default/Auto-Accept 执行。这样既享自动化、又不失人审关卡,是大型重构的稳妥姿势。
Shift+Tab,看到 plan mode on 再开口。AskUserQuestion,别让它替你拍板。Bash(*),收紧到 install/test/lint 等具名子动作。TodoWrite,没有可见清单的重构等于盲飞。Stop 钩子跑测试,用非零退码逼模型自愈。--permission-mode plan,否则 prompt 笔误就可能真改文件。https://code.claude.com/docs/en/permission-modes——三种模式的权威说明与 Shift+Tab 切换https://code.claude.com/docs/en/interactive-mode——键盘快捷键与输入模式全表https://deepwiki.com/windowdotonload/claude-code/4.4-plan-mode-and-exit-plan-mode——源码视角的 Plan 模式机制解析https://www.anyonebuilds.com/guides/claude-code-plan-mode——2026 年视角的实战指南https://claudeai.dev/docs/mechanics/foundation/plan-mode/——复杂多步任务的处理流程拆解[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
