规范先行,测试兜底:在存量代码中构建 AI Agent 系统
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 替代不了好的架构判断,但它们能把"我们到底达成了什么共识",从人脑和会议记录里搬进代码仓库——变得可搜索、可执行、可验证。
在存量代码中持续迭代,这是我们认为最值得推广的一种工程习惯。
本文基于真实项目经验,代码示例来自生产代码库。
更多推荐


所有评论(0)