AReaL 多轮数学 Agent 训练实战:基于 ArealOpenAI 与 concat 模式的 GSM8K 多轮 GRPO 全流程解析
【免费下载链接】AReaLThe RL Bridge for LLM-based Agent Applications. Made Simple & Flexible.项目地址: https://gitcode.com/GitHub_Trending/are/AReaL
导读
本文以 AReaL 开源仓库中的 examples/multi_turn_math 示例为核心,完整讲解如何从Qwen/Qwen2.5-1.5B-Instruct出发,训练一个具备多轮纠错能力的 GSM8K 数学解题 Agent。你将掌握 AReaL 中ArealOpenAI客户端、concat训练数据组织模式、回合间奖励折扣(reward discount)与对话树导出等核心机制,并能够把单轮 GRPO 配置改造成多轮 Agent 训练配置,直接复现与二次开发。
示例概览:用不到两行配置实现多轮训练
在 AReaL 中,单轮 GSM8K 数学 RL 训练的标准配置位于 examples/math/gsm8k_grpo.yaml。多轮版本 examples/multi_turn_math/gsm8k_grpo_mt.yaml与单轮配置相比只额外增加了两段配置:
export_style: concat agent_run_args: max_turns: 2export_style: concat:指定训练数据的导出风格为concat(拼接模式)。该模式下,Agent 多轮对话的所有交互会被组织成一棵"对话树"(conversation tree),并以叶子节点作为最终导出样本,同时为每个 token 计算正确的loss_mask与turn_ids,保证多轮轨迹可以在 GRPO 训练中正确计算损失与优势。agent_run_args.max_turns: 2:限定 Agent 最多进行 2 轮交互;若第 1 轮答案正确则提前终止,否则追加一条"纠错提示"进入第 2 轮。
这两项配置由自定义配置类MultiTurnGRPOConfig承接(定义见 examples/multi_turn_math/config.py),它在 AReaL 基础配置GRPOConfig之上新增了两个字段:
@dataclass class MultiTurnGRPOConfig(GRPOConfig): agent_run_args: dict = field( default_factory=dict, metadata={"help": "Arguments for running the agent."}, ) export_style: str = field( default="concat", metadata={ "help": "Export style for the completions. By default export_style=concat." }, )从源码结构看,agent_run_args目前只消费其中的max_turns键(在gsm8k_rl_mt.py中通过config.agent_run_args.get("max_turns", 2)读取),这种"通用 dict + 具体消费"的设计便于后续扩展更多 Agent 运行参数而不改动配置基类。
运行方式与命令行参数
示例的运行入口与单轮 GSM8K 示例完全一致,复用 AReaL 的表达式配置加载机制:
python3 examples/multi_turn_math/gsm8k_rl_mt.py \ --config examples/multi_turn_math/gsm8k_grpo_mt.yaml \ scheduler.type=ray \ experiment_name=gsm8k-grpo-multiturn trial_name=trial0命令行要点:
--config指定 YAML 配置文件;scheduler.type=ray通过"键值覆盖"语法把调度器切换为 Ray(对应 areal/infra/launcher/ray.py 的 Ray 启动路径),多机场景下这是推荐选择;experiment_name、trial_name用于标识实验与尝试,会贯穿 rollout、actor、saver、recover、evaluator、stats_logger 等所有组件(YAML 中以${experiment_name}、${trial_name}引用)。
配置文件中的完整 GRPO 训练参数与单轮示例保持同一套体系,例如:
| 区块 | 关键参数 | 示例值 | 说明 |
|---|---|---|---|
cluster | n_nodes/n_gpus_per_node | 1/8 | 单机 8 卡 |
rollout | backend | "sglang:d4p1t1" | 使用 SGLang 推理后端,数据并行 4 × TP 1 |
rollout | max_concurrent_rollouts | 256 | 最大并发 rollout 数 |
gconfig | n_samples | 4 | 每个 prompt 采样 4 条轨迹 |
gconfig | temperature | 1.0 | 采样温度 |
actor | backend | "fsdp:d4p1t1" | 使用 FSDP 训练,数据并行 4 × TP 1 |
actor | lr/eps_clip | 1.70e-5/0.4 | 学习率与 PPO 裁剪系数 |
actor | kl_ctl | 0.0 | 关闭 KL 惩罚(由 loss_mask 天然截断 prompt 部分) |
actor | use_decoupled_loss | true | 使用解耦损失(decoupled loss) |
ref | scheduling_strategy | colocation → target: actor | 参考模型与 actor 同卡共置 |
train_dataset | batch_size | 256 | 训练 batch 大小 |
注意rollout.dump_to_file: true,该选项会把 rollout 结果落盘,便于调试多轮轨迹的导出是否正确。
训练脚本结构:奖励函数、Agent 与 Workflow 三层
训练脚本 examples/multi_turn_math/gsm8k_rl_mt.py 清晰地划分为三个层次:
1. 奖励函数gsm8k_reward_fn
def gsm8k_reward_fn(result, answer): try: worker = get_math_verify_worker() return worker.verify(str(result), str(answer)) except Exception: return 0.0奖励函数直接复用了 AReaL 的MathVerifyWorker(实现在 areal/reward/init.py),它内部基于math_verify库完成答案解析与比对:
- 提取配置同时使用
ExprExtractionConfig(try_extract_without_anchor=...)与LatexExtractionConfig,既能提取显式锚定(如answer = 1)的答案,也能解析无锚定表达式; - 比对精度
precision=6,即 6 位有效数字一致即判对; - 单次 verify 调用有 5 秒线程级超时,超时或异常一律返回
0.0,避免个别难解析样本拖慢整个 rollout。
该 worker 被设计为进程级单例(get_math_verify_worker缓存全局实例),配合AsyncRewardWrapper在子进程池中异步执行。
2. 多轮 AgentMultiTurnMathAgent
class MultiTurnMathAgent: def __init__(self, gconfig, reward_fn, max_turns=2): self.gconfig = gconfig self.max_turns = max_turns self.async_reward_fn = AsyncRewardWrapper(reward_fn) async def run_agent(self, data, client: ArealOpenAI): messages = data["messages"].copy() for _ in range(self.max_turns): response: ChatCompletion = await client.chat.completions.create( messages=messages, **self.gconfig.to_openai_args_dict(), ) message = response.choices[0].message messages.append(message) reward = await self.async_reward_fn( result=message.content, answer=data["answer"] ) client.set_reward(response.id, reward) if reward == 1: break else: messages.append({ "role": "user", "content": "Your answer is either wrong or not parsable to the reward function. " "You may misunderstand the original question. " "Please carefully read the original question, check the previous errors, " "and try to answer it again.", }) return reward关键机制逐条拆解:
- OpenAI 兼容调用:通过
client.chat.completions.create发起生成,采样参数由gconfig.to_openai_args_dict()转换而来。这里的client是ArealOpenAI,一个继承自官方AsyncOpenAI的客户端,但其底层请求被路由到 AReaL 自己的推理引擎(详见 areal/experimental/openai/client.py)。 - 逐轮奖励绑定:
client.set_reward(response.id, reward)把本轮生成结果与即时奖励绑定。set_reward的实现(见client.py)会把奖励写入InteractionCache中对应交互对象,并同步更新total_reward累计值。 - 提前终止:一旦奖励为
1(答案正确)立即跳出循环,避免无效的后续轮次。 - 纠错提示:答案错误或不可解析时,追加一条固定英文提示,要求模型"仔细重读原题、检查之前的错误并重新作答",这正是多轮 RL 训练中让模型学会自我修正(self-correction)的关键信号来源。
3. 自定义 Rollout WorkflowMultiturnRLVRWorkflow
class MultiturnRLVRWorkflow(RolloutWorkflow): def __init__(self, reward_fn, gconfig, tokenizer, export_style="concat", max_turns=2): ... self.chat_template_type = "concat" if export_style == "concat" else "hf" self.agent = MultiTurnMathAgent( gconfig=gconfig.new(n_samples=1), reward_fn=reward_fn, max_turns=max_turns, ) async def arun_episode(self, engine, data): client = ArealOpenAI( engine=engine, tokenizer=self.tokenizer, chat_template_type=self.chat_template_type, ) reward = await self.agent.run_agent(data=data, client=client) stats_tracker.get(workflow_context.stat_scope()).scalar(reward=reward) client.apply_reward_discount(turn_discount=0.9) completions_with_reward = client.export_interactions(style=self.export_style) return completions_with_rewardWorkflow 是 AReaL 中"一集 rollout"的抽象,arun_episode定义单个数据样本的完整展开过程,注意三个关键点:
chat_template_type的选择:export_style == "concat"时,客户端必须以chat_template_type="concat"初始化(构造逻辑见client.py中的AsyncCompletionsWithReward),此时提示词构造走_concat_prompt_token_ids_with_parent路径——即子轮 prompt 由"父轮完整 token 序列 + 新增消息渲染的 token 增量"拼接而成,并通过 EOS 计数对齐实现跨轮 token 精确拼接。apply_reward_discount(turn_discount=0.9):多轮场景下,最后一轮的成败实际上反映了整条轨迹的质量。该方法按创建顺序倒序遍历缓存交互,把后续轮次的奖励以reward[i] += reward[i+1] * turn_discount的几何折扣方式向前传播(实现见 areal/experimental/openai/cache.py)。示例取0.9,即第 1 轮会获得第 2 轮奖励 90% 的"信用",让中间轮次也能收到远端成败信号。export_interactions(style="concat"):以concat风格导出时,InteractionCache会依据"输入消息列表严格前缀"关系构造对话树(采用最长前缀规则确定父节点),最终只返回叶子节点交互(见cache.py的export_interactions)。也就是说,一次多轮交互最终产出 1~2 个训练样本:第 1 轮答对的只有一个叶子样本;答错后走完 2 轮的,则以第 2 轮叶子为样本,但样本内通过turn_ids区分两轮输出。
concat 模式的底层原理:从对话树到训练张量
concat模式并非简单地把多轮文本拼在一起,而是有一套完整的 token 级处理逻辑,其核心实现在 areal/experimental/openai/types.py 的InteractionWithTokenLogpReward.to_tensor_dict()与 areal/experimental/openai/client.py 的_concat_prompt_token_ids_with_parent。
父链 token 拼接与对齐
_concat_prompt_token_ids_with_parent采用如下方案保证跨 tokenizer 兼容:
- 父交互的 token 序列由
input_tokens + output_tokens_without_stop组成,并人为补一个 EOS(父轮若因长度截断而缺 EOS,则补上的 EOS 会在训练时被loss_mask掩盖,不参与损失计算); - 对"父消息 + 父输出 + 本轮新增消息"整体套用 chat template,得到
all_tokens; - 统计父 token 中 EOS 的数量,在
all_tokens中找到第 N 个 EOS 的位置作为截断点; - 最终子轮 prompt =
父 tokens + all_tokens[截断点+1:],实现"父前缀精确复用、仅增量渲染新消息"。
从源码注释可以确认该方案的目的:不同模型的 chat template 在追加消息时可能增删 token(如 think token),直接重算全量 prompt 会破坏前缀一致性,而 EOS 对齐法保证拼接结果与逐轮独立渲染等价。
训练张量的构造
to_tensor_dict()负责把交互树展开成 GRPO 训练所需的张量行。对concat模式且有父节点的子交互,它会递归取父节点的logprobs / loss_mask / versions / turn_ids,再与本轮的输出拼接:
loss_mask:父轮与新增增量部分为 0,本轮输出 token 为 1,保证损失只算模型真正生成的 token;turn_ids:父轮 turn 为历史值,本轮输出 token 的 turn 为max(父turn_ids) + 1,从而在一条样本内区分不同轮次的输出,供轮次相关的统计与 mask 使用;logprobs:父轮部分直接继承父节点在生成时记录的输出 logprob,中间 padding 区域填 0。
这些张量随后通过concat_tensor_interactions聚合为批次(types.py),再交给 areal/trainer/ppo 中的 GRPO 损失计算。
两种导出风格对比
| 维度 | concat(多轮 Agent 推荐) | individual |
|---|---|---|
| 样本组织 | 构造对话树,只导出叶子 | 导出全部交互 |
| 轮次区分 | turn_ids区分 | 每条独立样本 |
| 奖励传播 | 依赖apply_reward_discount先回传 | 同样依赖 discount 为每轮赋奖励 |
| 适用场景 | 多轮对话、工具调用、Agent 轨迹 | 单轮 RLVR |
export_style合法值校验在MultiturnRLVRWorkflow.__init__中完成,非法值会直接抛出ValueError。此外cache.py对concat导出还有一道保护:只有当所有交互的chat_template_type == "concat"时才允许按concat导出,否则报错并提示改用individual——原因是某些 tokenizer 的 template 会增删 think 等 token,无法可靠构造前缀一致的对话树。
奖励的异步化与容错:AsyncRewardWrapper
多轮场景下奖励计算发生在 rollout 热路径中,MultiTurnMathAgent将同步奖励函数包装为AsyncRewardWrapper(实现在 areal/api/reward_api.py),它提供了三层保障:
- 进程池隔离:奖励函数通过
ProcessPoolExecutor在子进程中执行(max_workers默认取(CPU 数 / GPU 数) // 2),避免math_verify等解析逻辑阻塞事件循环; - 超时控制:单次奖励计算默认 15 秒超时,超时后自动重试,最多 3 次;全部失败返回
0.0; - 进程池自愈:检测到
BrokenProcessPool时自动重建执行器并重试,最后一次失败则上抛异常。
此外,AsyncRewardWrapper的进程池按max_workers键全局共享,并通过atexit注册清理钩子,避免子进程泄漏。
运行与验证:reward 曲线与统计
示例目录下提供了训练过程的奖励曲线图 reward_curve.png,用于直观观察多轮训练的收敛情况。训练过程中,arun_episode会通过stats_tracker.get(workflow_context.stat_scope()).scalar(reward=reward)把每条轨迹的最终奖励上报到统计系统,最终由 areal/utils/stats_logger.py 负责输出(可配置接入 wandb 或本地日志)。
关于本地复现,需注意以下几点前提:
- 配置文件默认单机 8 卡 FSDP + SGLang 推理,实际运行时需按本机 GPU 资源调整
cluster.n_gpus_per_node与 rollout/actor 的d4p1t1并行度; - 模型权重通过
actor.path: Qwen/Qwen2.5-1.5B-Instruct从 Hugging Face 拉取(AReaL 也提供hf_utils.load_hf_tokenizer统一加载 tokenizer,见 areal/utils/hf_utils.py); - 若要在其他规模模型上训练,只需替换
actor.path、ref.path、sglang.model_path、vllm.model与tokenizer_path五处模型引用。
从本示例出发:扩展方向
examples/multi_turn_math是 AReaL Agentic RL 能力的最小完整演示,它清晰展示了"OpenAI 兼容 Agent 循环 + 对话树导出 + 回合折扣奖励"这条技术主线。基于同样的机制,仓库中还有更复杂的 Agent 化示例可以对照学习:
- examples/search_agent:引入搜索工具的 Agent,其
concat导出同样依赖对话树; - examples/scaffolding:RLVR 脚手架,展示了基于工具的复杂轨迹构造;
- docs/en/tutorial/agentic_rl.md:AReaL 官方 Agentic RL 指南,对奖励归一化(group 级 vs batch 级)与
concat模式的适用边界有更系统的说明。
小结
本文完整覆盖了 AReaL 多轮 GSM8K 数学 Agent 训练的五个层次:配置项差异(export_style与agent_run_args.max_turns)、训练脚本结构(奖励函数 / Agent / Workflow 三层)、ArealOpenAI 客户端(逐轮奖励绑定与交互缓存)、concat 模式原理(EOS 对齐拼接、对话树叶子导出、turn_ids 训练张量)、奖励工程(进程池异步化与回合折扣)。其中每一步都能在仓库源码中找到精确实现(client.py、cache.py、types.py、reward_api.py),读者可循着这些路径深入阅读,将其改造为适合自己任务的多轮 Agent 训练方案。
【免费下载链接】AReaLThe RL Bridge for LLM-based Agent Applications. Made Simple & Flexible.项目地址: https://gitcode.com/GitHub_Trending/are/AReaL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考