什么是大模型的环境工程:一道题只有「不全对也不全错」才算数

从 GRPO 组内优势的手算出发:8 次 rollout 全对或全错,优势严格为 0,算力白花。由此推出难度有效带 p∈[0.25,0.75],并结合 SWE-bench 字段清单、DAPO 动态采样与 rollout 吞吐工程,说清环境工程到底在工程什么。

你花钱采的 rollout,有一大半可能对训练毫无贡献。决定这个比例的不是模型有多强,是你的题出得对不对

这句话不是修辞,是一个能手算出来的等式。它也是我理解「环境工程」这四个字的入口——「工程」两个字落在哪里,答案是:落在维持一批「不全对也不全错」的题上。

把这条主线说全一点:一道题在这一轮训练里有没有用,取决于同一道题的多次尝试有没有分歧。全对,没分歧;全错,也没分歧。没分歧的题,梯度严格为零,这一题上花的算力一分钱都没换成参数更新。于是环境工程的第一性任务不是「造更多题」,而是造一批分歧度落在中间带、并且随着模型变强而持续更新的题——再加上让每次尝试足够便宜、可复现、判得准。

本站上一篇讲环境的文章《大模型多试几次,为什么反而选错了?》问的是另一个问题:判据对不对(验证器漏了需求,多试反而更糟)。这一篇问的是有没有信号(判据就算完美,难度不对,算力照样白花)。这是两个独立的故障,第五节我会用一张二维表把它们放到一起。

名词速查

术语一句话解释
环境(environment)一段能被反复执行、并自动给结果打分的程序:有初始状态、可用工具、判完成的规则
rollout让模型在环境里从头跑一遍任务,得到一条完整轨迹和一个分数
GRPO一种 RL 训练方法:同一道题采样一组答案,用组内相对好坏当训练信号,不需要额外的价值网络
组内优势(group advantage)一次 rollout 的分数减去这组的平均分(GRPO 里还会再除以这组的标准差)
零梯度优势算出来是 0,参数不动——这次采样等于没发生
通过率 p模型在某一道题上单次尝试成功的概率
组大小 G同一道题采样几次
动态采样多采一些题,把「全对」和「全错」的题丢掉,只留有分歧的进这一批训练
FAIL_TO_PASS / PASS_TO_PASSSWE-bench 式任务里的两组测试:前者必须由修不通变通过,后者必须一直通过
flaky 测试同样的代码有时过有时不过的测试;在环境里它就是奖励噪声

一、先把「环境」当数据结构看:它到底长什么样

抽象定义讲不清楚,看字段最快。SWE-bench Verified 这个数据集,每条任务实例带的字段是(一手证据:我今天读的 HuggingFace 数据集卡的 features 列表):

repo, instance_id, base_commit, environment_setup_commit,
image, eval_script, log_parser,
problem_statement, hints_text,
patch, test_patch,
FAIL_TO_PASS, PASS_TO_PASS,
version, created_at, difficulty, eval_type

这份字段清单本身就是「环境工程」的定义。 把它分组看,五件事一个不少:

部件对应字段它保证了什么
可复现的初始状态base_commitenvironment_setup_commitimage每次开局完全一样:同一个提交点、同一个装好依赖的容器镜像
可执行eval_script有一条确定的命令能把这个仓库跑起来、把测试跑完
可判定FAIL_TO_PASSPASS_TO_PASS什么叫做完了,写成了两组具体的测试名,不靠人看
可解析log_parser测试框架吐出的日志能被程序转成结构化的通过/失败
可比较patchtest_patch有一份人类的参考解,和一组不该被模型看到的测试

三个容易被跳过、但恰恰是工程量所在的地方:

第一,image 是个容器镜像,不是一段文本。 环境不是数据集——它是一台能开机的机器。这条决定了后面所有的成本结构(第四节)。

