Software Engineering

SDD 如何落地:从标准文档到执行循环

2026年3月1日By SchemaX
#SDD#Spec-Driven Development#Software Engineering#Testing#AI Agent#Self-Healing
SDD 如何落地:从标准文档到执行循环 - Software Engineering相关的学习和博客内容图片

Spec-Driven Development 不是一个新概念,但它过去一直有个很现实的问题: 太贵。

贵的不是文档本身,而是这几件事叠加起来的成本:

  • 需求要整理
  • 设计要展开
  • 任务要拆分
  • 验收要写清楚
  • 一旦需求变动,前面所有文档还要一起更新

这也是为什么很多团队最后只剩两种状态: 要么不写 Spec,要么写了但没人维护。

大语言模型真正改变的,不是“替代工程判断”,而是显著降低了把模糊意图转换成结构化文档的成本,也降低了文档之间持续对齐的成本。正因为这件事变便宜了,SDD 才第一次对大多数团队变得现实可行。

这篇文章我想按三个问题来讲清楚:

  1. 一套可执行的 SDD,到底需要固定哪些关键信息
  2. 这些信息如何在人和大语言模型协作下形成文档
  3. 当文档稳定之后,流程如何真正运行起来

为了让讨论保持具体,下面仍然用一个最小例子贯穿全文: Todo Reminder

第一部分:一套可执行的 SDD,需要先固定哪些信息

一套 SDD 要真正跑起来,至少要先回答四个问题:

  • 我们到底要做什么
  • 我们准备怎么实现
  • 我们准备按什么顺序交付
  • 我们怎么判断它真的做对了

如果这四个问题没有被稳定记录下来,后面的实现、测试和修复就没有共同参照物。

也正因为如此,很多看起来不同的工程实践,最后都会自然收敛到四类核心信息:

  • 目标和边界
  • 结构和设计
  • 任务和顺序
  • 验收和案例

在工程实践里,这四类信息通常会分别落成下面四份文档:

  • requirements.md
  • design.md
  • tasks.md
  • cases.md

这四份文档刚好对应了前面那四个问题:

  • 我们要解决什么问题
  • 我们准备怎么实现
  • 我们准备按什么顺序交付
  • 我们怎么判断它真的做对了

它们之间的关系可以压缩成一条非常短的链路:

requirements -> design -> tasks -> cases

1. requirements.md:定义问题,而不是定义代码

requirements.md 的作用,是把“想做什么”说清楚。

它应该回答四类问题:

  • Goal: 功能目标是什么
  • Requirements: 必须满足哪些行为
  • Constraints: 约束条件是什么
  • Non-Goals: 明确不做什么

比如在 Todo Reminder 这个例子里,它可以很简单:

# Feature: Todo Reminder

## Goal
Remind users of tasks before their due time.

## Requirements
- System must store tasks
- Each task must support a due datetime
- System must trigger a reminder before due time
- Completed tasks must not trigger reminders

## Constraints
- Use in-memory storage
- Single-process execution
- Timezone = UTC

## Non-Goals
- No UI
- No persistence
- No external notification service

这一层最重要的价值,不是写得长,而是把边界钉死。边界一旦不清楚,后面的设计、任务和测试都会漂移。

2. design.md:把需求翻译成结构

如果说 requirements.md 解决的是“做什么”,那 design.md 解决的是“怎么落到结构上”。

最小设计文档通常只需要包含:

  • 核心组件
  • 数据模型
  • 接口定义
  • 关键行为规则

例如:

# Design: Todo Reminder

## Architecture
- TaskStore
- Scheduler
- Notifier

## Data Model
Task:
- id: string
- title: string
- due_at: datetime
- completed: boolean

## Interfaces
TaskStore.add(task)
TaskStore.list()
Scheduler.run(current_time)
Notifier.notify(task)

一个好的最小设计文档,不需要面面俱到,但必须让后续实现不再依赖“脑补”。

3. tasks.md:把设计拆成可执行单元

tasks.md 是从“结构”走向“交付”的桥。

它至少要做两件事:

  • 把实现拆成可执行任务
  • 给出任务之间的依赖关系

例如:

# Tasks

- [ ] T1: Implement Task model
- [ ] T2: Implement TaskStore
- [ ] T3: Implement Scheduler logic
- [ ] T4: Implement Notifier
- [ ] T5: Wire components together

## Dependency
- T2 depends on T1
- T3 depends on T2
- T5 depends on T3, T4

没有这份文档,开发很容易变成“想到哪写到哪”。而一旦要把执行交给 Agent,tasks.md 更是必要,因为 Agent 需要一个足够清晰的最小工作单元。

4. cases.md:把验收条件写成事实

cases.md 对应的是“怎么验证”。

