verl Agentic RL 训练全指南:基于异步 Rollout、多轮对话与工具调用的智能体强化学习实践
【免费下载链接】verlverl/HybridFlow: A Flexible and Efficient RL Post-Training Framework项目地址: https://gitcode.com/GitHub_Trending/ve/verl
导读
本文以 verl(HybridFlow)官方文档 docs/start/agentic_rl.rst 为主线,系统讲解如何在 verl 中构建 Agentic RL(面向智能体的强化学习)训练流水线:通过基于 Ray Actor 的 Server 端异步 Rollout 机制消除工具调用等待期间的 GPU 空转,通过多轮对话与工具调用让模型学会使用外部工具,并通过 LangGraph 等 Agent 框架扩展自定义智能体循环。读完本文,你将掌握从数据集预处理、工具注册、训练脚本配置到 trace 调试的完整实操链路,并理解 token 级 API 设计、负载均衡、delta 式分词等底层原理。
一、Agentic RL 的三大能力支柱
Agentic RL 的目标是用强化学习提升后端模型(policy 模型)作为 Agent 完成任务的能力。与单轮问答式 RL 不同,Agent 需要与环境反复交互:发起工具调用、等待工具结果、继续推理、再调用……这个过程引入了一个关键矛盾——如果按同步方式串行执行,模型在等待工具返回结果时 GPU 会完全空闲,训练吞吐被拖垮。
为此,verl 在训练过程中围绕三个方向构建了一整套能力,这也是本仓库中 Agentic RL 的全部内涵:
- 基于 Server 的异步 Rollout(Server-based asynchronous rollout):用 asyncio 协程机制并发调度每个 rollout 请求,避免 GPU 空转。
- 多轮对话与工具调用(Multi-turn conversations and tool calls):让模型能在对话过程中按需调用工具并消化工具返回结果。
- 基于 LangGraph 的 Agent 框架(LangGraph-based Agent):提供可插拔的 Agent 循环抽象,支持用户自定义复杂 Agent 行为。
下面依次深入每一块,先讲系统原理,再给可复现的实操步骤。
二、Server 端异步 Rollout:原理与架构
2.1 为什么必须把推理引擎与 Agent 拆开
Agent 通过各类工具调用与环境交互,为了不让 GPU 在等待工具返回结果时空闲,verl 引入基于 asyncio 的协程路由机制,将每个 rollout 请求异步执行。异步 rollout 要求推理引擎(Server)与 Agent(Client)在架构上分离,其设计目标有两点:
- 负载均衡:在多 GPU 间平衡负载,降低长尾请求(long-tail requests)对整体性能的影响。为此,verl 以 recipe 的形式实现了流式调度能力
recipe/stream_mode。 - 隔离性:防止 Agent 特有功能(如 trace 追踪)影响推理引擎本身的稳定与性能。
2.2 系统组件
整个异步 rollout 体系由三个核心组件构成,职责划分如下:
| 组件 | 角色 |
|---|---|
| AgentLoop | 客户端(Client),实现 Agent 功能,即用户定义的智能体循环 |
| LLMServerClient | 推理网关(Inference gateway),为 AgentLoop 提供generate接口 |
| AsyncServer | 服务端(Server),每个实例连接推理引擎的一个 DP(数据并行)组 |
其中,AgentLoop的核心抽象位于 verl/experimental/agent_loop/agent_loop.py,它定义了三个层次的类:
AgentLoopBase:协程化的抽象基类,用户只需实现run方法;AgentLoopWorker:并行运行 agent loop 协程的 worker 类;AgentLoopManager:并行管理多个 worker 的 manager 类。
从源码注释可以看到,AgentLoopManager是 verl 中一个可完全替换的 Agent 框架实现,NVIDIA Nemo-Gym、AWS Bedrock AgentCore、SWE-agent 等外部框架都可以作为替代品接入。
2.3 为什么用 token 级的 "generate" 接口而不是 Chat Completion API
Client 与 Server 之间基于 Ray Actor 使用generate函数通信,而非标准的 Chat Completion API,原因在于token 与文本之间的转换是不可逆的:
- 例如,从
"<think>"这串文本转换出的 token,与 LLM 实际生成的 token 往往不是同一组; - 训练阶段必须严格使用 LLM 推理产出的 token,否则 advantage(优势函数)计算会失真,进而影响模型性能;
- 由 Server 提供 token 级 API,可以让 Client 维护"工具调用产生的文本"与"LLM 返回的 token"之间的对应关系,从而为训练输出正确的 token。
这套设计在 docs/advance/agent_loop.rst 中有更完整的论述:文档指出,几乎所有 Agent 框架(LangGraph、CrewAI、LlamaIndex 等)都通过 OpenAI Chat Completion API 调用 LLM 并保存消息历史,但 verl 在 DAPO 单轮训练和 retool 多轮训练的实践中发现,对最终消息应用 chat template 后得到的 token_ids,与逐轮拼接 prompt_ids 和 response_ids 得到的 token_ids 并不相等。这种不一致对 serving/agent 系统无伤大雅,但对 RL 训练是致命的——它会让轨迹偏离 policy 模型的真实分布,甚至导致单轮 PPO 训练不收敛。工具解析器改写内容、decode-encode 往返损失是产生不一致的两大来源。
2.4 推理引擎适配:SGLang 与 vLLM 的差异
AsyncServer向上层统一提供generate函数,针对 SGLang 和 vLLM 各有一套实现,以屏蔽底层差异:
| 引擎 | 实现方式 |
|---|---|
| SGLang | 使用 SGLang 引擎的async_generate接口,该接口位于每个 TP 组的首张 GPU 上,因此AsyncServer需要通过 Ray Actor远程调用async_generate |
| vLLM | 使用 vLLM 引擎的generate接口,该接口通过ZMQ与 TP 组内的 GPU 通信,因此可以在AsyncServer进程中直接调用 |
2.5 AgentLoopWorker 的调度细节
在 verl/experimental/agent_loop/agent_loop.py 的AgentLoopWorker.generate_sequences实现中可以看到完整的并发调度逻辑:
- 默认假设为单轮 Agent:如果 batch 中没有
agent_name字段,则用config.agent.default_agent_loop填充; - 为 batch 中每个样本
asyncio.create_task一个_run_agent_loop协程,再通过asyncio.gather并发执行; - 采样参数(
temperature、top_p、top_k、repetition_penalty=1.0、logprobs)统一从 rollout 配置中提取,验证(validate)阶段会覆盖为val_kwargs中的贪婪采样参数。
一个值得注意的调度技巧(来自官方文档的 tip):如果AgentLoopWorker的数量等于 batch_size,那么每个 worker 恰好负责一条 prompt,多协程并发调度可以让 GPU 始终处于忙碌状态。
2.6 一个 PPO step 的完整 rollout 时序
综合 docs/advance/agent_loop.rst 的描述,一个 PPO step 的 rollout 阶段分为以下 8 步:
PPOTrainer从数据集中采样一个 batch,调用AgentLoopManager.generate_sequences;AgentLoopManagerwake_up所有异步 LLM Server 实例,将推理引擎(vLLM/SGLang)与训练引擎(FSDP/Megatron-LM)之间的权重同步;AgentLoopManager将 batch 切分为多个 chunk,分发给AgentLoopWorker;AgentLoopWorker收到 chunk 后,为每条 prompt 实例化一个用户自定义的AgentLoopBase实例,运行run协程直至结束,得到AgentLoopOutput;- Agent loop 中需要 LLM 生成时,调用
LLMServerClient.generate(prompt_ids); LLMServerClient第一轮选择请求数最少的 Server 实例发送请求(后续轮次会发送到同一个 Server 实例,即 sticky session);AsyncLLMServer收到请求后,通过 IPC/RPC 与 model_runner 交互并生成响应(vLLM 与 SGLang 略有差异,见 2.4 节);- 所有 AgentLoopWorker 完成全部 prompt 后,
AgentLoopManager汇总结果返回PPOTrainer,然后sleep所有 Server 实例——释放 KV cache 并把权重 offload 到 CPU 内存。
2.7 AgentLoopOutput 与 response_mask
AgentLoopOutput是 agent loop 的标准化输出(见 agent_loop.py 中AgentLoopOutput类定义),包含以下核心字段:
| 字段 | 含义 |
|---|---|
prompt_ids | Prompt token ids |
response_ids | 响应 token ids,同时包含 LLM 生成 token 与工具响应 token |
response_mask | 响应掩码,1 表示 LLM 生成的 token,0 表示工具响应 token |
response_logprobs | 响应 token 的 log 概率 |
multi_modal_data | 多模态工具返回的图片/视频/音频数据 |
reward_score | 轨迹的奖励分数 |
num_turns | 对话轮数(含 user、assistant、tool) |
response_mask是多轮训练的关键:它保证 loss 只覆盖 LLM 自己生成的 token,工具返回的内容虽然参与 attention,但不参与 loss 计算。在_agent_loop_postprocess的注释中给出了一个直观的例子:当响应序列为[5,6,7,(tool start)8,9(tool end),10,11,12]时,response_mask形如[1,1,1,(tool start),0,0(tool end),1,1]。
另外,多轮 agent loop 输出的数据还会额外附带num_turns、turn_scores、tool_rewards等字段(由ToolAgentLoop.run写入extra_fields),供奖励设计与调试使用。
三、快速上手:GSM8K 单轮 Agentic RL(异步 Rollout)
3.1 准备数据集与模型
按照 GSM8K 示例 准备数据集和模型 checkpoint 即可。核心要点:
- 数据集需预处理为 parquet 格式;
- 模型 checkpoint 需为 HuggingFace 格式(本文示例为
Qwen/Qwen3-8B)。
3.2 开启 Agent Loop 的两个必要配置
使用 agent loop 必须设置两个选项:
data.return_raw_chat: True actor_rollout_ref.rollout.mode: asyncdata.return_raw_chat=True:让数据集保留原始 chat 消息结构([{"role": ..., "content": ...}]),因为 agent loop 需要基于消息列表而非拼接文本进行交互;actor_rollout_ref.rollout.mode=async:开启异步 rollout 模式,走 server-based 异步推理路径。
3.3 启动训练
示例脚本默认使用 SGLang 推理引擎,也可以通过修改rollout_name切换到 vLLM:
bash examples/grpo_trainer/run_qwen3_8b_fsdp.sh以 examples/grpo_trainer/run_qwen3_8b_fsdp.sh 为例,该脚本默认配置了 GRPO + FSDP 训练 Qwen3-8B,关键参数包括:
INFER_BACKEND(默认vllm):rollout 后端,可选vllm | sglang | trtllm;actor_rollout_ref.rollout.name:对应推理后端名;actor_rollout_ref.rollout.tensor_model_parallel_size:TP 大小(GPU 默认 2,NPU 默认 4);actor_rollout_ref.rollout.n=5:每个 prompt 采样 5 条轨迹(GRPO 需要);data.max_prompt_length/data.max_response_length:输入输出长度上限;data.filter_overlong_prompts=True、data.truncation='error':超长 prompt 的处理策略。
该脚本同时支持 NVIDIA GPU 与 Ascend NPU(通过torch_npu自动探测 DEVICE),NPU 下会自动启用参数/优化器 offload 与 sequence parallel。
四、多轮对话与工具调用
4.1 工具与配置文件准备
多轮工具调用 Rollout 的准备工作参考 Multi-turn Rollout Support。启用多轮 rollout 的基础配置:
actor_rollout_ref: rollout: multi_turn: True name: "sglang"这会激活 SGLang 引擎用于 rollout 阶段的多轮交互。
4.2 Tool Agent Loop 与 agent_name 字段
Tool Agent Loop 相比单轮 agent 多了一个额外要求:在数据集中添加agent_name字段。rollout 时会根据该字段选择使用tool_agent_loop还是single_turn_agent(默认)。
在源码 agent_loop.py 的_agent_loop_registry中可以看到注册机制:tool_agent与single_turn_agent都是注册名,AgentLoopWorker._run_agent_loop会断言agent_name in _agent_loop_registry,否则直接报错并列出所有已注册的 agent loop。用户也可以通过rollout.agent.agent_loop_config_path加载自定义 agent loop 配置(见AgentLoopWorker.__init__中的OmegaConf.load逻辑)。
4.3 完整实操:GSM8K + 工具 + MLflow trace
官方文档给出了一条完整的端到端命令序列:
# 安装 mlflow 以查看 toolcall 和 llm trace pip install mlflow # 下载并预处理 GSM8K 数据集到 ~/data/gsm8k/,并添加 "agent_name" 字段 python examples/data_preprocess/gsm8k_tool_agent_loop.py # 启动带工具调用并启用基于 mlflow 的 trace 的训练(便于调试 rollout 细节) bash examples/sglang_multiturn/run_qwen2_5_3b_gsm8k_tool_agent_mlflow_fsdp.sh # 训练完成后,启动 mlflow server 查看 trace mlflow ui -h 0.0.0.0 -p 5000 --backend-store-uri sqlite:////tmp/mlruns.db # 然后在浏览器中打开 http://<your ip address>:5000 查看 trace这里以 examples/data_preprocess/gsm8k_tool_agent_loop.py 为例解读数据预处理的关键逻辑:
- 从
openai/gsm8k加载数据,提取####后的最终答案作为 ground truth; - 每条数据写入
agent_name: "tool_agent"字段; - system prompt 要求模型"逐步推理后再调用
calc_gsm8k_reward工具,在生成最终答案前至少调用一次并根据需要修正答案,最终以#### <answer>格式输出"; extra_info.tools_kwargs中为calc_gsm8k_reward工具注入了create_kwargs: {"ground_truth": solution},实现按样本注入工具参数(verifed 工具在创建时需要知道该题的正确答案)。
注意:训练过程中,因为模型有时无法生成正确的 toolcall 标签,控制台会输出"Failed to decode tool call"错误信息,这不代表训练异常,属于正常现象。
关于 trace 功能的更多细节,参见 Rollout trace。
4.4 工具注册的两种方式
方式一:基于BaseTool的有状态工具
基于 verl/tools/base_tool.py 的BaseTool实现自己的工具,然后在 YAML 配置文件中指定:
tools: - class_name: "" config: type: native tool_schema:并通过 rollout 配置启用:
actor_rollout_ref: rollout: tool_kwargs: tools_config_file: <path_to_tool_yaml_file>BaseTool工具拥有完整的create/execute/release生命周期。每个轨迹(trajectory)都会创建独立的工具实例,适合需要按轨迹隔离状态的场景:沙箱虚拟机、临时目录、数据库行等。它支持从数据集注入tools_kwargs(如 4.3 节中的calc_gsm8k_reward例子)。
如果工具会产出多模态输入,应在execute()实现中返回多模态输入列表。图像和视频需要在返回前完成处理,例如 Qwen2.5-VL 场景:
from verl.utils.dataset.vision_utils import process_image, process_video async def execute(self, ...) -> tuple[str | dict[str, Any], float, dict]: img1 = process_image(img1) video1 = process_video(video1) # 注意:vLLM 中 (image | video) 的 key 是 ("image" | "video") 而非 ("images" | "videos") return ToolResponse(image=[img1, ...], video=[video1, ...], text="..."), 0, {}同时需要在数据集配置中设置return_multi_modal_inputs: False,让 rollout 阶段动态处理多模态输入(详见 4.7 节)。
方式二:基于@function_tool的无状态函数工具
对于无状态工具,定义一个完整的BaseTool子类加 YAML schema 显得过重。verl 提供了@function_tool装饰器(见 verl/tools/function_tool.py),可以直接把普通 Python 函数注册为工具,schema 推断交给transformers.utils.get_json_schema(读取函数签名与 Google 风格 docstring):
actor_rollout_ref: rollout: mode: async multi_turn: enable: True format: hermes # 或你的模型 chat template 支持的其他格式 function_tool_path: path/to/your_tools.py agent: default_agent_loop: tool_agent工具定义示例(来自 multiturn.rst 与 tests/experimental/agent_loop/function_tool_examples.py):
from verl.tools.function_tool import function_tool @function_tool def get_weather(city: str) -> dict: """Get the current weather for a city. Args: city: The city to look up, e.g. "Tokyo" or "San Francisco". """ return {"temperature_c": 17.3, "condition": "drizzle"} @function_tool("calculator") # 显式命名会覆盖函数名 def calculator(expression: str) -> str: """Evaluate a Python-style arithmetic expression. Args: expression: A Python-style arithmetic expression, e.g. "(3+4)*5". """ return str(eval(expression, {"__builtins__": {}}, {}))schema 推断规则:
- 参数类型来自函数的类型注解,支持泛型与联合类型:
list[T]/dict[K, V]、Optional[X]/X | None(生成nullable)、int | float(生成 JSON["integer", "number"])、Literal["a", "b"](生成enum); - 每个参数的描述来自 docstring 的
Args:部分; - 没有默认值的参数标记为
required; - 函数可以是 sync 或 async,sync 函数通过
asyncio.to_thread派发,不会阻塞事件循环; *args/**kwargs无法在 JSON Schema 中表达,注册时会直接报错,可变长输入请改用param: list[T];- 也可以给装饰器传
schema=参数,完全绕开推断,直接提供自定义的OpenAIFunctionToolSchema(或同构 dict)。
返回值会被统一归一化:
| 返回值 | 归一化结果 |
|---|---|
str | 包装为ToolResponse(text=...) |
dict | JSON 序列化进ToolResponse(text=...) |
ToolResponse | 原样透传 |
(response, reward)或(response, reward, metrics) | 元组解包,Nonereward / metrics 视为0.0/{} |
function_tool_path与tool_config_path可以同时设置,AgentLoopWorker启动时会将两者合并进同一个注册表,两个路径下重名会直接报错。RLHFDataset复用同一加载器,因此 prompt 长度过滤看到的工具 schema 与 rollout 实际使用的完全一致。
4.5 ToolAgentLoop 的状态机
verl/experimental/agent_loop/tool_agent_loop.py 中ToolAgentLoop用状态机驱动整个多轮循环,四个状态依次流转:
PENDING -> GENERATING -> PROCESSING_TOOLS -> GENERATING -> ... -> TERMINATEDPENDING:通过 Continuous Token builder 构建初始 prompt token ids(ct_build_initial_tokens),多模态输入在此阶段被渲染进 token 序列;GENERATING:调用server_manager.generate生成模型响应。此阶段会把工具解析器的 stop token 注入stop_token_ids,让生成在每次工具调用后自动停止,然后用ct_merge_assistant_token把新生成的 assistant token 合并进运行时 token 序列,最后用tool_parser.extract_tool_calls解析是否包含工具调用——有工具调用则进入PROCESSING_TOOLS,否则终止;PROCESSING_TOOLS:并发执行工具调用(上限为max_parallel_calls),把{"role": "tool", ...}消息追加进对话历史,用ct_merge_context_msg合并工具响应 token,随后回到GENERATING;TERMINATED:触发条件包括response_mask长度达到response_length、assistant_turns达到max_assistant_turns、user_turns达到max_user_turns、或本轮没有工具调用。
相关配置参数(rollout.multi_turn下):
| 参数 | 含义 |
|---|---|
max_user_turns | 用户轮次上限 |
max_assistant_turns | 助手轮次上限 |
max_parallel_calls | 单轮最多并行工具调用数 |
max_tool_response_length | 工具响应文本最大长度 |
tool_response_truncate_side | 截断方向:left/right/ 其他(两侧截断) |
format | 工具调用解析格式,如hermes |
工具执行遵循两条契约(见_call_tool):FunctionTool无生命周期、直接调用;BaseTool子类走create -> execute -> release完整生命周期。工具名不存在、参数 JSON 解析失败、执行抛异常时,都会返回错误文本作为ToolResponse并记 warning,而不会中断训练。
4.6 多轮分词:delta-based tokenization 与 tokenization sanity check
多轮 rollout 的分词有一个天然难点:对完整消息列表应用 chat template 并分词后,得到的是一个扁平的 token 列表,无法区分哪些 token 属于 assistant 消息,也就无法正确构造 loss mask。
verl 采用delta 分词策略:每次 LLM 生成新消息时:
- 对历史消息
messages[:i]应用 chat template(add_generation_prompt=True,排除 assistant 提示符以免计入 loss); - 对包含最新消息的
messages[:i+1]再应用一次 chat template; - 只对两次序列化字符串之间的delta做分词,并给这段 token 打上
loss_mask = 1。
# 使用 tokenizer 时 prev = tokenizer.apply_chat_template(messages[:i], add_generation_prompt=True, tokenize=False) curr = tokenizer.apply_chat_template(messages[:i+1], add_generation_prompt=False, tokenize=False) token_ids += tokenizer.encode(curr[len(prev):], add_special_tokens=False) loss_mask += [1] * len(token_ids) # 只掩码新增的 assistant token多模态场景(使用 processor 时)逻辑相同,只是把tokenizer换成processor并带上 images/videos:
prev = processor.apply_chat_template(messages[:i], add_generation_prompt=True, tokenize=False) prev_model_inputs = processor(text=prev, images=images, videos=videos, return_tensors="pt")[0].tolist() curr = processor.apply_chat_template(messages[:i+1], add_generation_prompt=False, tokenize=False) curr_model_inputs = processor(text=curr, images=images, videos=videos, return_tensors="pt")[0].tolist() token_ids += curr_model_inputs["input_ids"][len(prev_model_inputs["input_ids"]):] loss_mask += [1] * len(token_ids)为了防御未来模型的 chat template 变化导致 delta 与完整分词结果悄然不一致,verl 默认在每个 rollout 结束时对两种分词结果做对比校验(tokenization sanity check),通过actor_rollout_ref.rollout.multi_turn.tokenization_sanity_check_mode配置三种模式:
| 模式 | 行为 |
|---|---|
strict(默认) | 严格比较 delta 与完整分词结果,任何差异都输出 warning |
ignore_strippable | 忽略空白字符(\n、\t、\r、空格)差异,仍检查有意义的文本差异 |
disable | 完全关闭校验,仅在确认差异无害时使用 |
actor_rollout_ref: rollout: multi_turn: tokenization_sanity_check_mode: "ignore_strippable" # "disable" | "ignore_strippable" | "strict"如果看到如下 warning,请去日志中核对不匹配的子串:
Inconsistent training and inference tokenization detected. This may lead to unexpected behavior during training. Please review your chat template to determine if this is intentional. For more information, refer to the multiturn README.md.特殊情形:部分模型(如 Qwen/QwQ-32B 与 Qwen3 系列)在 chat template 渲染时会移除内部推理内容,导致消息内容跨轮次变化,delta 分词失效。此时 verl 退回到固定 base conversation(只含一条 system 与一条 user 消息,不含 assistant 与推理内容,跨轮次保持一致):
BASE_CHAT_HISTORY = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "I am a user."} ] prev = tokenizer.apply_chat_template(BASE_CHAT_HISTORY, add_generation_prompt=True, tokenize=False) curr = tokenizer.apply_chat_template([*BASE_CHAT_HISTORY, messages[i]], add_generation_prompt=False, tokenize=False) token_ids += tokenizer.encode(curr[len(prev):], add_special_tokens=False) loss_mask += [1] * len(token_ids)该方法对 Qwen3 系列有效。Qwen/QwQ-32B 的 chat template 目前存在一个 bug,修复 PR 尚未被采纳,需要先下载修复版模型:
pip install huggingface_hub hf download Qwen/QwQ-32B --revision refs/pr/81训练与推理模板不一致:即使修复了 delta 不匹配,推理端 chat template 移除推理内容仍会带来新矛盾——训练使用完整推理内容而推理不使用。为规避不确定性,verl 默认训练与 rollout 都使用完整响应(含推理)。但这样做的代价是:长推理内容容易撑爆上下文窗口(多轮场景尤其明显),且 rollout 与生产环境(默认 chat template 会丢弃历史推理)不一致。如需缓解,可开启:
actor_rollout_ref.rollout.multi_turn.use_inference_chat_template = True4.7 数据集中的多模态输入处理
RLHFDataset通过return_multi_modal_inputs标志控制多模态输入是否预处理进每个样本:
return_multi_modal_inputs: True(默认):数据集会预处理并在每个样本中附带multi_modal_inputs字典(processor 产出的模型就绪表示,如图像/视频张量)。适合单轮或 SFT 式训练,此时模型期望 batch 中所有模态齐全;return_multi_modal_inputs: False:数据集不包含multi_modal_inputs字段。推荐用于多轮 RL 或工具增强 rollout,因为 rollout 过程中模型可能动态生成新的多模态输入,避免 batch 中数据冲突或冗余。
五、Agent 框架:基于 LangGraph 的自定义 Agent
5.1 系统架构与组件
当内置的ToolAgentLoop不够用时,verl 提供 Agent 框架扩展层。其设计哲学(来自 docs/advance/agent_loop.rst)是:
- 目标:可插拔的用户自定义 agent loop;为不同推理框架提供标准请求 generate API;在多个推理 Server 之间提供请求级负载均衡;
- 非目标:不规定工具如何定义、如何调用。
核心组件如下:
| 组件 | 角色 |
|---|---|
| ChatModel | LangChain 的 LLM 对象,用于适配LLMServerClient提供的 "generate" API |
| ReactAgentLoop | Agent 适配层,默认支持一个朴素的 LangGraph Agentic;可以派生新类来支持用户自定义 Agent,需要实现run函数来完成 Agent 调用 |
| AsyncServer | Server,每个实例连接推理引擎的一个 DP 组 |
5.2 AgentLoopBase:用户唯一需要实现的接口
AgentLoopBase是 agent loop 的抽象基类,run是用户唯一需要实现的方法。给定 prompt 消息(格式为[{"role": "user"}, {"content": "..."}])和采样参数,用户可以在run中做任何事:调用 LLM generate API、调用工具(web search、数据库查询、代码沙箱等)、与环境交互、reflection……:
class AgentLoopBase(ABC): @abstractmethod async def run(self, sampling_params: dict[str, Any], **kwargs) -> AgentLoopOutput: """Run agent loop to interact with LLM server and environment. Args: sampling_params (Dict[str, Any]): LLM sampling params. **kwargs: dataset fields from `verl.utils.dataset.RLHFDataset`. Returns: AgentLoopOutput: Agent loop output. """ raise NotImplementedError在AgentLoopBase.__init__中可以拿到LLMServerClient(即server_manager),用户随时可以通过LLMServerClient.generate与 LLM 交互。run最终返回AgentLoopOutput,其中的prompt_ids、response_ids、response_mask会被用于 RL 训练。
5.3 LLMServerClient:负载均衡与 sticky session
LLMServerClient充当多个AsyncLLMServer实例的代理,提供两个关键能力:
- 负载均衡(load balance):第一轮选择请求数最少的 Server 实例发送请求;
- 粘性会话(sticky session):将
request_id绑定到 Server 实例,后续轮次同一request_id的请求始终发往同一 Server 实例,从而保证同一轨迹的多轮生成落在同一份 KV cache / 权重副本上。
LLMServerClient被传入AgentLoopBase.__init__,用户通过LLMServerClient.generate(request_id, prompt_ids, sampling_params)获取 response_ids。
5.4 AsyncServerBase:接入新推理引擎的扩展点
其他推理引擎可以通过实现AsyncServerBase轻松接入。抽象类定义了两个必须实现的 API:
class AsyncServerBase(ABC): @abstractmethod async def chat_completion(self, raw_request: Request) -> JSONResponse: """OpenAI chat completion API.""" raise NotImplementedError @abstractmethod async def generate(self, prompt_ids: list[int], sampling_params: dict[str, Any], request_id: str) -> list[int]: """Generate response ids given prompt ids.""" raise NotImplementedErrorvLLM 与 SGLang 的AsyncLLMServer都已官方支持并经过充分测试,二者都实现了这两个 API。它们在进程模型上的差异(来自 docs/advance/agent_loop.rst):
- vLLM:Async LLM Engine 与 Server 同进程运行,ModelRunner 与 FSDP/Megatron-LM worker 同进程运行,二者通过ZeroMQ通信;Server 收到请求后直接调用 engine 生成 response_ids;
- SGLang:Async LLM Engine 与 FSDP/Megatron-LM 的 worker-0 同进程运行,并 spawn 多个子进程作为 ModelRunner,同样通过 ZeroMQ 通信;Server 收到请求后远程调用 worker-0获取 response_ids。
5.5 自定义 agent loop 的注册与加载
新 agent loop 类通过register(agent_name)装饰器注册:
@register("my_agent") class MyAgentLoop(AgentLoopBase): ...注册表_agent_loop_registry会记录{"_target_": "module.ClassName"},AgentLoopWorker运行时通过hydra.utils.instantiate实例化。数据集中的agent_name字段(如"tool_agent")就是查这张注册表的 key。
更多细节可参考仓库中 LangGraph agent 的 recipe 文档(recipe/langgraph_agent/example/README.md)以及 tests/experimental/agent_loop/ 下的测试用例,例如 test_basic_agent_loop.py、test_call_tool_on_cpu.py。
六、工程落地要点与注意事项
6.1 训练正确性:token 一致性是生命线
- 训练阶段必须使用 LLM 实际生成的 token 而非文本重编码结果,否则 advantage 计算失真(见 2.3 节);
response_mask决定 loss 范围,工具 token 的 mask 必须为 0;- 多轮场景务必关注 tokenization sanity check 的 warning,出现
Inconsistent training and inference tokenization detected时需检查 chat template。
6.2 上下文长度与模板一致性
- 长推理内容 + 多轮交互很容易超过上下文窗口,必要时开启
use_inference_chat_template=True让 rollout 与生产环境对齐; - Qwen/QwQ-32B 需使用
refs/pr/81修订版模型(见 4.6 节)。
6.3 工具状态管理
- 需要按轨迹隔离状态(沙箱、临时目录、DB 行)或注入
tools_kwargs→ 用BaseTool+tool_config_path; - 无状态工具 → 用
@function_tool+function_tool_path,零生命周期开销; - 两套工具可同时使用,但注意名称冲突会在启动时报错。
6.4 性能与调试
- 异步 rollout 让 GPU 在工具调用期间持续工作,
AgentLoopWorker数量等于 batch_size 时每个 worker 恰好负责一条 prompt(见 2.5 节); - 训练日志中出现
"Failed to decode tool call"是正常现象,不代表训练异常; - 使用 mlflow trace(
mlflow ui -h 0.0.0.0 -p 5000 --backend-store-uri sqlite:////tmp/mlruns.db)可视化多轮对话与工具调用细节;trace 功能的完整机制见 Rollout trace。
参考文档索引
| 主题 | 仓库路径 |
|---|---|
| Agentic RL 官方指南(本文主文档) | docs/start/agentic_rl.rst |
| Agent Loop 内部设计 | docs/advance/agent_loop.rst |
| 多轮 Rollout 支持 | docs/sglang_multiturn/multiturn.rst |
| Rollout trace | docs/advance/rollout_trace.rst |
| GSM8K 示例 | docs/examples/gsm8k_example.rst |
| Agent Loop 源码 | verl/experimental/agent_loop/agent_loop.py |
| Tool Agent Loop 源码 | verl/experimental/agent_loop/tool_agent_loop.py |
| 函数工具定义 | verl/tools/function_tool.py |
| 工具基类 | verl/tools/base_tool.py |
| GSM8K 工具化数据预处理 | examples/data_preprocess/gsm8k_tool_agent_loop.py |
| GRPO 训练示例脚本 | examples/grpo_trainer/run_qwen3_8b_fsdp.sh |
| Agent Loop 测试用例 | tests/experimental/agent_loop/ |
【免费下载链接】verlverl/HybridFlow: A Flexible and Efficient RL Post-Training Framework项目地址: https://gitcode.com/GitHub_Trending/ve/verl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考