论文精读

精读:把公开代码库里已实现的功能翻成 RL 环境

CodeMidas 只用源代码作任务侧输入,把开源仓库已实现的功能自动翻成可执行编码 RL 环境;5,545 个任务训 MiMo-V2.5,五个外部基准全涨,而答案本身大概率已在预训练语料里。

论文: CodeMidas: Scaling Agentic Coding RL Environments from Code Itself
作者: Bowen Ye, Lei Li, 等
arXiv: 2609.22068
编码智能体 强化学习环境 数据合成 执行验证 GRPO 数据污染 代码库 后训练

1 论文概览

一句话总结:CodeMidas 用一条全 agent 化的流水线,把开源仓库里「已经实现的功能」直接翻成可执行的编码 RL 环境——删掉核心实现作为起点,另存原实现作为参考解,再按「跑原始代码得到的行为」造测试,最后用 rollout 过滤掉能泄漏、判定不一致、以及一边倒的题;5,545 个任务训 MiMo-V2.5,五个外部基准全涨。

核心问题:agentic 编码 RL 需要两样东西——足够多样的任务,和足够可靠的验证器。既有流水线把任务绑在人类的开发记录上:issue、PR、commit、既有测试、文档。可挖的题量于是受限于「人类留下多少记录」,而不是「世界上存在多少代码」。作者要问的是:能不能只拿源代码本身当任务侧输入。

“Open-source codebases offer a rich source of such tasks, while existing methods typically rely on development artifacts such as issues and commits, limiting the range of tasks that can be extracted.” —— 摘要

主要贡献:

  1. 任务侧输入彻底解绑。Table 1 把八条流水线按「是否需要 issue / PR / commit / 既有测试 / 书面描述」排开,CodeMidas 是唯一五列全免的一条,且语言覆盖 23 种,对比组是 20 / 1 / 1 / 1 / 1 / 11 / 1 / 15。
  2. 四段式构造流水线:任务设计与代码库改造 → 执行锚定的测试构造 → 环境准备与执行一致性检查 → 出辊后过滤(泄漏探测、判定一致性、通过率筛选)。每一段都把 agentic 算力花在构造侧,而不只是花在训练侧。
  3. 规模与覆盖:5,545 个任务来自 3,185 个代码库,23 种语言、15 个技术领域;参考解中位 142 行(IQR 66–305),65.9% 的任务跨两个以上源文件。
  4. 实证:GRPO + 纯二进制执行奖励训 MiMo-V2.5,DeepSWE 10.0% → 21.7%,ProgramBench Almost Solved 4.5 → 21.5,Terminal-Bench v2.1 63.7% → 72.2%。
  5. 两条分析:任务规模与质量(1k < 3k < 5k,且过滤后的 3k 打赢未过滤的 8k),以及训练过程中的行为变化(探索 27.2 → 40.1 次调用、起草比 0.358 → 0.629、自验证命令 2.03 → 2.53)。

2 方法详解

2.1 任务从哪来:删掉实现,留下上下文

一个 agent 先看代码库结构与构建元数据,挑出「有公共入口、有可观测结果」的功能;接着追踪公共入口与共享依赖来划定任务边界,把选定的核心实现删掉,再调整剩下的代码,让它成为一份连贯的开发起点。被删掉的原实现单独留作参考解。任务声明只规定输入、可观测行为、必需的公共接口,内部用哪个 helper、哪个算法由解题方自己定。声明与代码边界是一起修订的:共享组件与项目上下文要保留,不能抽成一个孤立的函数题。

这一步的设计取舍很关键:任务不是「修一个 bug」,而是「把一段已存在的公共功能补回去」。声明的自由度留给了内部实现,公共接口却是照着原实现写的。

2.2 测试锚在「原始代码跑出来的行为」上

agent 把声明里的行为要求映射成测试输入与边界用例,在参考副本上调用公共入口并记录结果:CLI 工具走命令执行,纯函数走输入输出用例,有状态 API 走调用序列(含顺序与清理行为)。声明定死的部分,用参考执行来定期望值;声明没规定的部分,只查约束——比如只强制异常类型,不固定报错文案。

