资讯动态

SDD实战:从AI失控到发布中文排版npm包的完整流程

发布时间:2026/9/8 22:20:42 来源:尧图企业网站定制
上个月整理一个长期维护的开源文档项目发现中文排版问题已经积累到没法忍英文直引号和中文弯引号混在一起中英文之间没有空格标点后面还挂着半角空格目录层级也乱。这些规则明明完全可以用脚本自动化。本来只想顺手做个 npm 包结果我在和 AI 反复拉扯三个小时后决定换一种开发方式——SDDSpecification-Driven Development规格驱动开发。这篇博文就记录我怎么用 SDD 把一个排版 npm 包从 0 做到发布的全过程。如果你正在尝试 AI 协作开发或者准备发布自己第一个 npm 包这篇应该能帮你在“怎么跟 AI 配合才不翻车”这件事上少走不少弯路。1. 从 Vibe Coding 的失控到 SDD我给 AI 划了条跑道1.1 一次真实翻车对话式开发到第 3 轮就开始拆东墙补西墙最开始我用的方法就是现在圈子里很流行的 Vibe Coding——直接把需求扔给 AI说“帮我写一个函数把中文引号转成弯引号顺便处理下中英文之间的空格”。第一轮很顺利AI 生成了一段看起来不错的 TypeScript 代码跑测试也通过了。等到第二轮我想再加一个“统一 Markdown 标题层级”的功能就继续在同一段对话里提需求。AI 很勤快地改了代码结果运行测试发现之前处理引号的逻辑坏了因为 AI 为了省事把正则表达式整体重写了一遍原本那段已经验证过的normalizeQuotes逻辑被合并进了一个更“聪明”的函数里边界条件全部走样。第三轮我尝试让它修复结果 AI 连 public API 的签名都改了把参数从string改成了options对象。在一个还没发布的 npm 包内部这么干倒没什么但这暴露了一个致命问题——如果 API 一旦发布这种随意改动就是灾难。1.2 失控的本质缺的不是提示词而是“验收标准”这个翻车经历让我意识到Vibe Coding 适合的场景是探索原型比如“帮我画个界面布局”“给我一段算法伪代码”。但如果要做一个会被别人安装、长期维护的 npm 包哪怕是个很小的包事情就完全不一样了。npm 包的核心资产是接口稳定性一旦别人npm install了你的包并调用了 API你改个签名就等同于强迫所有使用者改代码。对话式开发之所以失控是因为它在每一轮都让 AI 承担了过多的“决策权”用什么数据结构、怎么命名、要不要保留某个参数、边界情况怎么处理通通都由模型自由发挥。人类的反馈回路是滞后的——往往是 AI 写完了、测试挂了、人再去看源码才能发现问题。所以我把坐标轴扭了一下不再让 AI 决定“做什么”而是先把“做什么、不做什么、做出来长什么样”全部写成规格再让 AI 在规格的边界内干活。这就是 SDD 的思路。2. SDD 三级框架速览AI 该被当工具、搭档还是主力2.1 三级分类的核心差异关注 AI 协作开发的读者最近应该经常看到 ThoughtWorks 工程师 Birgitta Böckeler 提出的 SDD 三级分类框架。它的核心不是某种具体编程技巧而是描述了“AI 在开发流程中处于什么位置、规格由谁来定义”的三种模式。我根据自己的理解把三级框架整理成了下面这张表层级人类负责什么AI 负责什么典型场景第一级AI 作为编辑器/工具详细规格、验收标准、架构约束按规格生成代码片段工具库开发、重构已有模块第二级AI 作为结对搭档任务拆解、优先级、关键技术决策按任务实现功能参与方案讨论功能开发、模块整合第三级AI 作为自主 Agent给定高层目标与最终验收标准自主规划任务、自主编写代码、自主调试内部工具、非关键路径的一次性需求这个框架最有价值的点在于它把“规格从哪来”和“责任在谁身上”讲清楚了。第一级里规格完全由人写AI 是一个高效编码器第二级里人给任务级描述AI 可以在小范围内做实现决策第三级里人的输入收敛到目标层面AI 的自主度最高。2.2 我给这个包选的层级第一级偏第二级实话说我之前也看过很多鼓吹“AI Agent 全自动开发”的帖子但真正动手做一个要发布的 npm 包时我还是选择了第一级为主、局部用第二级的策略。原因很简单npm 包的接口一旦发布就很难收回。包名、函数签名、导出路径、返回值格式这些统统要先定死。这个决策只有长期维护过开源包的人才能理解——你可以快速迭代但你不能让使用者跟着你做指向性不明确的“断崖式变更”。所以我宁愿自己在规格阶段多花一点时间也不希望 AI 在实现阶段“灵机一动”给我设计一个新接口。2.3 六步实践指南的总览结合三级框架和实际项目过程我沉淀了一套六步操作流程算是我的 SDD 六步实践指南一句话目标声明用一段话描述这个包解决什么问题。规格文档编写明确模块接口、输入输出、处理规则、边界条件。任务清单拆解把规格拆成可独立验收的任务。AI 结对实现每个任务把规格片段和测试用例一起喂给 AI。自动化验证与回归跑测试、跑 lint、核对类型声明。人工 Review 与文档同步检查 AI 的“自由发挥”优化规格和文档。接下来我会用实际做排版本text-arranger的过程把这六步逐一展开。3. 为什么拿“排版包”当试验田恰到好处的复杂度3.1 项目定位中文技术文档自动排版工具 text-arranger我做的这个包项目代号叫text-arranger定位是中文技术文档自动排版工具。它要处理的不是代码本身而是文档文本里那些“看着不爽但很难手动全量修”的排版细节。核心规则我圈定了四个模块normalizeQuotes把英文直引号统一成中文弯引号“”‘’。insertSpaces在中英文、中英文与数字之间自动插入半角空格。normalizePunctuationSpacing清理中文标点后面的多余半角空格比如你好 world变成你好world。normalizeHeadings把 Markdown 标题层级按目录结构规范化避免跳级。除了这几个纯函数模块还有一个 CLI 入口方便直接对文件批量执行text-arranger format README.md。这个包并不复杂但也绝不是“ hello world ”级别的玩具。它需要处理 Unicode 范围判断、正则表达式的边界、CLI 参数解析、文件读写还要输出 Node API 和命令行两种使用方式。3.2 为什么这个复杂度适合第一个 SDD 项目很多读者可能正在纠结“我该拿什么项目来练 SDD”。我个人的建议是不要一上来就拿一个完整微服务做实验也不要拿一个只有 20 行的纯函数当例子。前者会让规格文档失控后者体现不出 SDD 的价值。text-arranger的复杂度恰好落在“黄金区间”功能模块多5 个左右但每个模块边界清晰规则需要精确描述但单条规则都容易写测试它最终要发布成 npm 包对接口稳定性有真实要求而不是练完就扔。这个复杂度能让你走完 SDD 的完整闭环但不会让你在写规格阶段就累到想放弃。4. 写规格书才是重头戏一份能直接喂给 AI 的文档长啥样4.1 规格模板结构正式动手写规格之前我先定了一个模板。格式不用太复杂但下面几项必须有模块名与一句话目标函数签名输入描述与输出描述处理规则按优先级列出边界条件表验收测试用例我给每个模块都按这个模板写了一份规格。拿normalizeQuotes模块举例子// 模块normalizeQuotes // 目标将英文直引号替换为中文弯引号 // 签名normalizeQuotes(input: string, options?: { keepAsciiQuotes?: boolean }): string // 规则 // 1. 默认将成对的 替换为 “ 和 ” // 2. 默认将成对的 替换为 ‘ 和 ’ // 3. 当 keepAsciiQuotes 为 true 时不处理英文引号 // 4. 不成对的引号保持原样输出 // 异常 // - input 为 null 或 undefined 时返回空字符串并在控制台输出 warning // - input 不是 string 类型时抛出 TypeError你可能会觉得这种写法很机械但正是这种机械感让 AI 失去了“自由发挥”的借口。它知道自己不需要设计接口不需要判断输入类型要抛什么错只需要把规则翻译成代码。4.2 输入输出定义与伪代码除文字规则外我还为关键模块写了伪代码进一步压缩 AI 的决策空间。比如insertSpaces模块的核心逻辑伪代码如下遍历输入字符串的每个字符 如果当前字符是中文字符CJK且前一个字符是 ASCII 字母或数字 在前一个字符和当前字符之间插入一个半角空格 如果当前字符是 ASCII 字母或数字且前一个字符是中文字符 在当前字符与前一个字符之间插入一个半角空格为什么要写伪代码因为在 AI 编程中最浪费时间的不是“写代码”而是“反复纠正 AI 对需求的理解”。伪代码能在第一次就让 AI 朝着正确方向走减少无意义的返工轮次。4.3 边界条件表AI 最容易翻车的地方我单独为每个模块整理了一张边界条件表这些内容普通需求文档里很少写恰恰是 AI 最容易翻车的地方。以下截取normalizeQuotes的一部分输入期望输出说明他说你好他说“你好”最普通的成对替换hello world“hello world”全英文也处理他说你好她问你好吗他说“你好”她问“你好吗”多对引号同时处理He said Im fineHe said “I’m fine”英文撇号不参与配对他说你好他说你好不成对引号原样保留他说:你好他说‘你好’冒号后无空格、引号正常处理null console.warn异常输入统一拦截给 AI 这张表时我通常会附一句“请以这些输入输出对作为测试基准不要额外改变行为。”有了这个约束AI 就不太会在Im这种缩写场景里把撇号强行改成中文弯引号——这是正则方案最容易犯的错。4.4 验收测试先行测试用例就是需求的可执行翻译规格文档是给人看的测试用例则是给 AI 和后续维护者看的。我采用的方式是在让 AI 写实现代码之前我先用 Vitest 把测试用例写出来。import { describe, it, expect } from vitest; import { normalizeQuotes } from ../src/normalizeQuotes; describe(normalizeQuotes, () { it(converts straight quotes to curly quotes, () { expect(normalizeQuotes(他说你好)).toBe(他说“你好”); }); it(handles multiple pairs, () { expect(normalizeQuotes(他说你好她问你好吗)).toBe(他说“你好”她问“你好吗”); }); it(does not convert apostrophes in English, () { expect(normalizeQuotes(He said Im fine)).toBe(He said “I’m fine”); }); it(keeps unpaired quotes as-is, () { expect(normalizeQuotes(他说你好)).toBe(他说你好); }); it(throws on non-string input, () { expect(() normalizeQuotes(42 as any)).toThrow(TypeError); }); it(returns empty string on null/undefined with warning, () { const warnSpy vi.spyOn(console, warn).mockImplementation(() {}); expect(normalizeQuotes(null as any)).toBe(); expect(warnSpy).toHaveBeenCalledOnce(); warnSpy.mockRestore(); }); });在这里测试用例不只是一种验证手段更是和 AI 沟通的“契约语言”。我在提示词里直接引用这段代码让 AI 把测试当成验收目标。5. 从规格到代码AI 协作的实际过程记录5.1 任务拆解与提示词模板规格写完以后整个包被拆成 8 个任务5 个功能模块每个单独成一个任务再加上 CLI 封装、聚合导出入口index.ts、以及测试补充一共 8 个。每个任务都尽量保持“一次只实现一个功能”的粒度。实际用到的提示词模板长这样你是一名 TypeScript 开发者。请严格按照规格实现以下模块。 模块名normalizeQuotes 规格 在这里粘贴对应模块的规格文档 验收测试 在这里粘贴对应模块的 Vitest 测试用例 约束 1. 使用 TypeScript 编写导出类型声明。 2. 纯函数不要引入任何外部依赖。 3. 不要修改测试用例文件。 4. 实现后请直接给出完整代码。 请先阅读规格和测试在没有疑问的情况下直接输出代码。我特意加了“在没有疑问的情况下直接输出代码”这句是为了避免 AI 每轮都在“能不能先问你几个问题”上浪费 token。真正有问题时它输出的代码自然过不了测试到时候我再和它对话修复。5.2 AI 第一版代码与 Review 后的修正以normalizeQuotes为例AI 第一版给出的是基于正则的写法export function normalizeQuotes(input: string, options: { keepAsciiQuotes?: boolean } {}): string { if (input null) { console.warn(normalizeQuotes: input is null or undefined); return ; } if (typeof input ! string) { throw new TypeError(normalizeQuotes: input must be a string); } const { keepAsciiQuotes false } options; if (keepAsciiQuotes) return input; return input.replace(/([^]*)/g, (_match, group) “${group}”); }前面几个简单测试能过但它在处理多对引号、英文撇号、以及“冒号后引号”的场景时仍然不够稳。我把测试跑给 AI 看以后它在第二轮改成基于状态机的逐字符遍历方案export function normalizeQuotes(input: string, options: { keepAsciiQuotes?: boolean } {}): string { if (input null) { console.warn(normalizeQuotes: input is null or undefined); return ; } if (typeof input ! string) { throw new TypeError(normalizeQuotes: input must be a string); } const { keepAsciiQuotes false } options; if (keepAsciiQuotes) return input; let result ; let inDouble false; let inSingle false; for (let i 0; i input.length; i) { const ch input[i]; const prev i 0 ? input[i - 1] : ; const next i input.length - 1 ? input[i 1] : ; if (ch (!inSingle)) { if (!inDouble) { result “; inDouble true; } else { result ”; inDouble false; } } else if (ch !inDouble) { // 处理英文缩写中的撇号例如 Im if ((prev a prev z) || (prev A prev Z) || (next a next z) || (next A next Z)) { result ch; // 保留撇号 } else if (!inSingle) { result ‘; inSingle true; } else { result ’; inSingle false; } } else { result ch; } } return result; }这次判断已经比第一版严谨很多但仍然有一个隐藏问题如果文本里已经存在中文弯引号AI 的状态机可能会跳过它们这没问题但如果一个字符串里英文引号本身不成对比如他说你好状态机遍历完也不会自动修正恰好符合我的规格要求——不成对就原样保留。这说明一个事实AI 生成代码可以很快但“判定它是否真的正确”这件事依然需要人来做。5.3 循环迭代机制不是一次给全而是小步快跑我在这次实践中确认了一个经验不要一次性把所有模块的规格和提示词都丢给 AI。AI 的上下文窗口虽然越来越大但大上下文反而容易让它对早期模块的约束“失焦”。我的做法是每轮只喂一个模块的规格和测试。AI 提交代码后我立刻跑vitest run。测试没过就把失败信息原样贴回对话让 AI 根据报错信息修改测试过了再进入下一个模块。实测下来8 个任务平均每个任务经历 2.4 轮通过最顺利的insertSpaces一轮就过最曲折的normalizeHeadings修了 4 轮才把所有边界条件覆盖。如果没有规格文档和测试用例做锚点这个数字大概率会成倍上涨。6. 发布 npm 包时踩的坑从 PowerShell 限制到镜像证书过期6.1 PowerShell 执行策略与 npm 命令找不到的经典问题写完代码、跑完测试接下来是发布阶段。如果你用的是 Windows大概率会碰到这个报错npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。这本质上是 PowerShell 的执行策略在拦截 npm 自带的.ps1脚本。解决办法不是去设置“取消脚本限制”那是危险操作而是只针对当前用户放宽到RemoteSignedSet-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserRemoteSigned的意思是本地创建的脚本可以直接运行从远程下载的脚本必须经过签名。这样既解决了 npm 的运行问题又不会真的把安全策略完全关掉。还有一个类似的高频问题npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这意味着nodejs的安装目录不在PATH环境变量里。检查一下C:\Program Files\nodejs是否在系统环境变量中不在就手动加进去改完记得重启终端让环境变量重新加载。6.2 镜像源问题eunsupportedprotocol 与证书过期发布 npm 包不像安装依赖那样对源敏感但本地开发时你通常要安装 CLI 工具链这时候镜像源就能刷一波存在感。很多人项目里保留了老旧的 registry 配置比如指向https://registry.npm.taobao.org的旧地址。在 npm 7 之后官方已经不再支持http://开头的 registry如果你配置的是http://registry.npm.taobao.org安装依赖时会直接报npm error code eunsupportedprotocol另外旧域名registry.npm.taobao.org的 TLS 证书在前两年已经停止更新实际使用中很容易出现npm err! code cert_has_expired npm err! request to https://registry.npm.taobao.org/... failed, reason: certificate has expired解决方案是把 registry 切到官方源或者使用已经完成迁移的npmmirror.com新域名npm config set registry https://registry.npmjs.org # 或者 npm config set registry https://registry.npmmirror.com配置完后建议用npm config get registry确认生效。还有个细节如果你的项目里有.npmrc文件里面的 registry 优先级比全局配置更高排查时一定要检查项目级.npmrc。6.3 ESM/CJS 双格式导出与 exports 字段我写的包源码是 TypeScript需要同时输出给 Node 的 CommonJS 使用者和现代打包器下的 ESM 使用者。这里我推荐直接用tsup它基于 esbuild配置非常简单// tsup.config.ts import { defineConfig } from tsup; export default defineConfig({ entry: [src/index.ts], format: [esm, cjs], dts: true, clean: true, sourcemap: true, target: es2019, });光有tsup还不够package.json里必须显式写清楚模块导出路径{ name: text-arranger, version: 0.1.0, type: module, main: ./dist/index.cjs, module: ./dist/index.js, types: ./dist/index.d.ts, exports: { .: { import: { types: ./dist/index.d.ts, default: ./dist/index.js }, require: { types: ./dist/index.d.cts, default: ./dist/index.cjs } }, ./package.json: ./package.json } }这里最容易踩的坑是忘了type: module会导致.js文件被 Node 当成 CJS 解析从而出现export语法报错而反过来如果exports没有定义require分支老项目里require(text-arranger)就会失败。这个双格式导出的问题属于“看起来不起眼、实际一踩一个准”。6.4 发布与版本管理给首次发布者的建议发布 npm 包本身流程不复杂先npm login然后npm publish。但有几个细节值得单独说。一是首次发布时包名不能和现有包冲突发布前最好先npm view text-arranger查一下重名。二是 npm 默认只发布files字段里的文件我建议在package.json里显式声明要发布的目录{ files: [dist, README.md, LICENSE] }这样可以把src、测试文件、构建缓存都排除在外包体更干净。三是发布后如果要补充内容记得改version再发布npm publish不允许相同版本号重复发布。如果你用 GitHub 管理项目可以用changesets管理版本和 changelog它会自动生成minor/patch建议对整个发布流程的规范性提升不是一星半点。7. SDD 的真实收益与适用边界用了两周后的冷静总结7.1 量化对比SDD 与 Vibe Coding 的差别包发布以后我又回看了 Vibe Coding 阶段写的代码尝试从几个维度量化这段差距维度Vibe Coding 阶段SDD 阶段功能实现耗时约 3 小时重复厮杀规格加实现约 5 小时单元测试覆盖率约 30%95% 以上接口变更次数3 次以上定稿后 0 次边界条件处理靠 AI 随缘规格表逐项保障文档同步成本高代码变了文档要跟着瞎猜低文档从规格中直接生成单看第一次开发耗时SDD 反而更慢因为它把原先可以“糊着写”的时间转移到了规格编写和评审上。但如果你把后续找 bug、改接口、补文档的时间也算进去SDD 的收益是明显的尤其是在维护窗口超过两周的项目上。7.2 哪些场景不适合 SDD虽然这次实践结果不错但我不想把它包装成万能方法论。SDD 不适合下面三类场景第一需求极度模糊的探索性原型比如“做一个自动生成漫剧脚本的工具”你连最终交互都还没想清楚写规格只会让你卡在第一步第二一次性脚本或临时验证代码这种代码用完即弃花 30 分钟写规格是浪费第三没有测试基础的项目SDD 高度依赖自动化测试作为人类和 AI 之间的契约如果项目本身测试基建很差SDD 会变成只有文档没有验证的“架空流程”。7.3 给想入坑的人三个建议基于这次经验如果你想在自己的下一个 npm 包或者内部小工具里引入 SDD我有三个很具体的建议。第一规格文档一定要有版本号。AI 协作过程中规格不是在对话里随便改的每一处变更都要记录版本和日期。我在实践中发现没有版本号的规格文档会在迭代一两轮后彻底失真人脑根本记不住哪个版本对应哪段实现。第二测试用例永远早于提示词。不要等到 AI 写完了才补测试那等于把你的验收标准暴露给“幸存者偏差”。正确顺序是写一个测试跑一遍看到它失败红然后把测试和规格一起交给 AI等 AI 实现后跑通绿。第三人工 Review 是 SDD 流程不可省略的一环。AI 可以写出通过测试的代码但它不会告诉你“这里其实有更简洁的写法”“这个函数命名有歧义”“这段逻辑后面很难扩展”。我这次在 Review 阶段手动重构了insertSpaces的 Unicode 判断逻辑——AI 用的是硬编码的字符范围数组我换成了一份带注释的CJK字符属性表。这种改进规格文档不会替你想到测试用例也不会强制要求只有人坐在这里看代码时才会发生。把规格文档当成 AI 的“施工图”把测试用例当成双方的“验收单”把人工 Review 当成最后一道“监理”。这套组合就是我目前最顺手的工作流。从最初那个乱糟糟的文档项目到最终发布到 npm 的text-arranger我用 SDD 换来的最大收获不是代码量而是对项目边界的控制感——在 AI 越来越强的时代知道让 AI 做什么、不做什么比单纯会写提示词重要得多。至于下一个项目我大概率会直接按这套流程再跑一遍只是规格文档的版本号我会从一开始就老老实实写清楚。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价