一、引言
在之前的一篇文章《AI TDD 开发方式初体验》中,我分享了使用 AI 辅助 TDD 开发的初步尝试,那次体验让我看到了 AI 辅助编码的巨大潜力,但也暴露出一个问题:
AI 生成的代码结构和命名往往不可控。
同样的需求,AI 可能给出完全不同的文件结构、类名、方法名。这对于个人项目或许无妨,但在团队协作中,这种不确定性是致命的。
如何让 AI 在 TDD 流程中输出可控、可预期的代码?
答案是:轻量Spec。
本文将以一个完整的"猜数字"游戏为例,展示当 AI TDD 遇上轻量Spec,会碰撞出怎样的火花。

二、什么是轻量Spec

2.1 定义
轻量Spec 是介于需求文档和代码实现之间的一层轻量级设计规格。
需求文档 → 轻量Spec → 代码
(做什么) (怎么组织) (具体实现)
它不是详细设计文档,不需要画 UML 图,也不需要写伪代码。它只需要回答几个关键问题:
- 有哪些类/函数?叫什么名字?
- 放在哪些文件里?
- 测试策略是什么?
2.2 轻量Spec 的结构
以下是本次猜数字游戏的完整轻量Spec:
Module: GuessNumber Game
Rules:
- 答案是 4 个互不重复的数字(0~9)
- 玩家每次输入也是 4 个互不重复的数字
- A = 数字与位置都一致的个数
- B = 数字存在但位置不一致的个数
- 返回格式:"{A}A{B}B"
- 猜中即得到 "4A0B",游戏结束
Attempts:
- 玩家最多可猜 6 次
- 超过 6 次仍未猜中 → 返回 "You are lose"
History:
- 系统记录每次 Guess 的输入与结果(A/B)
- 可通过 GetHistory() 获取历史记录(按顺序保存)
Invariants:
- 输入必须为 4 位不重复数字,否则返回 error
- 游戏结束后再次 Guess → 返回 error
Design:
- 设计类为Game,构造时注入答案,核心方法为Guess,文件名为game.go
- 答案生成属于领域规则,函数名为GenerateAnswer,文件名为game_service.go
- API为函数Guess,文件名为api.go
- 故事级测试考虑Guess,GenerateAnswer使用gomonkey/v2打桩,测试文件为api_test.go
- 任务级测试仅考虑GenerateAnswer,测试文件为game_service_test.go
- Game类不单独测试
可以看到,轻量Spec 包含五个部分:
| 部分 | 作用 |
|---|---|
| Rules | 核心业务规则,AI 生成代码的逻辑依据 |
| Attempts | 边界条件,约束游戏流程 |
| History | 功能需求,明确 API 接口 |
| Invariants | 不变式,定义异常处理规则 |
| Design | 架构设计,约束代码结构和命名 |
这样的结构既包含了业务规则(做什么),又包含了技术设计(怎么组织),让 AI 既能理解需求,又能输出符合预期的代码结构。
2.3 特点
| 特点 | 说明 |
|---|---|
| 轻量 | 几行到几十行,AI 辅助10分钟写完 |
| 聚焦 | 只关注结构,不关注实现细节 |
| 够用 | 足够指导 AI,又不过度设计 |
三、为什么需要轻量Spec

