核心功能实现
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 迭代节奏的三个层级
为了避免把"迭代"这个词糊在一起,我们把它拆成三个层级:
- Token 级迭代:Claude 在生成下一行代码时的微观选择。这是 LLM 的内部工作,工程师不可见。
- 函数级迭代:单次对话产出 1-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 先写测试、再写实现。这有两个好处:
- 测试是契约——Claude 写测试时被迫思考边界条件,减少幻觉式实现。
- 审查门槛降低——工程师只需读一遍测试就能判断 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";
}
}
审查要点:类型字段是否齐全?错误类是否包含必要的上下文?
Commit:feat(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 生成是否安全?错误处理是否一致?
Commit:feat(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);
// ...原有逻辑
}
审查要点:校验函数命名是否清晰?错误信息是否便于用户理解?是否覆盖所有边界?
Commit:feat(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 标记是否合理?
Commit:test(user): 增加 repository 单元测试,覆盖率 95%
4.5 第 5 轮:业务规则与权限校验
对话提示词:
增加业务规则:
- 同一 email 不能注册两次(重复时抛 DuplicateError)
- 仅管理员能删除用户(接口签名加 actorRole 参数)
修改 src/user/repository.ts,更新 src/user/errors.ts,更新对应测试。
不要修改路径与文件结构。
审查要点:DuplicateError 是新增还是复用?actorRole 是否影响其他调用方?测试是否相应更新?
Commit:feat(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. 不引入额外依赖
更新对应测试,确保性能优化不破坏功能。
审查要点:双索引的写操作是否原子?日志格式是否符合团队约定?测试是否覆盖索引一致性?
Commit:perf(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 轮已删除的依赖。
根因:长会话的上下文窗口被填满后,早期的对话内容被压缩或丢失。
挽救方案:
- 每 5-7 轮迭代后,让 Claude 输出一份"当前项目状态摘要":当前文件清单、关键决策、未解决问题。把摘要写入
.claude/state.md,作为下次会话的起点。 - 把高频信息固化到 CLAUDE.md:例如"User.email 必须唯一"、"所有错误必须继承自 BaseError"。CLAUDE.md 在每次会话开始时都会注入。
- 使用 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 的输出基于概率分布,"宣称完成"在训练数据中是高频模式,即使任务并未真正完成。
挽救方案:
- 用 Hooks 做"完成态验证"——例如 PostToolUse 后强制跑
pnpm test,失败则 Claude 不得宣称完成。 - 在 prompt 中明确"完成判定"——"完成的判定是
pnpm test全部通过、pnpm lint零警告、pnpm typecheck零错误"。 - 不信任自然语言"完成声明"——只信任 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 里 grepfailed|FAIL反向验证。
这三条对策一起跑下来,本人项目里的"幻觉式完成"误报率从约 8% 降到了 < 1%。
5.3 反复修改同一处代码的"打地鼠"陷阱
症状:第 X 轮改了文件 A,第 X+1 轮回到文件 A 又改一次,第 X+2 轮再改一次……同一处代码在 3 轮内被修改 3 次。
根因:每一轮都只解决了局部问题,没有触及更深的设计缺陷。
挽救方案:
- 触发"打地鼠告警"——同一文件 3 轮内被修改 ≥ 3 次时,强制暂停迭代,让 Claude 输出"这块代码反复修改的根因是什么?"。
- 退出迭代循环,做一次小型重构——把这块代码抽离、重写或拆分。
- 把根因记录到 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 维护的三条反直觉法则:
- 不要追求"完整性"。CLAUDE.md 不是设计文档,而是"高频偏差校正器"。只把 Claude 反复犯错的点写进去,那些 Claude 一次就能做对的事情不需要写。一份 200 行的 CLAUDE.md 通常比 800 行的更有效——后者的信号被噪音稀释了。
- 每月做一次"减法 review"。打开 CLAUDE.md,逐条问自己"过去 30 天,如果删掉这条,Claude 会出错吗?"。回答"不会"的条目立即删除。CLAUDE.md 的衰老速度比想象的快——团队代码库变了、依赖升级了、最佳实践改了——半年不维护的 CLAUDE.md 通常已经误导多过帮助。
- 用具体例子而非抽象规则。"错误处理要规范" 是空话;"所有自定义错误必须继承 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 设计的三个原则:
- 小而专:每个 Skill 只解决一个清晰场景。"crud-repository" 不要塞进"生成 OpenAPI 文档"的能力——那是另一个 Skill。颗粒度太大的 Skill 会变成"什么都不擅长"的大杂烩。
- 示例先行:在 Skill 目录的
examples/子目录里放 2-3 个真实项目的运行实例,并在 SKILL.md 里清晰链接。Claude 在执行 Skill 时会优先参考这些示例,输出质量远高于纯描述型 Skill。 - 可被人类阅读: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 结束前回答以下七个问题,作为"迭代健康度"的体检:
- 本 sprint 平均每个功能用了几轮迭代完成?(健康范围:3-7 轮)
- 平均每轮迭代的 commit 体量?(健康范围:30-80 行)
- 有多少轮迭代触发了"打地鼠告警"(同文件 3 轮内 ≥ 3 次)?(健康范围:< 5%)
- 有多少 PR 因为"幻觉式完成"被打回?(健康范围:< 2%)
- CLAUDE.md 本周新增了多少条共识?(健康范围:3-10 条)
- Skills 库本周被显式调用了多少次?(健康范围:> 团队人数 × 5)
- 工程师在迭代过程中"等 Claude 输出"的累计时间是否 < 30 分钟 / 人 / 天?(健康范围:是)
这七个问题是团队级别的"迭代体温计"。任何一个长期失温(连续两周不在健康范围)都说明工作流有进一步优化的空间——或许是 prompt 不够精准、或许是 CLAUDE.md 老化、或许是 Skills 的颗粒度不对。每个 sprint review 上花 10 分钟过一遍这张表,长期收益远高于花 10 分钟讨论某个具体技术细节。
留给读者的两个习题:
- 在你当前项目里挑一个未完成的功能,用本节的"6 轮迭代脚本"跑一遍。完成后记录每轮耗时,对比传统模式。
- 在
.claude/skills/下添加一个iteration-checkpointskill,把"每 5 轮输出项目状态摘要并写入 .claude/state.md"固化下来。
这两个习题会让多轮迭代从"理论上知道"变成"肌肉上习惯"。
延伸阅读
- Anthropic Claude Code 官方文档 — Plan 模式、Hooks、Skills 全览
- Boris Cherny: Claude Code Best Practices — Anthropic 官方实战心法
- Hypothesis-Driven Development(ThoughtWorks) — 假设驱动开发原则
- Vitest 官方文档 — 现代 TypeScript 测试框架
- Trunk-Based Development — 基于主干的迭代分支策略
- Test-Driven Development by Example(Kent Beck) — TDD 经典原著
- Pragmatic Programmer 20th Anniversary Edition — 软件工艺与迭代节奏
- Continuous Delivery(Jez Humble & David Farley) — 高频小步发布的工程基础
- Refactoring 2nd Edition(Martin Fowler) — 重构步骤、安全网与回滚策略
- git bisect 官方文档 — 二分法定位失败 commit
- Awesome Claude Code — 社区维护的 Claude Code 资源集