← 课程地图
第 02 章 · 实战之旅

一行修复的一生

从一个真实的 GitHub bug 报告,到一道 SWE-bench 题目,再到 Agent 的轨迹与训练数据——全程真实数据,无一虚构。

主角:sympy__sympy-20590 · 按 → 开始 · 建议先看完第 01 章 · 节点可点击直达

概念不熟?先过第 01 章 · 概念骨架 →
第 1 幕 · PR 怎么变成任务

2020 年 10 月,一个 bug 报告

github.com/sympy/sympy · issue #20590 · 原文摘录
Symbol instances have __dict__ since 1.7?

In version 1.6.2 Symbol instances had no __dict__ attribute:

>>> sympy.Symbol('s').__slots__     # 1.6.2
('name',)
>>> sympy.Symbol('s').__dict__      # 1.7 → 多出来了!
1
用户发现:升级 1.7 后 Symbol 多了 __dict__ 每个实例多背一个字典——百万级符号运算时内存显著膨胀
2
危害:实例内存膨胀,违背 不可变设计 Symbol 本应像数字一样轻量不可变——带 dict 就可能被塞私有属性
3
关键:报告者只知道现象,不知道根因 这正是好题面的特征——解题者必须自己定位问题源头

这段话将原封不动成为题目的 problem_statement

第 1 幕 · PR 怎么变成任务

有人修了它:一行代码 + 一个测试

该 PR 的 diff(两处改动 · 真实内容)
--- a/sympy/core/_print_helpers.py    (修复)
+    __slots__ = ()   ← mixin 声明空 slots,子类不再被迫带 __dict__
--- a/sympy/core/tests/test_basic.py   (新增测试)
+def test_immutable():
+    assert not hasattr(b1, '__dict__')
+    with raises(AttributeError):
+        b1.x = 1
1
根因:Printable mixin 没声明 __slots__,拖累全部子类 Python 的继承规则:父类无 slots,子类实例就会带上 __dict__
2
修复:__slots__ = () 一行,附两行注释 真正的根因修复往往极短——难在找到它,不在写它
3
PR 自带测试 test_immutable——裁判也来了 断言实例无 __dict__、塞属性必抛错——把「修好了」变成可判定的事实

题面、答案、判分——一道题的三要素凑齐了

第 1 幕 · PR 怎么变成任务

提炼:机器从 PR 抠出 4 个字段

PR 里的东西变成字段角色
issue 正文problem_statement题面——Agent 读到的一切
PR 代码 diffpatch答案——评分对照,不给 Agent 看
PR 测试 difftest_patch评测——判分合约,也不给看
合并基点base_commit起点——仓库快照,hash 锁死
5
还要过机器筛选:修前测试必须真的挂、题面不得泄漏答案 并非每个 PR 都能出题——「真题」是一道道筛出来的

四个字段 + 两道筛子——一道 SWE-bench 题的诞生

第 1 幕 · PR 怎么变成任务

这道题的 jsonl 真身

swe-bench-lite.jsonl 中的一行(字段截断展示)
{
  "instance_id":       "sympy__sympy-20590",
  "repo":              "sympy/sympy",
  "base_commit":       "cffd4e0f86fe...",
  "problem_statement": "Symbol instances have __dict__ since 1.7?...",
  "patch":             "diff --git a/sympy/core/_print_helpers.py...",
  "test_patch":        "diff --git a/sympy/core/tests/test_basic.py...",
  "FAIL_TO_PASS":      ["test_immutable"],
  "PASS_TO_PASS":      ["test__aresame", "test_structure", ... 共 21 个]
}
1
F2P ×1:修好前挂、修好后必须过——证明修对了 就是屏 2 新增的 test_immutable——这道题唯一的「新裁判」
2
P2P ×21:原有测试一个不能挂——证明没改坏 test_structure、test_equality 等 21 个旧测试全部在场——防回归网

第 2 幕:把这道题交给 Agent——一场闭卷考试

第 2 幕 · Agent 解题与轨迹

Agent 拿到什么 · 看不到什么

拿到 ✓ 考卷与工具
  • problem_statement——issue 原文,一字不差
  • base_commit 时刻的完整仓库——可读可改可跑
  • 一套工具:读文件、改文件、跑命令
看不到 ✗ 答案与裁判
  • patch——开发者的修复,评分对照
  • test_patch——新增测试,判分合约
  • 目标只有一个:让隐藏测试变绿

像闭卷考试——考卷只有题面,没有答案

第 2 幕 · Agent 解题与轨迹

解题循环真跑起来

本题典型轨迹(示意 · 与真实记录同构)
① read   sympy/core/symbol.py          → 找 Symbol 定义
② grep   "Printable|__slots__" 全仓    → 锁定 mixin
③ read   sympy/core/_print_helpers.py  → Printable 无 __slots__
④ think  根因 = mixin 未声明 slots,子类被迫带 __dict__
⑤ edit   _print_helpers.py             → 添加 __slots__ = ()
⑥ bash   pytest test_basic.py -k immutable
         test_immutable PASSED ✓