3.1 没有 Spec 时的 AI 输出
让 AI 直接根据需求开发,可能得到这样的结构:
guess-number/
├── main.go
├── game/
│ ├── game.go
│ ├── game_test.go
│ └── generator.go
├── handler/
│ └── handler.go
└── models/
└── record.go
问题来了:
- 为什么要分 game 和 handler 两个包?
- generator.go 和 game.go 为什么要分开?
- 每个类都有测试,是否过度测试?
不同时间、不同上下文,AI 给出的结构可能完全不同。
3.2 有 Spec 时的 AI 输出
有了轻量Spec,AI 生成的结构完全可控:
guess-number/
├── api.go ← 指定的
├── api_test.go ← 指定的
├── game.go ← 指定的
├── game_service.go ← 指定的
└── game_service_test.go← 指定的
每个文件的名字、每个函数的命名,都和 Spec 一致。
3.3 对比总结
| 维度 | 无 Spec | 有 Spec |
|---|---|---|
| 文件结构 | 随机 | 可控 |
| 命名规范 | 不一致 | 统一 |
| 测试策略 | AI 自行决定 | 人为指定 |
| 返工成本 | 高 | 低 |
| 团队协作 | 困难 | 顺畅 |
四、AI TDD 的红绿重构循环
4.1 经典 TDD 循环
TDD 的核心是红-绿-重构循环:
🔴 红:写一个失败的测试
↓
🟢 绿:写最少的代码让测试通过
↓
🔄 重构:优化代码,保持测试通过
↓
(回到红,开始下一个循环)
4.2 AI TDD 的变化
在 AI TDD 中,这个循环变成了人机协作:
| 阶段 | 传统 TDD | AI TDD |
|---|---|---|
| 🔴 红 | 人写测试 | AI 生成测试,人审查 |
| 🟢 绿 | 人写实现 | AI 生成实现,人审查 |
| 🔄 重构 | 人重构 | AI 重构,人审查 |
人的角色从"编码者"变成了"审查者"和"决策者"。
4.3 轻量Spec 在循环中的作用
轻量Spec 就像一份契约,约束 AI 在每个阶段的输出:
轻量Spec(约束)
↓
🔴 红:AI 生成测试 → 文件名、函数名、打桩方式都符合 Spec
↓
🟢 绿:AI 生成实现 → 类名、方法名、文件结构都符合 Spec
↓
🔄 重构:AI 优化代码 → 保持结构不变,只优化内部实现
五、实战:猜数字游戏开发
5.1 准备工作
开发前,我准备了以下文件:
requirement.md(需求)
猜数字游戏规则:
- 输入4个0~9中不同的数字
- 返回格式"?A?B",A表示数字和位置都对,B表示数字对位置不对
- 猜中显示"4A0B",游戏结束
- 6次没猜中,显示"You are lose"
story.md(故事拆分)
| 故事 | 描述 |
|---|---|
| 故事1 | 计算数字和位置都相同的个数(A) |
| 故事2 | 计算数字相同但位置不同的个数(B) |
| 故事3 | 控制玩家尝试次数(最多6次) |
| 故事4 | 记录猜数历史 |
spec.md(轻量Spec)
Module: GuessNumber Game
Rules:
- 答案是 4 个互不重复的数字(0~9)
- A = 数字与位置都一致的个数
- B = 数字存在但位置不一致的个数
- 返回格式:"{A}A{B}B"
Attempts:
- 玩家最多可猜 6 次
- 超过 6 次仍未猜中 → 返回 "You are lose"
History:
- 系统记录每次 Guess 的输入与结果
- 可通过 GetHistory() 获取历史记录
Invariants:
- 输入必须为 4 位不重复数字,否则返回 error
- 游戏结束后再次 Guess → 返回 error
Design:
- 设计类为Game,核心方法为Guess,文件名为game.go
- 答案生成函数名为GenerateAnswer,文件名为game_service.go
- API为函数Guess,文件名为api.go
- 故事级测试使用gomonkey/v2打桩,测试文件为api_test.go
- Game类不单独测试
tcode-rule.mdc(测试代码规则)
请你作为TDD专家级程序员,按照以下要求生成测试代码:
- 测试代码仅仅能够满足当前故事就可以,不要考虑后续故事
- 测试代码要求是GWT格式(given-when-then三段式)
- 使用goconvey测试框架
示例:
Convey("测试场景", t, func() {
Convey("given 前置条件", func() {
Convey("when 执行操作", func() {
Convey("then 验证结果", func() {
So(result, ShouldEqual, expected)
})
})
})
})
tdd-rule.mdc(AI 开发流程)
1. 选择第一个故事
2. 根据当前故事和轻量Spec生成测试代码
3. 根据测试代码和轻量Spec生成产品代码
4. 测试成功(失败则自动修复,最多3次)
5. 重构代码
6. 测试成功
7. 选择下一个故事,循环直到结束
5.2 红绿重构实战
一句指令启动开发:
@spec.md @requirement.md @story.md @tdd-rule.mdc
请开发该需求
故事1:实现 xA 判断
🔴 红:生成测试代码
AI 生成了符合 Spec 的测试文件 api_test.go:
func TestGuess_Story1_PositionMatch(t *testing.T) {
Convey("测试数字和位置都相同的个数", t, func() {
Convey("given 答案为1234,输入全部匹配", func() {
ResetGame()
patches := gomonkey.ApplyFunc(GenerateAnswer, func() string {
return "1234"
})
defer patches.Reset()
Convey("when 输入1234", func() {
result, err := Guess("1234")
Convey("then 返回4A0B", func() {
So(err, ShouldBeNil)
So(result, ShouldEqual, "4A0B")
})
})
})
})
}
注意 AI 生成的测试代码同时符合两个规则:
- 测试文件名
api_test.go✅ 符合 Spec 中的 Design - 使用
gomonkey打桩 ✅ 符合 Spec 中的 Design - GWT 三段式结构(given-when-then)✅ 符合 tcode-rule
- 使用 goconvey 框架 ✅ 符合 tcode-rule
- 仅测试当前故事 ✅ 符合 tcode-rule
🟢 绿:生成产品代码
AI 生成了三个文件:
game.go(Game 类):
type Game struct {
answer string
}
func NewGame(answer string) *Game {
return &Game{answer: answer}
}
func (g *Game) Guess(input string) (string, error) {
// 验证输入
if err := g.validateInput(input); err != nil {
return "", err
}
// 计算A
a := g.countA(input)
return fmt.Sprintf("%dA0B", a), nil
}
api.go(API 层):
func Guess(input string) (string, error) {
if game == nil {
answer := GenerateAnswer()
game = NewGame(answer)
}
return game.Guess(input)
}
game_service.go(答案生成):
func GenerateAnswer() string {
// Fisher-Yates 洗牌算法生成4位不重复数字
...
}
运行测试:
$ go test -v -gcflags=all=-l ./...
=== RUN TestGuess_Story1_PositionMatch
测试数字和位置都相同的个数
given 答案为1234,输入全部匹配
when 输入1234
then 返回4A0B ✔✔
--- PASS
🔄 重构:本轮无需重构
故事2:实现 xB 判断
🔴 红:添加测试
Convey("given 答案为1234,数字全对但位置全错", func() {
Convey("when 输入4321", func() {
result, err := Guess("4321")
Convey("then 返回0A4B", func() {
So(result, ShouldEqual, "0A4B")
})
})
})
🟢 绿:添加 countB 方法
func (g *Game) countB(input string) int {
count := 0
for i := 0; i < 4; i++ {
for j := 0; j < 4; j++ {
if i != j && input[i] == g.answer[j] {
count++
break
}
}
}
return count
}
测试通过 ✅
故事3 & 故事4
按同样的红绿重构循环,AI 完成了:
- 尝试次数控制(最多6次)
- 历史记录功能
5.3 开发成果
项目结构(完全符合 Spec):
guess-number/
├── cmd/main.go # 主程序
├── api.go # API 层
├── api_test.go # 故事级测试
├── game.go # Game 类
├── game_service.go # 答案生成
├── game_service_test.go # 任务级测试
└── README.md
测试结果:
=== RUN TestGuess_Story1_PositionMatch ✔✔✔✔✔✔
=== RUN TestGuess_Story2_DigitMatch ✔✔✔✔✔✔
=== RUN TestGuess_Story3_Attempts ✔✔✔✔
=== RUN TestGuess_Story4_History ✔✔✔✔✔✔
=== RUN TestGenerateAnswer ✔✔✔✔✔✔✔✔✔✔
34 total assertions
PASS
4个故事,从零到完成,约7分钟。
六、轻量Spec 的最佳实践
6.1 推荐的 Spec 结构
| 部分 | 内容 | 作用 |
|---|---|---|
| Rules | 核心业务规则 | AI 理解"做什么" |
| Invariants | 边界条件、异常处理 | AI 处理边界情况 |
| Design | 架构、命名、文件结构 | AI 输出可控 |
6.2 配套的测试规则(tcode-rule)
| 规则 | 目的 |
|---|---|
| 仅满足当前故事 | 避免过度设计 |
| GWT 三段式 | 测试结构清晰 |
| 指定测试框架 | 统一技术栈 |
6.3 不该写什么
| 内容 | 原因 |
|---|---|
| 算法细节 | 让 AI 自由发挥 |
| 具体实现 | 这是 AI 的工作 |
| 过多约束 | 会限制 AI 的能力 |
6.3 粒度把握
太粗:只写"实现猜数字游戏" → AI 输出不可控
太细:写出所有函数签名 → 不如自己写代码
刚好:架构 + 命名 + 测试策略 → 可控且高效
原则:够用就好。
七、总结与思考

7.1 核心公式
AI TDD + 轻量Spec = 人做设计决策,AI 做红绿重构
- 人的职责:需求分析、架构设计、轻量Spec、代码审查
- AI 的职责:生成测试、生成实现、自动重构
7.2 适用场景
| 场景 | 是否适用 |
|---|---|
| 团队项目 | ✅ 保持风格一致 |
| 需要与现有代码集成 | ✅ 遵循现有规范 |
| 有明确架构要求 | ✅ Spec 约束输出 |
| 探索性原型 | ⚠️ 可以让 AI 自由发挥 |
| 简单脚本 | ❌ 杀鸡用牛刀 |
7.3 未来展望
轻量Spec 解决了 AI TDD 的"可控性"问题,让 AI 真正成为可靠的编程伙伴。
未来可以探索:
- 更复杂业务场景下的轻量 Spec 模板
- 轻量 Spec 的 AI 辅助生成
- 多 Agent 协作下的 Spec 分工
- Spec 与代码一致性的 AI 评审
AI 不会取代程序员,但会用 AI 的程序员将取代不会用的。
而轻量Spec,正是驾驭 AI 的缰绳。