写点什么

通过工具增强 LLM Agent 能力:veRL+ReTool 的完整实践指南

  • 2025-09-02
    北京
  • 本文字数:12492 字

    阅读完需:约 41 分钟

资料来源:火山引擎-开发者社区


LLM 的“结构化任务痛点”与 ReTool 的破局

大语言模型(LLM)擅长开放式对话,但面对数学推理、复杂逻辑计算等结构化任务时,往往会陷入两个困境:

  • 靠文本推理 “拍脑袋”,结果错误率高;

  • 不会主动调用工具(比如代码沙箱),无法利用工具的精确计算能力。

字节跳动的 ReTool 框架,用 “冷启动 SFT+RL 策略学习” 的组合拳,让 LLM 学会 “思考 - 执行 - 反馈” 的闭环:先通过监督微调(SFT)掌握基础工具调用,再用强化学习(RL)优化策略,最终在 AIME2024 数学数据集上达到 67% 准确率(仅 400 步训练),远超文本基线 RL 的 40%(需 1080 步)。

我们基于火山引擎 veRL 强化学习框架,完整复现了 ReTool 的 SOTA 效果。今天就把从 “环境搭建” 到 “训练调优” 的全流程,拆成通俗易懂的步骤分享给大家。

ReTool 的核心逻辑:让 LLM 学会 “用工具解决问题”

Retool 是一个专为大语言模型(LLM)设计的工具增强强化学习框架,核心在于通过动态交织的代码执行与强化学习策略优化,提升模型在结构化问题(如数学推理)中的解决能力。其工作分为两个关键阶段:

  • 首先,通过冷启动数据生成流水线构建包含代码增强推理轨迹的高质量数据集,以监督微调方式让模型掌握基础的工具调用与执行结果分析能力。

  • 随后进行工具调用策略学习。具体而言,Retool 在推理时会生成自然语言思考与代码片段的混合轨迹,当检测到代码终止标记时,将代码发送至异步沙盒执行,再将结果(含成功输出或错误信息)反馈给模型以指导后续推理,这种 “思考 - 执行 - 反馈” 的循环机制,配合基于最终答案准确性的奖励设计,使模型能自主发现最优工具调用模式,既提升推理效率又增强计算准确性。

论文链接:https://arxiv.org/pdf/2504.11536

实验设置:论文在训练过程中采用了 VeRL 框架,并选用 PPO 作为强化学习方法,其余设置详见论文。

实验结果:论文中在数学场景(如 AIME2024 数据集)验证,准确率提升至 67.0%(仅需 400 步训练),远超文本基线 RL 的 40.0%(需 1080 步)。下为在机器学习平台上的复现效果,验证集为 AIME2024,实验结果可在机器学习平台的实验管理中查看。

veRL:支撑 ReTool 复现的 “RL 基建”

要复现 ReTool,得有一个灵活、高效、支持生产环境的 RL 框架,veRL 是火山引擎推出的用于大语言模型(LLM)的强化学习库,具有灵活性、高效性且适用于生产环境。借助 veRL 强化学习框架,可以使模型在推理过程中动态插入代码块并与沙盒环境实时交互,根据执行反馈(如正确 / 错误结果)迭代优化工具使用策略。veRL 具有以下特点:

  • 异步推理请求机制:veRL Agent loop 采用异步机制,主要通过 Python 的 asyncio 库实现。在 AgentLoopWorker 的 generate\_sequences 方法中,为每个输入消息创建异步任务 \_run\_agent\_loop,并使用 asyncio.gather 并发执行这些任务,让工具调用和 GPU 计算能够同时执行,提高处理效率。工作流程如下:

  • 自定义工具:veRL 支持多种工具调用,并且可以让用户自定义工具。目前已经提供的工具包括 Search tool、代码沙箱、MCP 等,想接什么工具自己定;

  • 扩展 Agent loop,支持 LangGraph 等 Agent 框架:veRL AgentLoop 具备良好扩展性,可支持各类 agent 框架。开发者能将这些框架的独特优势集成到 Agent loop 里,例如利用 LangGraph 在图处理和智能推理方面的长处,提升 Agent 处理复杂任务的能力。

