1. 当AI写代码开始“抽卡”,问题到底出在哪
用AI写代码这件事,很多人应该都有类似的体验:同一个需求,早上问它给你生成一版能跑的,下午再问一遍,它给你换了个架构、换了个命名风格,甚至把之前明确说过的约束全忘了。你反复强调“不要用某个库”“错误处理要统一走中间件”“接口返回结构必须是这个格式”,它当时答应得好好的,下一轮对话又打回原形。这种状态我一般叫它“抽卡式编码”——能不能生成符合预期的代码,全看运气。
问题的根子不在于模型不够聪明,而在于我们和AI之间的协作方式本身是无规格的。我们习惯用自然语言一段一段地描述需求,模型每次都在一个模糊的上下文里做概率推断,它没有一份稳定、可复用、可校验的“契约”去对齐。人跟人协作的时候,靠的是需求文档、接口定义、编码规范这些东西;但到了人跟AI协作,绝大多数人把这些全丢了,直接开聊。结果就是AI每次都在重新猜你想要什么。
OpenSpec 这个项目要解决的就是这件事。它提出的核心概念叫规格驱动开发(Spec-Driven Development,简称 SDD),思路很直接:在让AI写代码之前,先把“规格”用结构化的方式写清楚,让规格成为人和AI共同遵守的唯一事实来源。而承载规格的载体,选的是Markdown——一个几乎所有开发者都熟悉、几乎所有工具链都支持的格式。你不需要学一套新的DSL,不需要装什么重型框架,用Markdown把需求、约束、接口、验收标准写明白,AI就能稳定地产出符合预期的代码。
这篇文章适合几类人看:一是已经在日常用AI辅助编码、但被“不稳定”折磨过的开发者;二是团队里想推行AI编码规范、但不知道怎么落地的技术负责人;三是对SDD这个概念好奇、想搞清楚它和传统需求文档区别的人。我会把OpenSpec背后的逻辑、规格该怎么写、实际落地时会踩哪些坑,尽量讲透。
2. 规格驱动开发到底“驱动”了什么
2.1 从“对话式提示”到“契约式规格”的转变
大多数人用AI编码的默认姿势是对话式的:我描述一段,它生成一段,不满意我再补充说明,它再改。这种方式在一次性、小颗粒的任务上没问题,但一旦任务变复杂、跨越多个文件、需要多轮迭代,上下文就开始漂移。模型记不住你三轮之前说过的约束,你也记不住自己到底提过哪些要求。最后代码能跑,但风格混乱、边界条件缺失、后续没法维护。
SDD 的做法是把“对话”升级成“契约”。在动手写代码之前,先产出一份规格文档,里面明确写清楚:这个模块要做什么、输入输出是什么、有哪些硬性约束、验收标准是什么、不允许出现什么。这份规格不是给人看的摆设,而是同时给人和AI看的执行依据。AI基于规格生成代码,人基于规格做review,双方对齐的是同一份东西,而不是各自脑子里的模糊印象。
这个转变的关键在于:规格是可持久化的。对话是流式的,说完就散了;规格是文件,躺在仓库里,随时可以查、可以改、可以版本化。下次再让AI改这个模块,你把规格丢给它,它就有了完整的上下文,不需要你从头再讲一遍。
2.2 为什么偏偏选Markdown作为规格载体
选Markdown不是随便定的。规格载体要满足几个条件:第一,人和AI都能轻松读写;第二,能表达结构(标题、列表、表格、代码块);第三,工具链支持好,能diff、能渲染、能转换;第四,学习成本低,团队里没人需要专门培训。
Markdown 几乎完美命中这四点。它的语法足够表达规格需要的结构——用标题分层、用列表列约束、用表格对比接口、用代码块写示例。同时它又是纯文本,git diff 看得清清楚楚,code review 的时候改动一目了然。更关键的是,AI对Markdown的理解非常成熟,你给它一份Markdown规格,它解析起来比解析一段散文式的自然语言准确得多。
这里有个实操细节值得说:Markdown的换行和表格在规格里特别重要。很多人写Markdown习惯一行写很长,渲染出来没问题,但在纯文本状态下读起来很痛苦,AI解析时也容易把本该分开的两条约束连成一条。我的习惯是每条约束独立成行,用列表项隔开,宁可多换行也不要挤在一起。表格则用来表达“输入字段-类型-是否必填-说明”这类结构化信息,比用文字描述清晰一个量级。
2.3 SDD和传统需求文档的本质区别
有人会问:这不就是写需求文档吗,有什么新鲜的?区别在于受众和执行方式。传统需求文档是写给人看的,写完就归档,开发的时候大家凭记忆和理解去实现,文档和代码之间没有强绑定。SDD的规格是写给AI执行的,它必须精确到AI能直接据此生成代码的程度,模糊的形容词(“尽量快”“用户体验好”)在规格里是无效的,必须换成可判定的标准(“响应时间P99小于200ms”“错误码统一为四位数字”)。
另一个区别是粒度。传统需求文档往往停留在功能层面,SDD的规格要下沉到接口、数据结构、边界条件、错误处理。因为AI不会像人一样“领会精神”,你写多细它就做多细,你没写的它就自己发挥——而它发挥的方向往往不是你想要的。
3. 一份能真正驱动AI的规格长什么样
3.1 规格的最小结构:目标、约束、接口、验收
一份能用的规格,我总结下来至少要有四块内容。目标说清楚这个模块解决什么问题,一到两句话,不要展开。约束列出所有硬性要求,包括技术栈、命名规范、禁止使用的方案、性能底线。接口定义输入输出,字段名、类型、必填性、取值范围都要写死。验收标准是可执行的检查项,最好能对应到测试用例。
这四块缺一不可。我见过有人只写目标就丢给AI,结果生成的东西方向对但细节全错;也见过只写约束不写验收,代码看起来符合要求但根本没法验证。规格的价值就在于它把“我以为我说清楚了”变成“白纸黑字写清楚了”。
3.2 用Markdown表格锁死接口定义
接口定义是规格里最容易出问题的地方,因为字段一多就容易漏。用Markdown表格来锁是最稳的。比如定义一个用户查询接口,表格里列清楚字段名、类型、是否必填、说明、示例值。AI拿到这张表,生成出来的结构体或类基本不会跑偏。
| 字段名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
| userId | string | 是 | 用户唯一标识,长度32 | "u_123456" |
| pageSize | int | 否 | 每页条数,默认20,最大100 | 20 |
| cursor | string | 否 | 分页游标,为空则从头开始 | "c_abc" |
这张表的好处是,它同时约束了字段名、类型、默认值和边界。AI生成代码时会照着这个来,review的时候你也照着这个查,双方标准统一。比用一段文字描述“接收用户ID和分页参数”精确太多了。
3.3 约束要写成“可判定”的句子
约束最忌讳写成口号。“代码要优雅”“性能要好”“注意安全”这种话对AI来说等于没说。有效的约束必须是可判定的,也就是能明确回答“做到了没有”。
举个例子,与其写“错误处理要统一”,不如写“所有对外接口的错误返回必须使用统一的ErrorResponse结构,包含code、message、traceId三个字段,code为四位数字字符串”。与其写“注意并发安全”,不如写“对共享map的读写必须加锁,或使用sync.Map”。前者AI只能猜,后者AI能直接执行。
我一般会把约束分成两类:技术约束(用什么语言、什么库、什么模式)和禁止约束(不许用什么、不许出现什么)。禁止约束特别有用,因为AI有时候会自作主张引入一些你不想要的依赖或写法,明确禁掉能省很多事。
3.4 验收标准要能对应到测试
验收标准写得好不好,一个简单的判断方法:能不能直接翻译成测试用例。如果一条验收标准你没法写出对应的断言,那它就不够具体。
比如“分页接口在cursor为空时返回第一页数据”这条,可以直接写成测试:传入空cursor,断言返回的数据条数等于pageSize,且第一条数据的ID等于全量数据的第一条。“错误码为四位数字”这条,可以写成测试:遍历所有错误返回,断言code字段匹配正则^\d{4}$。规格里的验收标准越接近测试,AI生成的代码质量越有保障,因为它在生成时就知道自己要满足什么。
4. 把规格接进日常编码流程的实操路径
4.1 从存量项目切入还是新项目起步
推行SDD,我的建议是从新模块或新项目起步,不要一上来就改造存量代码。存量项目的历史包袱重,规格很难一次性补全,强行推容易让人产生“这玩意儿没用”的抵触。新模块没有历史约束,你可以完整地走一遍“写规格-生成代码-review-迭代”的流程,跑通之后再考虑往存量上迁移。
具体做法是:挑一个边界清晰、依赖不多的新功能,比如一个独立的查询接口或一个工具函数库。先花半小时把规格写出来,然后让AI基于规格生成代码,你对照规格做review。跑完一轮你会发现,review的效率比之前高很多,因为你有明确的对照物,不用靠感觉判断代码对不对。
4.2 规格的版本管理和迭代节奏
规格不是写完就锁死的,它会随着需求变化而迭代。关键是要把规格当成代码一样管理:进git、走review、有版本记录。每次需求变更,先改规格,再让AI基于新规格改代码,而不是直接口头告诉AI“把那个字段改一下”。
迭代节奏上,我习惯小步快跑。规格不要一次写太大,一个模块的规格控制在能一次review完的体量。写完一版就让AI生成,生成完发现问题就回头改规格,改完再生成。这个循环跑几轮,规格会越来越精确,AI的输出也会越来越稳。最怕的是一次性写一份巨长的规格,AI生成一大堆代码,你review到崩溃,最后也不知道问题出在规格还是代码。
4.3 让AI参与规格的补全而不是替代
一个很实用的技巧:规格的初稿可以让人写,但补全和查漏可以交给AI。你把初稿丢给AI,让它指出哪些地方模糊、哪些约束不可判定、哪些接口字段缺说明。AI在这件事上比人细心,它能发现你习以为常但实际没写清楚的地方。
但要注意,AI补全的内容必须经过你确认才能进规格。它有时候会自作主张加一些你没要求的约束,或者把某个字段的类型改掉。规格的最终解释权必须在你手里,AI只是帮你查漏的工具,不是规格的作者。
5. 那些没人告诉你但一定会踩的坑
5.1 规格写太细反而限制AI发挥
这是个反直觉的坑。规格的目的是对齐,不是把AI变成只会照抄的机器。如果你把每一行代码该怎么写都规定死,AI就失去了它最擅长的部分——在明确约束下给出合理的实现方案。我见过有人把规格写成伪代码,结果AI生成的东西跟手写没区别,完全没发挥出AI的效率优势。
正确的度是:约束写死,实现放开。你规定接口长什么样、错误怎么处理、性能底线在哪,但具体用什么算法、怎么组织内部函数,让AI自己决定。这样既保证了输出符合预期,又保留了AI的灵活性。
5.2 Markdown格式细节导致的解析偏差
Markdown看起来简单,但格式细节对AI解析的影响比想象中大。几个高频问题:换行——有些Markdown渲染器把单换行当软换行,AI解析时可能把两行合并;表格对齐——表格的竖线没对齐不影响渲染,但AI读的时候可能错位;代码块语言标注——不标语言,AI可能猜错上下文。
我的做法是:规格文件里所有列表项之间空一行,表格严格对齐,代码块一律标注语言。这些细节看起来琐碎,但能显著降低AI理解偏差的概率。另外,如果你的规格要转成其他格式(比如转Word给非技术同学看),提前确认转换工具对Markdown语法的支持程度,表格和嵌套列表是最容易出问题的。
5.3 团队协作时规格的“最后一公里”
SDD在个人用的时候很顺,推到团队里就会遇到“最后一公里”问题:有人不写规格直接让AI生成,有人写了规格但跟代码对不上,有人觉得写规格太慢不如直接聊。这些问题的根源不是工具,是习惯。
我的经验是,先用工具强制,再靠习惯固化。比如在CI里加一个检查:改了代码但没改对应规格的PR,打上待确认标签。或者在code review模板里加一项“规格是否同步更新”。强制一段时间后,大家会慢慢发现写规格其实省了后面扯皮的时间,习惯就养成了。最忌讳的是一上来就靠自觉,大概率推不动。
6. 规格驱动之后,人和AI的分工边界在哪
跑通SDD之后,你会发现人和AI的分工其实变得更清晰了。人负责定义问题——写规格、定约束、划边界、做验收;AI负责解决问题——在规格框架内生成实现、补全细节、处理边界。人不再需要逐行写代码,但需要把“要什么”想得比以前更清楚。
这个分工的转变对开发者其实是个挑战。以前你可以边写边想,现在你得先想清楚再让AI写。很多人不适应,觉得写规格比写代码还累。但换个角度想,写规格的过程本身就是把需求想清楚的过程,这部分工作以前也存在,只是被藏在了反复改代码的循环里。把它提前、显式化,长期看是省时间的。
OpenSpec 这类工具的价值,不在于它用了多高深的技术,而在于它把“规格先行”这个朴素的工程原则,用Markdown这种低门槛的载体,接进了AI编码的工作流。它没有发明新概念,只是把老道理用对了地方。至于要不要用、怎么用,还是得结合自己团队的实际节奏来。我的建议是先用一个小模块试水,跑通一轮再决定要不要铺开。试错的成本很低,但一旦跑顺,AI编码的稳定性会有肉眼可见的提升。