ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SDD+Spec-kit:规格先行,终结前后端接口扯皮

SDD+Spec-kit:规格先行,终结前后端接口扯皮 开需求评审会的时候我经常遇到这样的场景产品讲完一个需求后后端、前端和测试对这个字段到底允不允许为空异常情况下返回什么状态码这个状态机到底有几种合法流转各执一词。最尴尬的是谁都拿不出一个可以执行、可以验证的东西来当裁判。后来我开始尝试 SDDSpecification-Driven Development规范驱动开发配合 Spec-kit 这类工程化工具把先写规格再写代码变成实际可落地的工作流这类争执确实少了很多。这篇文章就围绕 Spec-kit 的链路拆解、落地步骤和踩坑经验展开适合正在带团队搭规范、或者想改善接口协作和代码验收方式的技术负责人、后端开发、全栈工程师参考。1. 为什么我会从写完代码补文档转向SDD1.1 传统流程里三个说不出口的真相先说说我为什么放弃原先那套代码优先文档随缘的做法。最直接的原因是我复盘过几个延期严重的项目发现根因不在编码速度而在大家理解不一致。产品以为的需求、后端实现的逻辑、前端期望的接口、测试写出的用例四者经常对不上。这里有三件大家都不太愿意直说的事。第一文档是奢侈品不是必需品。需求刚确定时没人写文档项目中期开始补等到代码改了几轮之后文档已经和实际实现脱节了。第二代码评审靠感觉。评审时大家各自带着对需求的理解去 review但对的实现长什么样没有一个统一基准所以评审意见经常变成个人偏好之争。第三测试其实测的是实现不是需求。测试代码是照着函数写的而不是照着需求规格写的需求变了测试往往还挂在旧语义上。用一个类比来说这就像盖房子的时候不画施工图先让工人砌墙砌完再补图纸。发现问题时要么砸墙重来要么将就着住。代码领域的结果比房子还糟糕因为代码改起来键盘一敲就行大家更容易陷入先写了再说的循环后续返工成本反而更高。1.2 规格先行到底解决了什么问题SDD 的核心思路其实特别朴素在写代码之前先写一份机器可读、人也读得懂的规格说明spec把需求转换成明确的结构化描述包括做什么、边界条件是什么、输入输出长什么样。然后让工具基于这份 spec 去生成代码骨架、测试骨架和文档让实现和规格始终保持对应关系。我给团队引入这套思路之后最大的变化不是大家变得更爱写文档了而是讨论问题的锚点变了。以前讨论需求时扯的是 我觉得应该……现在讨论的是spec 里这条验收标准到底对不对。分歧提前暴露在写代码之前解决成本比改代码之后低得多。这里要区分一个概念SDD 里的 spec 和我们常说的设计文档不是一回事。设计文档写完后基本是纸靠人自觉去同步spec 在 Spec-kit 这类工具里是契约可以被解析、校验、生成代码甚至可以挂在 CI 里检查实现有没有偏离规格。文档是给人看的规格是给人和工具一起用的这是本质差异。2. Spec-kit 给工作流带来的第一个变化spec 成为一等公民2.1 spec 怎么设计才具备工程价值用 Spec-kit 的第一步是写 spec。但怎么写直接决定了这个工具链的价值上限。我见过不少初尝试的人把 spec 写成了一段产品需求摘要例如用户输入邀请码系统校验是否有效。这种 spec 生成出来的东西和没写一样因为缺少可判定的细节。我的建议是用结构化格式来写比如 Markdown 加 frontmatter或者直接用 YAML/JSON。核心字段建议包含唯一标识、需求描述、接口契约、验收标准、依赖约束、优先级。下面是我在项目里比较常用的一份 spec 结构以邀请码校验接口为例id: auth-invite-001 title: 邀请码有效性校验 description: 用户在注册前提交邀请码系统返回该邀请码是否有效及其所属套餐类型 priority: high depends_on: [] interface: method: POST path: /api/v1/auth/validate-invite request: - name: code type: string required: true description: 邀请码长度6-12位 response: success: status: 200 content_type: application/json schema: valid: boolean plan_type: string expires_at: string failure: status: 422 schema: error_code: string message: string acceptance_criteria: - 邀请码为空时返回422error_code为EMPTY_CODE - 邀请码长度小于6位或大于12位时返回422error_code为INVALID_LENGTH - 邀请码不存在时返回200valid为falseplan_type为空字符串 - 邀请码存在且未过期时返回200valid为trueplan_type为对应套餐值为什么一定要结构化因为纯文本的段落无法被程序可靠解析工具就没法在后续帮你做生成、校验和追踪。结构化相当于给需求打了标签让机器知道哪些字段是接口定义、哪些是验收标准、哪些是依赖。这一步做扎实后面生成的东西才有价值。2.2 从 spec 到代码骨架的生成逻辑Spec-kit 的第二个能力是从这份 spec 生成代码骨架。它不会帮你写业务逻辑而是把所有已经被规格定义清楚的部分生成出来。具体来说它会生成的内容包括接口路由的 stubs、请求和响应的数据类型定义、DTO 类、以及对应测试文件的基本结构。拿上面邀请码的例子来说工具会生成类似这样的路由和类型定义。以 TypeScript 为例// generated from spec: auth-invite-001 export interface ValidateInviteRequest { code: string; } export interface ValidateInviteSuccessResponse { valid: boolean; plan_type: string; expires_at: string; } export interface ValidateInviteFailureResponse { error_code: string; message: string; } export async function validateInvite(req: ValidateInviteRequest): PromiseValidateInviteSuccessResponse | ValidateInviteFailureResponse { // TODO: implement business logic from auth-invite-001 throw new Error(Not implemented); }这种意图骨架的实际价值是让开发者的注意力集中在业务逻辑上而不是浪费在取变量名、定义接口类型、搭路由这些重复劳动上。更重要的是骨架和 spec 是绑定的如果后端逻辑写完了才发现规格里把响应字段类型定义错了改掉 spec 重跑一次生成就能发现问题不用满项目搜字段。2.3 从 spec 到测试用例的自动映射Spec-kit 另一个我很喜欢的特性是把验收标准映射成测试用例。这不是什么高深的魔法本质是工具读取 acceptance_criteria 字段每条标准对应生成一条语义明确的测试用例。还是以上面的 spec 为例它会生成类似这样的测试describe(validateInvite [auth-invite-001], () { test(it returns 422 with EMPTY_CODE when invite code is empty, () { // TODO: assert behavior from acceptance criteria }); test(it returns 422 with INVALID_LENGTH when code length is out of 6..12, () { // TODO: assert behavior from acceptance criteria }); test(it returns validfalse when invite code does not exist, () { // TODO: assert behavior from acceptance criteria }); test(it returns validtrue and plan_type when invite code is valid and not expired, () { // TODO: assert behavior from acceptance criteria }); });这份测试骨架不是最终的测试但它直接表达了验收标准。开发者在实现时看到的不是我该怎么写函数而是这段代码必须让这些语义成立。测试描述直接来源于 spec天然防止了测试和需求脱节的问题。3. 用一个真实需求把 Spec-kit 跑通3.1 完整链路的一次走读光说不练没意义这里走一遍我用 Spec-kit 处理一个真实需求的完整流程。场景是用户系统的邀请码校验接口前后端都在同一个仓库开发接口契约需要提前对齐。第一步写 spec也就是上一节给出来的那一份文档。写的时候我和前端同学坐在一起把请求、响应、异常分支一条条过了一遍最后敲定的字段就是前文那个 YAML。这里没有直接让前端等后端代码而是双方先对着 spec 达成一致。第二步用 Spec-kit 解析 spec 并生成骨架。这一步在命令行里一行命令就完成了它会自动创建路由文件、类型定义、测试文件并在项目的 spec 索引里登记这条规格。生成完之后我先跑一遍已有测试确保新生成的骨架不会破坏原有构建。第三步填充业务实现。我在生成的 TODO 处写邀请码查库逻辑查询邀请码表判断状态、过期时间等。实现的时候我只关心一件事让四条验收标准通过不多脑补需求之外的行为。第四步运行规格校验。Spec-kit 带一个 spec check 模式它会重新解析当前代码中对外暴露的接口签名和 spec 中定义的接口契约做比对发现不一致就会报错。这一步可以理解为契约的静态检查。比如我把响应字段的 plan_type 拼成 planType 了校验就会失败。第五步提交并走正常评审。评审时我先贴出 spec 链接再贴出生成的测试列表评审者不需要猜需求直接看规格和测试对应关系即可。3.2 哪些步骤必须靠人哪些工具管不了必须诚实地说Spec-kit 并不是把思考过程替代了。它处理的是规格已经明确后的机械工作真正有价值的部分仍然需要人来完成怎么拆解需求、怎么定边界、异常情况有哪些。具体到流程里这三件事工具帮不上忙明确业务规则。比如邀请码的有效期怎么算、是否可以重复使用这些必须产品、开发、运营确认。设计领域模型。spec 里的字段关系、聚合边界还是需要人来定义。判断异常分支兜底。哪一个异常码对应哪一种错误语义工具无法推测必须由人写入验收标准。我见过把 Spec-kit 当成AI 自动生成业务代码来期望的人结果都失望了。它真正擅长的是消灭机械劳动放大规格的价值而不是替你决策。4. 踩坑实录Spec-kit 落地的五个关键问题4.1 坑一spec 写得太粗生成的东西没有价值我最早犯的错就是把 spec 写成了需求摘要比如校验邀请码有效性一句话加一个接口路径。生成的测试只有一条生成的类型定义也是几个字段草草了事等于走了流程却没拿到收益。后来我总结出一条可行的标准如果这条 spec 生成的验收标准还不够前端写 mock、不够测试同学写用例那说明细化程度不够。验收标准的颗粒度至少要细到输入什么、在什么条件下、得到什么结果并且允许出现边界条件和异常条件。想不清楚异常分支就不要开始写实现。4.2 坑二spec 变更没有留痕代码有版本管理但规格这种指导代码的东西反而容易被忽略。很多团队把 spec 当成一次性文档写完就丢。结果需求变了一次代码改了spec 还是旧的几轮下来整个流程名存实亡。解决办法很简单把 spec 当作代码一样对待放在 git 仓库里每次修改走同一个 review 流程。Spec-kit 也支持从 git 历史定位某条 spec 的变更记录回查时能看清楚一条接口契约是哪个迭代变成这样的。这个习惯建立之后我发现很多存量接口的历史包袱都能找到源头了。4.3 坑三生成器结果被当成不可改的圣旨这个坑很微妙。Spec-kit 生成的骨架是好的开发起点但有人会把生成的文件当成不可改的标准代码后面要增加一个字段宁愿去改 spec 重新生成也不愿直接改代码。这反而把简单问题复杂化了。我的经验是区分托管文件和手写文件。路由入口、类型定义、测试骨架这些规格直接映射的部分交给工具生成尽量不手工改而业务实现文件则是完全由人维护的。如果确实需要改类型定义先改 spec再重新生成然后检查测试是否受影响。这样既避免和生成器打架也不至于被生成逻辑绑架。4.4 坑四同事不认 spec配合断裂把 Spec-kit 推向团队时最容易遇到的问题不是工具不好用而是别人觉得增加了额外负担。特别是前端同事会认为后端给个接口定义文档就够了为什么还要跟着看 spec。我的处理方式是分两步。第一步不要强制所有模块都走 SDD只挑一两个接口密集、协作频繁的模块做试点。第二步把 spec 的收益和前端同学的痛点点对点对齐接口字段变了导致前端 mock 返工、类型定义不一致导致联调出错这些问题通过 spec 的契约集中定义之后解决的是他们最烦的那部分。当大家发现 spec 能替他们挡掉一些无意义的反复沟通时接受度明显高很多。4.5 坑五spec 和实现漂移后没人发现代码写嗨了之后直接绕过规格实现的情况一定会发生。比如验收标准说空邀请码返回 422但实现时图省事统一返回 200 和 validfalse。前端如果按 spec 的契约处理这个 bug 只有联调时才会暴露。治本的手段是把规格校验放进 CI。我在流水线里加了一步 spec check每次提交都会跑一遍校验检测接口签名、字段名和状态码是否与 spec 一致不一致就直接失败。你会发现哪怕只是把这一步加上团队成员对规格的认真程度都会上一个台阶因为不按规格写代码从道德问题变成了构建问题。5. Spec-kit、TDD、BDD 三者的角色分界线5.1 一个容易混淆的问题经常有人问我Spec-kit 和 TDD、BDD 是不是重复了。表面上它们都是先定义再实现的实践但实际解决的环节完全不同。我花了不少时间才把它们的边界理清楚这里分享一个比较直观的理解方式。TDDTest-Driven Development关注的是函数级别怎么保证行为正确它的产物是单元测试核心循环是红-绿-重构约束的是开发者的编码过程。BDDBehavior-Driven Development关注的是用户场景级别的行为它的产物通常是 human-readable 的场景描述比如 Given/When/Then关注的是业务和开发的沟通语言。而 SDD 配合 Spec-kit 关注的是更高一层的交付契约。它先于测试、先于实现把接口边界、验收标准、依赖关系定义清楚。你可以把 TDD 和 BDD 理解为保证实现过程靠谱的手段而 SDD 是保证实现之前大家对齐的手段。三者根本不是同一个环节的竞争关系。5.2 三种实践的定位对比实践核心产物关注粒度解决问题的环节主要使用时机TDD单元测试函数/类实现是否正确编码过程中BDD场景描述用户行为需求是否被清晰表达需求分析/验收SDD Spec-kit结构化规格接口契约/验收标准协作是否对齐开发前这个表看起来简单但对团队排流程很有用。我现在的标准流程是先用 BDD 的思想梳理用户场景再把场景中的接口和边界拆进 spec开发时用 TDD 逐函数落地最后用 Spec-kit 生成的测试骨架和 CI 校验做兜底。三者各管一段不冲突。5.3 组合打法里的顺序关系如果团队还处于比较初级的阶段我不建议一上来就三种实践一起推。我自己是从 TDD 做起然后引入 BDD 的叙述方式最后才上了 Spec-kit 的规格管理。组合起来之后的顺序大概是用 BDD 的场景描述确定做什么。用 SDD 的 spec 确定接口长什么样、边界在哪里。用 TDD 确定每个函数怎么实现才是对的。用 CI 里的规格校验确定代码有没有偏离最初的契约。这个顺序每往下一层约束就越具体同时改动成本也越高。规格错了改起来比测试错了改起来贵测试错了改起来比一个函数错了贵。所以越靠前的事越值得多花时间做扎实。6. 我的使用心得与后续扩展建议6.1 适合引入 Spec-kit 的团队形态不是所有团队都应该立刻上 Spec-kit。我观察下来最适合的是三类情况三人以上协作且接口密集的项目前后端并行开发、经常需要对齐合同语义的团队需求变更频繁、需要长期维护大量历史接口的系统。反过来如果是一个人做原型、代码写完就扔、或者所有接口都是自己一个人定义自己一个人消费那上这套工具确实会嫌重收益不明显。这不算打脸而是工具本来就有它的适用边界。我在小项目里也只会挑核心模块写 spec其余部分直接写代码。6.2 轻量落地的三步法如果你想在团队里试不要一上来就要求所有模块都过一遍写 spec、生成、校验的完整流程。我建议分三步走。第一步选一个模块试点。挑一个接口最多、返工最多的模块把它的关键接口写成结构化 spec让工具生成骨架观察联调时是不是真的少了一些字段对不上的反复沟通。第二步从粗到细迭代规格。第一版 spec 允许粗糙只需要把接口契约和主干验收标准定好。等开发过程中发现边界问题再补进 spec。关键是要保持改 spec 先于改代码的习惯。第三步逐步扩大生成覆盖。先只生成类型和骨架稳定之后再启用测试映射最后再加 CI 校验。让团队的每一步变化都只有一部分改动量持续看到收益后再加码。6.3 还能往哪些方向扩展Spec-kit 这套思路落地之后还可以很自然地延伸出一些高收益的组合玩法。一个是 spec 直接驱动 API 文档因为接口契约已经在 spec 里了生成 OpenAPI 文档或者 Markdown 文档就是纯体力活文档永远不会和规格脱节。另一个是 spec 驱动前后端类型对齐前端可以直接从 spec 生成 TypeScript 类型定义后端生成的类型和前端生成的类型来自同一个源头天然消灭字段名不一致的问题。还有一个我后来才意识到的用法把设计评审和 spec 评审合并。以前设计评审摆出来是一堆 PPT 和时序图现在可以直接对着 spec 逐条过验收标准哪些边界没考虑到、哪些字段定义有歧义评审时一目了然。规格成了活的评审材料。对我个人来说Spec-kit 最大的价值不在于生成代码省下的那几个小时而是它改变了团队处理分歧的方式。争执少了返工少了代码和需求的一致性能被机器兜住这在工程实践里是值得花时间建立的习惯。如果你也被部门之间对需求理解不一致折磨过不妨从手头一个接口密集的小模块开始把规格写得稍微结构化一点体验一下先定契约再动手的感觉。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进