资讯动态

从vibe coding到SDD:我用AI Agent开发npm中文排版包的经验

发布时间:2026/9/8 20:41:38 来源:尧图企业网站定制
1. 为什么我不再一句话甩给 AI改回先写规格再写代码vibe coding 刚火的那阵我的节奏基本是在聊天窗口里描述一个需求AI 直接吐出一坨代码我粘贴、运行、报错、继续让它改。做两三屏的小脚本还好真要做一个给别人装的 npm 包时这个流程崩溃得特别快——不是 AI 不努力而是我和它之间根本没有一份共同认可的设计稿。项目过到第三天我自己都忘了最初那版加空格规则和后来修修补补的行为是不是一回事。这次做排版工具我换了个思路先把 SDDSpec-Driven Development规格驱动开发完整走了一遍先写清这个包应该做什么、明确不做什么、每种输入该出什么结果再让 AI Agent 按规格实现最后用规格里的样例反向生成验收测试。整个过程和以往最大的区别是我当的是需求方和验收方而不是替 AI 盯着每一行代码的质检员。SDD 近半年在 AI 辅助编程语境里被反复提起ThoughtWorks 那位工程师 Birgitta Böckeler 提出的三级分类框架也经常被拿来讨论。按我在实际项目里的理解它大致把协作模式分成三档第一档是只有一句任务描述的纯意图级本质上还是 vibe codingAI 的自由度最高第二档是决策级人把技术选型、接口边界、核心输入输出样例写清楚AI 在限定范围内填代码第三档是契约级每个行为规则都拆到可验收、可测试的粒度模型实现完必须有测试通过作为证据。我这次做的工具包规模不算大所以我走了第二档为主、第三档混合的路线——核心规则全部落到可测的验收清单里但没给整个仓库写几百页文档。很多人一听 SDD 就抵触觉得是给 AI 编程加文档负担。我一开始也这么想。但这份排版工具的需求恰好能把问题暴露得很清楚中文排版规则看起来人人知道真落到代码边界上全是灰色地带。中文和英文之间要加空格这句话AI 可能默认对 URL、版本号、代码块里的内容也加空格你不想让它加就必须写在规格里。没有规格的时候这些灰色地带全靠试错喂给大模型。有了规格之后AI 第一轮写出来的代码就非常接近最终目标剩下的都是微调而不是推翻重来。2. 给排版包定规格规则表、反例表、非目标一个都不能少2.1 先定范围它能做什么更重要的是不做什么这个 npm 包做的是一件小事把一段中文文本按常见中文排版习惯整理一遍。它接收字符串返回处理后的字符串。我给它设计了两个入口模式plain 模式处理纯文本markdown 模式额外跳过代码块和行内代码避免破坏用户贴的代码片段。规格第一版里我没着急写实现而是先列了一个做什么/不做什么清单。做什么包括中文与英文之间补半角空格、中文与数字之间补半角空格、中文语境内的省略号整理成规范形式、URL 和邮箱作为整体保留并在前后补空格。不做什么包括不做繁简转换、不做错别字校对、不做分词、不做 PDF 或 HTML 渲染级的版式。把不做什么写清楚这个动作后来的价值大到我意外——AI 特别容易在实现过程中自作主张地帮你加功能比如顺手把英文标点全转成全角或者把 Markdown 的标题符号处理坏了。有了一条非目标声明Agent 在规划实现时会自己绕开这些范围。核心规则我列成了下面这样一张规则表每条配一个输入输出样例编号行为规则输入示例预期输出R-01中文与英文之间补一个半角空格写TypeScript代码写 TypeScript 代码R-02中文与数字之间补一个半角空格一共20个版本一共 20 个版本R-03URL/邮箱视为整体前后补空格内部不处理访问https://example.com访问 https://example.comR-04连续多个点号或西文省略号归一为规范省略号恩...好恩……好R-05Markdown 代码块与行内代码内容原样保留写TypeScript写TypeScript这张表看起来简单但在写规格时已经逼我做了好几个以前没想过的决定比如写 Node.js 代码里 Node.js 前面的空格和后面的空格都要补但 R-02 的20个版本数字后面补空格后如果下一个字符是中文句号要不要再补规则本身不生歧义才会让 AI 的第一版实现偏离最小。2.2 规格里的反例AI 最需要不要这么做给 AI 写规格时正向规则给再多它也会在边界上自由发挥。我后来在 spec 文件里加了一整节反例表专门记录那些看起来符合规则、但实际不该被处理的情况。这块内容值得讲。比如 R-03 规定 URL 前后补空格但https://example.com内部的两条斜杠之间绝不能插入空格中文里的版本号 v1.2.3要不要在.前后加空格按中文排版习惯版本号应该整体视为一个 token不加空格。AI 如果只看了 R-02 和 R-03很容易把版本号当成英文和数字混合文本拆得稀碎。规格里我写了反例v1.2.3 整体保留不插入空格IP 地址 192.168.1.1 同理。这类例子越具体后面验收测试出来时和 AI 来回扯皮的次数就越少。spec 文件我用的是 Markdown不是某种新语言。结构固定成背景、范围、非目标、规则表、反例表、验收清单、待办问题。为什么用 Markdown因为 Claude Code 这类 Agent 对 Markdown 的解析能力已经足够好而且人读起来也顺畅不需要为了形式上的结构化引入额外工具链。第一次写完 spec 大概花了一个多小时其中一半时间是在补反例和边界例子不是写废话。这就是 SDD 的核心体验人在规格阶段把歧义杀干净后面 AI 实现才有可能是直线球。2.3 接口设计在规格里敲定函数签名比实现先定下来接口部分我直接在规格里定死了没让 AI 自由发挥export type FormatMode plain | markdown; export interface FormatOptions { mode?: FormatMode; } export function formatText(input: string, options?: FormatOptions): string;设计时只暴露一个主函数而不是拆一堆 spaceBetweenChineseAndEnglish、normalizeEllipsis 等细碎 API 再由用户自己组合。原因很简单排版规则之间有先后顺序和互相影响。比如 URL 识别要先于空格插入如果用户在 URL 内部先被加了空格后面的 URL 保护逻辑就识别不出来了。一个黑盒入口内部按固定管道顺序执行规则才是最不容易滥用的 API 形态。这个决定也直接写进了规格的接口说明里AI 按图实现时没有纠结的空间。3. 把实现交给 AI AgentAGENTS.md 和阶段化指令是怎么配合的3.1 初始化项目让 Agent 一进来先读规格我这次用的主力是 Claude Code 的终端 Agent 模式。和聊天窗口里贴代码最大的区别是Agent 能直接读写仓库文件、跑测试、改代码。但这不代表你可以把整个仓库丢给它以后当甩手掌柜。真正关键的工程动作是建立约束文件。我在项目根目录放了一份 AGENTS.md内容是# 项目约定 1. 本项目是规格驱动开发任何行为变更先改 spec/spec.md再改 src 下实现。 2. 核心逻辑集中在 src/typography.ts入口导出来见 src/index.ts。 3. 规则编号 R-xx 的验收用例必须保留在 test/typography.test.ts。 4. 改完代码后必须运行npm run test npm run typecheck。 5. 你不确定某个边界行为时先查 spec不要猜测。这份文件用大白话告诉 Agent 三件事项目边界在哪、改动规格和代码的先后顺序是什么、完成后必须用什么命令自证。第一次执行会话里我发现 Agent 确实会在动手前主动打开 spec 看一眼而不是直接凭系统提示词里的印象写代码。AGENTS.md 相当于给 Agent 装了个项目常识省得每次对话都得重复交代背景。3.2 阶段化推进一次只实现一档规则规格虽然只有几十行但我没有让 Agent 一把梭全部实现。原因有两个一是单次上下文窗口内代码多了以后模型的注意力会下降二是规则之间有关联一次性实现全部规则如果某个中间行为错了排查时根本分不清是哪一步引入的。我的拆法是按规则之间的依赖关系分成三个批次。第一批只做 R-01 和 R-02让基于字符遍历的中英文之间插空格逻辑先跑通第二批做 R-03加入 URL/邮箱保护因为它的实现会改变第一批的行为第三批做 R-04 和 R-05 的 Markdown 模式最后再做整体联调。每个批次推进时我给 Agent 的指令大致是这样请读取 spec/spec.md实现 R-03。 要求 - URL、邮箱在内部不处理空格 - 仅在 URL 前后补空格 - 版本号和 IP 按反例表原样保留 - 补全 test/typography.test.ts 里 R-03 对应的用例 - 完成后跑 npm run test不要改动 R-01、R-02 行为。实际效果比我预想的好。Agent 拿到这份指令后会先在代码里找它认为的 URL 边界再对照规格里的 URL 反例逐个核验实现过程中还主动问了我一个规格没写明的问题markdown 的链接文本和链接地址要不要同样处理这个问题说明模型真的在拿规格推演实现细节了而不是憋着写完全部代码再等我喷。对于这种规格遗漏我的处理是先补充到 spec 里再让 Agent 继续绝不让它自行发挥之后就忘了记录。3.3 让 Agent 自己写单元测试规格成了测试脚本SDD 流程里最容易偷懒的环节是测试。我以前经常遇到AI 说都测过了实际上只跑了一遍主流程的情况。这次我强制要求一条规则至少一组用例且测试解释文字里要带上规格编号。这个约束放在 AGENTS.md 里目的是让 Agent 提交的代码自带可验证性。当 Agent 实现完一个批次它会顺手跑一次测试。真正跑挂之后它有两种处理方式如果代码实现偏了它会主动修实现如果它发现规格本身有矛盾会停下来在对话里指出问题而不是强行糊一个输出确保测试通过。这第二条尤其重要——AI 为了满足测试用例而在实现里写死特例的情况很常见但因为我们每条规则对应的输入样例都是开放式的它没法靠特例糊弄过去。4. 验收测试让规格条目变成 test 用例AI 没法再糊弄测试不是给规格交差而是整个流程里最硬的验收面。我在项目里用的是 Vitest因为配置轻量、对 TypeScript 原生友好。每个规则编号在测试文件里都有对应的 describe 分组测试名直接写规则编号和含义打开测试文件就像打开一份可执行的规格清单。describe(R-01 中文与英文之间补半角空格, () { it(中文后直接跟英文单词, () { expect(formatText(写TypeScript代码)).toBe(写 TypeScript 代码); }); it(英文单词后直接跟中文, () { expect(formatText(本文介绍ofDocker用法)).toBe(本文介绍 ofDocker 用法); }); }); describe(R-03 URL 前后补空格内部保持原样, () { it(中文和 URL 之间自动补空格, () { expect(formatText(访问https://example.com测试)).toBe(访问 https://example.com 测试); }); it(URL 内的斜杠不做处理, () { expect(formatText(链接https://example.com/a/b结尾)).toBe(链接 https://example.com/a/b 结尾); }); });这些用例不是 AI 拍脑袋生成的绝大多数直接来自规格里的规则表和反例表我只是把它们翻译成了断言。翻译过程中我发现一个特别值得提醒的点Agent 生成测试用例时有一种天然倾向是把输入写得太规整比如只测中文和 English 之间这种经典场景而不去测中文和 node.js 这种带点号的英文 token或者数字、英文、中文三者连在一起的混乱真实文本。真实文本大多是脏的所以规格里的反例表在这个阶段成了金矿每一个反例都能变成一条测试。规格驱动还有一个反直觉的优点——它把人改需求的成本降得很低。比如我原来没考虑时间格式 3:30 PM这种输入后来实测发现规则 R-02 会把冒号后面的空格处理得很难看。按照旧习惯我可能直接说AI 修一下这个 bug然后 Agent 改完代码测试也过了但没人知道这条行为是不是后来又会被别的地方破坏。在 SDD 流程里我的做法是先改规格在反例表加一行英文时间格式保留原样 3:30 PM不拆分冒号前后然后让 Agent按规格变更更新实现和测试。Agent 这次不仅能修好行为还会主动在测试里补一个3:30 PM的回归用例。这才是规格作为单一事实来源的意义。测试过程中我还撞见过一个很典型的 AI 问题模型实现 R-04 省略号规则时倾向于把输入里的三个英文句点...直接替换成单个中文省略号 U2026而中文排版正确表现其实是两个连着的 U2026 字符……。单从字符看一个是 U2026两个也是 U2026 的重复但如果实现里只 replace(..., …)最后输出在大多数系统字体下视觉长度不对。这个细节靠肉眼 review 代码很难发现但规格里的输出应包含两个连续的 U2026这一句让测试直接给出了明确失败。从那之后我更确信SDD 里最值钱的不是规则本身而是规则后面的验收标准精确到了可断言的程度。5. 走完发布流程PowerShell 执行策略、registry 证书过期还有双格式打包5.1 package.json 的 exports 怎么写才不坑用户本地实现和测试都过了以后剩下最后一公里是发布到 npm。这个环节看着不起眼实际坑比我想象的多。先看 package.json 的核心配置{ name: cn-typo-fmt, version: 0.1.0, type: module, files: [dist], main: ./dist/index.cjs, module: ./dist/index.js, types: ./dist/index.d.ts, exports: { .: { types: ./dist/index.d.ts, import: ./dist/index.js, require: ./dist/index.cjs } } }我明确选择了双格式输出同时支持 ESM 的 import 和 CommonJS 的 require。现在新项目大多用 ESM但很多老项目还在用 require你不给 CJS 出口人家一装就报ERR_REQUIRE_ESM这个体验很劝退。源码 TypeScript 写好之后我用 tsup 一把打包出index.jsESM、index.cjsCJS和index.d.ts类型声明比手动配 rollup 省心得多。发布前的检查动作也很重要。跑一遍npm pack --dry-run看一下即将打进 tarball 的文件列表。我第一次打包时发现 dist 之外还有 spec 目录和测试文件会被带进去虽然无损但会让包很臃肿。加files: [dist]之后才干净。这一步花不了两分钟但很值得养成习惯。5.2 我实际遇到的四个发布报错发布过程中我撞了四个典型的坑每个都能在网上搜到一堆同款问题这里直接给出我的处理方案。第一个也是最经典的在 Windows PowerShell 里执行 npm 命令直接报错npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。这是因为 PowerShell 的脚本执行策略默认是 Restrictednpm 的 .ps1 包装脚本跑不起来。修复命令Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserRemoteSigned表示本地脚本可以运行、从网上下载的脚本需要签名Scope CurrentUser只影响当前用户不需要开管理员权限终端。如果你不想动执行策略也可以直接改用 cmd 里的npm.cmd但不建议长期这么绕因为后面还有很多工具链脚本会踩同一个限制。第二个是环境变量问题提示npm 不是内部或外部命令。这种情况通常是 Node.js 安装目录没加进 PATH或者改完 PATH 之后没重启终端。网上很多教程让重新安装 Node其实没必要。打开系统环境变量把 Node.js 所在目录通常是C:\Program Files\nodejs加到 Path 里然后重新开一个终端就解决了。第三个是 registry 证书过期npm ERR! code CERT_HAS_EXPIRED请求地址指向https://registry.npm.taobao.org。这是老版淘宝镜像域名证书过期导致的不是你本机的问题。处理办法是切回官方源或新镜像源npm config set registry https://registry.npmjs.org/如果团队必须用国内镜像也应该用现在维护中的域名而不是已经停用的老域名。证书过期类报错最迷惑的点在于它报的是一堆 TLS 错误容易让人误判成网络问题或代理问题实际换源就好。第四个是 npm 安装时出现的npm warn deprecated node-domexception1.0.0。这类 deprecation 警告来自你依赖树的某个子依赖不是你的包本身有问题。我一开始还专门去查要不要锁版本后来想清楚了只要不是导致实际报错的 deprecated 警告就不值得为它打乱依赖版本。真正要留意的是警告里是否带上npm ERR那才是要处理的。5.3 发布成功不算完立刻在空目录里验证一次npm publish成功那一刻容易让人误以为任务结束了。我的建议是永远在一个全新的目录里验证一次装包和使用。具体做法mkdir /tmp/verify-typo cd /tmp/verify-typo npm init -y npm install cn-typo-fmt node -e const { formatText } require(cn-typo-fmt); console.log(formatText(用SDD做的排版包测试));这一步能同时验证三件事tarball 里没有漏文件、CJS/ESM 入口都正确、代码在干净环境下能正常运行。我见过太多包作者本地能跑别人一装就废原因大多出在漏了 dist 目录、exports 字段写错或 package 里少了某个文件。空目录验证是成本最低的保险。6. 一段时间用下来SDD 最划算的场景和不划算的场景这套流程跑完一个实际发布的项目之后我心里对 SDD 的适用边界有了更清楚的答案。如果你只需要一段二三十行的脚本处理完就扔那 vibe coding 依然是最快的让 AI 天马行空没问题。但只要你准备做一个要发布、要维护、要给别人用的 npm 包哪怕逻辑不算复杂规格先行都值得。它其实不是给 AI 增加流程是在保护你的项目不被模型的合理猜测带偏。我现在的判断标准很简单这个文件或者这个包以后会不会有第三个人看会不会被测试覆盖会不会持续迭代超过一周三个问题里有一个是是我就先写规格再写代码。规格不用写长关键是规则表加反例表加验收清单。真正让我坚持用下去的原因是SDD 把我和 AI 的对话从你猜我想要什么变成了按这份文档执行不确定就问我协作质量稳定了很多。后续如果再做大一点的工具我会试试把规格拆成多份文件并按 openspec 的目录习惯维护变更记录让整个演进过程有迹可查。最后分享一个小技巧规格文件写好之后先别急着让 AI 动代码自己照着规格里的验收样例在脑子里过一遍看有没有哪个例子其实有两种解读。每找出一个歧义就等于后面少一次和 Agent 的无效返工。这种花一小时写规格省下三小时改 bug的交易做过一次你就回不去了。

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

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

免费获取报价