这篇文章对应视频:【[Agentic RL] 12 verl infra AgentLoop 基本概念及流程,AgentLoopManager,hybrid训练与推理】(BV135zrBaEEU)。
如果你已经看完我在上一篇里写的 “Agent Loop 为什么需要 async rollout” 与 “response_mask 基本概念”,那么这篇就是 infra 深挖版:把 verl 的 AgentLoop 体系从“能用”讲到“你能改、能调、能排障”。
你看完应该能回答这些工程问题:
AgentLoopManager / Worker / AgentLoop / AsyncLLMServerManager各自负责什么,边界怎么划?- 为什么 async rollout 不是优化项,而是 multi-turn tool use 的必要条件?它和 vLLM 的 continuous batching 怎么配合?
sticky session为什么必须有?它和 prefix cache、load balancing 是什么关系?- “hybrid 推训”到底在复用什么资源?
wake_up / sleep在做什么?什么时候应该直接用 standalone 部署?
系列导航:
关联阅读(建议顺序):
- 先建立 AgentLoop 的基本直觉(async + 状态机 + mask)
- token-in-token-out:为什么多轮对话里 encode/decode 不一致会直接把 RL 训崩
- 理解 KV cache 与吞吐瓶颈:你才知道 hybrid 在省什么
- 下一篇:把 AgentLoop 和 Ray trainer 的代码路径串起来
配套仓库(你本地已下载)里,本文主要对齐这些笔记(按推荐顺序):
- 架构鸟瞰(四层抽象 + hybrid wake_up/sleep):
/Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/agent/agent_loop_arch.ipynb
- 细节拆解(async vs sync、状态机、sticky session、output 结构、reward 前置):
/Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/agent/agent_loop_details.ipynb
- 代码入口(指向 verl 源码的 agent_loop.py):
/Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/agent/agent_loop_code.ipynb
- GPU/Replica 配置(num_replicas vs engine internal DP):
/Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/agent/agent_loop_config.ipynb
- 推理模式(inference-only agent loop):
/Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/agent/agent_loop_inference.ipynb
0. 一句话定位:AgentLoop 是“可训练的 LangChain”,区别在于你必须保留 token 级轨迹
很多人第一次看 verl 的 agent loop,会觉得:
这不就是把 LangChain/LangGraph 的 ReAct 流程编排搬进来了吗?
某种意义上没错(agent_loop_inference.ipynb 也明确说推理模式下就是 react)。但 RL 训练让它变得更“硬核”:
- 你不仅要“跑通流程”,还要产出可用于优化的轨迹数据:
- token ids
logp对齐所需的信息(以及至少能重算)response_mask(区分 action token vs observation token)- turn/reward 的挂载位置与口径
- 你必须在 infra 层面解决:
- 工具 IO 的长尾延迟(straggler)
- 多轮请求的路由一致性(sticky session)
- 推理与训练对同一组 GPU 显存的争抢(hybrid)
所以这篇文章会更像“系统设计与排障手册”,而不是算法推导。
1. 四层抽象:谁负责“怎么聊”,谁负责“怎么调度”,谁负责“怎么生成”
verl 的一个很工程化的拆分是:把 “Agent” 这件事拆成 4 层,每一层只负责一件事(agent_loop_arch.ipynb):
AgentLoop:只关心 逻辑(怎么多轮对话、怎么决定调用工具、怎么终止)。AgentLoopWorker:只关心 并发与编排(asyncio 并发跑很多条 loop,把结果合并)。AgentLoopManager:只关心 分发与资源切换(Ray 分发 chunk、hybrid wake_up/sleep、汇总)。AsyncLLMServerManager:只关心 推理服务网关(负载均衡、sticky session、RPC)。
一张图就够(来自 notebook,我稍微改了命名):
1 | graph TD |
这个拆分真正的价值在于:你改动某个维度时,不会“牵一发动全身”。
- 想换工具协议/状态机:改
AgentLoop。 - 想调并发粒度:改
Worker(或 worker 数)。 - 想换推理引擎:改
ServerManager(vLLM / SGLang / OpenAI compatible server)。 - 想做推训资源复用:改
Manager的 wake_up/sleep 策略。
2. AgentLoopManager:从 Trainer 接单,到 hybrid wake_up/sleep 的总控
从 agent_loop_code.ipynb 给出的入口看,训练主控(例如 RayPPOTrainer)在需要新 rollout 时会调用:
AgentLoopManager.generate_sequences(prompts=batch)
它的核心职责可以总结为 3 件事:
- 拆 batch:把一个大 batch 切成多个 chunk。
- 分发到 Worker:Ray remote 调
worker.generate_sequences.remote(chunk)。 - 资源切换(可选):如果启用了 hybrid/free_cache_engine,就在生成前后做:
wake_up():让推理引擎准备好(权重同步/显存准备/prefix cache 重置等)sleep():让推理引擎释放 KV cache,给训练阶段让路
一个你在工程里一定会遇到的点:
- hybrid 不是“永远更快”,它是显存复用的节省方案。
- 你的瓶颈如果在网络 IO、tool latency、或者 reward 计算,那 hybrid 可能不会带来收益,反而增加 wake_up/sleep 的切换开销。
我的建议是:
- 小规模先跑 standalone(推理和训练分不同 GPU),把正确性与指标闭环打通。
- 再考虑 hybrid,把 GPU 利用率榨到极限。
3. AgentLoopWorker:CPU 节点上的并发器,避免 straggler 拖垮整批
agent_loop_details.ipynb 一开始就强调:
- Worker 被调度到 CPU 节点上运行;
- 大量的工具调用与环境交互在 CPU 上异步并发;
- GPU 侧只负责“生成”,通过 server manager 统一调用。
核心实现范式是:
- 每个 sample 一个 async task
await asyncio.gather(*tasks)等全部完成后在 Worker 内做一次同步(merge)
这给了你一个很清晰的“同步边界”:
rollout 阶段尽可能异步;update 阶段必须同步。
这个同步边界一旦画清楚,你很多设计会自然变对:
- 工具输出永远是 observation(mask=0)。
- 只有 LLM 生成 token 是 action(mask=1,参与梯度)。
- reward 如果要前置,应该在 “merge 前”完成,把分数挂载到轨迹里。
4. AgentLoop:状态机是 tool-use agent 的通用接口
对 tool-use agent,verl 的实现思路是“显式状态机”(在 agent_loop_details.ipynb 给了一个很标准的 5 状态版本):
1 | stateDiagram-v2 |
这套状态机的工程意义是:
- 你后续想加 “planner / memory / tool router / user simulator”,都能自然插到对应状态里。
- 你想做并发限制(比如
max_parallel_calls),也能在 PROCESSING_TOOLS 里做得很干净。
同时它强迫你把 “action vs observation” 分清楚:
GENERATING输出的是 action token(参与梯度)。PROCESSING_TOOLS / INTERACTING输出的是 observation(不参与梯度)。
5. async 为什么不是优化项:Sync 训练会把 agent 绑成“旅行团”
如果你用 batch 同步推进 multi-turn,会发生一种非常典型的“木桶效应”(notebook 把它讲得很形象):
- 同一个 batch 里,只要有一个 agent 慢(生成长 / tool 慢 / 网络慢),整个 batch 就卡住;
- 更糟的是:LLM 生成与 tool 执行是交替的,sync 模式会让快 agent 也必须等慢 agent 才能进入下一轮生成。
对比一下伪代码(摘自 agent_loop_details.ipynb):
1 | # Sync: 强制绑定的“旅行团” |
1 | # Async: 各跑各的“自由行” |
真正关键的一句是:
await只挂起当前 agent,不挂起别人。
于是 “快任务” 可以不断发起下一轮生成请求,vLLM/SGLang 的 continuous batching 就能持续吃满 GPU。
6. AsyncLLMServerManager:负载均衡 + Sticky Session(多轮必需)
6.1 为什么必须 sticky:多轮对话需要 prefix cache 命中
如果多轮请求被随机路由到不同 server:
- prefix cache 命不中(每轮都当新 prompt),吞吐会掉;
- 更糟的是:不同 server 的 tokenizer/chat template/状态如果不完全一致,你会把 token-in-token-out 彻底破坏。
所以 verl 用了一个很直接的设计:
- 用 LRU 维护
request_id -> server映射(同一 trajectory 固定路由) - 用最小堆维护 server 的“当前负载”(粗粒度的请求计数)
agent_loop_details.ipynb 给出了一个非常直观的代码骨架(我原样贴出来,便于你对齐实现思路):
1 | class AsyncLLMServerManager: |
6.2 你一定要想清楚:request_id 的定义与生命周期
sticky 的关键在 request_id:
- 必须是 “同一 trajectory 跨轮一致” 的 id;
- 且要避免不同样本冲突(否则会错误粘连);
- 还要考虑 LRU 淘汰后继续请求会被重新分配 server(notebook 也提示了这一点)。
工程上我更建议你:
- 用
trajectory_id(而不是 “当前 turn id”)作为 sticky key; - 在日志里把
trajectory_id -> server_id打出来(排障时非常关键)。
7. AgentLoopOutput:交错轨迹(interleaved trajectory)必须显式带 mask
agent loop 的输出不是“纯 LLM token 序列”,而是交错的:
- LLM 生成 token(action)
- tool 返回 token(observation)
- LLM 继续生成 token(action)
所以输出结构里至少要包含:
response_token_ids:包含 LLM 与 tool 的所有 tokenresponse_mask:LLM token=1,tool token=0num_turns:多少轮 user/assistant/tool(用于 shaping 或统计)
这件事在 PPO/GRPO 里会直接影响梯度:
- 你只能对
mask=1的 token 计算logp、ratio、KL、entropy; - tool 输出如果混进去,会污染分布,导致训练发散(这也是 token-in-token-out 那篇强调的点)。
8. Reward 前置:async rollout 下把 RM 打分放进 rollout 阶段
agent_loop_details.ipynb 给了一个非常重要的时序图:在 async 模式下,reward 计算被前置到了 rollout 阶段内部完成,Trainer 在 training phase 里可以直接跳过 compute_rm_score。
1 | sequenceDiagram |
这一步的好处是:
- rollout 阶段就能把 “轨迹 + reward” 封装完整;
- training phase 可以更纯粹地做 advantage / loss / update;
- 并且在多 worker 并发下,reward 的吞吐也更容易 scale。
但它也带来一个新挑战:你要更清楚 reward 的挂载口径(挂最后 token?按 turn 分配?是否加 tool shaping?)。
9. Hybrid 推训:wake_up / sleep 到底在干嘛
agent_loop_arch.ipynb 把 hybrid 的动机说得很直白:
- 显存宝贵;
- 训练(FSDP/ZeRO)和推理(vLLM/TP)对显存的占用形态完全不同;
- 如果推理引擎一直常驻,会吃掉大量 KV cache,训练可能直接 OOM;
- 所以需要在 “rollout 阶段” 与 “update 阶段” 之间切换推理引擎状态。
在 verl 里,这个机制通常通过:
wake_up():推理引擎准备好(可能包含权重同步 / cache reset / 显存准备)sleep():推理引擎释放 KV cache 等显存占用
并且在 standalone 模式下可以跳过(推理和训练不在同一组 GPU)。
工程建议(经验,不是教条):
- 你在单机/小集群:先 standalone(降低复杂度)。
- 你在多机大集群:hybrid 才值得(否则 GPU 利用率很难拉满)。
10. GPU/Replica 配置:num_replicas vs 引擎内部 DP
agent_loop_config.ipynb 给了一个很实用的公式:
num_replicas = total_gpus / (TP × DP × PP)
这里有一个很容易混的点(notebook 也专门对比了):
num_replicas:服务级 replica(多个完全独立的推理服务),由AsyncLLMServerManager做负载均衡,松耦合。data_parallel_size:引擎内部 DP(一个推理服务实例内部的并行),由 vLLM/SGLang 自己处理。
你在集群扩展与容错上,往往更依赖 num_replicas;
你在单实例吞吐上,才更依赖引擎内部并行(TP/PP/DP)。
一个典型例子(notebook 的例子):
- 16 GPU 集群,模型推理需要 4 卡 TP:
rollout_world_size=4num_replicas=16/4=4(4 个独立 server)
11. 排障 Checklist:你要记录哪些日志,才能真的 debug AgentLoop
我建议最少记录这几类日志(否则你会在“看起来卡住了”时毫无头绪):
- 轨迹级路由
trajectory_idserver_id(sticky 选择结果)- LRU 是否命中/被淘汰
- 异步并发
- 每个 sample 的
llm_latency / tool_latency / turns / token_count max_parallel_calls触发次数
- 每个 sample 的
- 输出口径
response_mask中 1/0 的比例- tool 输出 token 长度分布(防 OOM)
- hybrid 切换
- wake_up/sleep 耗时
- 权重同步耗时(如果有)
- KV cache 释放前后显存占用(对齐你 vLLM 那篇的显存分析)
12. 小结
- AgentLoop 是 agentic RL 的“数据生成器”:它要产出可训练轨迹,而不是只跑通流程。
- async rollout 是 multi-turn tool use 的必要条件:它把 straggler 从系统瓶颈里移走,并让 continuous batching 真正发挥作用。
- sticky session 是多轮必需品:它连接 prefix cache、token-in-token-out 与推理吞吐。
- hybrid(wake_up/sleep)是显存复用策略:先把 correctness 与指标闭环跑通,再考虑复杂度更高的 hybrid。