之后是本文里我最认可的一段工程细节:

“An agent then reviews every assertion for restrictions unsupported by the statement, such as exact wording, incidental ordering, or internal structure. It replaces these restrictions with behavioral checks while preserving the checks required by the statement. A task is rejected if an assertion depends on a private symbol and has no behavioral substitute.” —— §3.2

也就是说,测试与声明同源(都来自同一份原始代码),靠一轮专门的复查把「声明没支持、实现却强制」的限制剔掉,剔不掉就整题弃用。这正面处理了合成环境里最老的两个毛病:测试过严(拒绝等价的正确实现)与测试过松(放过错误实现)。

2.3 环境准备与执行一致性

从统一基础镜像起,agent 按项目声明装依赖、准备构建与运行时资源,然后清理一切可能暴露被删实现的东西:编译产物、缓存副本、构造 agent 留下的文件,以及已安装的目标项目副本;与目标功能相关的原测试也一并删掉。

一致性检查用六个全新容器:两个装起点代码,四个装参考解。起点必须两次都失败,参考解必须四次都通过。这道筛子便宜、可并行,还能挡掉一大批执行不稳定的题——顺带挡掉的是「起点意外能过」这类弱测试题,代价是两次采样对偶发通过的检出率有限。

2.4 出辊后过滤

执行检查只覆盖了起点与参考解两端。训练之前,论文再用 rollout 补三道筛子:

  • 泄漏探测:一个 agent 专门尝试走捷径——搜遍解题方可见的环境(编译产物、缓存、构造残留、已安装的项目副本),为每处疑似泄漏留下命令与输出证据;另一轮复核拿证据对照参考解与验证器,确认能绕过实现工作的题一律弃用。
  • 判定一致性:一个编码 agent 每题尝试四次,审阅 agent 对照声明、验证器、参考解查 rollout,标出假阳性(判定为错却过测)与假阴性(判定为对却没过)。有验证器缺陷的题弃用。
  • 通过率筛选:一个前沿模型每题尝试若干次,只保留既有成功又有失败的题。论文自己也承认这道筛子分不清原因:

“All-pass or all-fail outcomes may reflect task difficulty or remaining defects, such as weak tests or requirements missing from the statement. These outcomes do not reveal the cause.” —— §3.4

三道筛子里,第三道对最终数据集的形状影响最大,也是第四节要重点追问的地方。

2.5 与既有路线的区别

Table 1 给出的是一条「输入依赖」的刻度:SWE-rebench V2 要 PR 和 commit,daVinci-Env 还要 issue,R2E-Gym 要 commit,SWE-smith / SWE-Flow / SWE-Hub 要既有测试,R2E 与 MindForge 要书面描述(docstring / 文档)。CodeMidas 五列全免,绕开了「开发记录覆盖面」这个天花板,代价是把「描述从哪来」这件事从人类记录搬到了 agent 生成。

奖励侧它走的是最朴素的一条:GRPO + 二进制执行奖励,没有 reward model,没有 learned verifier,连 advantage 的标准差归一化都关掉了(附录 A1)。对比 SWE-RL 用参考补丁相似度、Agentic Rubrics 用 rubric 打分,CodeMidas 把可信度全压在执行上。

本专栏此前记过两条造环境的路线:Terminal-Universe 把轨迹反演回环境,SPADE 让环境生成器与策略自博弈。CodeMidas 是第三条——不去造新环境,而是从既有代码里挖出环境。三条路线的原料不同,「答案是否已经躺在预训练语料里」这个问题只在第三条上最尖锐。

3 实验结果

3.1 主要指标

基准初始策略CodeMidas RL绝对增益相对增益
DeepSWE(pass rate)10.021.7+11.7 pp×2.17
ProgramBench(Almost Solved)4.521.5+17.0 pp×4.78
Terminal-Bench v2.1(pass rate)63.772.2+8.5 pp+13.3%
CodeMidas Val(200 题 × 3 次)35.044.7+9.7 pp+27.7%

