OpenSpec规格驱动开发:Laravel项目从文档到代码的同步实践
1. 规格驱动开发到底在解决什么问题
第一次接触 OpenSpec 是在一个 Laravel 项目里。当时团队三个人,后端接口改了三次,前端每次都要重新对字段,测试用例写完发现跟需求文档对不上,需求文档又跟实际代码对不上。最离谱的一次是上线前一天发现某个字段的校验规则在三个地方写了三种不同的逻辑。这种场景做久了就会明白,问题不在于谁写错了代码,而在于规格和实现之间没有一条可追溯的链路。
OpenSpec 这个思路的核心,就是把“规格”从一份静态文档变成驱动开发流程的活文件。它不只是一个写文档的工具,而是一套让规格、代码、测试三者保持同步的工作方式。你可以把它理解成给项目装了一个“需求编译器”——你写清楚要什么,它帮你把结构固定下来,后续所有改动都围绕这个结构走。
这套东西适合谁?如果你是一个人写小工具,可能觉得没必要。但只要是两人以上的协作项目,尤其是接口频繁变动、需求方反复改口、测试和开发经常扯皮的场景,规格驱动开发能省掉大量沟通成本。Laravel 这类框架本身结构就很规整,配合 OpenSpec 能把“控制器-服务-模型”这条链路上的契约关系写得很清楚。
我实测下来的感受是:前期多花二十分钟写规格,后期能省掉至少两小时的联调扯皮。这笔账怎么算都划算。
1.1 从“文档写完就过期”到“规格即代码”
传统开发流程里,需求文档的宿命基本就是写完就进归档文件夹。为什么?因为它跟代码是分离的。代码改了,没人会回去改文档。时间一长,文档就成了“历史参考”,没人敢信。
OpenSpec 的做法是把规格文件放在项目仓库里,跟代码一起版本管理。规格变了,代码必须跟着变;代码变了,规格也要同步更新。这就把“文档维护”从一个可选项变成了流程的一部分。你可以用 Claude Code 或者 Codex 这类工具来辅助生成和校验规格,但核心逻辑是:规格文件是项目的一等公民,不是附属品。
具体来说,一个 OpenSpec 规格文件通常包含几个部分:能力描述、接口契约、数据模型、验收条件。能力描述说清楚这个模块要做什么;接口契约定义输入输出;数据模型约束字段类型和关系;验收条件给出可测试的判断标准。这四块写清楚,开发和测试就有了共同的参照系。
1.2 为什么 Laravel 项目特别适合这套方法
Laravel 本身有一套很明确的约定:路由定义、控制器方法、表单请求验证、资源类、模型关联。这些约定天然适合用规格来描述。比如一个StoreUserRequest的验证规则,在规格里就可以写成明确的字段约束表;一个 API 资源类的输出结构,在规格里就是响应契约。
我试过在一个中等规模的 Laravel 项目里引入 OpenSpec,最直接的收益是:表单请求验证规则和规格文件可以互相校验。以前经常出现规格里写“邮箱必填”,但代码里required漏了的情况。现在规格文件里定义好字段约束,写代码时直接对照,漏掉的概率大幅降低。
另外 Laravel 的测试体系很完善,规格里的验收条件可以直接映射成 Pest 或 PHPUnit 的测试用例。规格说“用户名为空时返回 422”,测试就写一条对应的断言。这样规格就不再是“给人看的文档”,而是“给测试看的输入”。
2. 核心概念拆解与工具链选型
OpenSpec 不是某一个具体的软件,而是一套方法论加工具组合。理解它的关键在于搞清楚三个东西:规格文件的结构、辅助工具的分工、以及跟现有开发流程怎么衔接。
2.1 规格文件的四层结构
我习惯把规格文件分成四层来写,从粗到细依次是:能力层、契约层、模型层、验收层。
能力层用一两句话描述这个模块的职责边界。比如“用户管理模块负责注册、登录、信息修改和权限校验”。这一层不涉及技术细节,主要是给非技术人员看的。
契约层定义接口的输入输出。对于 Laravel 项目,这一层通常对应路由和控制器方法。我会用表格把每个端点的请求方法、路径、请求参数、响应结构列清楚。这个表格后来可以直接转成 Postman 集合或者 OpenAPI 描述文件。
模型层描述数据结构和关系。字段名、类型、是否可空、默认值、索引情况都写明白。这一层跟 Laravel 的迁移文件和模型类是对应的。写规格的时候顺手把迁移文件的结构确定下来,后面写代码就是填空。
验收层给出可测试的判断条件。每条验收条件都应该是二值的:要么通过要么不通过。比如“用户名长度小于 3 时返回 422 状态码”就是一条合格的验收条件。“用户体验良好”这种就不是。
注意:验收条件不要写“应该”“尽量”这类模糊词。写规格时偷的懒,测试阶段都要加倍还回来。
2.2 Claude Code 与 Codex 在规格驱动中的分工
Claude Code 和 Codex 这类工具在规格驱动开发里扮演的是“规格助手”的角色。它们能帮你做三件事:根据自然语言描述生成规格草稿、检查规格与代码的一致性、根据规格生成测试骨架。
我自己的用法是:先用自然语言把需求说一遍,让 Claude Code 生成一份规格草稿,然后人工审一遍,把模糊的地方改精确。改完之后,再用它来检查现有代码是否符合规格。比如规格里写“邮箱字段唯一”,它会去检查迁移文件里有没有加唯一索引、表单请求里有没有写unique规则。
Codex 在代码生成方面更顺手一些。规格确定之后,可以让它根据契约层生成控制器方法骨架,根据模型层生成迁移文件,根据验收层生成测试用例。但要注意:生成的代码必须人工审查。我遇到过生成的迁移文件字段类型跟规格不一致的情况,原因是自然语言描述有歧义。
这两个工具的具体安装和配置网上教程很多,核心就一点:确保它们能读取到你项目里的规格文件。通常做法是把规格文件放在项目根目录的specs/文件夹下,然后在工具配置里把这个路径加进去。
2.3 跟现有 Laravel 流程的衔接方式
引入 OpenSpec 不需要推翻现有流程,可以在几个关键节点上做增量改进。
第一个节点是需求评审。以前评审的是 PRD 文档,现在评审规格文件。规格文件比 PRD 更结构化,评审时能直接发现字段遗漏、类型不一致这类问题。
第二个节点是开发启动。以前是看完文档直接写代码,现在是先确认规格文件里的契约层和模型层,再动手写迁移和控制器。这一步多花的时间,会在联调阶段省回来。
第三个节点是代码审查。审查时对照规格文件检查:接口实现是否符合契约、字段约束是否跟模型层一致、验收条件是否都有对应测试。这样审查有了客观依据,不再靠感觉。
第四个节点是回归测试。规格文件里的验收条件就是回归测试的用例来源。每次改动后跑一遍对应测试,能快速发现规格与实现的偏离。
3. 从零搭建一套可运行的规格驱动流程
这一部分我以 Laravel 项目为例,把完整流程走一遍。假设我们要做一个简单的文章管理模块,包含文章的增删改查。
3.1 环境准备与目录结构规划
首先在 Laravel 项目根目录下建一个specs/文件夹。我习惯按模块分子目录,每个模块一个规格文件。文章模块就是specs/article/article.spec.md。
规格文件用 Markdown 写,因为 Markdown 既能被人读,也能被工具解析。文件头部放元信息:模块名、负责人、最后更新时间、关联的代码路径。这些元信息在后续自动化检查时很有用。
目录结构大概是这样:
specs/ article/ article.spec.md article.acceptance.md user/ user.spec.md user.acceptance.md把验收条件单独放一个文件,是因为验收条件会频繁变动,而契约和模型相对稳定。分开之后,改动影响范围更清晰。
3.2 编写第一份规格文件:文章模块实例
打开article.spec.md,按四层结构来写。
能力层:
文章管理模块负责文章的创建、读取、更新和删除。文章属于某个用户,包含标题、正文、状态三个核心字段。状态分为草稿和已发布两种。
契约层用表格:
| 方法 | 路径 | 请求参数 | 响应 | 说明 |
|---|---|---|---|---|
| POST | /api/articles | title, body, status | 201 + 文章对象 | 创建文章 |
| GET | /api/articles/{id} | 无 | 200 + 文章对象 | 获取单篇 |
| PUT | /api/articles/{id} | title, body, status | 200 + 文章对象 | 更新文章 |
| DELETE | /api/articles/{id} | 无 | 204 | 删除文章 |
模型层:
| 字段 | 类型 | 约束 | 默认值 |
|---|---|---|---|
| id | bigint | 主键,自增 | 无 |
| user_id | bigint | 外键,非空 | 无 |
| title | string | 非空,最大 200 | 无 |
| body | text | 非空 | 无 |
| status | enum | draft, published | draft |
| created_at | timestamp | 自动 | 无 |
| updated_at | timestamp | 自动 | 无 |
验收层写几条关键条件:
- 标题为空时返回 422
- 标题超过 200 字符时返回 422
- 状态不是 draft 或 published 时返回 422
- 创建成功后返回 201 且响应体包含文章 id
- 删除不存在的文章返回 404
这份规格写下来大概十五分钟。写完之后,后面写迁移文件、表单请求、控制器、测试用例都有了明确参照。
3.3 根据规格生成迁移与模型
有了模型层的表格,迁移文件就是照抄。字段名、类型、约束、默认值都在表里。我一般会打开 Claude Code,把模型层表格贴进去,让它生成迁移文件草稿,然后人工核对一遍。
核对重点看三个地方:字段类型是否匹配(比如 text 和 string 的区别)、索引是否加上(user_id 应该加索引)、外键约束是否正确。生成的代码经常在索引这块偷懒,需要手动补。
模型类里的$fillable和$casts也根据模型层来写。status 字段用 enum 类型的话,casts 里要指定对应的枚举类。这些细节规格里写清楚了,写代码时就不用反复翻文档。
3.4 表单请求验证与规格对齐
Laravel 的表单请求类是把规格里的约束落地的关键位置。验收层写的“标题为空返回 422”“标题超过 200 字符返回 422”,对应的就是StoreArticleRequest里的required和max:200规则。
我习惯在表单请求类的注释里标注对应的规格文件路径和验收条件编号。这样以后改规则时,能快速定位到规格里的哪一条需要同步更新。
/** * 对应规格:specs/article/article.spec.md * 验收条件:AC-001, AC-002, AC-003 */ class StoreArticleRequest extends FormRequest { public function rules(): array { return [ 'title' => ['required', 'string', 'max:200'], 'body' => ['required', 'string'], 'status' => ['sometimes', 'in:draft,published'], ]; } }这种注释看起来不起眼,但在多人协作时能省很多沟通。新人接手时看到注释就知道该去哪找上下文。
3.5 验收条件转测试用例
验收层每一条条件都应该对应至少一个测试方法。用 Pest 写的话大概是这样:
it('标题为空时返回 422', function () { $user = User::factory()->create(); $this->actingAs($user) ->postJson('/api/articles', ['body' => '内容']) ->assertStatus(422) ->assertJsonValidationErrors('title'); });测试方法名直接抄验收条件的描述,这样测试报告读起来就是验收清单。跑一遍测试,全绿就说明实现符合规格。有红的就说明要么代码有问题,要么规格写错了。两种情况都需要处理。
我自己的习惯是:先写测试再写实现。规格里的验收条件写完之后,先把测试骨架搭出来,跑一遍看着它红,然后再去写控制器逻辑让它变绿。这样能确保测试真的在测东西,而不是事后补一个永远通过的测试。
4. 实操中踩过的坑与排查技巧
规格驱动开发听起来很顺,但实际操作中有一堆细节问题。这一部分把我踩过的坑整理出来,能帮你少走弯路。
4.1 规格与代码不同步的三种典型场景
第一种是“代码改了规格没改”。这种情况最多发生在紧急修复时。线上出了 bug,直接改代码上线,规格文件忘了更新。几次之后规格就跟代码脱节了。解决办法是把规格更新纳入代码审查清单,改代码的 PR 必须包含规格文件的改动,否则不合并。
第二种是“规格改了代码没改”。这通常发生在需求变更时。产品说加个字段,规格文件更新了,但开发忘了改迁移和表单请求。解决办法是在规格文件里加一个“实现状态”标记,每个契约和模型字段标注是否已实现。定期扫描未实现项。
第三种是“规格和代码都改了但测试没改”。验收条件变了,测试用例还是老的。这种情况最隐蔽,因为测试还是绿的,但测的东西已经不对了。解决办法是验收条件文件单独版本管理,每次改动都触发对应测试的审查。
4.2 工具链配置中的常见报错与处理
用 Claude Code 或 Codex 辅助生成规格时,最常见的问题是工具读不到项目上下文。表现是生成的规格跟项目实际结构对不上,比如字段名用了驼峰而项目用蛇形。
处理方法是确保工具的工作目录是项目根目录,并且在配置文件里明确指定规格文件夹路径。如果工具支持项目级配置,把 Laravel 的版本、数据库类型、命名约定写进去,生成的规格会准确很多。
另一个常见问题是生成的验收条件太模糊。比如生成“用户输入有效时创建成功”这种没法测试的条件。这时候需要人工改写,把“有效”拆解成具体的字段约束。我一般会准备一个验收条件模板,让工具按模板格式生成,减少模糊表述。
4.3 多人协作时的规格评审要点
多人项目里,规格文件的评审比代码评审还重要。因为规格错了,后面全错。评审时重点看四个地方:
- 契约层的接口路径和请求方法是否跟路由文件一致
- 模型层的字段类型是否跟迁移文件一致
- 验收条件是否每条都可测试
- 规格文件里的术语是否跟团队统一词汇表一致
我见过因为术语不统一导致的返工:规格里写“文章状态”,代码里用status,测试里用state,联调时对字段对半天。后来团队建了一个术语对照表,规格、代码、测试统一用同一套词,这类问题就没了。
4.4 规格文件的版本管理策略
规格文件跟代码一起放在 Git 仓库里,但分支策略可以灵活一点。我的做法是:主分支上的规格文件永远反映当前线上版本的实际行为;开发分支上的规格文件反映即将上线的变更。合并开发分支时,规格文件的 diff 就是这次变更的“需求摘要”。
这样做的好处是,回滚代码时规格文件也跟着回滚,不会出现代码回滚了但规格还是新版本的情况。另外,发布时可以从规格文件的 diff 自动生成变更日志,省掉手动整理。
提示:规格文件的提交信息建议用“spec: 描述变更内容”的格式,方便过滤和检索。
5. 规格驱动开发的扩展玩法
基础流程跑通之后,可以在这套方法上叠加一些扩展,进一步提升效率。
5.1 从规格自动生成 API 文档
契约层的表格本身就是 API 文档的素材。写个脚本把规格文件里的表格转成 OpenAPI 格式,再喂给 Swagger UI 或者 Redoc,就能自动生成可交互的 API 文档。这样文档永远跟规格同步,不会过期。
我用 Node.js 写过一个简单的转换脚本,核心逻辑就是解析 Markdown 表格,按 OpenAPI 规范组装 JSON。大概一百行代码,跑一次几秒钟。比起手动维护 Swagger 注解,这种方式省事得多。
5.2 规格变更影响分析
当规格文件发生变更时,可以自动分析影响范围。比如模型层加了一个字段,影响的是迁移文件、模型类、表单请求、资源类、测试用例。写个脚本扫描规格文件的 diff,匹配到对应的代码文件,生成一个影响清单。代码审查时对照这个清单检查,不容易漏。
这个脚本的实现思路是:在规格文件里给每个字段和契约标注关联的代码文件路径,diff 时提取变更的标注,汇总关联路径。标注格式可以用 HTML 注释,不影响 Markdown 渲染。
5.3 与持续集成流程的结合
在 CI 流程里加一步“规格一致性检查”:跑一个脚本对比规格文件和实际代码,发现不一致就报错。比如规格里写 title 最大 200,但表单请求里写的是 max:255,脚本检测到差异就阻断合并。
这个检查不需要很复杂,初期可以只检查字段名和类型是否匹配。后面逐步加上约束条件、默认值、索引的检查。每加一项检查,就堵住一类常见错误。
我自己的项目里,这个检查跑在测试之前。规格一致性检查通过之后才跑单元测试。这样如果规格和代码脱节,能更早发现,不用等到测试失败再去排查。
5.4 规格文件的模板化与复用
不同模块的规格文件结构高度相似,可以做成模板。新建模块时从模板复制,填内容就行。模板里预置好四层结构的标题、表格格式、验收条件的写法示例。这样新人也能快速写出格式规范的规格文件。
模板还可以按项目类型分:API 模块模板、后台管理模块模板、数据处理模块模板。每种模板的契约层和模型层侧重点不同。API 模块侧重请求响应结构,后台模块侧重权限和操作日志,数据处理模块侧重输入输出格式和边界条件。
用模板的好处是规格文件风格统一,工具解析起来也稳定。我试过让 Claude Code 按模板生成规格草稿,准确率比自由格式高不少。
5.5 规格驱动在遗留项目中的渐进式引入
老项目不可能停下来先写全套规格再继续开发。我的做法是渐进式引入:新功能必须写规格,老功能在改动时补规格。每次改一个老模块,就顺手把这个模块的规格补上。半年下来,核心模块的规格基本都齐了。
补规格的时候不用追求一次写完美。先把契约层和模型层补上,验收条件可以后面慢慢加。关键是让规格文件先存在,后面再迭代完善。存在比完美重要。
对于特别混乱的老模块,可以先写一份“现状规格”,描述当前实际行为而不是期望行为。这份规格的作用是给重构提供基线。重构时对照现状规格,确保行为不变。重构完成后再把现状规格更新成目标规格。
6. 一些个人体会与实用建议
规格驱动开发这套方法,核心价值不在于工具多先进,而在于它强迫团队在写代码之前把问题想清楚。很多扯皮的根源是需求本身就没说清楚,规格文件把这个“说清楚”的过程结构化了。
我自己的经验是:规格文件写到第三份的时候,团队就会形成默契。大家知道哪些地方容易出歧义,写规格时会主动把这些地方定义清楚。这种默契一旦形成,后面新项目启动时,规格文件的编写速度会快很多。
另外一点体会是:不要追求规格文件的“完整覆盖”。一开始就要求所有模块都有规格,只会导致大家敷衍了事。先从最痛的那个模块开始,把规格写扎实,让团队看到实际收益,后面推广就顺了。
最后分享一个实用技巧:规格文件里的验收条件,用AC-001、AC-002这样的编号。测试方法名里带上编号,代码注释里也带上编号。这样从规格到测试到代码,有一条明确的追溯链路。出问题时顺着编号查,很快能定位到是哪条验收条件对应的实现出了偏差。这个小习惯,在项目变大之后能省很多排查时间。