软件工程# 软件工程

AI Coding 时代,为什么要先写 Spec 再写代码?

AI Coding 时代,为什么要先写 Spec 再写代码?
文章摘要通过AI生成文章摘要
DyuGPT
AI Coding 时代,为什么要先写 Spec 再写代码?

AI Coding时代,代码生成速度加快,但需求不明确会导致方向错误和浪费。因此,先编写Spec(规格说明)至关重要,它能明确系统行为、边界和验收标准,减少理解偏差。SDD(规格驱动开发)通过先分析、编写最小Spec、验证方案、绑定测试、检查偏差等步骤,确保实现符合真实需求。核心在于将模糊决策提前到编码前,避免AI自行猜测隐藏规则。

使用 AI Coding 工具时,很多人习惯直接输入一句:

text
帮我实现一个优惠券领取接口。

几分钟后,Agent 可能已经创建了 Controller、Service、Mapper 和测试代码。

代码生成得很快,但真正的问题才刚刚开始:

  • 一个用户能领取几次?
  • 库存扣减是否允许超卖?
  • 重复请求如何处理?
  • 数据库失败后是否需要回滚?
  • 接口返回结构能不能修改?
  • 优惠券过期后应该返回什么?
  • 并发请求下什么结果才算正确?

如果这些问题没有提前说明,AI 只能自行猜测。

这也是为什么在 AI Coding 时代,SDD——Spec-Driven Development,规格驱动开发,开始变得越来越重要。

它的核心并不复杂:

在编写代码之前,先明确系统应该做什么、不能做什么,以及如何证明它已经完成。

一、什么是 Spec?

Spec 可以翻译为规格、规范或需求说明。

但它不是一份几十页的形式化文档,也不等于传统的产品需求文档。

对一个具体开发任务来说,一份可用的 Spec 至少应该回答四个问题:

text
目标是什么?
输入和输出是什么?
有哪些约束和异常情况?
如何判断任务已经完成?

例如,“实现优惠券领取接口”不是一个完整的 Spec。

更完整的描述应该是:

markdown
## 目标

为用户提供优惠券领取接口。

## 业务规则

1. 一个用户只能领取同一张优惠券一次;
2. 优惠券必须处于可领取时间范围内;
3. 库存不能小于 0;
4. 并发请求不能造成超卖;
5. 重复请求需要返回第一次领取结果;
6. 领取成功后异步发送通知。

## 接口约束

- 不修改现有响应结构;
- 用户未登录时返回 401;
- 库存不足时返回业务错误码 COUPON_SOLD_OUT;
- 优惠券不存在时返回 COUPON_NOT_FOUND。

## 验收标准

- 单用户重复请求只能生成一条领取记录;
- 100 个并发请求竞争 10 张库存时,成功数量必须等于 10;
- MQ 重复投递不能重复发送权益;
- 数据库异常时不能永久扣减库存。

有了这些规则,开发者和 Agent 才知道应该实现什么。

二、为什么 AI 写代码越快,Spec 越重要?

传统开发中,程序员会在编码过程中不断补全需求。

遇到模糊的地方,可以询问产品,也可以根据项目经验作出判断。由于代码写得相对慢,很多问题会在实现过程中逐渐暴露。

AI Coding 改变了这个节奏。

Agent 可以在需求仍然模糊时,迅速生成一整套“看起来完整”的代码。

速度越快,错误方向造成的浪费反而越大。

例如,需求本来需要数据库条件更新:

sql
UPDATE coupon
SET stock = stock - 1
WHERE id = ?
  AND stock > 0;

但由于 Spec 没有写“并发下不能超卖”,Agent 可能生成:

java
Coupon coupon = couponRepository.findById(id);

if (coupon.getStock() <= 0) {
    throw new BusinessException("库存不足");
}

coupon.setStock(coupon.getStock() - 1);
couponRepository.save(coupon);

这段代码在单元测试里可能完全正常,但在并发场景下存在风险。

问题不是 AI 不会写原子扣减,而是需求没有告诉它:

