第 6 章:实战开发

核心功能实现

多轮迭代开发

6.2 节落幕时,工程的"骨架"已经立起来了:仓库、tsconfig、ESLint、CLAUDE.md、Skills、CI 流水线。这一刻,团队会面临一个比脚手架阶段更难的问题——如何把"骨架"长出"血肉"。在传统模式下,这意味着接下来 2 到 8 周的功能开发会反复经历"写代码 → 跑测试 → 调试 → 改代码"的循环;而在 Claude Code 的协作模式下,这个循环的颗粒度被压缩到分钟级别,每一次对话都是一次"假设 → 验证 → 提交"的迭代闭环(Iterative Loop)。这种节奏的转变并非锦上添花,而是工程文化层面的根本重构。

本节要回答的问题不是"Claude 能不能写代码",而是"如何让多轮迭代成为团队稳定的肌肉记忆"。我们会拆解迭代的最小有效单元、人机分工的边界、典型反模式,最后落到一份可以立即套用的"6 轮迭代用户管理模块"的实战脚本。读完这一节,工程师应当能直接把方法论搬到自己的项目里、产品经理应当能向团队解释"为什么我们的开发节奏看起来又快又稳"。

需要先澄清一个常见误解:多轮迭代不是"迭代万能论"。有些场景多轮迭代非但不增效,反而会拖慢节奏:

  • 一次性脚本:例如"读 CSV、清洗 100 条数据、输出 JSON"。需求清晰、生命周期短、不需要演进——这种场景一次成型即可,多轮迭代是过度工程。
  • 强依赖外部规范的代码:例如实现一份既定的 RFC 协议。规范本身就是"完成定义",让 Claude 一次贴着规范实现,再用对照测试验证即可。
  • 机器学习管线(Pipeline)的中间步骤:例如"把 embedding 模型从 v1 切到 v2"。这种迁移有明确的等价关系约束,分多轮迭代反而会引入"半新半旧"的不一致状态。

适合多轮迭代的场景特征是:需求带歧义、决策有空间、上下文会演化——典型如业务功能开发、领域建模、UI 交互、API 契约设计。本节聚焦的"核心功能实现"正是多轮迭代的甜蜜点(Sweet Spot),所有的方法论都建立在这个前提之上。读者在套用之前,先判断自己的场景是否落在这个范围内。


一、为什么核心功能需要多轮迭代

1.1 一次成型 vs 多轮迭代的代价对比

软件工程史上,"一次把功能写对"的诱惑从未消失。瀑布模型(Waterfall)、详尽的 PRD、长达数月的开发周期,本质上都是"赌一次写对"。但凡有过交付经验的工程师都知道——这种赌局长期负收益

维度一次成型(Big Bang)多轮迭代(Iterative Loop)
单次提交的代码量通常 ≥ 500 行通常 ≤ 80 行
单次审查耗时≥ 30 分钟≤ 5 分钟
Bug 定位平均成本高,需要回溯整段逻辑低,最近一次提交即嫌疑人
反向重构的代价几乎无法局部回滚可单提交 revert
Claude 的上下文消耗一次塞入大块代码,token 浪费小步快跑,每轮聚焦
失败的心理成本高,"白干两天"低,"再来一轮"

Claude Code 把每一轮迭代的边际成本压到了"一次对话 + 一次 git commit"的程度。这意味着多轮迭代不再是工程美德,而是工程经济学的最优解

更关键的是,多轮迭代的"产物"不只是代码,还包括:每一轮的 prompt(沉淀为 Skills 的输入语料)、每一轮的审查决策(沉淀为 CLAUDE.md 的团队共识)、每一轮的 git diff(沉淀为未来 review 的上下文档案)。一次成型的开发模式只产出代码,多轮迭代会产出"代码 + 流程资产"。半年累计下来,这部分流程资产对团队效率的复利效应往往超过代码本身的价值。

1.2 Claude Code 与传统 IDE 在迭代成本上的根本差异

传统 IDE 时代,"迭代成本"主要由以下几部分构成:开发者切换上下文的脑力开销、查文档/搜代码的时间、写测试的时间、跑测试的时间、调试的时间。其中前三项往往占了 60% 以上。

Claude Code 把这些时间压到接近零:

  • 上下文切换:Claude 始终在线,不需要工程师"重新加载脑内 RAM";
  • 查文档/搜代码:Claude 通过代码库扫描与 MCP(如 context7)实时获取信息;
  • 写测试:让 Claude 在写实现的同时生成测试草稿;

剩下的"跑测试 + 调试"部分由工程师审查,但因为单次提交极小,调试范围也极小。

经验数据:本人在多个项目中统计,Claude Code 协作下平均每轮迭代耗时 8-15 分钟(含审查与 commit),而传统模式下同等粒度的迭代往往需要 30-60 分钟。这种 3-4 倍的提速并不是"AI 写得快",而是"循环本身变短了"。

