TDD inside the agent loop - theater or actual value?

TL;DR · AI 摘要
TDD在AI代理循环中未显著提升代码质量,非TDD方案在测试质量上表现更优。
核心要点
- Sonnet 4.6评估显示TDD流程未带来明显质量提升
- Opus 4.8判定非TDD方案设计质量更高
- 实验使用Claude生成差异化的业务逻辑任务
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- TDD在AI代理中的价值验证
- 实验方法
- Sonnet 4.6评估
- Opus 4.8判定
- 核心发现
- TDD无显著优势
- 非TDD质量更优
金句 / Highlights
值得收藏与分享的关键句。
Opus 4.8判定非TDD方案在设计和测试质量上略高
使用Sonnet 4.6进行TDD遵循度评估
Claude生成包含特异性逻辑的任务以增加方案差异性
代理循环中的 TDD:剧场效果还是实际价值?
Birgitta Böckeler
Birgitta 是一位高级工程师和 AI 辅助交付专家。她拥有 20 多年软件开发、架构和技术领导经验。
本文属于“探索生成式 AI”系列。该系列记录了 Thoughtworks 技术人员在软件开发中使用生成式 AI 技术的探索过程。
TDD(测试驱动开发)工作流程可以通过多种方式与 AI 增强的编码结合使用:
- 人类编写测试:人类以某种形式定义测试场景,无论是自然语言、BDD 风格还是直接编写代码。然后由 AI 编写实现代码使这些测试通过(可能第一步会将人类的场景转换为代码)。
- 人类的审查检查点:AI 编写一个失败的测试,人类查看该测试以确认其是否在测试期望的行为,然后 AI 编写实现代码。
- 完全嵌入代理循环:提示代理依次先编写失败的测试,然后编写实现代码并验证之前失败的测试是否通过。
目前来看,最后一种用法是最常见的。但真正让代理完全遵循 TDD 工作流程是否真的有意义?它是否真的能提供价值,还是说这是少数几个对人类有益但对编码代理无关紧要甚至有害的案例之一?
我创建了一个探索性评估方案来初步探讨这个问题并观察结果。这远非全面且结构化的评估结果,但它确实产生了一些假设,供你努力让代理使用 TDD 时参考。
TLDR;根据 Opus 对结果质量的判断,基于 TDD 工作流程与无 TDD 工作流程之间没有明显差异。相反,Opus 多次将非 TDD 工作流程解决方案在设计和测试质量上评为略高。解决方案之间的突变得分也没有显著差异。
实验设置
- 任务:我借助 Claude 创建了小型、中型和大型任务,所有任务均涉及一些业务逻辑的全新实现。我要求其生成大量建议,请求具有独特性和特定性的逻辑,以增加解决方案之间存在差异的可能性,而不仅仅是重复训练数据中已占主导地位的内容。
- 指令:所有运行中均包含实现至少 80% 代码覆盖率的指令。
- 模型:使用 Sonnet 4.6 生成解决方案。
- TDD 遵循程度的判断:TDD 遵循程度的评估也由 Sonnet 4.6 完成。
- 解决方案的判断:Opus 4.8 在不了解解决方案创建方式的情况下,比较了两个解决方案及其测试的质量。我没有提供非常具体的高质量标准输入,因为这是一个非常开放的探索。根据我的经验,如果我给出更具体的输入,模型可能会过度关注我列出的质量标准。Opus 在代码质量判断方面表现出色。对于解决方案的排序,它即兴创建了一个评分标准并传递给所有评估单个解决方案的子代理。
当你根据我的结果得出自己的结论时,需要考虑的主要注意事项是:
- 显然这是一个非常小的样本量,因此请谨慎对待
- 对“质量”的判断几乎完全交由 Opus 决定(仅有少数关于测试质量的指导原则)
- 所有运行都没有完全遵循 TDD,但基本符合
- 提供给代理的编码任务均为全新项目且规模较小,仅涉及业务逻辑
代理在 TDD 方面的表现如何?
在开始之前,我需要确保 TDD 指令确实被遵循。历史上我在这方面并不顺利:代理经常先编写实现再生成测试,跳过确认红色步骤,或过度实现当前测试之外的内容,使下一个测试无需变红即可通过。
最终使用的提示语与 Sonnet 配合良好,足以用于对比,但所有会话在一定程度上都表现出这些失败情况。对于每次 TDD 运行,我让独立代理根据会话记录判断工作流程的执行情况,以避免无意中考虑未真正执行的运行。
结果
我创建了 5 批解决方案,每批包含两个非 TDD 和两个 TDD 解决方案。其中一批还增加了两个仅要求先编写测试(但不完全遵循 TDD 纪律,没有逐步红/绿)的运行。
在小型任务(1 批)和中型任务(3 批)中出现了一些模式:Opus 将两个非 TDD 解决方案评为 #1 和 #2,两个 TDD 解决方案评为 #3 和 #4。只有一次——在我通过更明确的重构和设计评审步骤强化 TDD 提示后——TDD 解决方案排名第一。在同一批中,使用相同提示的另一个 TDD 解决方案却排名最后。对于大型任务,TDD 结果处于中间位置,而两个非 TDD 运行分别占据了最佳和最差位置。
(详见附录)
假设
总结来说,TDD 和非 TDD 在各批次中都同时出现过最佳和最差解决方案,但总体而言 TDD 表现略差。
Opus 分析会话记录后发现,非 TDD 和先写测试的运行总是先完整设计(架构、数据类型、边界情况、契约)再编写任何代码或测试,而不是逐步处理每个需求/测试。这似乎使数据模型更优、跨领域边界情况更全面、功能完整性更高。
TDD 指令实际上阻碍了这种前期设计步骤。这些运行中的设计是从许多局部最小决策的总和中产生的,且很少回顾,因此往往锁定在第一个测试所确定的形状上。代理未考虑编写测试的行为根本不会被实现。
当我与 Ivett Ördög 讨论这个问题时,她提出了一个理论:“AI 代理的训练方式是它们见过完整的函数和这些函数的描述。实际逐步 TDD 示例在训练数据中占比极小。这意味着 LLM 对代码的内部表示是需求到代码的直接翻译,而不是如何达到这种表示的过程。”
TDD 的目标——在代理循环中仍能实现吗?
以下是我对在代理循环中使用TDD(测试驱动开发)的一般性思考,不仅基于本次实验,还包括我个人在使用TDD时的终极目标。这里跳过一些关于测试本身存在的必要性(如重构安全网、活文档、测试覆盖率)以及单元测试的特定目标,专注于TDD工作流中特有的目标。
测试优先 >> 避免同义反复
测试优先的实践使我可以更轻松地断言期望的输出,而不是重复实现细节。这种测试在实现错误时永远不会失败,因为它源自相同的逻辑。当断言与具体实现路径解耦时,测试实际上可以检测行为是否偏离预期。
在代理循环中是否仍然实现?在我的实验中,即使先编写测试,某些TDD会话仍然存在这个问题。一个明显的例子是,测试将实现的输出与自身进行对比,通过重复运行相同代码生成“预期”答案(参见本观察列表中的第4点)。先编写测试并不能可靠地防止这种情况——它可能降低概率,但对LLM来说,这已经是能期望的极限。不过基于这个小数据集,我无法得出关于概率的明确结论。
测试优先 >> 可测试性
测试优先确保代码从一开始就设计为可测试,而不是事后添加复杂且脆弱的测试。
在代理循环中是否仍然实现?实验结果并未给出明确答案。就我选择的任务规模和性质而言,这些任务并未涉及需要暴露设计复杂性的场景。不过在一定程度上,可测试性是驱动设计的必然结果(见下文)。
红绿 >> 测试有效性
观察测试先失败后通过(红绿)的过程,可以证明测试确实能捕获回归问题。
在代理循环中是否仍然实现?当移除人类参与时,这种逻辑是否成立?测试变红只有在有人检查其失败原因时才有意义。当代理既编写测试又确认其失败时,红色测试仅表明代理运行了测试并观察到失败,而非失败原因是否正确。我的实验中TDD遵循度评估也显示了这一点:代理有时会跳过或伪造红色步骤,甚至在测试之前就实现代码使测试立即通过。回归测试的有效性可以通过变异测试进行监控和改进(如我在此处所述)。但跨所有解决方案的变异得分并未显示TDD运行比非TDD运行产生显著更高的变异得分。只要我拥有评估回归质量的机制,具体如何实现并不重要。
在代理循环中是否仍然有效?至少从实验来看,TDD运行中并未展现出更优的设计。现在甚至让我开始怀疑,根据Opus的评分,TDD是否反而让情况变得更糟,因为非TDD方案往往排名更高,而它列出的设计缺陷也让我觉得有道理。但当然,数据集规模太小,无法得出任何明确结论。(如果有人有时间和资源运行更大规模的实验,那将非常有趣!)
当人类先写测试时,迫使我们优先考虑使用场景而非实现细节,必须在明确如何构建功能之前,面对定义行为和预期的摩擦。代理不会经历这种摩擦,可以在规划实现的瞬间同时编写测试。如果没有人类在两者之间进行检查,先写测试是否还有存在的意义?
小步前进 >> YAGNI原则
仅编写足够通过下一个测试的代码是一种克制。其目的是阻止我们构建尚未被要求的抽象或处理案例。
在代理循环中是否仍然有效?这是一种高度依赖人类的益处,当代理自主执行TDD时会完全丧失。我们不再能体验那种必须深入思考所构建内容复杂性的摩擦。理论上这种思考应该转移到编写给代理的规范时,但目前我们缺乏类似TDD的机制来逐步推敲规范。难道不能让代理以小步方式工作,并在发现可能不必要的内容时向我们提问吗?根据我的一般经验,它们在这方面表现不佳。在实验中,最小实现的指令也未能有效阻止它们构建更多内容。它们经常过度实现,超出当前测试需求,因为它们能访问完整的功能要求。我们通常不会逐条喂给代理规范,那样效率会非常低。
小步前进 >> 快速、局部反馈
一次只迈出一小步意味着当测试失败时,我能几乎精确地知道原因,因为自上次通过状态以来唯一改变的就是你刚刚编写的内容。
在代理循环中是否仍然有效?实验设置并未显示代理在有无TDD情况下调试时陷入困境的频率差异。但根据我的一般经验,即使没有刻意采取小步前进的方式,代理通常也能合理地判断测试失败的原因。我仍然怀疑,当它们陷入困境时,小步TDD是否能真正有效缓解问题,以及整体的成本收益比是否仍然成立。
小步前进 >> 信心与学习
Kent Beck在《测试驱动开发实例》的序言中,对TDD的最大理由是"管理恐惧"。他说对困难问题的合理恐惧会让开发者变得犹豫、不善沟通且回避反馈。通过TDD,每个通过的测试都向我们展示进展,让我们可以安心知道进展已被锁定。测试是一种心理机制,帮助我们持续前进。
在代理循环中是否仍然有效?这本质上是关于管理人类的恐惧并给予人类放松许可的。当代理在循环内部执行TDD时,这种益处无法转移,因为它无法像我们自己逐步执行TDD时那样给予我相同的控制感和信任。
成本
至少消耗3倍的token
详见附录中的详细数据。
当然,由于 TDD 工作流程需要更多步骤和工具调用,因此会消耗更多 token。然而,其中许多内容会命中缓存,因此请注意,token 数量 3 倍或更高的倍数并不直接代表成本增加的幅度。(不幸的是,我在实验过程中没有跟踪缓存命中情况。)
提示维护与测试
TDD 是一种对模型来说似乎并不“自然”的过程。这就像在训练数据面前进行一场艰难的斗争,需要对提示进行大量迭代,才能让模型大多数时候遵循该流程。例如,在我完成第一批测试后意识到代理在红-绿-重构循环中几乎没有进行重构时,我修改了提示,更加强调这一步骤,因为这显然对 TDD 至关重要。之后我让 Opus 回顾这些会话,看看它是否发现重构努力有所提升。Opus 确实报告了重构步骤的增加——不过,它也列举了一些案例,其中代理试图重构,但最终认为设计已经足够好,即使 Opus 明显认为设计存在问题(例如,所有功能都实现在一个大型模块中,而显然可以拆分为多个职责模块)。
TDD 是一套相对复杂的指令,包含大量变量,因此代理对其解释方式存在大量差异。因此,我认为这类提示在不同模型间的波动性可能比简单指令更大,这意味着需要付出努力才能确保提示在不同模型和模型版本间持续有效。
我的结论
我认为,目前越来越多的证据表明,对模型执行任务的方式过于具体的要求并不可持续。相反,我们应该尽可能寻找多种方式监控结果并提供反馈。这些反馈应尽可能自动化,同时我们需要仔细思考在哪些环节作为评判者介入,判断什么是“好”和“正确”的标准。
尽管我清楚我的小规模评估远不能代表 TDD 有效性的全面视角,但它确实没有给我任何新的证据表明这些努力是值得的。特别是如果我们能找到其他方法实现 TDD 的大部分好处,那就更不用说了。
我个人已经停止要求编码代理先写测试,更不用说执行 TDD(老实说,我从未真正这样做过),直到看到评估或其他有力论据说服我为止。我目前更关注在代理循环之外使用 TDD 时的益处,并探索实现这些益处的替代方法。
如何获得良好的回归测试?
……以便在现有功能失效时,代理和我都能接收到信号
我仍然重视扎实的回归测试,因为尽管代理当然可能以错误的方式修复红色测试,但至少红色测试能为其提供反馈信号,让它重新检查可能已失效的原有需求。我通过变异测试来监控和改进回归测试质量,而不是通过提供详细的 TDD 指令并寄希望于最佳结果。
如何将常规重构融入流程?
……以便代码库始终保持易于修改的状态
重构仍然至关重要,但在代理循环中,传统TDD的小步迭代似乎并不是一种高效或有效的重构方式。一些重构触发器示例:为代理提供静态代码分析访问权限;定期审查结构和模块化程度;建立团队仪式以保持对代码库的良好理解并及早发现偏离;关注每次变更涉及的文件数量趋势,以及每次变更的token数量。
如何获得信心?
……这样才能让我敢于将代码推送到生产环境
最困难的问题仍然是:我们如何获得TDD曾给予我们的信心?如何管理恐惧?如何锁定进展?我目前还没有明确的答案,但我想提一个看似能作为构建块的方案:我最近尝试了Ivett Ördög倡导的Approved Scenarios方法。用我的话来说(她可能不完全认同),这是一种由每个应用的定制测试运行器支持的半手动测试方式。该运行器以易于理解的方式向我展示功能测试场景,并允许我在彻底确认后“冻结”这些预期(场景/固定装置)。当未来这些冻结的预期被违反时,我必须重新批准它们。我的同事Matteo Vaccari在这里详细介绍了他使用该方法的体验。
无论未来是什么让我们对软件产生信任和信心——我认为我们所熟知的TDD角色在GenAI时代之后将显著缩小。
附录:根据Opus评估的结果
你可以在该仓库中找到完整结果。
- NT = 无TDD指令
- T = TDD指令
- TF = 测试优先指令
所有批次中的token使用情况
按任务规模分解:
| 任务 | NT平均token | T平均token | T/NT倍数 | |------|-------------|------------|----------| | 小型 | 119,815 (n=2) | 1,018,245 (n=2) | 8.50x | | 中型 | 736,486 (n=2) | 2,181,105 (n=6) | 2.96x | | 大型 | 253,621 (n=2) | 1,239,408 (n=2) | 4.89x |
仅中型任务添加了测试优先指令 注意:这些数字只是会话成本的粗略代理,而非衡量解决方案中代码量或思考量的指标。记录token使用情况的设置使用了pi-coding-agent SDK的getSessionStats(),该方法会统计会话中每个助手回合的输入+输出+缓存读取+缓存写入使用量。由于每次回合都会重新读取累积的上下文,而每次重新读取(通常大部分来自缓存)都会在该回合的缓存读取中被再次计数,因此“总token数”更多地跟踪了会话所用回合数,按上下文增长规模加权。因此它将廉价的缓存读取token与昂贵的新token同等加权,很可能高估了TDD的真实成本。鉴于样本量较小,将倍数视为方向性指标:TDD的成本可靠地高出数倍,具体多少倍是可变的。
中型任务,第一轮
任务:构建一个4阶段Python流水线(解析→聚合→格式化→验证),将原始ROW_ID:CATEGORY:VALUE:PERIOD字符串转换为纯文本报告。
数据
| ID | TDD | 测试数量 | 覆盖率 | 突变得分 | 总token | 回合数 | 工具调用 | |----|-----|----------|--------|----------|---------|--------|----------| | NT1 | 否 | 75 | 100% | 84.2% | 769,814 | 31 | 37 | | NT2 | 否 | 107 | 89.6% | 703,159 | 21 | 24 | | T1 | 是 | 30 | 81.0% | 1,519,762 | 71 | 28 | | T2 | 是 | 34 | 99% | 77.3% | 2,580,897 | 103 | 60 |
总体结论
排名
结论
1
按阶段划分的模块,dataclasses,Decimal;无正确性错误,最强的错误处理,仅检查重复的ROW_ID;验证存在自引用但无害
2
按阶段划分的模块,dataclasses,float/round;工程化最佳且测试套件最完整,但验证器拒绝自己的有效小数输出(误拒错误);TOTAL行从未验证
3
单一模块,dicts,float;核心阶段正确但验证存在循环(重新运行格式化器);接受nan/inf,忽略重复的ROW_ID
4
单一模块,dicts,float;活跃的TOTAL行错误(人数统计转换为美元)被测试固化;完全缺失验证检查#3
(本轮排名仅基于结论、测试数量、覆盖率和变异得分——Opus的设计/代码/测试子评分从下一轮开始引入)
中等任务,第2轮
任务:与01-medium相同的4阶段报告流水线,以更严格的TDD实践重新运行,并新增一个以测试优先的变体(NT1/NT2复用01-medium的代码库)
方法
无TDD
20
TF2
测试优先
90
92%
619,531
27
26
29
98%
2,099,280
96
95
TF1
62
268,323
17
16
25
2,017,739
89
设计
代码
测试
平均
实现/测试LOC
8
8.0
497 / 881
7
484 / 850
7.5
330 / 430
6
6.5
207 / 304
6.0
348 / 360
142 / 228
最完整的测试套件,最干净的验证复用格式化器布局;HEADCOUNT未受测试保护地混入美元总计
全程使用Decimal,强大的解析/验证;检查3是不可达的死代码,验证模块杂乱
设计整洁,Decimal,正确解析;测试更精简,包含一个无操作测试,验证存在自引用
整洁的主流程,正确格式化;对畸形输入崩溃而非返回结构化解析错误
5
单文件方案中最强的解析器;TOTAL行损坏(人数统计作为美元),死代码被发布
最紧凑(142行代码);对畸形输入崩溃,验证最同义反复,测试套件最薄弱
中等任务,第3轮(改进的TDD指令)
任务:与01-medium相同的4阶段报告流水线,使用改进的TDD提示(强调前期设计和重构)重新运行;NT1/NT2再次复用01-medium的代码库
51
90.2%
3,447,283
117
116
43
81.1%
1,421,671
61
7.67
7.33
7.0
6.67
主要弱点
仅输出HEADCOUNT的TOTAL行以$显示;验证是子字符串检查而非算术运算
小数HEADCOUNT→在有效输入上引发虚假的ValidationError;测试依赖猴子补丁
NaN/Infinity导致流水线崩溃而非ParseError;验证重新运行格式化器
验证是同义反复(检查聚合项本身);宽度检查仅是注释
小型任务
任务:构建一个Python模块,验证格式为DAY-TIME-ROOM-CHECKSUM的医疗预约时段代码,返回结构化结果标识哪个规则失败及原因
122,108
10
15
58
92.3%
117,522
93.6%
894,451
55
93.2%
1,142,039
68
9
8.67
总体最佳——dataclass结果,无错误,61个带原因断言的测试
非常接近——dataclass结果,但Unicode数字规范存在偏差
正确且整洁,但使用dict结果+测试更少+存在死代码
最差设计(自由文本错误)+真实崩溃错误
更大任务
任务:构建一个内存中的Python忠诚度积分引擎,包含分级积分获取率(青铜/白银/黄金),365天消费跟踪用于分级重新计算,以及积分兑换功能
69
86.9%
322,148
14
13
22
85.6%
1,225,517
63
85.2%
1,253,300
67
/
66
74
89.4%
185,094
11
正确性
8.25
6.75
唯一包含真实输入验证的方案;精确的边界测试;仅存在少量购买顺序异常的边缘情况
类型清晰的数据模型,所有核心规则正确;缺少错误处理,存在重复购买ID的缺陷,存在无效状态字段
功能上最正确(探测未发现任何缺陷);未类型化的嵌套字典,存在遗留结构,测试用例最少
设计评分最高,74个测试用例——但存在两个严重缺陷:批量提取顺序错误;未来日期的积分被错误计入可消费额度
致谢
感谢 Ivett Ördög、Matteo Vaccari、Dan Mutton、Lukasz Plotnicki 和 Emily Bache 花费时间进行评审,并提供宝贵的反馈和讨论,帮助改进本文内容。
本文使用 GenAI 进行研究,整合思路构建结构,并润色语言。
最新文章(8月10日):
TDD inside the agent loop - theater or actual value?
上一篇文章:
The Economic Benefit of Refactoring