首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >17-ToolCalling和Sandbox环境如何成为reward的一部分

17-ToolCalling和Sandbox环境如何成为reward的一部分

作者头像
anzhsoft
发布2026-07-13 11:12:38
发布2026-07-13 11:12:38
540
举报

环境反馈一旦进入轨迹,工具调用和沙箱执行就不再是外围功能,而是 reward 的来源之一。

上一篇说明了 agent loop 为什么必须坚持 token-in/token-out:多轮轨迹里既有模型生成 token,也有工具 observation token,训练系统必须用 response_mask区分它们。本文继续看这条轨迹为什么会变得更复杂:当模型可以调用工具,环境就不再是训练外部的背景,而会进入 rollout、reward 和 DataProto。

本文的核心判断是:tool calling 让“环境反馈”变成训练样本的一部分,sandbox 则让部分 reward 计算变成受控执行问题。二者不在同一层:tool calling 发生在 rollout 的多轮交互里,sandbox reward 多发生在 reward manager 的评分路径里。把这两层分清,才能看懂 agent RL 里工具响应、工具分数、reward extra info 和最终 actor update 是如何接起来的。

先看环境进入训练系统的位置。读这张图时注意两条路径:上半部分是 rollout 阶段的工具调用,下半部分是 reward 阶段的 sandbox 执行。它们最终都要回到同一个训练 batch。

tool calling 与 sandbox 的系统边界

这张图对应的源码边界是:AgentLoopWorker在初始化时按 rollout 配置加载工具,ToolAgentLoop在多轮状态机里解析并执行工具;reward manager 则在没有自定义 reward function 时,可以根据 reward.sandbox_fusion配置把默认评分函数包装成 sandbox 调用(verl/experimental/agent_loop/agent_loop.py:412-418verl/experimental/agent_loop/tool_agent_loop.py:93-113verl/trainer/ppo/reward.py:124-157)。

1. 工具不是字符串,而是 prompt 合同

很多人第一次看 tool calling,会把它理解成“模型输出一段 JSON,然后程序执行”。这只说对了一半。在训练系统里,工具首先是 prompt 合同:模型能看到哪些工具、每个工具有哪些参数、参数如何被解析、同一条样本是否只允许选择部分工具,都必须在 rollout 前确定。

rollout.multi_turn配置里把这件事拆成两条路径:tool_config_path用于加载继承 BaseTool的有状态工具,function_tool_path用于加载 @function_tool装饰的轻量函数工具;同时还有 max_parallel_callsmax_tool_response_lengthtool_response_truncate_side和 tool parser format 等控制项(verl/trainer/config/rollout/rollout.yaml:164-220)。

下面这张图把工具装配过程画出来。它补的是:工具不是在模型输出后才临时出现,而是在 prompt assembly 阶段就已经进入了 chat template。

工具 schema 如何进入 agent loop

AgentLoopWorker.__init__()会调用 load_all_tools(),把 native tools 和 function tools 合并加载到 worker 内部(verl/experimental/agent_loop/agent_loop.py:412-418)。ToolAgentLoop.__init__()再把工具按名字放进 self.tools,把 OpenAI-compatible schema 放进 self.tool_schemas,并根据 multi_turn.format选择 ToolParserverl/experimental/agent_loop/tool_agent_loop.py:103-113)。在单条样本上,extra_info.tool_selection还可以把全局工具集过滤成该样本可用的工具子集(verl/experimental/agent_loop/tool_agent_loop.py:144-155)。

所以,tool calling 的第一层约束不是执行,而是“模型被允许调用什么”。这个约束和数据集字段、chat template、tool parser 共同决定模型输出是否能被解释成可执行动作。

2. 工具执行有两种合同:有状态生命周期和无状态函数

当模型输出 tool call 后,系统才进入执行层。下面这张图展示 ToolAgentLoop如何从 assistant token 中解析调用、并发执行工具,再把结果变成 tool message。

工具调用执行合同

