SDD+AI Agent实战:从规格到npm包的高效开发全流程

发布时间:2026/9/8 19:55:54
SDD+AI Agent实战:从规格到npm包的高效开发全流程 这阵子我试了一套很有意思的开发方式一个需求用传统方式做大概要两天这次一个下午加一个晚上就搞定而且质量比我预期的高不少。核心就是把之前靠感觉、靠白板、靠嘴上说的需求整理过程变成一份机器和人都能读懂的规格说明再用 AI Agent 按规格把代码填出来整个交互过程就是 SDDSpec-Driven Development规格驱动开发。我不是说 AI 能替你写代码就完事了真正值钱的是把这个过程变成可审计、可回滚、可复现的。刚好最近我用这套思路做了一个排版用的 npm 包从规格、编码、测试到发布全过程夹着不少坑今天一次性讲清楚。1. 从 vibe coding 到 SDD为什么我觉得“直接让 AI 写”不够用先说背景。之前有一段时间我特别沉迷 vibe coding需求描述几句话往编辑器里一贴Agent 吭哧吭哧生成几百行代码看着挺爽。但用了几次就发现问题代码能跑但漏洞特别多而且很多漏洞不是语法层面的是需求理解层面的。我让它做一个“排版工具”它给我做成了“纯文本处理工具”完全没考虑终端 ANSI 颜色序列、中英文混排的宽度差异、表格列宽自适应这些真实场景里的硬需求。后来我研究了不少 AI 工程化实践看到一个概念让我挺有共鸣——国外有工程师提过 SDD 的三级分类框架。大致是这么分的第一级纯 vibe coding需求在脑子里AI 靠提示词自由发挥第二级有规格至少把输入、输出、边界条件写清楚AI 按规格实现第三级被约束的生成harnessed不仅写规格还有验证方法、测试套件、性能约束AI 的生成结果会被自动校验。我第一次看到这个分类的时候心里就说这不就是我一直想做的事吗我之前大部分时间都处在第一级偶尔运气好到第二级。真正稳定、可复用的 AI 协作开发必须是第三级。所以这次我做这个 npm 包一开始就给自己定了规矩先写规格再谈代码。而且规格不是我拍脑袋写是需要把“什么样算排版正确”这个模糊概念变成一个可验证的规格集合。1.1 为什么 SDD 特别适合工具类 / 库类项目可能有人会问SDD 适合所有项目吗我的经验是特别适合那种行为边界清晰、输入输出可以定义的库和工具类项目。比如我这个排版 npm 包本质上做的是数据转换工作输入字符串和配置参数输出排版后的字符串。这类项目非常适合规格驱动。为什么因为它的行为是可枚举的。你可以非常精确地说表格单元格内容超过列宽时怎么办中文按两个字符宽度计算颜色符号不能被计算成可见宽度。这些都能写成测试用例而测试用例就是规格的活体版本。反过来那种探索性很强、交互流程特别长的产品功能比如做一个复杂的 React 页面SDD 也能用但规格会写得比较累视觉交互部分很难完全用文本定义。所以我的建议是第一回试用 SDD建议从库、CLI、工具函数这类项目开始见效最快打击感最爽。1.2 SDD 不是“AI 帮我写代码”而是“我先定义正确”我见过太多人把 SDD 理解成“写提示词”。其实差的不是提示词怎么组织而是你有没有能力在没有 AI 的情况下把需求说成一条条精确的规格。举个具体例子。做排版包我第一个要定义的是“命令行表格”长什么样。这不只是画个框的问题你得定义边框用哪些字符、列和列之间空几格、内容超长是截断还是换行、数字列右对齐还是左对齐。每个问题都是一条规格每条规格最终对应一个或多个测试用例。写规格是这个项目里最花时间、也最值钱的部分。AI Agent 真正发挥效率是在规格写清楚之后它只需要做一件事把这些规格翻译成高质量的 TypeScript 代码并且不偏离规格的语义。2. 项目立项与规格设计这个 npm 包到底要做什么我这次做的东西对外叫排版工具实际上是一个同时支持终端输出格式化和文本对齐的库外部形态是 npm 包同时附带一个简单的 CLI。名字不说了避免广告嫌疑功能上主要解决这几类问题把二维数组输出成对齐良好的终端表格对一段文本做左对齐、右对齐、居中和两端对齐自动换行要求中文按两个西文字符宽度计算支持 ANSI 颜色序列计算宽度时能正确剥掉颜色码提供 CLI 命令支持从 stdin 读入文本处理后输出到 stdout。功能列出来后下一步不是写代码而是写规格。我整理了一个精简版规格片段展示一下大概的粒度。注意这些都是可以用测试验证的不是“系统应具备良好的用户体验”这种废话。2.1 规格片段渲染表格的行为定义输入是一个二维数组每一行的列数可以不一样第一行默认作为表头也可以配置是否忽略表头。规格要点如下表格列数为所有行中的最大列数每列宽度为该列所有单元格的“显示宽度”最大值加 2左右各一个空格列宽有上限和下限配置超宽截断并添加省略号表头行默认加粗底部输出分隔线支持配置对齐方式全局对齐、按列对齐、表头单独对齐。每一列宽度的计算不是简单地取字符串 length而是按显示宽度计算。显示宽度的含义是一个英文字母占 1一个中文汉字占 2ANSI 序列占 0。这个定义直接对应后面的核心算法所以在规格阶段必须明确。2.2 规格片段文本对齐和自动换行文本排版这块规格定义更细。比如两端对齐指在不超过指定宽度的前提下通过调整单词间空格数让每一行左右两端对齐最后一行保持左对齐。这里有个问题英文靠空格分词中文呢中文没有空格两端对齐如果按空格分词一行可能全是中文词排出来的效果和纯左对齐没差别。所以我定义了一条规则中文场景下按单个字符级粒度调整间距但必须在标点处禁止行首这个属于排版的基本规则。自动换行的规格是这样的优先在空格处断行空格断不了就按字符断行中文标点不能出现在行首断行后的文本行宽度不得超过设定宽度。这些规则看起来很基础但一旦写成规格Agent 生成的代码就不会跑偏。我实际测试时发现只要把这条标点禁则写清楚AI 生成的换行算法基本就是正确的。2.3 设计非目标Non-goals规格里最容易忽略的部分规格里还必须有“不做什么”的清单这个太重要了。我加了几条非目标不支持 Rich Text 格式解析、不支持表格单元格合并、不支持把 Markdown 解析成表格、不处理图片和链接。为什么要专门写这个因为 AI 生成代码时有个习惯喜欢顺手扩展功能。我遇到过一次我让它实现表格渲染它给输出结果套了一个 Markdown 形式还自作主张支持了 Markdown 表格语法解析。功能很炫但不是我要的反而增加了包的体积和复杂度。写上非目标后AI 这类发挥基本就绝迹了。规格写完我顺手整理了一份 SDD 六步实践指南核心流程是需求拆解、规格编写、规格评审、Agent 生成实现、自动化验证、人工 review 与迭代。后面几个章节我会按这个流程把这次实践拆开讲每一步都对应实际操作。3. AI Agent 协作实操从规格到核心算法的高效落地规格定稿后进入真刀实枪的阶段。我用的开发工具是支持 Agent 模式的编辑器背后挂的是最新的大模型接口。就实现方式来说关键不是选哪个模型而是你怎么把规格投喂给 Agent并要求它严格遵循。我一般把规格拆成多个小任务分步执行而不是一次性让 Agent 生成整个项目。我的做法是先让它搭项目骨架包括 package.json、tsconfig、目录结构、构建工具。这些不需要规格属于工程模板Agent 做得很快。然后是核心算法库的实现我会把规格逐条拆开喂进去每拆一条要求它补对应的测试。3.1 ANSI 宽度计算最容易翻车的地方排版第一个核心算法是显示宽度计算。终端里的字符串肉眼看到的宽度和 length 属性是两回事原因就在 ANSI 转义序列比如\x1b[32m是绿色开始\x1b[0m是重置。这些字符本身不显示但会被 JavaScript 的 length 计算在内。如果直接拿 length 去对齐彩色文字永远对不齐。我给的规格是先剥离 ANSI 序列再对剩余文本计算显示宽度。剥离用正则/\x1b\[[0-9;]*m/g这个是最常见的 ANSI SGR 序列覆盖颜色和样式。计算显示宽度时需要区分全角和半角。TypeScript 实现大致是这样export function displayWidth(input: string, ansiEnabled true): number { const text ansiEnabled ? input.replace(/\x1b\[[0-9;]*m/g, ) : input; let width 0; for (const ch of text) { const code ch.codePointAt(0)!; if (code 0x1100 ( code 0x115f || code 0x2329 || code 0x232a || (code 0x2e80 code 0xa4cf code ! 0x303f) || (code 0xac00 code 0xd7a3) || (code 0xf900 code 0xfaff) || (code 0xfe10 code 0xfe19) || (code 0xfe30 code 0xfe6f) || (code 0xff00 code 0xff60) || (code 0xffe0 code 0xffe6) || (code 0x1f300 code 0x1f64f) || (code 0x1f900 code 0x1f9ff) )) { width 2; } else { width 1; } } return width; }这段代码看着长本质就一句话常用全角字符区间返回 2其他返回 1。这个区间列表是参考 Unicode East Asian Width 标准整理的不是随便写的。实测下来对中文、日文假名、朝鲜文、Emoji 的处理都符合预期。Emoji 本身是可变宽度的这里按 2 处理在大多数终端里是合理近似。规格到这里还不够我还定义了padEnd、padStart、center这几个辅助函数的行为。它们的规格是补齐宽度时按显示宽度计算如果原文本显示宽度大于目标宽度直接返回原文本不做截断。截断单独由truncate函数负责而不是混在 padding 逻辑里这个职责划分能让 AI 生成的代码结构干净很多。3.2 自动换行算法空格优先、字符兜底、中文标点禁则自动换行是第二个核心算法。我的换行策略分三个层级在空格处断行空格不保留在行尾如果一个单词本身超过最大宽度只能强制按字符断行此时英文单词被拆开中文文本按字符断行但行首禁止出现特定标点比如逗号、句号、感叹号、问号、顿号、分号、冒号、右括号、右引号。第三点是中文排版的灵魂。没有这条规则AI 生成的换行算法处理中文时会在行首留下一个逗号这种排版错误特别显眼。规格里把这个禁则表列出来代码实现就是断行后检查一下首字符如果命中禁则表就把这个字符推回到上一行。const FORBIDDEN_LINE_START new Set([ , 。, 、, , , , , ”, 』, 》, , 】, …, —, ]); export function hardWrap(text: string, maxWidth: number): string[] { const lines: string[] []; let current ; for (const ch of text) { const next current ch; if (displayWidth(next) maxWidth current ! ) { if (FORBIDDEN_LINE_START.has(ch) lines.length 0) { // 把当前行塞回去让禁止行首的标点出现在上一行末尾 if (current.endsWith( )) { current current.slice(0, -1); } lines.push(current ch); current ; } else { lines.push(current); current ch; } } else { current next; } } if (current ! ) lines.push(current); return lines; }这段代码是给 Agent 看规格后它生成的初版我再 review 时调整了几处细节。比如处理中文标点禁则时原始版本直接把current ch一起 push 了没有去掉 current 末尾的空格结果上一行末尾多了一个空格视觉上有个小缺口。这就是为什么 SDD 的第四步「人工 review」不能省。规格能约束大体行为但这些细小的排版洁癖还是需要人眼把关。3.3 两端对齐的实现按空格扩展还是按字符扩展两端对齐在某些环境下争议很大因为有很多种实现策略。我用的是最经典的策略对已经切好的行计算该行显示宽度和目标宽度之间的差值把差值均摊到空格上空格多的多摊空格少的少摊。如果行内没有空格比如一行全是中文那就切到字符级在字符之间插入全角空格补齐宽度。这里有一个隐藏问题一行里正好有一个空格和一个超长单词时均摊逻辑会出问题。所以我规格里明确写了单空格行不做均摊直接保持左对齐否则单个空格会被拉成一大段空白视觉上非常怪异。这又是一条只有真实排版需求才会想到的场景AI 自己大概率不会主动处理。Agent 生成的两端对齐初版在英文文本上表现不错但中文文本会插入半角空格来对齐效果很丑。我后续补了一条规格中文场景下优先用全角空格补齐因为半角空格在混排时宽度不对。定了这条之后测试用例也同步补上Agent 再迭代版本时就稳定了。4. 测试、调试与 npm 发布流程SDD 的第五步是自动化验证第六步是发布交付。我对自动化验证的理解是测试用例就是规格的实体化测试通过规格才算实现。所以每一条规格都要有测试用例覆盖这个项目我一共写了 60 多个测试覆盖宽高计算、换行、对齐、表格渲染、命令行输出等。4.1 测试策略单元测试为主、快照测试辅助测试框架选了 Vitest原因就是快TypeScript 支持好和 Vite 生态天然配合。主要分成两类纯函数单元测试直接测 displayWidth、wrap、align、renderTable 这类函数给输入断言输出快照测试对比较复杂的表格渲染输出做快照后续改动一眼能看出影响。表格渲染的测试很有意思。它生成的是一大段字符串包含制表符、空格、ANSI 码人工逐个字符断言太痛苦。快照测试在这里非常合适第一次运行生成快照我人工确认快照内容正确之后每次改动只需要对比快照差异。有一次 Agent 重构了 padding 逻辑快照测试立刻显示出所有表格右边框空了一格问题定位几乎零成本。4.2 调试 Chrome 页面和 CLI 输出这个项目有一个比较特殊的调试需求生成的表格不仅在终端显示还要嵌入到 Web 环境里供浏览器页面调用。所以测试覆盖了纯 Node 环境也要覆盖浏览器环境。我在这里用了一个调试工具链通过 Chrome DevTools 调试协议辅助定位问题。具体做法是用官方提供的 chrome-devtools-mcp npm 包在本地启动一个 stdio server把它作为一个 MCP 工具接入到 Agent 对话里。这样 Agent 在开发过程中可以直接连接一个 Chrome 实例实时执行 JavaScript、查看页面布局、检查样式。这个能力对排版调试太关键了。比如我怀疑表格在 web 端显示时每个单元格宽度比终端里多出一个像素这个用纯 Node 测试根本发现不了但 Agent 通过调试协议打开页面直接读 getBoundingClientRect 的结果几秒钟就找到了原因CSS 的 white-space 属性没设置导致连续空格被折叠。这种跨环境的问题日志打一百遍都不如实际连上浏览器看一眼。4.3 发布 npm 包从版本号到真实下载量发布 npm 包的流程本身不复杂但有几个点值得讲。首先包名要在 npm 上搜一下重名非常常见。我这次也踩了起好的名字一查已经被占用了最后加了 scope 前缀变成scope/package-name的形式。版本号我严格按照语义化版本管理初始开发用 0.1.0所有公开 API 定稿后升 1.0.0之后每次破坏性变更升 minor修复 bug 升 patch。AI 自动生成代码时常会改动公开 API所以每次 Agent 迭代完我都会跑一遍npm run typecheck再检查导出的类型定义是否有变化避免发布出去别人一升级就崩。构建和发布脚本我是这样组织的{ scripts: { build: tsup, test: vitest run, typecheck: tsc --noEmit, prepublishOnly: npm run typecheck npm run test npm run build }, files: [dist] }prepublishOnly这个钩子是保障发布之前强制跑类型检查、测试和构建任何一环挂了都不能 publish。这样就不会出现把 TypeScript 源码直接发出去、或者测试失败还强行发布的情况。files字段限定只发布 dist 目录package.json、README 是 npm 自动带的其他乱七八糟的文件不会进包。关于发布本身我再说一个经验把包推上 npm 不是终点而是维护责任的开始。我发布后第二天就收到一个 issue用户反馈表格的右边框在某些终端里显示错位。排查后发现是终端的 Unicode 渲染宽度和我的计算值不一致具体是某些符号在特定终端里被当成双倍宽度。最后我加了一个配置项允许用户手动指定宽度表问题解决。这种用户反馈是纯靠测试用例覆盖不了的SDD 的规格也会跟着用户场景不断进化。5. 常见问题与排查技巧实录最后这部分是我踩过的一堆坑的合集不一定都和 AI 有关但每一个都在 SDD 协作过程里冒出来过整理成一个速查表方便大家直接用。现象根因排查与解决表格列宽忽大忽小列宽计算用了字符串 length没算显示宽度全局替换为 displayWidth 函数并补测试彩色文本对不齐ANSI 码被计入显示宽度剥离 ANSI 序列后再计算宽度注意正则覆盖 SGR 全部模式换行后行首出现中文逗号没加标点禁则逻辑在 hardWrap 中维护 FORBIDDEN_LINE_START 集合并回退上一行两端对齐后单词之间空格过大对单个空格的行强行均摊单空格行直接返回不做均摊web 端表格显示错位white-space 默认折叠连续空格设置 white-space: pre或在容器上使用等宽字体AI 生成代码多用了一层深拷贝规格里没写性能要求规格补充“对输出为只读视图不改变输入引用”回头测试一下输入对象未 mutate发布后用户终端渲染不一致终端对 Unicode 宽度判断不同暴露宽度表配置项允许用户自定义Agent 顺手实现了 Markdown 解析规格里缺非目标声明写规格时必须包含 Non-Goals 清单在这里多说两句 AI 协作的体会。经常有人问我AI 生成代码质量不稳定怎么办。我发现大部分不稳定都不是模型问题而是规格不清晰。你把规格写到“这个函数接收两个参数返回一个数组数组元素类型为接口 X”这个粒度AI 基本不会发挥。真正会发挥的地方一定是你没写清楚的地方。与其抱怨 AI 太自由不如反思是不是规格太宽松。SDD 的价值就是把不确定性前移在编码之前解决。用一句话总结我的感受以前是想到哪写到哪现在是先定标准再施工AI 只是施工队规格才是图纸。这个排版包目前已经发布到 npmstar 不算多但真实使用中反馈还行。我下一步打算在规格里把性能预算也写进去比如渲染一万行表格的时间上限让测试去兜底。这又是 SDD 往下走的一个方向把非功能需求也规格化。对我来说这比单纯堆功能更让我安心。