
对 coding agent 来说,环境不是背景板。它要么参与 reward,要么参与 rollout;接在哪一层,训练出来的能力就不同。 第2篇把CubeSandbox放进veRL训练链路:接在reward里只是最终评分,接进AgentLoop才会改变轨迹。文章拆清两条路径、response_mask边界和GPU/ Sandbox集群拆分,帮你看懂coding agent RL如何落地
上一篇:腾讯开源 CubeSandbox:当 AI 开始替你动手,Sandbox 就不再是可选项,从 AI Agent 进入办公和研发场景讲起,解释了 CubeSandbox 为什么不是“更快的 Docker”,而更像 AI Agent 的环境执行层。本文继续回答下一个问题:这个环境层怎么进入 veRL 训练?
先给新读者补三个概念:
本文的核心判断是:CubeSandbox 可以通过两条路径进入 veRL。第一条是 reward-only:模型生成最终答案,reward function 创建 sandbox 执行测试。第二条是 AgentLoop:模型在 rollout 中反复操作 sandbox,环境 observation 进入上下文,但 observation token 不能参与 policy loss。这两条路径解决的问题不同,不能混在一起讲。
先看最容易落地的方式:rollout 阶段仍然让模型一次性生成答案,CubeSandbox 只在评分阶段出现。读图时注意,sandbox 不改变模型生成过程,它只负责验证最终输出。

veRL 自定义 Reward 接入 CubeSandbox
这个路径适合 patch generation、代码题、SQL 题、脚本题等任务。模型输出最终代码或 patch;reward function 创建 sandbox,把答案写进去,执行测试,然后返回分数和 metadata。
veRL 文档说明,自定义 reward function 可以放在独立文件里,并通过 custom_reward_function.path和 custom_reward_function.name指定;函数参数包括 data_source、solution_str、ground_truth和 extra_info(docs/preparation/reward_function.rst:55-70)。配置里也有对应字段:reward.custom_reward_function.path和 name(verl/trainer/config/reward/reward.yaml:7-13)。
把 CubeSandbox 接进来时,逻辑可以压成五步:
solution_str
-> create sandbox from template_id
-> write patch or code
-> run tests in sandbox
-> return score + structured metadata
这个方式的优点是侵入小。veRL 不需要先理解“代码环境”是什么,reward function 自己把真实执行包起来。缺点也很清楚:模型在生成最终 patch 前看不到 pytest输出、文件内容和命令反馈,因此训练出来的更像“一次性解题模型”,不是会探索仓库的 coding agent。
如果目标是训练能自己读文件、跑测试、根据错误继续修改的 agent,sandbox 就不能只出现在最后评分。它必须进入 rollout。
下面这张图展示 AgentLoop 接入 sandbox 的路径。读图时注意,环境 observation 会被追加回上下文,下一轮模型生成会看到这些 observation。

veRL Agent Loop 接入 CubeSandbox
veRL 的 AgentLoopOutput不是一段字符串,而是一组 token 级字段:prompt_ids、response_ids、response_mask、logprobs、reward、turn 数和 extra fields。源码注释明确写着,response_ids包含 LLM generated token 和 tool response token;response_mask对模型生成 token 是 1,对工具响应 token 是 0(verl/experimental/agent_loop/agent_loop.py:88-120)。
这就是 AgentLoop 接 sandbox 时最重要的边界。模型生成的 bash/edit/final token 要进入 loss;sandbox 返回的 stdout、stderr、文件片段和测试日志要进入上下文,但不能被当成模型自己生成的 token。generate_sequences()的 docstring 也把 multi-turn 的 token 合同写清楚:responses里既有 LLM generation,也有 observation tokens,response_mask用 1/0 区分(verl/experimental/agent_loop/agent_loop.py:450-470)。
ToolAgentLoop的执行过程也证明了这一点。它在 PENDING -> GENERATING -> PROCESSING_TOOLS -> TERMINATED状态之间循环;工具阶段会并发调用工具,把 tool message 追加到上下文,再把这些 token 的 response_mask追加为 0(verl/experimental/agent_loop/tool_agent_loop.py:118-200、273-374)。
所以,AgentLoop 接 CubeSandbox 时,真正要守住的是这条训练合同:
assistant action tokens -> response_mask = 1
sandbox observation tokens -> response_mask = 0
both enter input_ids / attention_mask / position_ids
only mask=1 tokens participate in actor loss
这也是我们第 16、17 篇已经建立过的边界:环境反馈要进轨迹,但不能污染 policy gradient。
这两种接法经常被混在一起,但它们训练的是不同能力。
路径 | 模型能看到环境反馈吗 | 适合任务 | 主要风险 |
|---|---|---|---|
reward-only | 不能,只在最后被打分 | 一次性 patch、代码题、SQL 输出 | reward 稀疏,模型无法从中间错误恢复 |
AgentLoop | 能,stdout/stderr/文件内容进入上下文 | 多轮 coding agent、SWE-bench agent | 轨迹变长,长尾更明显,mask 和 tokenization 更容易出错 |
reward-only 更像“考试后批改”;AgentLoop 更像“边做题边看反馈”。前者简单稳定,后者更接近真实 coding agent,但系统复杂度也更高。
实际工程里可以组合使用:AgentLoop 负责多轮探索,最终 reward function 仍然在干净 sandbox 中 apply patch 并跑测试,给出最终评分。
veRL 的 actor、critic、reference model、rollout backend 主要吃 GPU;CubeSandbox 主要吃 CPU、内存、磁盘 IO、网络和 KVM 能力。把两类 workload 混在同一批节点上,容易互相干扰。
下面这张图展示更清晰的部署形态。读图时注意,GPU 集群负责生成和训练,sandbox 集群负责环境执行和验证,中间通过任务请求、metadata 和 reward 结果连接。