ToolParser负责从 response token 解码出工具调用。Hermes parser 会查找 <tool_call>...</tool_call>,把里面的 JSON 解析成 FunctionCall(name, arguments);GPT-OSS 和 Qwen3 coder 路径也各自实现了不同格式的 parser(verl/experimental/agent_loop/tool_parser.py:44-113)。

解析之后,ToolAgentLoop._handle_processing_tools_state()会取最多 max_parallel_calls个 tool call,创建异步任务并 asyncio.gather()verl/experimental/agent_loop/tool_agent_loop.py:273-286)。真正执行在 _call_tool():先校验工具名,再把 arguments 当 JSON 解析;如果是 FunctionTool,直接调用函数并归一化返回值;否则走 BaseTool生命周期,依次 create()execute()release()verl/experimental/agent_loop/tool_agent_loop.py:376-451)。

BaseTool的接口说明了为什么工具是“环境”而不是普通函数:它可以为一条 trajectory 创建实例,执行时返回 ToolResponse、step reward 和 metrics,结束后释放实例(verl/tools/base_tool.py:24-93)。相对地,FunctionTool更轻量,只要求函数有可推导 JSON schema 的签名和 docstring,返回值会被归一化成 (ToolResponse, reward, metrics)verl/tools/function_tool.py:44-64verl/tools/function_tool.py:213-258)。

这两个合同的差别很重要:无状态函数适合简单查询或格式转换;有状态工具适合浏览器、代码环境、游戏环境、仿真器这类需要 per-trajectory state 的环境。

3. 工具响应进入上下文,但不承担 policy loss

工具执行结束后,系统不会只保存一个 “tool result” 字段。它会把工具返回重新包装成 message,追加到对话上下文,再 tokenize 成 response 区间的一部分。这里最容易误读:工具 observation 在 responses里,但它不是模型生成 token。

下面这张图把这一层拆开。看图时注意 response 区间里的两种 token:assistant token 的 mask 是 1,tool observation token 的 mask 是 0。

工具 observation 如何回到训练轨迹

源码里,工具返回可以是 text,也可以带 image/video 字段。ToolAgentLoop会构造 role 为 tool的 message;如果有图像,还会把 image data 追加到本轮多模态输入。随后它把 tool messages 经过 chat template 转成 token ids,追加到 prompt_ids,同时给这些 token 的 response_mask追加 0;如果有 rollout logprobs,也给 observation token 补 0.0(verl/experimental/agent_loop/tool_agent_loop.py:287-374)。

工具自身的分数也不会丢。AgentData里保留 turn_scorestool_rewards,每次工具执行返回的 tool_reward会被 append 到 agent_data.tool_rewards,最终写入 AgentLoopOutput.extra_fieldsverl/experimental/agent_loop/tool_agent_loop.py:50-90verl/experimental/agent_loop/tool_agent_loop.py:181-200verl/experimental/agent_loop/tool_agent_loop.py:334-335)。

最终 _postprocess()会把这些 extra fields 放入 DataProto 的 non-tensor batch:turn_scorestool_rewardsreward_extra_infomin_global_stepsmax_global_steps等字段都以稳定 key 集合进入下游(verl/experimental/agent_loop/agent_loop.py:895-986)。这说明工具结果既影响下一轮上下文,也能影响 reward 和日志,但不会被误当成 actor 自己生成的 token。

4. Sandbox 是 reward 路径里的受控执行

tool calling 让环境反馈进入 rollout;sandbox 则解决另一类问题:有些 reward 不是文本匹配或 reward model 打分,而是要执行代码、跑测试、比较输出。此时训练系统需要一个受控的执行环境,避免把任意生成代码直接跑在 trainer 进程里。

reward.yaml里有 sandbox_fusion配置,包含 URL、最大并发和每个 sandbox 进程的内存限制(verl/trainer/config/reward/reward.yaml:85-93)。当用户没有提供自定义 reward function 时,load_reward_manager()会检查 sandbox_fusion.url;如果存在,就用 multiprocessing manager 创建 semaphore,把 sandbox URL、并发控制和 memory limit 传给默认 compute score(verl/trainer/ppo/reward.py:124-157)。