第二,PASS_TO_PASS 是一等公民。 只写「哪些测试要变绿」是不够的,还得写清「哪些测试本来是绿的、不许被弄坏」。少了这一组,模型可以把不相干的测试删掉来交差。这是软件工程里的回归测试概念被原样搬进了奖励函数,也是上一篇讲的「判据完整性」在字段层面的样子。

第三,log_parser 的存在说明判分是脏活。 pytest、unittest、tox 的输出格式各不相同,要把「测试通过了吗」这件事变成一个布尔值,得为每个仓库写一个解析器。这类活占了造环境的大部分人力,论文里通常一句话带过。

另一种更抽象的接口长相,是 Gym 风格的。开源环境套件 GEM 的 README 里给的例子是这样(一手证据:GEM 仓库 README 原文):

observation, info = env.reset()
next_observation, reward, terminated, truncated, info = env.step(action)

reset() 回到确定的开局,step() 交一个动作、拿回观察和奖励——这就是 2016 年 OpenAI Gym 的那套契约,一行没改。所以「环境」这个词在这里是回温的旧词:强化学习六十年的工具箱可以直接借,新的只是动作从「按方向键」换成了「敲一条 shell 命令」,观察从像素换成了终端输出。

一句话收住这一节:环境不是题库,是「能开机、能判分、能重置」的一段程序。 造它的成本和维护数据集不在一个量级上。


二、核心:难度不是风格问题,是算术问题

这一节是全文的地基,而且只需要中学算术。

GRPO 的组内优势,手算一遍

GRPO 的做法是:同一道题采 G 次,每次得一个奖励 r(这里用最常见的二值情形:做对是 1,做错是 0),然后把这组奖励标准化,当成每条轨迹的优势:

Ai=rimean(r)std(r)A_i = \frac{r_i - \text{mean}(r)}{\text{std}(r)}

G = 8,把「对了几次」从 8 到 0 走一遍(下面每个数字我都用 python3 验算过,属一档证据):

对/总组内均值组内标准差做对那条的优势做错那条的优势
8/81.00000.00000
7/80.87500.3307+0.3780−2.6458
6/80.75000.4330+0.5774−1.7321
4/80.50000.5000+1.0000−1.0000
1/80.12500.3307+2.6458−0.3780
0/80.00000.00000

两头那两行是这篇文章的全部动机:8 次全对或 8 次全错时,组内标准差是 0,每条轨迹的分数都等于组内均值,优势严格是 0,梯度也就是 0。 这道题上花的 8 次 rollout——8 次容器启动、8 轮工具调用、8 次跑测试——换来的参数更新是零。

有人会问:那不除标准差不就没这问题了?不行。分子 r_i − mean(r) 在全同的时候本身就是 0,除不除都是 0。有一篇 2026 年的论文正好把这件事讲成了一句话——《GRPO, Dr. GRPO, and DAPO Are Three Operations on One Number》:三种方法看起来是三个技巧,其实都在动同一个数——组内标准差,也就是「这道题的多次回答分歧有多大」;GRPO 除以它,Dr. GRPO 不除,DAPO 把它等于 0 的题直接扔掉(二档证据:论文摘要原文,我未复现其分析)。

所以「分歧度」不是某一种算法的实现细节,它是这一整类方法的共同货币。没有分歧,就没有信号。

从「全同概率」推出难度的有效带

既然全同就废,那一道题有多大概率会废?把单次通过率记作 pG 次独立尝试全对的概率是 p^G,全错是 (1-p)^G,于是:

  • 零梯度概率 = p^G + (1-p)^G
  • 有效率 = 1 − p^G − (1-p)^G

G = 8 时(一档:python3 算的):

单次通过率 p零梯度概率有效率
0.010.92277.7%
0.050.663433.7%
0.100.430557.0%
0.200.167883.2%
0.300.057794.2%
0.500.007899.2%
0.700.057794.2%
0.800.167883.2%
0.900.430557.0%
0.950.663433.7%
0.990.92277.7%

