<!-- title: Plan模式造零返工的大重构 -->

# Claude Code Plan 模式造零返工的大重构

## 概述

本教程教你在 Claude Code 里用 **Plan 模式（Plan Mode）** 完成"先勘探、出方案、再动手"的大重构，把返工率压到接近为零。你将掌握三幕剧式工作流：只读勘探代码库 → 写方案文件并提交审批（`ExitPlanMode`）→ 审批通过后用 `TodoWrite` 逐步落地，再配 `PostToolUse`/`Stop` 钩子自动跑 lint 和测试。适合做过几个 Claude Code 小项目、想挑战多文件重构的中级读者。

## 前置条件

- 已安装 Claude Code（建议较新版本，Plan 模式与 `ExitPlanMode` 在 2025 年成熟、2026 年仍为主线机制）
- 手头有一个**可被重构的真实多文件项目**（JS/TS/Python 均可），本教程以 Node.js 项目为例
- 一个终端，一个编辑器，约 30–45 分钟
- 对 `git` 基本操作有了解（重构前先 `git commit` 打快照，便于回滚）

---

## 一、先搞懂：Plan 模式到底限制了什么

Claude Code 有三种**权限模式（permission mode）**，用 `Shift+Tab` 在 CLI 里循环切换，或用 `--permission-mode` 启动参数指定：

| 模式 | 读文件 | 改文件 / 跑命令 | 典型用途 |
|------|--------|-----------------|----------|
| Default（默认） | 自由 | 每次改动前询问你 | 日常交互开发 |
| Auto-Accept（自动接受） | 自由 | 自动放行，不再问 | 你盯着看、信任度高的批量活 |
| **Plan（计划）** | **自由** | **一律禁止** | 勘探、分析、出方案 |

Plan 模式的核心契约是：**模型只能读、搜、想，不能写、不能编辑、不能跑会改东西的命令。** 它把 Claude 锁进一个"只读沙箱"，逼它先把问题想透，把方案落到一段你可以否决的文字里，而不是一堆你得回滚的坏文件。

> 一句话记住：Plan 模式下，最便宜的 bug 还停留在"一句话"阶段——你点否决就完了，成本为零；出了 Plan 模式再发现的错，就是一堆要手工理清的烂摊子。

进入 Plan 模式有三种姿势：

1. **手动**：在交互式 CLI 里连按 `Shift+Tab`，直到底部状态栏显示 `plan mode on`。
2. **半自动**：模型自己判断"这活儿够复杂、得先勘探"，调用 `EnterPlanMode` 工具把自己切进去——尤其当它原本想用 `AskUserQuestion` 问你方案选型时，切 Plan 模式更合适。
3. **无头（headless）**：`claude -p "重构思路" --permission-mode plan`，让 Claude 在 CI 或脚本里只产出方案文本、不动文件。

---

## 二、第一幕：勘探（只读摸底）

### 操作说明

进入 Plan 模式后，给 Claude 一个**目标导向而非步骤导向**的指令。差的指令把活儿规定了，好的指令把"要解决什么"说清、把决策权留给模型。

以本教程的实战案例——**把项目里散落的 `console.log` 统一替换为结构化日志（pino）**——为例：

```text
进入 plan 模式。目标：把整个 src/ 下所有 console.log / console.error
统一迁移到 pino 结构化日志。先勘探：列出所有调用点，按 debug/info/warn/error
分类，搞清楚哪些是真正要保留的错误日志、哪些是临时调试残留。
不要改任何文件，勘探完给我一份迁移方案。
```

模型会动用三类**只读工具**：

- `Glob`：按模式找文件，例如 `src/**/*.js`
- `Grep`：内容搜索，定位 `console.log` 调用点，支持 `-n` 行号、`output_mode: "count"` 统计
- `Read`：读具体文件，理解上下文（这行 log 在哪个函数里、记的是请求参数还是异常）

### 预期结果

勘探阶段你应该看到类似这样的工具调用链：

```text
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 处
```

### 常见问题与解决

- **问题：模型勘探得太浅，只 grep 了一层目录。**
  解决：在指令里点明递归范围与分类维度。比如追加"包括子目录、按日志级别分类、标注疑似调试残留"。
- **问题：模型想直接动手改。**
  解决：确认你确实在 Plan 模式（底部状态栏）。若仍想改，说明权限模式没切对——重按 `Shift+Tab`。
- **问题：项目太大，勘探一次烧很多 token。**
  解决：把勘探范围切片，先 `src/routes/`，再 `src/services/`，分批进 Plan 模式。

---

## 三、第二幕：提案（写方案 + ExitPlanMode 审批）

### 操作说明

勘探完，模型会把方案写进一个**计划文件（plan file）**，然后调用 `ExitPlanMode` 工具，把方案端到你面前审批。这一步是整个工作流的咽喉——**你不点头，它一行代码都改不了。**