1
看 → 想 → 做,循环推进直至测试变绿 每一步的输入输出都被记进轨迹——这就是「过程」的原材料
2
好轨迹:6 步直达根因,一次改对 定位准(一次 grep 锁定)、改动小(1 文件 1 行)、验证闭环(跑测试确认)

这是顺的一版——绕 60 步的版本同样存在

第 2 幕 · Agent 解题与轨迹

轨迹里记了什么

1
每一步:调了什么工具、什么参数、返回什么 read 了哪个路径、edit 了哪几行、bash 输出了什么——全程留痕
2
每一步:token 与耗时——成本可审计 哪一步烧了多少钱、卡了多久,事后一目了然
3
结尾:model_patch(Agent 的答卷)+ %Resolved 答卷与判分封存在轨迹尾部——结果维度与过程维度在此汇合
4
全程 append-only——只增不改,像飞机黑匣子 不能事后美化、不能删除污点——分析才有可信的事实基础

过程维度的所有分析,都从这份「黑匣子」出发

第 2 幕 · Agent 解题与轨迹

同一道题,两条轨迹

轨迹 A · 6 步直达 一次改对
  • 定位准:一次 grep 锁定 Printable
  • 改动小:1 个文件、1 行代码
  • token 少、耗时短、验证闭环
  • resolved ✓——可作示范教材
轨迹 B · 60 步绕路 试错后蒙对
  • 反复读同一批文件,重复检查
  • 改错多处又回滚,最后凑巧改对
  • token 数十倍、耗时数十倍
  • 也 resolved——但不可示范、不可复用

结果维度 A = B(都对);过程维度 A ≫ B——训练该学谁?

第 3 幕 · 轨迹变训练数据

原始轨迹 ≠ 训练数据

1
工具输出动辄几万 token——直接训练会爆上下文 一次 read 可能带回整份源文件;跑一次测试输出几十屏日志
2
失败轨迹混在里面——全学就是学会失败 未 resolved 的轨迹若无区分地进训练集,模型会模仿坏习惯
3
死循环、重复、跑偏——噪声先洗掉 同一文件读 8 遍、无效命令来回试——这些段落学进去就是浪费
4
训练要的是 messages 数组——格式也对不上 轨迹是事件流,训练要对话样本——中间隔一道转换工序

所以有一道「手工处理 + 标注」的工序

第 3 幕 · 轨迹变训练数据

SFT:轨迹 → messages 数组

清洗后的一条 SFT 样本(OpenAI 风格 · 截断)
[
 {"role": "system",    "content": "你是代码修复 Agent..."},
 {"role": "user",      "content": "Symbol instances have __dict__ ..."},
 {"role": "assistant", "content": "先看 Symbol 的定义",
   "tool_calls": [{"function": {"name": "read",
     "arguments": {"path": "sympy/core/symbol.py"}}}]},
 {"role": "tool",      "content": "class Symbol(Atom, ...): ..."},
 {"role": "assistant", "content": "根因在 Printable mixin,加 __slots__ = ()"}
]
1
截断超长工具输出,只保留关键行 几十屏 pytest 日志压成「PASSED / FAILED + 关键报错」
2
删掉死循环与重复段,保留有效推理 读 8 遍同一文件只留 1 遍;关键判断一步都不能少
3
只留 resolved 轨迹,或 A/B 对比保留 对比保留能让模型同时学「好路径」与「该避开的路径」

这份手工活,决定模型学到的是「节奏」还是「垃圾」

第 3 幕 · 轨迹变训练数据

标注:给轨迹贴上标签

结果标签 · 机器判 resolved 与否 %Resolved 说了算——跑一遍判分 harness 即得,无需人工
过程标签 · 人工 / LLM judge 哪步关键、哪步浪费 逐步打标:关键转折、无效动作、重复检查——训练信号更细
质量分级 · 三档 A / B / C A 直达 · B 绕路但成 · C 失败但有学习价值——不同档不同用法
反作弊 · 人工抽查 有没有骗绿 改测试文件、特判输入绕过判分——P2P 网之外的最后防线

本仓 analysis 六子模块,做的正是后三类

第 3 幕 · 轨迹变训练数据

RL:轨迹 = rollout

1
轨迹就是 rollout——Agent 自己采样自己试 同一道题采样 N 条轨迹,天然形成对比组
2
reward 不用人工标:%Resolved 是可验证 reward 测试跑出来就是客观信号——不像写作那样需要主观打分
3
P2P ×21 在防 reward hacking——改测试骗绿会被逮住 只把新测试改绿不行:21 个旧测试任何一个挂掉就判负
4
人工的价值,移到「出题」与「反作弊」 判分自动化后,人去做机器做不了的两端

SFT 学「怎么做」,RL 学「哪条路更好」

终章 · 闭环

这就是 Agent 自我演化

评测 → 分析 → 构造 → 训练 → 再评测 · 本平台三条主线的「自我演化」闭环

下一章 · 上下文工程 →
1 / 14 ← → 翻页 · 深挖 ↗ 新标签 · Esc 退出
专注模式 · 第 02 章 实战之旅 · 生成时间:2026-09-11(v2.1 章节化重构) · agentsoft-research-platform teaching-web-platform