更深一层的观察:节省下来的时间不会自动转化为"做更多功能"——它会被工程师用于以下三件事的某一件:(1)提高代码质量,例如给同一个功能补上更详尽的边界测试;(2)做以前来不及做的重构,例如把第 3 轮迭代里"先抄一份"的代码抽离为公共模块;(3)陪 Claude 做更多对话探索,例如让 Claude 列举 5 种实现方案并讨论各自的折衷。这三种用法的优先级取决于团队当前阶段——MVP 期优先(3)、稳定期优先(1)、成长期优先(2)。

值得警惕的反向观察:如果团队把节省下来的时间全部用于"做更多功能",迭代成本曲线会快速反弹。原因是新功能不会消失,技术债会随线性增长,而工程师的"债务清算时间"被节省走的时间挤压殆尽。本人称之为"AI 节奏陷阱"——节奏快不代表方向正确。

1.3 迭代节奏的三个层级

为了避免把"迭代"这个词糊在一起,我们把它拆成三个层级:

  1. Token 级迭代:Claude 在生成下一行代码时的微观选择。这是 LLM 的内部工作,工程师不可见。
  2. 函数级迭代:单次对话产出 1-3 个函数 / 一个组件 / 一个类型定义。这是工程师每分钟级别能感知到的循环。
  3. 功能级迭代:完成一个用户故事(User Story)或验收标准(Acceptance Criteria)。这是产品经理能看到的循环。

混淆这三个层级会带来麻烦。例如要求 Claude "实现整个用户管理模块"——这是功能级迭代,但 Claude 会在内部塞进十几个 token 级与函数级决策,等工程师审查时一切都已木已成舟。正确做法是把功能级拆到函数级,再让 Claude 接管 token 级。

层级混淆的另一种常见后果是审查疲劳。当工程师面对一份 600 行的 PR diff,注意力会很快被消耗——前 100 行还能仔细看,后 500 行往往就只是扫一眼。Claude 的"幻觉"在大尺寸 PR 中不容易被发现,恰恰因为审查者已经累了。把功能级拆成 5-7 个函数级 PR、每个 PR ≤ 100 行 diff,审查注意力可以保持高位,Bug 漏过的概率显著下降。这是把"层级管理"从纯流程问题升级为人因工程问题——尊重人脑的认知边界,而不是假设审查者能一直保持 100% 专注。


二、迭代单元的最小有效设计

2.1 让 Claude 理解"小而完整"的任务边界

"小而完整"是迭代单元的金标准。意味着可审查、可回滚、可在 5 分钟内验证;完整意味着这一轮迭代的产出能独立运行、独立通过测试,不需要等下一轮才能跑起来。

举个对照例子。

❌ 不好的任务边界:

帮我实现用户管理模块。

太大了。Claude 会一次性塞进数据模型、CRUD、校验、错误处理、测试、文档——任何一处错都会牵连其他部分。

✅ 好的任务边界:

帮我实现 User 类型的 TypeScript 接口定义,含 id、name、email、createdAt 字段,并写 1 个最小工厂函数 createUser(input) 用于测试夹具。不要写 CRUD,不要写校验。

边界清晰、产出明确、可在 60 秒内验证。

2.2 一次提交一个可验证假设

这条原则借鉴自 Hypothesis-Driven Development(假设驱动开发)——每一次提交都对应一个可被测试证伪的假设。

# 第 1 轮:假设 User 类型只需 4 个字段
git commit -m "feat(user): 定义 User 类型与最小工厂函数"

# 第 2 轮:假设 createUser 在 email 不合法时应抛错
git commit -m "feat(user): createUser 增加 email 格式校验"

# 第 3 轮:假设需要持久化到 SQLite
git commit -m "feat(user): 增加 saveUser 持久化到 better-sqlite3"

每个 commit 对应一个假设。如果某个假设被推翻(例如团队决定 email 校验交给前端做),可以单独 git revert 而不影响其他部分。

2.3 从 TDD 到 ATDD 的对话式实践

传统 TDD(Test-Driven Development,测试驱动开发)流程是 Red → Green → Refactor——先写失败的测试、再写最少代码让测试通过、最后重构。

Claude Code 让这个循环可以更自然。一种行之有效的对话模板:

我要实现 createUser 函数。先帮我写一个 Vitest 测试,覆盖以下场景:
- 正常输入返回带 id 与 createdAt 的 User
- email 为空字符串应抛 ValidationError
- name 长度超过 100 应抛 ValidationError

写完测试后停下,等我确认再写实现。

让 Claude 先写测试、再写实现。这有两个好处:

  1. 测试是契约——Claude 写测试时被迫思考边界条件,减少幻觉式实现。
  2. 审查门槛降低——工程师只需读一遍测试就能判断 Claude 对需求的理解是否正确。