曲线是对称的钟形,p = 0.5 最好。要求有效率不低于 90%,解出来的区间是 p ∈ [0.2502, 0.7498]——这就是 G = 8难度的有效带

先把这张表的适用边界钉住,因为它决定了你能怎么用它。上面的算术依赖两个假设:① 同一道题的 G 次 rollout 独立、通过率固定;② 奖励是二值的。 真实情况两条都不完全成立——同一策略同一提示下的多次采样是相关的(实际分歧通常比独立假设算出的低,所以真实有效带比这个区间更窄),p 在训练中会漂移,部分任务的奖励是连续分数(连续奖励下 std 几乎不会恰好为 0,这套二值算术就不适用)。所以这张表该当量级参考和方向判断用,别当预测值用。

(一个实现细节:标准差为 0 时,不同框架有的产出 0、有的产出 NaN 后跳过这批,但两种情况下这批样本都不贡献有效梯度,结论不变。)

这个数字有三个直接的用处:

第一,它把「太简单」和「太难」放在了同一个位置。 直觉上「太难」像是浪费(模型做不出来),「太简单」像是没坏处(模型都做对了,挺好)。算术上它们完全对称:p = 0.95p = 0.05 的有效率都是 33.7%。一个已经被刷穿的题库和一个完全做不动的题库,在训练循环里是同一种废物。

第二,它给硬题标了价。 如果一道题的 p 只有 0.05,想让它至少有 50% 的概率产生信号,需要 G = 14(一档:算到 G=14 时有效率 0.5123 首次过线)。也就是说,难题不是不能用,是要按更大的组去采——代价直接写在采样预算上。

第三,它能算出算力浪费率。 假设你要凑一批 512 个有效 prompt:

题库平均通过率有效率实际要采的 prompt 数多花
0.3094.2%543+6.1%
0.5099.2%516+0.8%
0.7094.2%543+6.1%
0.9057.0%899+75.6%
0.9533.7%1521+197.1%

一个平均通过率 0.95 的题库,rollout 预算要花三倍。这不是模型的问题,是题的问题。

DAPO:论文里的对策,和它的代价

上面这套账不是我推出来的新东西——DAPO 四个组件里就有一个叫动态采样(dynamic sampling),干的正是这件事:训练前先超采,把准确率等于 1 和等于 0 的 prompt 过滤掉,保证进这一批的每个 prompt 都有非零梯度。论文给出的动机与上面的推导一致:优势为 0 就没有梯度,会削弱这一批的梯度幅度、放大噪声敏感度,从而降低样本效率;而且经验上,训练过程中准确率等于 1 的样本数会不断增加(二档证据:DAPO 论文,我未复现其实验)。

最后这半句最要紧,它揭示了一个反馈回路:模型越训越强 → 题的 p 自己往上爬 → 有效率自己往下掉。 用上面的表读一遍:p 从 0.3 爬到 0.6,有效率从 94.2% 升到 98.3%(还在变好);再从 0.6 爬到 0.9,有效率跌到 57.0%。所以静态题库必然过期,而且是在你没改任何东西的情况下自己过期的。

这就是「环境工程」里「工程」的确切位置:它不是一次性造一批题的活,是维持一个通过率分布持续落在有效带里的活。 造题只是它的一个子任务。

对策也有代价,得说清楚。动态采样意味着每批的采样成本不固定;有对比研究报告它使每个训练步的耗时增加超过 25%,且回答越长开销越大;也有人指出过滤会重定义比较集合,可能让次优回答拿到正的优势(二档证据:均见于这篇 PPO/GRPO/DAPO 对比分析与相关讨论,我未复现)。DAPO 作者的辩护是:在同步、非流水线的 RL 系统里,生成时间本来就被长尾样本主导,多采一些不一定真的更慢。

我的判断:这一段的账最值得记住的不是某个具体数字,而是换了一把尺子——评价一个训练环境的第一个指标不该是「有多少道题」,而该是「这一轮里有多少比例的 rollout 产生了非零梯度」。前者是库存,后者是产能。


