TDAD: 用图式测试影响分析降低 AI Coding Agent 回归

"Tell the agent which tests to check, not just how to do TDD"

Posted by zwt on March 19, 2026

0. 论文信息

  • 标题:Test-Driven Agentic Development: Reducing Code Regressions in AI Coding Agents via Graph-Based Impact Analysis
  • 中文意译:测试驱动的智能体开发:用图式影响分析减少 AI 编程智能体的代码回归
  • 链接:https://arxiv.org/abs/2603.17973
  • PDF:https://arxiv.org/pdf/2603.17973
  • 时间:2026-03-18(v1)
  • 代码:https://github.com/pepealonso95/TDAD
  • 备注:本文原文可获取;下面的实验数字优先来自 arXiv 正文与项目 README。若某些实现细节作者未完全展开,我会明确标注。

1. 先说结论

今天我会选这篇。

原因很简单:它不是再做一个“会写代码的 agent”壳子,而是盯住一个更实际的问题——agent 修好了当前 issue,却顺手把原来通过的测试搞挂了。这件事在真实代码审查里比“单点修复率”更接近能不能 merge。

我的判断:

  • 作者声称:TDAD 通过代码-测试图谱 + 影响分析,把需要验证的测试提前暴露给 agent,可显著降低回归。
  • 实验观察:在 SWE-bench Verified 上,Qwen3-Coder 30B 设定下,test-level regression rate 从 6.08% 降到 1.82%,P2P failures 从 562 降到 155;同时发现 TDD prompt 单独使用反而更差(9.94%)。
  • 我的判断:这篇最有价值的点不是“GraphRAG”这个词,而是它给了一个非常务实的经验——对 coding agent,提供结构化上下文(哪些测试最该跑)往往比给一大段流程口号(你要按 TDD 做)更有用。 这点很值得做进真实 agent 系统。

2. 它在解决什么问题?

问题不是 agent 会不会修 bug,而是:

修一个 bug 的 patch,是否会把一堆原本通过的测试搞坏?

作者把这个问题叫 regression。在 SWE-bench 这类 benchmark 里,大家通常盯的是 resolution rate(问题有没有修好),但实际工程里更重要的是:

  • 你改动后有没有破坏别的模块;
  • 你有没有因为共享接口变化,引发跨文件、跨模块连锁损坏;
  • 你跑的测试是不是只覆盖了改动附近,而漏掉了真正受影响的地方。

作者给出的具体动机很强:

  • vanilla agent 在 100 个实例里造成 562 个 pass-to-pass 测试失败
  • 平均每个生成补丁会打坏 6.5 个测试
  • 在个别案例里,一个 patch 能把 322 个原本通过的 P2P 测试全打挂。

这不是边角问题,而是默认会发生的问题。

3. 核心思路一句话

先离线把仓库索引成“代码—测试依赖图”,再在 agent 修代码时,把“最可能受影响的测试集合”以一个轻量技能形式喂给它,让它优先验证这些测试,而不是盲跑或只靠 TDD 提示词。

4. 方法拆解(按可复现视角写)

4.1 系统总览

TDAD 分两阶段:

阶段 A:离线建图 / 索引

给一个 Python 仓库,解析出:

  • File 节点
  • Function 节点
  • Class 节点
  • Test 节点

以及五类边:

  • CONTAINS:文件包含函数/类
  • CALLS:函数调用函数
  • IMPORTS:文件导入文件
  • TESTS:测试覆盖/关联代码实体
  • INHERITS:类继承类

阶段 B:在线影响分析

当 agent 改了若干文件后,TDAD 对这些 changed files 做 impact analysis,输出:

  • 哪些测试最可能受影响;
  • 按置信度打分后的测试清单;
  • 一个静态 test_map.txt 供 agent 用 grep 查询。

重点是:运行时不需要图数据库服务,不需要 MCP,不需要额外 API。 agent 只拿到一个 text map 和一份很短的 skill 说明,就能用。

4.2 图谱 schema

原文明确写了 4 类 node、5 类 edge:

Node types

  • File:Python 源文件,关键属性包括 path, content_hash
  • Function:顶层函数,关键属性包括 name, file, lines, signature
  • Class:类定义,关键属性包括 name, file, bases
  • Test:测试函数或测试方法,关键属性包括 name, file, is_test

Edge types

  • CONTAINSFile -> Function/Class
  • CALLSFunction -> Function
  • IMPORTSFile -> File
  • TESTSTest -> Function/Class
  • INHERITSClass -> Class

