SDD 划定什么是合法,TDD 守住合法不被悄悄破坏。

引言

我们的指标问数产品一直在持续迭代。新功能要建立在旧代码之上,每一次改动都得穿过已有的模块边界,阻力并不小。

项目早期靠的是 vibe coding,功能出得很快。但版本迭代多了之后,新旧代码越来越难融合,代码逐渐失控:下游配置会在不知不觉中失效,往往要等到运行时报错,才发现某个字段的默认值被人无意间改掉了,或者某个 Agent 的装配逻辑经过几轮重构后早已偏离最初的设计。

问题不在于人不够细心,而在于缺少一套机制。存量代码这么大规模,单靠经验和 code review 去维持稳定,代价实在太高。

为此我们引入了两套方法论:SDD(规范驱动开发) 和 TDD(测试驱动开发)。本文想分享这段实践,以及这两种方法是如何在 AI Agent 系统的迭代中相互咬合、彼此成就的。

什么是 SDD

SDD 的核心主张可以浓缩成一句话:规范是唯一的真相,代码只是规范的实现。

在传统开发里,代码才是真相。想知道某个接口接收什么参数、某个模块有哪些合法状态,往往只能去读源码。文档跟不上变化、注释慢慢过时、口头约定被人遗忘——这些几乎是每个团队都躲不开的老问题。

SDD 把这个顺序倒了过来:先定义规范,再动手写实现。规范不只是一份文档,更是一套可执行的约束——它划定了什么合法、什么不合法,并且由机器强制执行,而不是寄望于人的自律。

什么是 TDD

TDD 的核心主张同样可以浓缩成一句:测试要先于实现存在。

它不是"写完代码再补测试",而是先写测试,再写实现,最后才重构。测试在这里不是质量保障的最后一道关卡,而是一件设计工具——先把测试写出来,会逼着你提前想清楚接口该长什么样、边界在哪里、异常路径怎么处理。写实现是为了让测试通过,而不是反过来让测试去迁就实现。

两者的关系

SDD 和 TDD 不是彼此竞争的关系,而是相互补位:SDD 负责把规范定下来,TDD 负责守住这份规范不被破坏。

问题

谁来回答

什么是合法输入?

SDD — Schema 字段定义

合法输入产生正确输出吗?

TDD — 测试断言验证

默认值是什么?

SDD — 字段默认值声明

默认值有没有被改坏?

TDD — 锁定默认值的测试

接口契约是什么?

SDD — 类型签名与约束

契约有没有被破坏?

TDD — 测试失败即报警


SDD 定义"合法",TDD 保证"合法不被破坏"。

二者缺一不可:只有规范没有测试,规范早晚会被悄悄破坏;只有测试没有规范,验证逻辑又会散落各处,重复而且容易跑偏。

实践一:Agent 配置的 Schema 规范

Agent 协作框架需要一套配置系统,来描述"一个 Team 由哪些 Agent 组成,每个 Agent 又挂载了哪些技能和工具"。

我们没有一上来就写代码,而是先把数据结构定义清楚。Schema 用声明的方式回答了所有关于"什么是合法 Team 配置"的问题:

Schema 本身就是文档,Pydantic 本身就是验证器。 任何不符合规范的配置,在构造的那一刻就会立刻报错,不用等到运行时才炸出来。

实践二:测试锁定默认值契约

Schema 定好之后,测试自然而然就跟了上来。我们为每个字段的默认值、每个必填项、每个边界条件都写了测试。

乍一看这些测试"未免太简单了",不过是检查一下默认值。但它们的价值从来不在于发现 bug,而在于把契约锁死

三个月后,有人想改 sandbox 的默认值,测试立刻报红。这不是坏事,而是系统在提醒你:"你动的是一个有下游依赖的契约,想清楚再改。"

实践三:注册表的行为契约

Agent 注册表负责管理所有 Team 定义,要支持注册、查询、防重复。我们先写测试,再写实现,测试完整描述了注册表的行为契约——三个场景,涵盖正常路径和两种异常路径:

