Day 1: 从零搭建 SWE-bench Coding Agent

十天写一个能在 benchmark 上跑分的编码 Agent,这是第一天的记录。

为什么做这件事

大模型编码 Agent 是当下最热的方向之一。SWE-bench 是目前最主流的编码 Agent 评测基准——给你一个真实的 GitHub issue 和对应仓库,Agent 需要自主定位问题、阅读代码、生成修复 patch,最终通过测试。

我想用 10 天时间,从零开始搭建一个能在 SWE-bench 上跑出成绩的 Agent,并把过程完整记录下来。目标不是刷榜,而是理解:一个编码 Agent 到底需要什么?

Day 1 目标:可运行的基础框架

第一天的任务很明确:搭出一个端到端能跑通的框架。不追求效果,但要能在真实的 SWE-bench 实例上完成完整流程:加载任务 → 准备代码仓库 → Agent 自主修复 → 提取 patch。

架构选择:ReAct Loop

编码 Agent 的主流架构有三种:

  1. ReAct Loop(思考→行动→观察循环):最成熟,SWE-agent、OpenHands 等都用这个范式
  2. Plan-then-Execute:先生成完整计划,再逐步执行
  3. Tree Search:多路径搜索+回溯,如 SWE-Search

Day 1 选择了 ReAct,理由很简单:最成熟、最容易实现、最容易调试。后续可以在此基础上加入规划和搜索能力。

ReAct 的核心循环:

while 未完成 and 步数 < 上限:
    1. 将当前对话历史发给 LLM
    2. LLM 返回思考 + 工具调用(或最终回答)
    3. 执行工具,将结果加入对话历史
    4. 回到 1

项目结构:8 个文件,不多不少

myagent/
├── config.py       # 配置(模型地址、步数上限、token 限制)
├── log_utils.py    # 日志 + 输出截断
├── llm.py          # LLM 客户端封装
├── tools.py        # 工具定义 + 实现
├── prompt.py       # 系统提示词
├── agent.py        # ReAct 循环引擎
├── harness.py      # SWE-bench 数据集集成
└── run.py          # CLI 入口

Day 1 的原则是够用就好——不做过度抽象,不搞插件系统,每个文件职责清晰。

核心设计决策

1. 工具设计:4 个够用

给 Agent 的工具越多,prompt 越长,模型选择越困难。Day 1 只保留 4 个核心工具:

工具用途
bash执行任意 shell 命令(跑测试、查文件等)
read_file带行号读取文件内容
edit_file精确字符串替换编辑
search_code正则搜索代码(类似 grep)

注意 edit_file 用的是 search/replace 模式而非行号编辑——让模型提供要替换的精确字符串,找到唯一匹配后替换。这比行号编辑可靠得多,因为行号在多次编辑后容易错位。

2. 提示词:精简但有方向

系统提示词需要告诉 Agent 三件事:

  • 你在做什么:修复这个仓库的这个 bug
  • 怎么做:定位代码 → 阅读理解 → 最小修改 → 运行测试验证
  • 规则:最小修改、不改测试、不加注释

关键经验:提示词里必须包含如何运行测试。不同项目的测试方式差异很大:

python
REPO_TEST_COMMANDS = {
    "django/django": "python tests/runtests.py {test_module} --verbosity=2",
    "scikit-learn/scikit-learn": "python -m pytest {test_file} -xvs",
    "sympy/sympy": "python -m pytest {test_file} -xvs",
    # ...
}

这是 Day 1 最重要的发现之一——第一次运行时,Agent 花了 28 步(总共 40 步上限)在试图搞清楚 Django 怎么跑测试。加上测试命令后,第二次运行只用了 22 步就完成了全部流程。

3. 上下文管理:生命线

编码 Agent 的上下文增长很快:系统提示词 + 工具定义 + 多轮工具调用和结果。管理策略:

  • 工具输出截断:超长输出用 head_tail 策略(保留开头和结尾,中间截断)
  • 消息裁剪:当估算 token 超过阈值时,保留系统提示词和最近 N 条消息,裁剪中间的旧消息
  • context overflow 恢复:捕获上下文溢出错误,强制裁剪后重试

4. 防御机制

  • 死循环检测:连续 3 次相同的工具调用 → 注入提示 "你似乎在重复,换个方法"
  • 步数上限:最多 40 步,防止无限运行
  • 工具错误友好提示:edit_file 找不到匹配时,返回附近的部分匹配帮助模型修正

端到端验证:django__django-11179

选了一个经典的 SWE-bench 实例做验证:

Bug: delete() 在没有依赖关系的模型实例上调用后,不会将主键设为 None

期望: 删除后 instance.pk 应该是 None

Agent 的执行轨迹(精简版):

Step 1-2: 读取 django/db/models/deletion.py,定位快速删除路径
Step 3-6: 读取测试文件,理解测试结构
Step 7-11: 分析 fast delete 路径 vs 正常 delete 路径的差异
Step 12: 添加一行修复:setattr(instance, model._meta.pk.attname, None)
Step 13-21: 运行测试验证(使用 python tests/runtests.py)
Step 22: 确认所有 FastDeleteTests 通过,总结修复

生成的 patch:

diff
@@ -277,6 +277,7 @@ class Collector:
             if self.can_fast_delete(instance):
                 with transaction.mark_for_rollback_on_error():
                     count = sql.DeleteQuery(model).delete_batch([instance.pk], self.using)
+                setattr(instance, model._meta.pk.attname, None)
                 return count, {model._meta.label: count}

一行修复,测试通过。这正是标准答案。

Day 1 经验总结

什么有效

  • ReAct loop 简单有效,作为起点非常合适
  • search/replace 编辑模式比行号编辑可靠
  • 在提示词里嵌入仓库特定的测试命令,能显著减少浪费的步骤

什么需要改进

  • 上下文窗口管理需要更智能的策略(摘要替代粗暴裁剪)
  • 单一实例验证不够,需要批量评测才能看到真实水平
  • 提示词还有大量优化空间

明天的计划

  • 批量跑 SWE-bench_Lite(300 个实例)的子集,建立 baseline 成功率
  • 分析失败案例,找到最大的改进方向
  • 考虑引入更智能的上下文压缩

这是「十天开发编码 Agent」系列的第一篇。代码开源在项目仓库中,欢迎关注后续更新。

Linked from

Comments (0)

No comments yet

imfht
April 7, 2026