如果你要自己复现,一个最小可行版本其实就是:

  1. 用 AST 建 File/Function/Class/Test
  2. 先把 IMPORTS + CALLS + 粗粒度 TESTS 做出来;
  3. 后面再补更细的 linking heuristics。

4.3 Indexing pipeline

(1) AST parser

作者用 Python 标准库 ast 解析仓库。 抽取内容包括:

  • 函数定义:签名、行号范围、docstring
  • 类定义:基类、方法
  • import 语句
  • 函数体内 call target

测试识别规则也很朴素:

  • 文件名:test_*.py*_test.py
  • 函数名:test_*
  • 类名:Test*

(2) Graph builder

把解析结果转成图:

  • 每个文件一个 File node;
  • 文件里的函数/类建立 CONTAINS
  • import 建 IMPORTS
  • inheritance 建 INHERITS
  • call target 建 CALLS

(3) Test linker

这是最关键的部分,因为真实 Python 仓库里 test 组织方式很乱。

作者给了 3 个按优先级排列的 linking 策略:

  1. naming convention:比如 test_foo.py -> foo.py
  2. prefix matching:逐步截断 test 文件 stem,找最像的 source 文件
  3. directory proximity:当多个候选都像时,用目录距离判定

此外还专门提到:

  • 对 Django 这类把测试堆在 tests.py 的情况,做了 proximity-based mapping。

这意味着它并不是靠昂贵动态分析,而是靠一组静态但足够工程化的 heuristics。

4.4 Impact analysis

给定 changed files 后,TDAD 并行使用 4 种策略打分,再合并:

  • direct TESTS edge
  • transitive call chain
  • coverage-style linkage
  • import-based relation

原文给了一个统一评分形式:

score = (1 - c_w) * w_strategy + c_w * confidence

其中:

  • c_w = 0.3
  • confidence ∈ [0,1]
  • 论文列出的典型 confidence:
    • direct TESTS:1.0
    • transitive call:0.56
    • coverage:0.5
    • imports:0.45

测试选择分三级:

  • high:>= 0.8
  • medium:0.5 ~ 0.8
  • low:< 0.5

默认最多返回 50 个测试

作者还提供三种权重配置:

  • conservative:偏 precision
  • balanced:默认
  • aggressive:偏 recall

4.5 它怎么接入 agent?

这是这篇最聪明也最务实的部分。

TDAD 最终给 agent 的不是一个复杂服务,而是两个静态工件:

  1. test_map.txt
  2. SKILL.md

其中 SKILL.md 被作者收敛成一个很短的操作说明,大意只有三步:

  1. 修 bug
  2. grep test_map.txt 找相关测试
  3. 跑这些测试并修回归

作者特别强调,这个 skill 最终只有 20 行。而且在 auto-improvement loop 里,把说明从 107 行压到 20 行,本身就是最大增益来源之一。

我的理解:

  • 对小/中型本地模型,长 prompt 经常不是帮助,而是噪声;
  • 真正有价值的是“定位信息”而不是“流程说教”;
  • 所以 TDAD 更像是一个 context tool,而不是一个复杂 orchestration framework。

4.6 Backend 架构

原型最初使用:

  • Neo4j + Docker

后续迭代迁移为:

  • NetworkX in-memory backend(默认)
  • 持久化:.tdad/graph.pkl
  • 安装:pip install tdad
  • Neo4j 仍可选,通过 TDAD_BACKEND=neo4j

这点对可复现很关键:

  • 你不需要先搭一个数据库才能试;
  • 在 agent 系统里也更容易落地。

5. 实验设置

5.1 Benchmark

  • 数据集:SWE-bench Verified
  • 来源:12 个流行 Python 仓库,共 500 个经人工验证的 issue 实例
  • 每个实例包括:
    • issue 描述
    • 仓库快照
    • FAIL_TO_PASS 测试
    • PASS_TO_PASS 测试

论文里分两阶段:

  • Phase 1:100 个实例
  • Phase 2:25 个实例

5.2 模型与 agent 设定

Phase 1

  • 模型:Qwen3-Coder 30B
  • 量化:Q4_K_M 4-bit
  • 推理:llama.cpp
  • context window:32K
  • temperature:0
  • 每个实例 patch 生成预算:15 分钟

Phase 2

  • 模型:Qwen3.5-35B-A3B
  • 量化:4-bit
  • 推理:MLX on Apple Silicon
  • agent:OpenCode v1.2.24

