写这篇时,我做了一个很小的排序实验:候选答案越多,系统越容易宣布成功;继续增加候选,真正选对的概率却开始下降。打印出来的两列数字,一列接近满分,另一列掉回了两成。
这是人为设定候选分布的机制实验,没有调用大模型。它让我更想讲清楚一个问题:大模型产生了正确答案,系统就一定能把它交到用户手里吗?
我的判断是,可执行、可验证、难度可调的环境,会成为 Agent 训练的重要资产。最近的论文正在推进这件事。但读这些研究时,我最关心的先是验收规则:如果它漏掉了需求,多做实验、多生成候选,可能会让漏洞更容易胜出。
先把几个词放到同一张桌上
| 术语 | 本文里是什么意思 |
|---|---|
| 环境 | Agent 实际操作的世界,例如有文件、命令和测试的代码仓库 |
| 验证器(verifier) | 检查结果是否满足约定的程序;像阅卷器,能力取决于评分规则 |
| 可验证奖励(RLVR 中的 VR) | 把能够自动检查的结果转成训练分数,例如测试通过给奖励 |
| 强化学习(RL) | 根据尝试得到的奖励调整模型,让某些行为以后更容易出现 |
| 候选选择 | 生成多个结果,再按验证结果和偏好挑一个交付;本身不必更新模型 |
| 奖励投机(reward hacking) | 得分变高,实际目标却没有完成;不要求模型有欺骗意图 |
可验证奖励与强化学习的背景,可以接着读《目标函数即命运》和DeepSeek-R1 学习笔记。这里集中解决生成、验证、选择三者不一致的问题。
把完整系统拆成四个部件,后面的研究就有了落点:
| 部件 | 可以调什么 | 调整后的作用与故障 |
|---|---|---|
| 任务环境 | 初始状态、工具、任务难度 | 增加练习范围;若环境不能重置,前一次尝试会污染后一次 |
| 候选生成 | 尝试数量、策略多样性 | 增加找到好解的机会,也增加碰到验收漏洞的机会 |
| 验证器 | 正确性的判据与覆盖范围 | 筛除坏解;漏掉需求时,错误会拿到通行证 |
| 选择或学习规则 | 如何挑答案、如何使用奖励 | 决定哪些结果被交付或强化;额外偏好可能放大漏洞 |
这个系统需要独立的验收环节,是因为“得到高分”与“完成真实目标”可能分离。 如果两者完全等价,而且检查足够便宜,那么很多额外复核就可以省掉。接下来的实验只挪动其中一个判据,看看差别有多大。
三篇研究,把关注点移向环境
2025 年的 SWE-smith 从 Python 仓库构造执行环境,并合成能让已有测试失败的任务。论文报告了来自 128 个仓库的 5 万个实例。这里的价值是同时提供可操作的任务和检验修复的条件;任务、轨迹与工具也有开源入口。这些是论文报告的规模,不能当成今天的项目实时统计。
2026 年 8 月的 EnvHarness 更进一步:在原环境外加可编程组件,改变起点、交互规则或任务组合,同时保留原来的验证器。其官方仓库把这些组件称为 Setup、Rule、Link。对我而言,值得记住的设计选择是:练习条件可以变化,验收依据被保留下来。
9 月 3 日发布的预印本 Environment Evolution for Terminal Agents 继续研究环境难度:沿场景新颖性、技能稀有性和执行长度演化环境,再按代组织训练。论文也承认,太容易的环境难以持续区分不同轨迹的优劣。这项新结果还需要后续复现,本文只用它说明研究问题正在向哪里移动。
我把它们串起来的方式是:有题做 → 有地方执行并验收 → 有持续合适的题可学。 这是一条研究线索,不能据此断言模型规模、数据质量或训练算法已经不重要。
它也有一个更老的祖先:按学习者能力安排练习的课程学习,以及软件测试里用故障检查测试强度的做法。新变化在于,这些练习和反馈开始直接服务于语言模型的行动与学习。
不过,“保留了验证器”只保证判据没有跟着练习一起漂移,并不自动证明原判据足够完整。下面就给它一个反例。
实验:排序正确,却少了一个数
需求是把 [3, 1, 3, 2] 升序排列,保留所有元素及其出现次数。
我写了三种固定候选,模拟生成器可能产出的结果:
| 候选 | 输出 | 人为设定的单次出现概率 |
|---|---|---|
| 正确实现 | [1, 2, 3, 3] | 20% |
| 有缺陷的捷径:先去重再排序 | [1, 2, 3] | 5% |
| 失败实现:原样返回 | [3, 1, 3, 2] | 75% |
这些概率是为了暴露机制而设定的,不是任何模型的实测分布。每次抽取独立、允许重复,候选数增加时分布不变。
弱验证器只检查相邻元素是否升序。它会放过丢失重复元素的捷径。强验证器再检查输入与输出的元素计数一致,于是只有正确实现能通过。
最后加入一个明确的选择规则:在通过验收的候选里选输出最短的;全部失败就不交付。 这模拟了“通过之后,再偏好简短结果”的二次优化。对当前输入,捷径比正确结果短,所以一旦混入候选池,它就会胜出。
这里没有复杂推理,也没有模型主观上想作弊。一个缺少计数检查的验证器,加上一条局部合理的长度偏好,已经足够。
可复制运行的代码
把下面保存成 verifier_demo.py,运行 python3 verifier_demo.py。只需 Python 3 标准库,无账号、无模型调用费用。
from collections import Counter
from itertools import product
from math import isclose
source = [3, 1, 3, 2]
outputs = {
"correct": sorted(source),
"shortcut": sorted(set(source)),
"failure": source,
}
weights = {"correct": .20, "shortcut": .05, "failure": .75}
def weak(out):
return all(a <= b for a, b in zip(out, out[1:]))
def strong(out):
return weak(out) and Counter(out) == Counter(source)
def select(batch, verifier):
passed = [name for name in batch if verifier(outputs[name])]
return min(passed, key=lambda name: len(outputs[name])) if passed else None
# 穷举候选类型序列,按各序列的概率加权;不是随机抽样。
for n in (1, 2, 4, 8):
totals = [0., 0.]
for batch in product(outputs, repeat=n):
mass = 1.
for name in batch:
mass *= weights[name]
for i, verifier in enumerate((weak, strong)):
totals[i] += mass * (select(batch, verifier) == "correct")
assert isclose(totals[0], .95**n - .75**n, abs_tol=1e-12)
assert isclose(totals[1], 1 - .8**n, abs_tol=1e-12)
print(f"exact n={n} weak={totals[0]:.8f} strong={totals[1]:.8f}")
print("n weak_accept weak_correct strong_correct")
for n in (1, 2, 4, 8, 16, 32):
print(f"{n:2d} {1-.75**n:.2%} {.95**n-.75**n:.2%} {1-.8**n:.2%}")
Counter 统计每个元素出现几次;set 会去重,所以它正是捷径丢数据的位置。product(..., repeat=n) 列出长度为 n 的所有候选类型序列。isclose 的容差只用于允许计算机小数运算的舍入误差。
实际输出
exact n=1 weak=0.20000000 strong=0.20000000
exact n=2 weak=0.34000000 strong=0.36000000
exact n=4 weak=0.49810000 strong=0.59040000
exact n=8 weak=0.56330752 strong=0.83222784
n weak_accept weak_correct strong_correct
1 25.00% 20.00% 20.00%
2 43.75% 34.00% 36.00%
4 68.36% 49.81% 59.04%
8 89.99% 56.33% 83.22%
16 99.00% 43.01% 97.19%
32 99.99% 19.36% 99.92%
三列分别是:弱验证器至少放过一个候选的概率、弱验证器配合最短选择实际交付正确结果的概率、换成强验证器后交付正确结果的概率。所有比例的分母都是全部任务尝试,包含无结果交付的情况。
我对候选数为 1、2、4、8 的情况做了穷举,与公式交叉核对;16、32 的结果由公式计算,没有穷举,更没有训练模型。
如果只看第一列,候选数为 32 的系统几乎完美。看真正交付的结果,它却比候选数为 8 时差了很多。换掉验证器后,生成器和最短选择规则都不动,结果就随候选数增加而改善。
用两次抽取,手算为什么会反转
先补一点最小数学:概率 20% 可以写成小数 0.20。两个独立事件同时发生,概率相乘;至少发生一次,可以用 1 减去“一次也没发生”的概率。代码里的 **n 表示把同一个数连乘 n 次。
弱系统要选对,需要同时满足:没有捷径出现,并且至少出现一个正确候选。
以两次抽取为例:
- 每次不是捷径的概率是 95%,两次都不是,就是
0.95 × 0.95 = 0.9025。 - 这个范围里还包含“两次全是失败”的情况,要减掉
0.75 × 0.75 = 0.5625。 - 所以选对的概率是
0.9025 - 0.5625 = 0.34,也就是 34%。
推广到 n 次,得到 0.95**n - 0.75**n。最初,多抽能补上“没出现正确答案”的缺口;后来,越来越难维持“没有捷径出现”,正确答案就越来越容易被挤掉。
强系统把捷径挡住了,只要出现过正确候选就能选对:两次都没出现的概率是 0.80 × 0.80 = 0.64,因此成功率为 36%。一般形式是 1 - 0.80**n。
崩点很具体:候选池已经找到了好解,选择器却把它扔掉了。 继续只给生成端加预算,修不到这个位置。
不要把反例读成“多试必然更差”
上面的下降依赖两个条件:验证器放过捷径,选择规则又偏爱捷径。若把最短优先换成“从通过者中随机挑一个”,当前分布下,通过者中正确与捷径的比例是 20 比 5,最终选对概率会逐渐趋近 80%,不会出现同样的后期下降。
真实模型的不同尝试也可能高度相关,或者会根据反馈改变策略。这些都不满足当前实验的独立固定分布假设。实验能证明的是一个反例机制,不能提供真实 Agent 的故障发生率。
它同样没有证明强化学习一定走向投机:这里做的是推理阶段的候选筛选,没有权重更新。与训练有关的启发是,如果这些通过结果随后被当成高质量轨迹收集,或者直接拿来给奖励,验收盲点就可能进入下一轮学习。
从这个玩具,带走一项实际检查
我更愿意用三种问题审查一个所谓的“可验证环境”。
先问它能否拒绝一个已知错误。 排序不只要求单调,还要求元素计数不变。文件转换不只要求输出存在,还要检查内容是否完整。先手写一个“看上去像完成了”的坏结果,往往比再增加一轮模型调用更快暴露判据缺口。
再问谁有权改变判据。 练习可以自适应,候选可以自我修改,但不能因为这次总过不了,就悄悄降低通过标准。代码和测试同时由 Agent 修改时,独立验收尤其必要。权限与链路的设计可接着看《面向 Agent 的门禁产品设计》。
最后问成功率测的是哪个环节。 “候选里存在正确答案”“验证器放行了答案”“最终交付正确答案”是三个指标。实验里它们能差得很远,业务看板却很容易只剩一盏绿灯。
对训练环境还要多看一步:任务是否既可解,又能让不同尝试产生有区别的反馈。把题变难到所有候选都失败,并不自然带来更多学习价值;把题变简单到全部通过,也可能失去区分度。环境研究需要同时面对难度与验收质量。
我关心环境工程,就是因为这些判据、状态和练习条件,决定了模型的尝试最终被怎样解释。新论文提供了扩大练习规模的方法;工程团队仍要证明,扩大的不是一套容易自我报喜的规则。
小结
生成更多候选,增加的是找到好解的机会;交付能否变好,还要经过验证和选择。
本实验的故障从计数检查缺失开始,被最短优先放大。强验证器补上这一条后,同一候选分布得到了完全不同的结果。
下一步只需五分钟和本机 Python:运行上面的脚本,再把 select 的返回语句改成 return passed[0] if passed else None,并删除两条公式断言及末尾公式输出表,只看 exact 开头的四行。原公式绑定了“最短优先”,不能继续用来检查新规则。你会亲手看到:验收规则相同,选择方式不同,系统也会交出不同成绩。
参考来源
论文
- SWE-smith: Scaling Data for Software Engineering Agents:从代码仓库构造可执行任务与训练数据。
- EnvHarness: Awakening Static Worlds for Agent Learning:通过外部组件调整环境,保留已有验证器。
- Environment Evolution for Terminal Agents,v1:环境难度的演化方向与训练调度。
工程实践
- SWE-smith 官方仓库。
- EnvHarness 官方仓库。
- 本文排序实验:代码、设定及原始数值输出见正文;它是机制验证,不是模型能力榜单。