从 0 到 1 复现 ReTool 的完整步骤

获取详细实践文档:https://console.volcengine.com/ml-platform/region:ml-platform+cn-beijing/modelPractice/detail?id=mp-20250807230210-2r8g5

目标:复现 ReTool 论文效果,通过 Multi-turn 协作机制,提升模型在数据领域内的效果(如数学推理任务),同时确保训练效率和安全。

能力依赖

  • Multi-turn 交互:支持模型与沙盒的异步反馈循环。

  • Async-Rollout:实现非阻塞式执行,提升并发效率。

组件:

  • 火山引擎机器学习平台

  • veRL 强化学习训练框架

  • vePFS

  • veFaaS

Step1:环境准备:搭好 “工具调用的基建”

创建 veFaaS 服务

veFaaS 云沙箱管理--函数服务-火山引擎:https://www.volcengine.com/docs/6662/1656341

  • Sandbox 实例规格:16c / 64G

  • 并发设置:16/实例

  • 实例数上限:需要根据 reward_model.sandbox_fusion.max_concurrent 进行设置,若 reward_model.sandbox_fusion.max_concurrent =256,则实例数上限为 256/16(并发)=16

  • 获取函数服务域名




准备代码

创建开发机,在 vePFS 对应目录克隆代码:

  cd /your\_path  git clone https://github.com/volcengine/verl.git -b v0.5.0  pip install -e .["all"] --no-build-isolation  
复制代码


准备数据集、模型

创建开发机,在 vePFS 对应目录下载数据集

  • sft 数据集 swordfaith/ReTool-SFT-multi-turn

  • RL 数据集 BytedTsinghua-SIA/DAPO-Math-17k

  • 评测数据集 BytedTsinghua-SIA/AIME-2024

  export HF\_ENDPOINT=https://hf-mirror.com  huggingface-cli download --repo-type dataset --resume-download {DATASET} --local-dir {YOUR\_PATH}  
复制代码


预处理数据集,生成 ReTool 需要的 sft 及 RL 数据集。

如果不能直连 huggingface,将脚本里的数据集改为下载好的数据集路径:

  python3 examples/data\_preprocess/dapo\_multiturn\_w\_tool.py  python3 recipe/retool/retool\_multi\_turn\_sft\_preprocess.py   
复制代码


火山引擎提供 TOS 对象存储预置模型权重文件,方便客户自助复制,加速试验。以 Qwen/Qwen2.5-32B-Instruct 模型为例。

  import os   # cp 需要带-r 参数,否则不会下载目录;传输速度慢可以根据开发机的 CPU 数量调整 -j -p 并发参数   ! tosutil cp tos://preset-models-{VOLC\_REGION}/{model\_name}/ {model\_path}/{os.path.dirname(model\_name)} -r -u -j=32   
复制代码


如果不使用预置模型,可以自行下载。

  export HF\_ENDPOINT=https://hf-mirror.com  huggingface-cli download --resume-download {MODEL\_NAME} --local-dir {YOUR\_PATH}  
复制代码


编辑脚本

在 verl/recipe/retool 目录下插入 run_qwen2-32b_sft.sh,脚本内容如下。

