概要Uni-Agent 是 verl 社区推出的一个面向 Agent RL 的训练/推理一体框架以同一套交互栈同时支撑大规模 Agent 推理和 RL 训练。它在 verl字节跳动开源的 LLM RL 训练框架之上构建 Agent 抽象层Model/Tool/Env 三层将 sandbox 执行委托给 SWE-ReX。项目的核心主张可以概括为推理 训练同一入口——把Agent 如何与环境交互并产出 token-level 训练数据封装成一套可复用、可扩展、可训练的工程系统。无论你是跑 1000 个并行的 SWE-Bench 推理任务还是进行 GRPO/GSPO 强化学习训练底层走的是同一套AgentInteraction交互循环。这种设计使得从模型推理验证到大规模 RL 训练的切换成本趋近于零——只需换一个 YAML 配置不需要重写任何交互逻辑。注意因为可能本文参考的verl版本不够新且 verl 社区本身就在突飞猛进因此可能某些对verl的认知不够准确本文为“从代码反推设计理念” 时间仓促因此肯定存在很多错误还请读者不吝指出本篇存在的问题谢谢。0x01 修改扩展点我们首先看看如何通过修改扩展点来完成改造。其实本节应该属于上一篇但是因为字数限制只能挪到此处。1.1 verl 扩展点全景verl 的AgentLoopBase通过__init__注入 6 个依赖、通过rollout.*和async_training.*开放 4 个配置命名空间、通过verl.utils和verl.tools提供 4 个工具库、通过experimental.agent_loop暴露 3 个管线入口、通过extra_fields预留 2 个数据契约通道。加上 3 个继承扩展点总共6 类 22 个扩展点。下面我们从verl 提供了什么和Uni-Agent 怎么用两个角度逐一梳理并用统一的编号保证两边对应关系清晰。编号类别verl 提供Uni-Agent 使用方式Uni 使用?E1继承AgentLoopBaseABC / run() 抽象接口UniAgentLoop(AgentLoopBase)实现run()/ 实现完整 Agent 交互栈✅E2继承AgentLoopOutputBaseModel / 9 固定字段 as_dict()convert_to_agent_output()填充全部 9 字段✅E3继承_agent_loop_registryregister()装饰器不走装饰器通过agent_loop_config_pathYAML 外部注入✅E4注入self.configHydra DictConfig读取 verl 训练超参 agent_loop_config_path✅E5注入self.tokenizerHF AutoTokenizer封装到AgentChatModel用于 tokenize/decode/boundary 探测✅E6注入self.server_managerLLMServerClient封装为AgentChatModel.client对 interaction 层隐藏 verl 细节✅E7注入self.processorHF AutoProcessor不使用——Uni-Agent 当前无多模态支持❌E8注入self.dataset_clsRLHFDataset类不使用——Uni-Agent 通过 DataProto 接收数据❌E9注入self.data_config数据集配置不使用——Uni-Agent 管理自己的数据格式❌E10配置rollout.agent.*num_workers, agent_loop_config_path, default_agent_loopagent_loop_config_path挂载 Agent YAML自有配置体系num_workers计算并发✅E11配置rollout.multi_turn.*enable, max_parallel_calls, max_user_turns, max_assistant_turnsenableTrue是前置条件max_parallel_calls1✅E12配置async_training.*staleness_threshold, partial_rollout, trigger_parameter_sync_step, require_batches训练脚本配置Uni-Agent 不感知但透传max_global_steps✅E13配置rollout.modeasync/sync训练脚本配置rollout_modeasync✅E14工具库verl.tools.schemasOpenAIFunctionToolCall,OpenAIFunctionToolSchema,ToolResponse两个 Parser 都输出OpenAIFunctionToolCall保证 Tool 层解耦✅E15工具库apply_chat_template()verl.utils.chat_templaterollout_cache初始化和增量追加时调用✅E16工具库normalize_token_ids()verl.utils.tokenizer所有 tokenize 结果标准化处理✅E17工具库simple_timer()verl.utils.profilerquery 耗时和 tool 耗时跟踪E18管线AgentLoopManager训练和推理都用同一套调度器generate_sequences()✅E19管线DataProto批次协议通过non_tensor_batch[raw_prompt]和[tools_kwargs]传递 per-sample 数据✅E20管线main_ppo/fully_async_main训练入口通过 Hydra 配置注入不修改入口代码✅E21数据契约AgentLoopOutput.extra_fields: dict注入traj_maskedtraj_exit_reasonmax_global_steps✅E22数据契约TokenOutput.extra_fields: dict透传max_global_steps从 token 级到 output 级✅按照最小侵入的组织原则Uni-Agent 对不同类别的扩展点采用了不同的改造深度。从上表可以看出verl 的 22 个扩展点中Uni-Agent 使用了 19 个未使用的 3 个E7 processor、E8 dataset_cls、E9 data_config都与多模态和 verl 内置数据集管理相关——这是 Uni-Agent 当前尚未覆盖的领域。1.2 关键扩展点详解22 个扩展点中下面 几个是最核心的——它们决定了 verl 和 Uni-Agent 之间谁做什么的分工形态也是理解 Uni-Agent 架构最关键的切入点。E1: AgentLoopBase——继承契约这是 verl 与 Uni-Agent 之间唯一的代码级耦合点。verl 通过AgentLoopBase定义了 Agent 的抽象契约class AgentLoopBase(ABC): def __init__(self, trainer_config, server_manager, tokenizer, processor, dataset_cls, data_config, **kwargs): self.config trainer_config.config # 完整 verl Hydra 配置 self.server_manager server_manager # LLMServerClient self.tokenizer tokenizer # HF AutoTokenizer abstractmethod async def run(self, sampling_params, **kwargs) - AgentLoopOutput: ...契约的含义verl 对 Agent 的认知只有一句话——给它 sampling_params 和 kwargs它还你一个 AgentLoopOutput。Agent 内部发生了什么ReActPlan-Execute跑容器调 APIverl 完全不关心。Uni-Agent 的run()将这个一行契约扩展为 8 步完整管线UniAgentLoop.run(): 1. _init_config() → YAML verl config 合并为 config_dict 2. _init_chat_model() → AgentChatModel(clientself.server_manager, tokenizer...) 3. _init_tools_manager() → ToolsManager(tools parser) 4. _init_env() → AgentEnv(deployment_config) 5. env.start() → install_tools() 6. AgentInteraction.run() → 多轮 ReAct 交互 7. reward_spec.compute_reward() 8. convert_to_agent_output() → AgentLoopOutput改造深度~200 行。这 8 步中的每一步都对应一个独立模块Model/Tool/Env/Reward它们之间通过rollout_cache这个共享数据结构耦合。E3: 注册机制——外部注入 vs 装饰器注册verl 提供了_agent_loop_registry和register()装饰器内置了single_turn和tool_agent两个 Agent。_agent_loop_registryregister()装饰器允许任何 Agent 框架注册自己的实现。Uni-Agent 刻意不使用register()装饰器而是通过agent_loop_config_pathYAML 中的_target_: uni_agent.agent_loop.UniAgentLoop由hydra.utils.instantiate动态创建并注入 registr。# verl 预留的扩展口 [agent_loop.py:421-426] agent_loop_config_path self.rollout_config.agent.agent_loop_config_path if agent_loop_config_path: resolved_path resolve_config_path(agent_loop_config_path) agent_loop_configs OmegaConf.load(resolved_path) for agent_loop_config in agent_loop_configs: _agent_loop_registry[agent_loop_config.name] agent_loop_config这种方式使得 Uni-Agent 完全不需要被 verl 的register()装饰器绑定——它通过配置文件外部注入实现了零代码级耦合。这也是最小侵入原则的典型体现verl 预留了一个口子Uni-Agent 通过 YAML 配置钻了进去。这种方式使得 Uni-Agent 可以完全控制自己的初始化流程而不受 verl 内置 Agent 的初始化契约约束。E6: server_manager——推理引擎注入与封装verl 注入的self.server_manager是一个LLMServerClient实例在 Fully Async 模式下是FullyAsyncLLMServerClient。Uni-Agent 不是直接使用它而是做了一次适配封装# agent_loop.py:172-177 model_config { client: self.server_manager, # verl 注入 → 转交 AgentChatModel tokenizer: self.tokenizer, # verl 注入 → 转交 AgentChatModel max_model_len: max_model_len, # verl config → 转交 AgentChatModel sampling_params: sampling_params, } chat_model AgentChatModel(**model_config)封装后AgentChatModel通过self.client.generate(request_id..., prompt_ids..., sampling_params...)调用推理引擎完全不依赖 verl 的类型系统。这意味着AgentChatModel是可单独测试的mock 一个generate()接口即可interaction/层对 verl 零依赖——如果换训练框架只需改agent_loop.py中的透传代码OpenAICompatibleChatModel可以复用同一套AgentInteraction因为它实现了相同的query()接口E10-E12: 配置命名空间——verl 的 Agent 配置插槽verl 在 Hydra 配置体系中为 Agent 预留了三个配置命名空间命名空间关键配置项Uni-Agent 消费位置rollout.agent.*num_workersworker 数agent_loop.py 计算 per-worker 并发agent_loop_config_pathAgent YAML 路径agent_loop.py 加载 Agent 配置default_agent_loop默认 Agent 名verl 内部使用匹配 registryrollout.multi_turn.*enable多轮开关前置条件——Uni-Agent 正常运行依赖此值为 Truemax_parallel_calls并行 tool 数当前固定为 1与 Uni 硬编码一致max_user_turns/max_assistant_turnsUni-Agent 用自己的max_turns字段控制async_training.*staleness_thresholdfully_async_rollouter.pypartial_rolloutfully_async_rollouter.pytrigger_parameter_sync_stepfully_async_rollouter.pyrequire_batches控制 training buffer 最小样本数其中最关键的是agent_loop_config_path——它是 verl 发现 Uni-Agent 的桥梁。训练脚本中设置agent_loop_config_path${AGENT_CONFIG_PATH}verl 读取该 YAML将其中定义的 Agent 配置_target_: uni_agent.agent_loop.UniAgentLoop注入 registry然后由hydra.utils.instantiate动态创建实例。E14: tools.schemas——标准化 Tool Call 格式verl 定义了OpenAIFunctionToolCall、OpenAIFunctionToolSchema、OpenAIFunctionCallSchema等标准数据结构。Uni-Agent 的两个 Parser 将 model 的非结构化文本输出解析为这些标准结构Model 文本输出: 思考...\ntool_call\nfunctionexecute_bash\nparametercommand\nls /\n... │ XMLToolParser 或 HermesToolParser ▼ OpenAIFunctionToolCall( iduuid-..., typefunction, functionOpenAIFunctionCallSchema( nameexecute_bash, arguments{command: ls /} ) )这种解析即标准化的设计带来了一个重要保证ToolsManager.get_tool_bash_command()接收到的永远是OpenAIFunctionToolCall无论上游用的是什么 parser。Tool 层与 Model 层的耦合被 verl 的标准数据结构解开了。E21-E22: extra_fields——双端元数据通道extra_fields 是 verl 为 Agentic RL 预留的最灵活的扩展通道。它分为两端分别搭乘不同的数据载体Output 端E21——搭乘AgentLoopOutput流向 verl trainer# Uni-Agent 注入 [agent_loop.py:227-229] extra_fields[traj_masked] traj_masked # 该 trajectory 是否被 mask extra_fields[traj_exit_reason] traj_exit_reason # 退出原因 # verl 消费 [agent_loop.py:960-971] # 从所有 inputs 收集 extra_fields → np.ndarray → non_tensor_batchToken 端E22——搭乘TokenOutput从 verl server_manager 流向 Uni-Agent# verl 填充 [fully_async_rollouter.py:147-149] final_output.extra_fields[global_steps] global_steps final_output.extra_fields[min_global_steps] min_global_steps final_output.extra_fields[max_global_steps] max_global_steps # Uni-Agent 透传 [model.py:129-131] max_global_steps token_output.extra_fields.get(max_global_steps, None) if max_global_steps is not None: rollout_cache[extra_fields][max_global_steps] max_global_steps这两条通道的流向正好相反E21 是 Agent → TrainerAgent 告诉 trainer 这个 trajectory 的质量如何E22 是 Trainer → Agenttrainer 告诉 Agent 当前的训练进度。它们共同构成了 verl 和 Agent 之间的元数据对话。0x02 Uni-Agent 关键技术Uni-Agent 的技术栈可以概括为四个层次每一层都是Agentic-first哲学的具体体现——不是在一个现成 RLHF 框架上零散补几个 Agent 接口而是从第一天就为多轮交互、工具调用、环境管理、RL 训练设计的原生基础设施。把多轮交互、工具调用、环境生命周期、失败恢复、轨迹过滤和 token 级训练数据构建当作第一等公民。层机制解决的问题交互层ReAct 五步循环 Duck Typing Model 抽象一套代码同时服务训练token 级和推理message 级环境层沙箱工具 沙箱内 reward 多后端部署工具隔离执行、真实环境验证、代码不变配置切换数据层rollout_cache 增量追加 boundary token 探测token 级精确对齐mask1模型生成/ mask0环境返回天然区分训练层Sync/Fully Async Staleness 三层防御GPU 利用率最大化off-policy 偏差可控本章从 Agentic-first 这条设计哲学出发逐层展开 Uni-Agent 交互栈的几个核心技术ReAct 循环、rollout_cache、Duck Typing 抽象层、沙箱工具与奖励计算、多后端部署、以及 Sync/Async 训练模式。2.1 Agentic-first是什么Agentic-first的核心主张可以用一句话概括同一套AgentInteraction.run()同时服务推理与训练。环境必须被系统管理而不是临时拼接reward 计算必须发生在环境可访问的时刻rollout_cache/response_mask/extra_fields必须天然面向 RL 消费。Agentic-first决定了 Uni-Agent 与 OpenHands / SWE-Agent 的本质区别——不是能不能跑 Agent而是它把Task → Agent推理 → Trajectory → Reward → RL Update → Better Model这一闭环做成了可复用基础设施。2.2 完整的 Multi-Turn 交互栈Uni-Agent 没有使用 VeRL 的ToolAgentLoop而是用UniAgentLoop 实现了上述闭环。Uni-Agent 是目前唯一将 Agent 交互栈与 verl Agentic RL 训练管线深度集成的开源框架。class UniAgentLoop(AgentLoopBase): async def run(self, sampling_params, **kwargs): # verl 只提供这个接口框架 # 下面全是 Uni-Agent 自己的逻辑: self.chat_model self._init_chat_model(...) # Agent 特有 self.tools_manager self._init_tools_manager(...) # Agent 特有 self.env self._init_env(...) # Agent 特有 self.interaction AgentInteraction(...) # Agent 特有 await self.env.start() # 环境管理 interaction_result await self.interaction.run() # 交互循环 reward_score, info await self.reward_spec.compute_reward( interaction_resultinteraction_result, ) # 奖励计算 output await self.convert_to_agent_output(...) # 转为 verl 格式2.2.1 UniAgentLoop vs ToolAgentLoop为什么不使用verl 内置的ToolAgentLoop而是新写一个UniAgentLoop ToolAgentLoop 是为轻量本地 tool 调用设计的——在 Python 进程内调用FunctionTool本地函数每次 tool call 独立无状态是一个通用的 ReAct 状态机实现。而真实 Agent 任务需要的是持久化远程沙箱 复杂环境管理——文件系统、进程状态跨步保持、bash 命令执行。这不是多几个参数的差异而是架构假设的根本不同。UniAgentLoop 在 ToolAgentLoop 基础上增加了 sandbox 环境管理、bash-based tool 隔离执行、双 tool parser 和双 model 后端——这些是 verl 的ToolAgentLoop完全不提供的。维度verl ToolAgentLoopUniAgentLoopTool 执行Python 进程内调用 FunctionTool远程沙箱中执行 bash 命令环境无状态——每次 tool call 独立有状态持久沙箱——文件系统、进程状态跨步保持适用任务轻量 tool计算器、API 查询重量级 agent 任务SWE、代码修复、搜索状态机PENDING→GENERATING→PROCESSING_TOOLS 循环更灵活的 step 循环 多种退出条件Reward无内置 reward依赖 verl RewardManager内置 reward spec在沙箱内运行测试实现了两层 Reward 系统并发控制无asyncio.Semaphore 控制全局并发