1. 从代码先写、文档后补到规范先行SDD到底在解决什么问题大多数团队做项目的流程都差不多需求会上聊一轮产品经理写个PRD开发看一眼就开始建表、写接口、搭页面。等到联调的时候发现字段对不上等到测试的时候发现边界条件没覆盖等到上线之后发现当初说好的这个功能先不做其实做了、这个必须做其实漏了。然后大家坐下来复盘结论永远是下次需求评审要更仔细——但下次还是老样子。这个问题的根子不在于谁不认真而在于规范本身没有被当作工程产物来对待。PRD是给人看的自然语言文档接口文档是Swagger里随手写的注释数据库设计是某个人脑子里的隐性知识。这些东西没有版本、没有校验、没有依赖关系自然也就没法在流水线里被自动检查。等到问题暴露往往已经是集成阶段甚至生产环境了。SDDSpecification-Driven Development规范驱动开发要解决的就是这件事把规范从一份Word文档提升为可版本化、可校验、可生成代码、可驱动测试的一等工程资产。而Spec-kit就是把这套理念落地成工具链的一个工程化套件。它做的事情说白了就一句话——让规范成为整个研发流程的唯一事实来源Single Source of Truth代码、测试、文档都从规范派生出来而不是各自为政。我第一次接触这套思路的时候直觉是这不就是又一层形式主义吗。但真正在一个中型项目里跑通之后感受完全变了需求变更的时候改的是规范文件代码生成器重新跑一遍测试用例自动更新接口文档同步刷新。原来需要三个人对半天的同步工作变成了一次提交。这不是效率提升百分之几十的问题而是协作模式本身发生了变化。这篇文章适合几类人看一是正在被文档和代码两张皮折磨的技术负责人二是想了解SDD这套方法论到底怎么落地的架构师三是手头项目接口多、变更频繁、联调成本高的开发者。我会从Spec-kit的核心机制讲起拆解它为什么这样设计然后给出可复现的实操步骤最后重点讲我在实际使用中踩过的坑——这些坑官方文档里基本不会写。2. Spec-kit的核心机制拆解规范是怎么变成代码的2.1 规范即源码Spec-kit的三层结构Spec-kit把规范拆成了三个层次这个分层是整个工具链的骨架理解了它后面的东西都好说。第一层是领域模型层Domain Layer。这一层描述的是业务实体和它们之间的关系比如订单有哪些字段、订单和用户是一对多还是多对多、订单状态有哪些合法取值。这一层用的是结构化的Schema定义不是自然语言。为什么必须结构化因为只有结构化的东西才能被程序解析、校验和生成代码。你用自然语言写订单要记录下单时间机器没法判断你到底要的是创建时间还是支付时间。第二层是行为契约层Contract Layer。这一层描述接口的输入输出、错误码、前置条件、后置条件。它回答的是调用这个接口会发生什么——成功返回什么、失败返回什么、什么情况下会失败。这一层是前后端联调的核心依据也是自动化测试用例生成的来源。第三层是流程编排层Flow Layer。这一层描述跨多个接口的业务流程比如下单→支付→发货→确认收货这条链路里每一步依赖前一步的什么输出、异常情况下怎么回滚。这一层最容易被忽略但恰恰是bug最集中的地方——单个接口测着都没问题串起来就出问题根因就是流程层的约束没有被显式表达。这三层的关系是领域模型定义有什么行为契约定义能做什么流程编排定义按什么顺序做。Spec-kit的代码生成器就是沿着这三层往下推导生成数据模型代码、接口骨架、测试桩、API文档。2.2 为什么是驱动而不是辅助这里有个关键区别需要说清楚。市面上很多工具是辅助性质的——你写完代码它帮你生成文档你定义好接口它帮你生成Mock。这类工具的问题是代码仍然是主体规范是附属品。代码改了规范不一定改规范改了代码也不一定跟着变两者之间没有强约束。Spec-kit的驱动意味着方向是反过来的规范是主体代码是派生物。你改规范代码生成器重新跑代码跟着变。这就带来一个很实际的约束——你不能随手改生成的代码因为下次生成会被覆盖。所有变更必须回到规范层去做。这个约束一开始会让人很不适应。我带的第一个项目里有个开发同学为了赶进度直接在生成的Controller里加了个字段结果第二天重新生成代码被覆盖他改的东西全没了气得不行。但正是这次事故让大家意识到绕过规范改代码本质上是在制造技术债。后来我们定了个规矩——生成的代码目录标记为只读所有变更走规范CI里加了一道检查如果生成代码和规范不一致就直接失败。2.3 规范文件的组织方式与命名约定Spec-kit对规范文件的组织有明确的约定这套约定不是随便定的背后有它的道理。specs/ domain/ user.spec.yaml order.spec.yaml product.spec.yaml contracts/ user-api.contract.yaml order-api.contract.yaml flows/ purchase.flow.yaml shared/ enums.spec.yaml errors.spec.yaml按领域拆文件而不是按技术层拆是为了让变更的影响范围可控。你改订单相关的规范只需要关注order相关的几个文件不用在一个几千行的巨型文件里找。shared目录放的是跨领域共享的枚举和错误码定义这些是全局约束改动影响面大单独隔离出来便于审查。命名上我建议加一个约定文件名用单数。user.spec.yaml而不是users.spec.yaml。因为领域模型描述的是一个用户长什么样不是用户列表。这个细节看起来无所谓但在生成代码的时候单数命名能让生成器更准确地推断出实体名和集合名的对应关系减少手工配置。2.4 代码生成器的推导逻辑Spec-kit的代码生成器不是简单的模板替换它做了一层语义推导。举个例子你在领域模型里定义了Order: fields: id: { type: uuid, primary: true } userId: { type: uuid, ref: User.id } status: { type: enum, values: [pending, paid, shipped, completed, cancelled] } createdAt: { type: datetime, auto: true }生成器看到ref: User.id会自动生成外键约束和关联查询方法看到status是枚举会生成状态机校验逻辑看到createdAt带auto: true会在创建逻辑里自动注入时间戳。这些推导规则是Spec-kit内置的你也可以通过配置文件扩展。这里有个经验推导规则越丰富规范文件就能写得越简洁但可预测性会下降。我建议在项目初期把推导规则限制在最小集合等团队熟悉了再逐步放开。否则新人写规范的时候根本不知道哪些东西是自动生成的、哪些需要显式声明很容易写出冗余甚至冲突的定义。3. 环境搭建与第一个可运行Demo从零跑通一条链路3.1 工具链安装与版本选择Spec-kit本身是一个命令行工具依赖Node.js运行时。安装方式很直接npm install -g spec-kit-cli spec-kit --version但这里有个坑Spec-kit的版本和生成器插件的版本必须匹配。我遇到过CLI是2.3、生成器插件是2.1的情况跑起来不报错但生成的代码里少了一些字段排查了半天才发现是版本不兼容。建议在项目根目录放一个.spec-kit-version文件锁定版本号CI里也按这个版本装。# .spec-kit-version 2.3.1另外如果你的项目需要生成多种语言的代码比如后端Java、前端TypeScript需要额外安装对应的生成器插件spec-kit plugin install spec-kit/generator-java spec-kit plugin install spec-kit/generator-typescript插件装完之后在spec-kit.config.yaml里配置生成目标generators: - plugin: spec-kit/generator-java output: ./backend/src/main/java options: package: com.example.order - plugin: spec-kit/generator-typescript output: ./frontend/src/api options: strict: true3.2 写第一份领域模型规范我们拿一个最简单的场景练手一个待办事项Todo应用。先写领域模型# specs/domain/todo.spec.yaml Todo: description: 待办事项 fields: id: type: uuid primary: true generated: true title: type: string required: true maxLength: 200 completed: type: boolean default: false priority: type: enum values: [low, medium, high] default: medium dueDate: type: date nullable: true createdAt: type: datetime auto: true写这份文件的时候有几个决策点值得说明。title的maxLength设成200是因为数据库里如果用VARCHAR200是个比较安全的长度既不会太短导致截断也不会因为太长影响索引效率。priority用枚举而不是整数是为了避免1代表高优先级还是低优先级这种歧义——枚举值直接可读前后端不会理解错。dueDate允许为空是因为不是所有待办都有截止日期强制必填反而会让用户乱填。3.3 定义行为契约领域模型定义完了接下来定义接口契约# specs/contracts/todo-api.contract.yaml CreateTodo: method: POST path: /api/todos input: title: { type: string, required: true } priority: { type: enum, ref: Todo.priority, default: medium } dueDate: { type: date, nullable: true } output: success: status: 201 body: { type: object, ref: Todo } errors: - code: INVALID_TITLE status: 400 when: title is empty or length 200 - code: DUPLICATE_TODO status: 409 when: title already exists for the same dueDate注意errors里的when字段它用的是可执行的断言表达式不是自然语言描述。这意味着Spec-kit可以拿它来生成测试用例——每个when条件对应一个测试场景。这是规范驱动测试的关键测试用例不是手写的是从规范里推导出来的。3.4 生成代码并跑通规范写好了执行生成命令spec-kit generate --all生成完之后目录结构大概是这样backend/src/main/java/com/example/order/ model/Todo.java controller/TodoController.java service/TodoService.java dto/CreateTodoRequest.java dto/CreateTodoResponse.java frontend/src/api/ todo.ts types.ts生成的Controller里已经包含了参数校验、错误码返回、基本的CRUD逻辑。你需要做的只是填充业务逻辑的空洞——比如DUPLICATE_TODO的检查逻辑生成器会留一个TODO注释你在这个位置补上查重代码。这里有个实操建议先跑通再优化。不要一上来就想着把规范写得多完美先用最小可运行的规范跑通整条链路确认生成、编译、启动、调用都没问题再逐步补充细节。我见过太多人卡在规范怎么写才规范这个问题上写了三天规范一行代码没跑起来最后放弃了。4. 规范驱动测试让测试用例从规范里长出来4.1 从契约的when条件生成测试骨架Spec-kit最让我惊喜的功能是测试生成。前面在契约里写的那些when条件可以直接生成测试用例spec-kit generate-tests --contract todo-api --framework junit生成的测试类大概长这样Test public void createTodo_withEmptyTitle_shouldReturn400() { CreateTodoRequest request new CreateTodoRequest(); request.setTitle(); // 断言返回400和INVALID_TITLE错误码 } Test public void createTodo_withDuplicateTitle_shouldReturn409() { // 测试逻辑 }生成的是骨架断言部分需要你补但测试场景的覆盖是完整的——因为每个when条件都对应一个测试方法。这就解决了测试漏场景的问题只要规范里写了测试里就有。4.2 边界条件的系统化覆盖手写测试最大的问题是边界条件靠人想想到哪测到哪。Spec-kit的做法是把边界条件也规范化。比如title字段定义了maxLength: 200生成器会自动生成三个测试长度199通过、长度200通过、长度201失败。这种系统化的覆盖靠人是很难保证的。我在实际项目里做过对比同一个模块手写测试的边界覆盖率大概在60%左右用Spec-kit生成的测试骨架补全后覆盖率能到90%以上。而且更重要的是这些测试是跟着规范走的——规范改了测试重新生成不会出现代码改了测试没改的情况。4.3 测试与规范的同步机制这里要讲一个关键机制规范变更时的测试失效检测。Spec-kit在生成测试的时候会在测试文件头部写入一个规范文件的哈希值。CI里跑测试之前会先校验哈希——如果规范变了但测试没重新生成直接失败。// SPEC-HASH: a3f8c2d1e4b5... // 此文件由Spec-kit生成请勿手动修改这个机制看起来简单但解决了一个很实际的问题规范改了测试没跟上。在传统流程里这靠代码审查来保证但审查者往往只看代码不看规范很容易漏。有了哈希校验机器帮你把关。5. 踩坑实录那些官方文档不会告诉你的问题5.1 生成代码与手写代码的边界怎么划这是落地Spec-kit遇到的第一个大问题。生成的代码不能改那业务逻辑写哪Spec-kit的答案是分层生成的代码只包含规范能表达的部分——数据模型、接口签名、参数校验、错误码。业务逻辑放在单独的Service实现类里这些类不被生成器覆盖。但实际操作中这个边界很模糊。比如下单时检查库存这个逻辑算业务逻辑还是规范能表达的约束我的经验是能用声明式表达的放规范需要命令式逻辑的放手写。库存必须大于0是声明式的可以放规范库存扣减要加分布式锁是命令式的放手写。划这条线的时候建议团队一起过一遍形成明确的约定。我们当时定了个规则规范里只放是什么不放怎么做。这个规则帮我们避免了很多争论。5.2 规范文件的合并冲突处理多人协作的时候规范文件的合并冲突比代码冲突更难处理。代码冲突你还能看懂逻辑规范文件的YAML冲突尤其是嵌套结构里的冲突很容易改错。我们的做法是规范文件按领域拆分到足够细让不同人改不同文件从源头上减少冲突。另外在.gitattributes里把规范文件标记为需要手动合并*.spec.yaml mergemanual *.contract.yaml mergemanual还有个小技巧规范文件里字段的顺序保持稳定新增字段加在末尾。这样diff的时候更清晰合并的时候也不容易出错。5.3 生成器插件的性能问题项目大了之后全量生成会变得很慢。我们有个项目有200多个领域模型全量生成一次要3分多钟。CI里每次提交都跑一遍累积起来很浪费时间。解决方案是增量生成spec-kit generate --changed-only这个命令只重新生成受影响的文件。但要注意增量生成依赖依赖关系图的准确性。如果你的规范里有跨文件的引用而依赖图没建对可能会漏生成。建议在CI里定期跑一次全量生成确保没有遗漏。5.4 团队协作中的规范评审流程工具再好流程不对也白搭。Spec-kit落地最大的阻力不是技术是习惯。开发同学习惯了直接写代码现在要先写规范多了一道工序自然有抵触。我们的做法是把规范评审纳入现有的代码评审流程——规范变更必须走PR必须有人review。review的重点不是语法是业务语义这个字段的定义对不对、这个错误码合不合理、这个流程的异常分支覆盖全了没有。review通过之后生成代码、跑测试、合并一气呵成。刚开始大家觉得麻烦但跑了一两个月之后效果出来了联调时间明显缩短因为接口契约在规范阶段就对齐了线上bug减少因为边界条件在测试阶段就覆盖了。数据摆在那里抵触情绪自然就消了。6. 从单项目到多项目规范复用的工程化实践6.1 共享规范的版本管理当一个组织里有多个项目的时候规范复用就成了刚需。比如用户模型、错误码定义、通用枚举这些在每个项目里都差不多没必要各写各的。Spec-kit支持把共享规范发布成包spec-kit publish --package shared-specs --version 1.2.0其他项目通过依赖的方式引用# spec-kit.config.yaml dependencies: - package: shared-specs version: ^1.2.0这里有个版本策略的建议共享规范用语义化版本主版本号变更意味着有破坏性改动。比如错误码的删除、字段类型的变更这些都要升主版本。项目升级依赖的时候CI会自动检测兼容性不兼容直接失败。6.2 跨项目的规范一致性校验多项目场景下最容易出的问题是规范漂移——A项目改了共享规范的一个字段B项目还在用旧版本两边对不上。Spec-kit提供了一个校验命令spec-kit verify --against shared-specs1.2.0这个命令会检查当前项目的规范是否和指定版本的共享规范兼容。我们把它加到了CI的必过环节任何不兼容的变更都会在合并前被发现。6.3 规范仓库的CI流水线设计规范仓库本身也需要CI。我们的规范仓库流水线大概是这样阶段检查项失败处理语法校验YAML格式、Schema合法性阻断合并语义校验引用完整性、枚举值合法性阻断合并兼容性校验与上一版本的兼容性阻断合并生成验证试生成代码并编译阻断合并发布打标签、发布包自动执行这套流水线跑下来规范仓库的质量就有了保障。下游项目引用的时候不用担心拿到一个有问题的版本。7. 我个人的几条实操心得先说一个反直觉的结论Spec-kit最大的价值不在于生成代码而在于强制团队把需求想清楚。写规范的过程本质上是一次结构化的需求分析。很多在写代码时才会暴露的歧义在写规范的时候就会暴露——因为规范要求你把每个字段的类型、约束、默认值都写清楚含糊不过去。第二条心得是关于渐进式落地。不要一上来就全项目铺开选一个边界清晰、变更不频繁的模块先试点。跑通之后用数据说话——联调时间缩短了多少、测试覆盖率提升了多少、线上bug减少了多少。有了这些数据再推广到其他模块阻力会小很多。第三条是关于规范的粒度。规范写得太粗生成的东西没用写得太细维护成本高。我的经验是规范写到接口级别就够了不要试图把每个方法的实现细节都写进去。规范描述的是契约不是实现。实现细节留给代码那是代码该管的事。最后一条也是我觉得最重要的一条规范是活的不是死的。很多团队把规范当成一次性文档写完就扔那了。Spec-kit的价值恰恰在于规范是持续演进的——每次需求变更改规范、重新生成、跑测试、合并这个循环跑得越顺规范的价值就越大。如果规范写完就不动了那和写一份Word文档没什么区别。我在实际使用中最大的体会是这套东西对团队的工程素养要求比较高。它不是一个装上就能用的工具而是一套需要团队共同遵守的工作方式。工具能帮你检查语法、生成代码、跑测试但把需求想清楚这件事最终还是得靠人。Spec-kit做的是把这件事变得有结构、可检查、可追溯而不是替代人去做这件事。想明白这一点落地的时候心态会好很多。