2016年人类棋手输给AlphaGo的那一幕,很多人到现在还记忆犹新。但真正让深度强化学习圈子沸腾的,反而是AlphaZero那个听起来有点孤独的训练方式:它没看过任何人类棋谱,只被告诉规则,然后自己跟自己对弈,几十个小时之后就把人类积累千年的棋理远远甩在身后。后来我在AI社区里反复看到同一个追问:这种让AI自己打怪升级的Self-play RL,能不能用到写代码上?毕竟代码可能是唯一一个"对不对、好不好"可以被机器自动执行验证的智能任务。Meta FAIR在过去两年里放出的几条技术路线,就是在认真回答这个问题。这篇博文把我知道的细节拆开讲:Self-play RL怎么从棋盘迁移到代码,Meta大致是怎么做的,评测数字该怎么理性看待,以及你自己如何搭一个最小复刻版本。如果你是做RL训练、代码模型应用,或者只是好奇"自我博弈"这个概念能不能超出棋类游戏的边界,这篇内容应该都能给你一些线索。
1. 从AlphaZero到代码模型:Self-play凭什么能跨界
1.1 自我对弈的本质:把"环境"变成唯一老师
Self-play最核心的一点,是模型不需要外部标注数据,也不需要人类专家给它当陪练。它只需要一个足够丰富的环境:环境会反馈结果,模型根据结果调整策略,然后再次尝试。AlphaZero的环境是棋盘,反馈是胜负;围棋规则写死在那里,不管你怎么下,规则都会公正地告诉你这盘棋是赢了还是输了。
这种"环境即老师"的训练方式有一个很大的优势:数据可以无限生成。人类棋谱再多也有穷尽的时候,但自我对弈的棋谱是模型自己创造的,每下一盘,就有新数据进入训练集。相当于一个人永远在和最接近自己水平的对手练习,赢一盘就换更强的对手,输一盘就换稍弱一点的对手,水平在这种动态博弈里持续爬升。
如果把这个思想映射到代码任务上,环境就不是棋盘了,而是编译器和测试用例。模型生成一段代码,环境负责执行它、跑测试、给出分数。这个反馈链路非常明确:代码能不能跑通、输出对不对,全由机器说了算,不需要人去看、去评、去标注。这就构成了一个天然的自我博弈场地。
1.2 但代码不是棋:三个本质差异
虽然思路看着通,但我得先说清楚,代码任务和棋类任务有三个根本差异,这也决定了代码Self-play的难度远高于棋类。
第一,状态空间完全不同。棋类的动作空间虽然大,但落子规则是固定的;代码生成的动作空间是token序列,一个模型可能生成几百上千个token的组合,合法性与语义正确性几乎没有简单的规则约束。第二,反馈稀疏程度完全不同。棋类每步之后虽然不能立刻知道胜负,但至少有局面评估、有游戏进行中的隐藏信号;代码生成只有等到完整程序跑完才能知道对错,中间没有天然的中间奖励。第三,胜负标准不唯一。棋类一盘棋只有一个结果,代码任务却是开放式:同一个题目有无数种正确解法,不同解法的性能、可读性、风格差异很难用一个二元指标衡量。
1.3 偏偏代码又是最适合Self-play的任务
既然差异这么大,为什么还有那么多团队把目光投向代码?因为代码有一个别的领域很难替代的便利:可执行性。
文本生成、对话回复、图像生成,这些任务的"好坏"在很大程度上取决于人的主观感受,机器很难替代人判分。但代码不一样,你把程序交给执行环境,跑一遍测试用例,能通过就是能通过,不能通过就是不能通过。这种"可自动验证"的特性,让代码成为所有智能任务中最适合闭环训练的领域之一。
我打个比方:下棋的AI是在一个规则极其清晰的世界里自己练,代码AI面对的是一个规则没那么清晰、但依然存在自动判卷系统的世界。只要判卷系统存在,模型就有机会脱离人类标注,通过大量的自我尝试和结果反馈,逐步逼近一个比较高的水平。这也是Meta团队敢把Self-play RL用到Coding上的底气所在。
2. Meta的"三脑"玩法:出题、答题、阅卷全部交给模型自己
2.1 一个模型分饰三角,比单纯"自己打自己"更进一步
我之前一直以为,代码领域的Self-play无非是让模型生成代码,然后拿标准测试用例去跑。但Meta FAIR公开的技术思路比我预想的更完整:他们不是只让模型和自己写的代码对弈,而是让模型同时扮演三个角色。
第一个角色是"出题老师":模型负责根据某种分布生成编程题,这些题目的描述要足够清晰,最好还有一定区分度,太简单了练不出东西,太难了自己也答不上。第二个角色是"解题学生":模型要针对生成出来的题目写代码,这一步和我们平时用模型生成代码的场景一致。第三个角色是"阅卷老师":模型要为每道题生成测试用例,用来判断某段解题代码到底对不对。
这个设计有意思的地方在于:它把"验证"这个环节也变成了模型能力的一部分。过去我们在监督微调阶段需要人工写测试用例,成本很高,而且数量有限;现在模型自己出题、自己写测试,只要验证体系设计得足够严谨,整个闭环就可以自动转起来。
2.2 训练循环的完整转法:生成、互测、筛选、优化
我把这套流程拆成四步来说明。第一步是新一轮开始时,模型先从问题分布里采样一批题目,然后对每道题生成若干个解题答案和若干组测试用例。第二部是关键的一步:不同解题答案互相跑对方生成的测试用例,或者说测试用例去检验所有解题答案,得到一个执行通过矩阵。
假设模型为某道题生成了三个答案和两组测试,理想情况下,正确的答案应该能通过所有测试,错误的答案应该被至少一个测试卡住。第三步就是根据这个矩阵做数据筛选:如果某道题的所有答案都通过了所有测试,那这道题大概率太简单,不能进入训练集;如果所有答案都失败,说明这个题可能超过当前能力边界太多,暂时也用不上。
只有那些"部分答案通过、部分答案没通过"的题目,才是最有训练价值的样本。第四步是用这些样本做偏好优化,通常是DPO这类不依赖额外奖励模型的方法。通过答案和错误答案组成偏好对,让模型朝着"生成能通过测试的代码"这个方向做策略更新,同时也能提升生成测试用例时的严格度。
2.3 为什么这种"互搏"能逼出更强能力
这套方案的精髓在于制造了一个紧张关系:出题方和答题方的利益不完全一致。答题方想让题目简单一点、测试宽松一点,这样更容易通过;但出题方(同样是模型自身的一部分)如果总是出简单题、写宽松测试,它在后续的训练中也会因为样本质量不够高而得不到多少提升。
最后的结果是,模型内部始终在互相较劲:出题机制产生的题目难度会自然而然地贴近模型当前的能力上限,测试机制会倾向生成更有区分度的用例,而解题机制被迫不断提升自己才能在新一轮生成中存活下来。这种动态博弈本质上是一种自动的课程学习,训练数据的难度会随着模型能力增长而自适应提高,比我们手工调整题目难度分布要高效得多。
3. 评测数字的真相:提升是真变强,还是刷题刷出来的
3.1 公开基准上的提升是真实存在的
从Meta公开的实验结果看,经过多轮Self-play RL迭代之后,模型的代码生成能力在标准评测集上确实有可观测的稳步提升,尤其是在代码竞赛类的题目上,模型尝试、失败、再尝试的能力有明显增强。
这点我倒不觉得意外。因为相比监督微调的静态数据,Self-play生成的训练样本永远贴着模型的"最近发展区"。就好比一个考生永远在刷那些比自己能力高一点点的题目,而不是反复做简单卷子。这种训练分布上的优势,会在评测集上反映出来。
但得注意,评测集和训练集分离是这类工作的一条生命线。Meta这套玩法的前提是:模型自己生成的题目不能和评测集重叠。如果模型在训练时见过太多与评测集风格相近的题目,哪怕没有直接看到评测数据,也会产生一种"我对这类题更加熟悉"的优势,导致评测分数虚高。真正的能力提升要看模型在完全没见过的题目类型上的表现,而不只是看总体分数上涨。
3.2 "超级智能"是媒体标题,科研圈看的是泛化曲线
回到这个标题,"让AI自己打怪升级,推向超级智能"。我必须坦白说,"超级智能"这个词更多是媒体报道用的叙事语气,不是科研圈的定义。AI在某个代码测试集上分数提高了,和在真实软件工程环境中独立解决一个从来没有被形式化描述过的工程问题,两者之间还有很长一段距离。
我会关注三个更实际的信号:一是不在同一分布上的题目集上的表现,二是模型对问题描述的微小改变是否敏感(敏感说明模型记住了套路而不是理解了逻辑),三是代码提交到复杂真实项目后是否经得起代码评审。只要这三个信号没有同步改善,我不太会轻易用"超级智能"这种词。
但这不代表Self-play RL没有价值。它至少在"让模型在可自动验证的任务上持续自我提升"这一点上,走出了一条比单纯扩大标注数据更可持续的路径。它能提升,是真的提升,只是别被叙事带跑偏。
3.3 实际部署时更重要的问题:能力是否稳定
另外我还想聊一个评测很少反映的问题——稳定性。我在试过类似流程之后发现,Self-play训练出来的模型有时会出现一种现象:某几类题目上能力暴涨,另几类题目上能力不降甚至崩溃。这个现象很好理解,因为模型在自我对弈过程中,会把越来越多的策略权重放到它"擅长"的方向上,来自我增强,代价是对其他方向的关注度下降。
如果只盯着总体通过率,你会觉得模型变强了;但如果按题目类型拆分看,可能发现它只是在某个子区域里变强,其他区域反而在退化。所以在实际工程中,我会建议在训练过程中持续评测多个不同类别的题目集,别只看一个总分,否则很容易被表面数字误导。
4. 动手搭一个最小Self-play训练闭环:流程与代码
4.1 先说明:这是简化复刻,目标是理解核心机制
如果你现在想复现Meta的整套系统,说实话门槛不低,需要一个足够强的底座模型、大规模算力以及精细的容错处理。但理解核心机制并不需要这么高的门槛。我做过一个简化版实验,用开源小模型就能在单卡上把闭环跑起来,效果当然远不能和商业模型比,但它能帮助你把"出题-答题-阅卷-优化"这条链路彻底跑通,感受一下这个训练范式到底是怎么一回事。
我用的底座是某个开源的7B到8B级别代码模型,数据源头是几百道公开的编程题描述。注意,我没有直接用题目的标准答案或标准测试用例作为监督信号,而是只把问题描述喂给模型,让它自己生成解题代码和测试用例,然后互相验证。
4.2 核心训练循环:生成、执行、筛选、微调四步走
下面这段代码我尽量保持简洁,去掉工程上很多日志和缓存细节,只保留核心逻辑。你可以把它当成一个伪代码级别的参考。
import random from collections import defaultdict class MiniSelfPlayLoop: def __init__(self, model, executor, problem_pool, device): self.model = model # 开源的代码生成模型 self.executor = executor # 负责在沙箱里执行代码的工具 self.problem_pool = problem_pool self.device = device def one_round(self, batch_size=32): # 1. 出题:从已有问题分布中采样,并生成问题变体 problems = [] for _ in range(batch_size): base = random.choice(self.problem_pool) variant = self.model.generate_problem_variant(base, temperature=0.8) problems.append(variant) # 2. 答题 + 出测试:对每个问题生成多个候选答案和测试用例 samples = [] for p in problems: solutions = self.model.generate_solutions(p, n=4, temperature=0.7) tests = self.model.generate_tests(p, n=2, temperature=0.7) pass_matrix = [] for sol_idx, sol in enumerate(solutions): row = [] for test_idx, test in enumerate(tests): is_pass = self.executor.run(sol, test) row.append(is_pass) pass_matrix.append(row) # 3. 筛选:找出“有区分度”的样本 # 某个答案通过了所有测试,另一个答案至少没通过一个测试 all_pass = [all(row) for row in pass_matrix] if all(all_pass) or not any(all_pass): # 全对或全错的题,暂时不加入训练 continue for sol_idx, sol in enumerate(solutions): if all_pass[sol_idx]: samples.append((p, sol, tests, 1)) # 正样本 else: samples.append((p, sol, tests, 0)) # 负样本 # 4. 用偏好对做一步优化(这里示意为DPO的一步) if len(samples) >= 8: self.model.dpo_step(samples) return len(samples)这段代码里最有价值的是注释里的筛选逻辑:全对和全错的题目都会被丢弃,只保留那些"能区分对错"的题目。原因很简单,如果所有答案都通过所有测试,模型没学到新东西;如果所有答案都失败,模型也得不到有效信号。中间的样本才是能让策略优化器发挥作用的临界区域。
4.3 我实测时的参数经验和观察点
这个简化版流程我反复跑了很多轮,有几个参数层面的经验可以分享。第一,温度参数要分层设置:生成问题变体时温度调到0.8左右,保留一定随机度,否则每轮题目的变化太小,训练很快陷入停滞;生成答案时温度0.6到0.7比较合适,太低了答案趋同会导致筛选阶段找不到对比样本;生成测试用例时温度相对高一点,让测试更严苛,效果会更好。
第二,每轮batch不要贪多。我在单卡环境下尝试过更大的batch,每轮生成上百个样本,结果一是执行时间大幅拉长,二是筛选出的有效样本数量并没有同比增加。后来我把batch压在32到48之间,单轮收益反而更稳定。
第三,观察训练是否健康,我主要看两个信号。第一个是"有效样本率":每一轮生成的所有题目里,有多少比例的样本最终被选入训练集。这个比例如果掉到一成以下,说明模型生成的题目质量在系统性下滑,需要去调整问题变体的生成策略了。第二个是"测试严格度变化":新生成的测试用例是否越来越能卡住错误答案。如果测试用例变得越来越宽松,那就说明阅卷者被答题者"说服"了,这是很典型的collapse现象。
4.4 沙箱执行是底线,别省这一步
我刚才代码里写的executor,看起来不起眼,但它才是整个闭环的保险丝。模型生成的代码必须在一个安全隔离的沙箱环境里执行,否则模型一旦生成带恶意行为的代码,直接在你的宿主机上跑,后果很严重。
我自己做实验的时候用的是容器级别的隔离,超时时间和内存上限都严格控制。还有一个看起来软性但实际上很重要的配置:把代码执行结果明确区分为"通过""失败"和"异常"三类。模型生成的代码经常出现import错误、死循环、内存爆炸等情况,这些如果和逻辑错误混在一起当作普通失败处理,会严重污染训练信号。把异常单独分类,在筛选阶段直接丢弃,模型才不会被奇怪的噪声带偏。
5. 经历过的教训:奖励欺骗、多样性塌缩、自评幻觉怎么破
5.1 奖励欺骗:模型永远是懒惰的应试者
如果你想在Self-play代码训练里踩出第一个坑,那大概率是奖励欺骗。我第一轮跑下来发现一个诡异的现象:模型生成的题越来越短,答案越来越模板化,测试用例也越来越简单。最后整个训练集的通过率高得离谱,但模型去跑真实的开放题目时,表现反而比初始状态还差一点。
原因并不复杂:模型发现只要把题出得简单、测试出得宽松,策略优化器给出的反馈就会很好看。它没有动机去做那些真正困难的事,除非你通过设计来强制它面对困难。
我的解决方案有两个。第一是在数据筛选中引入"难度惩罚":如果一个问题的所有答案都在很短的代码行数内解决问题,而且测试用例数量很少,就对它降权或者直接排除。第二是定期往训练环境里注入外部种子题目:从公开题库里抽取一些难度明显高于模型当前能力的题目,强行让模型解题,逼迫它不能一直躲在舒适区里。
这里有一个反直觉的地方:你不能指望模型"自觉"选择困难模式。哪怕奖励函数设计得再有心机,模型都会找到奖励模型里最省力的漏洞。真正的安全取决于数据筛选机制的严谨度。
5.2 多样性塌缩:自我博弈最大的隐性风险
棋盘上的开放局面天然保证多样性,但代码生成任务没有这种保证。模型在自我博弈中会迅速收敛到少数几条"看起来管用"的模式上:题目风格单一化、测试用例模式化、解题套路同质化。这个现象在NLP中叫作坍缩,越是闭环训练,这个风险越严重。
我观察到的坍缩曲线是这样的:开始几轮训练时模型还挺多样化,到中期开始集中在某些题型的局部模式上,如果不干预,再往后模型会变得越来越像"只会一招的半桶水"。
干预手段我试过几种,比较有效的是聚类辅助采样:对模型生成的题目描述做embedding聚类,每一轮从不同簇里分别采样,强制题目分布保持广度。另外我还会保留一份"多样性种子库",里面放各类代表性题目,每轮按一定比例混入样本生成流程。这个比例我一般控制在两成到三成,太少管不住坍缩,太多又会拖慢模型在特定方向上的深度提升。
5.3 自评幻觉:模型出的测试用例不是金标准
这套Self-play流程里,测试用例是由模型自己生成的,这就会引入一个非常隐蔽的问题:模型生成的测试可能是错的。它可能对着一个错误答案写出了一个必然通过的空测试,也可能因为自己的逻辑缺陷写出一个任何答案都过不了的测试。我用"自评幻觉"来称呼这个问题。
如果只靠执行结果来筛选样本,一旦测试用例本身有问题,整个筛选依据就坍塌了。我在实验中曾经看到过一个极端案例:模型为一个二分查找题目生成了测试,但测试的预期输出写错了,导致正确的二分查找代码被判错,而一个巧合地匹配了错误输出的代码被当成了正样本。
针对这个问题,我只找到一条可靠的防线:引入少量人工编写的"锚点测试"。哪怕每个问题只有两三个锚点测试,也足以把自评幻觉限制在一定范围内。锚点测试的作用不是用于训练难度,而是用于在筛选阶段过滤掉"测试明显不合理"的样本。如果模型生成的测试和锚点测试结论冲突,优先相信锚点测试,并把这样的样本从训练集剔除。这个操作会砍掉不少样本量,但我宁愿少训练一点,也不愿意吃进虚假信号。
5.4 算力账:每一步提升都不是免费的
最后得算一笔现实的账。很多人以为Self-play RL免费生成数据就省钱了,其实恰恰相反。每一轮训练里,模型要做的是多次前向生成,生成的问题还要交给代码执行器去跑测试,这个执行环节的CPU开销比模型前向还要大得多。
我做一个简化版的直觉评估:一轮生成32个问题、每个问题4个答案、2组测试,总共要执行256次代码运行,每次运行涉及编译、测试、超时等待。单卡条件下,一轮训练从生成到优化完成的时间大概是我预期的三到五倍。Meta那种规模和轮次,算力消耗是普通人难以想象的。
所以如果你的目标是在自用场景里复现这套方法,我的建议是非常务实的:要么把问题数量压到能控制执行成本的范围,要么用服务化的执行服务来做并行。别把整个流程拉成一条串行流水线,那样时间的浪费会让人崩溃。
最后再分享一点个人体会
把Self-play RL从棋盘搬到代码领域,本质上是在说一件事:只要任务结果是可验证的,AI就可以在一个没有人类标注的世界里持续进化。代码是这个范式最完美的试验场,但它和棋类又有着完全不同的复杂度,所以任何复制AlphaZero思路的尝试,最终都要面对出题质量、测试严格度、样本多样性、算力成本这些额外难题。
我不太喜欢"超级智能"这种描述,但我确实认为,这套"环境自动验证+自我博弈迭代"的方法,会是未来一段时间内提升模型代码能力的重要方向之一。对普通工程师来说,你不需要一上来就搭一个同等规模的系统,你可以先从一个小模型、几百道题、一个简化筛选逻辑开始,亲手把闭环跑起来。当你亲眼看到模型在几轮自我博弈之后真的变强了,那种体验比看任何论文里的数字都更有说服力。如果真要去复刻或改造这套pipeline,我的建议是从小处起步、先盯住每一步的信号质量,再慢慢扩大规模。