可以根据存储空间大小调整 trainer.save_freq

  set -x    # ================= data/model/tool =================    dapo\_math\_17k=retool\_dapo数据集路径  aime\_2024=AIME-2024数据集路径  model\_path=xxxx/retool-multiturn-sft-qwen2.5-32b-sp8/global\_step\_168/huggingface #计划将sft的checkpoint保存的路径  train\_files="['$dapo\_math\_17k']"  test\_files="['$aime\_2024']"    # tool  tool\_config\_path=xxxxx/verl/recipe/retool/sandbox\_fusion\_tool\_config.yaml    # wandb  project\_name=retool\_async\_rl\_lusz  experiment\_name=qwen2.5-32b\_dapo\_xibin\_process\_dataset  default\_local\_dir=xxxxx/checkpoint/$experiment\_name# checkpoint路径  # ================= algorithm =================  adv\_estimator=grpo    use\_kl\_in\_reward=False  kl\_coef=0.0  use\_kl\_loss=False  kl\_loss\_coef=0.0    clip\_ratio\_low=0.2  clip\_ratio\_high=0.28    max\_turns=8  max\_prompt\_length=2048  max\_response\_length=16384  actor\_lr=1e-6    train\_batch\_size=512  ppo\_mini\_batch\_size=64  n\_resp\_per\_prompt=16  n\_resp\_per\_prompt\_val=1    # ================= perfomance =================  infer\_tp=4 # vllm  train\_sp=8 # train  offload=True    actor\_max\_token\_len\_per\_gpu=$(( (max\_prompt\_length + max\_response\_length) * 1 ))  log\_prob\_max\_token\_len\_per\_gpu=$(( actor\_max\_token\_len\_per\_gpu * 4 ))    python3 -m verl.trainer.main\_ppo \      algorithm.adv\_estimator=$adv\_estimator \      algorithm.use\_kl\_in\_reward=$use\_kl\_in\_reward \      algorithm.kl\_ctrl.kl\_coef=$kl\_coef \      data.train\_files="$train\_files" \      data.val\_files="$test\_files" \      data.return\_raw\_chat=True \      data.train\_batch\_size=$train\_batch\_size \      data.max\_prompt\_length=$max\_prompt\_length \      data.max\_response\_length=$max\_response\_length \      data.filter\_overlong\_prompts=True \      data.truncation='error' \      data.custom\_cls.path=recipe/retool/retool.py \      data.custom\_cls.name=CustomRLHFDataset \      custom\_reward\_function.path=recipe/retool/retool.py \      custom\_reward\_function.name=compute\_score \      actor\_rollout\_ref.model.path=$model\_path \      actor\_rollout\_ref.model.use\_remove\_padding=True \      actor\_rollout\_ref.model.enable\_gradient\_checkpointing=True \      actor\_rollout\_ref.actor.use\_kl\_loss=$use\_kl\_loss \      actor\_rollout\_ref.actor.kl\_loss\_coef=$kl\_loss\_coef \      actor\_rollout\_ref.actor.clip\_ratio\_low=$clip\_ratio\_low \      actor\_rollout\_ref.actor.clip\_ratio\_high=$clip\_ratio\_high \      actor\_rollout\_ref.actor.clip\_ratio\_c=10.0 \      actor\_rollout\_ref.actor.optim.lr=$actor\_lr \      actor\_rollout\_ref.actor.use\_dynamic\_bsz=True \      actor\_rollout\_ref.actor.ppo\_mini\_batch\_size=$ppo\_mini\_batch\_size \      actor\_rollout\_ref.actor.ppo\_max\_token\_len\_per\_gpu=$actor\_max\_token\_len\_per\_gpu \      actor\_rollout\_ref.actor.ulysses\_sequence\_parallel\_size=$train\_sp \      actor\_rollout\_ref.actor.fsdp\_config.param\_offload=$offload \      actor\_rollout\_ref.actor.fsdp\_config.optimizer\_offload=$offload \      actor\_rollout\_ref.ref.log\_prob\_max\_token\_len\_per\_gpu=$log\_prob\_max\_token\_len\_per\_gpu \      actor\_rollout\_ref.rollout.name=vllm \      actor\_rollout\_ref.rollout.mode=async \      actor\_rollout\_ref.rollout.tensor\_model\_parallel\_size=$infer\_tp \      actor\_rollout\_ref.rollout.multi\_turn.enable=True \      actor\_rollout\_ref.rollout.multi\_turn.max\_user\_turns=$max\_turns \      actor\_rollout\_ref.rollout.multi\_turn.max\_assistant\_turns=$max\_turns \      actor\_rollout\_ref.rollout.multi\_turn.tool\_config\_path=$tool\_config\_path \      actor\_rollout\_ref.rollout.multi\_turn.format=hermes \      actor\_rollout\_ref.rollout.gpu\_memory\_utilization=0.9 \      actor\_rollout\_ref.rollout.n=$n\_resp\_per\_prompt \      actor\_rollout\_ref.rollout.val\_kwargs.top\_p=0.6 \      actor\_rollout\_ref.rollout.val\_kwargs.temperature=1.0 \      actor\_rollout\_ref.rollout.val\_kwargs.n=$n\_resp\_per\_prompt\_val \      trainer.logger=['console','vemlp\_wandb'] \      trainer.project\_name=$project\_name \      trainer.experiment\_name=$experiment\_name \      trainer.n\_gpus\_per\_node=8 \      trainer.val\_before\_train=True \      trainer.log\_val\_generations=100 \      trainer.nnodes=4 \      trainer.save\_freq=30 \      trainer.default\_local\_dir=$default\_local\_dir \      trainer.test\_freq=5 \      trainer.total\_epochs=1 $@ \  