进一步,ATDD(Acceptance Test-Driven Development,验收测试驱动开发) 把这条链路延伸到验收层。让 Claude 把产品经理写的"用户故事"翻译成可执行的验收测试(如 Playwright 用例),然后再下推到单元测试和实现。

2.4 让 Claude 主动列出"完成定义"

每一轮迭代开始前,让 Claude 先输出一份 Definition of Done(完成定义),并等工程师确认。

我要在 6.3.4 节加一个 createUser 函数。开始之前,请输出本轮迭代的 Definition of Done,
包含:
- 文件清单(新增 / 修改)
- 函数签名
- 测试用例编号
- 不在本轮范围内的事项

我确认后你再开始写代码。

Claude 输出示例:

本轮迭代的 Definition of Done:

【新增文件】
- src/user/types.ts(User、CreateUserInput、ValidationError)
- src/user/factory.ts(createUser 函数)
- src/user/__tests__/factory.test.ts(3 个用例)

【函数签名】
function createUser(input: CreateUserInput): User
type CreateUserInput = { name: string; email: string }

【测试用例】
1. 正常输入返回 User,含 id 与 createdAt
2. email 不合法抛 ValidationError
3. name 超长抛 ValidationError

【不在本轮范围】
- 持久化(下一轮)
- 鉴权与权限校验
- API 路由
- updateUser / deleteUser

工程师读完点头,Claude 才开始动手。这条流程的隐含价值是——把"理解偏差"前置发现,而不是在 200 行代码写完后再发现


三、迭代过程中的人机分工

3.1 工程师保留的高阶决策点

Claude 是优秀的"执行者",但远未达到优秀"决策者"的水准。以下决策必须由工程师把关,不能让 Claude 自动决定

决策类型例子为什么不能让 Claude 拍板
架构边界把鉴权放在 API 层还是中间件决定后续 6 个月的演进方向
业务规则"用户能否同时拥有多个手机号"Claude 不知道你公司的合规要求
性能约束"响应时间必须 < 200ms"决定后续技术选型与基础设施
安全决策"密码用 bcrypt 还是 argon2"长期合规、攻击面、密钥管理
数据合规GDPR、个保法的字段处理法律责任无法转嫁
资源成本是否启用 LLM 调用作为业务功能直接影响商业模型

工程师在每轮迭代开始前确认这些决策,再把"如何执行"交给 Claude。

判断"这是不是高阶决策"的简易标准:问自己一个问题——"如果一年后回头看,我会希望当时花 10 分钟思考、还是希望当时让 AI 自动决定?"。任何一年内仍然影响系统行为的决策(架构、契约、协议、数据格式、依赖选择),都值得人类多花一点时间。低于一年的决策(变量名、函数顺序、注释措辞)放心交给 Claude。这条标准不完美,但能挡住 80% 的"不该让 AI 拍板"的事。

另一个常见误区是"我让 Claude 给我几个方案我来选"——这看起来像是工程师在决策,实际上 Claude 列出方案的过程已经隐含了筛选偏见。它倾向于训练数据里高频的方案,例如永远把 PostgreSQL 排在 SQLite 前面、永远把 Redis 排在内存缓存前面。要破这层偏见,工程师应该自己先列出 3 个候选,再让 Claude 补充第 4、5 个候选并指出每个的隐性风险。先列后补,结果质量明显高于纯让 Claude 起草。

3.2 Claude 接管的执行细节

反过来,以下执行细节让 Claude 接管能极大节省时间:

  • 语法层:TypeScript 类型推导、泛型约束、装饰器写法
  • 接口签名:函数命名、参数顺序、返回类型
  • 重复模式:CRUD 模板、Validator、DTO 转换
  • 测试夹具:Faker.js 数据生成、mock 函数
  • 样板代码:Express 路由、Vue SFC 骨架、错误中间件

Claude 接管这些细节时,工程师审查的重点是"这一段代码是否符合项目惯例"——而项目惯例由 CLAUDE.md 与 Skills 沉淀,进一步把审查门槛降低。

3.3 Plan 模式如何把"自动执行"降级为"人工审批"

Claude Code 的 Plan 模式(Plan Mode)是迭代过程中的关键守门人。它的核心机制:在执行任何写文件、跑命令的动作之前,强制 Claude 输出一份完整的 Plan 让工程师审批

启用方式:

# 命令行启动时进入 Plan 模式
claude --plan-mode

# 或会话中临时切换
/plan

Plan 输出示例:

我将完成以下动作:
1. 修改 src/user/factory.ts,添加 phoneNumber 字段处理(约 12 行)
2. 修改 src/user/types.ts,扩展 User 接口(约 3 行)
3. 修改 src/user/__tests__/factory.test.ts,新增 2 个测试用例(约 25 行)
4. 运行 pnpm test src/user 验证(约 8 秒)
5. 提交 commit "feat(user): 增加 phoneNumber 字段"

