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 的主流架构有三种:
- ReAct Loop(思考→行动→观察循环):最成熟,SWE-agent、OpenHands 等都用这个范式
- Plan-then-Execute:先生成完整计划,再逐步执行
- 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
- 怎么做:定位代码 → 阅读理解 → 最小修改 → 运行测试验证
- 规则:最小修改、不改测试、不加注释
关键经验:提示词里必须包含如何运行测试。不同项目的测试方式差异很大:
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:
@@ -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」系列的第一篇。代码开源在项目仓库中,欢迎关注后续更新。