测试覆盖、代码审查与质量调优
经过 6.3 节的多轮迭代,我们已经把一个空的脚手架推向了功能可用的最小可行产品(Minimum Viable Product, MVP)。表面上看,主流程跑通了、UI 截图也拿得出手,但任何一位真正把代码送上过生产环境的工程师都知道:从「Demo 跑通」到「敢交付」之间,还隔着一整条质量保障链路——测试、审查、调优、门禁。这条链路的每个环节,过去由人主导、AI 辅助;从 Claude Code 普及之后,许多团队开始反过来:让 AI 主导执行,让人主导决策。
本节面向两类读者。对产品经理而言,这一章试图回答:为什么 AI 写出来的代码看似能跑,但在压力测试和真实用户场景下会塌方?团队应当怎样的「质量节奏」才能让 AI 不是隐患而是助力?对工程师而言,这一章给出可立即落地的对话脚本、Vitest/Playwright 配置片段、Hooks 与 Plan 模式的协作范式,以及把「质量」从口号变成 CI 阶段的具体做法。
需要预先建立的一个心智模型是:在 Claude Code 工作流里,「质量」不再是某一个阶段的工作,而是贯穿整个对话的「持续约束」。约束既来自外部(CI、Hooks、CODEOWNERS),也来自内部(CLAUDE.md、Skills、PR 模板)。本节会从最基础的「测试观重塑」一路讲到「质量门禁工程化」,每一节都包含三类内容:理论澄清、对话脚本、可直接复制的配置代码。读者可以按章节顺序阅读,也可以把本节当作「日常工作的速查手册」——遇到具体问题时跳到对应小节。
最后请注意:本节不追求「面面俱到」,而追求「每一个建议都能在你下一次对话里立即应用」。如果某个工具或做法暂时与你的项目不匹配,跳过即可;但希望你能在阅读后立刻打开当前项目的 CLAUDE.md,对照本节增补至少一条新规则——这就是 AI 协作时代「学习即落地」的最朴素方式,也是把本节内容真正内化为团队竞争力的唯一捷径。
一、为什么 AI 协作时代的测试观要重写
1.1 LLM 生成代码的「看似正确」陷阱
给产品经理:你可以把大语言模型(Large Language Model, LLM)想象成一位读了几十亿行代码的实习生。他写代码的速度极快、风格干净,但他并不真的「运行」过自己写的东西。他靠的是「这看起来很像正确的代码」。这就是「看似正确」陷阱。
在传统编程中,「编译通过」「类型检查通过」「单元测试通过」这三道关卡能拦截绝大多数低级错误。Claude Code 工作流里,前两道由 LLM 自行处理,几乎不会失败;但「单元测试通过」这一道,如果测试本身也是 LLM 写的,就会形成一个值得警惕的闭环:模型既写实现又写测试,二者可能同时朝错误的方向倾斜,对外呈现「绿色 CI」。
Anthropic 工程师 Boris Cherny 在公开分享中反复强调:「Claude Code 不会替你思考边界条件,它只会非常自信地把你描述过的边界条件落实成代码。」这句话是理解本节所有内容的钥匙——AI 的强项是「把已说清楚的事情做出来」,弱项是「主动质疑没说清楚的部分」。
举一个常见的反例:让 Claude 实现一个分页函数,它通常会正确处理 page=1, pageSize=10 这种主流场景,但极少主动测试 pageSize=0、page=负数、pageSize > totalCount 这类边界。除非你问它:「列出所有可能的边界情况」,它才会真的把这些情况枚举出来。
更深层的原因在于:LLM 的训练语料里,「正常路径」的代码远多于「异常路径」。互联网上的开源仓库往往展示「这个功能怎么用」,而不展示「这个功能怎么坏」。结果就是模型对正常路径的概率分布极其密集、对异常路径的概率分布稀疏。当你不显式提示异常路径时,模型会按训练分布来生成代码——这是统计意义上的必然,不是「模型不够聪明」。
// 看似正确的实现:Claude 第一稿生成
export function paginate<T>(items: T[], page: number, pageSize: number): T[] {
const start = (page - 1) * pageSize
return items.slice(start, start + pageSize)
}
// 隐藏问题:page=0 / page=-1 / pageSize=0 / 浮点数 / NaN 全部静默通过
// 单元测试如果只覆盖 page=1, pageSize=10,CI 会显示绿色,但生产会出事
1.2 测试金字塔在 Claude Code 工作流中的新形态
经典测试金字塔(Test Pyramid,Mike Cohn 提出)由下至上分别是:单元测试、集成测试、端到端测试,比例大致为 70%/20%/10%。这个模型在 AI 协作时代依然成立,但比例与重心发生了微妙变化。
常见做法是把比例调整为「单元 50% + 契约/集成 30% + 端到端 15% + 探索性 5%」。原因有三:第一,AI 生成单元测试的成本极低,但覆盖率边际收益递减;第二,AI 在生成「跨模块契约测试」时表现出色,可以快速捕捉接口漂移;第三,端到端(End-to-End, E2E)测试从过去的「奢侈品」变成「必需品」,因为它是少数几种能验证「整体行为是否符合用户意图」的手段,而 LLM 最容易在「整体语义」上偏移。
值得强调的是新增的「探索性」一档。探索性测试(Exploratory Testing)指人工或半自动地以「破坏者视角」尝试打破系统,例如随机点击、并发操作、模糊输入(fuzz testing)。这类测试无法被 LLM 完全替代,但 LLM 可以协助生成探索脚本、整理探索结果、分析发现的异常。一种经典做法是:让 Claude 阅读应用的功能列表,自动生成「攻击者会怎么玩」的探索路径,然后由工程师按照路径手工或脚本执行。
1.3 把测试当作「对话契约」而非验证最后一道关卡
给产品经理:在传统流程里,测试是开发的最后一步——「写完代码再补测试」。在 Claude Code 工作流里,测试是开发的「第一步」——它既是给 AI 的需求说明,也是给 AI 的验收标准。
这种范式叫做「测试驱动对话」(Test-Driven Conversation, TDC)。它脱胎于 Kent Beck 的测试驱动开发(Test-Driven Development, TDD),但形式更松散:你不必先写出可运行的测试,只需要把「期望的输入输出对」「期望的边界行为」用文字 + 伪代码的形式甩给 Claude,让它先生成测试,再生成实现。
这样做有三重好处。第一,测试用例本身成为人类与 AI 共享的「契约文档」,避免「我以为我说清楚了」的灾难。第二,AI 会根据测试主动审视自己的实现,减少幻觉。第三,未来重构时,测试套件成为防止 AI 把功能改坏的安全网。
需要特别强调的一点是:测试用例不是越多越好。常见做法是把「测试用例」分为三档——契约测试(Contract Test)、行为测试(Behavior Test)、回归测试(Regression Test)。契约测试只测「函数签名与不变量」、行为测试覆盖主流业务路径、回归测试针对历史 bug 增量补充。三档测试共同构成可持续维护的测试套件,而不是一锅粥。
在 PM 视角下,测试驱动对话的最大价值不在于「代码质量」而在于「需求质量」。当你尝试把一个需求写成测试用例时,你会被迫思考所有「之前没想清楚」的细节:用户输入超长字符怎么办?操作中断怎么办?两个用户同时点击同一个按钮怎么办?这些问题在传统的 PRD 评审中往往被「等开发反馈再说」搪塞过去,而测试驱动对话把它们前置到了需求拆解阶段。这是 AI 协作给产品流程带来的最隐秘也最深远的改变。
二、与 Claude 协作生成测试
2.1 单元测试:让 Claude 同时写实现与测试
给产品经理:单元测试是测试一个函数(或一个小模块)在特定输入下是否返回预期输出。它是测试金字塔的底座,也是 AI 最擅长批量生成的部分。
工程实践中,最稳健的方式是「同步生成」:要求 Claude 在同一轮对话里产出实现和测试,并通过 /think 或 Plan 模式让它先列出测试场景,再写代码。下面是一个推荐的对话模板:
请为以下需求生成 Vitest 测试和 TypeScript 实现:
功能:解析用户输入的时长字符串(如 "2h30m"、"45s"、"1d6h")为毫秒数。
要求:
1. 先在 Plan 模式下列出至少 12 条测试用例(含正常、边界、非法输入)
2. 用例审查通过后再生成实现
3. 实现通过 pnpm vitest run --reporter=verbose 即可验证
4. 禁止吞掉非法输入,必须 throw new Error
Claude 通常会先回应类似下面的用例清单,再开始写实现:
// tests/parseDuration.spec.ts
import { describe, it, expect } from 'vitest'
import { parseDuration } from '../src/parseDuration'
describe('parseDuration', () => {
// 正常场景
it('parses "2h30m" to 9_000_000 ms', () => {
expect(parseDuration('2h30m')).toBe(2 * 3600_000 + 30 * 60_000)
})
it('parses "45s" to 45_000 ms', () => {
expect(parseDuration('45s')).toBe(45_000)
})
it('parses "1d6h" to 108_000_000 ms', () => {
expect(parseDuration('1d6h')).toBe(24 * 3600_000 + 6 * 3600_000)
})
// 边界场景
it('parses "0s" to 0 ms', () => {
expect(parseDuration('0s')).toBe(0)
})
it('parses uppercase "2H30M"', () => {
expect(parseDuration('2H30M')).toBe(2 * 3600_000 + 30 * 60_000)
})
// 非法场景
it('throws on empty string', () => {
expect(() => parseDuration('')).toThrow()
})
it('throws on "abc"', () => {
expect(() => parseDuration('abc')).toThrow()
})
it('throws on negative "-2h"', () => {
expect(() => parseDuration('-2h')).toThrow()
})
it('throws on overflow "9999d"', () => {
expect(() => parseDuration('9999d')).toThrow(/overflow/i)
})
})
这个模板的精髓在于「禁止吞掉非法输入」这一条。LLM 默认倾向于「容错」——遇到非法输入返回 0、null 或空字符串,而不是抛出异常。但真实生产环境中,「静默失败」比「抛异常」更危险,因为它会让上游错误悄悄漫延。把这个原则写进 CLAUDE.md,可以让所有后续生成都保留这一行为约束。
2.2 边界条件测试:用对话探索遗漏的场景
边界条件(Boundary Conditions)是「正常输入与非法输入的交界处」,例如数组的第一个/最后一个元素、整数的最大值/最小值、空集合、null 与 undefined 的差异等。Claude 在第一轮通常会遗漏 30%~50% 的边界,但在被显式追问后会快速补齐。
推荐的对话节奏是:
第一轮:「请列出这个函数的所有可能边界条件,按风险等级排序」。 第二轮:「针对你列出的高风险边界,逐个生成测试用例」。 第三轮:「假设你是一个恶意用户,你会用什么输入打破这个函数?」
第三轮是最有价值的,因为它把 Claude 从「合作模式」切换到「对抗模式」。在对抗模式下,模型会主动提出诸如「输入超长字符串导致正则回溯」「Unicode 控制字符」「JSON 注入」等更深层的边界。
工程师可以把这个三轮节奏沉淀为一个 Skill,命名为 boundary-explore,每次新增函数时主动调用。这样可以避免「记得问对抗问题」这件事本身依赖人脑的记忆。把流程变成可被工具调用的 Skill,是 Claude Code 工作流相比传统 IDE 的核心优势之一。
值得提醒的是:边界条件的「探索深度」取决于你描述上下文的深度。如果你只说「这是一个分页函数」,Claude 顶多能想到 page=0 这种通用边界;但如果你说「这是一个面向千万级数据库表的分页函数,被前端无限滚动列表调用」,Claude 会主动考虑「深翻页性能」「offset 漂移」「并发写入下的数据重复或丢失」等深层边界。上下文密度直接决定了边界覆盖密度。
2.3 集成测试与端到端:Plan 模式审视外部依赖
集成测试(Integration Test)验证多个模块协作的正确性,端到端测试(E2E Test)验证从用户视角的完整流程。这两类测试都涉及外部依赖:数据库、HTTP 接口、文件系统、第三方 SDK。Claude Code 在生成此类测试时,最容易犯的错误是「假设依赖永远可用」——例如直接连接生产数据库、对真实 API 发请求、不清理测试数据。
进入 Plan 模式(按 Shift+Tab 切换)后,要求 Claude 在动手前先输出「依赖矩阵」:每个外部依赖、是否需要 Mock、Mock 的边界、清理策略。下面是一个集成测试的 Vitest + MSW(Mock Service Worker)骨架:
// tests/integration/orderApi.spec.ts
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
import { createOrder } from '../../src/api/order'
// 把外部 HTTP 调用 mock 在测试边界内,CI 不需要真实网络
const server = setupServer(
http.post('https://api.example.com/orders', async ({ request }) => {
const body = await request.json() as { sku: string, qty: number }
if (body.qty <= 0) {
return HttpResponse.json({ error: 'INVALID_QTY' }, { status: 400 })
}
return HttpResponse.json({ id: 'ord_123', status: 'created' })
})
)
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
describe('createOrder integration', () => {
it('creates order on valid input', async () => {
const r = await createOrder({ sku: 'SKU-1', qty: 2 })
expect(r.id).toBe('ord_123')
})
it('throws on qty=0', async () => {
await expect(createOrder({ sku: 'SKU-1', qty: 0 })).rejects.toThrow('INVALID_QTY')
})
})
E2E 层面,常见做法是用 Playwright(Microsoft 出品的开源浏览器自动化框架)。让 Claude 阅读 playwright.config.ts 与既有用例后,生成新流程的 E2E 用例时,可以指定「严格基线」——不允许使用 await page.waitForTimeout(任意数字),必须用 expect(...).toBeVisible() 这类条件等待,从根上避免 flaky test(间歇失败的测试)。
2.4 测试夹具(Fixture)与工厂方法的自动生成
Fixture 指测试运行前准备好的「样板数据」,工厂方法(Factory)指根据少量参数批量构造对象的小工具。手写 Fixture 是繁琐的,但 LLM 生成 Fixture 几乎零成本。建议在项目根放一个 tests/fixtures/index.ts 桶文件,把所有 Factory 集中导出:
// tests/fixtures/index.ts
import type { User, Order, Product } from '../../src/types'
let userIdCounter = 1
export function makeUser(overrides: Partial<User> = {}): User {
return {
id: `usr_${userIdCounter++}`,
name: 'Test User',
email: `test${userIdCounter}@example.com`,
createdAt: new Date('2026-01-01T00:00:00Z'),
...overrides,
}
}
let orderIdCounter = 1
export function makeOrder(overrides: Partial<Order> = {}): Order {
return {
id: `ord_${orderIdCounter++}`,
userId: 'usr_1',
sku: 'SKU-1',
qty: 1,
status: 'created',
...overrides,
}
}
export function makeProduct(overrides: Partial<Product> = {}): Product {
return {
sku: 'SKU-1',
title: 'Test Product',
priceCents: 999,
stock: 10,
...overrides,
}
}
让 Claude 在新增类型时同步更新 fixtures/index.ts,可以避免「测试数据散落各处」的反模式。配合 CLAUDE.md 中的提示「新增类型必须同步生成 Factory」,工程化效果立竿见影。
进阶模式是引入「随机化 Fixture」——基于 faker.js(开源数据生成库)让 Factory 默认生成随机但合法的数据,只在测试需要确定性时用 overrides 覆盖。这样能在测试运行间隙意外发现「依赖固定数据顺序」「依赖某个特定字段长度」的隐藏 bug。需要注意的是:随机化必须配合「种子(seed)记录」机制——一旦某次测试失败,能精确复现当次的随机数据,否则会变成一场调试噩梦。
2.5 让 Claude 阅读现有测试套件,生成补全用例
当项目已经存在一定规模的测试套件后,最有价值的协作方式是「补全」——让 Claude 扫描现有用例,识别遗漏的覆盖角落。推荐对话脚本:
请扫描 tests/ 目录下所有 .spec.ts 文件,并对照 src/ 下的实现:
1. 列出测试覆盖率报告中行覆盖低于 80% 的文件
2. 对每个文件,提出 3~5 条新增测试用例的建议
3. 优先级排序:业务关键 > 边界条件 > 异常路径
4. 不要立即写代码,先输出报告
这一步通常能在十几秒内识别出几十处「漏测点」,远比工程师人工逐文件 review 高效。报告确认后,再分批让 Claude 生成实际用例,每批不超过 10 个,便于人工审核。
三、覆盖率指标的正确用法
3.1 行覆盖、分支覆盖、变异测试的区别
给产品经理:覆盖率(Coverage)是回答「你的测试到底测了代码的多少」这个问题的指标。它有三种粒度,从粗到细:行覆盖、分支覆盖、变异测试。
行覆盖(Line Coverage):被测试执行过的代码行数占总行数的比例。这是最常见的指标,但也最容易误导——一行代码被「执行」不代表它的行为被「断言」过。
分支覆盖(Branch Coverage):每个 if/else、switch、三元表达式的「真分支」和「假分支」是否都被覆盖。比行覆盖更严格,但仍然只能保证「分支被走过」,不能保证「分支的行为正确」。
变异测试(Mutation Testing):自动修改源代码(例如把 > 改成 >=、把 && 改成 ||),然后跑测试套件,看是否有测试因此失败。如果修改了代码但所有测试仍然通过,说明这部分代码「没有真正被测试约束」。常用工具是 Stryker(开源、支持 TS/JS/Vue)。
变异测试是当前最接近「真测试覆盖率」的指标,但运行成本也最高,常见做法是只在每周一次的夜间 CI 上跑全量、平时只跑被改动文件的变异。
3.2 覆盖率达到 80% 之后,剩下 20% 怎么取舍
业界长期存在一个共识:行覆盖 80% 是合理目标,90%+ 是奢侈,100% 是迷信。原因是最后 20% 通常包含三类代码:错误处理分支、防御性代码、平台兼容性补丁。这些代码的「期望行为」是「永远不被触发」,强行覆盖会导致测试本身脆弱。
更好的做法是:让 Claude 对未覆盖代码做「风险标注」——逐行解释「如果这行代码出 bug,最坏后果是什么、触发概率多大」。然后人工决定哪些必须补测试、哪些可以忽略、哪些应该删掉(YAGNI 原则——You Aren't Gonna Need It)。
3.3 让 Claude 解释每条未覆盖代码的风险等级
对话脚本示例:
请对照 coverage/lcov-report 的结果,扫描 src/payment/ 下所有未覆盖的代码行:
1. 按文件分组列出未覆盖的代码块
2. 对每个代码块,标注风险等级(高/中/低):
- 高:涉及金额、用户身份、外部 API 调用
- 中:涉及内部状态变更
- 低:日志、调试、字符串拼接
3. 对高风险代码块,立即补测试用例
4. 对中风险代码块,列入 TODO
5. 对低风险代码块,建议是否可以删除
这个流程可以把「机械的覆盖率指标」转换为「业务驱动的测试投入」,避免团队陷入「为覆盖率而覆盖率」的内耗。
3.4 把覆盖率纳入 CI 守门,但不要做「硬性卡死」
常见做法是设置一个「下限」而不是「目标」——例如「整体行覆盖不低于 75%、新增代码行覆盖不低于 90%」。这样既保证基线,又允许局部低覆盖(例如基础设施代码)存在。Vitest 配置示例:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
thresholds: {
lines: 75,
functions: 75,
branches: 70,
statements: 75,
// 对新增代码(与 main 对比的 diff)要求更严格
// diff 模式需要 vitest 3.x +
},
exclude: ['**/*.config.ts', '**/types/**', '**/fixtures/**'],
},
},
})
「硬性卡死」的反模式是把阈值设到 95% 以上——这会让团队转而绕过测试(写无意义的 expect(true).toBe(true)),反而损害质量文化。
另一个值得警惕的反模式是「覆盖率作为绩效指标」。一旦覆盖率与考核挂钩,工程师会立即学会如何「刷覆盖率」:把没断言的代码塞进测试函数体、对返回值不做任何检查、用 expect.anything() 取代具体断言。这些手法都能让覆盖率数字上升而不真正提升质量。健康的团队会把覆盖率作为「健康度参考」而非「考核目标」——它告诉你哪里值得关注,但不告诉你「该不该奖励某个工程师」。
补充一个常被忽略的视角:覆盖率不仅要看「广度」也要看「时效」。一段三年没改过的代码,即使覆盖率 100%,也可能因为依赖升级而隐式失效;一段昨天刚写的代码,即使覆盖率 60%,因为最近被多次执行,反而更可信。常见做法是引入「测试新鲜度」指标——上次测试通过到现在的时间间隔,把超过 30 天没跑过的测试视为「需要重新验证」。
四、代码审查——AI 与人各司其职
4.1 让 Claude 做「第一遍审查」,工程师做「决策审查」
给产品经理:代码审查(Code Review)是同事在合并代码前互相检查的过程,类似「论文同行评议」。它的目的不仅是抓 bug,更是知识传播、风格统一、责任分担。
AI 协作时代,代码审查可以拆成两层。第一遍由 Claude 做:检查命名、风格、明显的 bug、安全漏洞、与既有代码的一致性。第二遍由工程师做:判断架构选择、业务正确性、可维护性、是否值得合并。
这样分工的理由是:第一遍是「机械活」,AI 比人快 10 倍且不会疲劳;第二遍是「判断活」,AI 没有上下文与责任,无法替代人。Anthropic 在 Claude Code 文档中明确建议:「Never let AI be the final approver of code merging into main.」(永远不要让 AI 做合并到主干的最终批准者。)
实践细节上,可以让 Claude 的「第一遍审查」自动生成一份评审报告(review report),格式包含:每条改动的风险等级、需要工程师重点关注的代码块、AI 自身的不确定性区域(uncertainty zones)。这份报告作为 PR 评论自动发到 GitHub/GitLab,工程师在收到通知后只需要把注意力放在 AI 标注的「不确定区域」上,而不是从头读完整 diff。这种「注意力路由」是 AI 协作给代码审查带来的最实际的提速。
此外,团队应当建立一个共识:如果 Claude 的第一遍审查标注了「PASS」但工程师后续发现了问题,应当把这个 case 反馈到 review-skill 的 Checklist 中,让下次审查能识别此类问题。换句话说,Checklist 是活的——每一次 Claude 漏掉的问题,都应该补一条规则进去。这是「Compound Engineering」(复利式工程)的核心思想:每一次教训都让系统变得更聪明,而不是让单个工程师变得更累。
4.2 审查清单(Checklist)的工程化沉淀
把团队的 review 经验沉淀为 Checklist,存放在 .claude/skills/code-review/ 下,然后让 Claude 每次审查时自动加载。一个示例 Checklist:
# Code Review Checklist
## 命名与风格
- [ ] 函数名是动词短语,变量名是名词短语
- [ ] 没有缩写(除非全队公认,如 `id`、`url`)
- [ ] 文件长度 < 300 行(超过则建议拆分)
## 类型与安全
- [ ] 没有 `any`(除非有注释说明原因)
- [ ] 外部输入有 zod/valibot 校验
- [ ] 异常被显式抛出,没有静默吞掉
## 业务正确性
- [ ] 金额、库存、积分等数值字段使用整数(分/克),不用浮点
- [ ] 时间字段使用 ISO 8601 字符串或 Date 对象,不用本地时间字符串
- [ ] 数据库写入有事务边界
## 测试
- [ ] 新增/修改的逻辑有对应测试
- [ ] 测试覆盖了至少一条非法输入路径
- [ ] 没有 `it.skip` 或 `it.only` 残留
## 安全
- [ ] 用户输入没有直接拼接到 SQL/Shell/HTML
- [ ] 没有把密钥、Token 写进代码
- [ ] 日志没有打印 PII(个人身份信息)
让 Claude 在每次 review 时输出「逐条勾选 + 不通过原因」,可以极大降低人工 review 的认知负担。
4.3 安全敏感代码:永远不让 Claude 拍板
涉及密码学、认证、授权、支付、个人信息(Personally Identifiable Information, PII)的代码,必须人工最终审查。原因不是 AI 写不好,而是责任无法转移——一旦出事,「Claude 给的方案」不能成为团队的免责理由。
实践上,可以在仓库根的 CODEOWNERS 文件中标注「敏感目录」,并在 .claude/settings.json 的 Hooks 中加一条 PreToolUse 规则:当 Claude 试图修改这些目录时,先打印警告并要求显式确认。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"command": "if echo \"$CLAUDE_TOOL_PATH\" | grep -qE '(auth|crypto|payment|pii)/'; then echo 'WARNING: editing security-sensitive code, require human review before commit' >&2; fi"
}
]
}
}
4.4 用 Skills 把团队 Code Review 规范固化
Skills(参考 5.2 节)是 Claude Code 的「可复用工作流模块」。一个 review-skill 通常包含:触发关键词、执行步骤、输出模板、依赖的命令。下面是一个最小化的 review-skill 描述:
---
name: review-pr
description: 对当前分支与 main 的 diff 做完整代码审查
---
## 步骤
1. 运行 `git diff main...HEAD --stat` 获取改动文件清单
2. 对每个改动文件:
a. 读取改动内容
b. 对照 `.claude/skills/code-review/checklist.md`
c. 逐条标注 PASS / FAIL / N/A
3. 汇总输出:
- 总改动行数
- FAIL 项汇总(必须修复)
- 建议项汇总(推荐修复)
- 风险评估(合并到 main 的风险等级:低/中/高)
4. 不自动修改文件,只输出报告
## 禁止
- 禁止假装通过(必须有具体证据)
- 禁止跳过 FAIL 项(即使工程师认为不重要)
4.5 PR 描述与 Commit Message 的 AI 协作
PR 描述与 Commit Message 是「未来的自己」最常依赖的文档。让 Claude 在每次 commit 前自动生成草稿,工程师只做微调,可以让仓库历史的可读性大幅提升。一个推荐的 commit message 模板(中文):
<类型>(<范围>): <简短描述>
<详细说明:为什么改、改了什么、影响什么>
<关联:Issue / 需求文档 / 设计稿>
类型遵循 Conventional Commits 规范(feat/fix/docs/refactor/test/chore/perf)。注意把这条规则写进 CLAUDE.md,否则 Claude 默认会用英文。
PR 描述则更进一步——它不仅描述「改了什么」,还要描述「为什么改」「如何验证」「回滚方案」。这四要素是工程团队应对生产事故时最依赖的信息。让 Claude 在生成 PR 描述时强制包含这四个章节,可以让仓库的「事故响应能力」整体上一个台阶。一个推荐的 PR 描述模板:
## 改了什么
- 新增 `parseDuration` 函数,支持 d/h/m/s 后缀
- 修改 `OrderService.create` 接收新的 timeout 参数
## 为什么改
- 关联需求:PROD-1234 订单超时配置化
- 当前硬编码 30 分钟,运营无法按品类调整
## 如何验证
- 单元测试:`pnpm vitest run tests/parseDuration.spec.ts`
- 集成测试:在 staging 环境创建一笔订单,确认 timeout 生效
- 手工验证:管理后台「订单配置」页面调整后立即生效
## 回滚方案
- 改动是向后兼容的:旧调用方不传 timeout 时使用默认 30 分钟
- 出现问题时直接 revert commit `<hash>`,无需数据迁移
这种结构化的 PR 描述让 reviewer 不必反复在 IM 上追问「这个改动到底想解决什么问题」。把模板存放在 .github/pull_request_template.md 中,结合 Claude 自动生成,工程师只需做最后一道润色。
五、质量调优——从「能跑」到「好用」
5.1 性能调优:让 Claude 阅读 profiling 数据并提出假设
给产品经理:性能调优是把「能跑」的代码变成「跑得快」「跑得省资源」的代码。它分两步:先测量瓶颈在哪,再针对性优化。永远不要凭直觉优化——「过早优化是万恶之源」(Donald Knuth)。
测量阶段,常用工具有:Chrome DevTools Performance 面板、Node.js 内置的 --inspect + --prof、Vue DevTools 的 Performance 面板、node --cpu-prof 生成的 V8 火焰图。把 profiling 输出保存为 JSON 或 HTML 后,可以直接喂给 Claude:
请阅读附件中的 cpu-profile.json(V8 CPU profiler 输出),分析:
1. 占用 CPU 时间最多的前 5 个函数
2. 调用栈最深的路径
3. 提出 3 个优化假设,每个假设:
- 假设的瓶颈成因
- 验证方法(如何确认假设成立)
- 修复方案(如果假设成立)
4. 不要直接动手改,先列假设
这个对话流程的关键是「先假设、后验证、再修复」。LLM 喜欢直接跳到「修复方案」,但跳过假设环节常常会出现「优化了不是瓶颈的地方」。
5.2 内存与资源泄漏的对话式排查
内存泄漏(Memory Leak)在 Node.js / 浏览器场景中常见的根因:未清理的 setInterval、未解绑的 EventListener、闭包持有大对象、缓存无 TTL(Time To Live)。Vue/React 应用中还有「组件卸载未清理副作用」这种典型问题。
排查时把 Heap Snapshot(堆快照)的对比结果交给 Claude,让它识别「时间点 A 到时间点 B 之间,哪类对象数量增长异常」。一旦定位到嫌疑类,再让它扫描代码寻找该类的创建点,最终找到泄漏根因。
5.3 错误处理与可观测性(Logging / Tracing)
可观测性(Observability)是「事后理解系统行为」的能力,由三大支柱组成:日志(Logs)、指标(Metrics)、追踪(Traces)。
让 Claude 帮你统一日志格式(推荐 JSON 结构化日志,便于后续聚合)、规范错误码(按业务模块前缀化,如 ORD_NOT_FOUND、PAY_GATEWAY_TIMEOUT)、添加 OpenTelemetry(CNCF 标准追踪协议)的关键 span。一个常见的反模式是「到处 console.log」,应当让 Claude 在 review 阶段把这些 console.log 替换为结构化 logger 调用。
5.4 用户体验调优:从崩溃率、首屏时间到交互流畅度
前端质量调优的核心指标是 Web Vitals(Google 提出的用户体验三件套):LCP(Largest Contentful Paint,最大内容绘制时间,目标 < 2.5s)、INP(Interaction to Next Paint,交互响应时间,目标 < 200ms)、CLS(Cumulative Layout Shift,累计布局偏移,目标 < 0.1)。
让 Claude 阅读 Lighthouse 报告或 Real User Monitoring(真实用户监控)数据,按「影响最多用户的问题」排序,逐项给出修复建议。注意:影响 5% 用户的 LCP 抖动比影响 0.1% 用户的崩溃更值得优先修复——这是产品判断,不是技术判断。
在前端调优场景下,Claude 还擅长一个传统工程容易忽视的环节:「资源预算」(Performance Budget)。把首屏 JS 体积上限设为 200KB(gzipped)、字体文件总大小上限设为 100KB、首屏图片懒加载阈值设为视口外 200px——这些数字写进 CI 阶段的 bundlesize 或 size-limit 配置后,每一次 PR 都会被自动拦截超标。让 Claude 在每次新增依赖前先估算「这个依赖会让首屏 bundle 增加多少 KB」,可以避免「装一个工具库膨胀 50KB」的常见悲剧。
服务端调优同理——常见做法是设定「每个 HTTP 接口 P99 响应时间不超过 500ms」「每个数据库查询不超过 50ms」「每条消息处理不超过 200ms」。把这些阈值嵌入接口的集成测试或合成监控(Synthetic Monitoring),让 Claude 在新增接口时自动检查并提示风险。
5.5 渐进式重构:让 Claude 一次只改一个维度
重构(Refactoring)是「不改变外部行为的前提下改善内部结构」(Martin Fowler 定义)。AI 协作时,最危险的反模式是「一次大重构」——让 Claude 同时改架构、改命名、改类型、改测试,最终人工无法 review。
正确做法是「一次只改一个维度」:先单独改命名,跑测试,提交;再单独改类型,跑测试,提交;再单独改架构,跑测试,提交。每一步都让 git 历史可回滚。把这条规则写进 CLAUDE.md:「重构必须分维度、分批提交,单次提交不超过 200 行 diff」。
六、质量门禁的工程化
6.1 把测试、Lint、类型检查、安全扫描接入 Hooks
Hooks(参考 5.4 节)是 Claude Code 在工具调用前/后执行命令的扩展点。把质量检查接入 Hooks,可以让「写代码 = 自动跑检查」成为不可绕过的肌肉记忆。一个 .claude/settings.json 的示例:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "pnpm exec tsc --noEmit && pnpm exec eslint --max-warnings=0 \"$CLAUDE_TOOL_PATH\""
}
],
"Stop": [
{
"command": "pnpm exec vitest run --reporter=dot && echo 'all tests passed'"
}
]
}
}
PostToolUse 在每次文件修改后跑类型检查与 Lint,Stop 在 Claude 即将停止时跑全量测试。这两层 Hook 配合,可以把 95% 的低级错误拦在本机。
工程实践中还有第三层值得加上——「PrePush Hook」,在 git push 前跑一次完整的本地 CI。这一层的价值在于:如果远程 CI 跑全量需要 10 分钟,本地 PrePush 跑相同检查只需要 2 分钟(因为可以跳过镜像构建、跨平台兼容性等步骤),把失败拦截在 push 之前可以省下大量「等 CI 失败再来一次」的循环时间。Husky(Git Hooks 管理工具)+ lint-staged(只对暂存文件跑检查)是最常见的本地实现组合。
需要警惕的是「Hook 层级过多导致的开发体验下降」。如果每次保存文件都要等 30 秒检查通过,工程师会本能地寻找绕过方式(例如 --no-verify)。健康的 Hook 设计是「分层快速失败」——最快的检查放最前面(如 Lint < 1s),最慢的检查放最后面(如全量测试),允许工程师在中间任何一层「短路」继续工作,但绝不允许跳过最终的 push 前检查。
6.2 Plan 模式作为「高风险变更的最后一道闸门」
Plan 模式(Shift+Tab 切换)让 Claude 先输出执行计划、等待人工确认后再动手。它特别适合三类场景:数据库 schema 变更、生产配置文件修改、影响超过 10 个文件的批量重构。
实操经验是把这三类场景列入 CLAUDE.md 的硬规则:「修改 prisma/schema.prisma 必须先进入 Plan 模式」「修改 .github/workflows/ 必须先进入 Plan 模式」。这样 Claude 在动手前会自动切换,避免遗忘。
6.3 失败回滚:git 二分 + Claude 协助定位
一旦 CI 失败,最高效的定位手段是 git bisect(二分查找首次引入 bug 的提交)。Claude 可以协助你写 bisect 脚本:
# 给 Claude:请生成一个 bisect 脚本,自动判断当前 commit 是好是坏
# 判断标准:pnpm vitest run tests/regression/order.spec.ts 通过即为好
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect run pnpm vitest run tests/regression/order.spec.ts
定位到引入 bug 的 commit 后,让 Claude 阅读该 commit 的 diff、解释「这次改动为什么会触发 bug」,并给出修复方案。这套流程通常 5 分钟内能定位绝大多数回归问题。
补充一个进阶技巧:当 bisect 跨越的提交数量很大(例如几百个)时,可以让 Claude 先做「语义二分」——根据 commit message 的关键词预判嫌疑提交,例如优先验证包含「refactor」「rename」「upgrade」字样的提交。这种语义启发式定位常常能把 bisect 步数从 9 步压缩到 3 步。
另一种值得掌握的回滚策略是「特性开关回滚」(Feature Flag Rollback)。把每个新功能藏在一个特性开关后面(例如使用 LaunchDarkly、Unleash 或自建的开关服务),出问题时不需要 git revert + 重新部署,直接在配置面板把开关切回 off 即可。Claude 可以协助你识别哪些代码路径应该加开关、生成开关配置的样板代码、并为每个开关写一份简短的「启用/关闭」运维 Runbook。
七、把质量内化为团队习惯
给产品经理:所有工具、配置、Skills 加起来都比不上一件事——团队是否真的相信「质量是大家共同的责任」。AI 协作只能放大已有的文化,不能凭空创造文化。
7.1 周会上谈质量,而不是只谈进度
许多团队的周会只谈「这周做了什么、下周做什么」,质量话题只在事故发生后被动出现。健康的团队会在每次周会预留 10 分钟,固定问三个问题:本周覆盖率涨跌?本周线上错误率涨跌?本周有哪些「绿色 CI 但其实有问题」的差点出事?这种主动复盘会让 AI 协作产生的隐性问题被尽早识别。
7.2 把质量数据变成所有人都能看的仪表盘
测试覆盖率、Lint 警告数、类型 any 数量、PR 平均合并时长、生产错误率——把这五个数字放在团队首页或飞书/钉钉机器人每日推送,会比任何说教都有效。让 Claude 协助生成数据采集脚本与可视化页面(推荐用 Grafana 或自建的简易 Dashboard),一次投入长期受益。
7.3 新人 Onboarding 的质量传承
每来一位新工程师,让 ta 在前两周做一件事——读完仓库根的 CLAUDE.md、.claude/skills/ 下所有 Skill、最近 20 个 PR 的 Review 评论。这个过程比任何文档培训都更高效,因为它把「团队的质量观」具象化为可阅读的对话历史。
更进一步,可以让新人在第一周完成一个「故意失败的练习」——在沙箱仓库里提交一段明知有 bug 的代码,观察团队的 CI、Hooks、AI Review 各自能在哪一层拦下来。如果某一层没拦住,说明那一层的规则需要补强;如果全部拦住了,新人也能直观地理解「我们的安全网到底有多密」。这种「红队演练」式的 Onboarding,会让新人对质量体系建立深刻的肌肉记忆。
7.4 与 AI 协作的「反脆弱」心态
Nassim Taleb 提出的「反脆弱」(Antifragile)概念在 AI 协作时代格外贴切——一个反脆弱的系统不仅能在压力下不崩溃,反而能从压力中变得更强。把这个理念套用到 Claude Code 工作流:每一次 AI 写错的代码、每一次 CI 拦下的问题、每一次线上事故,都应当转化为对 CLAUDE.md、Skills、Hooks 的具体增量改进。三个月之后,你的项目会演化成一个「AI 越用越聪明」的有机体,而不是「AI 越用越乱」的随机森林。
具体落实到对话层面,每周做一次「质量复盘对话」——把本周所有 CI 失败、所有线上告警、所有 review 中发现的问题贴给 Claude,让它分析共性、提炼模式、起草规则更新提案。工程师审查后采纳,规则就此固化进系统。这种「对话即治理」的工作方式,是传统工程团队没有过的全新形态。
总结
测试、审查、调优、门禁这条链路,过去是工程师的「负担」,现在是 AI 与工程师协作的「主战场」。本节给出的所有对话脚本、配置片段、Skills 模板,目标只有一个——让质量从「事后补救」变成「事前内嵌」。当质量约束被结构化地嵌入对话流、Hook 流、CI 流之后,Claude Code 就从一个「会写代码的助手」升级为「会守护代码的伙伴」。下一节(6.5)我们将带着「质量已达标」的代码进入部署上线环节,看 Claude Code 如何协助处理 CI/CD、灰度发布、生产监控、回滚演练,把整个交付链路做成一个端到端可观测、可回滚、可演进的工程系统。
延伸阅读
- Anthropic Claude Code 官方文档
- Boris Cherny: Claude Code 工程心法(YouTube 公开演讲)
- Vitest 官方文档
- Playwright 官方文档
- Mock Service Worker(MSW)官方文档
- Stryker Mutator 变异测试工具
- Mike Cohn: The Forgotten Layer of the Test Pyramid
- Martin Fowler: Test Pyramid
- Google Web Vitals 官方文档
- OpenTelemetry 官方文档
- Conventional Commits 规范
- ISTQB 软件测试基础大纲
- Kent Beck: Test-Driven Development By Example
- Martin Fowler: Refactoring(重构)官方资源站点
- Anthropic: Claude Code Best Practices Blog