测试就是活的 API 文档。 这三个场景完整地告诉新成员:注册表能做什么、不能做什么,遇到错误时又会怎样反应。

实践四:Prompt 也能 SDD

AI 系统有一个传统软件不会遇到的难题:Prompt 承载着核心逻辑,却没有类型系统来约束它。

我们把 Agent 的行为规范写成 YAML,用结构化格式把约束固定下来。YAML 是规范,运行时是实现。规则要变,只需要改 YAML,代码可以完全不动。

Prompt 三层架构

三层分离的价值就在这里:静态规则不用随每次请求重发,可以共享缓存;领域知识能按客户替换;运行时注入的内容压到最少。要改意图判断规则,只动 Layer 1;要新增行业示例,只动 Layer 2——都不需要碰代码。

实践五:生命周期管理的异常路径

Session 生命周期管理是资源泄漏的高发地带。MCP 连接、临时目录、工具句柄,任何一处没清理干净都是隐患。

我们用 TDD 把异常路径也覆盖到了,其中最关键的一条是:

MCP 连接关闭时抛出异常,cleanup 不能因此中断,其他资源必须继续释放。

这种行为用自然语言说起来很轻巧,但如果不写测试,就没有任何机制能保证它真的存在,也没法知道它哪天被悄悄改坏了。

实践六:新功能开发从红色测试开始

TDD 最容易被误解的一点是:很多人把它当成"测试覆盖率工具",功能写完了才回头补测试。这样写出来的测试永远是绿的——因为是对着已有实现写的,只能描述现有行为,管不住未来的行为。

真正的 TDD 是从一个注定会失败的测试开始的。功能还没影子的时候,先写一个测试,声明它"应该怎样工作",跑起来,看它报红,然后再动手开发,直到它变绿为止。

方式

测试何时写

测试的作用

补测试(错误方式)

功能完成后

描述已有行为,永远绿色,无约束力

先写测试(TDD)

功能开发前

声明意图,红色是目标,实现服务于测试

在我们的 Agent Hub 开发中,每次新增一项能力,第一步永远是先写一个描述"它应该怎样工作"的测试,跑一遍,看到红色,再去开发。红色不是失败,而是目标。

这个习惯带来的最大改变其实是心态上的:开发者不再问"功能写完了吗",而是问"测试绿了吗"。功能完成的标准,从一件主观的事变成了客观的事。

六条经验

1. Schema 是最便宜的文档

写 Pydantic Model 花的时间和写 dict 差不多,换来的却是类型检查、自动文档、验证报错、IDE 补全。把 Schema 当成一笔投资,而不是负担。

2. 测试默认值不是多此一举

默认值背后是一个设计决策。锁定它,才能防止系统行为被无意中改变。默认值测试其实是在说:"这个值不是随便定的,改之前先想清楚。"

3. 先写测试,逼自己想清楚接口

写测试的时候,你站的是调用方的视角,这个视角能更早暴露接口设计上的问题——比 code review 更早发现,比调试更省成本。

4. Prompt 规范化是 AI 系统的核心工程问题

把 Prompt 写成 YAML、分层管理,让不同层拥有不同的缓存和更新策略——这不是格式上的讲究,而是实实在在的工程问题。

5. 测试是活的契约,不是死的文档

测试会失败,而一个失败的测试其实是在说:"有人正在改动一个受约束的东西。"这是信号,不是噪音。

6. SDD 和 TDD 缺一不可

只有 SDD,规范虽然存在,却没人知道它何时被破坏;只有 TDD,测试虽然覆盖到位,验证逻辑却散落各处。两者结合起来,规范才是唯一的真相,测试则是全天候的自动巡逻。

结语

AI Agent 系统的复杂度不输给传统分布式系统,还多了一层难处:它的行为由 Prompt 定义,而 Prompt 没有编译器。

SDD 和 TDD 替代不了好的架构判断,但它们能把"我们到底达成了什么共识",从人脑和会议记录里搬进代码仓库——变得可搜索、可执行、可验证。

在存量代码中持续迭代,这是我们认为最值得推广的一种工程习惯。

本文基于真实项目经验,代码示例来自生产代码库。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