请审批 [Y/n]:

工程师可以选择全部批准、部分批准、修改后再批准、整体拒绝。这条机制把"AI 自动 commit"的失控风险降到接近零。

3.4 Hooks 在每轮迭代前后的守卫作用

Hooks 是 Claude Code 的事件驱动守卫机制,它能在每个工具调用前后插入工程师定义的 shell 命令。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "pnpm lint --max-warnings=0 --quiet"
          }
        ]
      }
    ]
  }
}

这条 hook 会在 Claude 每次 Edit 或 Write 文件后立即跑 ESLint,警告会被立刻拦截。结合"Hooks 失败时 Claude 必须回应"的机制,可以做到:

  • Lint 不通过 → Claude 立即修复
  • 类型检查失败 → Claude 自动重写
  • 测试失败 → Claude 暂停并请求工程师介入

Hooks 的关键价值是把"人工审查"的部分自动化,让工程师专注在更高层的决策审查上。


四、实战:用户管理模块的 6 轮迭代

下面给出一个可以立即套用的对话脚本。我们要做的功能:实现一个最小化的用户管理模块,包含数据模型、CRUD、错误处理、测试、权限校验、性能优化。

技术栈:TypeScript + Vitest + better-sqlite3。

4.1 第 1 轮:数据模型与类型定义

对话提示词:

我要做用户管理模块。第一轮:定义数据模型。

请创建:
- src/user/types.ts,含 User 接口(id: string, name: string, email: string, createdAt: Date)
- src/user/types.ts 同文件含 CreateUserInput、UpdateUserInput
- src/user/errors.ts,含 ValidationError、NotFoundError 两个错误类

不要写实现,不要写测试。完成后输出文件清单与 commit message。

Claude 产出:

// src/user/types.ts
export interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

export type CreateUserInput = Omit<User, "id" | "createdAt">;
export type UpdateUserInput = Partial<CreateUserInput>;

// src/user/errors.ts
export class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = "ValidationError";
  }
}

export class NotFoundError extends Error {
  constructor(public resource: string, public id: string) {
    super(`${resource} not found: ${id}`);
    this.name = "NotFoundError";
  }
}

审查要点:类型字段是否齐全?错误类是否包含必要的上下文?

Commitfeat(user): 定义 User 类型与错误类

4.2 第 2 轮:核心 CRUD API

对话提示词:

基于第 1 轮的类型,实现 src/user/repository.ts,包含:
- createUser(input: CreateUserInput): User
- getUser(id: string): User
- updateUser(id: string, input: UpdateUserInput): User
- deleteUser(id: string): void

数据存储在内存 Map,先不做持久化。每个函数加 1 行注释说明用途。
不要写测试,不要做校验。

Claude 产出(示例):

// src/user/repository.ts
import { randomUUID } from "node:crypto";
import { User, CreateUserInput, UpdateUserInput } from "./types";
import { NotFoundError } from "./errors";

const store = new Map<string, User>();

// 新增用户:自动生成 id 与 createdAt
export function createUser(input: CreateUserInput): User {
  const user: User = {
    id: randomUUID(),
    name: input.name,
    email: input.email,
    createdAt: new Date(),
  };
  store.set(user.id, user);
  return user;
}

// 查询用户:找不到时抛 NotFoundError
export function getUser(id: string): User {
  const user = store.get(id);
  if (!user) throw new NotFoundError("User", id);
  return user;
}

// 更新用户:仅修改传入字段
export function updateUser(id: string, input: UpdateUserInput): User {
  const user = getUser(id);
  const updated = { ...user, ...input };
  store.set(id, updated);
  return updated;
}

// 删除用户:找不到时抛 NotFoundError
export function deleteUser(id: string): void {
  if (!store.has(id)) throw new NotFoundError("User", id);
  store.delete(id);
}

审查要点:内存存储是否合适?UUID 生成是否安全?错误处理是否一致?

Commitfeat(user): 实现内存版 CRUD repository

4.3 第 3 轮:边界条件与错误处理

对话提示词:

为 createUser 与 updateUser 增加输入校验:
- name:非空、长度 1-100
- email:必须包含 @ 与 .,长度 5-254

校验失败抛 ValidationError,包含 field 与人类可读 message。
仅修改 src/user/repository.ts,不要新建文件。

Claude 产出(局部):

function validateName(name: string) {
  if (!name || name.length < 1 || name.length > 100) {
    throw new ValidationError("name", "name 长度必须在 1-100 之间");
  }
}

function validateEmail(email: string) {
  if (!email || email.length < 5 || email.length > 254) {
    throw new ValidationError("email", "email 长度必须在 5-254 之间");
  }
  if (!email.includes("@") || !email.includes(".")) {
    throw new ValidationError("email", "email 格式不合法");
  }
}