三、题从哪来:三条造题路线,各有各的塌方点

有效带解决了「该出多难的题」,但题本身得有人造。目前公开的路线基本是三条:

路线怎么造代表工作与论文报告的规模塌方点
挖真实历史从 GitHub 的 issue 与 PR 里还原「问题 → 修复 → 测试」三件套SWE-bench 开的路;SWE-Gym 摘要自称是首个用于训练真实软件工程 Agent 的环境,含 2438 个真实 Python 任务实例,每个都带可执行运行时、单元测试和自然语言任务描述真实历史是有限的,而且难度分布由历史决定,你点不了菜
程序化变异在能跑通的仓库里主动注入缺陷,让已有测试失败,反过来当题SWE-smith 报告了来自 128 个仓库的 5 万个实例注入出来的 bug 分布不像真实 bug;难度可调但可能调出一堆同质题
改装与演化保留原环境的验证器,在外面加可编程组件改开局、改交互规则、改任务组合;或按维度让环境自己迭代变难EnvHarness(组件称 Setup / Rule / Link);Environment Evolution for Terminal Agents(沿场景新颖性、技能稀有性、执行长度演化,按「代」组织训练)改装保住了判据不漂移,但不自动证明原判据完整;演化容易漂向「对当前模型刚好难」而不是「真实世界重要」

三条路线串起来读,是一条很清楚的递进:有题做 → 有地方执行并验收 → 有持续合适的题可学。 第三条正好是第二节那个反馈回路的答案——因为静态题库会自己过期,所以必须有一条持续生成的路。

(SWE-Gym 摘要里还有一条值得记的细节:除了拿它训 Agent(自称最高 19% 的绝对提升),作者还用从这个环境里采出来的轨迹去训验证器,再配合推理时扩展,把 SWE-bench Verified / Lite 推到 32.0% / 26.0%。二档证据:论文摘要原文,我未复现。这条把上一篇的主题接了回来——同一个环境既产训练信号,也产验收能力。)

这里也有个必须说的诚实点:Environment Evolution 是 2026 年 9 月 3 日的预印本,还需要后续复现,我引它只是为了说明研究问题正在往哪儿移动,不是把它当结论。


四、当环境成为训练循环的吞吐瓶颈

第一节说过 image 是个容器镜像。这条的后果在这里兑现。

对比一下两种 rollout 的成本结构:

纯文本推理任务Agent 任务(代码仓库类)
一次 rollout 干什么GPU 上生成一段长思考链起容器 → 装/激活依赖 → 十几到上百步工具调用 → 跑测试 → 解析日志
GPU 在干什么一直在算大部分时间在等
一次的耗时量级秒级分钟级
噪声来源采样温度采样温度 + flaky 测试 + 网络 + 超时

所以在 Agent RL 里,瓶颈从训练器移到了环境层。这不是我的推论,是这批 2025–2026 工作共同的出发点:

  • SkyRL-Agent 做的就是多轮长程 Agent 训练的效率,靠异步分派 + 轻量工具集成 + 多后端互通;论文报告其优化后的异步流水线调度相比朴素异步批处理约有 1.55× 加速(二档:论文数字,未复现)。
  • ProRL Agent 的角度是「rollout 即服务」:把 rollout 从训练框架里解耦出来,支持无 root 的沙箱以便跑在共享 HPC 上,不绑定任何特定训练框架。
  • 训练框架侧,HybridFlow(verl) 这类工作则在解决数据流编排本身的灵活性问题。

有一个细节值得单独拎出来:flaky 测试在环境里不是「烦人的工程债」,它是直接掺进奖励函数的噪声。 同一份正确的补丁,这次跑过、下次跑不过,模型收到的就是两个矛盾的标签。对比第二节的账,这更糟——零梯度只是浪费,错标签是把梯度推向错的方向。所以环境工程里「让测试稳定」这件看起来最不学术的活,其实是奖励质量的一部分。