它的最佳写法通常不是测试框架代码,而是 Given / When / Then 这种行为描述:

# Cases

## Case: Reminder triggers before due
- Given a task with due_at in 30 seconds
- When scheduler runs
- Then reminder is triggered

## Case: Completed task ignored
- Given a completed task
- When scheduler runs
- Then no reminder is triggered

这份文档的意义在于,它把“验收标准”从人的感觉变成了可转译、可执行、可回归的资产。后面无论你要人工写测试,还是让 LLM 自动生成测试,都会以它为基础。

这四份文档为什么刚好够用

很多人做 SDD 时,第一步就把系统做重了。其实在大多数功能开发里,真正必须稳定存在的,往往就是这四份:

  • requirements.md 决定范围
  • design.md 决定结构
  • tasks.md 决定执行顺序
  • cases.md 决定验收真值

只要这四份文档是对齐的,一个最小闭环就已经成立:

Spec -> Tasks -> Code -> Test -> Feedback

第二部分:这四份文档是如何生成的

理解了文档本身之后,真正关键的问题是: 这四份文档到底怎么来?

如果答案是“全部靠人手写”,那么 SDD 在多数团队里还是太贵。如果答案是“全部交给模型自动写”,那最后大概率会得到一套看起来完整、实际上不可信的文档。

更实际的方式是: 由人给出目标、约束和判断,由大语言模型负责展开、结构化、补齐和对齐。

也就是说,这不是一个“人工 or 自动”的二选一问题,而是一个职责分工问题。

Step 0:先由人提供原始输入

在生成四份文档之前,必须先有一层人类输入。通常包括:

  • 业务目标
  • 用户场景
  • 关键约束
  • 风险点
  • 明确不做的范围

这一层不能省,因为它决定了整个系统的方向。模型可以帮你整理,但不能替你决定产品目标和业务优先级。

Step 1:先生成 requirements.md

这是最适合“人提供原始材料,模型负责整理”的环节。

典型流程是:

  1. 人先写一个粗糙需求描述
  2. LLM 把它整理成 Goal / Requirements / Constraints / Non-Goals
  3. 人再对照业务目标做确认和删减

在这个环节里,职责大致可以这样分:

  • 人负责:
    • 决定目标是否正确
    • 决定约束是否真实
    • 决定哪些需求不该做
  • LLM 负责:
    • 把自然语言整理成结构化文档
    • 发现缺失项和歧义项
    • 把含糊表达改写得更可执行

换句话说,LLM 在这里不是“拍板的人”,而是“把模糊意图加工成工程输入的人”。

Step 2:基于 requirements.md 生成 design.md

当需求稳定后,下一步是设计。

这里的典型流程是:

  1. requirements.md 作为输入给模型
  2. 让模型先产出一个候选设计草稿
  3. 人审查架构、数据边界、接口分层和明显的过度设计
  4. 定稿为 design.md

这个环节里,人和模型的分工会发生一点变化:

  • 人负责:
    • 判断架构是否合理
    • 判断复杂度是否过高
    • 决定哪些设计取舍符合团队现实
  • LLM 负责:
    • 把需求展开成组件、接口和数据模型
    • 提供多个设计备选方案
    • 帮助补全遗漏的连接关系

所以 LLM 在这里更像一个“初级架构草稿生成器 + 结构化讨论助手”。

Step 3:基于 design.md 生成 tasks.md

文档走到这一步,本质上是在做任务分解。

这个环节特别适合 LLM,因为任务拆分本身就是一种从结构到步骤的映射:

  1. 人提供 design.md
  2. LLM 根据组件和依赖关系拆出任务列表
  3. 人确认粒度是否合适、执行顺序是否合理、是否需要合并或拆细
  4. 输出 tasks.md

这里的职责大致是:

  • 人负责:
    • 决定任务粒度是否适合团队节奏
    • 决定是否需要串行或并行
    • 决定哪些任务必须人工处理
  • LLM 负责:
    • 做初步任务分解
    • 标注依赖关系
    • 让任务从“设计描述”变成“可执行清单”

如果没有 LLM,这一步通常很费时间;有了 LLM,这一步会明显加快。

Step 4:基于 requirements.mddesign.md 生成 cases.md

cases.md 的来源,不应该只是设计,也不应该只是需求,而应该同时参考两者。

因为它既要覆盖业务行为,也要符合实现结构。

这里的流程通常是:

  1. 人给出核心成功路径和关键风险点
  2. LLM 根据 requirements 和 design 枚举 Cases
  3. 人审核哪些 Case 是真正必须保留的
  4. 定稿为 cases.md