具体执行在 sandbox_fusion/utils.pycall_sandbox_api()会把 code、stdin、compile/run timeout、memory limit 和 language 打包发给 sandbox API,并对 504 做重试(verl/utils/reward_score/sandbox_fusion/utils.py:67-171)。check_correctness()则把多组输入输出提交到线程池并发执行,每个 case 经过 _process_single_case()调 sandbox,最后返回 True/False 以及 api error、compile error、runtime error、timeout 等 metadata(verl/utils/reward_score/sandbox_fusion/utils.py:449-578)。

这就是 sandbox 和 tool calling 的分工:工具执行发生在轨迹中,会影响后续模型上下文;sandbox reward 更像评分器的执行后端,主要影响 rm_scores和 reward extra info。它们都属于“环境反馈”,但一个改变 rollout 轨迹,一个改变训练信号。

小结:环境反馈不是一个外部注释

本文把 tool calling 和 sandbox 放回训练数据流:

代码语言:javascript
复制
tool schema
-> model emits tool call
-> tool executes with lifecycle
-> tool observation enters token trajectory with response_mask=0
-> tool_rewards / reward_extra_info enter non_tensor_batch
-> sandbox reward can score executable outputs
-> rm_scores / advantage / actor update

到这里,第三组已经说明:rollout 不只是生成文本,而是在维护一个可训练的环境交互轨迹。下一篇会继续看这个设计的代价:一旦 prompt 长度、response 长度、工具等待、sandbox 执行和多轮次数都变成随机变量,agent RL 为什么特别容易出现长尾 rollout 和 GPU 空转。

本文源码索引

  • verl/trainer/config/rollout/rollout.yaml:164-220:multi-turn、tool config、function tool、parallel calls、tool response 截断和 parser format 配置。
  • verl/experimental/agent_loop/agent_loop.py:412-418AgentLoopWorker如何按配置加载 native/function tools。
  • verl/experimental/agent_loop/tool_agent_loop.py:93-113ToolAgentLoop如何建立 tool map、tool schemas 和 parser。
  • verl/experimental/agent_loop/tool_agent_loop.py:144-155:单样本 extra_info.tool_selection如何过滤工具集。
  • verl/experimental/agent_loop/tool_parser.py:44-113:tool parser 抽象和 Hermes parser 的 tool call 解析。
  • verl/experimental/agent_loop/tool_agent_loop.py:273-286:工具调用如何受 max_parallel_calls控制并发执行。
  • verl/experimental/agent_loop/tool_agent_loop.py:376-451_call_tool()如何校验、执行 FunctionTool/BaseTool,并截断工具响应。
  • verl/tools/base_tool.py:24-93:有状态工具的 create/execute/calc_reward/release 生命周期。
  • verl/tools/function_tool.py:44-64213-258:轻量 function tool 的调用和返回值归一化。
  • verl/experimental/agent_loop/tool_agent_loop.py:287-374:工具响应如何变成 tool message、token ids 和 response_mask=0
  • verl/experimental/agent_loop/agent_loop.py:895-986:agent output 如何把 tool_rewardsturn_scoresreward_extra_info写回 DataProto。
  • verl/trainer/ppo/reward.py:124-157:reward manager 如何接入 sandbox fusion。
  • verl/utils/reward_score/sandbox_fusion/utils.py:67-171:sandbox API 调用、timeout、memory limit 和重试。
  • verl/utils/reward_score/sandbox_fusion/utils.py:449-578check_correctness()如何并发运行测试并返回结果 metadata。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 训推工坊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 工具不是字符串,而是 prompt 合同
  • 2. 工具执行有两种合同:有状态生命周期和无状态函数
  • 3. 工具响应进入上下文,但不承担 policy loss
  • 4. Sandbox 是 reward 路径里的受控执行
  • 小结:环境反馈不是一个外部注释
  • 本文源码索引
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档