复制代码


在 verl/recipe/retool 目录下插入 run_qwen2-32b_dapo.sh,脚本内容如下:

  set -x    # ================= data/model/tool =================    dapo\_math\_17k=retool\_dapo数据集路径  aime\_2024=AIME-2024数据集路径  model\_path=xxxx/retool-multiturn-sft-qwen2.5-32b-sp8/global\_step\_168/huggingface #计划将sft的checkpoint保存的路径  train\_files="['$dapo\_math\_17k']"  test\_files="['$aime\_2024']"    # tool  tool\_config\_path=xxxxx/verl/recipe/retool/sandbox\_fusion\_tool\_config.yaml    # wandb  project\_name=retool\_async\_rl\_lusz  experiment\_name=qwen2.5-32b\_dapo\_xibin\_process\_dataset  default\_local\_dir=xxxxx/checkpoint/$experiment\_name# checkpoint路径  # ================= algorithm =================  adv\_estimator=grpo    use\_kl\_in\_reward=False  kl\_coef=0.0  use\_kl\_loss=False  kl\_loss\_coef=0.0    clip\_ratio\_low=0.2  clip\_ratio\_high=0.28    max\_turns=8  max\_prompt\_length=2048  max\_response\_length=16384  actor\_lr=1e-6    train\_batch\_size=512  ppo\_mini\_batch\_size=64  n\_resp\_per\_prompt=16  n\_resp\_per\_prompt\_val=1    # ================= perfomance =================  infer\_tp=4 # vllm  train\_sp=8 # train  offload=True    actor\_max\_token\_len\_per\_gpu=$(( (max\_prompt\_length + max\_response\_length) * 1 ))  log\_prob\_max\_token\_len\_per\_gpu=$(( actor\_max\_token\_len\_per\_gpu * 4 ))    python3 -m verl.trainer.main\_ppo \      algorithm.adv\_estimator=$adv\_estimator \      algorithm.use\_kl\_in\_reward=$use\_kl\_in\_reward \      algorithm.kl\_ctrl.kl\_coef=$kl\_coef \      data.train\_files="$train\_files" \      data.val\_files="$test\_files" \      data.return\_raw\_chat=True \      data.train\_batch\_size=$train\_batch\_size \      data.max\_prompt\_length=$max\_prompt\_length \      data.max\_response\_length=$max\_response\_length \      data.filter\_overlong\_prompts=True \      data.truncation='error' \      data.custom\_cls.path=recipe/retool/retool.py \      data.custom\_cls.name=CustomRLHFDataset \      custom\_reward\_function.path=recipe/retool/retool.py \      custom\_reward\_function.name=compute\_score \      actor\_rollout\_ref.model.path=$model\_path \      actor\_rollout\_ref.model.use\_remove\_padding=True \      actor\_rollout\_ref.model.enable\_gradient\_checkpointing=True \      actor\_rollout\_ref.actor.use\_kl\_loss=$use\_kl\_loss \      actor\_rollout\_ref.actor.kl\_loss\_coef=$kl\_loss\_coef \      actor\_rollout\_ref.actor.clip\_ratio\_low=$clip\_ratio\_low \      actor\_rollout\_ref.actor.clip\_ratio\_high=$clip\_ratio\_high \      actor\_rollout\_ref.actor.clip\_ratio\_c=10.0 \      actor\_rollout\_ref.actor.optim.lr=$actor\_lr \      actor\_rollout\_ref.actor.use\_dynamic\_bsz=True \      actor\_rollout\_ref.actor.ppo\_mini\_batch\_size=$ppo\_mini\_batch\_size \      actor\_rollout\_ref.actor.ppo\_max\_token\_len\_per\_gpu=$actor\_max\_token\_len\_per\_gpu \      actor\_rollout\_ref.actor.ulysses\_sequence\_parallel\_size=$train\_sp \      actor\_rollout\_ref.actor.fsdp\_config.param\_offload=$offload \      actor\_rollout\_ref.actor.fsdp\_config.optimizer\_offload=$offload \      actor\_rollout\_ref.ref.log\_prob\_max\_token\_len\_per\_gpu=$log\_prob\_max\_token\_len\_per\_gpu \      actor\_rollout\_ref.rollout.name=vllm \      actor\_rollout\_ref.rollout.mode=async \      actor\_rollout\_ref.rollout.tensor\_model\_parallel\_size=$infer\_tp \      actor\_rollout\_ref.rollout.multi\_turn.enable=True \      actor\_rollout\_ref.rollout.multi\_turn.max\_user\_turns=$max\_turns \      actor\_rollout\_ref.rollout.multi\_turn.max\_assistant\_turns=$max\_turns \      actor\_rollout\_ref.rollout.multi\_turn.tool\_config\_path=$tool\_config\_path \      actor\_rollout\_ref.rollout.multi\_turn.format=hermes \      actor\_rollout\_ref.rollout.gpu\_memory\_utilization=0.9 \      actor\_rollout\_ref.rollout.n=$n\_resp\_per\_prompt \      actor\_rollout\_ref.rollout.val\_kwargs.top\_p=0.6 \      actor\_rollout\_ref.rollout.val\_kwargs.temperature=1.0 \      actor\_rollout\_ref.rollout.val\_kwargs.n=$n\_resp\_per\_prompt\_val \      trainer.logger=['console','vemlp\_wandb'] \      trainer.project\_name=$project\_name \      trainer.experiment\_name=$experiment\_name \      trainer.n\_gpus\_per\_node=8 \      trainer.val\_before\_train=True \      trainer.log\_val\_generations=100 \      trainer.nnodes=4 \      trainer.save\_freq=30 \      trainer.default\_local\_dir=$default\_local\_dir \      trainer.test\_freq=5 \      trainer.total\_epochs=1 $@ \  
