前言
修复一个已有软件项目,通常要先理解 issue,再找到相关代码,实施修改,并检查改动是否满足原来的行为约定。SWE-bench Lite 把真实开源项目中的这类问题整理成任务:给出仓库和 issue,要求提交修复补丁,再由预先确定的测试检查结果。本文沿着这些任务,比较大模型完成代码修复的几种工作方式。
一种方式是把 issue 和检索到的源码一次性交给模型,让它直接输出补丁。另一种方式是让模型进入循环:自己决定接下来搜索什么、读哪个文件、怎样修改、运行什么检查,再根据工具返回的结果继续行动。这里的 Agent Loop 指这样的交互循环;模型外围负责输入、工具、工作区与预算的执行框架,就是 harness。
两种方式同时改变了很多条件。Loop 可以获得更多源码,把修改写进实际文件,接收执行结果,还可以多次调用模型。即使它的成功率更高,也很难判断各项能力分别起了多少作用。为此,后续实验增加了一组无工具多轮修订:它仍然只能使用最初的材料,但有机会回看并修改自己的答案。这让一次生成、无工具修订与完整工具交互有了直接参照。
这项工作的起点见上一篇 Agent Harness Lab 复盘。第一轮中,静态 One-shot 在 20 个任务上解决了 4 个,完整 Loop 的任务平均成功率是 55%,相差 35 个百分点。One-shot 指一次调用后直接交付答案。这个结果显示了当时整套交互系统的收益,却没有区分更多生成机会与新环境信息的作用。
第二轮(Round 2)沿用这组任务,改进重复运行与对照设计,再追加一次长输出的静态参照。本文先介绍设计与结果,然后分析补丁失败阶段和三条真实轨迹,最后回顾隔离执行能力时遇到的工程问题。第二轮已完成;执行消融只做了部分准备,没有正式模型运行,最终决定停止。开发、实验和核验是在 AI 协作下完成的,下面的解释依据保存的输入、动作、补丁与评分记录,属于事后分析。
1. 第一轮实验的局限
1.1 多项能力同时变化
第一轮(下文记作 V1)的静态方法与 Loop 在源码访问、编辑和执行等方面都有差别。静态方法拿到 issue 和检索出来的源码片段,然后把修改写成 diff;Loop 可以继续查看仓库,选择实际文件,实施修改,运行命令,再根据结果继续行动。读到多少源码、怎样生成补丁、能否检查执行结果、调用多少次模型,以及什么时候结束,都一起发生了变化。
V1 的结果对应这两个工作系统的整体差异。比如,静态方法可能已经想对了算法,却没有拿到需要修改的文件;也可能找对文件,却输出了无法应用的 diff。Loop 通过读文件和编辑真实工作区,就可能跨过这两个障碍,甚至还没有用到测试反馈。如果最后看到 Loop 成功率更高,直接把增益归给执行测试后自我修复,就跳过了这些同样具体的差异。
两组的采样次数也不同。V1 的静态 One-shot 是每题一次,Loop 每题有八次独立运行,55% 是先计算各题成功比例,再对任务取平均,不能理解为 20 道题里有 11 道至少成功过一次。多次采样可以帮助观察稳定性,但两组对任务表现的估计精度不同。前篇还讨论了多采样覆盖率,那个问题与一次运行的平均成功概率又不同:试八次总能成功一次,并不等于每次都可靠。
第二轮首先加入无环境交互的多轮修订组,观察静态材料不变时,增加后续生成机会会得到什么结果。这个对照仍不能逐项估计 Loop 各组件的贡献,但可以减少第一轮中调用次数差异带来的解释空间。
1.2 一次生成、无工具修订与完整交互
可以先用一个普通的编程场景理解这三组。假设问题是配置加载函数默认以文本方式打开文件,而一个新解析器要求二进制流。只看 issue 和函数片段,模型可能就能想到加一个参数;再看一遍自己的答案,也可能发现参数没有传到真正打开文件的位置。可如果项目里还有包装函数、兼容性约定或现成测试,这些内容只有重新查看仓库才会出现。
第一种方式,把已有材料一次性读完,直接给出补丁。第二种方式,仍然只能使用原来的材料,但可以回看并修订自己的答案。第三种方式,既可以继续推理,也可以主动取得新源码、编辑文件,运行检查。这个例子只是说明方法差别,后面 Flask 的真实案例会展示这些动作在实际记录里怎样发生。
本文把三组记作 A0、A2 和 B。A0 是一次长输出的静态方法;A2 是最多三次调用的无环境反馈修订;B 是完整 Loop。三组分别对应不同的信息条件。A2 可以继续检查已有答案,后续调用不会得到新的源码、执行结果或评分证据。
A0、A2 与 B 的信息流。三组共享任务材料与累计输出上限,但后续可获得的信息不同。A0 是看到 A2/B 结果后追加的顺序扩展;官方评分都在候选冻结后进行,不进入模型的修订循环。
阅读图表时可点击图片,再选择原尺寸并在图内滚动;手机上建议这样查看标签与注释。
2. 第二轮实验设计与变量控制
2.1 先比较 A2/B,再追加 A0
第二轮沿用 20 个任务,来自 10 个仓库,每题每组独立运行四次。因此,每组计划运行 80 次,三组共 240 次。每次运行从自己的初始状态出发,某次的答案和工具结果不会传给同题的另一次运行。记录把四次重复编号为 0~3,后文沿用这个编号。四次重复用来估计同一道题的稳定性,不是让一个 Agent 连续修四次同一个问题。
最先完成的是 A2 与 B。两组交替安排运行,共享固定 issue 和静态检索材料。这里的检索采用 BM25,一种按词项匹配和文档相关性选出片段的方法;它提供大约 13K token 的源码上下文,token 是模型计量文字的基本单位。这些片段不保证覆盖所有相关文件。A2 的后续调用只能看到已有输入和自己的先前回答,不能读取新源码,也没有执行结果或评分反馈。B 则可以在此基础上继续查找、读取和编辑真实仓库,并执行命令。
A0 是在 A2/B 已有结果之后追加的。它复用 A2 第一次调用的系统提示与输入,在本组请求开始前冻结自己的协议。这让 A0 与 A2 的初始材料保持一致,也为一次长输出补上了直接参照。但时间顺序不能改写:三组不是同一时点预注册、同期完成的随机对照。追加 A0 是研究过程中对问题的进一步收窄,这个设计事实也限制了结果能承载的因果解释。
本轮记录的模型为 deepseek-flash,当时资料对应 V4.1 Flash;配置启用推理模式,并请求 high 推理强度。V1 使用了不同的模型版本,因此本文只比较 Round 2 内部的 A0/A2/B,不把两轮数字相减来计算框架升级的收益。
2.2 输出预算与候选选择
三组累计输出预算(completion)上限都是 131,072 token,其中包括推理 token,不能再加一次。A0 一次调用可以使用整个上限;A2 和 B 的单次上限为 65,536,并受剩余额度约束。A2 最多三次调用,B 最多 60 步,都不要求必须用尽预算。
三组采用相同的累计输出上限,但实际计算资源消耗仍然不同。B 会重复带入上下文、接收工具输出,还会占用执行时间;A2 后续调用需要重新读自己的答案。相同输出上限只约束一个预算维度,不能直接写成 same compute。
候选选择也必须与结果分开。A2 没有拿三份答案去评分,再挑一份最好的。它按生成顺序保留最后一个符合条件的候选:回答没有因长度被截断,而且能从中提取出非空 diff。只有提取出的补丁符合条件,才会替换先前候选;一句非空解释并不算新补丁。B 则从最终工作区收集真实修改。所有组的候选都先冻结,随后才进入独立评分,模型看不到这一轮官方结果。
这样的规定会影响失败是什么。如果 A2 后一次回答只解释原因,却没给出可提取的补丁,就不能把解释当作更好的候选。如果 B 在工作区写了一个测试文件,那个文件也可能进入最终 diff。最终评分接收的是收集到的补丁字节,生成过程中留下的文件也可能影响结果。后面的 Flask 案例会具体展示这一点。
3. 三组实验结果与任务差异
3.1 完整 20 题与共同 19 题的结果
| 比较范围 | A0 |
A2 |
B |
|---|---|---|---|
| 完整 20 题,各 80 次 | 25/80 · 31.25% | 两次 null,不报告同口径均值 | 54/80 · 67.50% |
| 共同 19 题,各 76 次 | 25/76 · 32.89% | 31/76 · 40.79% | 52/76 · 68.42% |
完整 20 题中,A0 成功 25/80,任务平均成功率为 31.25%;B 成功 54/80,为 67.50%。B 比 A0 高 36.25 个百分点。因为每题都有四次运行,这里的任务平均值与总成功数除以 80 相同;统计上的单位仍然是任务,而不是把 80 次都当成互不相关的软件问题。
同一道题的四次运行共享 issue、仓库和难度。把它们全部打散,会把重复运行带来的信息量估得过大。因此,置信区间按任务做配对 bootstrap:每次抽取一组任务,保留所抽任务的四次重复,并在被比较的组里使用同一组任务索引。Bootstrap 是通过重采样观察结果波动的方法;配对以任务为单位,各组同编号的运行没有共享随机数。
冻结分析使用 10,000 次重采样、种子 20261005 和原任务集合。完整 20 题中,B−A0 的 95% 区间为 18.75~53.75 个百分点。这个区间描述当前这组任务上的不确定性,不能自动外推为所有仓库、所有问题或所有模型的总体保证。任务只有 20 个,重新抽到哪些难题,确实会明显改变差值。
三组放在一起时,需要处理 A2 的两个 null。它们位于 requests-2148 的两次运行;null 表示冻结评分链没有得到数值结果,不能按失败补成 0。为了让三组在同一个完整任务集合上比较,原分析使用共同 19 题,整题排除 requests-2148。这与只删掉两个缺值再计算 31/78 不同,后者会让该题在 A2 中获得较小权重,却在其他组中保留完整权重。
表中沿用已冻结的 reviewed 层,也就是按既定证据条件完成复核后的评分记录。A0 的 requests-2148 第 2 次重复原来也是 null,已有复核凭完整的补丁应用失败证据将它记为 0。A2 两次缺少相同判定所需的完整证据,仍保留 null,不能只凭一条错误消息补齐结果。
共同 19 题中,A2−A0 为 7.89 个百分点,95% 区间 2.63~14.47;B−A2 为 27.63 个百分点,区间 9.21~47.37;B−A0 为 35.53 个百分点,区间 17.11~53.95。
冻结任务均值与任务配对 95% bootstrap 区间。完整 20 题 A0/B 各 80 次;共同 19 题三组各 76 次,整题排除含两次 A2 null 的 requests-2148。A0 为后追加组,图中比较不代表三组同期设计。
3.2 组间差异、输出量与请求费用
共同 19 题中,无工具多轮修订组 A2 的任务均值高于一次生成组 A0。这是两组最终候选的表现差异,记录没有据此证明某次修订把错误答案改成了正确答案。A0 与 A2 同时改变了调用次数、单次预算和修订结构,又是顺序完成的两组;7.89 个百分点不能直接归给某一种自我修订机制。
B 与 A2 的差异则更接近原来的研究问题:给静态方法后续修订机会后,完整交互系统仍有更高的任务均值。只是 B 额外获得的能力仍然成束出现。继续读源码可以改进定位,真实编辑可以改进补丁交付,执行结果可以暴露行为错误,反馈又会影响下一步检索和修改。在这组任务与记录的模型配置下,完整 Loop 的任务平均成功率更高;源码交互、实际编辑、执行、反馈与修订的贡献仍无法分别估计。
实际输出量也有助于判断这个解释。80 条最终运行链累计 completion token,A0 约 223 万,A2 约 507 万,B 约 221 万;平均每次约为 27,855、63,378 和 27,563。A2 的输出约是 A0 的 2.3 倍,B 的输出量却与 A0 接近。这组记录没有呈现输出越长、成功率越高的关系。这里计算的是记录中的输出数量,不包含工具执行成本,也不是模型实际算力测量。
按历史价格和全部已知正式请求的 token 记录估算,A0 约 2.69 美元,A2 约 6.21 美元,B 约 4.17 美元;B 另有四次请求缺少可计价记录。这里的 B 费用包含中断的旧尝试,与上段最终 80 条运行链的输出量不是同一个集合。金额是已知模型请求的估算,并非完整账单,也没有计入工具执行、搭建和维护环境的时间。判断方法是否划算,还需要把这些投入与具体任务的收益放在一起看。
逐题结果显示,B 的优势分布并不均匀。完整 20 题中,B 相对 A0 有 12 题更高、7 题相同、1 题更低;共同 19 题中,B 相对 A2 有 8 题更高、10 题相同、1 题更低。均值里的增益集中在一部分任务,而不是所有题各自提高一点。
逐题成功次数差,每题四次重复,每增加 1 次相当于 25 个百分点。B−A0 展示完整 20 题,B−A2 展示共同 19 题;任务按 B−A0 排序。图反映任务间差异,不把重复编号当作跨组配对的随机样本。
增益最集中的四题是:django-14238、pytest-11143、pytest-7490 和 sphinx-11445。A0/A2 都是 0/4,B 都是 4/4,仅这四题就占共同集合 B−A2 净增加 21 次成功中的 16 次。接下来结合高收益任务和失败案例,回看具体轨迹。
反例也同样具体。seaborn-3190 中 A0 是 3/4,A2 是 4/4,B 只有 2/4;flask-4045、xarray-4248 和 sphinx-7686 三组都是 0/4。完整 Loop 仍然会遗漏约束、选择无效动作或依赖覆盖不足的局部检查。下面先按补丁处理过程分类,再回到具体轨迹分析。
4. 补丁生成、应用与评分的失败阶段
4.1 候选交付与阶段分类
模型回答需要经过候选提取、补丁应用和测试等环节,才能得到评分。首先要有可提取的候选,候选要符合 diff 格式,能应用到评分环境里的源码,然后才轮到构造官方测试、执行测试和汇总结果。某一次运行最终是 0,可能是在最前面就没有交出补丁,也可能已经正确修复一部分功能,却在后面的行为检查里失败。因此,失败位置是解释成功率差异的一条线索。
正式实验结束后的失败阶段分析,记录每次运行最早可确认的失败。空候选(E)表示最终没有可用 diff,即使回答中有解释;格式失败(M)表示补丁结构无法正常解析;应用失败(P)表示格式成立,但修改落不到评分源码上。测试或评分失败(T)覆盖应用之后的测试补丁构造、测试运行和结果判断。成功记作 R,问号保留没有数值评分的 null。
这个分类能定位失败发生在哪一层,却不是每次失败的完整根因。尤其是 T,不能直接翻译成模型的产品代码写错了。如果官方测试补丁无法与候选共存,测试可能尚未完整构造;如果只运行了部分测试,即使那些测试全部通过,最终评分仍可能失败。要解释这些情况,必须回到轨迹与日志。
候选生成到评分的路径。E/M/P 对应较早的交付关口,T 汇集应用之后的测试与评分失败,不能仅凭标签认定产品代码根因。生成期间的局部执行与候选冻结后的官方评分是两个阶段。
按每组 80 个计划运行位置看,A0 的 E/M/P/T/R 是 5/13/16/21/25;A2 是 3/5/17/22/31,另有两次 null;B 是 3/0/0/23/54。B 的 77 个非空候选全部通过格式和应用检查。这个现象与 B 的交付方式相符:模型编辑实际文件,再收集工作区 diff,减少了手写补丁格式与上下文的负担。
但是,这些计数不能把 B 的全部增益解释完。A0/A2 的早期失败更多,B 的早期失败更少,只能说明交付路径确实不同。通过应用的候选已经是各组筛选后的不同集合,不能再比较它们的条件成功率,声称剩余差异就是执行反馈贡献。同一个能力也可能同时改善多个阶段:新读到的源码既能避免应用失败,也能让模型选择正确的修复位置。
每组 80 个计划位置的最早可确认阶段。A2 两次 null 原样保留;B 的 77 个非空候选均通过格式与应用,另三次为空。T 是测试或评分阶段失败的合并桶,不等于统一的产品代码错误。
4.2 静态材料缺口与补丁应用拒绝
前图按最终阶段统计了 33 次应用失败。应用日志还记录了另一次拒绝,但那次最终评分缺失,保留为 A2 null,因此这里共讨论 34 条拒绝记录。其中 29 条涉及要修改的文件没有出现在固定 BM25 材料中:模型可能知道该改哪个概念,却缺少文件里的准确代码与上下文。能继续读取文件,就有机会把这种猜测变成确认。
另一些应用拒绝发生时,相关代码已经可见。flask-4992 的 A0 第 1 次重复里,相关旧代码已经在输入材料中,候选仍使用了没有上下文的 hunk,最初的 git apply 拒绝了它。Hunk 是 diff 中描述一段局部修改的块;缺少周围未改动的代码,会让应用器无法按正常方式确认修改位置。后续尝试的部分应用与反向提示发生在第一次拒绝之后,不能倒过来解释成评分环境一开始就已经修好了问题。
还有几次拒绝的旧代码块,在 B 的完整可见文件里也找不到。部分评分环境还经过了准备提交,并非原公开任务的起始提交;现有材料不足以逐次确认完整评分源码基线及其影响。因此,29 次材料缺口是可观察事实,不是对 34 次失败的最终根因判决,更不能拿来给动态源码读取分配一个精确百分点。
对评分链而言,修复想法需要落实为目标代码上能够应用的修改。B 的实际编辑与 diff 收集方式减少了这类交付负担;这项观察仍不足以估计它独立带来了多少成功。
5. 真实轨迹中的运行反馈与失败原因
5.1 Django-14238:类型修复与复现环境调整
django-14238 涉及默认主键类型配置。Django 的 DEFAULT_AUTO_FIELD 可以指定模型未显式声明主键时使用的字段类。任务里,用户定义了 MyBigAutoField,继承 BigAutoField,希望把它设为默认类型;框架却把它判成不是 AutoField 的子类,拒绝使用。
这个问题的细节藏在类型兼容逻辑中。几种自动主键字段并不是简单排成一条普通继承链;Django 用 AutoFieldMeta 的特殊判断,让 BigAutoField、SmallAutoField 也能被视为 AutoField。原判断列出了受认可的类,但只认这些类本身,没有把它们的派生类一起纳入。因此,内置 BigAutoField 可以通过,自定义的 MyBigAutoField(BigAutoField) 却会掉出去。核心修改是把对象是否在列表中的判断,改成沿继承关系检查:
1 | - return subclass in self._subclasses or super().__subclasscheck__(subclass) |
这既让列出类型的子类也成立,又保留了其他正常的类型判断路径。
在这道题上,A0/A2 都是 0/4,B 是 4/4。具体轨迹可以进一步说明这四次成功中发生了什么。B 的第 0 次重复检查了 issubclass 的相关结果,再运行默认主键测试,记录里有 10 项通过;模型选项相关测试还有 22 项通过、3 项跳过。这些检查把代码里的类型关系和用户可见配置连接起来,而不只是看补丁是否语法正确。
第 3 次重复中,执行反馈主要用于调整复现环境。第 5 步,产品代码的修改已经完成。后面第 9 步的自定义复现发生导入失败,第 10 步遇到断言失败,第 11 步检查全局与应用配置的差异,第 12 步才让临时应用 myapp 的端到端复现通过。第 5 步以后,产品补丁没有继续改变;继续改变的是验证脚本与 Django 应用配置。
django-14238 的 B 第 3 次重复。产品修复在第 5 步完成,后续失败促使模型调整复现环境,直到第 12 步端到端检查通过;产品补丁没有再次改变。
这次运行中的失败反馈指向复现环境,后续调整没有再次修改产品补丁。 导入错误、断言结果和配置检查帮助模型定位了临时应用的构造问题。若只看失败后通过的顺序,就会漏掉被修改的对象:这条轨迹验证了已有产品改动,不能据此声称产品逻辑经过多轮纠错才修好。
静态组的部分候选也能提出相近的修复方向,却在补丁交付等环节没有拿到成功。所以,这道题支持的是 B 在定位、实际编辑和检查的组合下更可靠地完成任务。它没有单独证明:如果拿掉执行能力,原来四次 B 成功都会变成失败。那是后来执行消融想问的问题,需要另一个真正控制住静态访问条件的实验。
5.2 Flask-4992:本地测试与官方测试构造冲突
flask-4992 的背景就是前面配置加载的例子。Flask 的 Config.from_file 打开文件,再把文件对象交给用户提供的 load 函数。默认文本模式适合一些解析器,但 Python 的 tomllib.load 要求二进制文件对象。合理的接口需要允许调用者选择打开模式,让解析器拿到它要求的流类型,同时保持默认调用的兼容性。
B 的记录里有成功路径。例如第 1 次重复,一次局部配置测试记录 18 项通过,另一次临时模拟记录 2 项通过,随后官方评分运行了 19 项并得到成功。两边测试数量不同并不奇怪:生成期间运行的是模型选择的检查,官方评分还会构造任务规定的回归测试。局部检查用于帮助生成,官方测试用于最后判断;不能因为数字不同就把两者混成同一份验证。
第 0 次重复则留下了一个相反的例子。模型第 8 步为局部验证添加了 tests/static/config.toml,第 9 步的局部配置测试有 19 项通过。之后还检查了更广的套件与原代码对照,没有继续修改产品逻辑,最终在第 15 步结束生成。候选增加了文本与二进制模式选择,但这个测试 fixture 也留在工作区里,随 diff 一起进入最终候选。
Fixture 是测试使用的固定数据。问题在于,官方测试补丁同样要添加这个路径。候选应用本身已经成功,随后构造官方测试时出现同名文件冲突,预定的测试补丁没能完整落下。最终日志记录 18 项通过,冻结评分仍为 0。这次失败处在测试构造与评分链上,不能凭局部 19 项通过就追认成一次成功;也不能不看冲突,就断言新增 API 的实现必然错误。
flask-4992 的 B 第 0 次重复。局部 fixture 随候选提交,在生成结束后的官方测试构造阶段发生同路径冲突。局部 19 项通过与最终 0 属于不同阶段。
这条记录包含生成期间的局部反馈与生成结束后的官方结果。模型真正收到的是自己运行的局部测试输出。最终官方评分发生在它结束之后,它不知道将要应用的测试补丁,也没有机会依据最终冲突继续行动。讨论 Agent 的自我修订时,必须说清楚反馈来自哪一套检查,以及它在候选冻结之前还是之后出现。
它也揭示了交互式系统自己的交付风险。真实工作区方便修改与验证,但验证过程中生成的文件不一定都应该成为产品候选。工作区状态、候选收集和测试构造之间有一个接口,接口处理不当,会让候选在到达目标测试之前失败。
5.3 Flask-4045:相邻 API 的异常契约遗漏
并不是所有失败都可以用环境或测试构造解释。flask-4045 三组都是 0/4,B 的第 3 次重复显示了一个普通却重要的遗漏。这个 issue 与嵌套 Blueprint 的命名有关。Blueprint 可以组织一组路由;嵌套之后,框架会把父子名称用点号连接,例如 parent.child.c。点号在这里表达层级,如果用户给单个蓝图的 name 自己带上点号,层级含义就会混淆。
已有代码还限制 endpoint 不能含点号。Endpoint 是反向查找路由时使用的标识,不是 URL 路径;它与蓝图 name 是两个不同入口。原 issue 提到了已有 endpoint 限制,并要求补上蓝图名称限制。官方测试同时检查错误应如何暴露:含点号的 endpoint 应抛出 ValueError,而已有路径用的是 assert,实际得到 AssertionError。
这次 B 运行添加了构造蓝图时的 name 检查,验证了非法名称,也演示了合法嵌套的行为。那些局部检查通过,但没有覆盖 bp.route('/', endpoint='a.b') 这条 API 路径。官方日志在这里明确失败:旧 add_url_rule 的断言仍然抛出 AssertionError,没有满足测试期待的异常类型。
能否发现这个遗漏,取决于检查覆盖了哪些契约:名称拼接、构造参数、路由注册和错误类型各自都可以成立或失败。模型选了一条看似合理的复现路径,却遗漏了相邻入口。局部测试通过只覆盖已执行的检查。 未运行的 API 路径及异常契约,仍可能决定最终评分。
三个案例展示了运行结果的不同用途与限制。Django 的反馈帮助把验证环境搭对;Flask 配置加载的局部通过没能保证官方测试顺利构造;Flask 命名检查又留下了产品行为上的真实缺口。它们都涉及运行结果,但运行结果在每条轨迹中承担的角色不同。总体成功率不会直接呈现这些过程差异,轨迹分析补上了这一层信息。
6. 执行能力消融的设计与工程阻塞
6.1 控制静态访问与编辑条件
第二轮结束后,尚未回答的问题是:静态访问和编辑条件相同时,程序执行还能带来多少增益。可以设想关闭 B 的 Shell 后进行比较。但旧 Shell 不只执行测试:cat 可以读文件,rg 可以搜索,命令也能帮助确认工作区状态。删除这个工具,会同时删除静态访问能力,得到的仍然是多种条件一起变化的比较。
因此,候选执行消融设计了 FULL 与 STATIC。消融就是有控制地移除某种能力,观察结果变化。两组都保留相同的可信静态接口,可以读取、搜索、编辑源码,使用相同输入和候选收集方式;FULL 额外获得模型可调用的 run_task 及其运行结果,STATIC 没有程序执行。两组仍然都会收到读取或编辑错误,所以 STATIC 也不是毫无反馈的长输出方法。
每次执行都读取调用那一刻的源码快照,执行产生的文件等副作用不直接写回候选。这样,FULL 多得到的是运行信息,两组仍通过相同渠道编辑待交付的代码。计划是在原 20 题上,每组每题四次,共 160 次正式运行。它要估计的是,在静态访问与真实编辑相同的前提下,可控程序执行和运行信息的整体增量。
计划中的 FULL/STATIC 共用静态访问、编辑与候选收集,仅 FULL 多受控执行及运行结果。要开展有效比较,还需真实任务的源码与运行绑定、网络准入和评分传输完成验收;研究已决定停止,正式生成与评分均为 0/160。
这个对照没有进入正式运行。真正的困难在于,先要固定每题使用的源码,确认执行加载的是刚编辑的版本,再让联网测试与最终评分完整走通。任何一环没有接好,FULL 收到的反馈就可能与当前候选无关。下面两个任务暴露了源码绑定与联网执行尚未完成的环节。
6.2 Astropy:源码来源与编译产物绑定
Astropy 把一个运行环境问题变得很具体。它包含需要编译的扩展。对纯 Python 文件,通常可以较直接地检查导入路径;对扩展模块,磁盘上的源码、准备阶段生成的二进制产物,以及测试真正加载的模块,可能不是同一层东西。即使源码已经修改,旧的编译产物仍可能被加载,产生与当前候选无关的通过结果。为了比较执行能力,需要先确定每题应使用的源码,再确认准备环境与运行环境都来自这份源码。
已有组件验收记录中,准备和导入曾经成功,但完整源码捕获在 cextern/trim_wcslib.sh 的 0777 权限检查处被拒绝,后续程序运行没有执行。0777 表示文件允许所有用户读、写、执行,原捕获入口不能直接把这类可写路径当成可信来源。阻塞发生在运行之前,不能用它判断模型补丁的正确性;改掉权限数字,也不足以证明源码来自正确任务。
后来的只读补证确认,这个权限在准备前就已经存在,却没有确定它的成因。对预先存在的源码做哈希,也只能证明某两份字节相同,不能单独证明它们就是该任务应当使用的权威输入。新的完整来源路径仍缺少一份从其他可信来源预先固定的文件清单,无法用自己刚抓取的清单证明自己抓取正确。这是来源校验与内容一致性之间的区别。
最小方案还需要解决编辑后的运行绑定:哪些产物可以复用,哪些必须重建,以及测试实际加载哪个模块。已有离线测试能检查缓存规则与模拟文件系统上的边界,却不能证明真实 C/Cython 扩展已按当前编辑正确重建、链接并加载。准备成功、导入成功、编辑后运行可信,是三个不同的完成条件。
这些条件直接影响比较的有效性。如果 FULL 收到的结果来自旧二进制,反馈就没有对应当前编辑。正式运行之前,需要在真实任务中确认源码、编译产物和已加载模块的关系;仅有准备或导入成功的记录仍不足以完成这项验收。
6.3 requests:联网测试与评分传输
requests-2148 要修的是底层 socket reset 如何包装成 requests 异常。环境验收遇到的是另一层问题:既有回归测试需要访问 HTTPBin,并检查跨主机重定向时 Authorization 的处理,因此必须有能实际使用的联网测试路径。
旧流程要求相关 DNS 结果剩余有效期满足固定预检窗口。有一条记录里,剩余 TTL 约为 100.84 秒,低于 130 秒要求,流程便在启动官方 verifier 之前停止。TTL 是 DNS 记录还能缓存使用多久。这个结果只证明那次准入条件没有满足,不能证明 requests 的补丁或测试不正确。
后续方案让连接使用时再确认允许的地址,减少一次性预检对剩余 TTL 的依赖。离线模拟能验证过期判断与错误路径,但真实 HTTPS 获取与受控转发通道还没有接通。它完成了部分接口逻辑,还没有让真实回归测试跑起来。
网络之外,评分也存在最后一段距离。新执行环境生成的候选,要可靠地送到已有评分环境,再把原始日志与判分结果带回来。最小方案复用官方解析器,把冻结候选与报告绑定,并检查缺失状态怎样表示;但模拟评分器能够产生符合格式的报告,不意味着真实传输、结果收集与清理流程都已贯通。对 20 个任务而言,需要的是完整输入、实际执行与评分链的可靠连接。
6.4 未完成的验收与停止决定
后来尝试缩小实现范围,旧记录称这个工程方案为 minimal B;它与第二轮的 B 组不是一个新增实验。它不必整体迁移已有的 Windows/WSL 评分器,不把一次宽泛 DNS 预检当成唯一入口,也复用评分解析器。这些取舍减少了设计依赖,却没有消除决定实验有效性的工作:可信源码清单、准备与构建产物的绑定、真实网络通道,以及可靠送出候选、收回报告和清理运行状态,仍然需要完成并整合。
最后一份离线 Linux 工程检查报告记录 154 项测试:153 项通过,1 项属于 Windows 条件而跳过,没有失败或错误。这个数字回答的是新代码在合成文件系统、假 DNS 和模拟评分器上的接口与边界是否成立。它不是 154 道 SWE-bench 题,也不是 FULL/STATIC 的正式样本。旧阶段只有少量任务的部分组件验收,不能转移成新方案对全部 20 题的运行保证。
继续完成消融所需的投入,主要落在真实环境接入、来源核对与整体验收上。模型请求费用只是其中一部分;已有 API 费用估算无法代替对这些工程工作的判断。
最终,研究选择停止执行消融。FULL/STATIC 的正式生成与评分均为 0/160,没有产生执行能力的增益估计。 停止是对后续投入的取舍,不能解释为已经观察到执行反馈无效。
收尾时保留了已有结果、未完成的验收和剩余问题。第二轮的成功率差异仍然成立,进一步分离执行贡献所需的对照则没有完成。
7. 研究结论与未验证的机制
Round 2 给出的最明确结论是,在这组固定任务和记录的模型配置下,完整 Loop 的任务平均成功率更高。A0 的一次长输出、A2 的无环境反馈修订,以及 B 的环境交互,是三种不同的工作方式。A2 的均值高于 A0,B 又高于 A2;这些差异发生在调用结构与信息条件都不同的系统之间。已有结果不足以将它们解释为单项能力的因果贡献。
工具调用在这些轨迹中改变了可用信息或工作区状态。读取文件可能改变模型知道的事实;真实编辑改变待交付的状态;执行把某些假设变成可观察结果;后续行动再决定怎样使用这些结果。当前实验支持完整组合在本轮任务中表现更好,各能力的独立贡献仍需其他对照。
解释运行结果还需要确认它对应什么检查。Django 的失败消息来自复现配置;Flask 的绿色输出只覆盖局部检查;官方评分还可能在构造测试时遇到候选文件冲突。一个 Agent 不仅需要收到结果,还需要知道结果对应哪个代码状态、哪套断言和哪个阶段。这些条件决定一条运行结果能够支持多大范围的判断。
这对日常编程有一个朴素的启示:我会据此优先让模型查看真实代码、交付真实改动,并验证与问题相关的行为;还要认真选择检查范围与运行环境。局部测试通过是一条证据,完整任务完成是另一个判断。对于依赖编译产物、外部服务或复杂初始化的仓库,环境本身也要成为可检查的对象。
执行能力在相同静态访问下贡献多少,这个项目没有得到答案。FULL/STATIC 停止后,这个问题仍然开放。已有结果支持完整 Loop 在本轮固定任务上有更高的任务平均成功率,不能拆分源码交互、执行、反馈与修订的贡献。 三个案例保留了具体工作过程:哪些检查帮助验证,哪些局部通过没有覆盖完整任务,以及交付接口怎样影响最终结果。这些记录可以指导后续实践,也保留了需要另一项有效对照才能回答的问题。