CI 质量门禁失败后,研发真正需要的不是“把报错复述一遍”,而是一个可以复核的判断:这次失败是否由当前 Gerrit 变更引入、最早在哪个模块破坏了什么约束、应该修改代码还是测试,以及依据是什么。
这个问题表面上像“读取测试报告后让 LLM 给修复建议”,实际包含三类不确定性:测试平台页面层级复杂且噪声高;报告中的文件行号可能随提交漂移;DDD 项目的异常往往从接口层或集成测试暴露,根因却落在另一个限界上下文的应用服务、领域规则或基础设施适配器中。
我在设计 Test Repair Agent 时,参考了 Open Code Review 的核心取舍:把文件筛选、规则匹配、位置校验等可重复工作交给确定性程序,把需要理解语义和选择证据的部分交给 Agent。 ,将其迁移为“失败诊断—修复建议”的受约束工作流。
测试报告抓取(确定性)
↓
失败上下文与候选代码位置构建(确定性)
↓
Test Repair Skill:读取 → 定位确认 → 诊断分流 → 分析/委派 → 汇总 → 反思校验
↓
可追溯的根因分析与修复建议1. 报告抓取只解决“事实输入”,不让 LLM 参与
专有测试平台通常有三级页面:一级是缺陷类型汇总,二级是同类型问题列表,三级才是单个失败的字段、日志与堆栈。抓取器使用 Playwright 或平台接口逐层读取,展开折叠日志,并保留报告链接和页面定位信息用于追溯。
这里不需要 LLM。页面遍历、文本提取、堆栈解析、路径映射都是确定性任务;若在这一层让模型总结,不仅增加成本,也会把原始事实和模型推测混在一起。
最终给下游的不是三份按页面层级拆开的数据,而是一份以具体失败为单位的 JSON。一个 issue.id 对应一个三级详情页中的一个问题;它可以关联多个代码位置,但不应预先写入“根因”或“修复方案”。
{
"run": {
"repository": "order-service",
"change_id": "I12345",
"revision_sha": "a1b2c3d4",
"report_url": "https://test.example/reports/20260917"
},
"issues": [
{
"id": "001",
"category": "integration_test",
"summary": "创建订单时返回 500",
"url": "https://test.example/reports/20260917/error/info/detail/42",
"facts": [
{"type": "error", "value": "ValueError: buyer_id is required"},
{"type": "operation", "value": "POST /orders"}
],
"locations": [
{
"role": "failure",
"file": "src/order/application/create_order.py",
"line": 120,
"function": "create_order",
"snippet": "command.validate_buyer()"
}
],
"warnings": []
}
]
}字段刻意保持克制:facts 是报告可观察事实,locations 是经过重定位后的候选位置,warnings 记录“报告缺字段、路径无法映射、位置可信度不足”等不确定性。原始 DOM、整段日志和完整堆栈不直接塞进模型上下文;它们保存在证据存储中,只有需要时通过工具按片段读取。
2. 从报告行号到代码位置:先建立位置门禁
测试报告里的构建目录和行号不能直接作为修复位置:构建环境和本地路径不同,报告对应的提交可能落后于当前 revision,行号也会随着 Diff 漂移。
因此,位置定位要先于根因分析,并由程序完成候选生成与校验:
1. 用路径映射将报告路径转换为仓库相对路径;
2. 在 revision_sha 对应代码中校验文件是否存在;
3. 以函数名、异常附近文本、代码片段重新判定;
4. 结合 Gerrit Diff 判断该函数或片段是否受本次变更影响;
5. 生成有限的候选代码块,而不是把整份文件交给模型。
定位结果必须有降级策略。文件、函数、片段三项都能互相印证,才可以给出精确行范围;只有路径可靠时,只输出文件级候选;路径也无法确认时,明确标为 need_evidence,而不是伪造一个精确行号。
这对应 Open Code Review 中“位置定位与校验由工程逻辑保障”的思想。对于 Test Repair Agent,它的意义更大:位置错了,后面再的根因分析也是建立在错误证据上。
3. 从 Open Code Review 迁移什么,而不是照搬什么
Open Code Review 面向“从 Diff 中发现风险”;Test Repair Agent 面向“从失败现象回溯最早被破坏的约束”。两者输入与目标不同,但编排原则可以复用。
| Open Code Review 的思路 | 在 Test Repair Agent 中的迁移 |
| -------------------------- | ------------------------------------------ |
| 先筛选相关 Diff 和文件 | 先筛选失败事实、候选位置和受影响 Diff |
| 为不同文件构建独立上下文包 | 为不同限界上下文构建最小诊断任务包 |
| 规则匹配与 Agent 检索协作 | 错误类型、测试类型、架构层作为诊断策略入口 |
| 位置定位和反思校验 | 位置门禁、证据引用校验、结论一致性校验 |
| Agent 负责语义判断 | Agent 判断失败归因、领域不变量和修复方向 |
不能照搬的部分也很重要。代码审查可从“本次改了什么”开始;测试修复必须先回答“测试为什么失败”。一个失败可能来自业务回归、脏数据或 mock、环境依赖。若一开始就让模型阅读 Diff 并生成补丁,很容易把基础设施问题误判为代码回归,或者建议删除断言来让 CI 变绿。
4. Test Repair Skill:把推理做成有状态的诊断流程
Skill 不应是一段很长的 Prompt,而应是一套带输入、工具权限、阶段产物和退出条件的工作流。
4.1 信息读取:区分事实、候选与未知
Agent 首先只能读取 JSON 中的 facts、locations、当前 revision 和必要的 Diff 摘要。它需要把信息分成三类:
- 已观察事实:异常类型、失败操作、请求/响应、堆栈中的原始位置;
- 经程序验证的候选:已重新锚定的文件、函数、代码块和相关 Diff;
- 未知或冲突:缺少请求 ID、位置重定位失败、日志和断言矛盾等。
这一阶段禁止输出根因和修复结论。这样做并非限制模型能力,而是避免模型把“可能性”写成“事实”。
4.2 位置确认:位置未确认,不允许进入修复判断
Agent 通过受限工具读取候选函数、调用方、测试用例以及必要的 Diff。位置确认至少满足以下任一条件:
- 报告行号、函数名和局部片段一致;
- 报告片段能在当前 revision 中唯一匹配;
- 本次 Diff 修改了与失败符号直接相关的代码块。
否则 Skill 的正确输出是“需要补充证据”或“仅文件级候选”,而不是给出精确到行的结论。这个门禁既减少幻觉,也使后续指标中的“定位准确率”有清晰定义。
4.3 根因分析:从失败现象回溯最早偏离点
根因分析不是根据异常类型直接判断“该改代码还是改测试”,而是构建一条可验证的因果链:
测试预期
→ 测试准备的数据与依赖
→ 被测操作
→ 关键调用链与领域约束
→ 实际返回值 / 异常
→ 断言失败Agent 先从测试中提取“预期契约”:
- 测试准备了什么输入、fixture、mock 和前置状态;
- 执行了哪个接口、命令或事件;
- 断言期望什么结果;
- 实际结果与预期在哪个字段、状态或异常上发生偏离。
然后沿调用链寻找最早出现异常状态的位置。例如接口最终返回 500,只是失败表象;真正需要确认的是:
1. 请求进入应用服务时,输入是否完整;
2. 领域对象构造或领域规则校验时,哪个不变量首次失败;
3. 当前 Diff 是否改变了参数映射、领域规则、接口契约或事件载荷;
4. 若业务代码未偏离,测试 fixture、mock 或环境依赖是否已经不满足前置条件。
每个根因假设都必须由“报告事实 + 代码证据 + Diff 或运行证据”共同支撑:
| 候选结论 | 至少需要的证据 |
| ------------------- | ---------------------------------------------------------------------- |
| 代码回归 | 当前 Diff 改变失败路径,且能解释最早偏离点 |
| 测试期望过期 | 代码行为符合新契约,测试断言仍验证旧行为 |
| fixture / mock 问题 | 测试构造的数据或 mock 返回值违反当前接口契约 |
| 环境或依赖问题 | 失败与业务调用链无稳定关联,日志指向网络、数据库、权限或超时等外部依赖 |
如果存在多个解释,Skill 不应选择“最像”的一个,而应输出候选假设、已有证据和缺失证据。例如需要补充 Trace ID、消息载荷、数据库记录或上游接口响应。
根因的判定标准是:它不仅能解释最终断言为何失败,还能解释调用链中第一个不符合预期的状态或契约为何出现。
4.4 修复方案:明确改哪里、怎么改、怎么验证
确认根因后,Agent 不应直接输出一大段代码,而是先给出清楚的修复计划:
- 改哪里:具体文件和函数;
- 为什么改这里:这里是导致测试失败的直接原因;
- 怎么改:描述需要补充、修改或删除的逻辑;
- 影响什么:是否会影响其他接口、模块或已有逻辑;
- 怎么验证:修改后需要执行哪些测试。
例如,创建订单失败的原因是 customerId 没有传给 buyer_id,那么修复建议可以是:
修改位置:OrderCommandAssembler
修改原因:组装 CreateOrderCommand 时遗漏了 customerId,
导致 buyer_id 为空,后续领域校验失败。
修改方式:补充 customerId 到 buyer_id 的字段映射,
保留 buyer_id 为空时的原有校验逻辑。
验证方式:
1. 补充 customerId 正常传递的集成测试;
2. 补充 customerId 缺失时的异常测试;
3. 重新执行创建订单相关测试。如果修改只涉及当前模块,Agent 可以直接给出修复建议;
如果发现还涉及其他模块的接口、事件或字段契约,
则先让对应模块确认影响,再给出最终方案。
5. 遇到跨模块问题时,如何继续分析
前面的流程适合大多数单模块失败:测试报错后,找到对应函数,确认根因,再给出修复建议。
但 DDD 项目里,一次请求通常会经过多个业务模块。比如“创建订单”接口,既会处理订单数据,也可能需要从用户模块获取用户信息。
创建订单接口
→ 订单服务
→ 查询用户信息
→ 生成订单假设测试最后报错:buyer_id is required。
最后抛异常的位置在订单模块,但真正的问题不一定在订单模块。可能是订单服务忘了传递 customerId,也可能是用户模块返回字段变了,甚至只是测试 mock 少配了一个字段。
因此,遇到跨模块调用时,Agent 不应该只盯着最后报错的位置,而是沿着调用过程确认:
1. buyer_id 在哪个步骤还是正常的;
2. 从哪一步开始变成空值;
3. 这一步是否受到当前 Diff 影响。
如果字段在订单模块内丢失,就修订单模块;如果用户模块返回时已经缺失,就继续检查用户模块或模块间接口;如果测试数据一开始就没有该字段,就修改 fixture 或 mock。
多 Agent 也只在这种场景下才有意义。订单模块负责检查参数是否正确传递,用户模块负责检查返回字段是否变化,最后再汇总判断问题在哪一侧。
这样做的目标不是增加复杂度,而是避免把“最终报错的位置”误认为“真正需要修改的位置”。
6. 多 Agent 只用于真正复杂的问题
多 Agent 不是默认配置。
大多数测试失败只需要一个 Agent:读取报告、查看测试和相关代码、确认根因、给出修复建议。
只有问题明显跨多个模块时,才需要拆分。例如:
- 一个接口同时调用订单、用户、库存等模块;
- 本次 Diff 修改了模块之间的接口字段;
- 测试涉及消息队列、RPC 或异步任务;
- 单模块分析后,仍然无法确认问题是在本模块还是上游模块。
例如,订单模块发现 buyer_id 为空,但无法确认这个字段是在订单模块中丢失,还是用户模块本来就没有返回。这时可以分别检查两个模块,再汇总结果。
这样做的目的不是让系统更复杂,而是让每个 Agent 只关注自己负责的模块,减少无关代码和错误判断。
7. 输出前再做一次检查
Agent 给出修复建议前,系统还需要做一次简单检查,避免“看起来合理,实际上不可靠”的结论进入开发流程。
主要检查:
- 提到的文件、函数和行号是否真实存在;
- 根因分析是否有测试报告和代码证据支撑;
- 行号不确定时,是否老实降级到文件或函数级;
- 修复建议是否真的解决了根因;
- 是否出现“跳过测试”“删除断言”“吞掉异常”这类只让 CI 变绿的方案;
- 如果是网络、数据库或权限等环境问题,是否错误建议修改业务代码。
如果证据不足,系统应该明确提示“需要补充日志或重新执行测试”,而不是强行给出答案。
最终给开发者的内容保持简单:
1. 问题可能在哪;
2. 为什么判断是这里;
3. 建议怎么修改;
4. 修改后需要验证哪些测试。
8. 评测:不只看“模型答对了没有”
评测应把确定性层和推理层拆开,避免把抓取失败、位置漂移和模型误判混为一个分数。对于人工验证的 Gerrit 样本,可以设计如下消融:
| 方案 | 目的 |
| ----------------------------- | ------------------------------------ |
| Diff + LLM | 基线:只给当前变更和失败摘要 |
| 结构化失败上下文 + LLM | 验证报告事实清洗的价值 |
| 上下文 + 位置门禁 + Skill | 验证受约束流程对定位和幻觉的影响 |
| 上下文 + Skill + 按需模块委派 | 仅在跨上下文样本上验证协作收益与成本 |
建议至少记录六类指标:文件/行范围定位准确率、根因判断准确率、输出完整性、无证据主张比例、端到端耗时、Token 消耗。对于“行号准确率”,必须先写清口径:报告行发生漂移时,是要求命中同一可执行语句、同一函数还是固定行号;仅文件级降级不能被计为精确行命中。
还应该比较至少两款模型得不同表现结果。
9. 总结
Test Repair Agent 的关键不是让模型拥有更多上下文,而是让每一段上下文都带着明确职责进入正确阶段:
1. 抓取器把三级报告收敛为稳定、可追溯的事实 JSON;
2. 定位器用路径、函数、片段和 Diff 对抗行号漂移,并允许安全降级;
3. Skill 强制执行“先确认位置,再判断该修什么,再分析根因”;
4. DDD 场景以限界上下文和契约为单位拆分任务,而不是按目录并行;
5. 多 Agent 只服务于已证实的跨边界因果链,并由证据校验器收口;
6. 最终产出不是自动改代码,而是一份可以被研发复核、可继续执行的诊断与修复建议。
这样,系统不只是“会看测试报告的 LLM”,而是一条将不可靠自然语言推理限制在可验证工程边界内的诊断流水线。
参考项目与资料
- Alibaba Open Code Review:确定性编排、上下文筛选、位置校验与 Agent 语义判断协作的主要参考。
- Open Code Review Assurance Case:输入边界、结构化校验和可信输出的工程化思路。
- Repairnator:从 CI 失败触发自动程序修复的早期实践。
- Agentless:分阶段定位、修复和验证的软件工程 Agent 思路。
- AutoCodeRover:面向代码库检索和程序修复的 Agent 工作流。
- SWE-bench:评测软件工程 Agent 的公开基准与任务形式参考。