export function createUser(input: CreateUserInput): User {
  validateName(input.name);
  validateEmail(input.email);
  // ...原有逻辑
}

审查要点:校验函数命名是否清晰?错误信息是否便于用户理解?是否覆盖所有边界?

Commitfeat(user): createUser/updateUser 增加输入校验

4.4 第 4 轮:单元测试与 Fixture

对话提示词:

为 src/user/repository.ts 写 Vitest 测试,覆盖:
- createUser 正常路径
- createUser email 不合法
- createUser name 过长
- getUser 找不到
- updateUser 部分字段
- updateUser 不存在 id
- deleteUser 正常路径
- deleteUser 不存在 id

文件路径:src/user/__tests__/repository.test.ts
使用 beforeEach 清空 store。

Claude 产出(局部):

// src/user/__tests__/repository.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import {
  createUser,
  getUser,
  updateUser,
  deleteUser,
} from "../repository";
import { ValidationError, NotFoundError } from "../errors";

describe("user repository", () => {
  beforeEach(() => {
    // 当前实现是模块级 Map,测试需要重置
    // TODO: 第 5 轮重构为类,beforeEach 用 new Repository()
  });

  it("createUser 正常路径", () => {
    const user = createUser({ name: "Alice", email: "a@b.co" });
    expect(user.id).toBeDefined();
    expect(user.createdAt).toBeInstanceOf(Date);
    expect(user.name).toBe("Alice");
  });

  it("createUser email 不合法应抛 ValidationError", () => {
    expect(() =>
      createUser({ name: "Alice", email: "not-an-email" })
    ).toThrow(ValidationError);
  });

  // ...其余 6 个用例
});

审查要点:测试用例是否覆盖关键边界?是否对实现细节耦合过深?TODO 标记是否合理?

Committest(user): 增加 repository 单元测试,覆盖率 95%

4.5 第 5 轮:业务规则与权限校验

对话提示词:

增加业务规则:
- 同一 email 不能注册两次(重复时抛 DuplicateError)
- 仅管理员能删除用户(接口签名加 actorRole 参数)

修改 src/user/repository.ts,更新 src/user/errors.ts,更新对应测试。
不要修改路径与文件结构。

审查要点:DuplicateError 是新增还是复用?actorRole 是否影响其他调用方?测试是否相应更新?

Commitfeat(user): 增加唯一性校验与管理员权限

4.6 第 6 轮:性能优化与日志埋点

对话提示词:

最后一轮:
1. 把 store 从 Map<string, User> 升级为 Map<string, User> + Map<email, id> 的双索引,使重复 email 检查 O(1)
2. 在 createUser、deleteUser 增加 console.info 埋点,包含 actorRole、userId、timestamp
3. 不引入额外依赖

更新对应测试,确保性能优化不破坏功能。

审查要点:双索引的写操作是否原子?日志格式是否符合团队约定?测试是否覆盖索引一致性?

Commitperf(user): 增加 email 索引并补充审计日志


至此,6 轮迭代完成。每轮 ≤ 80 行新增代码、≤ 5 分钟审查、独立 commit。任何一轮的假设被推翻,都可以独立 git revert 而不影响其余部分。

关键观察:6 轮总耗时约 60-90 分钟(含审查与提交),相当于工程师独立开发的 1/4。但更重要的不是速度,而是过程中产出的可追溯历史——每个 commit 都对应一个明确的需求假设,未来 6 个月任何人来 review,都能通过 git log 重建当时的决策上下文。

对照传统模式的隐性差异:传统手工开发中,工程师常常会"边写边重构"——写到一半发现命名不对就改、写到一半发现接口设计不好就重写——最后 commit 时 diff 已经混杂了实现、调整、重构、重命名等多种意图。Code Review 看到这种 diff 会很头疼,因为审查者无法区分"哪些是这次的核心变更、哪些是顺手清理"。多轮迭代的 6 个清晰 commit 把这些意图分离开来,审查者可以按"假设维度"逐一确认,审查质量的上限因此被提高,而不仅仅是审查速度。这是多轮迭代相对传统模式最被低估的优势。


五、迭代失控的反模式与挽救

多轮迭代不是"开了挂",它有自己的失控模式。下面四种是本人在多个项目中反复观察到的反模式。

5.1 上下文溢出导致的"健忘"现象

症状:进行到第 8、9 轮迭代时,Claude 开始"忘记"早期的约定,例如重新使用第 2 轮已弃用的字段名、重新引入第 3 轮已删除的依赖。

根因:长会话的上下文窗口被填满后,早期的对话内容被压缩或丢失。

挽救方案

  1. 每 5-7 轮迭代后,让 Claude 输出一份"当前项目状态摘要":当前文件清单、关键决策、未解决问题。把摘要写入 .claude/state.md,作为下次会话的起点。
  2. 把高频信息固化到 CLAUDE.md:例如"User.email 必须唯一"、"所有错误必须继承自 BaseError"。CLAUDE.md 在每次会话开始时都会注入。
  3. 使用 Plan 模式开新会话:旧会话结束前让 Claude 输出 plan,新会话用 plan 作为起点。

