这两个月我把主力工作流切到了 spec coding,配合 spec-kit 做规格驱动开发。先给结论:对于边界模糊、逻辑分支多的业务模块,先把需求写成机器可读的规格说明,再让 AI 基于规格生成代码和测试,效果比传统“一句需求 + 一段 prompt”的方式稳定太多。如果你现在还停留在“把需求丢给 ChatGPT 让它自由发挥”的阶段,我建议你耐心看完全文,这套方法能把你从“AI 写代码我返工”的循环里捞出来。
我先快速解释一下 spec-kit 是什么。它不是一个普通的代码生成器,而是一套围绕规格说明组织开发流程的工具链:你把业务规则用规定格式写清楚,它负责校验规格的完整性、生成测试骨架、生成实现骨架,并且打通回归验证。换句话说,它让规格文件不只是“给 PM 看的文档”,而成了真正能驱动开发、驱动测试、驱动验收的工程资产。
这篇文章我会从“为什么必须换工作方式”讲起,拆解 spec-kit 的工作流设计,再用一个完整的购物车场景把从规格到代码再到测试的整条链路跑一遍,最后把我这两个月踩过的坑和一些已经落地的经验分享出来。无论你是后端、前端还是测试转开发岗,这套思路都值得抄一份。
1. 为什么我放弃了“拿到需求就动手写代码”这种工作方式
1.1 传统 prompt 开发模式的三个致命问题
先说个大实话:大部分人在用 AI 辅助写代码时,效率其实没有提升多少,只是把工作量转移了而已。你让它写一个结算模块,它给你生成 200 行代码,然后你开始 review。问题在于,它前置信息不足,只能基于自己的训练经验脑补需求,你如果不说清楚“满减和折扣能不能叠加”“优惠券是否影响运费计算”,它就默认按最简单的方式处理。结果就是:代码看起来能跑,但业务上到处是窟窿。
我过去两个月用传统方式写过三个业务模块,每次都是这个循环:
- 我给 AI 一段需求描述,它生成一个初版实现;
- 我 review 时发现某个边界条件没处理;
- 我补一句让它修,结果它把另一个已经正常的逻辑改坏了;
- 反复修三轮之后,我终于失去耐心,自己上手写。
这个循环的本质问题不是 AI 能力不行,而是需求信息没有结构化的传递通道。你给它的需求描述是散文,它给你的代码是逻辑,散文到逻辑的翻译损耗,最后全部由你的 review 时间来承担。
1.2 需求描述的“散文”问题与可执行契约
传统工作流里,需求从产品经理口中出来,变成一句话:比如“购物车结算时要根据数量给折扣”。这句话有很多种解读方式。数量是“件数”还是“品类数”?折扣是“整单打折”还是“超出的部分打折”?能不能和其他优惠叠加?这些不确定项,在代码里就是一堆 if-else,而且每个人写的 if-else 还都不一样。
spec coding 的基本思想,是把需求描述从“散文”翻译成“行为契约”。一段规格不是“购物车结算时要根据数量给折扣”这种自然语言,而是:
- 当购物车内有 3 件同款商品,单价 50 元,且满足“满 2 件打 9 折”条件时;
- 结算结果应为 135 元。
一旦规则写成这样,它就同时成为三样东西:给 AI 看的实现依据、给测试框架用的断言数据、给产品经理验收用的确认单。这三样东西来自同一个文件,就不会出现“开发理解的边界”和“产品预期的边界”不一致的问题。
1.3 spec-kit 在这套方法论里的角色
方法论可以不用任何工具,纯手工也能做。例如自己写一个 Excel 表,每条规则一行,再手工去对应测试用例。但问题是规则一多,格式难以统一,校验逻辑很难自动化,生成测试用例的过程又要重复劳动。
spec-kit 起到的作用,就是把这套方法论固化成标准流程。在我实际用的版本里,它处理的是这么几个环节:
- 规格解析:读取预设目录下的 spec 文件,检查结构是否合法;
- 完整性校验:检查每条规则是否包含输入、条件、预期结果三要素,缺失即报错;
- 测试生成:基于规则生成对应语言的测试骨架,测试直接引用规则 ID;
- 实现生成:根据规则生成初版实现代码,标注出未覆盖的分支;
- 回归挂接:把生成的测试接入现有测试框架,跑通后整个规格进入“已实现”状态。
这套流程解决的问题,是让“规格驱动开发”不依赖个人自觉,而是靠工具强制约束。你写少了它不让过,你写错了它告诉你哪里不完整。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. spec-kit 的规格文件结构:先看懂它想要的文档长什么样
2.1 一个最小 spec 文件长什么样
我目前主力使用 Python 技术栈,所以下面例子是 Python 生态下的样子。spec-kit 支持的规格格式是 Markdown + YAML frontmatter 混合体,核心由四个区域组成:元信息、行为场景、业务规则、验收断言。
markdown复制---
id: cart-001
title: 购物车金额结算
status: in-progress
owner: backend-team
tags: [cart, payment, discount]
non-goals:
- 不处理跨境税费
- 不处理优惠券组合叠加
---
# 购物车金额结算
## Scenario: 单件商品无折扣结算
- Given 购物车内有 1 件商品,单价 100 元
- When 发起结算
- Then 应付金额为 100 元
## Scenario: 两件以上商品享受数量折扣
- Given 购物车内有 3 件同款商品,单价 50 元
- And 当前满足“满 2 件打 9 折”条件
- When 发起结算
- Then 应付金额为 135 元
我把 frontmatter 里的 non-goals 单独标出来,因为这是很多人忽略但价值极高的字段。AI 在生成代码时,最大的问题是“过度实现”:你让它结算,它顺手把运费模板、优惠券、会员等级全考虑进去了,给你的代码复杂度凭空多三倍。non-goals 就是告诉生成器“哪些东西不该碰”。
2.2 为什么会话式的 Given-When-Then 比自然语言更靠谱
很多第一次接触 spec-kit 的人会问:这不就是拿 Markdown 写需求吗?用自然语言写“购物车里如果超过两件商品,结算时应付金额应打九折”不是更短吗?
关键在于可解析性。Gherkin 风格的 Given-When-Then 不是给人看的,是给机器看的。spec-kit 背后的解析器需要从语法层面识别“输入条件”“动作”“预期输出”三要素,才能据此生成测试代码。自然语言写“如果超过两件商品”,解析器无法可靠判断“超过两件”是条件还是动作,也无法自动提取出测试数据。
举个实际例子。下面这句话:
购物车有 3 件商品,单价 50 元,满 2 件打 9 折,应付 135 元。
人很容易理解,但机器提取“输入 = 3 件乘 50 元”和“预期 = 135 元”就不太稳定。而 Given-When-Then 格式把输入、动作、输出强制分到了三个语义槽里,生成的测试代码结构就是完整的。
python复制def test_cart_discount_when_multi_items():
# Given 购物车内有 3 件同款商品,单价 50 元
items = [{"price": 50, "quantity": 3}]
# When 发起结算
total = check_out(items)
# Then 应付金额为 135 元
assert total == 135
你可能觉得这是小事,但当你维护 300 条规则、每周都有新增时,这种结构化的价值就体现出来了:每条规则对应的测试用例是自动生成的,规则变更后测试跟着改,不会出现“需求改了但测试忘了同步”的情况。
2.3 frontmatter 字段:为什么我要强调状态和归属
规格文件不是写完就完了,它是一个持续迭代的活文档。所以我建议像管代码一样管规格文件。我在团队里强制要求每个规格文件必须有下面的字段:
| 字段 | 作用 | 我踩过的坑 |
|---|---|---|
id |
全局唯一编号,测试命名和 git commit 都用它 | 漏写之后,测试无法追踪到规格来源 |
title |
功能名,用于 code review 时快速定位 | 写得太泛,review 时根本看不出改了啥 |
status |
draft / in-progress / implemented / archived |
没有状态时,旧规格会被误当成新需求 |
owner |
负责这个模块的人 | 多人协作时必须靠它圈定责任 |
non-goals |
明确不做什么 | 这个字段治好了 AI 的“过度实现” |
举个例子,一个规格的 status 是 archived,但还在测试目录里存在。新同事接手时看到,以为是当前需求,照着旧规则写了个新模块,结果和现有逻辑冲突。后来我们约定:进入 archived 状态后相关测试必须从测试套件中移除,这不是图干净,而是明确信息边界。
3. 一次完整的 spec coding 实战:从一句话需求到可回归测试
3.1 需求背景与准备
纸上谈兵没意思,直接走一遍完整流程。我拿一个常见的电商场景举例:购物车结算。需求原文就一句话:
用户结算购物车时,系统应计算商品总价,如果同款商品数量达到 2 件及以上,享受 9 折优惠。
这句话在传统流程里会被直接交给开发,然后开发大概率会在代码里写:
python复制def check_out(items):
total = sum(item["price"] * item["quantity"] for item in items)
if total >= 100:
total *= 0.9
return total
注意,这里已经出现了一个理解偏差:需求说的是“同款商品数量达到 2 件及以上”,但开发往往随手写成“总价满 100 打九折”,这俩根本不是一回事。这就是散文式需求的天坑。所以我们要做的是,在写任何代码之前,先把这句话拆成可验收的行为规则。
3.2 编写规格文件:把需求文本变成可执行规则
我建了一个目录结构,把规格和代码分开管理:
code复制cart-module/
├── specs/
│ └── cart-001-settlement.md
├── src/
│ └── cart.py
└── tests/
└── test_cart_settlement.py
然后我基于那句需求,写完整规格文件。这里我故意把需求里的边界情况都显式标出来:
markdown复制---
id: cart-001
title: 购物车结算-数量折扣
status: in-progress
owner: backend-team
tags: [cart, discount, settlement]
non-goals:
- 不处理跨品类优惠
- 不处理折扣叠加
---
# 购物车结算-数量折扣
## Scenario: 单件商品无折扣
- Given 购物车内有 1 件商品,单价 100 元
- When 发起结算
- Then 应付金额为 100 元
## Scenario: 同款商品两件触发折扣
- Given 购物车内有 2 件同款商品,单价 50 元
- And 同款数量达到 2 件,享受 9 折
- When 发起结算
- Then 应付金额为 90 元
## Scenario: 同款商品三件触发折扣
- Given 购物车内有 3 件同款商品,单价 50 元
- And 同款数量达到 2 件,享受 9 折
- When 发起结算
- Then 应付金额为 135 元
## Scenario: 不同商品不叠加数量
- Given 购物车内有 A 商品 1 件,单价 50 元
- And B 商品 1 件,单价 50 元
- When 发起结算
- Then 应付金额为 100 元
注意最后一条,我是故意加进去的。它是一条边界规则:需求说的是“同款商品数量达到 2 件”,不是“购物车总数量 2 件”。如果规格文件里不写清楚这个边界,AI 很可能把“购物车里有两件不同商品”也判定成折扣。这条规则就是用来兜住这个理解偏差的。
3.3 用 spec-kit 生成测试骨架
规格文件写完后,运行 spec-kit 的解析命令。我用的命令大致是:
bash复制spec-kit generate tests --spec specs/cart-001-settlement.md --output tests/
它会基于规格文件里的每个 Scenario 生成一个测试函数,测试用 scenario 名生成命名规则,并挂上规格 ID
python复制import pytest
from cart import check_out
def test_cart_001_scenario_single_item_no_discount():
items = [{"name": "A", "price": 100, "quantity": 1}]
assert check_out(items) == 100
def test_cart_001_scenario_two_items_trigger_discount():
items = [{"name": "A", "price": 50, "quantity": 2}]
assert check_out(items) == 90
def test_cart_001_scenario_three_items_trigger_discount():
items = [{"name": "A", "price": 50, "quantity": 3}]
assert check_out(items) == 135
def test_cart_001_scenario_different_items_not_counted():
items = [
{"name": "A", "price": 50, "quantity": 1},
{"name": "B", "price": 50, "quantity": 1},
]
assert check_out(items) == 100
这里有个细节值得强调:生成的测试里,输入数据来自规格文件里的 Given 部分,预期输出来自 Then 部分。生成的只是骨架,数据完全由规格决定。你不用手动对着需求文本整理测试数据,这其实就是把“写测试”里面最机械的那部分自动化了。
3.4 第一版实现:为什么故意选一个有缺陷的写法
为了展示规格能兜住多少错误,我故意先写一个有问题的版本,然后在测试驱动下修正。这比你直接看最终正确的实现更有参考价值。第一版我写的是:
python复制def check_out(items):
total = sum(item["price"] * item["quantity"] for item in items)
if len(items) >= 2:
total *= 0.9
return total
然后跑测试:
text复制test_cart_001_scenario_single_item_no_discount PASSED
test_cart_001_scenario_two_items_trigger_discount PASSED
test_cart_001_scenario_three_items_trigger_discount PASSED
test_cart_001_scenario_different_items_not_counted FAILED
第四个测试失败了,原因是 len(items) >= 2 判断的是购物车里的商品种类数,而不是某一款商品的件数。这个 bug 在最开始的那句散文需求里完全看不出来,但规格文件里一旦显式写了“不同品类数量不该叠加”,问题立刻暴露。
修正实现:
python复制def check_out(items):
total = 0.0
for item in items:
if item["quantity"] >= 2:
total += item["price"] * item["quantity"] * 0.9
else:
total += item["price"] * item["quantity"]
return total
再次跑测试,四条全部通过。到这里,规格→测试→实现→回归的闭环就完成了。
3.5 验证通过后:规格状态流转与提交规范
测试通过后,把规格文件的 status 从 in-progress 改为 implemented,然后提交代码。提交信息我会带上规格 ID,形成可追溯链:
text复制feat(cart): implement cart-001 settlement quantity discount
- spec: cart-001-settlement.md
- scenarios: 4
- tests: test_cart_settlement.py
这条 commit 信息的好处是,两个月后你翻 git log,看到 cart-001,能直接跳回规格文件,看到这套逻辑当时是怎么定义的。如果后面需求要改,也是先改规格文件,再改代码,再跑测试。这个顺序顺序顺序很重要:先改规格,后改代码。
4. 实战一个月后的高频踩坑清单:这几条能救你大命
4.1 规格写了实现细节:模糊的正确好过精确的错误
这是我犯过最严重的错误。我一开始写规格时,喜欢把自己脑子里想的实现方案写进去,比如:
Given 购物车有商品列表,When 遍历列表并调用
apply_discount函数,Then 返回折扣后的金额。
这看起来没问题,但实际上是把代码实现提前锁死了。AI 基于这条规格生成代码时,只能照着 apply_discount 这个函数名去写,哪怕它有更简洁、更合理的实现方式,也不能用。更糟的是,当实现方案需要调整时,你改的不只是代码,还要回头改规格文件,成本直接翻倍。
正确的写法应该聚焦行为和数据结果:
Given 购物车内有 3 件同款商品,单价 50 元,And 同款数量达到 2 件享受 9 折,When 发起结算,Then 应付金额为 135 元。
规格里只保留“输入状态”和“输出结果”,至于 135 元是怎么算出来的,留给实现去决定。这就像你打车时只告诉司机目的地,不需要告诉他走哪条路。下面的表格是我自己定的规格边界检查标准:
| 内容 | 是否适合写进规格 | 原因 |
|---|---|---|
| 业务规则、输入输出关系 | 适合 | 这是验收的依据 |
| 函数名、类名、模块划分 | 不适合 | 属于实现约束,会限制生成器 |
| 交互行为(用户点了什么) | 适合 | 决定测试场景的触发条件 |
| 具体算法步骤 | 不适合 | 让实现者自行优化 |
| 性能指标(毫秒级响应) | 不适合做硬断言 | 环境波动大,适合在专项测试里做 |
4.2 规格粒度失控:每个函数都写 spec 会让你痛不欲生
刚开始推行 spec coding 时,我犯了一个“过度设计”的错误:要求团队对每个工具函数都写规格。结果规格文件数量是代码文件的三倍,维护成本剧增,写代码的时间比直接用 AI 生成慢很多,团队差点因此放弃这套方案。
后来我总结出一套粒度标准。需要写规格的,是下面这么几类:
- 核心业务规则,比如折扣、价格计算、权限判断,这些错了业务就错了;
- 对外接口边界,比如给其他服务调用的 API 的入参出参行为;
- 状态转换逻辑,比如订单从“待支付”到“已支付”的流转条件;
- 容易产生理解偏差的规则,比如“数量”到底指单品件数还是总件数。
不需要写规格的:
- 纯内部工具函数,比如字符串格式化;
- 无业务含义的数据加工;
- 只看一眼就明白的简单 getter/setter。
我的具体建议是:一个模块先只针对“让业务方看了能点头”的规则写规格,其他内部实现自由发挥。规格是契约,契约太多,每改一处需求都要同步修改一份契约,最终契约就被无视了。
4.3 AI 脑补不存在的需求:让 non-goals 当守门员
用 spec-kit 时,一个常见操作是让 AI 帮你从聊天记录或需求文档生成规格文件初稿。这个功能很好用,但有个让人头疼的问题:AI 会习惯性脑补需求。有一次我让它从一段运营给的结算需求描述里生成规格,它自动加了一条规则:“所有订单满 100 元包邮”。可原始需求里根本没提运费。
这种脑补规则直接进了 spec,就会生成测试,测试会要求实现包含运费逻辑,导致代码复杂度暴涨。解决办法就是我在前面反复提到的 non-goals 字段。我现在的流程是,AI 生成 spec 初稿后,人工 review 时必须做两件事:
- 逐条确认场景是不是原始需求里真的提到过;
- 把所有原始需求里没有的情况写进
non-goals,明确告诉生成器“不要动这部分”。
这个过程很像帮 AI 划边界:“你只要做这三件事,其他一概不管”。不是 AI 能力不够,而是业务需求本身就是有范围的,范围不划清楚它只能自己猜。
4.4 断言数据缺少场景描述:不要只留一个数字
规格生成的测试骨骼里,断言通常长得像:
python复制assert check_out(items) == 135
如果某一天这个测试挂了,你从报错里只能看到“期望 135,实际 130。那么问题来了:135 这个期望值代表的是哪个业务场景?为什么是 135?这就是我坚持在每条规则的人可读名称里带场景 ID 的原因。
测试函数名已经带了 scenario_three_items_trigger_discount,但还有一个更保险的做法:在规格文件里给每个 Scenario 加一个一句话的“业务意图”。比如:
markdown复制## Scenario: 同款商品三件触发折扣
- 业务意图:验证数量折扣是按单品件数计算,不是按品类数计算
- Given 购物车内有 3 件同款商品,单价 50 元
- And 同款数量达到 2 件,享受 9 折
- When 发起结算
- Then 应付金额为 135 元
这样即使函数名被重构改掉了,测试代码里的注释仍然保留了业务上下文。测试挂掉时,你能立刻明白它验证的是什么,而不是猜半天这个数字是哪来的。
4.5 规格变更没有质量闸门:比代码 bug 更隐蔽的坑
最后一条经验,也是我认为最容易被忽略的:规格文件是源头,源头一变,生成的测试和代码全都要跟着变。如果规格可以随意改动而不经过任何质量检查,那整套流程的价值就打了折扣。
我现在在 CI 里加了三个检查项:
- 规格格式校验:跑
spec-kit validate,确认 YAML frontmatter 合法,Scenario 结构完整,没有缺失 Then; - 测试与规格同步性检查:校验生成的测试文件是否和规格文件匹配,防止有人直接改了测试但没改 spec;
- 规则数量变化报告:拉取本次变更前后规则数量的 diff,让评审者一眼看到新增、删除了哪几条规则。
这三个检查把“改规格”这件事变得更谨慎。改代码只是改实现,改规格改的是契约。契约出了问题,实现再完美也没用。
5. 从“用工具”到“建规范”:我的 spec-kit 落地经验
5.1 先试点一个模块,不要全组一拥而上
如果你是团队里第一个引入 spec coding 的人,别急着跟全组推广。先找一个逻辑复杂度适中、边界清晰的核心业务模块,用 spec-kit 完整跑一遍,让全过程可见。把这个模块的规格文件、测试生成过程、代码实现放在一起,组内能直接看到“从需求到测试”的完整链路,比讲一百页 PPT 有效得多。
我当时选的试点模块是订单金额计算,一个典型的高分支逻辑模块。跑完三天后,另一个同事主动来问这个流程是怎么用的,因为他发现这个模块的需求评审会异常顺利——产品经理直接对照规格验收条件逐条确认,不再像以前那样“我口头说个大概,你看着办”。
5.2 团队约定:哪些规则必须写“可验收断言”
推行规范化不能靠自觉,得靠约定。我在团队里建立了一个简单的分级规则:
- P0 规则:面向收入、数据安全、核心流程。必须写成规格中的显式 Scenario,且必须有自动生成的测试覆盖;
- P1 规则:面向普通业务逻辑,逻辑可能变化。写成规格,但允许只写当前需求的场景,不需要刻意覆盖所有边界;
- P2 规则:非核心工具逻辑,不强制写规格。
这个分级的意义:让 P0 规则有高于普通代码的审查门槛,P1 规则保持灵活性,P2 规则不增加负担。没有这个分级时,大家容易走极端:要么什么都不写,要么把规格文件写成负担。
5.3 把规格当资产:版本管理、溯源、回归三者一体
规格文件要进 git,这个没有商量余地。我实际使用中发现,规格文件天然是很优秀的“文档资产”:它有 ID,有 owner,有状态,有验收条件。每次迭代后,规格文件的 status 从 draft 流转到 implemented,形成一条历时记录。
当线上出了一个问题,比如“某个订单的折扣算错了”,排查路径是反着走的:
- 看 git 里这条规则对应的模块最后一次变更;
- 找到这个模块的规格文件;
- 看对应 Scenario 的测试和实现。
这条链路比我以前直接打开代码找函数、再翻 git log 猜当初意图,要快十倍。
5.4 最后补一个实用技巧:给每个规格写“非目标”
前面几次提到 non-goals,最后我再细说一次这个字段怎么用。每次写规格时,除了写清楚“这条需求要做什么”,一定要花时间写下“这条需求明确不做什么”。比如你写“购物车结算”,非目标可以列:
- 不处理优惠券叠加;
- 不处理跨境商品税费;
- 不支持拆单发货;
- 不处理会员等级折扣。
这些非目标不是废话,它们直接决定 AI 生成代码时“少管闲事”到什么程度。没有这些约束时,我见过 AI 生成一个购物车结算函数,内部同时预判了配送方式、库存锁定和支付渠道。那些代码不是错的,但在这个需求里是多余的。spec coding 的一个核心价值,就是用规格把解决方案收敛到需求的真实边界里。
我个人现在的体会是,spec coding 真正改变的并不是写代码的方式,而是把“需求理解”这个环节重新放回了流程的核心位置。以前我们默认需求理解是产品经理的人,开发拿到文字后直接跳到实现。现在规格文件强迫每个人在动手之前,先把行为和验收条件想清楚。这件事在前 AI 时代靠个人自觉,在 AI 时代变成了整个工作流的起点。你可以把 spec-kit 当成一套脚手架来用,也可以只是手动照着这个思路把规格写下来。方法都是同一个:先定义清楚问题和边界,再让 AI 动手。
