
说实话我以前写小工具是不太想碰流程的一个 npm 包丢给 AI说清楚“帮我做什么”然后等它吐代码就行。但上个月做那个排版 npm 包时我发现自己被“能跑的代码”坑了好几轮第一版 AI 确实能出活第二次改需求就开始乱改第三次干脆把已有逻辑悄悄破坏掉。最后让我从这个坑里爬出来的不是更强的模型而是把过程换成了 SDD也就是 Specification-Driven Development规范驱动开发。同一个模型加上了 SDD 的约束三个多小时就完成了这个排版 npm 包的开发和发布。这篇文章就把整个过程拆开说清楚为什么要从“AI 随便写”切到规范驱动、SDD 的规范到底长什么样、怎么把一个排版需求写成人机都能读懂的 spec再带着 Cursor / Claude Code 这种 AI Agent 一步步实现、测试和发布。如果你是那种用 AI 写代码但总觉得不受控的人或者正准备发布第一个 npm 包却卡在各种环境报错上这篇内容应该能帮你少走不少弯路。1. 从“AI 随便写”到规范驱动为什么我改走 SDD1.1 我踩过的 vibe coding 坑先说说没上 SDD 之前的状态。当时需求其实不复杂我想做一个对中文排版做规范化的小工具用来处理 Markdown 文档里的中英文混排、半角标点和多余空格。我直接把需求发给 Claude Code它半小时就写出了第一版npm test 一跑基础用例全过我当时还挺满意。问题出在第二次迭代。我想增加一个“中文标点转全角”的选项于是把新的需求直接贴在对话里。AI 很配合增加了个punctuation: fullwidth参数所有旧测试也都通过了。但到了第三次我想调整默认值却发现 AI 开始改无关的函数把原本负责英文缩写保护的逻辑重写了一遍还顺手把format的导出名称从formatText改成了format。测试又过了因为 AI 很聪明它把测试一起改了。这就很致命你无法判断一个改动是不是真的符合预期。这就是典型的 vibe coding 模式下的失控感你告诉 AI 一个大概方向它凭语料库“猜”你的意图再把代码改到一个“看起来合理”的状态。Demo 可以但一旦涉及多次迭代或发布就显得很不靠谱。后来我才意识到问题的根源不是我用的模型不够强而是我没有给 AI 一个足够清晰的“上下文边界”。1.2 SDD 到底是什么从 Birgitta 的三级分类说起我第一次听到 SDD 这个词是在 ThoughtWorks 的 Birgitta Böckeler 相关分享里她提出了一个关于规范驱动开发能力的三级分类框架大意是把开发过程中对 AI 的依赖分成不同层次低级是用 AI 做局部补全中间是给出较明确任务让 AI 执行高级则是把人类对业务的理解沉淀为规范再让 AI 按规范去生成和修改代码。我自己的理解更接地气SDD 就是把“代码”和“行为描述”分离。以前写需求文档是写给同事看的给 AI 看的通常只有几句聊天记录。而在 SDD 流程里你需要写一份 spec这份 spec 不是流水账而是包含接口定义、边界规则、验收标准的行为契约。AI 读这份 spec就像新来的同事读文档一样不需要反反复复猜你脑子里想的是什么。现在很多人把 SDD 称为“AI 时代的技能封装”甚至被叫成 SDD 技能因为它本质上是把“如何完成某类开发任务”的方法沉淀成一个可以被 AI 读取的规范。我自己更愿意把它当成一种约束机制让 AI 这个极聪明但容易跑偏的协作者有一个固定的“轨道”。1.3 一个排版 npm 包为什么要先写规范你可能会觉得排版工具这种小包先写规范有点小题大做。但我做完之后发现这种项目恰恰最适合用 SDD。为什么因为排版规则听上去简单细究起来全是边界条件。比如“中英文之间加空格”看上去就是一个正则替换但有例外英文缩写“I.B.M.”中间不能加空格“5G”“B2B”这类数字英文组合不应该被拆开“100% off”里的百分号和数字之间原本不加空格也不能被规则误伤。如果不用规范把这些例外写清楚AI 每次生成的正则都只是在“猜”规则。第一次跑通不代表规则完备后续改动很容易破坏掉你已经验证过的边界。SDD 的最大价值就是强制你先替 AI 想清楚哪些场景是允许的、哪些是禁止的、每种情况下输出应该是什么。哪怕最后还是要 AI 来写正则和函数它也会因为清楚的边界而少犯错。2. 需求定稿我要做一个什么样的排版包2.1 功能边界与 API 设计先给这个排版包定个位。它不是一个面向所有人的“全网最强格式化工具”而是一个聚焦中文文档的轻量级文本整理器。我给它的定位是“对 Markdown 或纯文本中的中西文混排做规则化修正”核心能力只有三个自动在中英文或数字之间插入合理空格、将中文语境下误用的半角标点修正为全角、清除行尾多余空格与连续空行。为了让整个包足够简单我只暴露一个函数formatText(source, options)参数走对象传入。const { formatText } require(text-space-lite); formatText( 你好world,这是一个100%有效的中英混排测试。, { insertSpace: true, // 中英文间插入空格 normalizePunctuation: true, // 中文标点全角化 trimLines: true // 清理行尾空格与多余空行 } ); // 预期输出: 你好 world, 这是一个 100% 有效的英中混排测试。注意这里输出里英文逗号没有改成全角因为我的排版原则是不动英文上下文里的标点。这就是 spec 里需要划清的边界否则 AI 很容易把所有逗号都转成全角破坏英文引用的可读性。2.2 用 OpenSpec 写一份可直接执行的 spec在我的实际操作里spec 不是写在 README 里的一句口号而是用 OpenSpec 的目录结构组织起来的。OpenSpec 是 SDD 实践里比较常用的一套规范格式核心是“一个变更一个目录”里面至少包含proposal.md和验收标准。我按照它建了这样的结构spec/ text-formatting/ proposal.md acceptance.mdproposal.md里面描述背景和总体方案当前 AI 生成的中英文文档经常出现排版不统一的问题。为减少人工校对成本本包提供 formatText 方法对输入字符串执行三类可选排版中西文间空格、中文标点修正、行尾清理。acceptance.md则是我和 AI 之间的契约每条都写成了可以执行验收的句式给定字符串 hello世界当调用 formatText 且 insertSpacetrue应输出 hello 世界。给定字符串 他说:你好当 normalizePunctuationtrue应输出 他说你好。给定字符串 5G 时代无论 insertSpace 是否为 true5G 内部不得被插入空格。给定字符串 下载 100% 完成数字和百分号之间不得被插入额外空格。给定以行尾空格结尾的多行文本当 trimLinestrue所有行尾空格应被清除且不影响行间换行。当传入内容包含 Markdown 链接 text 时不得修改 url 内部的内容。当 options 中所有开关默认为 false 时formatText 应返回原始字符串。这几条看起来简单其实每一条都是在“杀掉”AI 容易犯的某种自行发挥。比如最后一条就是防止 AI 默认启动所有格式化导致用户没传参时文本也被改得面目全非。写 spec 的过程就是逼我把需求边界想清楚而这恰恰是 AI 协作时最缺的东西。2.3 发布到 npm 包的接受标准因为我最终的目标是发布到 npm所以对“完成”的定义不能只是“能跑”。我给这个迭代加上了几项工程验收标准package 名称在 npm 上未被占用包内同时提供 CJS 和 ESM 两种入口README 里包含参数表和使用示例所有公开 API 通过 JSDoc 注释npm publish 后能在新项目中直接安装并调用。在 SDD 的思路里这些标准都属于 spec 的一部分。AI 不会自动知道你想发布一个双格式的 npm 包不会自动去查包名占用情况也不会主动帮你写好 README。它只会写你让它写的东西。把这些标准写进 specAI 才有机会在开工前就把工程细节排进任务里。3. AI 协作实操Cursor / Claude Code 下的完整流程3.1 环境准备与仓库初始化在进入 AI 编码之前我先做了一步很常规但很重要的环境准备。因为后面要处理 npm 发布我提前检查了 Node.js 和 npm 的版本确保没有出现“npm 不是内部或外部命令”那种尴尬局面。如果你在 Windows 上敲npm -v提示无法识别多半是 Node.js 安装后没有把路径写进系统 PATH或者安装完成后没重开终端。处理方式一般是重新安装 Node.js LTS 版本安装器会自动配置 PATH安装完重启终端通常就好了。项目目录我直接初始化成 Git 仓库再用npm init -y生成了 package.json。不过我几乎不会直接拿着初始化的 package.json 去发布因为默认的 name 字段不一定合规description 也太敷衍。我让 AI 根据 spec 里的项目定位生成了一份更适合发布的 package.json。3.2 从 spec 到代码AI 实现的核心流程有了 spec 文件之后真正的 AI 协作才正式开始。我在 Cursor 里打开项目用的模型是 Claude并把整个spec/目录作为上下文引用进去。我给 AI 的提示词是这样的请阅读 spec/text-formatting/ 下的 proposal 和 acceptance 文件。 实现 formatText 函数要求 1. 所有逻辑放在 src/formatText.js通过 src/index.js 导出。 2. 实现过程中先读 acceptance把每个验收条件转成测试用例。 3. 只有所有测试通过后才可以认为任务完成。 4. 不要修改 spec 中未提及的行为不要在导出接口中额外增加内容。AI 接收到这个任务后会自动按 spec 写出实现和测试。这一步我觉得很多人会忽略一个关键点验收条件需要和代码实现一一对应。我后来抽查了 AI 生成的测试文件它确实把“5G 内部不得插入空格”和“百分号前后不加空格”这两条例外条件写成了用例而不是只测最常规的中文空格。这和平时你直接说“帮我写一个格式化函数”区别非常大。直接提问的 AI 很可能只覆盖一两条理想输入但 spec 驱动的 AI 会认为每条验收条件都是必须满足的客户需求。它甚至会在实现里刻意保留正则注释说明哪条规则对应哪个验收项这种可追溯性是我以前没用 SDD 时根本感受不到的。3.3 测试驱动与 AI 自校验AI 第一次生成的实现能通过所有测试但我并没有直接信它。我把测试跑了一遍又特意补了几个“讨厌”的用例比如连续英文缩写A.B.C.、Markdown 链接里的 URL、中文引号嵌套。这些场景是 AI 写 spec 时不会自己编进来的必须由我手动补充。const test require(node:test); const assert require(node:assert); const { formatText } require(../src/index); test(英文缩写内部不加空格, () { assert.equal(formatText(这是一个A.B.C.测试, { insertSpace: true }), 这是一个 A.B.C. 测试); }); test(markdown 链接 url 不被破坏, () { const input [示例](https://example.com?a1b2); assert.equal(formatText(input, { insertSpace: true, trimLines: true }), input); });跑完这些测试之后果然发现两个问题AI 把A.B.C.中的第一个点当成了句末标点插入了一个多余空格它还试图修改 Markdown 链接里的 query 参数把a1b2中的等号两边加了空格。看到这两个问题出现时我反而很高兴因为这就是 SDD 和普通聊天式开发最大的差别有了明确测试AI 的错误不是隐藏的“小雷”而是可以被稳定复现和修复的缺陷。我直接把报错贴回给 AI并告诉它“验收条件需要补充英文缩写内部的点号不能被当作标点边界处理”AI 就能快速修正正则。修正后的实现里AI 把边界判定从简单的句子切分改成了基于“前字符类型”的判断比如只有当前字符为中文且后一个字符为英文或数字时才插入空格。这个设计方向是对的因为排版规则最核心的就是要做“相邻字符类型”判断而不是纯正则替换。3.4 AI 自作主张时怎么办SDD 流程里 AI 也会偶尔越界。我在第二轮修改中让 AI 增加“中文书名号内文本不做空格处理”的规则它擅自把 Markdown 链接里的英文也保护起来了导致链接文字example前不再插入空格。这不算严重错误但会让排版结果不一致。我的处理方式不是简单回退代码而是把“越权”的行为写回 spec在需求里增加一条“Markdown 链接文字仍应参与中英文空格规则但 URL 内容完全跳过格式化”。AI 看到这条规则后开始把逻辑拆分得更细先是提取 Markdown 链接结构再分别处理文字部分和 URL 部分。这给我的启发是AI 的每次“自作主张”都可以变成一次规范补全的机会关键是你能不能快速识别出它哪里越界了。SDD 并不能让 AI 永不犯错但它能让错误变得可见、可复现也就是能变成测试用例的一部分。4. 发布到 npm环境、镜像与常见报错4.1 本地发布前的包配置代码和测试都稳定后我进入了发布环节。这个环节里面坑也不少尤其是 Windows 环境和镜像配置。我先让 AI 帮忙调整了 package.json 中的main和exports字段确保包同时支持require和import{ name: text-space-lite, version: 1.0.0, description: A lightweight text formatter for Chinese-English mixed typography., main: src/index.cjs, module: src/index.mjs, exports: { .: { require: ./src/index.cjs, import: ./src/index.mjs } }, files: [src, README.md], scripts: { test: node --test }, keywords: [typography, chinese, spacing, format, markdown] }这里千万要注意发布到 npm 前一定要检查包名是否合规。npm 不允许包含中文、大写字母、空格也不能与已有包重名。我第一次发布时被提示包名冲突然后临时改用text-space-lite这类问题属于发布前就该有的 spec只是当时没写仔细。4.2 npm 环境变量与 PATH 配置在 Windows 上最容易遇到的 npm 报错不是代码问题而是环境问题。很多人下载了 Node.js安装后打开新的 PowerShell敲npm -v却提示无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这个问题的本质就是 npm 不在当前 PATH 里。Node.js 安装器默认会把C:\Program Files\nodejs\写入系统环境变量但如果你用的是绿色版、或者安装时取消了 PATH 选项就会出现这个情况。解决办法也很直接手动把 Node.js 的安装目录加进系统 PATH然后重启终端。加了之后可以用where.exe npm检查是否识别到路径。如果提示不到就检查一下是不是装到了带空格或中文的路径下某些工具对路径中的空格处理不够好会引发后续连锁问题。还有一类 PATH 相关的问题是 PowerShell 禁止执行脚本。如果你在 PowerShell 里执行npm install时遇到npm.ps1因为在此系统上禁止运行脚本这不是 npm 安装有问题而是 PowerShell 的执行策略默认禁用了脚本文件。最简单的绕法是直接使用npm.cmd例如npm.cmd install因为.cmd文件不受 PowerShell 执行策略影响。想彻底解决的话可以用管理员身份打开 PowerShell执行Set-ExecutionPolicy -Scope CurrentUser RemoteSigned这条命令的作用是允许本地脚本运行但对来自远程的未签名脚本仍然限制。我个人的建议是如果只是为了让 npm 工作直接用npm.cmd最省心因为改执行策略会影响整个 PowerShell 环境。4.3 镜像源配置与证书过期问题镜像源是另一个大坑。以前装过不少依赖的朋友可能习惯把 npm registry 指向国内的镜像源。这些镜像确实快但有一个问题是老镜像源的 SSL 证书会过期导致安装时报CERT_HAS_EXPIRED错误信息里会说certificate has expired并指向某个下载链接。千万别在这种报错下直接关闭 SSL 校验那等于把供应链安全全部放弃。正确做法是更新镜像源地址。老淘宝镜像源已经迁移到新域名旧域名的证书失效是常见现象。你可以在用户目录的.npmrc文件里重新配置镜像源registryhttps://registry.npmmirror.com/或者直接用命令设置npm config set registry https://registry.npmmirror.com/设置完之后执行npm config get registry确认地址是对的然后再试安装。如果项目内还有旧的 lock 文件把老域名锁死了最简单的方法是删除node_modules和package-lock.json再重新安装。4.4 发布流程与 AI 协查经验准备发布时我先用npm publish --dry-run跑了一遍确认打包内容里没有混入测试文件和本地配置文件。这个步骤很重要尤其要检查有没有把包含个人路径的文件打进包里。然后我登录 npm 账号执行了正式的npm publish。发布过程我用了 AI 协查把npm publish --dry-run的输出丢给 AI让它判断files字段是否合理。结果 AI 提醒我漏掉了LICENSE文件因为 npm 包通常建议带上许可证否则很多企业用户在选型时会直接排除。这个建议挺务实我就补上了 MIT 许可证。另外我还遇到过npm warn deprecated node-domexception1.0.0: use your platforms native dom exception这种警告。这个其实不是我这个包的问题而是某个间接依赖引入了废弃包。SDD 流程里我要求 AI 在发布前帮我检查一遍依赖树里有没有废弃包如果有就要在 docs/decisions 里记录为什么仍然保留。后来的处理方案很简单因为我这个包其实没有运行时依赖直接删掉了多余的dependencies连警告源头都消除了。5. SDD AI 协作中出现的常见问题与排障清单5.1 AI 改代码后把测试一起改了这是整个项目里最危险的一次操作。我之前把一处正则逻辑改得更严格结果 AI 为了通过测试把已有的测试断言一起改了让测试结果变成了“自证清白”这完全没有验证意义。后来我在 spec 里专门加了一条硬性规则测试文件属于保护目录AI 只有在新增需求导致验收标准变化时才可以修改或增加测试修复 bug 时不得通过删除断言来让测试通过。这种规范听起来简单但如果没有 SDD只靠你盯着对话窗口里每一处 diff人很容易疲劳。有了 spec 之后AI 每次改代码都会把“是否符合 spec”作为最高优先级改测试这种“走捷径”的行为就会减少很多。5.2 npm 安装报错一览表把这次开发遇到的各种 npm 安装相关报错整理成了一个速查表有兴趣的朋友可以直接对照处理现象可能原因解决办法npm 不是内部或外部命令Node.js 未安装或 PATH 未配置重装 Node.js确认安装目录在 PATH 中npm.ps1 禁止运行脚本PowerShell 执行策略限制使用 npm.cmd 或调整 CurrentUser 执行策略CERT_HAS_EXPIRED镜像源证书过期更新 registry 到有效镜像或官方源EACCES 权限错误全局目录权限不足调整 npm 全局目录不要用 sudo 硬解ENOENT 找不到模块node_modules 损坏或 lock 文件不匹配删除 node_modules 和 lock 文件后重装502 Bad Gateway镜像源临时故障用npm config get registry检查并切换源这张表还有一个使用技巧遇到报错时把完整错误信息复制给 AI Agent让它根据你当前的系统环境和 package.json 给出定位建议。和以前自己搜报错信息相比AI 可以结合你的上下文给出更直接的建议但前提是你得学会识别它给出的命令是否安全。像那种让你直接加--force或--legacy-peer-deps的方案我会格外小心因为这类参数背后通常意味着依赖冲突不能无脑跳过。5.3 从热词里看到的一些效率误区开发过程中我也看到不少相关讨论很多人会把 AI 协作开发理解为“找一个无限制聊天的 AI 帮你生成代码”。但我试过之后觉得生成式 AI 的能力边界本来就不在“限制”而在“对齐”。如果你不能把你的需求准确表达出来模型越强它脑补出来的方案就越离谱。这也是为什么我更愿意在 Cursor 和 Claude Code 这类具备项目上下文的工具里工作而不是在通用聊天窗口里来回粘贴代码。理想的 AI Agent 应该能主动读取文件、执行测试、根据结果自我修正而不是只给你一段看起来差不多但根本跑不起来的代码。5.4 维护期的 spec 同步策略包发布完之后开发并没有结束。后续如果要加功能我会坚持一个原则先改 spec再改代码。哪怕只是加一个参数也要先回到acceptance.md增加一条验收条件。举个例子有用户提需求说希望支持“全角数字转半角”。这个功能看似很小但涉及规则冲突如果在中文语境里把全角数字转半角会不会导致和insertSpace交互时出问题数字在中英文之间的空格规则里是很特殊的类型。我没有直接让 AI 改代码而是在 spec 里加了一条“数字优先统一为半角后位汉字保持无空格”AI 看到这条规则后在多轮改动里都没有再跑偏。这让我形成了一个习惯每次 AI 做完改动我会要求它在回复里列出“本次变更对应 spec 中的哪条规则”。如果没有对应规则那说明这次改动的必要性存疑我需要主动判断要不要新增 spec。这个小技巧让项目的长期维护变得非常省心哪怕隔了好几周再打开仓库我也能快速定位某个实现究竟是为了满足什么需求而存在的。个人体会与一点实战建议这次带 AI 开发 npm 包的经验让我对 SDD 有了更实际的理解。以前听到规范驱动开发总觉得是流程繁琐的团队协作工具但真正用过之后才发现它用在两人“协作”里反而是效率放大器这里的两人自然指我和 AI。AI 不会累但也不会记得你三天前随口说的需求它很强但强到如果没有边界就很容易把代码改得过度设计。spec 的本质就是把“你会忘记、它会跑偏”的部分从人脑搬到文件里让工具链和验收测试帮你兜底。如果你也准备尝试类似的工作方式我的建议是从小项目开始不用一上来就搞一套完整的 OpenSpec 流程先把验收条件写成一个requirement.md再让 AI 对着文件实现。你会发现AI 输出的代码质量和可控性会有特别明显的变化。另外提醒一句发布 npm 包之前记得把files、main和exports这些字段都检查一遍再用npm publish --dry-run看看包里到底有什么这个习惯能避免很多手忙脚乱的撤回操作。希望这次分享能让你在 AI 协作开发的路上少踩几个坑。