1. 从一份 config.yaml 说起:SDD 到底在解决什么问题
第一次接触 OpenSpec 是在一个多人协作的后端项目里。当时团队里三个人,一个习惯先写接口文档再动手,一个喜欢直接开干边写边改,还有一个偏爱在聊天窗口里把需求讲清楚就开工。结果就是:接口字段对不上、边界条件各写各的、联调阶段天天吵架。后来有人甩了一份config.yaml出来,说"以后所有需求先落到这个文件里,再让 AI 按文件生成代码"。那份 yaml 就是 OpenSpec 的规范入口,而它背后代表的这套方法论,就是 SDD——Specification-Driven Development,规范驱动开发。
说白了,SDD 的核心主张只有一句话:先把"要做什么"用结构化、机器可读的方式写清楚,再让代码生成或人工实现去对齐这份规范。它跟传统的"先写文档再写代码"最大的区别在于,规范不是给人看的散文,而是能被工具解析、能被校验、能驱动生成的结构化数据。OpenSpec 就是把这套理念工程化落地的一个工具集,它用config.yaml作为项目级配置,用规范文件描述能力、接口、数据模型,再配合 Claude Code 这类 AI 编码助手,把规范直接翻译成可运行的代码骨架。
这套东西适合谁?我的判断是三类人最该关注:一是中小团队的技术负责人,苦于需求传递失真、返工率高;二是独立开发者,想用 AI 快速起项目但又不希望生成一堆风格混乱的代码;三是正在把 AI 编码工具引入工作流的工程师,需要一套"约束机制"让 AI 别乱发挥。如果你只是偶尔写个脚本,那 SDD 对你来说是杀鸡用牛刀;但只要项目超过两个人、生命周期超过一个月,规范驱动带来的收益就会指数级放大。
我踩过的第一个坑,就是把 SDD 理解成"写更详细的文档"。不是的。文档是给人读的,规范是给工具和人都能读的。这个认知差异,决定了你后面所有配置和文件组织的方式。下面我按实际落地的顺序,把 OpenSpec 这套东西拆开讲。
2. OpenSpec 的整体设计与核心思路拆解
2.1 为什么是"规范驱动"而不是"提示词驱动"
很多人用 Claude Code 的方式是:打开终端,敲一句"帮我写一个用户登录接口",然后看它生成什么。这种方式在一次性脚本上没问题,但在真实项目里会迅速失控——因为每次生成的风格、命名、错误处理都不一样,而且你没法追溯"这个函数当初是按什么需求写的"。
OpenSpec 的思路是把"提示词"升级成"规范"。规范是持久化的、版本可控的、结构化的。你不再对 AI 说"写个登录接口",而是先定义一份规范文件,里面写清楚:这个能力叫什么、输入是什么、输出是什么、有哪些边界条件、依赖哪些其他能力。然后 AI 基于这份规范生成代码。这样做的好处有三个:第一,需求变更时改规范而不是改提示词,历史可追溯;第二,多人协作时大家对齐的是同一份规范,不是各自的记忆;第三,规范可以被校验,字段缺失、类型不匹配这类问题在生成代码之前就能发现。
我实测下来最直观的感受是:返工率明显下降。以前联调阶段才发现字段对不上,现在在规范评审阶段就暴露了。这个提前量,就是 SDD 最大的价值。
2.2 config.yaml 在项目里扮演什么角色
config.yaml是 OpenSpec 的项目级配置入口,它决定了工具怎么理解你的项目结构、规范放在哪、生成物输出到哪、用哪个 AI 后端。一份典型的配置大概长这样:
project: name: user-service root: ./src specs: dir: ./specs format: openspec-v1 generator: provider: claude-code model: default output: ./src/generated overwrite: false validation: strict: true required_fields: - name - inputs - outputs这里每一项都不是随便填的。specs.dir决定规范文件的存放位置,我习惯放在项目根目录下的specs/,跟源码平级,方便 review。generator.overwrite我强烈建议设成false,因为一旦设成true,AI 重新生成时会覆盖你手改过的代码,这个坑我踩过一次,丢了大半天的改动。validation.strict打开后,规范里缺字段会直接报错而不是警告,前期严格一点,后期省心很多。
提示:
config.yaml建议纳入版本控制,但generator.output指向的生成目录是否入库,取决于你们团队对生成代码的信任度。我的做法是生成目录也入库,但加一条 CI 检查,确保生成代码和规范保持同步。
2.3 规范文件的结构设计逻辑
OpenSpec 的规范文件通常按"能力"拆分,一个能力一个文件。比如用户模块下有user.create.spec、user.login.spec、user.profile.spec。每个文件描述一个独立的能力单元,包含名称、描述、输入、输出、边界条件、依赖关系。
为什么按能力拆而不是按文件拆?因为 AI 生成代码时,上下文窗口是有限的。如果你把所有规范塞进一个大文件,AI 读的时候会丢失细节。按能力拆分后,生成某个接口时只需要加载相关的几个规范文件,上下文更聚焦,生成质量更高。这是我在实际项目里对比过两种组织方式后得出的结论——大文件方式生成的代码经常漏掉边界条件,拆分后明显改善。
2.4 与 Claude Code 的协作边界
OpenSpec 本身不生成代码,它负责"规范解析 + 提示词组装 + 结果校验",真正的代码生成交给 Claude Code。这个分工很重要:OpenSpec 是约束层,Claude Code 是执行层。约束层保证"生成什么"是确定的,执行层负责"怎么生成"。
这样设计的好处是解耦。哪天你想换个 AI 后端,只要 OpenSpec 支持,改一行config.yaml就行,规范文件不用动。我试过在同一个项目里切换不同的生成后端,规范层完全无感,这个灵活性在工具选型阶段特别有价值。
3. 核心细节解析与实操要点
3.1 环境准备:Claude Code 的安装与配置
在讲 OpenSpec 之前,得先把 Claude Code 跑起来,因为它是默认的生成后端。安装方式按平台分:
macOS 和 Linux 下,官方推荐的方式是通过包管理器安装。Ubuntu 上我一般用 npm 全局安装:
npm install -g @anthropic-ai/claude-code装完之后用claude --version验证。Windows 下稍微麻烦一点,建议在 WSL2 里操作,原生 Windows 环境偶尔会有路径解析问题。VS Code 用户可以直接装 Claude Code 扩展,在扩展市场搜 "Claude Code for VS Code" 即可,装完在设置里配置好可执行文件路径。
注意:安装过程中如果遇到网络相关的报错,先检查本地环境是否满足官方文档列出的前置条件。官方文档链接建议直接从项目仓库的 README 里找,不要从第三方转载页面拿,版本容易过期。
配置环节有几个关键点。第一是模型选择,Claude Code 支持切换不同模型后端,具体支持哪些以官方文档为准。第二是终端命令执行权限,Claude Code 默认会询问是否允许执行终端命令,如果你信任当前项目,可以在配置里放开,但生产环境项目我建议保持询问模式。第三是工作目录,一定要在项目根目录启动,否则它找不到config.yaml。
3.2 规范文件的编写规范与常见错误
写规范文件最容易犯的错是"写得太像文档"。比如有人会写"用户登录时应该验证密码是否正确",这句话对人来说很清楚,但对工具来说太模糊——什么叫"验证"?密码错误返回什么?这些都没说。
正确的写法是把每个能力拆成结构化的字段:
name: user.login description: 用户通过账号密码登录 inputs: - name: username type: string required: true - name: password type: string required: true outputs: - name: token type: string - name: expires_at type: integer errors: - code: INVALID_CREDENTIALS when: 用户名不存在或密码错误 - code: ACCOUNT_LOCKED when: 连续失败超过5次 dependencies: - user.store - token.issue这样写的好处是,AI 生成代码时能精确知道要处理哪些错误分支,不会漏掉ACCOUNT_LOCKED这种情况。我对比过模糊描述和结构化描述两种规范生成的代码,后者在错误处理上的完整度高出一大截。
常见错误我整理成了一张表:
| 错误类型 | 表现 | 后果 | 修正方式 |
|---|---|---|---|
| 描述模糊 | 用自然语言写需求 | AI 自由发挥,边界遗漏 | 拆成 inputs/outputs/errors |
| 字段缺失 | 没写 required | 生成代码不校验必填 | 打开 strict 校验 |
| 依赖循环 | A 依赖 B,B 又依赖 A | 生成顺序死锁 | 梳理依赖为有向无环图 |
| 命名不一致 | 同一概念多种叫法 | 生成代码命名混乱 | 建立术语表统一命名 |
3.3 生成产物的目录组织与命名约定
生成代码放哪、怎么命名,这个看似小事,实际上影响后续维护。我的约定是:生成目录按能力模块分子目录,文件名跟规范文件名对应。比如user.login.spec生成的代码放在src/generated/user/login.ts。这样从代码能反查到规范,从规范也能定位到代码。
命名上我坚持一个原则:生成代码和手写代码物理隔离。生成的全在generated/下,手写的业务逻辑在src/其他目录。这样重新生成时不会误伤手写代码,也方便在 code review 时区分"这是 AI 生成的"和"这是人写的"。这个隔离策略是我在第二个项目里才想明白的,第一个项目混在一起,后来重构时痛苦不堪。
3.4 校验机制:让规范在生成前就"跑一遍"
OpenSpec 的校验分两层。第一层是语法校验,检查 yaml 格式、必填字段、类型合法性。第二层是语义校验,检查依赖是否存在、命名是否冲突、错误码是否重复。第一层在保存文件时就能触发,第二层需要跑一次openspec validate命令。
我强烈建议把校验接进 CI。每次提交规范文件时自动跑一遍,不通过就阻断合并。这样能保证主分支上的规范永远是"可生成"的状态。实测下来,这个 CI 检查拦住了不少低级错误,比如有人改了错误码忘了同步依赖它的规范。
4. 实操过程与核心环节实现
4.1 从零搭建一个 OpenSpec 项目的完整流程
假设我们要做一个待办事项服务,从零开始走一遍。
第一步,初始化项目结构。在项目根目录执行 OpenSpec 的初始化命令(具体命令以你安装的版本为准),它会生成config.yaml和specs/目录骨架。如果工具没有初始化命令,手动创建这两个东西也行,config.yaml按前面给的模板填。
第二步,编写第一个规范。创建specs/todo.create.spec,描述"创建待办"这个能力。输入是标题和可选的截止日期,输出是待办 ID 和创建时间,错误包括标题为空、标题超长。
第三步,跑校验。执行openspec validate,确认规范合法。这一步会告诉你缺了哪些字段、依赖是否满足。
第四步,生成代码。执行openspec generate todo.create,OpenSpec 会组装提示词、调用 Claude Code、把生成结果写到generator.output指定的目录。
第五步,人工 review 生成代码。这一步不能省。AI 生成的代码在结构上通常没问题,但业务细节需要人确认。我一般重点看三处:错误处理是否完整、边界条件是否覆盖、命名是否符合团队约定。
第六步,把生成代码接入实际业务。生成的是骨架,真正的数据库操作、缓存逻辑还需要手写。手写部分放在generated/之外的目录,通过依赖注入或接口实现的方式接进去。
4.2 参数选择:模型、温度、上下文窗口怎么定
生成质量跟参数关系很大。模型选择上,复杂业务逻辑用能力强的模型,简单 CRUD 用轻量模型即可,没必要所有场景都上最强的。温度参数(如果后端支持调节)我一般设得比较低,因为代码生成需要确定性,温度高了会生成风格飘忽的代码。
上下文窗口是另一个关键。OpenSpec 在组装提示词时,会把相关规范文件的内容拼进去。如果依赖链很长,提示词会变得很大,超出窗口后 AI 会丢失前面的信息。我的应对策略是:控制单个能力的依赖数量,超过五个依赖就考虑拆分能力。这个数字不是绝对的,但超过五个后生成质量下降很明显,这是我多次实测的观察。
4.3 一次完整的生成现场记录
拿"创建待办"这个能力举例,我记录一下实际生成过程。
规范文件写好后,执行生成命令。OpenSpec 先解析规范,输出一份中间表示(可以理解为"给 AI 看的提示词")。这份中间表示大概包含:能力描述、输入输出定义、错误分支、依赖的接口签名。然后它调用 Claude Code,把中间表示作为上下文传进去。
Claude Code 返回的是一段 TypeScript 代码,包含函数签名、参数校验、错误抛出、以及一个 TODO 注释标记"这里需要接入实际存储"。我检查了一遍,发现它把"标题超长"的边界条件处理成了throw new Error,但我们团队的约定是用自定义错误类。于是我改了一下规范里的错误定义,加上error_class字段,重新生成,这次就对了。
这个过程说明一个点:规范不是一次写对的,是迭代出来的。第一次生成发现问题,改规范而不是改生成代码,这样下次生成才不会重蹈覆辙。这个习惯养成后,规范会越来越精确,生成质量也会越来越高。
4.4 与现有代码库的集成方式
OpenSpec 生成的是新代码,怎么跟已有代码库融合是个现实问题。我的做法是分三步:先让生成代码独立存在,跑通单元测试;再通过适配层接入现有业务逻辑;最后逐步替换掉旧的手写实现。
适配层的写法取决于你的架构。如果是依赖注入框架,把生成代码注册成 provider 即可。如果是简单的函数调用,写一个 wrapper 把生成函数的签名转成现有代码期望的签名。这一步不要偷懒直接改生成代码,否则下次重新生成又得改一遍。
提示:集成阶段建议保留旧实现一段时间,用 feature flag 控制走新路径还是旧路径。等新路径稳定后再移除旧代码。这个灰度策略在真实项目里救过我好几次。
5. 常见问题与排查技巧实录
5.1 生成代码不符合预期怎么办
这是最高频的问题。排查顺序我总结成"三看":一看规范是否描述清楚,二看依赖是否加载完整,三看模型是否选对。
大部分情况是规范的问题。比如生成代码漏了某个错误分支,回去看规范,发现那个分支压根没写。或者生成代码命名奇怪,回去看规范,发现同一个概念用了两种叫法。规范是源头,源头不清,下游必乱。
如果规范没问题,检查依赖加载。OpenSpec 生成时会加载依赖的规范文件,如果依赖路径写错,AI 拿不到依赖的接口签名,就会自己瞎编一个。这种情况生成的代码编译能过,但运行时对不上。
最后才怀疑模型。换个能力强的模型重试一次,如果还是不行,那基本可以确定是规范的问题。
5.2 规范与代码不同步的检测方法
项目跑一段时间后,经常出现"规范改了但代码没重新生成"或者"代码手改了但规范没更新"的情况。检测方法有两个:一是比对生成代码的哈希值,OpenSpec 可以在生成时记录哈希,重新生成时比对,不一致就报警;二是定期跑一次全量生成,看 diff 有多大,diff 大说明规范漂移严重。
我倾向于第一种,接进 CI 自动跑。第二种作为月度健康检查。两种结合,基本能保证规范和代码不脱节。
5.3 多人协作时的规范冲突处理
多人同时改规范,冲突是必然的。我的处理原则是:规范文件的粒度要细到"一个人一次只改一个文件"。如果两个人都要改user.login.spec,那说明这个能力该拆了。拆成user.login.password.spec和user.login.token.spec,各改各的,冲突自然消失。
如果实在拆不开,那就走正常的代码合并流程,人工解决冲突。规范文件是文本,合并冲突跟合并代码没区别。关键是合并后要重新跑校验和生成,确保合并结果仍然可用。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查动作 | 解决方式 |
|---|---|---|---|
| 生成命令报错找不到规范 | specs.dir 配置错误 | 检查 config.yaml | 修正路径 |
| 生成代码缺字段 | 规范里没定义该字段 | 打开 strict 校验 | 补全规范 |
| 生成代码覆盖手写逻辑 | overwrite 设为 true | 检查配置 | 改为 false |
| 依赖加载失败 | 依赖路径拼写错误 | 看生成日志 | 修正依赖名 |
| 生成质量突然下降 | 上下文超窗口 | 数依赖数量 | 拆分能力 |
| 校验通过但生成失败 | 模型后端不可用 | 检查后端配置 | 切换或重试 |
5.5 几个我踩过的坑
第一个坑是overwrite配置。前面提过,设成 true 后重新生成会覆盖手改代码。我现在的做法是生成目录只读,需要改就改规范重新生成,绝不手改生成代码。
第二个坑是规范文件编码。有次同事用 GBK 编码保存了一个规范文件,OpenSpec 解析时中文全乱码,生成的代码里注释都是乱码。统一用 UTF-8,这个没得商量。
第三个坑是依赖循环。A 依赖 B,B 依赖 A,生成时死锁。后来我加了一条规则:依赖关系必须是有向无环图,写规范时先在纸上画一遍依赖图,确认没环再落文件。
第四个坑是模型版本漂移。同一个规范,隔了一个月重新生成,代码风格变了。原因是后端模型升级了。这个没法完全避免,应对方式是锁定模型版本,升级时做一次全量回归。
6. 规范驱动开发的边界与我的实际体会
SDD 不是银弹,它有明确的适用边界。我总结下来,适合 SDD 的场景有三个特征:需求相对稳定、接口边界清晰、团队有规范意识。反过来,如果需求天天变、接口还在探索阶段、团队习惯自由发挥,那强行上 SDD 只会增加负担。
我在实际项目里的体会是:SDD 最大的价值不在生成代码,而在逼你把需求想清楚。写规范的过程,就是一次结构化的需求梳理。很多以前在编码阶段才暴露的问题,现在在写规范时就暴露了。这个提前量,比生成代码本身值钱得多。
另外一点,OpenSpec 这类工具还在快速演进,配置格式、命令、支持的模型后端都可能变。我的建议是:核心方法论(规范驱动)值得投入,具体工具保持关注但不要过度绑定。规范文件用通用的 yaml 写,即使哪天换工具,迁移成本也可控。
最后分享一个小技巧:刚开始用 SDD 时,不要一上来就全项目铺开。挑一个独立的小模块试点,跑通"写规范、生成、集成、迭代"这个完整闭环,再逐步扩大范围。我见过太多团队一上来就全量改造,结果规范写了一半发现方向不对,进退两难。小步快跑,是这套东西落地最稳的姿势。