一份合格的方案文件通常包含：

1. **目标与范围**：改什么、不改什么、边界在哪
2. **调用点分析结论**：勘探发现的分类汇总
3. **迁移规则**：每类调用怎么转（见下表）
4. **执行顺序**：按文件/模块的依赖顺序，先底层后上层
5. **回滚策略**：出问题怎么退（`git revert` / 备份分支）
6. **验证方式**：跑什么命令确认没改坏（`npm test` / `npm run lint`）

迁移规则示例表（方案里应该有）：

| 原调用 | 目标 | 规则 |
|--------|------|------|
| `console.log('xxx')` 普通信息 | `logger.info(...)` | 包成结构化对象 |
| `console.log('debug ...')` 调试残留 | **删除** | 标注后删，不留 |
| `console.error(...)` 真实错误 | `logger.error({err})` | 保留并结构化 |
| `console.warn(...)` | `logger.warn(...)` | 同上 |

### 遇到分叉：用 AskUserQuestion 而不是猜

方案里常会冒出**需要你拍板的岔路**。与其让模型瞎猜，不如在 `ExitPlanMode` 前先用 `AskUserQuestion` 问清楚。比如：

```text
AskUserQuestion
  Q: "src/legacy/ 下的 13 处 console.log 是否一并迁移？"
  选项:
    - 一并迁移（推荐）：保持全项目一致
    - 跳过 legacy：该目录即将废弃，不值得动
    - 只迁移 error 级别：最小风险
```

> 经验法则：只要方案里有一个"二选一"会影响后面十几个文件改动，就先问、再出方案。Plan 模式的 `EnterPlanMode` 触发条件之一，正是"原本要问选型问题"——把提问和方案合并到一起，最省往返。

### ExitPlanMode：把权限清单一起递上去

`ExitPlanMode` 不只是"请批准"，它还顺带声明**执行阶段需要的 Bash 权限**（`allowedPrompts`）。这样审批一过，模型跑 `npm install pino`、`npm test` 时不必再逐条问你。模型会写成类似：

```json
{
  "allowedPrompts": [
    { "tool": "Bash", "prompt": "install npm dependencies" },
    { "tool": "Bash", "prompt": "run tests" },
    { "tool": "Bash", "prompt": "run linter and formatter" }
  ]
}
```

### 预期结果

审批弹窗里你会看到完整方案 + 权限清单。此时你有三种选择：

- **批准**：进入第三幕执行
- **打回**：提修改意见（"legacy 也一起迁"、"先别删 debug，先注释"），模型回勘探/改方案
- **直接改 prompt**：换方向重来

### 常见问题与解决

- **问题：方案太笼统，像"我会逐个文件替换"。**
  解决：打回，要求它给出**每类调用的转换规则 + 文件执行顺序 + 验证命令**。模糊方案等于没方案。
- **问题：方案里漏了回滚策略。**
  解决：明确要求"包含 git 快照与回滚步骤"。没有退路的重构是定时炸弹。
- **问题：权限清单太宽（比如请求 `Bash(*)`）。**
  解决：打回收紧到具体子动作（install/test/lint），不要给空白支票。

---

## 四、第三幕：执行（TodoWrite + Hook 看门）

### 操作说明

批准后，模型退出 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 验证
```

### 配 Hook 做自动看门

执行阶段最容易翻车的是"改完忘了跑测试"。用两个钩子把质量门焊死。在 `.claude/settings.json` 里加：

```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" }
        ]
      }
    ]
  }
}
```

- **PostToolUse（matcher 匹配 Edit/Write）**：每次模型改完文件，自动跑 Prettier 格式化，省去手动对齐。
- **Stop（无 matcher，匹配 Agent 每次停下）**：模型每次告一段落，自动跑测试。测试挂了，模型看到失败输出会接着自愈修复。

> 关于 `Stop` 钩子的退码语义：返回非零退出码会把输出回灌给模型，等于"逼它继续修"；返回 0 则静默通过。想让失败自动触发修复，就让钩子在测试失败时 `exit 1`。

### 预期结果

执行过程会看到任务逐个从 `☐` 变 `☑`，文件被 `Edit` 逐个改写，每个改动后 Prettier 自动格式化，阶段性停下时 `npm test` 自动跑。最终一次 `Stop` 时测试全绿，任务清单全勾。

```text
Edit src/lib/logger.js      ✅ + PostToolUse → prettier
Edit src/routes/user.js     ✅ + prettier
...
TodoWrite  全部 ☑
Stop → npm test  PASS (23 specs)
```

### 常见问题与解决

- **问题：模型执行到一半偏离方案。**
  解决：`TodoWrite` 清单就是纠偏锚点。提示"对照任务清单，继续第 4 项"。
- **问题：Hook 把测试输出刷屏。**
  解决：`Stop` 钩子命令加 `--silent` 或重定向，只在失败时吐详细信息。
- **问题：改完才发现某文件不该动。**
  解决：依赖第二幕的回滚策略——`git checkout src/legacy/` 退回，回到 Plan 模式补方案。

---

## 五、实战案例：console.log → pino 全量迁移（完整可跑）

把三幕串起来。准备一个最小 Node.js 项目：

```bash
mkdir plan-refactor-demo && cd plan-refactor-demo
npm init -y
npm i express
mkdir -p src/routes src/services
```

造两个带"脏日志"的文件：

```javascript
// 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
```

```javascript
// 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`：

