Qwen3.5/Qwen3.6 35B-A3B 多轮工具调用 Agent RL 训练中出现工具调用格式异常并导致崩溃
问题描述
您好,感谢 verl 的开源工作。
我目前使用 verl v0.7.1,对经过 ms-swift SFT 后的 Qwen3.5/Qwen3.6 35B-A3B 模型进行多轮工具调用场景下的 Agent RL 训练。
在两次独立训练中,训练分别在大约 130 step 和 80 step 左右发生崩溃。通过观察训练曲线和生成轨迹日志,我发现崩溃前模型的工具调用格式出现了明显异常,且 malformed tool calls 数量显著增加。
训练曲线如下:
其中,answer_token 是我额外添加的统计指标。我的统计方式是:如果最后一轮回答中不存在 <tool_call></tool_call>,则认为该轮最终回答是有效 answer。可以看到,在约 80 step / 130 step 附近,模型输出中工具调用解析错误明显增多,并最终导致训练崩溃。
进一步查看训练生成的轨迹日志后,发现主要异常集中在工具调用生成格式上。
异常工具调用示例
点击展开异常工具调用示例示例 1:
I need a recent source that states the nominal GDP value and year so I can cite it directly.</think><tool_call>\n<function=search_web英文明确>\n<parameter=queries>\n[\"Japan annual GDP latest year nominal GDP official statistic\", \"Japan nominal GDP 2024 2025 government statistic\"]\n</parameter>\n</function>\n</tool_call>"示例 2:
but first I need fresh evidence.</think><tool_call>\n<function=search_web>\n<parameter=queries>\n[\"XRP latest news January 2026\", \"Ripple SEC case update January 2026 XRP\", \"XRP ETF speculation January 2026\", \"XRP on-chain activity exchange inflows January 2026\"]\n</parameter v2=\"\">\n</function v2=\"\">\n</tool_call>"我比较困惑的点是:在 SFT 和 RL 阶段,Qwen3.5/Qwen3.6 的工具调用格式已经从之前偏 JSON 的格式改成了 XML 风格。但是在 verl 的 RL 训练过程中,agent_loop 的 pending 阶段仍然会基于原有 YAML 工具参数定义拼接 tool schema,而 tool schema 本身仍然是 JSON 格式。
也就是说,最终要求模型输出的工具调用是 XML 风格:
<tool_call>
<function=search_web>
<parameter=queries>
...
</parameter>
</function>
</tool_call>但在 prompt 的 <tools>...</tools> 中,工具定义仍然是 JSON schema 风格。示例如下。
Tool definition prompt 示例
点击展开 tool definition promptPrompt after applying chat template:
<|im_start|>system
# Tools
You have access to the following functions:
<tools>
{"type": "function", "function": {"name": "search_web", "description": "Search the internet and return website abstracts", "parameters": {"type": "object", "properties": {"queries": {"type": "array", "description": "List of search queries; one topic per query"}}, "required": ["queries"]}}}
{"type": "function", "function": {"name": "fetch_web", "description": "Get the full text from web pages for detailed reading and analysis", "parameters": {"type": "object", "properties": {"urls": {"type": "array", "description": "List of full URLs (e.g., https://www.apple.com/apple-news/)"}}, "required": ["urls"]}}}
</tools>
If you choose to call a function ONLY reply in the following format with NO suffix:
<tool_call>
<function=example_function_name>
<parameter=example_parameter_1>
value_1
</parameter>
<parameter=example_parameter_2>
This is the value for the second parameter
that can span
multiple lines
</parameter>
</function>
</tool_call>
<IMPORTANT>
Reminder:
- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags
- Required parameters MUST be specified
- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after
- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls
</IMPORTANT>
actual_system_prompt
<|im_end|>我的担心是:这种 “JSON schema 工具定义 + XML 工具调用输出格式” 的混合表示,是否会在 RL 多轮训练过程中逐渐放大模型的格式不稳定问题,最终导致工具调用解析失败和训练崩溃。
训练配置
点击展开训练脚本export CUDA_DEVICE_MAX_CONNECTIONS=1
export VLLM_USE_V1=1
export VLLM_ALLREDUCE_USE_SYMM_MEM=0
set -xeuo pipefail
########################### Quick Config ###########################
TP=${TP:-8}
PP=${PP:-2}
CP=${CP:-1}
EP=${EP:-8}
ETP=${ETP:-1}
GEN_TP=${GEN_TP:-8}
ALL_OFFLOAD=${ALL_OFFLOAD:-True}
rollout_name="vllm"
project_name='qwen3_6'
exp_name='v1_4node_SFT_35b_megatron_gspo_64k_qwen3.6'
adv_estimator=grpo
train_batch_size=32
rollout_n=8
max_prompt_length=$((1024 * 16))
max_response_length=$((1024 * 48))
ppo_max_token_len_per_gpu=$((max_prompt_length + max_response_length))
infer_max_token_len_per_gpu=$(((max_prompt_length + max_response_length) * 1))
loss_agg_mode="seq-mean-token-mean"
loss_mode=gspo
use_kl_loss=False
kl_loss_coef=0.001
clip_ratio_low=3e-4
clip_ratio_high=4e-4
MODEL_PATH="/path/to/model"
########################### Parameter Arrays ###########################
DATA=(
data.train_files=${train_files}
data.val_files=${val_files}
data.train_batch_size=${train_batch_size}
data.max_prompt_length=${max_prompt_length}
data.max_response_length=${max_response_length}
data.truncation='error'
data.filter_overlong_prompts=True
data.filter_overlong_prompts_workers=16
)
MODEL=(
actor_rollout_ref.model.path=${MODEL_PATH}
actor_rollout_ref.model.trust_remote_code=True
actor_rollout_ref.model.use_remove_padding=False
)
ACTOR=(
actor_rollout_ref.actor.optim.lr=1e-6
actor_rollout_ref.actor.optim.lr_warmup_steps=10
actor_rollout_ref.actor.ppo_mini_batch_size=${train_batch_size}
actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=1
actor_rollout_ref.actor.ppo_max_token_len_per_gpu=${ppo_max_token_len_per_gpu}
actor_rollout_ref.actor.use_dynamic_bsz=False
actor_rollout_ref.actor.use_kl_loss=${use_kl_loss}
actor_rollout_ref.actor.kl_loss_coef=0.01
actor_rollout_ref.actor.kl_loss_type=low_var_kl
actor_rollout_ref.actor.entropy_coeff=0
actor_rollout_ref.actor.megatron.use_mbridge=True
actor_rollout_ref.actor.megatron.vanilla_mbridge=True
actor_rollout_ref.actor.megatron.use_remove_padding=False
actor_rollout_ref.actor.megatron.tensor_model_parallel_size=${TP}
actor_rollout_ref.actor.megatron.pipeline_model_parallel_size=${PP}
actor_rollout_ref.actor.megatron.context_parallel_size=${CP}
actor_rollout_ref.actor.megatron.expert_model_parallel_size=${EP}
actor_rollout_ref.actor.megatron.expert_tensor_parallel_size=${ETP}
actor_rollout_ref.actor.megatron.param_offload=${ALL_OFFLOAD}
actor_rollout_ref.actor.megatron.optimizer_offload=${ALL_OFFLOAD}
actor_rollout_ref.actor.megatron.grad_offload=${ALL_OFFLOAD}
actor_rollout_ref.actor.megatron.dtype=bfloat16
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.policy_loss.loss_mode=${loss_mode}
actor_rollout_ref.actor.loss_agg_mode=${loss_agg_mode}
actor_rollout_ref.actor.megatron.dist_ckpt_optim_fully_reshardable=False
++actor_rollout_ref.actor.megatron.override_transformer_config.attention_backend=auto
+actor_rollout_ref.actor.megatron.override_transformer_config.recompute_method=uniform
+actor_rollout_ref.actor.megatron.override_transformer_config.recompute_granularity=full
+actor_rollout_ref.actor.megatron.override_transformer_config.recompute_num_layers=1
+actor_rollout_ref.actor.megatron.override_transformer_config.moe_aux_loss_coeff=0.01
+actor_rollout_ref.actor.megatron.override_transformer_config.moe_z_loss_coeff=0.001
+actor_rollout_ref.actor.optim.override_optimizer_config.optimizer_offload_fraction=1
+actor_rollout_ref.actor.optim.override_optimizer_config.overlap_cpu_optimizer_d2h_h2d=True
+actor_rollout_ref.actor.optim.override_optimizer_config.use_precision_aware_optimizer=True
+actor_rollout_ref.actor.optim.override_optimizer_config.optimizer_cpu_offload=True
)
ROLLOUT=(
actor_rollout_ref.rollout.name=${rollout_name}
actor_rollout_ref.rollout.tensor_model_parallel_size=${GEN_TP}
actor_rollout_ref.rollout.gpu_memory_utilization=0.6
actor_rollout_ref.rollout.n=${rollout_n}
actor_rollout_ref.rollout.mode=async
actor_rollout_ref.rollout.dtype=bfloat16
actor_rollout_ref.rollout.checkpoint_engine.update_weights_bucket_megabytes=4096
+actor_rollout_ref.rollout.engine_kwargs.vllm.gdn_prefill_backend=triton
actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=1
actor_rollout_ref.rollout.log_prob_use_dynamic_bsz=False
actor_rollout_ref.rollout.log_prob_max_token_len_per_gpu=${infer_max_token_len_per_gpu}
actor_rollout_ref.rollout.multi_turn.format=qwen3_coder
actor_rollout_ref.rollout.val_kwargs.temperature=1.0
actor_rollout_ref.rollout.val_kwargs.top_p=0.95
actor_rollout_ref.rollout.val_kwargs.top_k=20
actor_rollout_ref.rollout.val_kwargs.do_sample=True
actor_rollout_ref.rollout.val_kwargs.n=1
actor_rollout_ref.rollout.temperature=1.0
actor_rollout_ref.rollout.top_p=0.95
actor_rollout_ref.rollout.top_k=20
)
REF=(
actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=1
actor_rollout_ref.ref.log_prob_use_dynamic_bsz=False
actor_rollout_ref.ref.log_prob_max_token_len_per_gpu=${infer_max_token_len_per_gpu}
actor_rollout_ref.ref.megatron.tensor_model_parallel_size=${TP}
actor_rollout_ref.ref.megatron.pipeline_model_parallel_size=${PP}
actor_rollout_ref.ref.megatron.context_parallel_size=${CP}
actor_rollout_ref.ref.megatron.expert_model_parallel_size=${EP}
actor_rollout_ref.ref.megatron.expert_tensor_parallel_size=${ETP}
actor_rollout_ref.ref.megatron.param_offload=${ALL_OFFLOAD}
)
ALGORITHM=(
algorithm.adv_estimator=${adv_estimator}
algorithm.use_kl_in_reward=False
)
TRAINER=(
trainer.critic_warmup=0
trainer.logger='["console", "wandb"]'
trainer.project_name=${project_name}
trainer.experiment_name=${exp_name}
trainer.n_gpus_per_node=8
trainer.nnodes=4
trainer.save_freq=20
trainer.default_local_dir=${modeldir}
trainer.val_before_train=True
trainer.max_actor_ckpt_to_keep=4
+trainer.val_only=False
trainer.test_freq=20
trainer.total_epochs=1
)
########################### Launch ###########################
python3 -m verl.trainer.main_ppo \
--config-path="$CONFIG_PATH" \
--config-name='AIME_megatron' \
"${DATA[@]}" \
"${ALGORITHM[@]}" \
"${MODEL[@]}" \
"${ROLLOUT[@]}" \
"${ACTOR[@]}" \
"${REF[@]}" \
"${TRAINER[@]}" \
"$@" 2>&1 | tee -a "${JOBLOG}"想请教的问题
想请教一下:
这种 “JSON schema 工具定义 + XML 工具调用格式” 的混合表示,是否可能导致 Qwen3.5/Qwen3.6 模型在 RL 训练过程中出现工具调用格式漂移?
从我的训练配置来看,是否有一些参数可能加剧工具调用格式不稳定?例如:
temperature=1.0top_p=0.95top_k=20rollout_n=8max_response_length=49152loss_mode=gspoclip_ratio_low=3e-4clip_ratio_high=4e-4algorithm.use_kl_in_reward=Falseactor_rollout_ref.actor.use_kl_loss=False
是否有其他可能原因导致训练格式崩溃?
如果需要,我也可以继续补充完整的报错 traceback 和更详细的 trajectory 日志。谢谢!
Source: verl-project/verl