Prime Intellect 开源多智能体 RL 训练框架:从「训一个 Agent」扩到「训一群 Agent 互相打交道」
- 训 Agent 长期卡在两件事上:写死的测试把对的答案判成 0 分,静态题库几轮就被模型吃透。
- 这次的解法是让 Agent 去治 Agent,评委进沙箱把代码真跑一遍再下判决,出题的自己把难度往「刚好卡住一半人」上靠。
- 四个现成环境全部开源,但光是「分数该跟谁比」这一个问题,就逼出了两个新算法。
从训一个 Agent 到训一群 Agent
用强化学习训一个 Agent,长期是这么个场面:一个模型对着一份固定的题库刷,答完由一套写死的测试打分。三件事跟着卡住,测试断言的常是某个实现细节,正确的解法没按它期待的方式写就拿 0 分;题库是静态的,模型几轮就吃透;全程没有第二方,学不会跟人来回拉锯。
Prime Intellect 今天发的东西就是冲这几件事来的:训练栈从「训一个 Agent」扩到「训一群 Agent 互相打交道」,你可以编排它们之间的任意交互、指定哪几个角色参与学习,再把功劳分配到整场交互上。代码落在 verifiers v0.3.0 和 prime-rl v0.8.0,8 月 7 日发布。
有件事得先分清。多 Agent 应用编排框架关心的是让几个 Agent 配合着把活干完;这套东西关心的是活干完之后,这一场里谁该学、每个角色的功劳怎么算。后面会看到,光「分数该跟谁比」这一个问题,就逼出了两个新算法。
新增的两个类:Agent 和 Env
verifiers 之前那版已经把跑一次单智能体要用的零件拆好了:一份 Taskset(要干的活)、一个 Harness模型跑在里面的那个程序,决定它能调什么工具、怎么读结果、怎么一步步往下走。模型是发动机,harness 是整台车,同一台发动机装进赛车和装进货车,能干的活完全不一样。(驱动模型的那个程序)、一个 Runtime(这个程序在哪台机器上执行)。这次做的事很朴素:给这三样找了个家,叫 Agent。
给它一个任务,还你一条 Trace一个 Agent 跑完一趟的完整记录:说了什么话、调了什么工具、拿了多少分、烧了多少 token。相当于每辆车自己的行车记录仪;一场里所有 Agent 的记录合订本,叫 Episode。(轨迹),这一趟说了什么话、调了什么工具、拿了多少分、烧了多少 token,全在里面。至于起沙箱、装工具服务、超时重试这些脏活,都被这一个动作盖住了。所以任何骨架、任何模型、任何运行环境都能任意配一遍,然后被一段普通代码指挥。
Env 是多智能体的家,签名同样一句话说完:Env.run(task, agents)。给你一个初始任务、一组已经备好的 Agent,剩下的控制流你自己编。每个 Agent 跑完,它的轨迹自动汇进一个 Episode,也就是这一场的全部记录。
Env.run 就是普通的 async Python:要顺序跑就 await 两次,要并发跑就开个 TaskGroup,要一来一回就写个 while 循环。没有 DSL,没有图编排,没有状态机配置文件。原来的单智能体在新框架里塌缩成了一行,这层抽象没往上加任何东西。
class SingleAgentEnv(Env[SingleAgentEnvConfig]):
async def run(self, task: Task, agents: Agents) -> None:
await agents.agent.run(task)
还有个顺手的设计:Agent 的名字是从配置字段名自动来的。配置类里写了 solver 和 judge 两个字段,代码里就有 agents.solver 和 agents.judge 可用,不用另外注册。
让 Agent 当评委去推翻测试
先说这件事有多常见。软件工程的训练任务里,对错是由测试用例判的,而测试用例经常在断言某个具体的实现细节,变量叫什么名字、函数返回的是列表还是生成器。一个把活干得漂亮的解法,只因为没按测试期待的方式写,照样拿 0 分。这个信号喂进训练,模型学到的本事就跑偏了:它练的是揣摩测试要看什么,不是把活干对。
换个大模型来打分行不行?一次调用就想给出准确判决,不现实:它看不到代码库、跑不了测试、验不了任何东西,只能对着一段文字猜。
评委做成 Agent 就不一样了。它有自己的沙箱,能翻代码、能看哪个测试挂在哪一行、能自己动手跑一遍,然后可以推翻那个死板测试的结论。
class AgenticJudgeEnv(vf.Env[AgenticJudgeEnvConfig]):
async def run(self, task, agents) -> None:
solution = await agents.solver.run(task)
if not solution.ok:
raise
await agents.judge.run(JudgeTask.from_trace(solution))
评委拿到的提示词写得很硬:
从轨迹里重建这个 Agent 做了什么,在你自己的沙箱里用真实执行去检验,凡是你能自己验的结果,绝不轻信轨迹里的说法。
verifiers 仓库里评委的评分提示词
判决怎么变成分:评委按事先定好的几条标准逐条打,写成 JSON 落到沙箱里的一个固定文件;框架严格校验,把每条判决记成解题人轨迹上的一个指标,再加权平均成一个叫 judge 的奖励。默认只认评委的分,任务集自己那套奖励权重是 0,想让两者并存,把权重调到 1 就行。
三处不在博客里的设计
评委在哪个箱子里干活,是选出来的。共享版让评委直接进解题人用过的那个沙箱,工作区原样保留,改过什么都在;隔离版给评委一个全新的箱子,只把解题人正式交上来的产物还原进去。选哪个环境 id 就等于选了这道安全边界,而不是留一个可能跟实际语义打架的开关。
评委的骨架必须能执行代码。这件事在构造环境的时候就检查,挂一个纯聊天的无工具骨架直接报错,一个不需要执行就能下的判决,那是插件式打分器,不叫 Agent 评委。
开工前先把地扫干净。解题人先用过这个箱子,所以评委动手之前,框架会把判决文件和轨迹文件先删掉。防的是两件事:解题人提前埋一份判决冒充评委的结果;或者在上传路径上放个符号链接,把评委的写操作重定向到它自己挑的文件上。
还有一条贯穿始终:评委的 token 永远不进训练数据。它是用来度量的尺子,尺子自己不该跟着被优化。
出题的 Agent 和做题的 Agent
强化学习要有用的学习信号,题目难度得贴着模型当前的水平:太简单没信息,太难也没信息。静态数据集给不了这个,它不会因为模型变强就自己变难。
做法是让模型给自己出题。一个 Agent 扮出题人,拿一个种子话题去构造新题;一组 Agent 扮解题人,各自去解。解题人答对拿分,这个好理解。出题人的分不看题目本身漂不漂亮,只看这道题对解题人群体做了什么。
全做对,说明太简单;全做错,说明太难,或者题目本身有毛病(比如出题人自己算的答案就是错的)。一半人做出来的时候,这道题给训练带来的信息最多,这个量叫 learnability(学习价值)。写成公式是 4p(1−p),p 是解出率,p=0.5 时正好等于 1。
这条曲线就是自动课程的全部秘密:模型变强,它出的题也跟着变难,不用人在旁边调难度。思路来自 Absolute Zero(Zhao 等人,2025)。
async def run(self, task: vf.Task, agents: vf.Agents) -> None:
proposed = await agents.proposer.run(task)
solve_task = SolveTask.from_trace(proposed)
async with asyncio.TaskGroup() as tg:
for _ in range(self.config.n):
tg.create_task(agents.solver.run(solve_task))
仓库里的实现比这段示意具体不少。种子话题有 6 个:比例混合、数论、组合、几何、概率、模算术。出题人被要求先写代码把数字构造出来、端到端验证一遍答案,最后吐一行 JSON,答案必须是一个整数,不给它蒙混的余地。默认派 4 个解题人去解同一道题。
两个座位可以配得完全不一样,这点很实用:
| 座位 | 骨架 | 跑在哪 | 为什么这么配 |
|---|---|---|---|
| 出题人 | codex | 真沙箱 | 它要写代码构造题目、验证答案,非跑不可 |
| 解题人 ×4 | null(无工具) | 不需要 | 只做题不动手,一道题只花模型调用的钱 |
哪边参与学习也是一个开关:可以两边都训,也可以只训出题人,或者只训解题人。
两个模型打扑克,对手是自己
Kuhn Poker 是博弈论教科书里的玩具模型,小到可以一句话说完:总共三张牌 J、Q、K(K 最大),两个人各下 1 个筹码的底注、各摸一张暗牌,先手选「过牌」还是「下注 1 个筹码」,一两个动作牌局就结束,输赢 ±1 或 ±2 个筹码。
选它就因为它小。小到能反复跑几万手看策略往哪儿收敛,而不是把预算烧在牌局本身的复杂度上。
环境管三样东西:谁手里是什么暗牌、当前公开的局面、这一步有哪些合法动作,全在主机端裁判。模型只要在回复里带一个方括号动作,比如 [check];带了两个或者带错,重问一次,再不行判它弃权。任务集是无限的,每手一个随机种子,想跑多少手跑多少手。
两个座位默认绑同一个模型,这就是自我对弈:策略变强,对手也跟着一起变强,课程自己往上爬,不用另外养一个对手服务。
功劳怎么分给不同角色
前面两个自我对弈的例子,都逼出了同一个问题:这次拿的分,该跟什么比。
给一次表现打分,得跟「平常水平」比才知道是好是坏,这个平常水平叫基线(baseline)。期末考 80 分是好是坏,要看班级平均分,基线就是那个平均分。GRPO 的做法正是这样:同一道题跑一组答案,谁比这组的平均分高就加强谁。
但如果全班有一半人考的是另一张卷子,平均分就没意义了。多角色训练遇到的就是这件事,而且是以两种不同的方式。
一个人的失败,不该算在另一个人头上
出题-解题这一场里,解题人的成绩只能跟「同一道题的其他尝试」比。跨题去比,等于拿一道难题的失败去惩罚一个正常发挥的解题人。出题人的成绩也只能跟「同一个种子话题出的其他题目」比。经典 GRPO 表达不了这个层级,它把一组 rollout 拉平算个平均值,不同难度的题和不同角色全混在一起。
Hierarchical GRPO 做的事就是分组:按(角色,这一场)分成若干组,每组各自算自己的基线。代码短得不像话,分组,组内减去组均值,就这些。
零和游戏里,平均分永远是 0
扑克那边是另一种失灵。零和游戏里,一组对局的奖励平均值恒等于约 0:一边赢多少另一边就输多少,不管策略是好是坏都一样。拿它当基线,等于没有基线。
更麻烦的是两个座位的奖励尺度是相反的。混在一起算,先手位那点结构性优势会被读成这个角色的永久功劳,它只是坐了个好位置,却一直在领赏。
RAE 的解法是给每个角色维护一条自己的移动平均线,滚动记住「这个角色平时大概能拿多少分」,功劳就等于这次拿了多少减去自己的平常水平。单智能体环境用它会自动退化成带移动平均基线的 REINFORCE。
两个算法都在 prime-rl 里,各自几十行。RAE 来自 SPIRAL 那篇论文(arXiv:2506.24119)。
把用户也做成一个 Agent
助手类的任务有个共同特点:没法用「一个提示词加一个回复」表示。真实的人手里有你看不到的背景,信息是一点点透露的,会对你的回答做反应,而且是他决定什么时候算办完了。
所以把用户也做成一个 Agent。一场就是用户和助手一来一回的对话,两边的轨迹都留在这一场的记录里。模拟用户默认冻结不训练,跟评委一个道理,它是环境的一部分;助手的轨迹按原任务打分,拿去训练。
信息是这么分配的。任务集里的那一行数据,被当成用户那一侧的世界来读:它的提示词变成用户系统提示里的「你的处境」,而助手拿到的是同一个任务、提示词被清空。所以助手一开始并不知道要干什么,只能靠对话摸出来。
用户的人设里写着几条规矩,用自己的话开口,说话短,保持人设,活是助手干的(任何工具调用、答案、格式都得让助手来做,不能自己代劳),细节要问了才说。目标达成了、或者确信助手办不到,就回一个结束标记收尾。默认最多 8 轮,防止对话跑飞。
async with (
agents.user.interaction(user_task) as user,
agents.assistant.interaction(assistant_task) as assistant,
):
ask = await user.turn("Hello! How can I help you today?")
while True:
answer = await assistant.turn(ask.last_reply)
if answer.terminated:
break
ask = await user.turn(answer.last_reply)
if ask.terminated:
break
这样一来,不同的用户群体、人设、隐藏目标、交互策略,都能插进同一个接口,助手那边也能单独配。它跟前面三个环境是同一套零件的不同拼法,不是另开一条评测管线。
现在能拿到什么
仓库里现成的骨架有 13 个,Claude Code、Codex、Kimi Code 这些真实的编程 Agent 都在里面,直接拿来给模型套上就能用。
运行环境 4 种:本地进程、Docker、Modal,还有他们自家的云沙箱。除了上面拆的四个环境,仓库里还带了一个 BestOfNEnv,同一个任务跑 n 次独立尝试,标出最好的那次、以及有没有任何一次过了阈值,做拒绝采样和 pass@k 评测用得上。
同一个 Agent 抽象,他们自己已经拿去做合成数据生成和筛选的管线了:每条轨迹格式统一、可审计,本身就是数据产物。博客最后那个例子只有十几行,Poolside 的 Laguna-S2.1 模型跑在 pool 骨架里,去 PyPI 上查 verifiers 最新发布的版本号。跟前面训练用的是同一个 Agent。
Prime Intellect 开源多智能体 RL 训练框架:从「训一个 Agent」扩到「训一群 Agent 互相打交道」
新增的核心只有两个类,四个现成环境全在仓库里跑得起来,一页带图讲完。
↓ 一页读完 · 有一张会动的图
强化学习训 Agent,一直卡在两件事上:测试判卷太死,一个正确的解法没按测试期待的写法来,照样判 0 分;题库是死的,模型几轮就能摸透,后面全是浪费。
Prime Intellect 这次把训练框架从练一个 Agent,扩成练一群 Agent 互相打交道。新增的核心只有两个类:Agent 把骨架程序、模型、运行环境打包成一个能反复用的零件,给一个任务、还一条轨迹;Env 是你自己写的控制流,普通异步 Python,单 Agent 场景在新框架里能缩成一行。
第一个例子治的是打分。写死的测试常纠结代码写法这类细节,一个干得漂亮的解法没踩中测试期待的写法,照样拿 0 分,模型练出来的本事是揣摩测试要看什么。
进不了代码库,跑不了测试
只能对着文字猜
能看哪个测试挂在哪一行,自己跑一遍验证
判决可以推翻那条死板的测试
评委开工前,判决文件会先被清空,防止被抢跑动手脚;评委自己的对话记录不参与训练,它是量尺,不该被顺带调教。
第二个例子治的是题荒。有用的学习信号得贴着模型当前的水平,太简单没营养,太难也没营养,静态题库给不了这个。
做法是一个 Agent 出题,另外几个 Agent 解题。出题这边的分数不看题目好不好看,只看解题的那群模型答对了多少:全对说明太简单,全错说明太难或者题目本身有毛病,一半人做对时,这道题给训练带来的信息量最大。
仓库里还能跑另外两种玩法:两个 Agent 对打最简版的扑克练心理博弈,策略和对手一起变强,不用另外养一个对手;把用户也做成 Agent,故意把任务信息藏在对方那一侧,逼助手只能靠问出来。
这几种自我对弈都撞上同一个问题,这次的分数该跟谁比。两个座位的奖励有时候没法放一起比,Prime Intellect 为此写了两个新算法:一个按角色和场次分组各算各的基线,一个给每个角色单独留一条自己的历史水平线。
差一点也判0
跑一遍
全错也没用
价值最高
都能直接拿来用
训练效果数字
这次全换成了另一个 Agent。