```javascript
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 行。三处全清，测试通过，零返工。

---

## 六、Mermaid 流程图：Plan 模式三幕与工具流转

```mermaid
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 模式接进流水线

Plan 模式不只是交互玩具。用 `-p` 把它接进 CI/脚本，让 Claude 在**不碰文件**的前提下产出方案，挂到 PR 评论里供人评审：

```bash
# 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 执行。这样既享自动化、又不失人审关卡，是大型重构的稳妥姿势。

---

## 八、常见坑与对策汇总

1. **勘探即动手**：忘了切 Plan 模式。养成习惯——复杂任务先连按两次 `Shift+Tab`，看到 `plan mode on` 再开口。
2. **方案过粗**：打回时明确要求"每类转换规则 + 文件顺序 + 回滚 + 验证"四件套。
3. **不问就猜**：见到二选一就要求模型先 `AskUserQuestion`，别让它替你拍板。
4. **权限清单过宽**：拒绝 `Bash(*)`，收紧到 install/test/lint 等具名子动作。
5. **执行无清单**：强制 `TodoWrite`，没有可见清单的重构等于盲飞。
6. **改完不验证**：`Stop` 钩子跑测试，用非零退码逼模型自愈。
7. **无头模式忘了加 plan**：CI 里务必带 `--permission-mode plan`，否则 prompt 笔误就可能真改文件。

---

## 延伸阅读

- Claude Code 官方文档 · 权限模式：`https://code.claude.com/docs/en/permission-modes`——三种模式的权威说明与 `Shift+Tab` 切换
- Claude Code 官方文档 · 交互模式：`https://code.claude.com/docs/en/interactive-mode`——键盘快捷键与输入模式全表
- Plan Mode & Exit Plan Mode（DeepWiki）：`https://deepwiki.com/windowdotonload/claude-code/4.4-plan-mode-and-exit-plan-mode`——源码视角的 Plan 模式机制解析
- Claude Code Plan Mode 完整指南：`https://www.anyonebuilds.com/guides/claude-code-plan-mode`——2026 年视角的实战指南
- Plan Mode（claudeai.dev）：`https://claudeai.dev/docs/mechanics/foundation/plan-mode/`——复杂多步任务的处理流程拆解

---

## 训练营适用性评估

- **适合类型**：中级训练营、重构实战营、AI 辅助工程效能营；不适合纯零基础（需先理解 Claude Code 基本交互）
- **预计教学时长**：讲授 25 分钟 + 动手 30 分钟（含三幕演练 + Hook 配置）≈ 1 课时
- **学员产出**：每人完成一个真实小项目的 Plan 模式重构，附带方案文件与 TodoWrite 清单截图
- **考核点**：能否独立完成"勘探→带 AskUserQuestion 的方案→ExitPlanMode 审批→TodoWrite 执行→测试全绿"全链路，且方案包含回滚与验证
- **进阶衔接**：本教程的"无头 Plan 接 CI"一节，可自然过渡到「Claude Code 入 CI 造自动 PR 审查机器人」一类自动化主题

## 参考来源

1. [Choose a permission mode — Claude Code Docs](https://code.claude.com/docs/en/permission-modes) — 官方对 Default / Auto-Accept / Plan 三种模式与 Shift+Tab 切换的权威说明
2. [Plan Mode & Exit Plan Mode — DeepWiki](https://deepwiki.com/windowdotonload/claude-code/4.4-plan-mode-and-exit-plan-mode) — 从源码视角解析 Plan 模式只读沙箱与 ExitPlanMode 审批机制
3. [Claude Code Plan Mode: Get a Plan Before It Touches Your Code — Clearly](https://www.clearly.sh/blog/claude-code-plan-mode) — 实战角度阐释"先出方案再动手"的价值与用法
4. [Claude Code Plan Mode: The Complete Guide (2026) — anyonebuilds](https://www.anyonebuilds.com/guides/claude-code-plan-mode) — 2026 年视角的 Shift+Tab 进入、何时使用与配套技巧
5. [pi-claude-code/docs/plan-mode.md — GitHub](https://github.com/fractary/pi-claude-code/blob/main/docs/plan-mode.md) — 只读分析、先规划后执行的社区整理文档

> [!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 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
