Agent 自动修 Bug 并提 PR:我在 3 万行代码仓库跑了 2000 次的踩坑实录
·
一、起因:修一个Bug的成本到底多高
我们有一个3万行的Python后端服务,每周平均收到12个Bug报告。从Issue打开到PR合并,平均耗时4.2天。瓶颈不在开发——修Bug本身可能只要30分钟——而在排队、上下文切换、代码审查。
于是有了这个实验:让Agent自动接收GitHub Issue → 定位代码 → 生成修复 → 跑测试 → 提PR。人不写代码,只做审查和合并。
我们在历史Bug数据库上跑了2000次回测实验。下面是四次迭代的数据:
| 版本 | 核心策略 | 修复成功率 | PR合并率 | 平均修复时间 |
|---|---|---|---|---|
| V1 | 直接把Issue+错误日志扔给LLM | 14% | 7% | 2.1min |
| V2 | 语义搜索+代码上下文注入 | 31% | 19% | 4.3min |
| V3 | AST理解+测试���动修复 | 49% | 27% | 6.8min |
| V4 | 沙箱+多轮验证+CI联动 | 63% | 32% | 9.2min |
二、系统架构

三、V4 核心实现
3.1 代码定位:不只是搜字符串
# 代码定位——语义搜索 + AST符号解析
import tree_sitter_python as tspython
from tree_sitter import Language, Parser
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
PY_LANGUAGE = Language(tspython.language())
parser = Parser(PY_LANGUAGE)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
code_index = Chroma(
collection_name="codebase",
embedding_function=embeddings,
persist_directory="./code_vectordb"
)
def locate_code(issue_title, error_traceback):
"""组合语义搜索和AST分析,精准定位需要修改的代码"""
query = f"{issue_title}\n{error_traceback}"
# 第1步:语义搜索——找最相关的文件
file_results = code_index.similarity_search_with_score(
query, k=5, filter={"type": "file_summary"}
)
# 第2步:从traceback中提取精确位置
position = parse_traceback(error_traceback) # 如 ("handlers/order.py", 142)
# 第3步:读取目标文件并用tree-sitter解���AST
if position:
with open(position[0]) as f:
source_code = f.read()
tree = parser.parse(source_code.encode())
# 找到目标行所在的函数
target_line = position[1]
root = tree.root_node
func_node = find_enclosing_function(root, target_line)
if func_node:
func_text = source_code[func_node.start_byte:func_node.end_byte]
# 解析函数内的所有符号(变量、调用、导入)
symbols = extract_symbols(func_node, source_code)
return {
"file": position[0],
"function": func_node.child_by_field_name("name").text.decode(),
"code": func_text,
"symbols": symbols,
"related_files": [r.metadata["path"] for r, _ in file_results]
}
# 回退:只用语义搜索结果
return {"file": file_results[0].metadata["path"], "search_results": file_results}
def parse_traceback(tb_text):
"""从traceback中提取文件名和行号"""
import re
match = re.search(r'File "(.+?)", line (\d+)', tb_text)
return (match.group(1), int(match.group(2))) if match else None
def find_enclosing_function(node, line):
"""递归查找包含目标行的最内层函数"""
if node.type == "function_definition":
start = node.start_point[0]
end = node.end_point[0]
if start <= line <= end:
# 检查子节点中有没有更内层的��数
for child in node.children:
inner = find_enclosing_function(child, line)
if inner:
return inner
return node
for child in node.children:
result = find_enclosing_function(child, line)
if result:
return result
return None
3.2 修复生成 + 沙箱
# 修复生成——沙箱执行+多轮验证
import subprocess
import tempfile
import os
def generate_and_test_fix(issue, code_context, max_attempts=5):
"""生成修复,在沙箱中运行测试,不通过则重试"""
for attempt in range(max_attempts):
# Step 1: 生成修复
fix_prompt = f"""## Bug报告
{issue['title']}
{issue['body']}
## 错误日志
{issue['traceback']}
## 需要修改的代码
文件:{code_context['file']}
函数:{code_context['function']}
```python
{code_context['code']}
```
## 函数内引用的符号
{code_context['symbols']}
请生成修复代码。要求:
1. 使用 unified diff 格式
2. 只修改必要的行,不要重构无关代码
3. 如果之前尝试失败了(第{attempt+1}次),请从错误中学习"""
if attempt > 0:
fix_prompt += f"\n## 上次尝试失败的错误\n{last_error}"
fix_diff = llm.invoke(fix_prompt).content
# Step 2: 应用diff
with tempfile.TemporaryDirectory() as tmpdir:
# 复制原文件到沙箱
original_file = os.path.join(tmpdir, os.path.basename(code_context['file']))
shutil.copy(code_context['file'], original_file)
# 应用补丁
apply_patch(original_file, fix_diff)
# Step 3: 在沙箱中运行相关测试
result = subprocess.run(
["python", "-m", "pytest", f"tests/test_{os.path.basename(code_context['file'])}",
"-x", "--timeout=30"],
capture_output=True, text=True,
timeout=60, cwd=tmpdir,
env={**os.environ, "SANDBOX_MODE": "1"}
)
if result.returncode == 0:
return {"diff": fix_diff, "success": True, "attempts": attempt + 1}
last_error = result.stderr + result.stdout
return {"success": False, "attempts": max_attempts, "last_error": last_error}
def apply_patch(filepath, diff_text):
"""应用unified diff"""
result = subprocess.run(
["patch", "-p0", filepath],
input=diff_text, capture_output=True, text=True
)
if result.returncode != 0:
raise ValueError(f"Patch apply failed: {result.stderr}")
3.3 自动提PR + CI联动
# 自动创建PR + CI联动
import requests
import hashlib
def create_pr(issue, fix_result, code_context):
"""在GitHub上创建PR,关联Issue,触发CI"""
# 生成有意义的PR描述(不是"fix bug")
pr_description = llm.invoke(f"""请基于以下信息生成PR描述:
Bug: {issue['title']}
修复说明: {fix_result['diff'][:500]}
修改文件: {code_context['file']}
测试结果: {fix_result['attempts']}次尝试后通过
格式:## 问题 / ## 修复方案 / ## 测试 / ## 影响范围""").content
# 生成分支名
branch = f"fix/{hashlib.md5(issue['title'].encode()).hexdigest()[:8]}"
# API创建PR
response = requests.post(
f"https://api.github.com/repos/{REPO}/pulls",
headers={"Authorization": f"token {GITHUB_TOKEN}"},
json={
"title": f"fix: {issue['title'][:60]}",
"body": pr_description,
"head": branch,
"base": "main",
"draft": False # 如果测试通过,直接发正式PR
}
)
if response.status_code == 201:
pr_data = response.json()
# 关联Issue
requests.post(
f"https://api.github.com/repos/{REPO}/issues/{issue['number']}/comments",
headers={"Authorization": f"token {GITHUB_TOKEN}"},
json={"body": f"🤖 Agent已提交修复: #{pr_data['number']}"}
)
return pr_data
raise Exception(f"Failed to create PR: {response.text}")
四、七个核心坑位
| # | 坑位 | 现象 | 根因 | 解法 |
|---|---|---|---|---|
| 1 | 上下文窗口不够 | 3万行代码,LLM的128K窗口也放不下 | 代码库远大于模型上下文窗口 | 语义搜索+AST局部提取,只给Agent看2-3个相关文件和函数 |
| 2 | 修复引入新Bug | Agent改了函数A的返回值类型,调用方B没同步改 | 只看了单个文件,没有分析调用链 | AST向上追溯3层调用方,检查兼容性 |
| 3 | 跨文件依赖遗漏 | 修复涉及import路径变更,Agent没改对应的import语句 | import关系在文件头部,不在搜索上下文内 | tree-sitter解析所有import语句,自动检查一致性 |
| 4 | 幻觉生成假代码 | Agent生成了调用api.v3.refund()的代码,但这个函数根本不存在 |
LLM基于训练数据"脑补"了API | 在Prompt中强制列举30个可用API函数(白名单),禁止调用未列出的 |
| 5 | 权限越界修改 | Agent修改了数据库配置文件,把连接池从10改到100 | Agent没有"什么不能改"的约束 | 文件权限白名单:只有/views/、/services/目录中的.py文件可修改 |
| 6 | 测试覆盖不足 | Agent的修复通过了现有测试,但在边界条件下仍然Bug | 原有测试用例没有覆盖边界情况 | LLM额外生成3个针对性测试(空值、极限值、并发) |
| 7 | PR描述太简单 | Agent提交的PR描述只有"Fix bug"三个字 | 没有要求Agent生成结构化PR描述 | 强制PR模板:问题/方案/测试/影响范围 四段式 |
五、效果数据
| Bug类型 | 总数 | 修复成功 | 成功率 | PR合并 |
|---|---|---|---|---|
| 空指针/NoneType | 520 | 412 | 79% | 231 |
| 类型错误 | 380 | 266 | 70% | 138 |
| 边界条件 | 310 | 171 | 55% | 89 |
| API调用错误 | 280 | 182 | 65% | 93 |
| 逻辑错误 | 260 | 117 | 45% | 52 |
| 并发/竞态 | 150 | 63 | 42% | 27 |
| 配置/环境 | 100 | 48 | 48% | 11 |
| 总计 | 2000 | 1259 | 63% | 641 |
成功率最高的是空指针/NoneType类(79%),因为它们通常是"加一行if判空"的机械操作。成功率最低的是并发/竞态Bug(42%),因为需要理解多线程语义和时序关系。
修复成本对比:
Agent修复成本 = (Agent执行时间 × 算力单价) + (Review时间 × 人力时薪)
= 9.2min × ¥0.15/min + 5min × ¥3/min ≈ ¥16.4/个
vs 人工修复:30min × ¥3/min = ¥90/个
六、环境依赖
| 组件 | 版本 | 用途 |
|---|---|---|
| Python | 3.11.9 | 运行环境 |
| tree-sitter | 0.22.6 | AST解析 |
| tree-sitter-python | 0.21.0 | Python语法树 |
| ChromaDB | 0.5.5 | 代码向量检索 |
| pytest | 8.2.1 | 测试验证 |
| OpenAI API | gpt-4o-mini | LLM推理 |
| GitHub API | v3 | PR创建/CI联动 |
七、总结
Agent修Bug不是魔法,而是工程。核心经验:
- 代码定位是关键。Context给对了,LLM修Bug的能力远超预期;给错了,就是撞大运。
- 沙箱执行不可省略。永远不要让Agent直接在生产代码上操作。隔离的测试环境+自动回滚是底线。
- AST比字符串搜索靠谱10倍。了解函数签名、调用链、import关系,才知道一个修改的影响范围。
⚠️ 适用边界:本方案适用于有良好测试覆盖的Python/JavaScript项目。对C++/Rust等编译型语言,需要额外处理编译错误;对没有测试的项目,会大量产生假阳性。不建议用于核心支付、认证等安全敏感模块。
参考:tree-sitter文档(2026-07)、LangChain CodeAgent(2026-06)、SWE-bench Verified基准(2026-05)、GitHub Copilot Agent mode(2026-06)
更多推荐


所有评论(0)