这个环节里:

  • 人负责:
    • 决定什么才算“真的通过”
    • 决定边界案例和高风险场景
    • 决定哪些 Case 不值得维护
  • LLM 负责:
    • 从需求和设计中提取行为路径
    • 生成 Given / When / Then 形式的案例
    • 补出被人遗漏的常见边界条件

大语言模型在这四份文档里,到底扮演什么角色

如果只说一句话,我会把 LLM 的角色概括成四个词:

整理者 / 展开者 / 对齐者 / 加速器

更具体一点,它主要做的是这些事:

  • 把松散需求整理成结构化规范
  • 把高层目标展开成设计和任务
  • 把文档之间的缺口暴露出来
  • 在文档变更时,帮助同步更新相关文档

它不应该做的事也很明确:

  • 不替人决定业务目标
  • 不替人拍板关键架构取舍
  • 不替人定义最终验收真相

也正是因为这个角色划分成立,SDD 才开始真正可行。因为过去最贵的,其实不是“想明白”,而是“把想明白的东西持续写成文档并保持一致”。LLM 把这部分成本打下来了。

为什么说没有 LLM,SDD 很难在大多数团队里落地

因为 SDD 最难的从来不是理念,而是维护成本。

每当需求变化时,你都要连锁更新:

  • requirements.md
  • design.md
  • tasks.md
  • cases.md

如果这件事全靠人手工做,大部分团队最终都会退回到口头同步。LLM 的价值就在这里: 它让“重新生成、重新整理、重新对齐”变成一件低成本动作。

所以更准确地说,不是 LLM 发明了 SDD,而是 LLM 让 SDD 从“少数团队能坚持的重流程”变成“大多数团队也能尝试的轻流程”。

第三部分:SDD 如何真正实施和运行起来

如果要把这套 SDD 真正实施起来,最合适的方式是让它围绕 task 进入一个持续验证、持续修复的执行循环。

先看整体流程

先不要引入 Agent。假设这整套流程暂时都由人来完成,它应该长成下面这样:

从 tasks.md 取当前 task
  -> 装载相关 requirements / design / cases / code
  -> 实现或修复代码
  -> 检查这些 cases 是否已经有对应测试
  -> 只为缺失或受影响的 cases 生成或更新测试脚本
  -> 调用 Test Runner 执行测试
  -> 汇总失败结果
  -> 对照 requirements / design / cases 判断失败来源
  -> 通过: 标记 task 完成,进入下一个 task
  -> 实现与 Spec 不一致: 继续修复代码
  -> requirements / design / cases 不一致: 回到文档层,由人处理

也就是说,有了 requirements.mddesign.mdtasks.mdcases.md 之后,系统就不再停留在写文档,而是围绕一个个 task 进入执行循环。

这个流程里最关键的是,每一轮都只围绕当前 task 运转,并且每轮都要经过测试验证。

顺着上面的流程图往下看,实际只有几件事在发生:

  1. 先从 tasks.md 中取出当前 task
  2. 围绕这个 task 装载相关的 requirements、design、cases 和代码上下文
  3. 实现代码
  4. 验证相关 Cases 是否已经被测试覆盖
  5. 只补充缺失的测试,或更新受影响的测试
  6. 执行测试并汇总结果
  7. 对照 requirements、design、cases 判断失败来源
  8. 通过就进入下一个 task,失败就回到修复,必要时回到 Spec

这里有两个边界需要特别明确:

  • 测试不是每一轮都从头生成,而是优先复用已有测试,只补缺失部分
  • 测试失败之后,不是立刻重新生成整套流程,而是先做失败汇总,再决定是修代码还是回到文档层

哪些环节适合由 Agent 来完成

当整体流程清楚以后,下一步才是判断:这里面哪些工作适合交给 Agent。

对编码任务来说,更合适的做法通常不是把流程拆成很多 Agent,而是保留一个确定性的执行循环,再配三个核心 Agent:

  • Loading Agent
    • 负责为当前 task 装载相关上下文
  • Coding Agent
    • 负责实现代码,或者根据失败结果继续修复
  • Verification Agent
    • 负责验证当前 task,包括检查测试覆盖、补齐缺失测试、调用 runner、汇总结果,并给出验证结论

而 task 的推进和结果记录这些动作,更适合放在一个确定性的 Task LoopHarness 里完成。

其中 Test Runner 仍然应该保持为确定性的执行组件,比如 pytest。它负责给出稳定结果,可以由 Verification Agent 调用。

这样分工之后,主循环会比较稳定:

  • Task Loop 负责推进流程
  • Loading Agent 负责装载上下文
  • Coding Agent 负责实现
  • Verification Agent 负责验证和归因

如果后面系统规模继续变大,可以再补一个后台的 Doc Maintenance Agent,专门扫描 requirements、design、cases 的陈旧或漂移问题。但它不属于主循环。