几点读法要校准。ProgramBench 的 +17 是百分点不是百分比,它的相对涨幅最响(4.78 倍)恰恰因为基数只有 4.5。Terminal-Bench 基数最高,+8.5 pp 折成错误率是从 36.3% 降到 27.8%(错误相对减少 23.4%)。SWE-bench Pro 与 RepoZero C2Rust 的绝对值正文一处也没给,只在 Figure 5 与 §5.1 的差值里出现——五个基准里有两个无法从正文核对基线水平。

3.2 规模与质量消融

1k / 3k / 5,545 三个高质量池:DeepSWE 17.57 / 19.05 / 21.70,CodeMidas Val 41.30 / 43.22 / 44.73。过滤后的 5k 打赢未过滤的 vanilla 8k:SWE-bench Pro +0.59、DeepSWE +4.59、Val +4.49。三刀下去,这组数字的承重能力要打折。

第一刀,三个数不是同一枚尺子量的。 正文写得很清楚:1k 的 41.30 出现在第 30 步,3k 的 43.22 出现在第 65 步,5k 的 44.73 出现在第 70 步。取的是各条曲线的峰值,不是同一 step 上的横切。1k 在中段见顶后回落,5k 在最后一步见顶——形状不一样,含义也不一样。

第二刀,规模与 epoch 绑死了。 附录给的批量是 32、每题 32 条 rollout。若按「每步 32 个任务」理解,70 步消耗约 2,240 个任务槽:1k 池跑了 2.24 个 epoch,3k 池 0.75 个,5k 池只有 0.40 个,彼此差 5.55 倍。1k 那条曲线见顶后回落的形状,正对应「同一批题被重复看了两遍以上」。论文既没做等 epoch 的对照,也没做等任务槽的对照,于是「池子更大所以更好」与「池子小所以过拟合」两种解释在这里无法分离。

第三刀,最大的那个差值不显著。 Val 只有 200 道题,+4.49 pp 按两比例非配对检验算,SE ≈ 4.94 pp,z = 0.91(p ≈ 0.36);就算把每题 3 次尝试当成独立样本(N = 600),z 也只有 1.57(p ≈ 0.115),仍未越过 0.05 这条线。反过来看 SWE-bench Pro 上的 +0.59 pp:那是三个差里最小的,而且恰好落在唯一完全外部、任务量也最大的那个基准上。质量过滤最响的证据,来自论文自己的验证集。

补第四刀:每 1k 任务的边际收益在 Val 上从 0.96 降到 0.59(递减),在 DeepSWE 上从 0.74 升到 1.04(递增)。两个方向,凑不出一条干净的 scaling 曲线。

还有一处结构性缺口:清理、执行一致性、三道出辊过滤一共五道手续,实验只给了「全开」与「全关」两档,没有逐段消融。5k 打赢 8k 只能归因于「这一整套」,无法知道是哪一道在起作用——本专栏第 15 期读 SP³O 时遇到过同型问题,解法是把消融表的行两两相减,这里论文压根没给那张表。

3.3 行为分析

行为指标早期后期
代码库探索首次编辑前的 read/search 调用27.240.1
代码起草起草比0.3580.629
自验证编辑后不同验证命令数2.032.53

同一任务、同一 checkpoint 内,写过并执行了自检的 rollout 通过率高 4.2 pp(95% CI 1.8–6.6);按中位数切分的探索与起草分别给 +0.7(CI −1.9 至 3.7)与 +1.95(CI −0.04 至 3.96)。用 CI 半宽反推:自验证隐含 z ≈ 3.4,起草 z ≈ 1.9(p ≈ 0.056),探索 z ≈ 0.49。三条里只有一条真正站得住,摘要却把三条并列成「探索更多、起草更重合、验证更多样」。

