1. 一次取消续费需求,让我从“写完就行”改回“先想明白再动手”
我接到过一个看起来特别小的需求:“会员自动续费要允许用户在 App 内取消。”产品同学口述完毕,我脑子里已经闪过整个接口调用链:查订阅状态、调支付渠道的取消接口、更新本地订单表、给用户发一条已取消的站内信。听起来是不是很简单?真正动手后才发现,问题全埋在一堆“常识”里。
取消续费之后,用户这个周期剩余的天数还能不能用?如果用户当天已经扣了下一周期的钱,要退款还是直接关闭自动续费?用户取消后再次点击“续费”,要不要保留原来的支付方式?要不要灰度控制?渠道回调失败时,系统该站在“用户已取消”还是“订阅仍生效”这一边?这些问题没有一个写进需求表里,却全部会在代码评审时被翻出来。结果就是:接口写完,review 了三轮,又被叫去和产品对齐了整整一下午。大家讨论的早已不是代码本身,而是“需求到底是什么意思”。
这件事让我很认真地捡起了“规范驱动开发”(Specification-Driven Development,简称 SDD)。通俗地讲,SDD 不是在代码写完后补一篇文档,而是要求任何一次行为变更,都先产出一份可以被评审、被修改、被接受的“行为规格”,再让代码、测试和评审全部照着这份规格走。我这里说的规格,不是那种几十页、写完就没人看的 PRD,而是放进代码仓库里、能随 git 一起演进、能直接在评审时逐字对齐的小型 spec。
前后试了几周之后,我发现真正让我坚持下来的,是把 openspec-cn 这套思路整理成了适合我自己团队的操作规范。它没有发明什么新概念,只是把“需求背景、目标与非目标、方案取舍、验收标准、落地任务”这些本来就该有但往往散落在聊天记录里的东西,固定成了结构化的仓库资产。你只需要把目录搭出来,把 spec 写清楚,后面所有环节都会自动变得好办很多。
1.1 SDD 和“写设计文档”不是一回事
很多团队听到“先写文档再开发”,第一反应是拒绝。上一套文档流程已经够烦了,再来一个?但 SDD 跟我们印象里的“设计文档”有本质区别。
传统设计文档往往是在需求确定后、开发开始前,由架构师写的一份大而全的方案,包含总体架构、模块划分、数据库设计、接口清单。它的阅读成本高,维护成本更高,开发到一半需求变了,文档却没有跟着变。另一种常见情况是,开发结束之后补一份“系统设计说明”,这种文档只能用来交差,对代码质量没有任何帮助。
SDD 的规格文件走的是完全相反的路线:小步、短命、贴近当前变更。它不需要覆盖整个系统的宏伟蓝图,只需要把你正在做的这一次变更说清楚。比如“允许用户取消自动续费”,就只需要回答四件事:
- 为什么要做这个变更;
- 做出来后用户能感知到什么;
- 哪些是这次要做的,哪些是明确不做的;
- 怎样才算做完了,能被验收。
这份 spec 放在代码仓库的 specs 目录里,和代码一起走 Git 流程。实现代码之前,先开一个只包含 spec 修改的分支,让产品、后端、前端、测试一起 review 这份行为描述。确认没问题后再把 spec 合并进去,接下来实现的每行代码、写的每个测试用例,都以这份 spec 为基准。这不是把文档重做一遍,而是把“需求”从口头和聊天记录里移到一个有版本、有讨论记录、有明确状态的地方。
1.2 openspec-cn 帮我省掉了什么
说来也惭愧,我最初不是直接用了完整的 openspec,而是先从它的文档里借了目录结构和 spec 模板。openspec-cn 本质上是一套面向中文研发团队使用习惯整理出来的规范驱动开发操作约定。它借鉴了 OpenSpec 对目录、命名、评审流程方面的设计,但把模板文案、示例、决策记录方式都做了更适合国内团队直接上手的处理。
我实际用起来,最值钱的是它自动帮我解决了三件平时最消耗精力的事。第一,spec 模板规定了每一份需求文档必须包含哪些小节,产品提需求时不需要我再追着问“边界是什么”“异常场景怎么处理”;第二,目录命名有明确约定,任何一次变更在仓库里都有唯一的编号,PR、Issue、发布说明可以直接引用;第三,规范文件本身是 Markdown 编写的,改动走 Git diff,哪句话变了、谁在什么时候改的、理由是什么,全部留下痕迹。
用上这套约定后,我和产品同学的协作方式也变了。以前产品在 IM 里说一段需求,我听完点头,转头就开始设计。现在我会说:“我先把它写成 spec,你来帮我 review 一下目标和验收标准。”刚开始对方觉得形式化,后来发现有分歧时直接把 spec 里某一句话拉出来讨论,效率比翻聊天记录高很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把 specs 目录搭对,等于成功了一半
很多人以为规范驱动开发的难点在“写文档”,其实真正的门槛在“目录怎么建”。目录决定了 spec 文件能否被自动归类、能否在多人协作时不冲突、能否被 CI 等工具识别。我从 openspec-cn 拿到并实际跑了几个项目后,最后沉淀下来的目录结构大概是这样的:
text复制<你的项目仓库根目录>/
├── specs/
│ ├── features/
│ │ └── billing/
│ │ └── 20250411-cancel-subscription/
│ │ └── spec.md
│ └── decisions/
│ └── 20250411-cancel-subscription-payment-keep.md
└── ...
看到这个结构,有 Git 使用经验的人应该已经能感觉到它的好处:每一次变更都对应一个独立目录,目录名里带日期和短横线主题,天然可排序、可检索。features 下面第一层是模块名,比如 billing、order、user;再往内层才是以日期开头的具体变更目录。
2.1 目录命名的两条铁律
我踩过的第一个坑是命名不规范。第一次我直接建了一个 specs/cancel/ 目录,里面塞了三五份不同功能的 spec。两周后再去看,自己都分不清哪份是哪次需求。后来统一成 模块名/日期-短名称 之后,才真正解决了定位问题。这里有三条约定我建议直接抄走:
- feature 目录名永远用动词短语描述用户可感知的行为,例如
cancel-subscription、export-report,不要用billing-refactor这种听上去就很像内部任务的名称。 - 日期必须用 YYYYMMDD 格式,并且放在名称最前面,这样编辑器里按文件名排序就是按时间排序,演进过程一目了然。
- 同名变更如果发生两次,例如用户取消续费功能做了两期,就用
-v1、-v2区分,千万不要覆盖写旧目录。
配套地,我还在目录下保留了 decisions/ 文件夹,专门存放架构决策记录。为什么要单独拎出来?因为 spec 正文记录的是“现在决定怎么做”,而决策记录要回答“为什么放弃了另一个选项”。两件事放在同一个文件里当然也可以,但一旦决策变多,正文会变得又长又难读,分开维护更清爽。
2.2 spec.md 的六个核心段落
在 openspec-cn 的模板基础上,我根据自己的使用习惯精简成了六个段落,每一段都有不可替代的作用。
- 元信息:写清楚 feature 名称、创建日期、当前状态(Draft / Ready / Accepted / Deprecated)、负责人。这段主要是为了后期检索和自动化流程识别。
- 背景与用户价值:用平实的语言说明为什么需要这个功能,用户现在遇到了什么问题。禁止直接写技术方案,哪怕你心里已经有了完美方案。写背景不是给程序员看的,是给产品、测试和新加入项目的同学看的。
- 目标与非目标:目标可以列三到五条可验证的业务效果;非目标在这里尤其重要,把“这次不做的事”一一列出来,可以挡掉大量后续的“加个顺便”需求。
- 方案与决策:这一段才允许出现接口名、数据表设计、页面改动等实现细节。如果过程中讨论过多个方案,在 decisions 目录里记录选择理由。
- 验收标准:必须是可以执行、可以验证的条目。能写数字就不写形容词,能写具体异常分支就不要写“正常情况如何”。
- 任务清单:当 spec 被接受后,把验收标准拆成可执行的原子任务,并勾选到具体的代码文件或模块。这部分是后面开发任务的直接输入。
这六个段落不是平均用力。在实际开发中,“背景与用户价值”和“目标与非目标”写得越短越清晰,越能避免后面的返工;反而很多人最看重的“方案与决策”是最不重要的,因为方案在评审环节还可以继续调整。
2.3 验收标准要写成能“执行”的语句
写验收标准是新手最容易翻车的地方。大部分人拿到模板会写“用户可以在个人中心找到取消续费入口,点击后成功取消,并收到通知”。这句话读完,你会发现自己根本没法拿它做测试。规范驱动开发的核心价值就在于将验收标准写成人能看懂、机器可验证的语句。
我后来给自己定了一条原则:验收标准里不能出现“可以”“能够”这类模糊动词,必须换成“当...时,系统应该...”的句式。比如:
- 当订阅状态为 active 且自动续费开关为 on 时,用户点击“取消自动续费”,系统应调用支付渠道取消接口,并将本地订单状态改为 cancel_pending。
- 若渠道取消成功,系统应向用户发送“自动续费已取消”通知。
- 若渠道返回失败,系统应保留原订阅状态,并在用户端展示可重试的失败提示。
- 若用户在当前周期内已扣费,取消自动续费后,用户仍可在剩余周期内正常使用会员权益。
这样的标准放到测试人员手上,可以直接转化成用例;放到开发人员手上,也能明确知道哪些分支需要实现。spec 里写得越清楚,后面的代码评审就越轻松。
3. 一个续费需求的三轮改写:从一句话需求到验收级 spec
只看结构不动手,照样学不会。我拿开头那个取消续费需求,走一遍我实际改写了三轮的完整过程。这个过程可以很好地展示 SDD 是如何把模糊需求一步步“逼”清楚的。
3.1 第一版:基本能看,但边界全是洞
最初我按自己的理解,很轻松地写出了 spec 草稿,关键内容是这样的:
markdown复制## 背景
会员用户可能因为价格或其他原因不再希望继续自动续费,需要提供一个取消入口。
## 目标
- 让用户能够取消自动续费。
- 取消后扣费周期不再续费。
## 非目标
- 不涉及退款流程。
- 不涉及优惠券补偿。
## 验收标准
- 用户点击取消按钮后,自动续费状态变为关闭。
- 后续周期不再发起扣款。
给产品同学一看,他立刻问了我三个问题:取消按钮放在哪里?小米退款还退不退?取消之后用户点“恢复续费”还能不能恢复?我愣住了。我只写了“让用户能够取消”,却完全没说取消后用户的价值状态怎么处理。这种 spec 如果直接进入开发,后期上线铁定被用户投诉。第一版的根本问题是没有写“现状约束”和“入口条件”,把用户当成了一个没有前置状态的抽象个体。
3.2 第二版:补齐状态和路径,但方案越界了
第二轮改写,我开始尝试把业务流程写细。我把用户分成了三类:未扣费用户可以立即取消;刚扣费用户可以取消并获得下期退款;已过退款窗口的用户只能取消但不退款。并且我试图直接在 spec 里写出接口路径:PUT /api/v1/subscription/auto-renew/status,参数传 enabled=false。
写出接口路径之后,后端同事立刻反对。原因是现有系统里自动续费开关并不是独立接口,而是跟着订阅单状态走的。如果 spec 直接定义了新接口,但没有说明数据库怎么写、渠道回调怎么同步,前端根本不知道该调哪个。
这一轮给我的教训是:spec 里可以写“用户界面上的操作路径”和“系统行为优先级”,但不应急着编造接口字段。接口设计属于方案层,应在 spec 评审时由相关技术负责人一起讨论,而不是写 spec 的人一个人拍板。到这一步,需求逐渐清晰,但我还是漏了一个重要维度:非目标其实不仅是“无需退款”,还包括“不能影响另一个已存在的行为”。
3.3 第三版:把非目标写明白,spec 才算成立
最后我翻阅了 openspec-cn 的示例,发现几乎所有优秀 spec 都花了不少篇幅写非目标。于是我把第三版中的“非目标”扩写成了这样:
markdown复制## 非目标(明确不在本次范围内)
- 不做自动续费的取消后挽留弹窗。
- 不改变历史已扣费订单的退款策略。
- 不影响“会员到期后手动续费”的行为。
- 不处理用户因违规被平台强制关闭续费的场景。
- 不提供批量取消接口。
## 验收标准
1. 当订阅状态为 active、自动续费开关为 on 且当前时间早于下个扣费日时,
用户可在“会员中心 - 自动续费管理”页面看到“取消自动续费”按钮。
2. 点击后弹出确认框,确认后系统先关闭自动续费开关,再展示取消成功页面。
3. 若支付渠道侧同时存在自动续费协议,系统应调用渠道侧解约接口;
若调用失败,本地开关保持关闭,但系统记录失败原因,不阻塞页面提示。
4. 取消成功后,用户在当前会员周期内仍能正常使用全部会员权益。
5. 取消成功后,用户再次进入同一页面,应看到“开启自动续费”按钮,
点击后可恢复自动续费,无需重新绑定支付方式。
这一版才算真正能驱动开发的 spec。原因很简单:每个验收标准都对应了一个具体场景,每个场景都能被测试用例覆盖,也都能被开发人员翻译成代码分支。它也把“不做挽留弹窗”“不做批量取消”写在了明面上,之后不管是产品想临时塞需求,还是开发想顺手扩功能,都会被这份 spec 拉回来。
3.4 决策记录:为什么留支付方式,要单独写一份 ADR
第三版验收标准里有条“无需重新绑定支付方式”,这背后其实藏着一个方案选择:保留用户已有的支付授权,还是让用户重新走一遍支付流程。当时团队内部有分歧。有人觉得取消续费就应该彻底解约,把支付授权一并删掉,更安全;也有人觉得留着重开方便,用户体验更好。
最后我们在决定保留支付方式的同时,还专门在 specs/decisions/ 下写了一条记录,把“删掉授权”和“保留授权”的利弊列了一遍,并注明最终选择保留的理由是“现有渠道协议允许单独解约自动续费而不删除支付方式,且用户重开率较高”。这份记录在代码评审阶段救了我们一次,因为后来有安全审计同事质疑为什么删了自动续费还要保留支付方式,直接把这个 ADR 拿出来,争议立刻终止了。
这也是为什么我非常建议把决策单独放目录而不是混在 spec 里。spec 更像一份面向当前版本的“事实描述”,ADR 则是“选择的历史”。代码仓库本来就有 git 历史,但那个历史只记录了“改了什么”,没有记录“为什么这么改”。ADR 恰好把后者补上了。
4. 从 spec 到合并 PR:把代码、测试和评审拉回同一张桌子
spec 写明白只是第一步。真正决定 SDD 能不能落地的,是后面这套执行习惯。规范写得再漂亮,如果开发时不看、评审时也不想、测试时不照做,那就是一张废纸。我自己是这样做闭环的。
4.1 先合 spec,再写代码
一个典型的需求流程分成两个 PR。第一个 PR 只加 spec 文件,不包含任何业务代码。这个 PR 的 review 人员包括产品、相关后端、前端,以及可能的测试同学。大家用 Git 的 diff 功能,逐行看验收条件表述是否合适、边界是否完整。达成一致后,spec 被合入主干,到这一步才算需求被正式接受。
为什么必须拆成独立的 PR?因为如果 spec 和代码同时出现在一个大 PR 里,reviewer 很容易被代码实现带跑,注意力全放在“这段逻辑有没有 bug”上,而不会去认真审视“需求描述本身是否成立”。另外,spec 单独合入后,它就有了独立的历史版本,之后代码分支从主干拉出来时,spec 就已经固定下来了,不会出现边写代码边改需求文本的混乱局面。
等 spec 合并后,我再从主干拉出功能分支开始写代码。功能分支通常命名为 feat/cancel-auto-renewal 这种一眼能认出目标的格式。提交信息里要带上 spec 的路径或编号,比如:
text复制feat(member): 实现用户在会员中心取消自动续费
refs: specs/features/billing/20250411-cancel-subscription/spec.md
这个习惯带来的直接好处是:任何人通过 git log 看到提交信息,都能快速跳到对应的 spec 文件,理解当初为什么这么写。代码评审时,reviewer 也不需要在评论区重新问一遍需求背景,点开 refs 里的 spec 就全明白了。
4.2 评审时对照验收标准,而不是对照感觉
功能代码完成后第二个 PR 的 review 清单,我基本不看代码风格,只关心两件事。
第一,当前 diff 是否完整覆盖了 spec 中所有验收标准。我会把 spec 里的验收标准复制到 PR 描述里,每一条对应加上链接到实现代码或者测试用例的引用,例如“第 3 条由 SubscriptionService.cancelAutoRenew() 方法覆盖,测试用例见 test/cancel_auto_renew_test.dart”。这样 review 时不是漫无目的地看 diff,而是拿着清单逐项打勾。
第二,有没有出现 spec 之外的行为。有时开发过程中会发现 spec 没考虑到的情况,比如支付渠道返回了一个 spec 未定义的特殊错误码。这时候正确做法不是悄悄在代码里加一个异常分支,而是回到 spec 发起一次修订,把新情况补充进验收标准。如果验收标准变更了,必须让产品同学也重新确认,防止开发人员自己放大或缩小了需求范围。
4.3 用 Issue 做任务看板,spec 做任务来源
规范驱动开发和敏捷开发并不冲突。我的做法是 spec 合入后,在 Issue 里为任务清单建立关联。每个子任务对应 spec 验收标准中的一到多条,标题直接引用标准编号;Assignee 领取任务后,不需要再去脑补需求,打开 spec 就能干活。
日常站会我们很少讨论“这个功能怎么做”,更多讨论的是“验收标准里第几条还在阻塞”。如果某个任务拖了两天还没完成,大概率是 spec 里有一条边界写得不清楚,而不是开发偷懒。这种情况下我会优先回头修订 spec,而不是让开发硬着头皮继续写。把规范当成活文档,是 SDD 落地中最容易忽略但最重要的一点。
4.4 上线之后要做的回写动作
spec 生命周期并没有在代码合并后结束。功能上线后,我通常还会留两天时间做一次“回写检查”:看看线上有没有出现 spec 没预料到的异常分支。一旦发现,就回到 spec 目录补充一条“已知边界”或修订验收标准,再触发一次代码修正。
这个回写动作是普通开发流程中最容易丢的。大多数人上线后就去忙下一个需求了,留下的问题只能等用户自己发现。通过回写把线上问题和 spec 同步起来,整个项目积累到后期,specs 目录就会变成一个非常完整的“业务行为字典”。新员工入职后不用追着老同事问业务规则,直接翻 specs 目录就能快速了解系统到底支持哪些行为。
5. AI 时代为什么更需要 SDD:把规格变成给 AI 的作业题
2025 年之后,团队里越来越多的人开始用各类代码助手写代码。但很快我们就发现一个现象:你不给 AI 足够明确的任务边界,它就会自行发挥。让它“加一个取消续费的功能”,它可能从路由到数据库、从前端弹窗到后端定时任务全部生成一遍,最后得到一份看起来完整、实际完全不可用的代码。
我自己在用了多种代码助手之后,最大的体会是:在大模型辅助开发的时代,规范驱动开发的价值不仅没有减弱,反而放大了。原因很简单,不管多智能的工具,都需要清楚输入才能有稳定输出。而 spec 恰好提供了一份高度结构化的输入。它把用户价值、目标边界、验收标准、技术约束全部压缩在一个 Markdown 文件里,直接可以作为 prompt 的核心上下文。
5.1 给 AI 的 prompt 不用再靠运气
我现在的习惯是,接到需求后先不急着把 spec 链接丢给 AI 让它“写代码”,而是分三步走:
- 先让人把 spec 写完并 review 通过。
- 把 spec 中的背景、目标与非目标、验收标准复制到会话上下文,作为任务的输入。
- 再让 AI 针对验收标准的某一条去实现,而不是让它一次性完成所有逻辑。
例如:
text复制请根据以下验收标准实现取消续费:
- 当订阅状态为 active 且自动续费开关为 on 时,调用取消接口...
- 当支付渠道解约失败时,本地开关仍关闭,并记录失败原因...
请只修改 SubscriptionService 文件,不要新增前端页面。
如果任务足够大,我会把 spec 拆成更小的子任务再分别交给 AI。一次只让 AI 处理一条验收标准,然后立刻验证。这个模式远比让 AI 一口气写完整个 feature 稳定得多,因为单条验收标准至少是可运行、可测试的,生成结果的偏差很容易被识别出来。
5.2 防止 AI 自己给需求“加戏”,非目标是最重要的刹车
代码助手很机灵,它会基于训练数据里的常见模式补齐全套功能。比如我让 AI“实现取消续费”,它很可能顺带生成一个小米退款逻辑或续费引导弹窗,而这些都写在 spec 的非目标里,明确说了不做。
SDD 在 AI 协作中的关键作用,就是提供一份“非目标清单”作为约束。我在 prompt 里会原样带上非目标部分,并且明确要求:“如果实现过程中发现需要超出上述范围的改动,请停下来告诉我,不要自行补充。”结果 AI 真的会更谨慎地少生成无关代码。这是我在实践里最惊喜的发现:AI 能不能管得住,不完全靠系统提示词,很多时候靠的是任务本身的边界是否足够清晰。
5.3 AI 写测试的能力,也要靠规格来喂
另一个常见误区是让人工检查 AI 生成的测试,结果测试全是自说自话,断言和生成代码完全一致,等于什么都没测。正确做法依然是把验收标准单独提出来,让 AI 针对标准逐条写测试,而不是直接复制业务代码让它写测试。比如:
text复制针对以下验收条款生成测试用例:
- 用户在周期内取消成功后,剩余周期仍可享受会员权益。
这样生成的测试会真正覆盖行为,而不是被实现细节牵着走。我再在代码评审时人工抽查几条极端边界,测试质量就会立刻提升一个档次。
5.4 如果一个代码任务连 spec 都写不出来,先别开发
现在我的团队里有条不成文的规矩:一个需求如果连写 spec 的人自己都说不清验收标准和边界,那就说明这个需求还没准备好进入开发阶段。与其拉上 AI 在代码里试错,不如回到产品讨论阶段把问题问清楚。
这套判断逻辑对人也适用。如果一个需求开发了三天还没有任何代码产出,但 spec 写了八百字,这不是效率低,反而说明这个需求确实想清楚了。搞清楚需求和实现哪个更耗时间,SDD 的价值就能体现得更明显。
6. 哪些情况下我会主动放弃这套流程
说了这么多 SDD 的好处,也得诚恳地讲一讲它不适用或性价比很低的场景。规范驱动开发并不是万能银弹,把流程套在不合适的需求上,只会增加摩擦力。
对只有两三行配置更改、或者纯升级依赖库这类简单任务,我基本不会写完整 spec。比如“把某 SDK 版本从 1.2 升到 1.3”,背景、验收标准清清楚楚,没必要为一个 yaml 文件改动写半页文档。这种情况我会采用轻量模式,在 PR 描述里写两三句话说明变更动机和影响范围,合规性完全可以通过代码 review 来保证。
对于一两个人维护的原型项目或内部工具,也未必需要立刻上规范驱动开发。整套流程最核心的资产其实是“多人共同理解行为边界”,如果项目只有一个人写、一个人用,那这份共识本身就在自己脑子里,写文档反而变成负担。
另外还有一种情况:当团队对业务一无所知、产品需求本身就是一张白纸时,强制写 spec 也容易空转。SDD 更擅长的是把已经被讨论出来的模糊需求逐步澄清,而不是替代产品经理去做市场探索。探索期的需求,更合适的做法是先做最小实验,等技术验证通过后再补 spec、再正式开发。
我对规范驱动开发最深的使用体会是:它不是用来增加文档工作量的,而是用来让失败发生的时机更早。以前我们写代码靠默认判断,问题在评审阶段才暴露,甚至上线后用户反馈才暴露;有了 spec,大多数分歧在 spec 评审阶段就会被大家翻出来,改一行文字的代价远远小于改几十行代码的代价。
如果你现在还被“需求一句话,代码写一天,评审吵三天”折磨,我非常建议先从一个中等复杂度需求开始试水。把 specs 目录建起来,把背景、目标与非目标、验收标准这三段先写明白,然后强迫自己在动手前先让产品同学 review 这份文件。坚持两三个迭代之后你会发现,代码评审的议题重心会自然地转移到“实现是否满足预期”上,而不再是“需求原文到底是什么”。
最后再分享一个小技巧:spec 不只是给这轮需求用的。当项目做满半年,specs 目录会积累成一份非常宝贵的业务变更史。下次产品提一个新需求,你可以先翻翻 specs 目录里有没有相似功能,把旧 spec 复制出来改改,比从零开始问需求要快得多。这份积累,才是规范驱动开发给人最大的复利。
