SDD Training
这是公司的SDD Training经过AI脱敏后总结出来的教程,供参考。
从项目提案到可验证的代码:一份可动手的 SDD 与 Harness 教程
当我们把一个模糊的想法交给 AI 编程助手,最容易丢失的不是代码,而是对需求的澄清、实现时的取舍和「到底验证了什么」。Spec-Driven Development(SDD,规格驱动开发)把这些信息写成可检查的产物;Harness Engineering 则设计 AI 使用这些产物的流程、权限与检查点。
可以把它们记成一句话:代码是当前状态,Spec 是目标,Plan 是路径,Harness 是护栏。 Harness 可以是可复用的提示词、项目规则、按需加载的技能、专职子任务或验证工具;不需要一开始就把所有组件配齐。下面从一个能运行的小项目开始,一步步加上真正需要的约束。
本文不依赖特定 AI 编辑器、社区 skill、浏览器插件或 MCP。需要一个能读取和修改本地文件、运行终端命令的 AI 编程助手。文中的「交给助手」均指在该助手的普通聊天窗口粘贴指令并附上所需文件;如果它没有子代理或浏览器工具,可以按文中的人工替代步骤继续。不要在不可信的代码库中无条件批准终端操作。
0. 准备一个可运行的起点
准备 Node.js 22 或更新版本,并检查 node --version 和 npm --version。在任意新目录 sdd-lab 中,创建下面的文件;本教程所有路径都相对于该目录,终端也从这里执行。代码不依赖第三方包,首次运行无需下载依赖。
目录结构:
sdd-lab/
package.json
proposal.md
server.js
public/index.html
tests/health.test.js
context/ # 后续由助手创建子目录和文档
.ai/prompts/ # 后续保存可复用的入口指令
package.json:
{
"name": "sdd-lab",
"private": true,
"type": "module",
"scripts": {
"dev": "node server.js",
"test": "node --test"
}
}
server.js:
import { createServer as createHttpServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
export function createServer() {
return createHttpServer(async (req, res) => {
if (req.method === 'GET' && req.url === '/api/health') {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.method === 'GET' && req.url === '/') {
const html = await readFile(new URL('./public/index.html', import.meta.url));
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
return;
}
res.writeHead(404);
res.end('Not found');
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
createServer().listen(3000, () => {
console.log('Open http://localhost:3000');
});
}
public/index.html:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>多人分账练习</title>
</head>
<body>
<main>
<h1>多人分账</h1>
<p>项目已启动;功能将在教程中逐步实现。</p>
</main>
</body>
</html>
tests/health.test.js:
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { createServer } from '../server.js';
test('health endpoint', async () => {
const server = createServer();
await new Promise((resolve) => server.listen(0, resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/api/health`);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), { ok: true });
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
proposal.md 是本次练习唯一的原始业务输入。刻意留一些待澄清问题,而不是预先把所有答案写好:
# 多人分账小应用:项目提案
朋友聚餐后,需要知道谁垫付了钱,以及每人应付多少。
目标:用户可以创建小组、添加成员、记录某位成员支付的费用、
按参与人均摊费用,并查看各成员余额及建议转账。
第一版不做账户、支付、邀请链接、百分比拆分或云端部署。
界面要能在手机和桌面浏览器使用。
待讨论:同名成员如何处理?金额如何舍入?刷新页面后数据是否保留?
先运行 npm test,应看到 1 个测试通过;再运行 npm run dev,打开 http://localhost:3000,应看到占位页面,访问 http://localhost:3000/api/health 应得到 {"ok":true}。开发服务器会持续运行,执行测试请用另一个终端。端口 3000 被占用时,先停止占用它的进程;本例故意固定端口以方便验收。
后续功能涉及金额与数据持久化:建议采用整数分计算、明确余数分配规则;单机练习可用内存存储,但必须说明重启后数据消失。不要把这个起点误认为已实现分账功能。
1. 从提案生成需求:先澄清,再写文件
第一层 Harness 是可重复使用的入口指令。让助手创建 .ai/prompts/proposal-to-requirements.md,内容可以直接用下面这段;普通 Markdown 文件只是提示词模板,不会自动被所有编辑器识别为斜杠命令。之后在新会话把它与 proposal.md 一起提供给助手。
任务:阅读 proposal.md,先列出会影响验收或数据设计的歧义,按主题分批向我提问。
不要在我答复之前猜测关键业务规则,也不要直接写代码。
记录我的决策;将范围按功能拆分到 context/requirements/,
文件用 01-功能名.md、02-功能名.md 等编号。
每份需求说明目标、范围、不做什么、关键业务规则、尚待确认事项,
只写到可以继续拆用户故事的粒度,不提前指定具体实现代码。
遇到相互矛盾的输入,先指出冲突并等待我决定。
向助手发送:「按照 .ai/prompts/proposal-to-requirements.md 处理 proposal.md。」回答它的问题时,可以为本练习约定:成员名在同一小组内不重复;金额以整数分计算;均摊余数按成员加入顺序分配;单机版重启会清空数据。这些是练习中的决策,不是分账应用的普遍规则。 请助手把回答写进相关需求,而不只留在聊天记录里。
检查点: context/requirements/ 下有编号的「小组与成员」「费用」「余额与结算」等需求;没有把尚未澄清的规则写成事实。若助手没有提问,修改入口指令后用新会话再试,避免当前对话的隐藏上下文掩盖提示词缺陷。
2. 从需求生成纵向用户故事
创建 .ai/prompts/requirements-to-stories.md,将下列内容放入文件。一次只给助手一份需求,防止它一次性生成难以评审的大量故事。
任务:根据给定的一份 context/requirements/ 文档生成用户故事。
在写入文件前,先检查需求中的假设与冲突并向我提问。
每个故事应是能独立演示的纵向切片,覆盖需要的界面、API、数据和验证,
不要按「只做前端」「只做后端」拆分。
使用「作为...我希望...以便...」及 Given/When/Then 编写可观察的验收标准。
为每个故事列出所依赖的故事编号;无依赖则明确写「无」。
输出到 context/user-stories/<需求编号>-<故事编号>-<名称>.md;
不要改写输入需求,不要写代码。发现需要修改需求时先征得我同意。
在新会话发送:「按照 .ai/prompts/requirements-to-stories.md 处理小组与成员的需求文件。」假设该需求编号为 01,你至少可以要求得到 01-01-create-group.md 和 01-02-add-member.md(实际编号以生成的需求为准)。故事 01-02 应写明依赖 01-01。例如新增成员的验收标准是:Given 已打开一个小组,When 输入不重复的名字并提交,Then 成员显示在列表中;重复名字要显示错误,不得悄悄添加。
检查点: 每条故事都能由用户操作和可见结果验证;有清晰依赖;没有把某个技术层独立当作完整故事。如果歧义仍在,先修改需求,再重新生成受影响的故事。不要通过删除已审定文件来「测试提示词」;优先在新会话对现有故事做一次评审。
3. 从故事生成可执行的实现计划
创建 .ai/prompts/story-to-plan.md:
任务:只为指定的一条用户故事制定计划,不修改业务代码。
先阅读对应需求、故事、当前代码和现有测试;如果依赖未完成,先报告。
检查金额、错误处理等模糊点;未确认的重要决策先问我。
计划须包含:目标与范围、相关需求和验收标准、依赖、现状、
将改动的文件/API/数据结构、分步实现顺序、业务边界、潜在副作用、
需要新增的自动化测试、浏览器验收步骤、我的决策及未解决问题。
为每条验收标准指出相应的验证方式,不得用「测试一下」代替测试用例。
输出到 context/implementation-plans/<需求编号>/<故事编号>/
<需求编号>-<故事编号>-<计划编号>-<名称>.md,初始状态写「待执行」。
一个故事可以有多份计划;计划必须说明执行前置条件。
在新会话发送:「按照 .ai/prompts/story-to-plan.md 为 context/user-stories/01-01-create-group.md 写计划。」如果文件名不同,替换为实际路径。期望路径类似 context/implementation-plans/01/01/01-01-01-create-group.md。这时项目仍只有健康检查:计划需要依据当前代码制定,而不是假设已有数据库、路由或表单。
检查点: 能沿着「需求 → 故事验收标准 → 计划中的改动与测试」逐项追溯;计划明确是否仅在内存保存数据、何时清空。若测试用例缺失,先修订模板并重做这一份计划,不要直接进入实现。
4. 执行计划,但不要把「写完」等同于「完成」
创建 .ai/prompts/execute-plan.md:
任务:执行指定实现计划。先读取计划、故事、需求和相关代码。
列出准备改动的文件、依赖检查结果和待确认事项,等待我明确批准后再修改。
只做计划覆盖的改动;新增/修改代码时补充测试,并运行 npm test。
不能跳过失败的测试。若被环境或需求阻塞,报告原始错误、影响和建议,
等我决定;不要虚构「通过」。
完成编码后按故事的 Given/When/Then 在浏览器逐项检查;
若无法操作浏览器,给出我能照做的操作和预期结果,把 UI 验证标记为待人工检查。
记录测试命令、结果、未通过项;只有自动化测试通过且验收已核实,
才将计划状态改为「完成」;一个故事的全部计划完成且验收通过,
才将故事标为「已实现并验收」。否则保持待验证或阻塞状态。
在新会话发送:「按照 .ai/prompts/execute-plan.md 执行 context/implementation-plans/01/01/01-01-01-create-group.md。先给我改动清单,等我批准。」核对它没有越过确认点,批准后观察它是否修改代码、添加测试并运行 npm test。你自己打开 http://localhost:3000,根据 01-01 的验收标准创建小组、检查必填错误和页面反馈;把手工结果交回助手记入计划;必要时重启 npm run dev 让 Node 服务读取新代码。
此时应看到四个模板、需求文件、故事文件、计划文件及其对应的代码和验证记录。一个计划只涵盖其对应故事,不要把「小组创建」通过误报为整个分账应用完成。
5. 把编码交给专职子任务
故事更多时,可以将「编排」和「修改代码」分开。如果助手支持子代理,在它的本地配置中创建一个名为 code-implementer 的子任务角色,给它如下职责;角色配置格式以实际工具文档为准,下面是任务契约而非通用斜杠命令。
输入:一份实现计划路径、对应需求与故事路径、允许修改的文件范围。
职责:检查依赖与代码现状;只完成这份计划的编码和自动化测试;运行 npm test。
输出:改动的文件、执行的命令与结果、逐项验收映射、未解决的问题。
权限:可以读写项目代码并运行测试;不得自行批准新需求、改动无关文件、
绕过失败测试或把计划标为最终验收完成。有歧义时返回主助手提问。
如果工具不支持子代理,就开启独立会话,把同一份任务契约、计划及相关文件交给助手,结束后将报告带回主会话。独立会话只提供更聚焦的上下文,不会自动提高结果正确性。现在先按第 3 步为故事 01-02 写计划,暂不执行,留给第 7 步测试编排;主助手届时仍需检查实际改动与测试证据。
6. 把 UI 验收交给只读检查者
再创建 ui-tester 的任务契约:
输入:用户故事及 Given/When/Then 验收标准、正在运行的应用地址。
职责:按标准逐项在真实浏览器中操作,记录输入、观察到的结果与错误。
输出:表格,列出「验收标准 | 通过/失败/无法验证 | 证据 | 备注」。
权限:不得修改源文件;不得将看不到的结果猜测为通过。
仅在所用工具确实提供浏览器操作能力时自动委派;配置时检查 ui-tester 不具有文件编辑权限。没有浏览器能力就由你自己打开 http://localhost:3000,按每条标准操作并填写相同表格。别把「模型读了页面源码」当作在浏览器里跑过流程;视觉问题也要在手机尺寸和桌面尺寸分别检查。
7. 让主助手按依赖编排多个计划
现在修改 .ai/prompts/execute-plan.md,追加:
若一个故事有多份计划,先列出依赖顺序,得到批准后依次执行;
每份计划的代码工作交给 code-implementer(若可用),读取其测试报告。
所有计划的自动化测试通过后,再由 ui-tester(若可用)或人工逐项验收。
不要让负责编码的子任务替自己给出最终 UI 通过结论。
最终汇总每份计划、测试结果与逐项验收报告,再决定状态。
为 01-02 创建并执行计划。检查点: 先完成它依赖的 01-01;看到编码报告、npm test 结果及添加成员/重复成员的实际操作记录。如果用分离会话模拟角色,主助手负责收集这些报告,而不是声称发生了工具级子代理委派。
8. 加上有停止条件的验证循环
原始的「验收失败就一直让 AI 修,直到全绿」不够安全:测试可能写错,环境可能坏,模型也可能原地打转。再次修改 .ai/prompts/execute-plan.md,追加以下规则:
UI 验收失败时,主助手记录失败步骤和可复现证据,
交给 code-implementer 修复(无子代理时交给独立编码会话)。
每轮修改后先重跑 npm test,再重新检查受影响及相关验收标准。
最多自动重试 2 轮;同一失败重复出现、出现新需求歧义、
无法复现或环境阻塞时停止并请我决策。
失败和未验证项不得标记为通过;所有标准有证据通过后才能完成故事。
继续为「记录一笔均摊费用」创建故事和计划;请助手对金额为 0、无法整除的分配以及非小组成员付款等边界写验收标准。执行后根据标准手工或用浏览器工具核查。如果恰好没有失败,就检查模板和任务交接是否具备修复回路即可,不要故意破坏代码来制造演示结果。 若出现失败,记录每一轮的输入、修改、自动化测试与复测结果;超过上限则保持「阻塞」。
9. 把依赖许可证检查前移,但不接私有服务
这个控制点只在确实要新增外部包时触发。本例基础代码使用 Node.js 标准库,因此无需安装包或调用外部许可证工具。可以在 .ai/prompts/execute-plan.md 中增加以下约束:
计划要新增第三方依赖时,先列出包名、锁定版本、引入原因、
可否用现有代码或标准库替代;暂停并等我批准。
使用公开的包元数据、发布包中的 LICENSE 文件和项目仓库核对许可证;
记录许可证名称、版本、证据链接及需要进一步确认的义务。
信息缺失或不明确时不要推断为允许使用,改由人工/法务判断。
不要在未批准前安装依赖;新增依赖后复查锁文件中的直接与间接依赖。
测试这条规则时,请助手仅评估「如果要引入 [email protected],需要先做哪些检查」,不要安装。先用 npm view [email protected] license 查看该特定版本的公开元数据,再核对相应发布包的许可证文本和版本;网络不可用就标记为「待验证」,不能声称审查通过。一个 license 字段不足以完成全部合规审查。npm audit 关注安全漏洞,不能替代许可证审查。这一步是公开、通用的检查流程,不依赖任何组织的审批结论。
下一步:维护这条工作流
- 需求变更: 先更新受影响的需求和验收标准,再从当前代码重新规划。未开始的计划可以修改;已完成的计划保留执行记录,新建计划描述增量改动,不要改写历史。
- 快速通道: 几行小修不一定要走完整四层文档,但仍要明确目标、改动、验证和结论;风险越高,检查点越多。
- 技术预研: 将已证实的约束及证据写入需求或决策记录,不要让一次聊天里的猜测成为架构事实。
- 缺陷分诊: 后期发现问题时,先补一条可复现的失败验收或测试,再修代码,并反查哪个需求、计划或验收环节漏掉了它。
- 精简 Harness: 只在观察到重复问题后新增规则;发现未触发的入口、相互矛盾的指令和不必要的角色时,修订或删除,用同一个小故事做回归测试。
交付的标准不是「AI 说完成了」,而是产物之间能追溯、测试确实运行、验收有证据、未解决问题明确留在台面上。把长期有效的业务规则同步到需求文档,保留必要的决策与验证记录,下一次会话才能从可信的现状继续工作。
评论已关闭