因果方向也是反的:这是同一任务内的相关,不是干预。顺手的轨迹才有东西可验、才走得到最后一次编辑,于是「先写后验」也可能是「做得顺」的结果而非原因。论文自己那句 “excluding undefined measurements” 点明了存活者偏差——没走到最后一次编辑的 rollout 被排除,而那批恰恰是最失败的。

交互长度的变化则提示另一层解释:SWE-bench Pro 37.3 → 50.1 个 assistant turn(+34.3%),Terminal-Bench 59.2 → 69.5(+17.4%),ProgramBench 却从 155.1 降到 122.8(−20.8%)。三个基准两个变长、一个变短,说明涨分里有一部分是「更舍得花步数」。论文没有回合预算对齐的对照,Val 上的学习曲线也显示平均长度随训练上升。

4 个人分析

4.1 亮点

  • 原料换对了对象。环境数量的天花板从「人类留下多少开发记录」变成「世界上存在多少代码」,23 种语言对 1 种语言是量级差,长尾语言是开发记录路线根本够不到的部分。
  • 测试构造的那轮复查值得抄。先跑参考执行定期望值,再逐条问「这条限制是声明支持的吗」,无行为替代就整题弃用。这是把「测试过严」从玄学变成了可执行的判据。
  • 泄漏探测做成了独立角色。一个 agent 专门找、另一轮专门复核、逐条留证据,比让构造者自检可靠得多。
  • 行为指标给了 CI,且按任务整簇重采样。这条 methodological 习惯在环境构造类论文里不多见。
  • 任务保留真实仓库结构。声明与代码边界一起修订、保留共享组件与项目上下文,不是抽函数题,是仓库里的活。

4.2 局限性

最要命的一条:参考答案就是公开仓库里已经存在的代码,模型大概率背过。 论文的防护写在 §4.1,但它防的是另一件事:

“We verified that the training set is disjoint from CodeMidas Val and all five external benchmark task sets.” —— §4.1

这是任务级去重,不是预训练语料去重。3,185 个开源仓库的代码,几乎必然在 MiMo-V2.5 的预训练语料里;任务形态又是「把这段已存在的公共功能按原接口补回去」,二进制执行奖励天然奖励「复现原样」。泄漏探测搜的是环境内可见的东西(编译产物、缓存、构造残留、已安装副本),搜不到参数记忆里的那份。

这条不是没法验,是论文没验:把任务池按「仓库或该功能出现在预训练截止之前 / 之后」切成两半,分别训,看增益是否收缩;或者把参考实现做风格改写后再造题。增益在「见过的仓库」与「没见过的仓库」上一样大,结论才站得住。顺带一个更小的口子:Val 那 200 题只说了与训练题不重合,没说仓库级是否互斥——5,545 题来自 3,185 个仓库,平均 1.74 题/仓库,同仓库的题高度同源。

第二条:没有任何一条与其他环境流水线的对照。 论文的动机是「既有方法受限于开发记录的覆盖面」,可实验里没有「用同等数量的 R2E-Gym / SWE-smith / SWE-rebench 式任务训一遍」这一臂。所有增益都是训后对初始策略。于是「源代码是可扩展的基础」这个结论实际只证明了「拿这批题做 RL 有帮助」,没证明「比开发记录路线更划算」。本专栏反复踩到的一类问题——找论文自己最便宜的替代方案对读——在这里是整整缺一条臂。

第三条:通过率筛把最难的那批题系统性地扔了。 只保留筛选模型下既有成功又有失败的题,留下的全部在前沿模型的能力边界之内。全失败的那批被丢掉,而论文自陈这道筛子分不清「题太难」与「题有缺陷」。第 13 期读 Unlocking the Unsolvable 时专门争过「连一条 rollout 都做不对的题」该不该要,CodeMidas 在预处理阶段就把它们滤掉了。更微妙的是:难度带是按前沿模型的成功率校准的,训练策略却是 MiMo-V2.5——筛子对着另一个策略定松紧。

第四、五条见 §3.2:规模消融的三个数字不同尺、质量消融只有全开全关两档。