原子性是这个功能的必要条件。

AI 擅长把明确需求转换成代码,却不擅长替业务方决定隐藏规则。

因此,AI Coding 的关键能力不只是写 Prompt,而是编写可以执行和验证的 Spec。

三、SDD 和传统需求文档有什么区别?

传统需求文档通常偏向产品视角,例如:

text
用户点击领取按钮后,可以成功领取优惠券。

这句话能描述用户体验,但不足以直接指导工程实现。

SDD 更关注系统行为:

text
当用户第一次领取且库存充足时:
- 创建领取记录;
- 库存减少 1;
- 返回领取成功。

当相同用户重复领取时:
- 不重复扣减库存;
- 不创建新记录;
- 返回已有领取结果。

当多个用户并发领取最后一张优惠券时:
- 只能有一个请求成功;
- 其余请求返回库存不足。

它不会规定每一行代码怎么写,但会明确:

  • 系统状态如何变化;
  • 哪些行为必须发生;
  • 哪些行为绝对不能发生;
  • 哪些结果可以被测试验证。

可以把二者简单理解为:

text
需求文档:用户想要什么
Spec:系统必须表现成什么样
代码:系统具体如何做到

四、一个适合 AI Coding 的 SDD 流程

在实际项目中,不需要建立非常复杂的制度。

一个功能可以按照下面五步推进。

第一步:先让 Agent 分析,而不是直接写代码

不要一开始就输入:

text
帮我实现接口。

可以先输入:

text
分析这个需求,不要修改代码。

请输出:

1. 当前调用链;
2. 需要确认的业务规则;
3. 可能影响的数据表;
4. 并发和事务风险;
5. 建议的验收标准。

Agent 的第一项任务不是编码,而是帮助发现 Spec 中缺少的信息。

第二步:编写最小可用 Spec

Spec 不需要很长,但必须具体。

推荐至少包含:

markdown
## 背景
为什么需要这个功能。

## 目标
这个任务要解决什么问题。

## 非目标
本次明确不处理什么。

## 行为规则
正常、异常和边界情况下系统如何表现。

## 技术约束
不能修改的接口、表结构或依赖。

## 验收标准
通过哪些测试证明完成。

其中“非目标”非常重要。

例如:

markdown
## 非目标

- 本次不增加优惠券转赠功能;
- 不修改前端交互;
- 不调整现有优惠券表结构;
- 不处理历史重复数据。

它可以防止 Agent 顺手进行没有必要的扩展和重构。

第三步:让 Agent 根据 Spec 输出实现方案

此时依然不要急着改代码。

text
根据 Spec 给出实现方案。

说明:

1. 修改哪些文件;
2. 每个文件负责什么;
3. 事务边界如何设计;
4. 幂等如何实现;
5. 如何处理失败补偿;
6. 准备增加哪些测试。

不要修改代码。

这一步的目标,是检查 Agent 对 Spec 的理解是否正确。

第四步:实现并绑定验收标准

确认方案后,再让 Agent 开始编码:

text
按照已经确认的 Spec 和方案实现。

要求:

1. 不修改 Spec 之外的功能;
2. 不改变现有接口协议;
3. 每条验收标准必须有对应测试;
4. 完成后逐条说明验收结果。

此时测试不再是代码写完后的附属品,而是 Spec 的可执行证明。

第五步:检查代码是否偏离 Spec

功能完成后,不应该只问:

text
代码有没有 Bug?

更应该问:

text
根据 Spec 审查当前 Git Diff。

逐条检查:

1. 是否满足全部业务规则;
2. 是否实现了非目标中的内容;
3. 是否改变了现有接口;
4. 每项验收标准是否有证据;
5. 是否存在 Spec 未定义但代码自行决定的行为。

这一步能够发现一种常见问题:

代码本身看起来合理,但实现的并不是原始需求。

五、Spec 最重要的部分不是功能,而是边界

很多 Spec 会详细描述正常流程,却忽略失败场景。

例如:

text
用户领取优惠券后,系统扣减库存并生成记录。