作者解释为什么故意不用最豪华设定:

  • 想证明 TDAD 不依赖 frontier 模型;
  • 想让实验更便宜、更可复现;
  • 想在“上下文特别珍贵”的资源约束条件下测试它。

5.3 评测指标

论文用了四个指标:

  • Resolution rate:所有 F2P 测试都通过的比例
  • Generation rate:是否生成了非空 patch
  • Test-level regression rate:总 P2P failures / 总 P2P tests
  • Instance-level regression rate:有至少 1 个 P2P failure 的 patch 比例

作者明确主张:

  • test-level regression rate 才是主指标,因为它能区分“只坏 1 个测试”和“炸掉 322 个测试”的严重性差异。

6. 关键实验结果(数字尽量逐字保留)

6.1 Phase 1:100 个实例上的回归控制

作者报告:

Vanilla baseline

  • Resolution: 31%
  • Test-level regression rate: 6.08%
  • Total P2P failures: 562

TDD prompting

  • Resolution: 31%
  • Test-level regression rate: 9.94%
  • Total P2P failures: 799

GraphRAG + TDD(TDAD)

  • Resolution: 29%
  • Test-level regression rate: 1.82%
  • Total P2P failures: 155

直接看结论:

  • 相比 vanilla,P2P failures 从 562 -> 155,减少约 72%
  • 相比 TDD-only,P2P failures 从 799 -> 155,减少约 81%
  • test-level regression rate 从 6.08% -> 1.82%,降低约 70%

灾难性回归案例

论文举了几个特别说明问题的例子:

  • astropy-13977:vanilla 把 322 个 P2P 测试全打挂;GraphRAG+TDD 只坏 12 个
  • django-13089:TDD prompting 单独使用把失败数从 4 直接放大到 352/352 全挂

这非常说明问题:

  • “多提示词 + 更认真做 TDD”不等于更安全
  • 如果没给 agent 结构化定位信息,它可能只是更积极地改更多代码。

6.2 代价:resolution 小幅下降

TDAD 的 resolution 从 31% 降到 29%,下降 2 个百分点

作者解释是:

  • TDAD 会让 agent 在高风险情况下更常 abstain,导致 empty patch 增多;
  • 换句话说,它更保守。

论文里的原话方向是:agent “knew what it didn’t know”。

我的判断:

  • 这在真实工程里其实未必是坏事;
  • 一个不确定就少出手的 agent,往往比“什么都敢改”的 agent 更可部署。

6.3 Phase 2:作为可复用 skill 接入另一个 agent 栈

在另一个模型 + agent 组合上,作者把 TDAD 作为 skill 重新评估:

Metric Baseline TDAD Skill Delta
Resolved 6/25 (24%) 8/25 (32%) +8pp
Generated 10/25 (40%) 17/25 (68%) +28pp
Res. of generated 6/10 (60%) 8/13 (62%) +2pp
Empty patches 15 8 -7
Regression Rate 0% 0% 0pp

这里的机制和 Phase 1 不一样:

  • Phase 1 主要体现为 减少回归
  • Phase 2 则更像是 帮助 agent 理解代码结构,从而更愿意、也更能产出 patch

也就是说,代码-测试图不仅是安全工具,也是导航工具。

7. Ablation / 分析里最值得记的点

7.1 TDD prompting paradox

这是本文最值得记的一条经验。

作者观察到:

  • TDD prompting 单独使用,回归率反而从 6.08% 升到 9.94%

论文给了两个解释:

  1. verbose prompt 挤占上下文:30B 小模型更需要仓库上下文,而不是一大段程序化说明
  2. ambition without localization:agent 因为“被要求更认真”而做了更大修改,但并不知道该验证哪些测试,所以 collateral damage 更多

作者还做了相关 ablation:

  • 只缩短 prompt,不给 graph context,不能解释 TDAD 改善;
  • 只加长 prompt,不给 graph context,也没有带来帮助;
  • 真正关键的是:short prompt + graph-derived context

这条结论我非常认同。

7.2 Auto-improvement loop

作者还做了一个外层自动改进循环,灵感来自 Karpathy 的 autoresearch。

流程大意:

  1. agent 每轮只改一个点
  2. unit tests 先 gate
  3. 小规模 benchmark 评估
  4. 好了就更新 best snapshot,差了就回滚

15 次迭代里:

  • 接受 4 次
  • 拒绝 11 次

最佳结果:

  • Generation:28% -> 80%
  • Resolution:12% -> 60%
  • Regression:全程 0%(10-instance eval)

其中最大增益来自:

  • SKILL.md 从 107 行精简到 20 行,resolution 直接从 12% -> 50%

