SDD驱动AI编程:用规格文档把npm排版包从想法变成现实

SDD驱动AI编程:用规格文档把npm排版包从想法变成现实 我得先交代一下背景。这个月我做了一件以前会拖三周、这次只用两天就搞定的事把一个叫 cn-type-space 的排版 npm 包从想法变成真实发布的库。代码本身并不复杂核心功能就是处理中英文混排时的空格、标点这类格式问题但真正值钱的不是那几百行正则而是我怎么用 SDDSpec-Driven Development规格驱动开发的方式让 AI Agent 按规格文档把这个包写出来。这篇文章不是教你某个魔改库有多牛而是聊聊我在做这个小包时怎么把 SDD 方法论落到一个真实的 npm 项目里。如果你也对 AI 编程感兴趣但总觉得 AI 生成的代码不靠谱、不敢直接上线那这篇应该能给你一个比较具体的参考怎么把“不靠谱”变成“可控”。1. SDD 与排版包为什么这两者能凑到一起先说结论排版工具几乎是 SDD 最理想的试验田。它的需求边界清晰、测试用例容易写、规则本身可以穷举而且错误的影响面可控。你要是拿一个用户界面复杂、交互逻辑模糊的产品去练 SDD大概率会在“需求说不清”这一步就卡死最后又退回手写代码。1.1 SDD 不是文档驱动开发换了个马甲我见过不少人误解 SDD以为就是把需求写详细一点再丢给 AI。真正的 SDD 核心不是“写文档”而是“写规格Spec”。文档是人看的目标是描述背景和意图规格是人和 AI 共同遵守的契约目标是定义输入、输出、边界和验收条件。之前 Thoughtworks 工程师 Birgitta Böckeler 在分享里把 SDD 场景分成三级这套框架我现在用得越来越顺手第一级规则明确、条件可枚举的任务。比如“中英文之间加空格”这种 AI 基本不会出错只要测试覆盖足够可以直接放权。第二级结构化流程任务。比如“解析一段文本 - 按规则链逐条处理 - 输出结果”AI 需要按固定步骤走但每一步的输入输出都要在规格里写死。第三级需要品味判断的开放式任务。比如“这段文案的语气是否合适”这类任务 AI 只能给建议最终拍板的必须是人。我做 cn-type-space 时就是这么分的空格和标点规则归第一级规则链的编排和执行归第二级包名、API 风格、默认配置这些偏设计的归第三级。1.2 为什么用“排版”来做第一个 SDD 项目我之前用 AI 做过几个内部小工具最大的痛苦就是代码改到后面AI 和我都不记得初版设计时的约束了。AI 会把一堆不相干的需求揉进去我 review 代码的时候还得猜它这个分支是为哪个需求加的。排版包天然避开了这个问题。它不依赖后端服务、没有数据库、没有鉴权唯一要做的就是“输入字符串 - 输出字符串”。一旦把一条规则定义成可测试的契约AI 的实现有没有跑偏跑一条用例就知道。而且排版规则非常像缩略版的“领域语言”。比如“中英文之间加半角空格”“中文标点前不加空格”“数字和百分号之间不额外加空格”这些规则都能用一句话描述清楚AI 理解起来几乎没有歧义。你再想想“优化用户购物体验”这种需求AI 根本不知道你要它干什么。所以我给的建议是如果你第一次尝试 SDD 驱动 AI 协作别选大项目选一个边界清晰、可测试、能独立发布的小工具。做完一个之后再往大项目上推。2. 动手前先写规格从“想要一个排版工具”到“可执行的契约”很多人用 AI 写代码第一步就是打开对话框说“帮我写个中英文排版库”。这跟在施工现场跟工人说“帮我盖个好看的房子”差不多——工人确实会开始干活但结果能不能住人、用的是什么材料、一共几层楼你完全不知道。SDD 的第一步就是把“想要一个排版工具”这种模糊念头翻译成一条一条可验证的规格。2.1 规格清单拆解输入、输出、边界我在写 cn-type-space 的规格时没有直接写“处理中英文空格”而是把所有规则拆成了这样编号规则名输入示例期望输出说明R1中英文间距你好world你好 world中文与英文之间补半角空格R2英中间距hello世界hello 世界英文与中文之间补半角空格R3中文标点前距你好 世界你好世界中文标点前不许有空格R4数字与百分号50 %50%数字与百分号之间不加空格R5连续空格压缩a ba b多空格合并成单空格受配置控制R6全角标点补充hello,worldhello, world英文逗号后补空格当启用英文规则时每一条规则都像一个单元测试的入参和预期输出。AI 实现完之后我拿这些用例一跑哪个规则没实现、哪个实现错了立刻就能看出来。这比“给我处理一下”要强太多了。AI 不用猜我到底要不要百分号前加空格我也不用为 AI 自己发明的规则买单。2.2 三级分类框架落到排版规则上大部分排版规则属于第一级可以直接让 AI 按正则或字符串逻辑处理。但有一个环节要做一下取舍默认配置的设定值。比如 R1“中英文之间补空格”这看似是铁律但有些排版标准比如某些学术期刊不允许在英文缩写和中文之间加空格比如“AI技术”这种。这种其实不是“对错问题”而是“品味问题”。我在规格里把它定义成第三级决策决定权留给使用方通过options.insertSpace暴露出来。规则链的编排则是第二级。我的设计是配置项先经过normalizeOptions()合并默认值和用户传入值然后按固定顺序执行清洗、空格、标点三条规则链最后再做一次尾部清理。这个顺序在规格里写死了AI 不能自己调换。为什么顺序重要因为先插空格再删标点前空格和先删标点前空格再插空格结果看起来差不多但边界情况完全不同。规格里写清楚顺序review 代码时才不会为了一个小分支吵半天。2.3 一份可以直接抄的 Spec 模板我用的是 Markdown 写的 Spec 文件放在项目根目录的spec.md结构和模板大致如下背景与目标控制在三段以内说清楚这个包解决什么问题、不解决什么问题。功能规格用上面那种编号规则表每一条都是一个“输入 - 输出”的契约。配置项说明每个配置项写清楚类型、默认值、影响哪条规则。非目标明确写“本版本不支持按字典白名单跳过特定词汇”“不支持自动识别语言”。这个很重要能防止 AI 自己给自己加需求。验收清单列出发布前必须通过的终端命令比如npm test、npm run lint、npm publish --dry-run。写完整份 Spec 大概花了我四十分钟但它直接省掉了我后面几天和 AI 来回拉扯的时间。我把spec.md和测试用例一起丢给 AI然后写了一句“严格按 spec 实现不要增加规格之外的 API”。这个约束条件帮了大忙。3. 六步实践落地的全过程AI 写代码我写边界网上关于 SDD 的实践总结不少很多文章会提到“SDD 六步实践指南”我这次实际跑下来的流程也基本对应明确目标 - 编写规格 - AI 生成初稿 - 人工审查 - 自动化测试 - 迭代维护。前两步上一章已经说完了这一章重点讲后面四步里我实际踩过的关键节点。3.1 前三步目标、规格、AI 初稿目标在第一章就定了做一个处理中英文混排格式的 npm 包定位足够小规则足够清楚。规格也在上一章拆完了。真正有意思的是第三步——让 AI 生成代码。我没有直接把spec.md全文贴进去就说“写吧”。那样 AI 经常会把代码写得和规格“形似而神不似”。我的做法是给出一个精简版的开发任务说明把规格文件路径指给它同时把验收用例直接复制进去你是 Node.js 库的作者。请阅读项目根目录下的 spec.md实现其中全部规则。 约束 1. 不要新增 spec 之外的 API 2. 所有规则必须导出为纯函数方便单元测试 3. 保持零运行时依赖 4. 如果有不确定的地方先列出来不要自己发明约定。这里有个细节我明确要求 AI “不要新增 API”“保持零运行时依赖”。为什么因为 AI 有很强的“过度设计”倾向它动辄就给你引一个 lodash、给你做一个可插拔插件系统。在这个项目里这些都不需要而且“零依赖”本身就是 npm 包的一个卖点。把这个写进任务说明等于提前把边界钉死。AI 生成初稿的过程比我预期的快第一次就给出了一个带五个函数的模块大概四百行。虽然代码风格有些地方比较啰嗦但它确实把 R1 到 R6 的核心逻辑都覆盖到了。3.2 后三步审查、测试、迭代审查这一步不能省。网上那些“让 AI 写完就能跑”的帖子十个里有八个是 vibe coding——让 AI 在一个巨大的上下文里噼里啪啦生成几百行然后人复制粘贴到项目里能编译通过就算赢。但真正要发布给别人用的包光“能跑”远远不够。我 review 的时候重点关注三个点有没有超出规格的行为。比如 AI 额外加了一个autoDetectLanguage()方法这不是我要的删掉。正则有没有灾难性回溯风险。排版包处理的是用户输入恶意构造的长文本一旦触发 ReDoS就是一个安全漏洞。错误处理是否符合预期。比如对null、undefined输入我期望是原样返回还是抛异常规格里没说清楚的话AI 可能自己选了我要补上。审查完之后立刻写测试。我把规格里的 R1 到 R6 表格直接转成了 vitest 测试用例又补了一些边界输入空字符串、纯中文、纯英文、只有标点、超长字符串。第一次跑测试就发现了一个问题R5 连续空格压缩会影响代码块的缩进如果用户传的是 Markdown 文档 这种代码缩进会被吃掉。这是我最初写规格时没有覆盖的场景。解决方式不是删掉 R5而是给压缩空格功能增加一个preserveIndent配置项默认关闭。这个决策就回到了第三章提到的第二级流程和第一级规则之间的关系规则实现没变但流程编排多了一个条件分支。测试通过后再进到迭代把新增配置项补回 spec重新跑一轮 AI 生成然后人工确认改动只涉及目标函数。整个迭代控制在两轮以内因为第一轮已经把主要骨架定死了第二轮只打补丁。3.3 核心实现一条中英文间距规则的诞生过程我拿 R1“中英文间距”来展示最终代码大概长什么样。这里要说明一下这段代码是 AI 生成的初稿加上我 review 后的修订版最后才定成这样。// src/rules/insertSpace.js // CJK 统一表意文字范围这里取常用区间做快速判断 const CJK_RE /[\u4e00-\u9fff\u3400-\u4dbf]/; const LATIN_OR_DIGIT_RE /[A-Za-z0-9]/; /** * 在中文字符与英文/数字之间插入半角空格。 * 对已经存在的连续空格不做处理避免破坏用户已有排版。 */ export function insertSpaceBetweenCjkAndLatin(input) { if (typeof input ! string) return input; return input .split() .map((char, index, arr) { const prev arr[index - 1]; const next arr[index 1]; if (CJK_RE.test(char)) { if (prev LATIN_OR_DIGIT_RE.test(prev)) return char; if (next LATIN_OR_DIGIT_RE.test(next)) return char ; } return char; }) .join(); }我让 AI 用逐字符扫描而不是正则全局替换是因为全局正则面对多次替换时容易产生二次空格问题比如“你好world”会被替换成“你好 world”但如果继续做英文和数字之间的处理可能会误伤已有的合法空格。逐字符扫描的缺点是性能略慢但排版文本通常不会超过几 KB实测在十万字符文本上也就十几毫秒完全可接受。另一个我手动改的地方是LATIN_OR_DIGIT_RE没有包含下划线。因为“hello_world”这种字符串在代码语境里是一个 token不应该被拆开。AI 初稿用的是/[A-Za-z0-9_]/我 review 的时候把下划线去掉了。这种细节不靠人盯着测试用例也未必能发现。4. 发布到 npm 之前先把这几关过了项目代码写完、测试全绿不代表可以立刻npm publish。我在这次实践里几乎把所有同事遇到过的 npm 坑都踩了一遍从本机环境到发布流程每个环节都能卡住你半天。4.1 发布前的 self-check 清单先列一下我发布包之前会逐项检查的清单照着过一遍能省掉很多麻烦包名是否可用npm view package-name返回 404 说明没人用否则要换名或改 scope。registry 是否正确npm config get registry应该指向官方源或你想发布的镜像源。我见过有人配了淘宝镜像还直接 publish结果上传到镜像源了一脸懵。files字段是否配置package.json 里写files: [dist]避免把源码、测试文件一起打进包里。双格式导出是否正常在 package.json 里确认main和module字段都指向了正确的产物。README 是否完备npm 包页面就是 README第一屏要写清安装命令和一行示例。版本号是否走语义化第一次发布用1.0.0没问题但之后改规则要按 major/minor/patch 来不然使用者升级时会被破坏性变更打懵。关于main和module的写法我的配置是这样的{ name: cn-type-space, version: 1.0.0, description: Tiny text formatter for Chinese-English mixed typography., main: ./dist/index.cjs, module: ./dist/index.js, exports: { .: { import: ./dist/index.js, require: ./dist/index.cjs } }, files: [dist], scripts: { test: vitest run, build: rollup -c } }注意exports字段的优先级高于main和module。如果你的 Node 版本支持exports现代打包工具都会优先读它。但为了让老项目也能加载main和module还是建议保留。4.2 从安装到发布的高频报错速查表我在开发遇到的几个报错覆盖面还挺广的。有些是环境问题有些是镜像配置问题整理成一张速查表供你排查报错信息原因解决办法npm : 无法加载文件 ...npm.ps1因为在此系统上禁止运行脚本Windows PowerShell 默认执行策略限制用管理员身份执行Set-ExecutionPolicy -Scope CurrentUser RemoteSigned后重试或者直接在 cmd / Git Bash 里运行npm.cmdnpm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称Node.js 没有加入 PATH 环境变量重新安装 LTS 版 Node勾选 “Add to PATH”或在系统环境变量里手动添加 Node 安装目录npm ERR! code CERT_HAS_EXPIRED且请求的是registry.npm.taobao.org旧版淘宝镜像证书失效换成官方源npm config set registry https://registry.npmjs.org/或新版镜像https://registry.npmmirror.comnpm WARN deprecated node-domexception1.0.0传递依赖弃用警告通常不影响构建但发布前尽量升级到包含修复的上游依赖npm ERR! 403 Forbidden在 publish 时出现包名被占用或 registry 指向了镜像源换包名执行npm config get registry检查并确认 npm 账号已登录npm login这里面最坑的就是旧淘宝镜像证书过期问题。很多教程让新手配registry.npm.taobao.org结果 2024 年证书一过期整条链路上的安装全部报错。如果你看到certificate has expired的提示第一反应别去管证书直接换 registry 就行。在发布前我还跑了一次npm publish --dry-run它会把将要发布的文件列表打出来。我一眼看到列表里有test/和spec.md是我第一批忘记过滤的文件当场就在 package.json 的files里补上了。这一步我强烈建议你养成习惯。5. 从这次实践里沉淀下来的协作方法项目做完之后我最大的感受是SDD 的收益不在“让 AI 写代码”这一步而在“让无用代码无处可藏”这一步。规格写得清楚AI 生成的每一行代码都能追溯到某条规则review 的时候就只需要回答“这行代码满足哪条规则”这一个问题效率高很多。5.1 哪些项目适合套用这套流程用 SDD AI 协作最适合的项目我总结下来有三个特征边界清晰、验收可写、失败影响可控。CLI 工具、格式化库、数据转换脚本、简单的后端接口都属于这一类。你要是一个月才改一次的小项目这套流程可能有点重但如果是一个要长期维护、有明确 API 契约的库前期花时间写规格绝对值。反过来没有明确“正确输出”的项目比如做一个视觉风格不确定的管理后台界面就不太适合。你没法写清楚“按钮好看”的输出是什么。这时候要的是快速原型探索不是规格约束。我现在的判断标准是如果测试用例类型比较复杂、预期很难用几行断言表达那 SDD 的回报会明显下降。5.2 我踩过几次坑后的三个心得最后说点代码之外的东西。这次实践让我意识到SDD 里的“审查”不是让 AI 帮着找 bug而是人对规格和实现的一致性的最终兜底。AI 能帮你写代码但不能替你决定“代码怎么写才是对的”。第一个心得规格写“不要做什么”比写“要做什么”更重要。我一开始只写了 R1 到 R6 该干什么结果 AI 自己发明了一套 dict 白名单机制。它就是觉得你可能想跳过某些词把这个机制嵌进去了。我在 Spec 里补了非目标清单之后这种幻觉才明显减少。第二个心得测试用例本身就是最好的 prompt。你与其在 prompt 里描述需求不如把测试代码直接贴给 AI。测试就是规格的机器可读版本AI 看着测试写实现准确率比我口头描述高三四倍。这个技巧后来我一直沿用。第三个心得版本号的管理要提前说好。我在做第二次迭代的时候想着加个配置项而已直接改了 minor 版本号就发布了。后来发现有个使用方在 README 里明确依赖1.x的旧 API升级后直接崩了。这提醒我凡是会改变对外行为的改动哪怕再小也至少要 bump minor并且变更日志要单独写清楚。如果你也想试试 SDD 驱动 AI 做一个小工具我的建议很简单先从自己最近想用的一个格式化脚本开始给它写一份规格再让 AI 按规格生成、按测试验收。等你跑完一轮你自然就知道为什么说“真正难的不是写代码是把问题描述到 AI 无法误解”。这个包本身很小小到不值得吹嘘但这种协作方式接下来会改变我之后做项目的习惯。