每个 Agent 是怎么工作的

把上面的流程映射到角色上,可以把核心角色的工作方式拆得更具体一点。

Task Loop / Harness

Task Loop 本身不是一个 Agent,而是整个流程的确定性骨架。

它负责:

  1. tasks.md 中取当前 task
  2. 调用 Loading Agent
  3. 调用 Coding Agent
  4. 调用 Verification Agent
  5. 根据验证结论决定是进入下一个 task,还是继续修复,还是回到文档层

它的价值在于把流程控制保持为确定性逻辑,而不是把所有步骤都交给模型自由决定。

Loading Agent

Loading Agent 负责把当前 task 真正需要的信息装配成一个可直接使用的上下文包。

它通常按这个顺序工作:

  1. 读取当前 task
  2. 根据 task 上的显式关联,找到相关的 requirements、design、cases
  3. 读取涉及的代码文件、接口定义和必要上下文
  4. 把这些信息整理成统一的 context bundle
  5. 把 context bundle 分发给 Coding AgentVerification Agent

这里最关键的一点是: Loading Agent 不是“猜哪些内容相关”,而是读取事先建立好的关联。

实践里最稳的方式通常是:

  • tasks.md 里给 task 标出关联的 requirement、design、case
  • 或者由前置步骤生成一个简单的 task-to-context manifest

只有这样,后面的执行循环才是可重复、可追踪的。

Coding Agent

Coding Agent 负责围绕当前 task 实现代码,或者在失败后继续修复。

它通常按这个顺序工作:

  1. 接收 Loading Agent 提供的 context bundle
  2. 判断这一轮需要改哪些文件、哪些函数
  3. 在 design 的约束下实现代码
  4. 测试失败后,读取失败 summary
  5. 只在当前 task 的范围内继续修复

它最重要的边界也很明确:

  • 只处理当前 task
  • 不扩散到无关模块
  • 不擅自修改 requirements 和 design
  • 修复时优先根据测试失败来收缩问题范围

Verification Agent

Verification Agent 负责当前 task 的验证阶段,承担一个完整的验证闭环。

它通常会按下面这个顺序工作:

  1. 接收 Loading Agent 提供的 Cases、代码状态和上下文
  2. 检查这些 Cases 是否已经有对应测试
  3. 只为缺失的 Cases 生成测试,或者更新明显失效的测试
  4. 调用 Test Runner
  5. 把失败结果整理成一个简短、结构化的 summary
  6. 给出这一轮的验证结论

Verification Agent 的职责可以直接概括成四件事:

  • 判断是否需要生成测试
  • 生成或更新测试脚本
  • 执行测试并整理结果
  • 输出当前 task 的验证结论

在转换时,最基本的规则就是:

  • Given -> 初始化数据
  • When -> 执行行为
  • Then -> 写断言

例如这条 Case:

## Case: Completed task ignored
- Given a completed task
- When scheduler runs
- Then no reminder is triggered

会被转成下面这类测试结构:

  • 被测对象是 Scheduler
  • Given: 创建一个 completed=TrueTask
  • When: 调用 scheduler.run(...)
  • Then: 断言提醒没有被触发

但在真正运行时,Verification Agent 不会每次都重新生成这段测试。更合理的做法是:

  • 如果这个 Case 已经有稳定测试,就直接复用
  • 如果这个 Case 是新增的,就补一个新测试
  • 如果代码结构变化导致旧测试失效,再更新对应测试

这样整个系统才不会在每一轮里重复生成同一批脚本。

而在调用 Test Runner 之后,它还要继续做两件事: 先整理结果,再输出验证结论。比如把冗长日志压缩成:

FAILURE SUMMARY:
- test_completed_task_ignored failed

ERROR:
Expected 0 call, got 1

这样 Coding Agent 收到的就不再是一整段噪音日志,而是一个可以直接用于修复的输入;而 Task Loop / Harness 收到的则是“继续修复”或“回到文档层”的信号。

这一部分的本质是什么

这一部分真正想说明的是:四份文档先把目标、结构、任务和验收条件固定下来,然后执行循环按 task 推进;代码实现之后,验证环节再决定是进入下一个 task,还是回到修复,或者回到文档层。这样,SDD 才真正从“写文档”变成“驱动交付”。

总结

如果把整篇文章压成一句话,我会这样概括:

SDD 的核心,是让四份文档成为一套可以持续驱动实现、测试和修复的控制系统。

这套系统里:

  • 人负责目标、约束、判断和取舍
  • LLM 负责整理、展开、补齐和对齐
  • 测试负责判断结果是否成立

于是,SDD 才从一种理想化流程,变成一种真正能跑起来的工程方法。