问题背景
近期在用verl框架进行Agentic-RL训练,之前的agent model实验我一般都是采用了与ToRL或者ReTool类似的数据格式,即模型的工具调用通过对应的特殊工具调用token实现,例如如下图所示,使用<code>......</code>标记工具调用,<interpreter>......</interpreter>填充工具调用结果:

但使用verl进行Agentic-RL训练时,框架的整体实现基于Sglang样式,输入数据集的格式更像是一种传统的多轮对话风格,只不过新增了一个role为tool的部分。一个典型的数据集格式如下:

所有的工具调用被统一为<tool_call></tool_call>,工具响应则变为<tool_response></tool_response>
基于以上数据集,可以很快完成ReTool的SFT&RL复现。然而,在迁移到领域Agent Model时,这样的数据集格式却带来一个"神奇"的问题。
问题描述
领域Agent Model可以调用多种工具,这些工具的基本结构相同,但具体的参数字段却是可以不同的,例如
bash
<tool_call>
{"name": "tool_name_1", "argument": {"para_name_x": value_x, "para_name_y": value_y}}
</tool_call>
另一个工具调用为
bash
<tool_call>
{"name": "tool_name_2", "argument": {"para_name_p": value_p, "para_name_q": value_q}}
</tool_call>
然而,在尝试将这种多轮次"多工具"数据转换成为verl中使用的parquet格式的多轮工具调用数据集时,会发现一个"神奇"的现象,即使用datasets里面的.to_parquet函数时,该函数会自动将所有的工具格式对齐,也就是会发生:
python
<tool_call>
{"name": "tool_name_1", "argument": {"para_name_x": value_x, "para_name_y": value_y, "para_name_p": None, "para_name_q": None}}
</tool_call>
......
<tool_call>
{"name": "tool_name_2", "argument": {"para_name_x": None, "para_name_y": None, "para_name_p": value_p, "para_name_q": value_q}}
</tool_call>
即to_parquet函数会自动认为所有的工具参数都包含一个"全集"的字段,导致字段冗余和上下文膨胀,不利于工具传参。
原因分析
HuggingFace datasets 的底层存储是Apache Arrow(arrow table),而.to_parquet() 实际是在把 Arrow 的 schema写入 Parquet(列式存储格式),而 Parquet/Arrow 对"结构化数据"的核心要求是:同一列的 schema 必须稳定且一致。
因此,当在某一列里放的是 dict(嵌套结构),Arrow 会把它推断为一个struct 类型,类似:
text
argument: struct<
para_name_x: ...,
para_name_y: ...,
para_name_p: ...,
para_name_q: ...
>
一旦它在全数据集中见过 para_name_x / y 和 para_name_p / q,为了保证整列 schema 一致,它就必须把 struct 的字段做"并集(union)"。于是:
- 某条样本没有的字段 → 只能补
null - 最终写 Parquet 时,schema 固化,字段对齐就不可避免
解决方案
要避免 Arrow 把 argument 解析成 struct,最直接、稳定的办法就是不要让这一列是 "嵌套 dict" 类型,而是把 argument 存成字符串。
也就是把:
json
"argument": {"para_name_x": 1, "para_name_y": 2}
改成:
json
"argument": "{\"para_name_x\": 1, \"para_name_y\": 2}"
这样 Arrow 看到的就是一个普通的 string 列,自然不会进行 struct schema 对齐,也不会产生"全集字段"。
参考
- ReTool-SFT-multi-turn版本数据集格式,huggingface.co/datasets/sw...