尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SDD+AI协作开发实战:从规范到发布一个中英文排版npm包

SDD+AI协作开发实战:从规范到发布一个中英文排版npm包 一个周末我把“做个排版 npm 包”这件事用 SDD 的方式彻底落地了。这个包的定位很简单解决中英文混排时的格式问题——中文和英文之间自动加空格、统一标点、规范化省略号和破折号同时不弄坏 Markdown 的代码块和链接。整个过程里真正写业务逻辑和测试用例的是 AI我主要负责把需求写成规范、把规范拆成任务、以及审查 AI 产出的代码这就是 SDDSpec-Driven Development规范驱动开发在 AI 协作开发中最大的价值让人做判断让 AI 做实现。如果你也在研究怎么把 AI 用进自己的项目或者想做一个能发布到 npm 的小工具这篇实战记录应该对你有帮助。1. 项目缘起与整体思路拆解1.1 排版这件小事为什么值得做成一个包先说痛点。我平时写技术博客、整理公众号文案、维护项目 README最烦的就是中英文混排的格式。举一个真实例子我写“使用AI协作开发npm包”这种句子理想状态是“使用 AI 协作开发 npm 包”中文和英文之间要留一个空格英文和数字之间要根据习惯处理标点方面中文语境下该用全角标点英文语境下该用半角标点。这些规则非常碎但每次要手动改一遍改完还总有漏网之鱼。编辑器插件我也试过几个比如 Sublime 的自动排版插件、Typora 里的中英文一键排版功能。但问题在于插件的规则是写死的有些规则我不需要我需要的规则它又没有更麻烦的是团队协作时别人用的是别的编辑器规则不一致同一篇文章换个人打开格式就乱了。所以我一直在想能不能把排版规则做成一个独立的 npm 包命令行能跑、代码里能调用、还能接进 CI 流程。只要有 Node.js 环境谁都能用同一套规则处理文本。这个需求其实不小。排版看起来是小事但一旦要做成通用工具就要考虑 Markdown 特殊语法、编码问题、不同平台换行符、规则可配置性。靠手工维护一堆正则太容易翻车这时候 SDD 的思路就派上用场了——先写清楚“这个包到底该做什么、做到什么程度”再用 AI 去填实现细节比我以前边写代码边想需求高效得多。1.2 SDD先写规范再让 AI 干活和 TDD 有什么本质区别很多同学可能对 TDD测试驱动开发比较熟先写一个会失败的测试再去写功能代码让测试通过。SDD 的思路完全不同它不是从测试出发而是从一份“规格说明”出发。Spec 里描述的是这个功能是什么、输入是什么、输出应该是什么、有哪些边界情况、哪些事情明确不做。AI 拿到这份 spec再去生成代码产出就会稳定很多。为什么 AI 时代 SDD 反而更合适我自己的体会是AI 非常擅长实现但非常不擅长猜需求。如果你只是丢一句“帮我写个排版工具”它大概率给你一个看着能用、实际一测全是问题的半成品。因为排版里的规则太多你不说清楚“中英文之间加空格”它就不会加你不说清楚“代码块内部不能动”它可能把代码块里的注释也当成普通文本改了。Spec 就是用来消灭这种模糊性的。TDD 和 SDD 并不是对立的。我这次的做法是先用 SDD 把规则和验收标准定义清楚再用类似 TDD 的方式把验收标准转成测试用例最后让 AI 实现功能并让测试通过。Spec 管“要做成什么样”测试管“怎么证明做对了”两者配合起来非常舒服。对比维度TDDSDD起点一条会失败的测试用例一份描述行为的规格说明关注点代码行为和结果验证需求边界和验收标准适合场景功能逻辑明确、算法可预期需求模糊、规则众多、需要 AI 协作AI 协作方式AI 生成实现让测试变绿AI 先帮写 spec再按 spec 写实现主要风险测试写偏导致实现跑偏spec 写得太空或太满导致开发困住我在这次项目里的结论是TDD 解决“做对了没有”SDD 解决“做什么才算对”。AI 时代后者更重要因为需求一旦定义错AI 会以极高的效率把错误放大。1.3 Birgitta Böckeler 的三级分类框架我理解的粒度分层这次动手之前我翻了不少 SDD 的资料。Thoughtworks 的杰出工程师 Birgitta Böckeler 提出过 SDD 的三级分类框架业内讨论度很高。按我自己的实践体会可以把这三个级别通俗理解为轻量级 spec、模块级 spec、系统级 spec。轻量级 spec 可能只是 prompt 里的两三句话比如“把这段函数改成支持可选参数默认值为 false老逻辑保持不变”适合改一个小函数或小修小补。模块级 spec 要有明确的接口和行为约束适合一个完整模块的从零开发。系统级 spec 则面向多个模块协作必须包含数据契约、接口协议、验收体系和边界场景。这次排版包整体属于模块级 spec但拆出来的每个规则文件又各自带着轻量级 spec。这样做的好处是AI 在实现某一条规则时不会迷失在全局需求里我也更容易审查每段代码是否达到预期。没有这个分层容易犯“什么都往一个 spec 里塞”的毛病最后 spec 变成一篇没人愿意读的长文档。2. 排版包的核心功能与设计原则2.1 功能范围先想清楚做什么更要想清楚不做什么做工具最容易犯的错误是贪多。我一开始也幻想过这个包能不能顺便把错别字也纠正了、把句子润色了、甚至把 Markdown 转成 PDF。冷静下来之后我把功能范围收敛到了下面几块功能模块具体内容处理方式中英文空格中文与英文/数字之间自动插入空格正则规则可开关标点规范统一中文省略号、破折号压缩重复空格正则规则可开关引号处理直引号转弯引号英文撇号保留上下文判断Markdown 保护代码块、行内代码、链接、图片语法不受影响占位符提取还原多种调用方式命令行 CLI、Node.js API、直接传字符串双入口设计行尾清理去除行尾多余空格、统一换行符基础清理规则与此同时我明确写下了 non-goals非目标不做语法检查不做拼写纠错不处理复杂排版比如页边距、字体、分页不打算做成一个在线服务。这些东西都写在 spec 里AI 就不会在实现过程中“自由发挥”加一堆我不需要的功能我自己的开发节奏也不会被带偏。2.2 技术选型为什么是 TypeScript 正则而不是上 AST这个包的输入输出都是文本核心操作是规则替换所以最直接的技术方案就是正则表达式。有人可能会问为什么不用完整的 Markdown AST 解析器我的判断是排版任务本质上是“在保留结构的前提下修整文本”对结构理解的要求没有想象中那么高。引入 AST 会显著增加依赖体积和复杂度而且 AST 解析器对格式的要求更严格用户传入一段不太规范的文本时AST 反而可能解析失败。最终选择是 TypeScript 加少量零依赖的实现。TypeScript 可以提供类型声明方便使用者获得智能提示零依赖意味着安装包的时候不用担心依赖冲突体积也小。包的整体结构我设计成下面这样packages/format-md/ ├── src/ │ ├── index.ts # 统一入口导出 format 函数 │ ├── cli.ts # 命令行入口 │ ├── utils/ │ │ └── tokenizer.ts # 占位符提取与还原 │ ├── rules/ │ │ ├── space.ts # 中英文空格规则 │ │ ├── punct.ts # 标点规范规则 │ │ └── quote.ts # 引号处理规则 │ └── types.ts # 配置项和输出类型 ├── tests/ │ ├── fixtures/ │ │ ├── good.md # 期望结果 │ │ └── bad.md # 待处理输入 │ └── format.test.ts ├── package.json ├── tsconfig.json └── README.md这个结构不是我拍脑袋定的而是在 spec 阶段就规划好的。每个规则文件只负责一类规则后续加新规则或者关掉旧规则都非常方便。AI 在实现时也不需要理解整个项目只要聚焦到对应文件即可。2.3 最关键的边界保护先占位、再处理、后还原排版包最容易翻车的地方不是空格规则写不出来而是把不该改的内容改了。比如用户文本里有一段代码块const str 使用AI开发npm包;在代码块内部使用AI开发npm包是字符串字面量加了空格可能导致语义变化而且用户大概率不想改它。再比如行内代码npm i -D format-md如果被空格规则处理成npm i -D format-md还好但如果正则没写好把反引号破坏了用户的 Markdown 就废了。所以处理流程必须设计成三步先提取保护对象用占位符替换再对剩余文本执行排版规则最后把占位符还原成原始内容。提取保护对象时我用的是全局匹配加数组存储// utils/tokenizer.ts const CODE_BLOCK_RE /[\s\S]*?/g; const INLINE_CODE_RE /[^\n]/g; const LINK_RE /\[[^\]]*\]\([^)]*\)/g; export function protect(input: string): { text: string; tokens: string[] } { const tokens: string[] []; let text input; const replaceTokens (match: string) { tokens.push(match); return \x00TOKEN_${tokens.length - 1}\x00; }; text text .replace(CODE_BLOCK_RE, replaceTokens) .replace(INLINE_CODE_RE, replaceTokens) .replace(LINK_RE, replaceTokens); return { text, tokens }; } export function restore(text: string, tokens: string[]): string { return text.replace(/\x00TOKEN_(\d)\x00/g, (_, index) tokens[Number(index)]); }选用\x00这种不可见字符做占位符前缀是为了尽可能避免和用户原文冲突。AI 生成第一版时用的是普通字符串占位我审查时发现如果原文本身包含相同字符串还原就会出错改成不可见字符后这个问题彻底消失。这种细节只有真正处理过文本替换的人才想得到。3. SDD 六步 AI 的实操全流程3.1 第一步用 AI 产出 spec 初稿我再人工校审我的经验是不要从零开始写 spec可以先让 AI 基于一个大致想法生成初稿然后人工做详细的增删和校准。这一步我用了一个结构化的 prompt你是一名资深前端工程师。请你帮我为「中英文排版 npm 包」编写一份规格说明书需要包括 1. 项目目标和用户场景 2. 功能清单分为必须实现和可选实现 3. 每条功能的输入输出示例 4. 边界情况和明确不做的 non-goals 5. 验收标准尽量用可测试的句式 请用中文输出条理清晰可以直接作为 AI 编码的输入依据。AI 生成初稿后我没有直接采用而是做了两轮修改。第一轮是收敛把一些不切实际的功能划入 non-goals比如“自动翻译”“语气润色”这些功能很诱人但会无限抬高复杂度。第二轮是补细节给每条规则补上正反例。比如“中英文之间加空格”这一条正例是使用AI开发-使用 AI 开发反例是代码块内部不能动、URL 不能动、连续数字不能拆。最终 spec 里的验收标准长这样# 排版规则 spec v0.1 ## 必须实现 - 在中文与英文之间插入一个空格 - 在中文与数字之间插入一个空格 - 将连续三个英文句点替换为中文省略号「……」 - 将连续三个及以上英文连字符替换为中文破折号「——」 - 去除行尾多余空格 - Markdown 代码块、行内代码、链接内容不得被任何规则改动 ## 非目标 - 不检查拼写错误 - 不处理字体、字号、页边距 - 不做内容润色或语义改写 - 不提供在线服务 ## 验收示例 | 输入 | 期望输出 | | --- | --- | | 使用AI开发npm包 | 使用 AI 开发 npm 包 | | 一共100个文件 | 一共 100 个文件 | | ...等待中 | ……等待中 | | ---分割线--- | ——分割线—— |有了这样一份 spec后面所有环节都有了判断依据。AI 写出的代码是否符合预期我用 spec 来检查我自己写测试用例也直接从验收示例里抄。提示Spec 不是写给甲方看的文档而是写给 AI 和未来的自己看的“施工图纸”。它可以短但不能含糊。3.2 第二步拆任务、逐个对话而不是扔一个大 promptSpec 定好之后我没有把整份 spec 一次性扔给 AI让它“按这个开发”。原因有两个一是上下文太长AI 容易忽略后面的细节二是一次性生成大量代码审查成本很高出了问题也很难定位。正确做法是把 spec 拆成独立的小任务每个任务单独起一个对话。我实际执行的任务拆解表是这样的任务编号任务内容AI 输入要点交付物T1搭建项目骨架和 TypeScript 配置技术栈、包结构、tsconfig 要求package.json、tsconfigT2实现占位符提取与还原代码块、行内代码、链接的正则和占位符方案tokenizer.tsT3实现中英文空格规则中文和拉丁字符集定义、正反例space.tsT4实现标点统一规则省略号、破折号、重复空格处理punct.tsT5实现引号处理规则直引号、弯引号、撇号的上下文判断quote.tsT6实现 CLI 入口参数解析、文件读写、标准输入cli.tsT7编写测试用例验收示例 边界用例format.test.tsT8编写 README 和发布准备使用说明、API 示例、npm 配置README.md每个任务我给 AI 的 prompt 非常聚焦比如 T3 我是这么写的请实现 fixSpacing 函数功能是在中文与英文之间、中文与数字之间插入空格。 字符范围中文使用 \u4e00-\u9fff英文使用 A-Za-z数字使用 0-9。 要求 1. 输入 使用AI开发输出 使用 AI 开发 2. 输入 一共100个文件输出 一共 100 个文件 3. 如果已经存在空格不要重复插入 4. 不要处理占位符 \x00TOKEN_xxx\x00 里的内容 请输出 TypeScript 代码和简短的说明。小任务的每一个输出我都会立即 review确认没问题再进入下一个任务。这样即使某个环节出错影响面也被控制在单个文件以内修复成本很低。3.3 第三步AI 写核心逻辑我 review 边界条件这一步是整个项目里“人机协作”密度最高的环节。AI 负责生成主体代码我负责盯细节。以空格规则为例AI 第一版生成的是这样的// rules/space.ts const CJK \\u4e00-\\u9fff\\u3400-\\u4dbf\\uf900-\\ufaff; const LATIN A-Za-z0-9; export function fixSpacing(text: string): string { return text .replace(new RegExp(([${CJK}])([${LATIN}]), g), $1 $2) .replace(new RegExp(([${LATIN}])([${CJK}]), g), $1 $2); }这段代码核心思路是对的正则一处理“中文在前英文/数字在后”正则二处理“英文/数字在前中文在后”。但我 review 时发现了几个问题。第一个问题是 LATIN 字符集把下划线排除了。URL 和文件名里经常有下划线比如my_file.md出现在中文句子里时按照当前正则md和中文边界会被加空格变成my _file .md这种奇怪格式吗不会因为_不在正则匹配范围内所以可能出现“中文字符_英文字符”这种混合却没被处理的情况。排版包的规则是允许这种混合出现吗我当时和 AI 就这个点来回了两轮最终决定把_排除在自动空格范围之外即中文与下划线之间不补空格因为下划线通常表示文件名或代码标识符不应该拆开。第二个问题更隐蔽。如果输入是“AI 开发”这样已经有空格上面的正则不会重复插入因为匹配模式要求中文后面紧跟英文中间不能有空格。这一点 AI 第一版实现是对的但我还是补了一个测试用例来防止以后改动时回归。第三个问题是我手工追加的当文本里有占位符\x00TOKEN_0\x00时中文和数字之间如果被插入空格占位符就被破坏了。比如“使用\x00TOKEN_0\x00开发”正则会把“用”和“\x00”当作中文和符号边界虽然不影响占位符本身但还原后会变成“使用 code 和开发”中间多出一个空格。解决方式是在 fixSpacing 执行前先检查文本中是否包含占位符如果包含就把整个替换逻辑限制在非占位符分段中。这个需求我写进了 T3 的验收标准AI 实现时一次性做对了。3.4 第四步测试驱动验证AI 生成用例人工补边界Spec 里的验收示例是我写测试用例的第一手素材但我没有让 AI 直接照抄而是让它先把验收示例转成 Vitest 用例然后我再人工补了三类边界用例空字符串、纯英文文本、包含代码块和链接的 Markdown 文本。实际测试文件里有一段长这样// tests/format.test.ts import { describe, expect, it } from vitest; import { format } from ../src/index; describe(format, () { it(should insert space between CJK and latin, () { expect(format(使用AI开发npm包)).toBe(使用 AI 开发 npm 包); }); it(should insert space between CJK and digits, () { expect(format(一共100个文件)).toBe(一共 100 个文件); }); it(should keep code blocks unchanged, () { const input js\nconst msg 使用AI开发;\n\n然后是正文; const output format(input); expect(output).toContain(const msg 使用AI开发); }); it(should keep inline code unchanged, () { const input 执行 npm i -D format-md 后完成安装; const output format(input); expect(output).toContain(npm i -D format-md); }); it(should handle empty string, () { expect(format()).toBe(); }); });这些用例跑起来之后AI 生成的代码第一次全绿了。但我没有就此收手因为“全绿”只能说明我写的这些用例通过了不能说明规则本身没有遗漏。我额外准备了一个fixtures/bad.md里面故意放了一堆真实场景的混乱排版然后手动跑一遍命令再对照fixtures/good.md逐行检查差异。这一步属于“看起来不起眼、实际最管用”的环节。检查过程中我发现了一个预期之外的问题中文省略号替换。原文如果是...规则会统一成……但如果在英文语境里比如 She saidWait...用户可能希望保留英文风格。最后我在配置里加了一个选项preserveEnglishPunct默认关闭开启后英文语境下的标点不会被强制转换。这个选项也是我在测试中实际用到的需求不是凭空想出来的。3.5 第五步打包发布从本地验证到 npm publish发布 npm 包之前有一堆准备工作我第一次发布时踩过不少坑这次直接按流程来。首先是package.json的关键字段{ name: format-md, version: 0.1.0, description: 中英文混排排版工具支持 Markdown 代码块保护, type: module, main: ./dist/index.js, types: ./dist/index.d.ts, bin: { format-md: ./dist/cli.js }, files: [ dist, README.md ], scripts: { build: tsc, test: vitest run, prepublishOnly: npm run build npm test }, license: MIT }files字段很关键它决定哪些文件会被打进 npm 包。如果你不写npm 默认会塞一堆无关文件进去比如tests、src、node_modules里的一些东西包体积变大不说还有可能泄露不必要的代码。我这次只发布dist和README.md干净又简洁。bin字段是 CLI 入口。这里有个容易被忽略的点如果用了 ESMCLI 文件第一行必须有#!/usr/bin/env node这样的 shebang否则安装后命令行工具无法执行。AI 生成代码时不会自动加这行是我 review 时补上的。发布前我用npm pack命令在本地生成了一个 tarball然后在一个干净的临时目录里安装模拟用户的使用环境。这一步能发现很多问题比如漏了dist、入口路径不对、依赖没打全。确认没问题后再执行npm publish。整个过程下来真正在 npm 中心和本地操作的时间反而很少大部分时间花在了前置校验上。4. 常见问题与排查技巧实录4.1 npm 环境问题速查从“禁止运行脚本”到证书过期开发这个包的过程中我被 npm 环境问题折磨过好几次这些报错看起来吓人其实多半是环境配置问题。我把最常遇到的几类整理成了一张速查表报错信息原因解决方案npm : 无法加载文件 npm.ps1因为在此系统上禁止运行脚本PowerShell 执行策略限制脚本运行以管理员身份执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser然后重开终端npm 不是内部或外部命令Node.js 未正确加入 PATH重新安装 Node.js 并勾选 “Add to PATH”或者手动把 Node.js 安装目录加入环境变量npm ERR! code CERT_HAS_EXPIRED镜像源证书过期或本地时间不正确先检查系统时间再执行npm config set registry https://registry.npmjs.org/切回官方源npm install卡住或超时默认源访问不稳定切换为访问速度更快的公共镜像源或使用公司内部私有源CERT_HAS_EXPIRED这个错误我想多说一句。我遇到的情况是某个旧镜像源的证书过期了请求registry.npm.taobao.org时直接报错。这种问题不一定是代码引起的第一步应该是检查本地系统时间如果时间不对所有证书校验都会失败。时间没问题的话执行npm config get registry看看当前用的是哪个源再决定是切换还是清除缓存。关于 PowerShell 执行策略有人可能会问为什么要用RemoteSigned而不是Unrestricted。RemoteSigned的意思是本地创建的脚本可以运行从网上下载的脚本必须经过签名才能运行。这个限制级别既满足开发需求又保留了基本的安全防护是当前场景下最稳妥的选择。如果你只需要当前用户生效一定要加-Scope CurrentUser别全局修改。4.2 发布 npm 包时容易踩的坑发布流程本身不复杂但有几个坑非常隐蔽。第一次发布时我连续踩了三个这次全部提前规避了。第一个坑是包名冲突。npm publish的时候如果提示403 Forbidden多半是这个包名已经被别人占用了。先执行npm view 包名查一下如果返回一堆信息说明名字已存在换一个名字或者改成你的用户名/包名的作用域包。第二个坑是忘记登录。新终端里执行npm publish提示ENEEDAUTH或者 401说明你还没登录。执行npm login输入用户名、密码和邮箱即可。这里有个细节如果你配置了非官方镜像源npm login会往那个源的服务器上登录可能会导致登录信息不对。稳妥做法是先切回官方源再登录。第三个坑是版本号忘记更新。npm 不允许用相同的版本号重复发布第二次发布时如果不手动更新package.json里的version会直接报错。我现在的习惯是在prepublishOnly脚本里加一个版本检查同时写代码时尽量用npm version patch这种命令来提升版本号这样package.json和 git tag 会一起更新避免忘记。4.3 排版规则处理文本时的三个大坑这个部分是纯业务层面的经验就算你不用 SDD 也能直接用上。第一个坑是正则对代码块的破坏。如果你不先保护代码块空格规则会把代码块里的字符串、注释全部按照中英文规则改一遍轻则格式乱掉重则改变代码含义。占位符机制就是用来解决这个问题的一定要在最开始做而不是最后兜底。第二个坑是引号处理。把直引号转成弯引号看起来简单实际非常容易误伤。比如英文中的撇号和引号是同一个字符规则一不小心就会把dont改成don’t这可能不是用户想要的。英文文本和中文文本混在一起时这种误伤很难通过简单正则完全避开。我的处理方式是让引号规则默认只处理中文字符前后的引号英文内部的撇号保持不变同时提供一个高级选项让用户按需开启更激进的转换。第三个坑是连续数字的处理。规则里“中文和数字之间加空格”但遇到iPhone15Pro这种词数字 15 夹在字母中间如果正则写得粗糙会被拆成iPhone 15 Pro这显然不是想要的。我在 spec 里明确规定只处理“中文直接相邻数字”的场景字母内部的数字不动。这类边界情况必须在 spec 阶段就写清楚否则 AI 实现出来一定会踩。5. 实操体会与后续计划做完这个包我最大的感受是SDD 和 AI 协作开发真正改变的其实是开发者时间的分配方式。以前写一个工具一半时间在写代码另一半时间在纠结“这个行为到底该怎么定义”。现在有了 AI写代码的时间被大幅压缩我在定义规范和 review 边界上花的时间反而更多了。但这不是坏事因为把问题想清楚本身就会减少返工而且这部分的思考很难被 AI 替代。对这个包本身我后续有几个明确的扩展方向。一是把规则引擎抽出来支持用户通过配置文件自定义规则这样不光我自己的排版习惯能用其他人也可以按自己团队的规范来。二是增加一个 Web 演示页面把命令行工具做成一个可以粘贴文本、实时预览效果的网页方便不熟悉命令行的内容创作者使用。三是研究一下要不要接入 Markdown AST 解析器虽然目前零依赖方案够用但如果要支持更复杂的语法保护引入 AST 会是更稳妥的路线。最后再分享一个小技巧开发这类发布到 npm 的小工具时无论你用不用 SDD都建议先把README.md的“快速开始”部分写好哪怕功能还没实现。因为 README 里写清楚“安装后输入什么命令、看到什么输出”就是在用最简单的方式定义验收标准。等代码写完了把 README 里的命令原样跑一遍整个工具好不好用立刻见分晓。这次我就是先写了 README 再开始写代码后面所有实现都在围绕 README 里承诺的能力服务基本没跑偏。
返回列表