【AI Native开发-04】如何写一份 AI 能执行的 Spec
这是“AI Native开发”系列的第 4 篇。
**案例说明:**本文的 API、鉴权和自动化测试契约属于假想全栈项目示例;本博客当前是 Astro 静态站,真实内容变更通过本地 CMS/Git 工作流完成。上一篇解决了“AI 怎么理解项目现状”,这一篇解决“AI 怎么理解你要做什么”。第 1 篇给出了 Spec 质量标尺(五个维度、阈值 7 分),本篇补上那套标尺背后的写作方法:一份 AI 能无歧义执行的 Spec,到底由哪十块东西组成,每块解决哪类失效,以及“非目标”为什么是全篇最值钱的一节。
摘要
本篇把 Spec 从“应该怎么写”落到“具体写什么”:给出十项结构(角色、目标、正常流程、异常流程、业务规则、权限边界、数据约束、验收标准、非目标、需求编号),每一项都对应第 1 篇 F1–F6 中某类失效的根治;用“删除草稿文章”功能给出从一句话需求到完整 Spec 的改写全过程;并给出 Spec 准出标准与可直接套用的模板。核心判断:Spec 不是写给人“理解”的,是写给 AI 和测试“执行”的——所以每一句话都必须能被翻译成一个测试或一个实现决策,写不出来的那句就是缺口。
先厘清:Spec 和需求文档差在哪
第 0 篇的术语约定里钉过一条:需求文档描述“我们想要什么”,Spec 描述“系统在什么条件下必须表现出什么行为”。这个区别在写作时的操作含义是:
需求文档的句子:
“删除功能要安全,不要误删文章。”
→ 人看了能点头,AI 看了只能猜:什么叫安全?什么算误删?
Spec 的句子:
“当文章 hide 为 false 或缺省(已公开)时调用删除接口,系统返回 409,
文章文件保持存在,列表中仍然可见。”
→ 条件明确、行为明确、可直接翻译成一个测试用例判据一句话:把这句话单独拎出来,一个没参与过讨论的人(或 AI),能不能据此判断某个实现“对”还是“不对”? 能,它是 Spec;不能,它是需求或者愿望。
另一个配套判断:需求文档允许存在“体验流畅”“适当提示”这类词,因为它是给人对齐方向的;Spec 里出现这类词就是漏洞——第 1 篇的 F6(非目标越界)和 F3(一致性缺口)最容易从这种模糊处长出来。
Spec 的十项结构,以及每一项防住哪类失效
一份可执行的 Spec 至少包含下面十块。顺序不是形式,每一块都对应一类具体的“AI 自由发挥”。
| # | 结构项 | 回答的问题 | 主要防住的失效 |
|---|---|---|---|
| 1 | 用户角色 | 谁能用这个功能? | F2 权限默认放行 |
| 2 | 用户目标 | 使用者到底要达成什么? | 目标漂移、做了不解决问题的功能 |
| 3 | 正常流程 | 最顺的路径怎么走? | 主干缺失 |
| 4 | 异常流程 | 每一步失败了怎么办? | F3 一致性缺口(补偿路径缺失) |
| 5 | 业务规则 | 有哪些硬性约束和状态条件? | F1 状态假设 |
| 6 | 权限边界 | 服务端必须拦截什么? | F2、越权 |
| 7 | 数据约束 | 数据长什么样、怎么变化? | F1、数据模型误读 |
| 8 | 验收标准 | 怎样算完成? | F5 验收标准坍缩 |
| 9 | 非目标 | 这次明确不做什么? | F6 非目标越界 |
| 10 | 需求编号 | 每条规则叫什么? | 追溯断裂(测试无法映射) |
下面逐项展开,每一项给“该写什么 + 反例 + 正例”。
1. 用户角色
列出与这个功能有关的所有角色,以及各自能做什么。博客项目里角色简单(管理员 / 访客),但越是简单越要写——因为 AI 对“简单权限”最容易想当然。
反例:“只有管理员能删除。”
正例:
- 管理员:通过 CMS 登录(Keystatic / Decap,均为 Git 身份),可删除文章
- 访客/未登录:没有任何删除入口,直接调用删除接口返回 403
- 本项目不存在自建用户表和角色系统,不要新建 user/role 模型最后一句是上下文事实(见上一篇 C1),写进 Spec 能直接挡住 AI 自建权限体系。
2. 用户目标
用一句话写使用者的真实意图,不是功能描述。
功能描述:“提供删除按钮和删除接口。”
用户目标:“管理员能把写错、废弃的草稿从系统里清掉,
同时绝不可能误删已经对外发布的文章。”目标里藏着验收的重心:这个功能的价值一半在“能删”,另一半在“绝不误删已发布”。后半句会在验收标准里变成高优先级用例。
3. 正常流程
主干路径,步骤化描述,每一步是“谁做什么、系统响应什么”。
1. 管理员在文章列表看到未公开(hide: true)文章旁有「删除」操作
2. 点击「删除」,弹出二次确认,确认文案显示文章标题
3. 管理员确认后,系统删除文章文件
4. 列表刷新,该文章消失
5. 刷新页面 / 重新构建后,文章仍然不存在(删除是持久的)注意第 5 步:静态站点里“列表消失”可能只是前端状态,必须写“重建后仍不存在”才算覆盖了真实行为。这一步对应第 2 篇 Playwright 里“监听 DELETE 请求”之外的持久化验证。
4. 异常流程
主干之外的每一个失败分支。这一节最能体现 Spec 质量——正常流程大家都会写,差距全在异常。
- 删除时文件已被其他人删掉(并发):
返回 404,列表静默移除该项,不报错崩溃
- 二次确认时点取消:不发任何删除请求,文章保持不变
- 删除过程中出错(文件系统/Git 提交失败):
文章保持原状,列表提示“删除失败,请重试”,不留下半删除状态
- 已发布文章调用删除:见业务规则,拒绝F3(一致性缺口)几乎都死在这一节的缺失:AI 实现了“删除成功”,但失败时文章处于什么状态、要不要回滚,全靠猜。
5. 业务规则
这个功能里所有“如果……那么……”的硬约束,尤其状态条件。
- 仅 hide: true 的文章允许删除
- hide 为 false 或缺省(已公开)的文章删除请求一律拒绝,返回 409 Conflict
- 文章是否公开由 frontmatter 的 hide 布尔字段表达,
系统中不存在 status 字符串字段(架构事实,见 pitfalls)
- 删除为物理删除 Markdown 文件,本功能不做回收站/软删除第三条是从前述教学案例的事故里长出来的规则(第 2 篇的 article.status 事故)。把它写进 Spec,而不是只写在 pitfalls,是因为它直接决定实现怎么读状态。
6. 权限边界
明确“前端隐藏”和“服务端拦截”是两件事,且服务端必须独立校验。
- 前端:访客看不到删除入口(体验层)
- 服务端:删除接口必须独立校验 CMS 身份,
未登录/无身份请求返回 403,且不执行任何删除(副作用为零)
- 403 的验证不只看状态码,还要确认文章文件仍然存在最后一句对应第 2 篇那条关键断言原则:不能只断言状态码,必须验证副作用没发生。 写进 Spec,AI 生成测试时就不会漏掉。
7. 数据约束
涉及的数据结构、字段、变化方式。
- 文章 = src/content/blog/<年>/<月>/<slug>.md 文件
- 删除 = 删除该文件(以及构建产物中对应页面在下次构建时消失)
- 不涉及数据库;frontmatter schema 定义在 src/content.config.ts
- 删除操作不产生新的字段或状态8. 验收标准
每条都可判定,最好直接编号(和第 10 项的 REQ 合并管理)。这一节是 F5 的解药:验收标准一旦坍缩成“测试通过”,前面所有漏洞都会隐身。
- AC-1:管理员删除 hide: true 文章 → 文件消失,重建后仍不存在
- AC-2:访客调用删除接口 → 403,且文件仍存在(副作用验证)
- AC-3:删除已公开(hide 非 true)文章 → 409,文件仍存在
- AC-4:确认框点取消 → 不发删除请求,文章不变
- AC-5:并发重复删除 → 第二次返回 404,不报错(对应第 1 篇 F4 幂等性缺失)第 1 篇讲过空实现测试:每条 AC 都应能被设计成“把实现换成空实现就会失败”。AC-2 如果只写“返回 403”,空实现也能过;加上“文件仍存在”就过不了。
9. 非目标(全篇最值钱的一节)
明确这次不做什么。它防 F6(非目标越界),同时是给 AI 的“不许扩大战线”指令。
本次不做:
- 回收站 / 撤销删除 / 软删除
- 批量删除
- 删除操作审计日志
- 已发布文章的“下线”流程(那是另一个功能)
- 任何全局权限模型改造为什么这一节最值钱:AI 的默认倾向是“把它觉得相关的都做了”——你要删除,它顺手给你加回收站、加批量、加操作日志,因为训练数据里“完整的删除功能”都长这样。每一条非目标都是在显式拆除一条它准备自行铺设的铁轨。而且非目标也是给未来的路标:“审计日志”不是永远不做,是这次不做,它会自然成为下一个 Spec 的候选。
10. 需求编号
给每条规则一个稳定 ID(REQ-001…),让 Spec、Plan、测试、Verify 报告能互相引用。
REQ-001 管理员可删除 hide: true 文章
REQ-002 访客删除请求返回 403 且无副作用
REQ-003 已公开文章删除返回 409
REQ-004 取消确认不发删除请求
REQ-005 删除后重建仍不存在(人工验收:本地构建确认)
REQ-006 删除失败时列表保持原状态并提示错误
REQ-007 目标文章不存在时返回 404;重复删除不产生额外副作用编号的价值在第 2 篇演示过:需求追踪矩阵 REQ → 用例 → 自动化 → 结论,靠的就是它。没有编号,“每条规则都被测了吗”这个问题永远回答不了。注意 REQ-005 标注人工验收——表里允许有不能自动化的行,但不允许缺行;REQ-007 则把 F4 的重复操作契约固定下来,避免后续文章自行解释。
从一句话到一份 Spec:改写全过程
原始一句话需求:
“给博客加个删除文章功能。”
第一步 补角色与目标:
谁删?管理员。为什么删?清理废弃草稿,绝不误删已发布。
第二步 拆正常流程:
列表 → 二次确认 → 删除文件 → 列表刷新 → 重建后仍消失。
第三步 穷举异常:
并发已删?取消?删除失败?已发布?
(每问一个“如果失败/如果不满足条件会怎样”,就补一条)
第四步 写死业务规则和数据事实:
hide 布尔表达公开状态、物理删除、无 status 字段、无回收站。
第五步 补权限边界:
服务端独立校验 403 + 副作用为零。
第六步 编号 + 准出检查:
每条规则给 REQ 号;逐条问“这能写成测试吗?”
写不成测试的,要么改写成可测,要么移到非目标。这个过程里,第三步(穷举异常)和第六步(可测性拷问)是 AI 帮不上太多、必须人来做判断的两步——它们恰好对应第 0 篇责任矩阵里“不可委托给 AI”的那类判断。AI 可以帮你把写好的要点扩写成规整文档,但“漏没漏异常场景”“这条验收够不够硬”,是人在 Gate 1 要签的字。
Spec 准出标准(Gate 1)
写完后逐条过,任何一条不过就退回,不允许带着缺口进 Plan:
[ ] 每条规则都能翻译成至少一个测试用例(可测性)
[ ] 角色、权限、服务端校验明确,前端隐藏 ≠ 安全
[ ] 正常流程 + 每个失败分支都有明确的系统行为
[ ] 状态、数据字段基于真实代码(已核对,不是假设)
[ ] 没有“尽量/适当/友好/流畅”等不可判定词
[ ] 非目标显式列出,且与业务规则不矛盾
[ ] 每条规则有 REQ 编号,可被 Plan 和测试引用
[ ] 两个没参与讨论的人,按这份 Spec 能得出相同验收结论最后一条是元标准:Spec 的读者是“不在场的人”——可能是另一个同事,也可能是三个月后的你自己,更可能是一个对你项目一无所知的 AI。
附录:Spec 模板(可直接复制)
# Spec: <功能名>
## 背景与用户目标
<!-- 一句话真实意图,不是功能清单 -->
## 用户角色
- <角色 A>:<能做什么>
- <角色 B>:<能做什么>
<!-- 写明项目里真实的身份机制,禁止 AI 自建 -->
## 正常流程
1.
2.
## 异常流程
- <失败场景>:<系统行为 / 返回 / 状态>
-
## 业务规则
- <如果……那么……>
-
## 权限边界
- 前端:
- 服务端(独立校验):
- 副作用验证要求:
## 数据约束
- 涉及的数据/文件/字段:
- 数据如何变化:
- 相关架构事实(引用 pitfalls / 决策记录):
## 验收标准(需求编号)
- REQ-001
- REQ-002
<!-- 每条都应可设计成“空实现会失败”的测试 -->
## 非目标
- 本次不做:
- 明确不改造的现有模块:
## 待确认问题
- <!-- 写 Spec 时悬而未决的点,进 Plan 前必须清零或转 Spike -->最后一栏“待确认问题”是留给第 5 篇的接口:如果某个问题不是“需求没想清”而是“技术上不知道可不可行”,它不该靠拍脑袋写进 Spec,而该走 Spike 先验证。下一篇就讨论这件事:当技术不确定性挡在 Spec 面前时,怎么用限时实验把它清掉,而不是让它流进实现阶段爆炸。
