ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用SDD驾驭AI Agent:npm排版包开发全记录

用SDD驾驭AI Agent:npm排版包开发全记录 我这段时间用 AI Agent 写代码的频率很高但说实话翻车次数也不少。一开始以为 AI 编程就是描述需求 → 复制代码 → 跑起来结果项目一大Agent 就开始自由发挥需求变来变去代码越改越乱。后来我把工作流切换到SDDSpec-Driven Development规格驱动开发重新做了个小工具——一个排版用的 npm 包整个过程完全换了个感觉需求先写成 specAgent 照着 spec 干活每一步都有验收标准开发效率和个人心态都稳了很多。这篇文章想跟你聊聊我是怎么用 SDD 完成一个 npm 排版包的包括我整理的六步实践指南、三层规格框架怎么落地、以及发布 npm 包时踩过的那些坑PowerShell 报错、证书过期、镜像源、publish 权限这些。如果你也在用 AI 写代码或者准备发布自己的第一个 npm 包这篇应该能帮你省不少时间。1. 为什么我会用 SDD 来做一个排版 npm 包1.1 先说结论SDD 是 AI 协作开发的正解现在的 AI 编程很多人还停留在vibe coding的状态想到什么就让 AI 写什么代码能跑就算赢。这种模式做原型很爽但做到第两天你就会发现AI 生成的代码像一盘散沙每次都重新理解需求改了一个地方另一个地方又崩了。我自己的体验是AI Agent 的智商取决于你给它的上下文约束而不是你的想象力。你描述得越模糊它发挥的空间越大出 bug 的概率就越高。SDD 正是对症下药先写一份明确的规格说明spec规定清楚输入、输出、边界条件、错误处理、验收标准再让 AI 按 spec 一步步实现。这不是限制 AI而是给 AI 画了一条清晰的跑道让它跑得快还不会跑偏。这次做排版 npm 包我最大的感受是有了 specAI 写出来的代码终于不是能跑而是可靠了。整个项目从初始化到发布中间几乎没有出现需求回流。1.2 什么是 SDD规格驱动的三层分类框架说到 SDDThoughtworks 的工程师 Birgitta Böckeler 提过一个三级分类框架我觉得特别有启发。她的大概思路是在 AI 协作开发中规格不能是一个笼统的东西要分成三个层级来写。需求规格What产品层面描述用户要什么解决什么问题不涉及具体技术。比如一个能自动修复 Markdown 文档排版问题的 CLI 工具。设计规格How技术层面描述用什么方案实现包含接口定义、数据结构、算法选型。比如采用 Node.js 实现对外暴露一个format(content: string): string函数内部通过正则和 AST 处理文本。任务规格Do执行层面把设计拆成 AI 可以一次完成的小任务每个任务有明确的输入、输出和验收标准。比如任务 1实现中英文之间加空格的正则逻辑通过以下 10 个单元测试。这三个层级对应到实际开发里就像建房子的图纸 → 结构设计 → 施工单需求规格管方向设计规格管方案任务规格管落地。AI Agent 真正执行的是任务规格但它依赖前两层做上下文否则就会在细节里迷失方向。1.3 排版包这个项目为什么天然适合 SDD你可能也会问为什么偏要选一个排版工具来试 SDD我当时的判断是排版工具规则明确、边界清晰、可测试性强是所有项目中最适合验证 SDD 工作流的类型。规则明确中英文之间加空格、标题层级不能跳级、代码块要有语言标注——这些都是可以量化的规则写进 spec 不会有歧义。边界清晰输入是一个字符串输出也是一个字符串没有外部依赖不需要处理数据库和网络Agent 可以专注实现逻辑。可测试性强每个规则都能写成单元测试验收标准非常客观。AI 写得好不好跑一遍测试就知道了。相比之下如果你拿一个社区论坛后端来测试 SDD复杂度会高很多因为涉及状态管理、权限、数据库事务spec 写起来本身就是巨大的工程。所以我建议想尝试 SDD 的朋友也从一个边界清晰的命令行工具开始先把工作流跑顺了再挑战更复杂的项目。2. 项目设计SDD 六步把需求变成可执行的 spec2.1 第一步把模糊需求固化成产品规格我在做之前需求其实很模糊我想做个好东西。后来强制自己用 SDD 的第一层——需求规格——把它写清楚才意识到把需求写出来本身就是一次深度思考。我最后写的需求规格长这样节选# 需求规格md-layout-cli ## 目标用户 - 在 GitHub 上维护技术文档的开发者 - 写作包含中英文混排内容的作者 ## 核心痛点 - 中英文之间缺少空格阅读体验差 - Markdown 标题层级随意跳级文档结构混乱 - 代码块没有语言标注高亮失效 - 段与段之间换行不一致diff 很难看 ## 功能需求 1. 自动在中英文、中文与数字之间插入空格 2. 规范化 Markdown 标题层级自动纠正跳级 3. 检查并补齐代码块语言标注 4. 统一换行符为 LF 5. 支持 CLI 单文件处理和 npm API 调用这一步不涉及任何技术实现纯粹回答这个工具要解决什么问题。但你会发现写到这里你已经把排版包从模糊概念变成了两个明确角色用户、作者和四类明确功能。我给 AI 的第一个指令就是阅读这份需求规格然后复述你对项目的理解——它会帮你检查需求有没有漏洞。2.2 第二步把产品规格拆成技术决策需求规格定完后紧接着写设计规格。这一步要回答用什么技术方案实现。我的选择如下运行时Node.js 18。原因是 CLI 工具生态成熟发布 npm 包最方便而且我可以直接用内置的node:test做单元测试不用额外装测试框架。入口设计同时提供format(content)函数作为 API和bin命令作为 CLI。核心实现用 Unicode 正则匹配 CJK中日韩统一表意文字、假名、谚文与拉丁字母/数字之间的边界用行级扫描处理 Markdown 标题层级。零依赖这是给自己定的硬约束。排版工具的核心逻辑很简单引入依赖会增加安全风险和下载成本这一点写进 spec 后AI 就没有自作主张去装chalk之类的库。设计规格里我还会写明不做什么不处理图片路径、不重构段落结构、不转成 HTML。这些边界声明特别重要否则 AI 很容易在一个排版工具里给你夹带一个 Markdown 解析器。2.3 第三到第六步任务拆分、验收标准、排期与反馈闭环在三级框架里任务规格是 AI 真正执行的东西。按照六步实践法后面的几步落到实操是这样第三步任务拆分。把设计规格拆成 4 个任务task-1初始化 npm 包结构和 package.jsontask-2实现格式化核心函数task-3实现 CLI 命令行入口task-4编写单元测试和 README。每个任务都标注了涉及文件、依赖关系和执行顺序。第四步验收标准。每个任务必须附带验收条件。例如task-2的验收条件就是运行node --test后12 个测试用例全部通过。我在 spec 里明确规定验收不通过Agent 必须自己修复不允许跳过或修改测试。第五步开发排期。不是人类排期而是给 Agent 的执行顺序约束。先做任务 1再做任务 2绝不能乱。很多 AI 翻车就是因为一次让它做太多事上下文塞爆前后逻辑对不上。第六步反馈闭环。每个任务完成后我会让 Agent 先自测再把 diff 发给我评审我反馈意见后它再进入下一个任务。SDD 不是甩手掌柜人还是要保留最终审核权。这套流程跑下来我发现 AI 写的代码质量有明显提升原因是它每一次只需要专注一件事而且有明确的完成定义不会再出现那种写到一半突然重构另一个模块的情况。2.4 工具选型Claude Code 与 Codex 的配合方式既然是 SDD AI 开发工欲善其事必先利其器。我用的是两个工具Claude Code用来做日常的代码生成、重构、解释。它对长上下文的处理比较稳能记住项目结构和 spec 细节适合在一个会话里连续完成多个任务。Codex CLI用来做交叉验证和快速实验。我会让它独立实现同一个任务然后对比两边的实现思路选出更稳的方案。安装这两个工具的命令刚好也踩过不少坑放到后面常见问题里一起说。这里提一句如果你也打算用命令行版本的 AI 工具安装完成后先跑一下ai --version确认环境 OK再进项目目录启动否则容易遇到命令找不到的困扰。3. 实操记录从 spec 到 npm 包发布3.1 项目初始化package.json 与目录结构按任务 1 的要求我先让 AI 初始化项目生成结构如下md-layout-cli/ ├── package.json ├── bin/ │ └── md-layout.js ├── src/ │ ├── formatter.js │ └── headings.js ├── test/ │ └── formatter.test.js ├── spec/ │ ├── requirements.md │ ├── design.md │ └── tasks.md └── README.md这份package.json是 AI 生成后我调整过的关键字段在这里{ name: md-layout-cli, version: 0.1.0, description: A lightweight markdown layout formatter for Chinese-English mixed text, main: src/formatter.js, bin: { md-layout: ./bin/md-layout.js }, files: [ src, bin, README.md ], scripts: { test: node --test test/, publish:patch: npm version patch npm publish --access public }, engines: { node: 18 }, license: MIT }值得强调files字段。不过我没急着在这一步让它跑npm pack先继续写核心逻辑。3.2 核心实现中英文混排格式化逻辑任务 2 是核心。我让 AI 基于设计规格里定好的正则规则实现src/formatter.js。最终保留下来的核心函数长这样// src/formatter.js const CJK_REGEX /[\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af]/; const LATIN_OR_DIGIT_REGEX /[A-Za-z0-9]/; // 核心在中英文/数字之间插入空格 function addSpaceBetweenCJKAndLatin(text) { return text .replace(/([\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af])([A-Za-z0-9])/g, $1 $2) .replace(/([A-Za-z0-9])([\u4e00-\u9fa5\u3040-\u30ff\uac00-\ud7af])/g, $1 $2); } // 核心规范化 Markdown 标题禁止跳级 function normalizeHeadings(text) { const lines text.split(\n); let lastLevel 0; const result lines.map((line) { const match line.match(/^(#{1,6})\s(.*)$/); if (!match) return line; const hashes match[1]; let level hashes.length; if (lastLevel 0 level lastLevel 1) { level lastLevel 1; } lastLevel level; return ${#.repeat(level)} ${match[2]}; }); return result.join(\n); } function format(text) { let output text.replace(/\r\n/g, \n); output addSpaceBetweenCJKAndLatin(output); output normalizeHeadings(output); return output; } module.exports { format, addSpaceBetweenCJKAndLatin, normalizeHeadings };这里有两个小心思值得展开CJK 范围我故意包含了汉字、日文假名、韩文谚文。很多排版工具只处理汉字但真实文档里可能出现日文注释所以一处正则三种文字都覆盖了。标题跳级修复逻辑默认规则是#标题不能从一级直接跳到三级。实现方式是记录上一个出现的标题级别lastLevel当下一个标题级别大于lastLevel 1时强制定级为lastLevel 1。这个逻辑简单但足够解决 90% 的手写 Markdown 跳级问题。在实现任务 2 的过程中AI 曾自作主张加了一个英文首字母大写的功能被我一票否决了。因为 spec 里明确写了不改变语义内容只处理排版英文首字母大写是内容层面的修改不是排版。这也是为什么我反复强调不做什么一定要写进规格。3.3 测试先行让 Agent 自己给自己验收任务 4 是测试。SDD 的优点在这里体现得最明显因为 spec 里已经定义了验收标准测试用例写起来就像抄答案。我让 AI 根据四个功能需求写出了对应的测试// test/formatter.test.js const test require(node:test); const assert require(node:assert/strict); const { format } require(../src/formatter); test(中英文之间插入空格, () { assert.equal(format(我喜欢JavaScript), 我喜欢 JavaScript); assert.equal(format(Node.js真好用), Node.js 真好用); }); test(中文与数字之间插入空格, () { assert.equal(format(我已经用了3年), 我已经用了 3 年); assert.equal(format(支持React18), 支持 React 18); }); test(标题跳级自动修正, () { const input # 一级标题\n### 三级标题; const output format(input); assert.equal(output, # 一级标题\n## 三级标题); }); test(统一换行符, () { assert.equal(format(a\r\nb), a\nb); }); test(不改变已有空格, () { const input 我喜欢 JavaScript 和 Vue; assert.equal(format(input), 我喜欢 JavaScript 和 Vue); }); test(混合内容集成测试, () { const input # 介绍\n大家好这是Node.js实战项目版本为18。\n### 二级标题; const output format(input); assert.match(output, /大家 好这是 Node\.js 实战项目版本为 18。/); assert.match(output, /## 二级标题/); });这里我犯过一个典型错误最后一条集成测试把大家好之间的空格也断言了。实际上 大家 和 好 都是中文正则不会在它们之间加空格。我第一次跑就红了AI 一脸无辜后来我仔细一看是自己把测试写错了不是逻辑写错了。这也是一个提醒AI 生成的测试用例人一定要看一遍不是能跑就行要确认它测的东西真的符合预期。跑测试是npm test或node --test test/。看到全部绿色我才会同意 Agent 进入下一个任务。3.4 打包发布npm publish 全流程任务收尾项目能跑了接下来是发布 npm 包。整个流程我拆成这几步登录 npmnpm login输入用户名、密码和一次性验证码。这里要注意npm 现在推荐使用 token 登录npm 官网 setting 里可以生成。确认版本号npm version patch会把版本号从0.1.0升到0.1.1并自动打 git tag。本地打包预览npm pack会生成一个.tgz文件可以用npm install -g ./md-layout-cli-0.1.1.tgz先全局安装测试一遍。这一步绝对不要跳我见过太多人直接npm publish结果包结构不对装下来根本不能用。正式发布npm publish --access public。如果你是第一次发公开包必须加--access public否则作用域包默认是私有的会报错。我专门写了publish:patch这个 scriptpublish:patch: npm version patch npm publish --access public一条命令完成版本升级、git tag 和发布。后来还做了别名publish:minor和publish:major分别对应小功能和大版本升级。发布完成之后可以用npm view md-layout-cli检查包是否存在也可以到 registry 页面看详情。第一次看到自己的包出现在 npm 上那种感觉还是很奇妙的——尤其是这个包从需求到实现有一半是 AI 写的而整个流程的掌控感却比以前自己手写代码还要强。4. 踩坑实录npm 工具链的常见问题与排查4.1 PowerShell 禁止运行脚本如果你在 Windows 上开发几乎必然遇到这个报错npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。我第一次看到这个直接懵了明明 Node.js 装好了为什么npm不能运行原因是 PowerShell 的执行策略默认是Restricted禁止运行任何 PowerShell 脚本而npm.ps1正好是一个脚本文件。解决办法是打开 PowerShell以当前用户身份放开脚本执行权限Set-ExecutionPolicy -Scope CurrentUser RemoteSignedRemoteSigned的意思是本地脚本可以运行从网络下载的脚本必须有数字签名。这样既解决了问题又不会把系统改成完全无限制模式。改完后重启终端npm --version就能正常输出了。如果你见到CommandNotFoundException sqlpackage.exe或者与npm不同的另一个报错往往是环境变量 PATH 没配置好。确认node安装目录通常是C:\Program Files\nodejs\已经加入系统 PATH并在新开的终端里测试不要用旧终端。4.2 证书过期与镜像源配置在开发过程中我遇到过一条很经典的安装报错npm ERR! code CERT_HAS_EXPIRED npm ERR! request to https://registry.npm.taobao.org/xxx failed, reason: certificate has expired原因是老版本的淘宝镜像域名registry.npm.taobao.org证书已经过期而很多教程还在让人配这个源。新版的镜像域名改成了registry.npmmirror.com如果你还在用老地址自然就会遇到证书错误。处理方式有两种使用国内镜像新域名npm config set registry https://registry.npmmirror.com使用官方源npm config set registry https://registry.npmjs.org/我个人建议能上官方源就用官方源体验最稳定发布包也必须在官方源操作。国内镜像适合加速下载依赖但如果你要npm publish记得先把源切回官方。npm config get registry可以随时查看当前源地址。还有个隐藏问题即便你改回了官方源npm 的缓存里可能残留了旧证书的元数据这时候建议清理缓存再重试npm cache clean --force4.3 install 报错与依赖清理开发过程中我用npm install -g 某个工具的方式给全局装了 AI 编程相关的命令行工具好几次遇到 EACCES 权限报错。这是 macOS/Linux 上全局安装的经典问题原因是全局目录需要 root 权限。网上有些教程让你加sudo我强烈不建议因为用 root 运行 npm 脚本会带来安全风险而且之后每次升级包都要 sudo。更好的方式是通过 nvm 来管理 Node.js这样全局安装目录就在你的用户目录下不需要任何额外权限curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash nvm install --lts nvm alias default 版本号装完 nvm 之后重新打开终端npm install -g再也不会报权限错误了。这也顺带解决了找不到 npm 命令的问题因为 nvm 会把 Node.js 路径写进你的 shell profile。另外还有一个高频问题npm install出现EINTEGRITY报错。这通常是缓存损坏或锁文件与 package.json 不同步导致的。先删掉node_modules和package-lock.json再执行npm cache clean --force最后重新npm install九成情况都能解决。4.4 pack 与 publish 阶段的坑本地开发一切正常发布才是最容易翻车的地方。我总结了三个高频坑包名冲突npm 上的包名是全球唯一的。如果你想起的名字已经被占了npm publish会直接报403 Forbidden或You cannot publish over the previously published versions。发布之前先跑一下npm view 你要用的包名如果返回 404 或者连个ETIMEDOUT都没有说明这个包名可能可用如果返回了版本信息就得改名。文件缺失很多人npm publish发布完装下来发现README没带进去或者源码路径不对。这就是files字段没配好。我在package.json里配了files后每次发布前还会跑npm pack --dry-run它会列出即将被打进 tar 包的所有文件清单确认一遍再发布能省不少事。force 滥用有时候发布报错网上有人会建议npm publish --force。这个 flag 的官方语义是忽略某些检查包括可能覆盖掉已有版本。除非你明确知道自己要做什么否则永远别用--force发布。我见过有人用--force把一个线上包的旧版本覆盖掉了导致某个团队依赖的版本突然消失事故现场非常惨烈。还有一个提醒npm version会自动修改package.json版本号并创建 git commit 和 tag。如果你没有配置好 git 用户信息这条命令会报错。先在项目里确认git config user.name和user.email有值再跑npm version patch否则要先git config user.name 你的名字 git config user.email 你的邮箱写在最后SDD 改变了我的开发习惯这次用 SDD 做排版 npm 包给我最大的启发不是 AI 多厉害而是写清楚本身才是核心竞争力。以前我总以为 AI 编程的核心是提示词技巧后来发现提示词技巧只是表面真正的底座是你对问题的定义能力。spec 写清楚了AI 就是你的超级执行者spec 写得稀烂AI 就是一台 Corrupted 的自动生成 bug 机器。我现在连平时的非 AI 项目也会先写一版简短的 requirements 再动手。这样做的额外好处是写完 spec 之后我常常会发现有些需求根本不值得做或者有更好的实现方式避免了一言不合就写代码的冲动。最后再分享一个小技巧写 spec 时一定要有一个单独的段落叫边界与不做项。允许 AI 做什么写了之后再把绝对不做什么写清楚比如不引入额外依赖、不改动语义、不重构无关代码。这条约束帮我挡掉了无数次 AI 的自由发挥你也可以试试。
返回列表