Some checks failed
Build and Deploy Log Center / build-and-deploy (push) Failing after 1m55s
400 lines
12 KiB
Markdown
400 lines
12 KiB
Markdown
# Bug 自动修复流程分析与优化建议
|
||
|
||
> 分析日期: 2026-02-25
|
||
> 当前版本存在的问题和改进方案
|
||
|
||
---
|
||
|
||
## 1. 当前流程概览
|
||
|
||
### 主流程 (fix_project)
|
||
|
||
```
|
||
1. 初始化
|
||
├─ 获取项目信息(本地路径、仓库地址)
|
||
├─ 获取待修复 Bug 列表(NEW/PENDING_FIX)
|
||
├─ 初始化 Git(pull + 创建 fix 分支)
|
||
└─ 更新所有 Bug 状态 → FIXING
|
||
|
||
2. 多轮修复循环(max 3 轮)
|
||
└─ 每轮:
|
||
├─ 调用 Claude CLI 修复代码
|
||
├─ 获取 diff 和修改文件列表
|
||
├─ 安全检查(文件数/行数/核心文件)
|
||
├─ 运行 Claude 生成的测试文件
|
||
├─ 上传修复报告
|
||
└─ 标记为 FIXED(无论测试是否通过!)
|
||
|
||
3. 自动提交(可选)
|
||
├─ commit → push fix 分支
|
||
└─ **直接合并到 main 并推送**
|
||
```
|
||
|
||
### 重试失败流程 (retry_failed_project)
|
||
|
||
```
|
||
1. 获取所有 FIX_FAILED Bug
|
||
2. 逐个分诊(triage)
|
||
├─ VERDICT:CANNOT_REPRODUCE → 标记为 CANNOT_REPRODUCE
|
||
└─ VERDICT:FIX → 重置为 PENDING_FIX
|
||
3. 批量调用 fix_project 修复
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 主要缺陷分析
|
||
|
||
### 🔴 **严重问题 1:测试验证与重试逻辑脱节**
|
||
|
||
**问题描述:**
|
||
- 代码中有 `max_rounds=3` 的多轮重试机制
|
||
- 但在 [core.py:241-254](log_center/repair_agent/agent/core.py#L241-L254),无论测试通过与否都标记为 `FIXED` 并 `break`
|
||
- **测试失败不会触发下一轮重试**
|
||
|
||
**问题代码:**
|
||
```python
|
||
# core.py Line 230-254
|
||
test_passed = bool(test_output) and "FAILED" not in test_output
|
||
|
||
# 无论 test_passed 是 True 还是 False,都标记为 FIXED
|
||
for bug in bugs:
|
||
self.task_manager.update_status(bug.id, BugStatus.FIXED)
|
||
# ...
|
||
results.append(FixResult(bug_id=bug.id, success=True, ...)) # ❌
|
||
|
||
# 然后直接 break,不会进入第 2、3 轮
|
||
break # Line 271
|
||
```
|
||
|
||
**影响:**
|
||
- 多轮重试机制形同虚设
|
||
- 测试失败的 Bug 被错误标记为已修复
|
||
- 浪费了 Claude 重试优化的能力
|
||
|
||
**建议修复:**
|
||
```python
|
||
# 第 1 步:运行测试
|
||
test_output = run_repair_test_file(project_path, test_file)
|
||
test_passed = self._check_test_passed(test_output)
|
||
cleanup_repair_test_file(project_path, test_file)
|
||
|
||
# 第 2 步:根据测试结果决定下一步
|
||
if test_passed:
|
||
# 测试通过,标记为 FIXED
|
||
for bug in bugs:
|
||
self.task_manager.update_status(bug.id, BugStatus.FIXED)
|
||
self._upload_round_report(...)
|
||
break # 成功,退出循环
|
||
else:
|
||
# 测试失败,记录上下文供下一轮使用
|
||
last_diff = diff
|
||
last_test_output = test_output
|
||
logger.warning(f"第 {round_num} 轮测试失败,尝试下一轮")
|
||
|
||
if round_num == max_rounds:
|
||
# 最后一轮仍失败,标记为 FIX_FAILED
|
||
for bug in bugs:
|
||
self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED)
|
||
self._upload_round_report(..., status=BugStatus.FIX_FAILED)
|
||
# 继续下一轮
|
||
```
|
||
|
||
---
|
||
|
||
### 🟡 **严重问题 2:Git 分支管理混乱**
|
||
|
||
**问题描述:**
|
||
1. [git_manager.py:55-75](log_center/repair_agent/agent/git_manager.py#L55-L75) `pull()` 会切回 main/master
|
||
2. [core.py:152-154](log_center/repair_agent/agent/core.py#L152-L154) 然后又从 main 创建新分支
|
||
3. [core.py:264](log_center/repair_agent/agent/core.py#L264) `auto_commit` 时**直接合并到 main 并推送**
|
||
|
||
**问题:**
|
||
- 没有 PR 审核流程,变更直接进入主分支
|
||
- 每次修复都创建新分支,但合并后删除,无法回溯
|
||
- 不符合现代 Git 工作流(应该创建 PR 供人工审核)
|
||
|
||
**建议修复方案:**
|
||
|
||
**方案 A:保留 fix 分支,推送后创建 PR**
|
||
```python
|
||
# core.py Line 256-267
|
||
if git_enabled and auto_commit and modified_files and git_manager:
|
||
bug_ids = ", ".join([f"#{b.id}" for b in bugs])
|
||
git_manager.commit(f"fix: auto repair bugs {bug_ids}")
|
||
git_manager.push()
|
||
logger.info("fix 分支已推送,请手动审核并合并")
|
||
|
||
# 可选:调用 gh CLI 创建 PR
|
||
# subprocess.run(["gh", "pr", "create", "--title", f"Auto fix {bug_ids}", ...])
|
||
```
|
||
|
||
**方案 B:如果确实需要直接合并(慎用)**
|
||
```python
|
||
# 添加配置项控制
|
||
if settings.auto_merge_to_main: # 默认 False
|
||
if git_manager.merge_to_main_and_push():
|
||
logger.info("已合并到 main 并推送")
|
||
else:
|
||
logger.info("fix 分支已推送,需人工审核")
|
||
```
|
||
|
||
---
|
||
|
||
### 🟠 **问题 3:安全检查位置不当**
|
||
|
||
**问题代码:** [core.py:209-225](log_center/repair_agent/agent/core.py#L209-L225)
|
||
|
||
```python
|
||
# Step 3: 安全检查(在修改后)
|
||
if git_manager and not self._safety_check(modified_files, diff):
|
||
failure_reason = "安全检查未通过"
|
||
git_manager.reset_hard() # ❌ 丢失所有修改
|
||
|
||
# 标记为 FIX_FAILED,但不会重试
|
||
for bug in bugs:
|
||
self.task_manager.update_status(bug.id, BugStatus.FIX_FAILED)
|
||
break
|
||
```
|
||
|
||
**问题:**
|
||
- Claude 已经修改了代码,安全检查失败后 `reset_hard` 丢失所有修改
|
||
- 状态已经更新为 `FIXING`,造成不一致
|
||
- 不会重试,浪费了 Claude 的工作
|
||
|
||
**建议:**
|
||
|
||
**方案 A:软性警告 + 继续测试**
|
||
```python
|
||
# 安全检查改为警告,记录但不阻断
|
||
if git_manager:
|
||
safety_passed, warnings = self._safety_check(modified_files, diff)
|
||
if not safety_passed:
|
||
logger.warning(f"安全检查警告: {warnings}")
|
||
# 将警告加入报告,但继续执行测试
|
||
|
||
# 让测试结果决定是否成功
|
||
```
|
||
|
||
**方案 B:预检查机制**
|
||
```python
|
||
# 在调用 Claude 前,先用只读工具做预分析
|
||
success, analysis = self.claude_service.analyze_bug(bug, project_path)
|
||
if "core file" in analysis.lower():
|
||
logger.warning("分析发现可能涉及核心文件,跳过自动修复")
|
||
# 标记为需要人工介入
|
||
```
|
||
|
||
---
|
||
|
||
### 🟡 **问题 4:测试通过判断过于简单**
|
||
|
||
**问题代码:** [core.py:231](log_center/repair_agent/agent/core.py#L231)
|
||
|
||
```python
|
||
test_passed = bool(test_output) and "FAILED" not in test_output and "Error" not in test_output.split("\n")[-5:].__repr__()
|
||
```
|
||
|
||
**问题:**
|
||
- 只检查 `"FAILED"` 字符串,不严谨
|
||
- 没有测试文件也会被判定为通过(`test_output` 为空时返回 `False`)
|
||
- 但代码逻辑:没有测试文件 → `test_output = "Claude 未生成测试文件"` → `bool(test_output) = True` → 可能误判
|
||
|
||
**建议:**
|
||
```python
|
||
def _check_test_passed(self, test_output: str) -> bool:
|
||
"""检查测试是否通过"""
|
||
if not test_output or "Claude 未生成测试文件" in test_output:
|
||
logger.warning("没有测试输出,无法验证修复")
|
||
return False # 没有测试视为未通过
|
||
|
||
# 检查常见失败标记
|
||
fail_markers = ["FAILED", "ERROR", "AssertionError", "Exception"]
|
||
for marker in fail_markers:
|
||
if marker in test_output:
|
||
return False
|
||
|
||
# 检查是否有通过标记(更严格)
|
||
if "OK" in test_output or "passed" in test_output.lower():
|
||
return True
|
||
|
||
# 无明确通过标记,保守判断
|
||
return False
|
||
```
|
||
|
||
---
|
||
|
||
### 🟠 **问题 5:代码重复**
|
||
|
||
**问题:**
|
||
- [core.py:95-290](log_center/repair_agent/agent/core.py#L95-L290) `fix_project`
|
||
- [core.py:467-562](log_center/repair_agent/agent/core.py#L467-L562) `fix_single_bug`
|
||
- 两个方法的核心逻辑完全一样,只是处理单个 vs 批量
|
||
|
||
**建议:**
|
||
```python
|
||
def fix_project(self, project_id: str, ...) -> BatchFixResult:
|
||
bugs = self.task_manager.fetch_pending_bugs(project_id)
|
||
return self._fix_bugs_batch(bugs, project_id, ...)
|
||
|
||
def fix_single_bug(self, bug_id: int, ...) -> FixResult:
|
||
bug = self.task_manager.get_bug_detail(bug_id)
|
||
result = self._fix_bugs_batch([bug], bug.project_id, ...)
|
||
return result.results[0] if result.results else FixResult(...)
|
||
|
||
def _fix_bugs_batch(self, bugs: list[Bug], project_id: str, ...) -> BatchFixResult:
|
||
"""通用的批量修复逻辑"""
|
||
# 原 fix_project 的核心逻辑
|
||
```
|
||
|
||
---
|
||
|
||
### 🟢 **问题 6:retry_failed_project 分诊逻辑可优化**
|
||
|
||
**当前流程:** [core.py:292-398](log_center/repair_agent/agent/core.py#L292-L398)
|
||
|
||
```
|
||
FIX_FAILED Bug → 分诊 → CANNOT_REPRODUCE / 重置为 PENDING_FIX → fix_project
|
||
```
|
||
|
||
**问题:**
|
||
- 分诊失败会保留 `FIX_FAILED` 状态,下次仍会重复分诊
|
||
- 没有分诊次数限制,可能死循环
|
||
|
||
**建议:**
|
||
```python
|
||
# 在 Bug 模型中增加 triage_count 字段
|
||
if bug.retry_count >= settings.max_triage_count:
|
||
logger.info(f"Bug #{bug.id} 已分诊 {bug.retry_count} 次,标记为 CANNOT_REPRODUCE")
|
||
self.task_manager.update_status(bug.id, BugStatus.CANNOT_REPRODUCE)
|
||
continue
|
||
|
||
# 分诊
|
||
success, output = self.claude_service.triage_bug(bug, project_path)
|
||
if not success:
|
||
# 分诊执行失败,增加计数但保留状态
|
||
self.task_manager.increment_retry_count(bug.id)
|
||
continue
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 优化后的理想流程
|
||
|
||
### 新流程设计
|
||
|
||
```
|
||
1. 初始化
|
||
├─ 获取项目信息和 Bug 列表
|
||
├─ Git pull + 创建 fix 分支
|
||
└─ 更新状态 → FIXING
|
||
|
||
2. 多轮修复循环(最多 3 轮)
|
||
└─ 每轮:
|
||
├─ 调用 Claude CLI 修复
|
||
├─ 获取 diff 和修改文件
|
||
├─ 【新增】软性安全检查(警告不阻断)
|
||
├─ 运行测试文件
|
||
├─ 【关键】严格判断测试是否通过
|
||
│ ├─ 通过 → 标记 FIXED + break
|
||
│ └─ 失败 → 记录上下文,继续下一轮
|
||
└─ 最后一轮仍失败 → 标记 FIX_FAILED
|
||
|
||
3. 提交代码
|
||
├─ commit + push fix 分支
|
||
├─ 【推荐】创建 PR 供人工审核
|
||
└─ 【慎用】auto_merge_to_main(需配置开关)
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 实施建议
|
||
|
||
### 优先级排序
|
||
|
||
#### P0(必须修复)
|
||
1. ✅ **修复测试验证逻辑** - 测试失败应触发重试
|
||
2. ✅ **移除自动合并到 main** - 改为创建 PR
|
||
|
||
#### P1(强烈建议)
|
||
3. ✅ **优化测试通过判断** - 更严格的检测逻辑
|
||
4. ✅ **重构代码消除重复** - 提取 `_fix_bugs_batch`
|
||
|
||
#### P2(建议优化)
|
||
5. ✅ **改进安全检查** - 改为软性警告
|
||
6. ✅ **添加分诊次数限制** - 防止重复分诊
|
||
|
||
---
|
||
|
||
## 5. 配置建议
|
||
|
||
### 新增配置项
|
||
|
||
```python
|
||
# config/settings.py
|
||
|
||
# Git 工作流
|
||
auto_merge_to_main: bool = False # 默认不自动合并
|
||
create_pr_after_fix: bool = True # 自动创建 PR
|
||
|
||
# 测试验证
|
||
require_test_pass: bool = True # 必须测试通过才标记 FIXED
|
||
test_timeout: int = 300 # 测试超时时间(秒)
|
||
|
||
# 重试策略
|
||
max_retry_count: int = 3 # 最大重试轮数
|
||
max_triage_count: int = 2 # 最大分诊次数
|
||
|
||
# 安全检查
|
||
safety_check_mode: str = "warn" # warn / block
|
||
max_modified_files: int = 10
|
||
max_modified_lines: int = 500
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 测试建议
|
||
|
||
### 需要覆盖的场景
|
||
|
||
1. **测试失败重试**
|
||
- Bug 修复后测试失败 → 第 2 轮修复 → 测试通过
|
||
- 3 轮都失败 → 标记为 FIX_FAILED
|
||
|
||
2. **Git 分支管理**
|
||
- 修复后创建 PR 而不是直接合并
|
||
- 多个 Bug 修复复用同一个 fix 分支
|
||
|
||
3. **安全检查**
|
||
- 修改超限文件 → 警告但继续
|
||
- 核心文件修改 → 警告并记录
|
||
|
||
4. **分诊流程**
|
||
- FIX_FAILED → 分诊 → CANNOT_REPRODUCE
|
||
- 分诊失败达到上限 → 标记为 CANNOT_REPRODUCE
|
||
|
||
---
|
||
|
||
## 7. 总结
|
||
|
||
### 关键改进点
|
||
|
||
| 问题 | 现状 | 改进后 |
|
||
|------|------|--------|
|
||
| 测试验证 | 无论通过与否都标记 FIXED | 测试失败触发重试,最终失败标记 FIX_FAILED |
|
||
| Git 工作流 | 直接合并到 main | 创建 PR 供人工审核 |
|
||
| 安全检查 | reset_hard 丢失修改 | 软性警告,继续测试 |
|
||
| 测试判断 | 简单字符串匹配 | 严格检测通过/失败标记 |
|
||
| 代码质量 | 逻辑重复 | 提取公共方法 |
|
||
| 分诊流程 | 可能重复分诊 | 添加次数限制 |
|
||
|
||
### 预期收益
|
||
|
||
- ✅ 多轮重试机制真正发挥作用,提高修复成功率
|
||
- ✅ Git 工作流更安全,避免直接污染主分支
|
||
- ✅ 测试验证更严格,减少假阳性
|
||
- ✅ 代码更简洁,维护成本降低
|
||
|
||
---
|
||
|
||
**建议:优先实施 P0 和 P1 的改进,P2 可以逐步优化。**
|