OpenSpec规格驱动开发:让Claude Code与Codex精准生成代码的实操指南
1. 规格驱动开发到底在解决什么问题第一次接触 OpenSpec 的人十有八九是被规格驱动开发这个词吸引过来的但真正上手之后才发现它要解决的根本不是怎么写代码的问题而是怎么让 AI 写对代码的问题。这两件事看起来差不多实际上差得很远。传统开发流程里需求文档、接口定义、数据库设计这些东西是给人看的人看完之后脑子里形成一个模糊的模型然后开始写代码。这个过程中信息损耗是巨大的但人类开发者有足够的上下文理解能力去弥补这些损耗。可当你把写代码这件事交给 Claude Code、Codex 这类 AI 编程助手时问题就暴露了——AI 不会猜它只会根据你给的信息做最直接的推断。你给它一句帮我实现用户登录它可能给你写一个完全不考虑你项目现有架构的登录模块字段命名、错误处理、鉴权方式全都跟你现有的代码风格对不上。OpenSpec 的核心思路就是在让 AI 写代码之前先把规格写清楚。这个规格不是给人看的模糊需求而是结构化的、机器可读的、包含足够约束的规范文件。它规定了数据模型长什么样、接口的输入输出是什么、边界条件怎么处理、错误码怎么定义。AI 拿到这份规格之后生成的代码就不再是猜出来的而是推导出来的。我拿一个实际场景来说明这个差别。假设你要给一个 Laravel 项目加一个订单导出功能。如果你直接跟 Claude Code 说帮我加一个订单导出 CSV 的功能它大概率会给你写一个控制器方法里面用fputcsv循环写文件然后返回一个下载响应。这个实现能跑但它没有考虑导出数据量大了怎么办、要不要走队列、字段权限怎么控制、导出记录要不要留痕、CSV 的编码和 BOM 怎么处理。这些细节如果你不写进规格里AI 是不会主动帮你补全的。而如果你先用 OpenSpec 写一份规格把这些约束都定义清楚AI 生成的代码就会自动带上队列任务、权限检查、导出日志、编码处理这些逻辑。这就是规格驱动开发的价值——把隐性的工程经验显性化让 AI 按照你的标准来干活而不是按照它的默认习惯来干活。提示规格驱动开发不是让你写更多的文档而是让你把原本存在于脑子里的隐性约束用结构化的方式表达出来。写规格的时间本质上是在替 AI 做决策省下来的是后面反复修改和调试的时间。2. OpenSpec 的规格文件结构与核心概念2.1 规格文件的基本组成OpenSpec 的规格文件不是随便写写的 Markdown它有一套相对固定的结构。理解这套结构是能不能用好它的前提。一份完整的规格通常包含以下几个部分实体定义Entity描述业务中的核心对象包括字段名、类型、约束、默认值、关联关系。这部分直接对应数据库迁移和模型定义。接口定义Endpoint描述每个 API 的路径、方法、请求参数、响应结构、状态码。这部分对应路由和控制器。行为规则Behavior描述业务逻辑的边界条件比如订单金额为负时拒绝创建库存不足时返回特定错误码。这部分对应服务层和验证逻辑。验收标准Acceptance描述功能完成的判定条件通常以给定-当-那么的格式书写。这部分对应测试用例。这四部分不是必须全部都有但实体定义和行为规则这两块我建议任何规格都别省。因为这两块是 AI 最容易自由发挥的地方你不约束它它就会按自己的理解来。2.2 实体定义怎么写才不踩坑实体定义看起来简单实际上最容易出问题。我见过太多人写实体定义的时候只写字段名和类型结果 AI 生成的迁移文件里字符串字段长度默认 255金额字段用 float时间字段没有时区处理。这些都是坑。正确的做法是实体定义里要把字段的完整约束都写清楚。比如一个订单实体你不能只写amount: decimal你要写amount: decimal(10,2), 非负, 必填, 单位元。这样 AI 生成的迁移文件才会是decimal(amount, 10, 2)验证规则里才会带上min:0。再比如关联关系你要明确写出来订单属于用户一个用户可以有多个订单AI 才会在模型里加上belongsTo和hasMany。如果你不写它可能只给你建一个user_id字段但不建关联方法。还有一个容易被忽略的点是枚举值。如果你的订单状态有待支付、已支付、已发货、已完成、已取消这几种你一定要在规格里列出来。否则 AI 可能给你用字符串存状态而不是用枚举或者常量。字符串存状态的问题在于后面做状态流转判断的时候很容易因为拼写错误导致 bug。2.3 行为规则的表达方式行为规则是规格里最有价值的部分也是最难写的部分。因为它要求你把脑子里那些理所当然的判断都写出来。比如用户不能重复下单同一个商品这个规则听起来很简单但你要写清楚是同一用户同一商品不能有未支付的订单还是完全不能有如果之前取消了能不能再下单我通常用这样的格式来写行为规则规则创建订单时检查库存 - 给定商品库存为 0 - 当用户尝试创建该商品的订单 - 那么返回错误码 STOCK_INSUFFICIENTHTTP 状态码 422这种格式的好处是它同时表达了触发条件、执行动作和预期结果AI 拿到之后可以直接生成对应的验证逻辑和测试用例。而且这种格式写多了之后你会发现自己在写规格的时候就已经把很多边界情况想清楚了这本身就是一种设计活动。2.4 规格文件放在项目里的位置OpenSpec 的规格文件通常放在项目根目录的specs/目录下按功能模块分子目录。比如specs/order/下面放订单相关的规格specs/user/下面放用户相关的规格。每个规格文件用 Markdown 格式文件名用功能名比如order-export.md。这个目录结构不是强制的但建议保持一致性。因为当你用 Claude Code 或者 Codex 来执行规格的时候你需要告诉它去哪个目录找规格文件。如果目录结构混乱AI 可能会找错文件或者把不相关的规格混在一起处理。注意规格文件不要写得太长。一个规格文件对应一个功能模块超过 500 行的规格文件建议拆分成多个。因为 AI 处理长文本的时候注意力会分散规格太长反而容易漏掉细节。3. 把 OpenSpec 接入 Claude Code 和 Codex 的实操路径3.1 环境准备与工具安装在开始之前你需要先把基础环境搭好。这里涉及几个工具Claude Code、Codex CLI以及 OpenSpec 本身。我按实际操作的顺序来说。Claude Code 的安装官方推荐的方式是通过 npm 全局安装。如果你用的是 macOS 或者 Linux直接执行npm install -g anthropic-ai/claude-code就行。Windows 用户要注意Claude Code 的桌面版需要开启虚拟化平台支持如果你在 Windows 上遇到 requires the virtual machine platform 这类提示需要去系统设置里把虚拟机平台功能打开然后重启。Codex 的安装类似也是通过 npm 或者直接下载安装包。Codex CLI 的安装命令是npm install -g openai/codex。安装完之后你需要配置 API 密钥才能使用。这里有个细节Codex 和 Claude Code 的配置文件是分开的不要搞混了。OpenSpec 本身不是一个需要安装的软件它更像是一套约定和规范。你只需要在项目里创建specs/目录然后按照规范写规格文件就行。不过有一些社区维护的 OpenSpec 模板和校验工具可以通过 npm 安装用来检查你的规格文件是否符合规范。3.2 让 Claude Code 读取规格并生成代码Claude Code 的工作方式是你在项目目录下启动它它会自动读取项目里的文件作为上下文。所以你要做的第一件事是确保规格文件在项目目录里并且路径清晰。启动 Claude Code 之后你可以用这样的指令来让它执行规格请阅读 specs/order/order-export.md 规格文件按照规格实现订单导出功能。 实现时请遵循项目现有的代码风格迁移文件放在 database/migrations 目录 控制器放在 app/Http/Controllers 目录服务类放在 app/Services 目录。这个指令里我明确告诉了 Claude Code 三件事读哪个规格文件、实现什么功能、代码放在哪里。这三件事缺一不可。如果你只说按照规格实现订单导出它可能会把代码放在它认为合理的地方但那个地方可能不符合你的项目结构。Claude Code 执行规格的时候会先读规格文件然后读项目里相关的现有代码然后生成新的代码。这个过程通常需要几分钟取决于规格的复杂度和项目的规模。生成完之后它会给你一个变更列表你可以逐个 review。这里有个实操心得不要让 Claude Code 一次性执行太大的规格。如果一个规格涉及十几个文件它生成到后面可能会忘记前面的约束。我通常会把规格拆成小块一次执行一个功能点生成完之后确认没问题再执行下一个。3.3 Codex 执行规格的差异Codex 和 Claude Code 在执行规格的时候行为有一些差异。Codex 更倾向于一次性生成完整的代码块而 Claude Code 更倾向于逐步修改现有文件。这个差异导致它们在处理规格时的策略不同。用 Codex 执行规格的时候我建议把规格文件的内容直接粘贴到对话里而不是让它去读文件。因为 Codex 的文件读取能力相对弱一些有时候会读不到或者读错。直接粘贴虽然麻烦一点但能保证它拿到的是完整的规格内容。Codex 生成代码之后你需要手动把代码放到对应的文件里。它不会像 Claude Code 那样自动修改项目文件。这个差异在操作上要多一步但好处是你可以先 review 再决定要不要用。还有一个差异是Codex 对 Laravel 项目的理解程度不如 Claude Code。如果你用的是 LaravelClaude Code 生成的代码通常更符合 Laravel 的惯例比如会自动用 Eloquent 的关联方法、会自动生成 FormRequest 验证类。Codex 有时候会生成比较通用的 PHP 代码需要你手动调整成 Laravel 风格。3.4 规格执行后的验证流程代码生成完之后不要急着提交。我通常按这样的顺序验证语法检查先跑一遍php -l检查语法错误或者用 IDE 的静态分析功能。迁移检查如果是数据库相关的规格先跑php artisan migrate --pretend看看迁移语句是否符合预期。单元测试如果规格里写了验收标准让 AI 同时生成测试用例然后跑测试。手动验证启动本地服务用 Postman 或者 curl 实际调一下接口看看返回结果是否符合规格。这个流程看起来繁琐但能帮你省下大量调试时间。我见过太多人直接让 AI 生成代码就提交结果线上出问题才发现规格里的约束没被执行。提示如果你在 Windows 上遇到 Claude Code 无法识别命令的问题比如提示 无法将 claude 项识别为 cmdlet这通常是环境变量没配置好。检查一下 npm 的全局安装路径是否在 PATH 里或者直接用 npx 来运行。4. 规格驱动开发在 Laravel 项目中的落地细节4.1 Laravel 项目的规格组织方式Laravel 项目有比较明确的目录结构这给规格组织提供了便利。我通常按这样的方式组织规格specs/ ├── models/ │ ├── order.md │ ├── user.md │ └── product.md ├── endpoints/ │ ├── order-api.md │ └── user-api.md ├── behaviors/ │ ├── order-workflow.md │ └── payment-rules.md └── acceptance/ ├── order-export.md └── user-register.md这种组织方式的好处是模型规格、接口规格、行为规格、验收规格分开存放AI 在执行的时候可以按需读取。比如你只需要实现模型和迁移就只让它读models/目录下的规格需要实现完整功能就让它读所有相关规格。4.2 迁移文件与模型定义的规格写法Laravel 的迁移文件和模型定义是紧密关联的规格里要把这两者的对应关系写清楚。我通常会在模型规格里同时写迁移字段和模型配置。以订单模型为例规格里会这样写实体Order 表名orders 字段 - id: bigint, 主键, 自增 - user_id: bigint, 外键关联 users 表, 必填, 索引 - order_no: string(32), 唯一, 必填 - amount: decimal(10,2), 非负, 必填 - status: enum(pending, paid, shipped, completed, cancelled), 默认 pending - paid_at: timestamp, 可空 - created_at: timestamp - updated_at: timestamp 模型配置 - 使用 SoftDeletes - 关联 User: belongsTo - 关联 OrderItem: hasMany - 状态字段使用常量定义不用魔术字符串这样写的好处是AI 生成迁移文件的时候会严格按照字段定义来生成模型的时候会自动加上 SoftDeletes trait 和关联方法。而且状态字段用常量定义这个要求能避免后面代码里到处出现pending这样的魔术字符串。4.3 接口规格与 FormRequest 的对应Laravel 的 FormRequest 是验证请求参数的标准方式规格里要把验证规则写清楚AI 才会生成对应的 FormRequest 类。接口规格里我通常会这样写接口POST /api/orders 请求参数 - product_id: required, integer, exists:products,id - quantity: required, integer, min:1, max:99 - remark: nullable, string, max:500 响应 - 成功201, { order_id: 1, order_no: xxx, amount: 100.00 } - 库存不足422, { error: STOCK_INSUFFICIENT, message: 库存不足 } - 商品不存在422, { error: PRODUCT_NOT_FOUND, message: 商品不存在 }这样写之后AI 生成的 FormRequest 里会自动带上exists:products,id这样的验证规则控制器里也会根据不同的错误情况返回对应的状态码和错误信息。4.4 队列任务与异步处理的规格表达Laravel 的队列是处理耗时任务的标准方案但 AI 默认生成的代码往往不会主动用队列。你需要在规格里明确写出来。比如订单导出功能规格里要写行为订单导出 - 导出请求不直接生成文件而是推送到队列 - 队列任务类名ExportOrdersJob - 队列名称exports - 导出完成后发送通知给用户 - 导出文件保留 7 天过期自动清理这样 AI 生成的代码就会包含 Job 类、队列推送逻辑、通知发送逻辑以及一个清理过期文件的定时任务。如果你不写这些它可能就给你写一个同步导出的控制器方法数据量一大就超时。4.5 权限控制与中间件的规格描述Laravel 的权限控制通常通过中间件和 Policy 来实现规格里要把权限规则写清楚。权限规则 - 只有 admin 角色的用户可以访问订单导出接口 - 普通用户只能导出自己的订单 - 导出接口需要 auth:sanctum 中间件 - 导出频率限制每分钟最多 3 次这样 AI 生成的代码就会带上auth:sanctum中间件、角色检查逻辑、频率限制配置。如果你不写它可能只给你加一个基础的 auth 中间件角色检查就漏掉了。注意Laravel 项目里有一个历史漏洞 CVE-2024-29291涉及某些版本的文件上传处理。虽然这个漏洞跟 OpenSpec 没有直接关系但如果你在用 AI 生成文件上传相关代码建议在规格里明确写上文件类型白名单、大小限制、存储路径隔离这些约束避免生成不安全的代码。5. 规格执行过程中的常见故障与排查思路5.1 AI 生成的代码偏离规格这是最常见的问题。你明明在规格里写了金额字段是decimal(10,2)AI 生成的迁移文件里却是float。这种情况通常有几个原因第一个原因是规格文件太长AI 读到后面忘了前面。解决办法是把规格拆短或者把关键约束放在规格文件的开头。第二个原因是规格里的表述有歧义。比如你写金额字段用 decimalAI 可能理解为用 decimal 类型但不指定精度。解决办法是写得更具体decimal(10,2)而不是decimal。第三个原因是 AI 的默认习惯覆盖了规格约束。比如 AI 默认喜欢用 float 存金额即使你写了 decimal它也可能自作主张改回 float。解决办法是在规格里加上强调性的说明比如金额字段必须使用 decimal(10,2)禁止使用 float 或 double。5.2 规格文件读取失败有时候 Claude Code 或 Codex 会提示找不到规格文件或者读取的内容不完整。这种情况通常是路径问题。Claude Code 读取文件是相对于项目根目录的所以你的规格文件必须在项目根目录下或者用相对于根目录的路径来引用。如果你把规格文件放在项目外面它就读不到。Codex 的文件读取能力更弱一些有时候即使路径正确也读不到。这种情况下直接把规格内容粘贴到对话里是最稳妥的办法。还有一个可能的原因是文件编码问题。如果你的规格文件是 GBK 编码AI 读取的时候可能会乱码。建议统一用 UTF-8 编码保存规格文件。5.3 生成的代码与现有代码冲突AI 生成代码的时候如果项目里已经有类似功能的代码它可能会生成重复的类或者方法。比如项目里已经有一个OrderServiceAI 又生成了一个OrderExportService两个类里有重复的逻辑。避免这个问题的方法是在规格里明确写出复用现有的 XX 类或者扩展现有的 XX 方法。另外在执行规格之前先让 AI 读一遍相关的现有代码它生成的时候就会考虑复用而不是重写。如果已经生成了冲突的代码不要急着删。先对比一下两边的逻辑看看哪边的实现更符合规格要求然后把好的部分合并进去。5.4 队列任务不执行用规格生成的队列任务有时候推送到队列了但不执行。这种情况通常是队列配置问题不是规格问题。检查几个地方队列驱动是否配置正确QUEUE_CONNECTION环境变量、队列监听进程是否启动php artisan queue:work、队列名称是否匹配规格里写的队列名称和实际推送的队列名称是否一致。还有一个容易忽略的点是如果规格里写了队列名称是exports但你的config/queue.php里没有配置这个队列任务就会被推到默认队列。所以规格里写的队列名称要跟项目配置保持一致。5.5 测试用例跑不过规格里写的验收标准AI 生成的测试用例有时候跑不过。这不一定是用例写错了可能是实现代码没完全按照规格来。排查的顺序是先看测试用例的断言是否符合规格里的验收标准再看实现代码是否满足断言最后看规格本身是否有矛盾的地方。我遇到过一种情况规格里写库存不足时返回 422但验收标准里写库存不足时返回 400两个地方不一致AI 生成的代码和测试用例就冲突了。所以规格写完之后自己要通读一遍确保前后一致。提示如果你在配置 Claude Code 或 Codex 的时候遇到网络相关的错误提示比如连接超时或者端点无响应先检查本地的网络配置和代理设置。这类问题通常跟工具本身无关而是环境配置的问题。6. 让规格真正落地的几个经验性做法6.1 规格先行代码后行这是最核心的一条经验。很多人用 AI 编程的习惯是先让 AI 生成代码然后根据生成的代码反推需求。这个顺序是反的会导致代码质量不可控。正确的做法是先写规格规格确认无误之后再让 AI 执行规格生成代码。写规格的过程本质上是在做设计。你把数据模型、接口定义、行为规则都想清楚了代码只是规格的翻译结果。我自己的习惯是每个功能在动手之前先花 20 到 30 分钟写规格。这 20 分钟里我会把字段类型、验证规则、错误处理、边界条件都过一遍。写完之后再让 AI 执行。这样生成出来的代码一次通过率能到 80% 以上剩下的 20% 主要是代码风格和项目惯例的调整。6.2 规格要跟着代码一起维护规格不是一次性的东西。功能迭代的时候规格也要跟着更新。否则下次让 AI 执行规格的时候它会按照旧规格生成代码跟现有代码冲突。我通常把规格文件和代码放在同一个 commit 里提交。改了代码就同步改规格。这样规格始终是代码的准确描述AI 执行的时候不会跑偏。如果项目比较大规格文件比较多可以写一个简单的校验脚本定期检查规格文件和实际代码的一致性。比如检查规格里定义的字段是否都在迁移文件里存在规格里定义的接口是否都在路由文件里注册。6.3 规格的粒度控制规格写得太粗AI 会自由发挥规格写得太细写规格的时间比写代码还长。这个度怎么把握我的经验是规格写到决策点为止。什么是决策点就是那些有多种合理选择、需要人为决定的地方。比如字段类型是 decimal 还是 float这是决策点要写进规格。但具体用哪个函数来格式化金额这不是决策点不用写。再比如接口返回的错误码格式这是决策点要写。但错误信息的具体措辞这不是决策点可以让 AI 自己定。按这个标准一个中等复杂度的功能规格大概在 100 到 200 行之间。超过 300 行就说明写得太细了低于 50 行就说明写得太粗了。6.4 用规格来传递项目规范每个项目都有自己的编码规范比如命名习惯、目录结构、错误处理方式。这些规范如果只存在于团队成员的脑子里AI 是不知道的。但如果你把它们写进规格里AI 就会遵守。我通常会在项目里放一个specs/conventions.md文件里面写项目的通用规范。比如控制器方法名用驼峰式服务类以 Service 结尾错误码用大写下划线格式所有数据库操作必须用事务包裹。然后在每个具体规格的开头引用这个文件。这样 AI 在执行具体规格的时候会先读通用规范再读具体规格生成的代码就会同时符合项目规范和功能需求。6.5 规格的版本管理规格文件也要做版本管理。当需求变更的时候不要直接改规格文件而是新建一个版本或者在规格文件里用注释标注变更历史。这样做的好处是当 AI 生成的代码出问题的时候你可以回溯到之前的规格版本看看是哪个变更导致的。另外如果项目有多个分支不同分支的规格可能不同版本管理能帮你理清这些差异。我通常用 Git 来管理规格文件跟代码用同一个仓库。规格的变更记录在 commit message 里写清楚比如更新订单导出规格增加队列重试配置。7. 关于规格驱动开发的一些个人体会用了大半年 OpenSpec 之后我最大的感受是它改变的不只是 AI 编程的效率而是整个开发流程的思维方式。以前写代码的时候很多决策是在写的过程中临时做的做完就忘了。现在写规格的时候这些决策被显性化地记录下来后面回头看的时候能清楚地知道当时为什么这么设计。另一个感受是规格驱动开发对团队协作的帮助很大。以前 code review 的时候reviewer 要花很多时间去理解代码的意图。现在有了规格reviewer 可以先看规格再看代码理解成本降低了很多。而且规格本身就是一种文档新人接手项目的时候看规格比看代码快得多。当然这套方法也不是没有代价。写规格需要时间执行规格需要时间验证规格执行结果也需要时间。如果你的项目很小或者功能很简单用规格驱动开发可能反而更慢。我的建议是只在那些需要长期维护、多人协作、逻辑复杂的功能上使用规格驱动开发。一次性的脚本、简单的 CRUD、原型验证这些场景直接让 AI 写代码就行不用上规格。还有一个体会是规格的质量直接决定了 AI 生成代码的质量。规格写得清楚AI 生成的代码就靠谱规格写得模糊AI 生成的代码就随机。所以与其花时间调 AI 的参数不如花时间把规格写好。这个投入产出比是最高的。最后说一个实操中的小技巧。如果你不确定规格该怎么写可以先让 AI 帮你写一版规格草稿然后你在草稿的基础上修改。比如你可以跟 Claude Code 说我要实现一个订单导出功能请帮我写一份 OpenSpec 规格草稿包含实体定义、接口定义、行为规则和验收标准。它生成的草稿可能不完美但能给你一个起点你在上面改比从零开始写要快得多。改完之后再让 AI 按照修改后的规格生成代码。这个AI 写规格草稿人改规格AI 执行规格的流程是我目前用下来效率最高的方式。