实战补充:本人在一个 12 人团队的中型项目(约 30 万行 TS 代码)中观察到,单会话最优长度大约是 60-90 分钟。再长则上下文压缩损耗显著上升、Claude 输出质量下降;再短则会话切换的"暖机成本"(重读 CLAUDE.md、重新理解当前 git 状态)反而高于收益。建议工程师把"番茄钟(Pomodoro)"工作节奏与会话节奏对齐——每 90 分钟主动结束会话,输出状态摘要,再开新会话。这条习惯三周后会形成肌肉记忆,团队的迭代节奏会自然稳定下来。

另一条经验是会话开始时主动注入"当前焦点"

本次会话的焦点是用户管理模块的 6 轮迭代中的第 4 轮:补全单元测试。
请阅读 .claude/state.md 了解前 3 轮的产出与决策。
不要触碰 src/order、src/billing 模块。
不要做超出第 4 轮范围的重构。

这种"焦点声明"能让 Claude 在长会话中保持任务边界,显著降低"漂移"概率。

5.2 Claude "幻觉式自信"——错误地宣称完成

症状:Claude 输出"我已经完成了 X 与 Y",但实际只做了 X,或者做了 X 但代码并不能跑。

根因:LLM 的输出基于概率分布,"宣称完成"在训练数据中是高频模式,即使任务并未真正完成。

挽救方案

  1. 用 Hooks 做"完成态验证"——例如 PostToolUse 后强制跑 pnpm test,失败则 Claude 不得宣称完成。
  2. 在 prompt 中明确"完成判定"——"完成的判定是 pnpm test 全部通过、pnpm lint 零警告、pnpm typecheck 零错误"。
  3. 不信任自然语言"完成声明"——只信任 git log 与测试结果。

进阶补充:本人在排查"幻觉式完成"的过程中总结出三个常见诱因——

  • 诱因 A:Claude 修改了文件但忘了运行测试。它会基于"代码看起来对"做完成判断。对策:Hooks 强制跑 pnpm test,并在 SessionEnd 阶段附加一份"本次会话所有改动文件的最终测试结果"摘要。
  • 诱因 B:Claude 改了实现但没改对应的测试。例如把函数签名从 (name, email) 改成 ({name, email}),但忘了同步更新 __tests__/ 里 6 个测试用例。对策:PreToolUse 阶段检查 "Edit 的文件名是否匹配 src/,若是则要求 Claude 报告对应 __tests__/ 文件是否需要同步修改"。
  • 诱因 C:Claude 跑了测试但只看了部分结果。某些 CLI 输出过长会被截断,Claude 可能只读到了头部 20 行的"PASS",未察觉尾部的 1 failed。对策:用 pnpm test --reporter=summary 让结果更紧凑,或在 Hooks 里 grep failed|FAIL 反向验证。

这三条对策一起跑下来,本人项目里的"幻觉式完成"误报率从约 8% 降到了 < 1%。

5.3 反复修改同一处代码的"打地鼠"陷阱

症状:第 X 轮改了文件 A,第 X+1 轮回到文件 A 又改一次,第 X+2 轮再改一次……同一处代码在 3 轮内被修改 3 次。

根因:每一轮都只解决了局部问题,没有触及更深的设计缺陷。

挽救方案

  1. 触发"打地鼠告警"——同一文件 3 轮内被修改 ≥ 3 次时,强制暂停迭代,让 Claude 输出"这块代码反复修改的根因是什么?"。
  2. 退出迭代循环,做一次小型重构——把这块代码抽离、重写或拆分。
  3. 把根因记录到 CLAUDE.md——下次类似情境直接规避。

实战案例:本人遇到过一次典型的"打地鼠"——某个 formatPrice(amount, currency) 函数在两天内被 6 轮迭代反复修改:第 1 轮加日元支持、第 2 轮加千分位、第 3 轮加负数处理、第 4 轮修复 Intl 在低版本 Node 的 polyfill、第 5 轮修复 RTL 货币显示、第 6 轮修复测试用例。第 6 轮提交后突然意识到——问题不是"再加一个 case",而是函数职责本身错了。这个函数应该被拆成 parsePrice + localizePrice 两个独立的纯函数,前者管数值规范化,后者管国际化展示。重构完成后,相关需求再没出现过修改。

经验教训:"打地鼠"的真信号往往是"职责错位"——一个函数(或一个文件)承担了过多上游决策,每个新需求都要在它身上落地。这是抽象边界的报警,而不是 Claude 写得不好。让 Claude 自己看出"职责错位"很难;让工程师在第 3 轮警告时主动停下来反思一下,往往一眼就能看出。