GPU 训练集群与 CPU/KVM Sandbox 集群拆分
这种拆分让容量规划更明确:
CubeSandbox 的 mini RL 示例里已经出现了 SWE-bench、E2B 兼容、template id、envd 注入镜像、并发运行和 pre-create 这些元素(CubeSandbox/examples/mini-rl-training/README_zh.md:1-3、60-91、198-238)。这说明它已经把批量 agent 任务里的模板准备、并发执行和环境预创建作为工程问题暴露出来,而不只是演示一次命令执行。
对文本任务来说,一条样本可能只有 prompt 和 answer。对 coding agent 来说,一条样本至少还要告诉训练系统:应该从哪个环境启动,仓库在哪里,运行什么测试,允许访问哪些网络,超时时间是多少。
一条简化后的样本可以这样理解:
{
"data_source": "swebench_lite",
"prompt": "issue description and repo instructions",
"ground_truth": "test metadata",
"extra_info": {
"template_id": "swebench-sympy-py311-v1",
"repo_dir": "/testbed",
"test_cmd": "python -m pytest ...",
"timeout": 900,
"max_steps": 30,
"network_policy": "offline-or-allow-pypi"
}
}
这里的 template_id很关键。它不是普通配置,而是“这条任务从什么初始世界开始”的引用。CubeSandbox 的快速开始文档也把控制面和数据面分开:Sandbox.create()负责生命周期,命令和文件操作通过数据面进入 sandbox 内的 envd;基础 SDK 示例包括创建、命令执行、文件读取、暂停恢复、网络访问控制等(CubeSandbox/examples/code-sandbox-quickstart/README_zh.md:9-12、117-128)。
在训练之前,最好把仓库 checkout、依赖安装、envd ready、测试缓存等步骤尽量固化到 template 或 snapshot。训练阶段只做“启动/克隆、执行动作、评分、清理”。这能减少环境抖动,也让 reward 更可复现。
Agent RL 里的 sandbox 不是一个个孤立请求,而是一批并发环境。一个 batch 可能包含多个 prompt,每个 prompt 还可能采样多条 trajectory。环境层如果没有生命周期设计,很容易出现泄漏、超时堆积、日志爆炸和 GPU 空等。
下面这张图展示批量 rollout 的 sandbox 生命周期。读图时重点看 pre-create、acquire、run、release/rollback/kill 这几个阶段。

批量 rollout 的 sandbox 生命周期
工程上建议把这些规则写死在系统里,而不是散落到 reward 脚本里:
template_id分组创建环境,提升模板本地性。finally语义,失败也要释放。CubeSandbox 快照示例展示了 create_snapshot()、clone(n=...)、rollback()和从快照启动新 sandbox 的接口(CubeSandbox/examples/snapshot-rollback-clone/README_zh.md:48-69)。这些能力在训练系统里的价值不是“API 好看”,而是让同一初始环境可以被批量复用和重放。
代码任务如果只返回 0或 1,训练很难诊断。失败可能来自模型,也可能来自环境。
建议至少拆出这些字段:
字段 | 含义 | 是否应该惩罚模型 |
|---|---|---|
patch_apply | patch 是否能应用 | 是,通常可作为格式/动作质量反馈 |
selected_tests | 相关测试通过数量 | 是,任务质量反馈 |
full_tests | 全量目标测试结果 | 是,主 reward |
timeout | 命令或 rollout 超时 | 视情况,可能是模型死循环,也可能是环境过慢 |
sandbox_error | envd 不通、VM crash、创建失败 | 通常不应直接惩罚模型 |
policy_error | 访问未授权网络或 secret | 取决于任务设计,通常要强约束 |
infra_error | CubeAPI、CubeMaster、Redis、网络异常 | 不应写成模型负反馈,应重试或丢样本 |
veRL 的 reward 路径已经支持把 extra info 带回 batch:extract_reward()会从 batch.non_tensor_batch里提取 reward_extra_keys对应字段(verl/trainer/ppo/reward.py:160-167)。AgentLoop 的 _postprocess()也会把 reward_extra_info、turn_scores、tool_rewards等字段放入 non-tensor batch(verl/experimental/agent_loop/agent_loop.py:895-986)。
这就是为什么环境执行层要有结构化 metadata。没有这些字段,训练曲线掉了,你很难判断是模型变差、题目变难、sandbox 变慢,还是网络策略误拦了依赖下载。
把 CubeSandbox 放进 veRL 之后,可以得到一条清晰的系统图:
dataset extra_info
-> template / sandbox lifecycle
-> reward-only or AgentLoop
-> observation / score / metadata
-> DataProto / rm_scores / response_mask
-> advantage and actor update
如果只是做一次性代码评分,先从 reward-only 开始,路径短、风险低。如果要训练真正的 coding agent,就必须进入 AgentLoop,并认真处理 response_mask、上下文长度、工具等待、环境并发和失败归因。
到这里,CubeSandbox 不再只是一个外部项目介绍。它补上的是 train/serve 系统里越来越重要的一层:当模型开始行动,环境本身就成为训练数据流的一部分。