这也暴露了第二节那个有效带的一个短板,我没有解法,直说:有效带只保证「梯度非零」,不保证「梯度有用」。 一道题的分歧可能全部来自 flaky 测试——8 次里对 3 次,但对错和模型的行为无关。这种题在我的统计里是「有效」的,实际是纯噪声。怎么把「有分歧」和「分歧有信息」区分开,我目前没有可手算的判据。

环境的分发方式也在标准化:像 GEM 这样的套件把接口对齐到 Gym,Prime Intellect 的社区环境仓库(PRIME-Community-Environments: Training-Ready RL Environments + Evals,一手:仓库 README 标题)把环境做成能直接装的包。环境正在从「每家自己攒的脚本」变成「可以被依赖、被版本化、被共享的软件制品」——这也是它配得上「工程」这个词的另一层意思。顺带一句:环境变成依赖,就也继承了依赖的全部安全问题,本站《当提示词成为依赖》讲的是同一类风险的另一个面。


五、两个独立故障:难度 × 判据

现在把这一篇和上一篇放到一张表里。环境有两个正交的质量维度——判据是否完整(验证器会不会放过错的解)和难度是否落在有效带(会不会全对/全错)。两个维度独立取值,就是四个象限:

难度在有效带难度跑偏(全对或全错)
判据完整正常工作:有信号,且信号指向真目标症状:算力账单很高,指标却几乎不动。看板上「跑了多少 rollout」很漂亮
判据不完整最危险:信号强、方向错。模型高效地学会钻验收漏洞,指标一路涨症状:分数看着在涨,但涨的是通过率而不是能力;两种病叠在一起最难诊断

左下角那格是上一篇的主题:判据漏了需求,多生成候选反而让漏洞更容易胜出(那篇有一个 5 分钟可复现的手算反例,p 分布固定下候选数从 8 增到 32,验收通过率升到 99.99%,真正交付正确答案的概率却从 56.33% 跌到 19.36%)。右上角那格是这一篇的主题。

这张表就是我想让你带走的东西:诊断一个训练环境时,先分别问这两个问题,别把它们混成一句「环境质量不行」。它们的修法完全不同——判据不完整要补检查项(先手写一个「看上去像完成了」的坏结果,看它能不能被拒),难度跑偏要调采样与题库(改 G、换题、上动态采样)。


六、自查清单与一个最低成本的实验

审一个所谓「可训练的环境」,我会按这个顺序问:

  1. 它能不能重置到确定的开局? 没有 base_commit + 镜像这一对,前一次尝试会污染后一次。
  2. 它的判据里有没有「不许弄坏什么」那一半? 只有 FAIL_TO_PASS 没有 PASS_TO_PASS,等于给删测试留了门。
  3. 它能拒绝一个已知的坏结果吗? 手写一个假装完成的解喂进去,看判据接不接。
  4. 测试稳定吗? 同一份参考解连跑 10 次,有几次结果不一样?这个数就是你奖励噪声的下界。
  5. 它现在的通过率分布长什么样? 见下面的实验。
  6. 谁有权改判据? 代码和测试都能被 Agent 改时,验收必须独立。这条接《面向 Agent 的门禁产品设计》

实验:不训练,也能测出你的算力浪费率

第五问是唯一需要跑的,而它不需要训练、不需要梯度、不需要多卡——只需要对你现有题库跑一轮 pass@1 采样

对每道题采 G 次(G 用你训练时的真实值,比如 8),记下每题对了几次。然后统计有多少题落在 0/G 或 G/G。这个比例就是你在当前模型下的零梯度率,一个上限意义上的算力浪费率。

# 用你自己的采样结果替换 pass_counts:每道题在 G 次尝试里对了几次
G = 8
pass_counts = [...]        # 例如 [0, 8, 3, 8, 8, 5, 0, 1, ...]