5.4 用 git 二分法 + Plan 模式回滚到稳定基线

症状:连续 N 轮迭代后突然有用例失败,但搞不清是哪一轮引入的。

挽救方案

# 启动 git bisect
git bisect start
git bisect bad HEAD              # 当前 broken
git bisect good HEAD~10          # 10 轮前 good

# git 会自动 checkout 到中间 commit
# 在该 commit 上跑测试
pnpm test src/user

# 标记结果
git bisect good   # 或 bad

# 重复直到锁定那个引入问题的 commit

锁定 commit 后,让 Claude 在 Plan 模式下分析:

git bisect 显示 commit abc123 引入了 src/user/repository.test.ts 的失败用例。
请:
1. git show abc123 输出 diff
2. 分析这个 commit 为什么引入失败
3. 输出回滚 plan(保留其他正确变更)

这条流程把"AI 自动迭代"与"传统调试工具"结合,形成了可追溯、可挽救的迭代体系


六、迭代节奏的工程化

一次性把 6 轮迭代跑完不难,难的是"团队 5 个人,每人每周做 20 轮迭代,6 个月后代码库依然清晰"。这要求迭代本身被工程化。

6.1 把多轮提交聚合为有意义的功能分支

直接在 main 上做 6 轮迭代不可行。推荐 Trunk-Based Development(基于主干开发)+ 短分支 + Squash Merge 的组合:

# 开始功能开发
git checkout -b feat/user-management

# 6 轮迭代各自独立 commit
# (在分支上累积 6 个 commit)

# 完成后做 squash merge
gh pr create --title "feat(user): 用户管理模块 v1" \
  --body "包含 6 轮迭代:类型定义、CRUD、校验、测试、业务规则、性能优化"

# 在 PR 中保留完整 commit 历史用于 review
# 合并到 main 时 squash 为一个 commit,保留 PR 链接以追溯

好处

  • main 分支的 commit 历史保持清晰(每个 commit = 一个功能)
  • PR 的 review 视角保持细粒度(6 个 commit 各自审查)
  • 万一需要回滚,整个功能能干净撤回

何时不该 Squash:上面的策略对 90% 的功能开发有效,但有两类例外。第一类是多人协作的长生命周期分支——例如 epic 级别的特性分支会持续 3-4 周,期间多人提交,Squash 会丢失协作历史与责任归属,应保留 merge commit 风格。第二类是包含独立可回滚 hotfix 的混合 PR——例如功能开发中顺手修了一个生产 bug,这个 bug fix 应该被独立保留,方便未来 cherry-pick 到其他分支,因此整个 PR 不能 Squash。判断标准简单:如果未来 12 个月内可能有人单独引用这个 commit,就不要 Squash

6.2 用 CLAUDE.md 沉淀本轮迭代学到的项目惯例

每完成一个功能(不是每轮迭代),花 5 分钟更新 CLAUDE.md。让 Claude 帮忙:

我刚完成了用户管理模块的 6 轮迭代。请扫描这次的 commits 与 PR diff,
提取以下惯例并附加到 CLAUDE.md:
- 错误处理:所有自定义错误继承自 BaseError,含 field/resource 上下文
- 测试结构:每个模块有独立的 __tests__ 目录
- 命名约定:repository 函数用动词开头(create/get/update/delete)
- 业务规则:所有写操作的入参必须含 actorRole 参数

只追加,不要重写已有内容。完成后让我审查 diff。

CLAUDE.md 是团队"沉淀的肌肉记忆"。每次迭代后的 5 分钟更新,半年后会让 Claude 与新成员上手时间缩短一半以上。

CLAUDE.md 维护的三条反直觉法则

  1. 不要追求"完整性"。CLAUDE.md 不是设计文档,而是"高频偏差校正器"。只把 Claude 反复犯错的点写进去,那些 Claude 一次就能做对的事情不需要写。一份 200 行的 CLAUDE.md 通常比 800 行的更有效——后者的信号被噪音稀释了。
  2. 每月做一次"减法 review"。打开 CLAUDE.md,逐条问自己"过去 30 天,如果删掉这条,Claude 会出错吗?"。回答"不会"的条目立即删除。CLAUDE.md 的衰老速度比想象的快——团队代码库变了、依赖升级了、最佳实践改了——半年不维护的 CLAUDE.md 通常已经误导多过帮助。
  3. 用具体例子而非抽象规则。"错误处理要规范" 是空话;"所有自定义错误必须继承 BaseError,例如 src/user/errors.ts 中的 ValidationError" 才是 Claude 真正能照着抄的指令。把 CLAUDE.md 里的每条规则后面挂一个具体的代码引用,效果会显著提升。

把这三条法则贯彻下去,CLAUDE.md 的健康度可以维持在"每条规则都在活跃服役"的状态,而不会沦为"看起来很全但谁也不信"的过期资料库。

