Home
avatar

.Sam

【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 面前时,怎么用限时实验把它清掉,而不是让它流进实现阶段爆炸。

AI Native SDD Spec 需求工程 软件工程