dead = sum(1 for c in pass_counts if c == 0 or c == G)
print(f"零梯度题占比 {dead / len(pass_counts):.1%}")

# 分档看是「太难」还是「太简单」,两者的修法不同
too_hard  = sum(1 for c in pass_counts if c == 0)
too_easy  = sum(1 for c in pass_counts if c == G)
print(f"其中 全错 {too_hard} 题 / 全对 {too_easy} 题")

拿到这两个数就能直接决策:全对占多数 → 题库被刷穿了,该换更难的题;全错占多数 → 要么加大 G(记住 p=0.05 需要 G=14 才有一半概率出信号),要么先降难度或补脚手架。 这比先训一轮再看曲线便宜得多。

想再验一遍第二节那些概率数字,公式就一行:1 - p**G - (1-p)**G。把你的 G 和目标有效率代进去,就得到你自己的有效带,不必用我这个 [0.2502, 0.7498]


通俗总结:用带学生的比方讲一遍

如果上面那些公式和论文让你读糊了,这一节把整篇话不带术语重讲一次。

把训模型想象成给一个学生出练习题。

你手上有一叠题,学生每道题做 8 遍。你怎么知道哪道题对他有用?

  • 一道题他 8 遍全做对 → 他早会了,这题白出。
  • 一道题他 8 遍全做错 → 他离得太远,这题也白出。
  • 一道题他对了 3 遍错了 5 遍 → 这才是能教会他东西的题,因为「哪次对了、哪次错了」的差别,就是他能学的全部内容。

模型训练里的那套数学,说的就是这件事,只是它说得更绝:全对和全错的题,学到的东西不是「比较少」,而是精确的零。 那 8 遍的算力全部作废。

于是三件事一下就顺了:

第一,「太简单」和「太难」一样糟。 我们直觉上觉得题简单点没坏处,其实它和难到做不动是同一种浪费。算出来的数还挺对称:做对九成半的题和只能做对半成的题,浪费程度一模一样。

第二,好题会自己变坏。 学生在进步,昨天让他对三错五的题,今天他八遍全对了——这题就废了。没人动过这道题,是它自己过期的。 这就是为什么造好一批题不算完事:得有人一直盯着、一直换。这也正是「环境工程」里「工程」二字的意思——它不是一次性的造题活动,是一份持续的运维工作。

第三,「环境」不是题库,是一台能开机的机器。 语文考试的判分只要对答案;但让模型改代码,你得给它一整个能跑起来的项目:代码得停在出 bug 的那一刻、依赖得装好、测试得跑得动、跑完的日志还得有人写程序去读懂。所以造一道这样的题,成本和「往表格里加一行」完全不是一个量级——这是这一整套东西为什么这么费劲的根源。

一句能讲给同事听的话

训模型的题,只有「他不全会也不全不会」的那些才算钱;而模型每变强一点,原来那批好题就自动作废一批——所以环境工程是个持续换题的活,不是一次性造库的活。

顺着这个比方,全文其它部分也能对上号:论文里的「动态采样」就是开考前先把全会和全不会的题挑出去,只发有分歧的那些;「rollout 吞吐」的那一堆工程,是因为给模型出一道代码题要真开一台机器,慢在开机和跑测试上,不慢在模型思考上;而 flaky 测试(同样的答案有时判对有时判错)比出错题更糟——出废题只是浪费,判错分是在教错东西

参考来源

arXiv 论文(编号与标题已逐个回查核实)

一手材料

  • SWE-bench Verified 数据集卡的 features 列表(HuggingFace)——第一节的字段清单。
  • GEM 仓库 README——reset() / step() 的接口原文。
  • Prime Intellect 社区环境仓库 README——环境作为可安装包分发。

本文所有 arXiv 编号与标题都通过 arXiv API 逐个回查过;正文里每个数字的档位(我亲手算 / 论文报告值未复现 / 二手未核一手)都在它出现的那句话里标了,不在这里重复。

本站相关