Bugfix 任务模板:日志、复现、补丁、回归测试四段式
用 Agent 修 Bug,最怕的是"修好了但不知道修好没"。本文给出一套可复用的 Bugfix 任务模板,把缺陷修复拆成日志采集、稳定复现、最小补丁、回归测试四个阶段,每个阶段都有对应的 Prompt 模板、上下文包模板和验收标准,让 Agent 修 Bug 不再是"碰运气"。
一、为什么 Bugfix 需要模板化
一个典型的 Agent Bugfix 流程是这样的:开发者把 Issue 链接扔给 Agent,Agent 读完 Issue 就开始改代码。问题在于——Issue 里的信息通常不够:错误日志只截了一半、复现步骤缺失、涉及文件没标注。Agent 只能靠猜,猜错了就反复改,最终提交一个"看起来能用"的补丁。
把"碰运气"换成"四段式":
| 阶段 | 没有模板 | 有四段式模板 |
|---|---|---|
| 日志采集 | Agent 自己翻日志,可能漏掉关键堆栈 | 日志模板指定时间范围、服务名、错误码、完整堆栈 |
| 复现 | "在我这里跑不起来" | 复现脚本 + 最小数据集,Agent 和人都能跑 |
| 补丁 | 改了一大片,不知道改对了没 | 限定修改范围,只动根因相关代码 |
| 回归测试 | 手动点一遍 | 自动化回归用例,CI 直接验证 |
模板的价值不是限制 Agent,而是把人的经验固化成 Agent 可以执行的步骤。
二、四段式流程架构
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 第一段:日志 │───▶│ 第二段:复现 │───▶│ 第三段:补丁 │───▶│ 第四段:回归 │
│ 采集与分析 │ │ 脚本与数据 │ │ 最小化修改 │ │ 测试与验收 │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ │ │ │
输出: 输出: 输出: 输出:
· 错误堆栈 · repro.py · diff.patch · test_report.json
· 影响范围 · fixture data · 修改说明 · 覆盖率报告
· 候选文件 · 预期 vs 实际 · 关联测试 · 验收结论每个阶段都有输入、处理和输出。上一段的输出是下一段的输入。如果某一段无法完成(比如日志不足以定位问题),流程回到上一段补充信息,而不是硬着头皮往下走。
三、第一段:日志采集模板
3.1 Prompt 模板
# bugfix-phase1-log-collection.yaml
name: Bugfix-Phase1-日志采集
version: "1.0"
description: 从错误报告和日志中采集完整的缺陷信息
inputs:
issue_url: # Issue 链接或错误报告
log_source: # 日志来源:文件路径 / ELK / CloudWatch
time_range: # 日志时间范围
service_name: # 服务名(微服务场景)
prompt: |
你是一个缺陷分析专家。请根据以下信息完成日志采集:
## 已知信息
- Issue:{{issue_url}}
- 日志来源:{{log_source}}
- 时间范围:{{time_range}}
- 服务:{{service_name}}
## 任务
1. 提取完整的错误堆栈(不要截断)
2. 识别错误类型(NullPointerException / TimeoutError / DataError / ...)
3. 标注错误发生的代码路径(文件:行号)
4. 列出可能相关的上下文日志(错误前后 30 秒)
5. 给出候选文件列表(最可能包含 Bug 的文件,按可能性排序)
## 输出格式
严格按以下 JSON 输出:
```json
{
"error_type": "",
"error_message": "",
"stack_trace": "",
"code_path": ["file:line", ...],
"context_logs": ["..."],
"candidate_files": [{"path": "", "reason": "", "confidence": 0.0}],
"impact_scope": {"affected_modules": [], "affected_users": "unknown"}
}
### 3.2 日志采集示例输出
```json
{
"error_type": "DataError",
"error_message": "Cannot read property 'amount' of undefined at OrderService.calculateTotal",
"stack_trace": "TypeError: Cannot read property 'amount' of undefined\n at OrderService.calculateTotal (src/services/order.js:142)\n at OrderController.checkout (src/controllers/order.js:58)",
"code_path": ["src/services/order.js:142", "src/controllers/order.js:58"],
"candidate_files": [
{"path": "src/services/order.js", "reason": "直接报错位置,order.items 为 undefined", "confidence": 0.85},
{"path": "src/controllers/order.js", "reason": "调用方,可能传入了不完整的 order 对象", "confidence": 0.45}
],
"impact_scope": {"affected_modules": ["order", "checkout"], "affected_users": "unknown"}
}四、第二段:复现模板
4.1 Prompt 模板
# bugfix-phase2-reproduction.yaml
name: Bugfix-Phase2-稳定复现
version: "1.0"
inputs:
phase1_output: # 第一段输出
repo_path: # 仓库路径
test_env: # 测试环境配置
prompt: |
你是一个测试工程师。根据第一段的日志分析结果,编写稳定的复现脚本。
## 错误信息
{{phase1_output.error_message}}
## 候选文件
{{phase1_output.candidate_files | map('path') | join(', ')}}
## 任务
1. 编写最小复现脚本(Python 或 Shell),不依赖生产数据库
2. 准备 fixture 数据(JSON 格式),模拟触发 Bug 的输入
3. 断言"错误情况"(当前行为)和"期望情况"(修复后行为)
4. 脚本必须能在本地 30 秒内运行完成
## 输出
- tests/reproduce/test_bug_<issue_id>.py
- tests/reproduce/fixtures/<issue_id>.json
- 复现命令:pytest tests/reproduce/test_bug_<issue_id>.py -v4.2 复现脚本示例
# tests/reproduce/test_bug_1234.py
"""
Issue: #1234 订单结算时 amount 为 undefined 导致 500
复现条件:订单包含已下架商品时,items 字段被过滤为空数组
"""
import pytest
from src.services.order import OrderService
def test_calculate_total_with_empty_items():
"""复现:当 order.items 为空时,calculateTotal 应返回 0 而非抛异常"""
service = OrderService()
# 模拟已下架商品被过滤后的订单
order = {
"id": "ORD-1234",
"items": [], # 根因:items 被过滤后为空,但代码假设 items[0] 存在
"coupon": None,
}
# 当前行为:抛出 TypeError
# 期望行为:返回 total = 0
result = service.calculate_total(order)
assert result == {"total": 0, "item_count": 0}
def test_calculate_total_with_none_items():
"""边界:items 字段缺失时不应崩溃"""
service = OrderService()
order = {"id": "ORD-1235", "coupon": None}
result = service.calculate_total(order)
assert result == {"total": 0, "item_count": 0}五、第三段:补丁模板
5.1 Prompt 模板
# bugfix-phase3-patch.yaml
name: Bugfix-Phase3-最小补丁
version: "1.0"
inputs:
phase1_output: # 日志分析结果
phase2_output: # 复现脚本路径
repo_path: # 仓库路径
prompt: |
你是一个高级开发者。根据日志分析和复现脚本,编写最小化补丁。
## 约束
1. 只修改根因相关代码,不做"顺便重构"
2. 修改行数不超过 20 行(如果超过,说明你没有找到根因)
3. 必须在修改前阅读相关代码的完整上下文(前后 50 行)
4. 每次修改必须附带注释说明"为什么这样改"
## 修改流程
1. 阅读候选文件,确认根因位置
2. 用 grep 搜索相同模式是否在其他地方也存在
3. 编写修复代码
4. 运行复现测试,确认通过
5. 运行相关模块的完整测试套件,确认无回归
## 输出
- 修改的文件列表和 diff
- 修改说明(每个文件改了什么、为什么)
- 测试运行结果5.2 补丁结果报告模板
{
"patch_id": "PATCH-1234-001",
"issue_id": "#1234",
"root_cause": "OrderService.calculateTotal() 假设 order.items 非空,直接访问 items[0].amount,当 items 为空数组时抛出 TypeError",
"files_changed": [
{
"path": "src/services/order.js",
"lines_added": 3,
"lines_removed": 1,
"change_summary": "在访问 items[0] 前增加空数组检查,items 为空时直接返回 total=0"
}
],
"total_lines_changed": 4,
"tests_added": ["tests/reproduce/test_bug_1234.py"],
"tests_passed": 12,
"tests_failed": 0,
"side_effects_checked": [
{"file": "src/services/refund.js", "reason": "相同模式,已检查,无此问题"},
{"file": "src/services/shipping.js", "reason": "相同模式,已修复"}
],
"verification_command": "pytest tests/reproduce/test_bug_1234.py tests/services/test_order.py -v"
}六、第四段:回归测试模板
6.1 Prompt 模板
# bugfix-phase4-regression.yaml
name: Bugfix-Phase4-回归验收
version: "1.0"
inputs:
phase3_output: # 补丁结果
repo_path: # 仓库路径
prompt: |
你是一个 QA 工程师。验证补丁的正确性和完整性。
## 验收清单
1. 复现测试是否通过(证明 Bug 已修复)
2. 相关模块测试是否全部通过(证明没有引入回归)
3. 边界条件是否覆盖(空值、null、极端输入)
4. 相同模式的代码是否都已修复(grep 搜索结果)
5. 代码变更是否在 20 行以内(超过需重新评估方案)
## 输出格式
严格按以下 JSON 输出验收报告:
```json
{
"verdict": "PASS | FAIL | CONDITIONAL_PASS",
"bug_fixed": true,
"regression_found": false,
"coverage_delta": "+0.3%",
"checks": [...],
"recommendation": "可以合并 | 需要补充 | 需要重做"
}
### 6.2 验收报告示例
```json
{
"verdict": "PASS",
"bug_fixed": true,
"regression_found": false,
"coverage_delta": "+0.3%",
"checks": [
{"name": "复现测试通过", "status": "PASS", "detail": "test_bug_1234.py 2/2 通过"},
{"name": "模块测试通过", "status": "PASS", "detail": "test_order.py 12/12 通过"},
{"name": "边界条件覆盖", "status": "PASS", "detail": "空数组、null、缺失字段均已覆盖"},
{"name": "同模式扫描", "status": "PASS", "detail": "refund.js 和 shipping.js 已修复"},
{"name": "变更行数", "status": "PASS", "detail": "共修改 4 行,低于 20 行阈值"}
],
"recommendation": "可以合并"
}七、真实经验与踩坑
7.1 复现脚本不能依赖生产数据
场景:Agent 写的复现脚本直接查生产数据库来获取"出错的那条记录"。 问题:CI 环境没有生产数据库访问权限,脚本在 CI 里跑不通;而且生产数据随时在变,今天能复现明天就不行了。 解决方案:所有复现脚本必须使用 fixture 数据(JSON 文件或 factory_boy)。fixture 文件跟测试代码一起提交到 Git,任何环境都能跑。如果 Bug 涉及特定的数据组合,把那个组合提取成最小 fixture,而不是 dump 整条生产记录。
7.2 补丁太大说明没找到根因
场景:Agent 提交了一个改了 8 个文件、120 行的补丁来修一个"undefined"错误。 问题:改太多文件意味着 Agent 在"到处试探",而不是精准修复。这种补丁 Review 成本极高,而且容易引入新问题。 解决方案:在补丁模板中硬性约束"修改不超过 20 行"。如果 Agent 说需要改更多,让它先停下来,重新分析根因。通常是漏看了某个关键逻辑,或者在错误的层级做修复。真正的根因修复往往只需要 1-5 行。
7.3 回归测试不只是"跑一遍现有测试"
场景:Agent 修完 Bug 后跑了 pytest,全部通过,然后报告"无回归"。
问题:现有测试可能根本没覆盖到 Bug 所在的代码路径。测试通过不代表 Bug 修好了,只代表没破坏已有功能。
解决方案:回归测试必须包含三部分——① 针对这个 Bug 的新测试(证明修好了);② 修改文件的已有测试(证明没破坏);③ 用变更分析工具选出的关联测试(证明影响范围可控)。文章中的 SmartTestSelector(参见上一篇质量门禁)可以自动完成第三步。
八、参数说明表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
issue_url |
string | 必填 | Issue 或错误报告链接 |
log_source |
string | 必填 | 日志来源,支持文件路径 / ELK / CloudWatch |
time_range |
string | "1h" |
日志采集时间范围,支持 30m / 1h / 24h |
max_patch_lines |
int | 20 |
补丁最大修改行数,超过需重新评估方案 |
candidate_file_limit |
int | 5 |
候选文件数量上限 |
confidence_threshold |
float | 0.3 |
候选文件置信度阈值,低于此值不纳入 |
fixture_format |
string | "json" |
复现数据格式,支持 json / yaml / csv |
repro_timeout_sec |
int | 30 |
复现脚本最大运行时间 |
regression_test_scope |
string | "smart" |
回归测试范围:smart(智能选择)/ full(全量)/ changed(仅修改文件) |
max_retry_on_fail |
int | 2 |
单阶段失败后最大重试次数 |
output_format |
string | "json" |
每阶段输出格式 |
approval_required |
bool | false |
补丁提交前是否需要人工审批 |
九、落地检查清单
- 日志采集输出包含完整堆栈,没有被截断
- 候选文件列表附带置信度和原因说明
- 复现脚本使用 fixture 数据,不依赖生产环境
- 复现脚本在 CI 环境中能 30 秒内跑完
- 补丁修改行数不超过 20 行
- 补丁附带修改原因注释
- 相同模式的代码已用 grep 扫描并一并修复
- 针对此 Bug 的回归测试已编写并通过
- 修改文件的已有测试全部通过
- 验收报告按模板 JSON 格式输出
- 整个四段式流程的输入输出都记录在 Issue 评论中
十、系列导航
上一篇:Agent 质量门禁:测试、Lint、安全扫描与代码审查闭环 下一篇:Lab 005:上下文包越大越好吗?Token 成本与修复成功率实验