第六条:验证器的体检只查了四次尝试,而且判定者是模型。 一致性检查让一个编码 agent 每题做四次,再由审阅 agent 判断「这份实现算不算满足声明」。四次采样覆盖不到多少解空间,审阅者的误判会直接留进训练集,而 RL 奖励的正是这个判定。

第七条:成本没记账。 附录只列训练与评测配置,没有 GPU 小时、没有种子数、没有候选池规模(Figure 1 金字塔里的留存数没有进正文)。按论文自己的描述粗算:RL 侧 70 步 × 32 任务 × 32 rollout ≈ 71,680 条轨迹,单条响应上限 516,096 token;构造侧每题至少 6 次容器执行 + 4 次审阅尝试 + 若干次前沿模型筛查 + 若干轮构造 agent 调用,量级与 RL 侧同阶甚至更高,而且被弃用的候选完全不计入 5,545。「可扩展」缺的那个数——每留存一题的单价——论文没给。

第八条:单模型、单种子、外部基准无误差棒。 全部结论建立在 MiMo-V2.5 的一次训练上,正文未报任何一个外部基准的题数,读者无法自行核对显著性。

4.3 对领域的影响

环境侧的竞争维度会变。过去比的是谁能爬到更多 issue 与 PR,接下来比的是谁能把代码切得更干净——Table 1 那种「任务侧输入清单」大概率会成为新的通用对比表。

更值得警惕的是它把「污染」这个老问题重新推到台前。合成环境的答案本来就躺在预训练语料里,RL 增益里有多少是唤起记忆、多少是学会做事,此前从没被认真区分过。这一刀如果不补,整条「用开源代码造 RL 环境」的路线都会带着一个说不清的解释:增益可能只是把预训练时见过的东西在测试时稳定复现出来。

对数据工程实践的提醒则是:可解性过滤是把双刃剑。它让数据变干净,也把能力边界之外全切掉了。挑题之前先想清楚你要的是「更稳的信号」还是「更宽的覆盖」。

5 实践启示

可以借鉴的:

  • 测试与声明同源的构造法:先跑参考执行定期望值,再逐条复查断言是否超出声明;依赖私有符号且无行为替代就弃题。
  • 起点必须失败 + 参考解必须通过的六容器一致性检查。便宜、可并行,是整套流水线里性价比最高的一道筛子。
  • 泄漏探测拆成「找」与「判」两个独立角色,并强制留证据。
  • 通过率筛要配套一句自陈:你丢掉的全失败题里,有多少其实是真难题。
  • 行为指标给 CI,且按任务整簇重采样(不要按 rollout 重采样)。

照这几点先补验证,再决定跟不跟:

  • 语料截止对照:把任务池按预训练截止前后切开分别训,量增益是否收缩。这一条不补,其余结论都悬着。
  • 补一条「同等题量的开发记录式数据」对照臂,才能判断路线优劣而不是有没有效果。
  • 规模消融改成等 epoch 或等任务槽,并在同一 step 上横切。
  • 外部基准报题数、种子数与 CI;Val 与训练题做仓库级互斥。
  • 报每留存一题的 agentic 成本(token / GPU 时 / 容器时),以及候选到留存的转化率。

潜在应用场景:企业内部代码库的 RL 环境化——非公开代码天然绕开了污染问题,是本方法最干净的用法;CLI 工具与库 API 的回归式能力训练(测试锚在既有行为上,正合适);多语言与长尾语言能力(23 种语言里开发记录路线够不到的那部分)。

风险提示:若你的模型预训练语料已含这批公开仓库,+11.7 pp 里有多少能迁移到「从没见过的仓库」是未知数。先在语料截止之后创建的仓库子集上量一次,再决定是否投入。

参考文献

Ye, B., Li, L., Li, S., et al. (2026). CodeMidas: Scaling Agentic Coding RL Environments from Code Itself. arXiv:2609.22068 [cs.AI]. LLM Core, Xiaomi;北京大学;香港大学;中国人民大学.

相关精读