规格先行:把需求写成 AI 可执行的规格与验收标准
AI 生成的代码不好用,多数时候不是模型能力不够,而是你给的规格只有一句话:"帮我写个优惠券核销"。这句话里缺了输入输出、缺了边界、缺了验收方式,模型只能按训练数据里最常见的写法猜一遍——猜出来的代码能编译,但和你的业务无关。
本章要解决的问题
- 同一句需求,AI 每次生成的实现都不一样,评审成本比手写还高;
- 产出"看起来对",却没有可执行的判定标准,只能靠肉眼读代码;
- 并发、过期、超发这些边界全靠临场想起,漏一个就是线上事故;
- 规格没写清的部分,AI 会自己发明字段名、状态值、错误码,然后扩散到整个项目。
规格(spec)的作用是把"猜"变成"对齐"。一份能交给 AI 的规格只需满足三条硬约束:输入输出是类型、边界条件是枚举、验收标准是命令。
规格模板
把下面这段存成 docs/spec/coupon-redeem.md,每次开新任务复制一份填空:
# 规格:<功能名>
## 背景
一句话说明这个功能在业务里的位置,以及为什么现在要做。
例:用户领取的优惠券需要在订单支付前核销,核销失败要给出可展示的原因。
## 输入输出
- 入口:<Go 函数签名 / HTTP 路由 / 消息主题>
- 输入:字段名、类型、是否必填、取值范围
- 输出:成功返回什么;失败返回哪些错误值(哨兵错误 + 对外文案)
## 边界条件
逐条枚举,不要写"等等""其他情况":
- 券不存在 / 已使用 / 已过期 / 不属于该用户
- 订单金额低于券门槛
- 同一张券被并发核销两次
- 数据库超时、行锁等待超时
## 验收标准
必须可执行、可证伪,每条都能被命令或断言判定:
- go test ./internal/coupon/... -run TestRedeem -count=1 全绿
- 并发 100 次核销同一张券,恰好 1 次成功,其余返回 ErrCouponUsed
- 核销成功后 status 由 unused 变为 used,used_at 非空
## 明确不做的事
- 不实现券的发放与退款退回
- 不新增数据库字段,不改表结构
- 不引入新的第三方依赖
最后一段「明确不做的事」最容易被省略,收益却最高。AI 越界往往不是能力问题,而是你没划边界。
把模板变成提示词
按 docs/spec/coupon-redeem.md 的规格实现 coupon.Service.Redeem。
约束:
1. 只输出 internal/coupon/redeem.go 一个文件;
2. 签名必须与规格一致:Redeem(ctx context.Context, userID, couponID int64) error;
3. 错误值只用规格里列出的哨兵错误,不要新造错误类型;
4. 边界条件逐条对应到代码分支,每个分支后用注释标注对应的边界条目;
5. 不修改其他文件,不新增依赖。
先只输出实现方案(函数拆解 + 事务边界 + 加锁方式),我确认后再写代码。
最后一句是关键:先要方案,再要代码。让模型在文字层先暴露理解偏差,比让它先写 200 行再返工便宜得多。
把模糊需求翻译成规格
| 模糊需求 | 翻译后的可执行规格 |
|---|---|
| "写个核销接口" | 签名 Redeem(ctx, userID, couponID int64) error,事务内 SELECT ... FOR UPDATE 加行锁 |
| "处理各种异常" | 枚举 6 个哨兵错误:ErrCouponNotFound、ErrCouponUsed、ErrCouponExpired、ErrCouponNotOwner、ErrBelowThreshold、ErrCouponLocked |
| "保证不超发" | 原子更新 UPDATE coupon SET status='used' WHERE id=? AND status='unused',判断 RowsAffected == 1 |
| "性能要好" | 单次核销 TP99 < 20ms,不允许全表扫描,需附 EXPLAIN 结果 |
| "加个测试" | 表驱动用例 ≥ 8 条,含并发用例,go test -race 通过 |
右列每一行都可以被验证:类型能被编译器验证,错误值能被断言验证,性能能被 EXPLAIN 验证,"加个测试"能被 -race 验证。左列任何一行都验证不了。
验收标准要写成命令
验收标准(acceptance criteria)的质量,取决于别人能否不读代码就判定通过:
| 写法 | 可判定性 | 说明 |
|---|---|---|
| "功能正常" | 不可判定 | 谁也不知道什么叫正常 |
| "核销成功返回 nil" | 部分可判定 | 只覆盖成功路径 |
"go test -run TestRedeem -count=1 全绿" | 可判定 | 有命令、有退出码 |
"并发 100 次仅 1 次成功,且 -race 无告警" | 强可判定 | 覆盖竞态,可复现 |
把验收命令直接写进规格,AI 就获得了自检能力:它跑一次测试就知道自己是否完成,而不是把"我觉得写好了"丢给你。注意 -count=1 不能省,否则改动后可能直接命中测试缓存。
常见坑
- 一句话需求就开工:产出不可复现,改两轮之后只能推倒重写;
- 规格里写"等等""类似情况":AI 会按概率补全,通常补成最常见而非你要的那一种;
- 只写正常路径:并发、超时、幂等全靠后续线上事故补课;
- 验收标准是形容词:好、快、稳都无法被证伪,等于没写;
- 忘记写"不做的事":AI 顺手加了缓存层、事件总线和新依赖,范围悄悄翻倍;
- 规格只存在于对话里:第二轮对话上下文一变就丢了,一定要落盘成仓库里的 markdown,用
@docs/spec/xxx.md引用。
小结:规格先行的核心是三条硬约束——输入输出写成类型、边界条件写成枚举、验收标准写成命令。"明确不做的事"与正面需求同等重要。把规格落成仓库文件,让 AI 自己跑验收命令,你只需要审结果。