这只描述了理想情况。

真正决定系统质量的,通常是下面这些边界:

  • 用户重复点击怎么办?
  • 请求超时后再次重试怎么办?
  • Redis 成功但数据库失败怎么办?
  • 数据库成功但 MQ 发送失败怎么办?
  • MQ 重复消费怎么办?
  • 优惠券刚好在请求过程中失效怎么办?
  • 两个请求同时扣减最后一张库存怎么办?

因此,一份好的 Spec 应该主动写出系统不变量。

所谓不变量,就是无论发生什么情况,都必须成立的规则。

例如:

markdown
## 系统不变量

1. 优惠券库存永远不能小于 0;
2. 同一用户和优惠券只能存在一条有效领取记录;
3. 同一个消息重复消费不会产生额外权益;
4. 接口失败不能留下无法恢复的中间状态;
5. 所有库存变化必须能够追踪。

这些内容比“使用 Redis 还是数据库锁”更重要。

技术方案可以变化,但系统不变量不能被破坏。

六、SDD 和 TDD 是什么关系?

SDD 和 TDD 并不是互相替代的关系。

TDD,也就是测试驱动开发,通常遵循:

text
先写失败测试
→ 编写最小实现
→ 测试通过
→ 重构

SDD 解决的是:

text
我们到底要实现什么?

TDD 解决的是:

text
如何通过测试逐步实现它?

可以把它们组合起来:

text
Spec 定义业务行为

验收标准转换成测试

先运行测试并确认失败

编写最小代码

测试通过

根据 Spec 再次审查

例如 Spec 中有一条:

markdown
100 个并发请求竞争 10 张库存时,只能成功 10 次。

对应的测试就应该真正模拟并发,而不是只测试单线程下库存从 10 减到 9。

如果测试无法证明 Spec 中的规则,测试数量再多也没有意义。

七、Spec 不应该变成新的形式主义

SDD 最大的风险,是把简单开发任务变成文档工程。

修改一个按钮文案,不需要写十页 Spec。

判断一个任务需要多详细的规格,可以看三个条件:

text
业务规则是否复杂?
失败代价是否很高?
是否需要多人或多个 Agent 协作?

对于简单任务,几行就够:

markdown
## 目标

将登录页按钮文案从“提交”改为“登录”。

## 约束

只修改中文文案,不调整布局和接口。

## 验收

中文环境显示“登录”,英文环境保持不变。

对于库存、支付、权限、数据迁移等高风险功能,则应该把异常场景和不变量写得更完整。

Spec 的目标不是增加文档数量,而是减少理解偏差。

八、真正的 SDD,是把模糊决策提前

很多软件问题并不是代码写错了,而是团队从未明确讨论过某个问题。

例如:

text
用户重复提交到底算成功还是失败?

如果 Spec 没有确定,Controller、Service、前端和测试可能会给出四种不同答案。

SDD 的价值,就是把这些模糊决策从上线后提前到编码前。

它会迫使团队先回答:

  • 这个功能真正的目标是什么?
  • 哪些行为必须保持兼容?
  • 什么情况算成功?
  • 什么情况必须失败?
  • 系统失败后应该处于什么状态?
  • 我们怎样证明实现是正确的?

这些问题一旦明确,后面的代码反而更容易写。

结语

AI Coding 降低了代码的生产成本,却没有降低正确理解需求的成本。

当 Agent 可以在几分钟内生成数百行代码时,开发中最昂贵的部分正在从“怎么写”转向:

text
写什么
为什么写
什么才算写对

SDD 并不是要求所有团队先写厚重文档。

它真正强调的是一个简单原则:

在让人或 AI 编写代码之前,先把目标、边界和验收标准说清楚。

未来优秀的开发者,可能不只是最会写代码的人。

更重要的是,他能够把一个模糊想法,转化成一份清晰、可执行、可验证的 Spec。

因为 AI 可以很快完成实现,但只有人能够先决定:

我们到底想让系统变成什么样。

继续阅读更多文章