这是一个很强的实证信号:

  • 对 agent 工具设计,简洁而高信息密度 经常比“完整流程说明”更重要。

8. 局限与适用边界

8.1 只覆盖 Python 生态

当前方法明显是围绕 Python 仓库设计的:

  • 依赖 Python AST
  • 测试命名与目录规则也都是 Python 习惯

如果迁移到 JS/TS、Java、Rust,多半需要重写 parser 和 linker。

8.2 静态分析的上限

它主要依赖:

  • AST
  • import
  • naming convention
  • directory proximity

这意味着对动态分发、反射、运行时 monkey patch、复杂 fixture/参数化测试,可能会漏链或误链。

8.3 结果仍带资源设定依赖

论文刻意用资源受限设定做实验,这有优点,但也意味着:

  • 在更强模型、更复杂 agent scaffolding 下,收益幅度未必完全一样;
  • 尤其 resolution 的 trade-off,可能会变化。

8.4 Phase 2 样本偏小

第二阶段只有 25 个实例,而且 regression rate 两边都是 0%。 这让它更像一个“迁移可行性证明”,还不是强统计结论。

9. 复现清单(Checklist)

9.1 数据

  • benchmark:SWE-bench Verified
  • 需要拿到每个实例的:仓库快照、issue 描述、F2P、P2P tests

9.2 索引构建

  • 解析所有 Python 文件:ast
  • 建四类 node:File / Function / Class / Test
  • 建五类 edge:CONTAINS / CALLS / IMPORTS / TESTS / INHERITS
  • 重点实现 test linker:
    • naming convention
    • prefix matching
    • directory proximity
    • 针对 monolithic test modules 的特殊规则

9.3 影响分析

  • 输入:changed files
  • 四路并行策略:
    • direct TESTS
    • transitive calls
    • coverage-style linkage
    • imports
  • 合并策略:保留最高分
  • 默认 top tests:50
  • profile:balanced(论文默认)

9.4 Agent 接入

  • 导出静态 test_map.txt
  • 准备精简 SKILL.md
  • agent 运行流程:
    1. 修 bug
    2. grep test map 找受影响测试
    3. 运行这些测试
    4. 若失败则继续修正

9.5 实验配置

Phase 1 近似复现

  • Qwen3-Coder 30B
  • 4-bit quantization(Q4_K_M)
  • llama.cpp
  • 32K context
  • temperature 0
  • 每题 15 分钟预算

Phase 2 近似复现

  • Qwen3.5-35B-A3B
  • 4-bit
  • OpenCode agent

9.6 评测

用 SWE-bench Docker harness:

  1. checkout base commit
  2. apply patch
  3. 跑 F2P
  4. 跑 P2P
  5. 记录 resolution / generation / test-level regression / instance-level regression

10. 我的判断:值不值得深读?

值得。 但我会把“值得读”的原因分成两层。

10.1 为什么值得读

第一层:问题选得对

它盯住的是 coding agent 最实际的失败模式之一:

  • 不是“能不能写出 patch”
  • 而是“能不能不把仓库搞坏”

第二层:方法足够工程化

它没有堆很重的在线系统:

  • 不是一个复杂平台;
  • 更像一个随时能加进现有 agent stack 的 skill。

第三层:给了一个很可迁移的设计原则

对 agent 来说,好的结构化上下文,常常比详细流程指令更重要。

这个原则不只适用于测试影响分析,也适用于:

  • tool selection
  • repo navigation
  • browser agent 的 action grounding
  • multi-agent 的 task routing

10.2 我保留的疑问

  1. 如果换成更强的 frontier coding agent,TDAD 还能有多大边际收益?
  2. 静态 test linkage 在超大仓库、复杂 fixture、生成式测试下会不会显著退化?
  3. 如果把 runtime trace / coverage 真正纳入,会不会进一步提升 precision?
  4. 真实企业 CI 里,除了 test regressions,还要不要把 lint、type-check、benchmark regression 一起纳入 impact map?

10.3 如果我要把它用起来

我会优先做两件事:

  • 短期:把它当成 coding agent 的一个 repo-level verification skill,用在 patch 生成后的本地验证环节;
  • 中期:把 test impact map 扩成更一般的“change impact map”,把 type checks、linters、build targets、benchmark suites 一起串进去。

11. 一句话总结

这篇论文真正有价值的,不是又做了个 agent,而是把“coding agent 的回归控制”从口号变成了一个可以落地的、轻量的、可复现的工具设计。