6.3 Skills 把高频迭代模板编码为可复用流程

某些迭代模式会反复出现:例如"为某个领域模块写 CRUD repository + 单元测试"、"为某个 API 加 Zod 校验 + 错误处理"。把这些模式沉淀为 Skills:

.claude/skills/
├── crud-repository/
│   ├── SKILL.md
│   └── template.ts
├── add-zod-validation/
│   ├── SKILL.md
│   └── examples/
└── refactor-to-async/
    └── SKILL.md

每个 Skill 是一个可复用的"迭代脚本"。下次有人要做类似功能,一句 请使用 crud-repository skill 为 Order 实体生成 CRUD 就能复用整套迭代流程。

Skills 的真正威力是让多轮迭代的最佳实践跨越个人,跨越项目,跨越时间——团队在第 N 个项目积累的迭代节奏,能直接被第 N+1 个项目继承。

Skill 设计的三个原则

  1. 小而专:每个 Skill 只解决一个清晰场景。"crud-repository" 不要塞进"生成 OpenAPI 文档"的能力——那是另一个 Skill。颗粒度太大的 Skill 会变成"什么都不擅长"的大杂烩。
  2. 示例先行:在 Skill 目录的 examples/ 子目录里放 2-3 个真实项目的运行实例,并在 SKILL.md 里清晰链接。Claude 在执行 Skill 时会优先参考这些示例,输出质量远高于纯描述型 Skill。
  3. 可被人类阅读:SKILL.md 既是 Claude 的指令,也是新成员的教材。控制在 100-300 行之间,结构清晰:「适用场景 → 输入要求 → 步骤 → 验收标准 → 已知边界」。当 Claude 与新成员的理解都依赖同一份文档时,团队的"知识同源"水准会显著提升。

衡量 Skills 价值的指标

  • 使用次数:每周被显式 / 隐式调用多少次?低于 1 次 / 周说明颗粒度或场景定位不准;
  • 节省时间:相比未使用 Skill 的人工实现,平均节省多少分钟?(建议设置 ≥ 15 分钟为合格阈值);
  • 错误率:使用 Skill 后产生的代码进入 main 后的 bug 数。错误率显著高于团队均值的 Skill 应该立即下线维护。

把这三项指标接入 Hooks 自动统计(例如 SessionEnd 时 append 到 .claude/skill-metrics.jsonl),半年后就能形成一张"团队 Skills 健康表",淘汰掉低 ROI 的 Skill,把时间花在高价值的固化上。


总结

多轮迭代不是 Claude Code 的"特性",而是它使用范式的核心。本节做了三件事:把"为什么要多轮迭代"用工程经济学说清楚、把"怎么做多轮迭代"拆成最小有效单元与人机分工、给出一份从用户管理模块开始的 6 轮迭代实战脚本。

完成 6.3 之后,团队应该已经具备了用 Claude 在分钟级节奏完成核心功能的能力。但功能跑起来只是开始——下一节 6.4 将讨论"测试覆盖、代码审查与质量调优",把"能跑"升级为"可上线"。

最后一份团队层面的检查表:当多轮迭代被广泛实践,团队需要在每个 sprint 结束前回答以下七个问题,作为"迭代健康度"的体检:

  1. 本 sprint 平均每个功能用了几轮迭代完成?(健康范围:3-7 轮)
  2. 平均每轮迭代的 commit 体量?(健康范围:30-80 行)
  3. 有多少轮迭代触发了"打地鼠告警"(同文件 3 轮内 ≥ 3 次)?(健康范围:< 5%)
  4. 有多少 PR 因为"幻觉式完成"被打回?(健康范围:< 2%)
  5. CLAUDE.md 本周新增了多少条共识?(健康范围:3-10 条)
  6. Skills 库本周被显式调用了多少次?(健康范围:> 团队人数 × 5)
  7. 工程师在迭代过程中"等 Claude 输出"的累计时间是否 < 30 分钟 / 人 / 天?(健康范围:是)

这七个问题是团队级别的"迭代体温计"。任何一个长期失温(连续两周不在健康范围)都说明工作流有进一步优化的空间——或许是 prompt 不够精准、或许是 CLAUDE.md 老化、或许是 Skills 的颗粒度不对。每个 sprint review 上花 10 分钟过一遍这张表,长期收益远高于花 10 分钟讨论某个具体技术细节。

留给读者的两个习题:

  1. 在你当前项目里挑一个未完成的功能,用本节的"6 轮迭代脚本"跑一遍。完成后记录每轮耗时,对比传统模式。
  2. .claude/skills/ 下添加一个 iteration-checkpoint skill,把"每 5 轮输出项目状态摘要并写入 .claude/state.md"固化下来。

这两个习题会让多轮迭代从"理论上知道"变成"肌肉上习惯"。

延伸阅读