复制代码


修改 recipe/retool/sandbox_fusion_tool_config.yaml,将 sandbox_fusion_url 填写为 step1-1 中建立的 veFaaS 地址:

  sandbox\_fusion\_url: "https://***.apigateway-cn-beijing.volceapi.com/run\_code"  
复制代码


设置训练参数

当前我们已经预置了一些调优后的参数,您还可以进一步自定义超参数。了解更多参数的含义和进行训练调优,可参考 veRL 官方调优指南 perf_tunning:https://verl.readthedocs.io/en/latest/perf/perf\_tuning.html

提交自定义任务

环境变量配置

volc cli 配置

volc cli 为机器学习平台的命令行工具,可以以命令行的方式便捷的进行任务提交,任务管理等操作。预置镜像已经安装 volc 命令行工具,进行升级操作。

  # 升级volc cli  ! volc upgrade  # 查看volc cli版本  ! volc v  
复制代码


可通过以下操作配置好 volc cli 和 jupyter notebook 需要的的环境依赖。

如果您不知道您的 AK/SK,可以通过 API 访问密钥(https://console.volcengine.com/iam/keymanage) 获得您当前身份的密钥对。

  # 火山引擎认证配置(替换为你的 AK/SK)   VOLC\_ACCESS\_KEY\_ID = '**'  VOLC\_SECRET\_ACCESS\_KEY = '=='# 默认使用开发机所在的 region   import os   VOLC\_REGION=os.environ['MLP\_REGION']   # 一次性设置所有环境变量(Jupyter魔法命令)   %set\_env VOLC\_ACCESS\_KEY\_ID={VOLC\_ACCESS\_KEY\_ID}   %set\_env VOLC\_SECRET\_ACCESS\_KEY={VOLC\_SECRET\_ACCESS\_KEY}   %set\_env VOLC\_REGION={VOLC\_REGION}   # 配置火山命令行工具(自动读取已设置的环境变量)   ! volc configure --ak $VOLC\_ACCESS\_KEY\_ID --sk $VOLC\_SECRET\_ACCESS\_KEY --region $VOLC\_REGION  # 验证配置文件(可选)   ! echo"volc config:" && cat ${HOME}/.volc/config   ! echo"volc credentials:" && cat ${HOME}/.volc/credentials  
复制代码


镜像配置

这里配置该文档所提交的所有任务,所用到的镜像信息,会默认使用你开机机所在 region 的镜像,如有其他需求,请更换为您所在的区域,以获取更好的体验。

  # 根据当前 region 生成镜像地址   image\_url = f'vemlp-{VOLC\_REGION}.cr.volces.com/preset-images/verl:v0.4.1'  print(f'image: {image\_url}')  
复制代码


资源配置

您可通过以下方式获取相关运行配置:

  queue='q-xxxx'  # 队列 id   flavor='ml.xxxxxx'# 8*80G显存显卡的机器  replicas=4  # 文件系统配置   mount\_path = "/file\_system"# vepfs 挂载路径   storage\_type = "Vepfs"     vepfs\_id = "vepfs-xxx"# vepfs id experiment\_name="qwen2\_3b\_function\_rm" # wandb 实验名称  script\_path = "/path/to/verl/recipe/retool"  ckpt\_path = "path/to/ckpt/huggingface"  
复制代码


提交 sft 任务

在进行 RL 训练前,需要先执行 sft 任务,冷启动模型。

  import yaml  def build\_envs():      envs = [          {"Name": "VOLC\_ACCESS\_KEY\_ID", "Value": VOLC\_ACCESS\_KEY\_ID, "IsPrivate": True},          {"Name": "VOLC\_SECRET\_ACCESS\_KEY", "Value": VOLC\_SECRET\_ACCESS\_KEY, "IsPrivate": True},          {"Name": "VLLM\_USE\_V1", "Value": "1", "IsPrivate": False},          {"Name": "MLP\_TRACKING\_REGION", "Value": "cn-beijing", "IsPrivate": False},          {"Name": "PYTHONPATH", "Value": {verl\_path}, "IsPrivate": False},      ]      return envs  # 定义配置内容  task\_config = {      "TaskName": "verl-retool-Qwen-32B",      "Description": "Retool Qwen-32B",      "Entrypoint": f'''cd {script\_path}                          bash run\_qwen2-32b\_sft.sh                        python3 -m verl.model\_merger merge --backend fsdp --local\_dir {ckpt\_path} --target\_dir {ckpt\_path}/huggingface''',      "Tags": [],      "Envs": build\_envs(),      "ResourceQueueID": queue,             #replace\_with\_your\_ResourceQueueID  "Framework": "PyTorchDDP",      "TaskRoleSpecs": [                           #replace\_with\_your\_TaskRoleSpecs          {              "RoleName": "worker",              "RoleReplicas": replicas,              "Flavor": flavor,          }      ],      "ActiveDeadlineSeconds": 864000,      "EnableTensorBoard": False,       #replace\_with\_your\_Storages  "Storages": [                                        {            "MountPath": mount\_path,            "Type": storage\_type,            "VepfsId": vepfs\_id,          }      ],      "ImageUrl": image\_url,                   #replace\_with\_you\_image\_url  "RetryOptions": {          "EnableRetry": False,          "MaxRetryTimes": 5,          "IntervalSeconds": 120,          "PolicySets": [],      },  }    # 将配置写入到 yaml 文件中  import datetime  verl\_retool\_config\_yaml = f'sft-verl-retool-{datetime.datetime.now().strftime("%Y%m%d\_%H%M%S")}.yaml'  with open(verl\_retool\_config\_yaml, "w") as file:      yaml.dump(task\_config, file, default\_flow\_style=False)    print(f"{verl\_retool\_config\_yaml} 文件已生成")    ! volc ml\_task submit --conf {verl\_retool\_config\_yaml}  
复制代码


通过 volc 命令行工具查询作业状态:

  ! volc ml\_task get --id t-xxxxxxxxxxx --output json --format Status  
复制代码


提交 RL 任务

请注意:RL 任务需要在 sft 任务执行完成后再提交,否则没有相应的 checkpoint。

首先配置自定义任务的启动参数,并通过 volc cli 命令行工具提交自定义任务。使用 Ray 框架进行分布式训练,执行下面的命令新建一个 yaml 任务配置文件:

  # 定义配置内容  import yaml  task\_config = {      "TaskName": "verl-retool-Qwen-32B",      "Description": "Retool Qwen-32B",      "Entrypoint": f'''cd {verl\_path}                        bash recipe/retool/run\_qwen2-32b\_dapo.sh''',      "Tags": [],      "Envs": build\_envs(),      "ResourceQueueID": queue,             #replace\_with\_your\_ResourceQueueID  "Framework": "Ray",      "TaskRoleSpecs": [                           #replace\_with\_your\_TaskRoleSpecs          {              "RoleName": "head",              "RoleReplicas": 1,              "Flavor": flavor,          },          {              "RoleName": "worker",              "RoleReplicas": replicas - 1,              "Flavor": flavor,          }      ],      "ActiveDeadlineSeconds": 864000,      "EnableTensorBoard": False,       #replace\_with\_your\_Storages  "Storages": [                                        {            "MountPath": mount\_path,            "Type": storage\_type,            "VepfsId": vepfs\_id,          }      ],      "ImageUrl": image\_url,                   #replace\_with\_you\_image\_url  "RetryOptions": {          "EnableRetry": False,          "MaxRetryTimes": 5,          "IntervalSeconds": 120,          "PolicySets": [],      },  }    # 将配置写入到 yaml 文件中  import datetime  verl\_retool\_config\_yaml = f'rl-verl-retool-{datetime.datetime.now().strftime("%Y%m%d\_%H%M%S")}.yaml'  with open(verl\_retool\_config\_yaml, "w") as file:      yaml.dump(task\_config, file, default\_flow\_style=False)    print(f"{verl\_retool\_config\_yaml} 文件已生成")    ! volc ml\_task submit --conf {verl\_retool\_config\_yaml}  
复制代码


观察训练中任务日志/实验过程/资源利用率

使用实验管理记录训练过程

veRL 中参数设置为 trainer.logger=['console','vemlp_wandb']

使用 Trace 工具分析训练过程

Agentic RL 在 rollout 过程中会有多轮对话、工具调用,以及用户交互的场景。在模型训练过程中,需要追踪函数调用、输入和输出,来了解数据在应用程序中的流动路径。Trace 功能通过记录函数的输入、输出和对应的时间戳,帮助在复杂的多轮对话中,查看数据在每次交互时的转换,最终得到输出的整个过程,有助于理解模型对数据的处理细节,来优化训练效果。

veRL Trace 功能集成了常用的 Agent trace 工具,已经支持的有 wandb weave 和 mlflow。用户可以根据自己的需求和习惯选择合适的 trace 工具。这里以 weave 为例,介绍下 trace 工具的使用方法。

基础配置

1.设置 WANDB_API_KEY 环境变量

2.veRL 配置参数

trainer.rollout_trace.backend=weave

trainer.logger=['console','wandb'] 。此项是可选项,trace 和 logger 是互相独立的功能,推荐使用 weave 时,也开启 wandb logger,在一个系统实现两项功能。

trainer.project_name=$project_name

trainer.experiment_name=$experiment_name

查看 Trace 日志

执行训练后,在项目页面中,可以看到 WEAVE 的侧边栏,点击 Traces 来查看。

每个 Trace 项目对应一个 trajectory。可以通过 step、sample_index、rollout_n、experiment_name 来过滤筛选需要查看的 trajectory。

开启 token2text 后,会自动在 ToolAgentLoop.run 的输出里面,增加 prompt_text 和 response_text,方便查看输入和输出的内容。

比较 Trace 日志

weave 可以选择多个 trace 项目,然后比较其中的差异。

复现 SOTA 的“关键经验”总结

Agent 在 RL 训练中需要使用 token 作为输入和输出

我们发现 decode 消息得到的 token_ids 可能与每一轮中通过合并 prompt_ids 和 response_ids 得到的 token_ids 不一致。对训练的影响是训练到 100 步左右时,模型性能会突然下降,同时 actor/grad_norm 指标也会变成 NaN。

这种不一致发生在哪里呢?

因为解码-编码有很多情况不可逆,比如生成 ”helloworld” 的 token 可能有几种组合情况,但是根据 ”helloworld” 转成的 token 只有一种组合,可能跟原来的 token 不同。

所以 veRL 采用了 token in and token out 的方式,让 agent 调用 llm generate 方法时,输入和输出都使用 token,来避免 token 和明文消息互相转换不一致的问题。

使用 SGLang 和 FlashInfer 算子时,Qwen2.5 模型大概率不会调用工具

下面是该现象的一个例子:

推测跟 FlashInfer 精度有关,SGLang 支持的其他算子并没有这个现象,已经通过在 veRL 中固定使用 FlashAttention 来避免这个问题。

对 SGLang 支持的算子做了测试,具体情况见下表,目前只发现 FlashInfer 有这个现象。

更多问题和解决方案参考

LLM

问题 1:qwen3 有深度思考模式,倾向于文本推理,很少输出代码,所以训练效果不佳。

解决方案:按 ReTool 论文用的 qwen2.5-32b 来复现。

问题 2:使用 SGLang + FlashInfer 算子时,模型不会调用工具。

解决方案:跟 FlashInfer 精度有关,SGLang 支持的其他算子并没有这个现象,已经通过在 veRL 中固定使用 Flashatten3 来避免这个问题。

sandbox

问题:因为 sft 后模型的行为是生成交互式代码,最后一行是变量名,不包含 print 函数,导致不会返回代码输出。

解决方案:在输入给 code sandbox 前对代码做处理,自动在最后一行添加 print。

模型性能

问题 1:训练 100 步之后模型能力下降。

解决方案:这是 veRL 早期实现问题,在最新版本已经解决。该问题是因为 LLM 输出的是 text 明文,训练时转换成 token 后,跟原始的 token 有差异,导致训练精度不佳。原因是 token 和 text 的转换不可逆,比如

转换成 token 之后,跟原始的 token 不一致。

问题 2:遇到性能提升不上去时,有哪些方法能帮助定位问题?

解决方案:

1.因为训练的数据只能包含 LLM 自己生成的内容,不应该包含 tool 生成的,所以需要把 tool 生成的部分 mask 遮住后再训练,并且保证 token 级别一致。

2.配置 trainer.log_val_generations=10 参数可以打印测试集的输入和输出,用于判断模型能力变化。

3.tool 本身也有可能出错,可以打印出 tool 输出内容,看是否有异常。在训练过程中识别到了多个 sandbox fusion 的 bug,均已解决。

4.规划了 trace 功能,用于分析训练过程,观测 LLM 和 tool 输出。可以关注改功能开发进展:https://github.com/volcengine/verl/issues/2188

问题 3:如何提升 acc 的 tricks?

解决方案:修改写 Python 代码时候的提示词+对应修改答案提取方式,带来了可观的涨点。

修改前:

  Execute Python code to perform calculations, data analysis, or other computational tasks.  
复制代码


修改后:

想试试?从 veRL 开始

复现 ReTool 不是终点,而是起点 —— 用 ReTool+veRL,你可以让 LLM 在数据处理、逻辑推理、复杂计算等任务中更靠谱。

用户头像

还未添加个人签名 2022-01-25 加入

还未添加个人简介

评论

发布
暂无评论
通过工具增强 LLM Agent 能力:veRL+ReTool 的完整实践指南_字节跳动_火山引擎开发者社区_InfoQ写作社区