一、起因:修一个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不是魔法,而是工程。核心经验:

  1. 代码定位是关键。Context给对了,LLM修Bug的能力远超预期;给错了,就是撞大运。
  2. 沙箱执行不可省略。永远不要让Agent直接在生产代码上操作。隔离的测试环境+自动回滚是底线。
  3. 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)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