#6252·verl

Qwen3.5/Qwen3.6 35B-A3B 多轮工具调用 Agent RL 训练中出现工具调用格式异常并导致崩溃

Author: DBMingCreated May 6, 2026Updated Sep 14, 2026

问题描述

您好,感谢 verl 的开源工作。

我目前使用 verl v0.7.1,对经过 ms-swift SFT 后的 Qwen3.5/Qwen3.6 35B-A3B 模型进行多轮工具调用场景下的 Agent RL 训练。

在两次独立训练中,训练分别在大约 130 step80 step 左右发生崩溃。通过观察训练曲线和生成轨迹日志,我发现崩溃前模型的工具调用格式出现了明显异常,且 malformed tool calls 数量显著增加。

训练曲线如下:

ImageImage

其中,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 风格:

xml
<tool_call>
<function=search_web>
<parameter=queries>
...
</parameter>
</function>
</tool_call>

但在 prompt 的 <tools>...</tools> 中,工具定义仍然是 JSON schema 风格。示例如下。

Tool definition prompt 示例

点击展开 tool definition prompt
Prompt 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 多轮训练过程中逐渐放大模型的格式不稳定问题,最终导致工具调用解析失败和训练崩溃。

训练配置

点击展开训练脚本
bash
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}"

想请教的问题

想请教一下:

  1. 这种 “JSON schema 工具定义 + XML 工具调用格式” 的混合表示,是否可能导致 Qwen3.5/Qwen3.6 模型在 RL 训练过程中出现工具调用格式漂移?

  2. 从我的训练配置来看,是否有一些参数可能加剧工具调用格式不稳定?例如:

    • temperature=1.0
    • top_p=0.95
    • top_k=20
    • rollout_n=8
    • max_response_length=49152
    • loss_mode=gspo
    • clip_ratio_low=3e-4
    • clip_ratio_high=4e-4
    • algorithm.use_kl_in_reward=False
    • actor_rollout_ref.actor.use_kl_loss=False
  3. 是否有其他可能原因导致训练格式崩溃?

如果需要,我也可以继续补充完整的报错 traceback 和更详细的 trajectory 日志。谢谢!