【Agentic RL / 强化学习框架】Uni-Agent 深度技术分析(1)--- 总体

【Agentic RL / 强化学习框架】Uni-Agent 深度技术分析(1)--- 总体

目录

  • [【Agentic RL / 强化学习框架】Uni-Agent 深度技术分析(1)--- 总体](#【Agentic RL / 强化学习框架】Uni-Agent 深度技术分析(1)--- 总体)
    • [0x00 概要](#0x00 概要)
    • [0x01 基本功能](#0x01 基本功能)
      • [1.1 竞品对比与定位](#1.1 竞品对比与定位)
        • [1.1.1 三者定位](#1.1.1 三者定位)
        • [1.1.2 七维度对比表](#1.1.2 七维度对比表)
      • [1.2 Uni-Agent 架构全景](#1.2 Uni-Agent 架构全景)
        • [1.2.1 全景图](#1.2.1 全景图)
        • [1.2.2 代码组织](#1.2.2 代码组织)
        • [1.2.3 DeployConfig 四后端](#1.2.3 DeployConfig 四后端)
    • [0x02 Agentic RL 的难点](#0x02 Agentic RL 的难点)
      • [2.1 Agentic RL vs 传统 RLHF](#2.1 Agentic RL vs 传统 RLHF)
      • [2.2 Agentic RL 六大核心需求](#2.2 Agentic RL 六大核心需求)
      • [2.3 Agentic RL 十大难点](#2.3 Agentic RL 十大难点)
    • [0x03 verl 的能力与系统性不足](#0x03 verl 的能力与系统性不足)
      • [3.1 设计范式差异](#3.1 设计范式差异)
        • [3.1.1 本质矛盾](#3.1.1 本质矛盾)
        • [3.1.2 分工边界](#3.1.2 分工边界)
        • [3.1.3 详细分工表](#3.1.3 详细分工表)
      • [3.2 verl 为 Agentic RL 做了什么](#3.2 verl 为 Agentic RL 做了什么)
      • [3.3 verl 的不足](#3.3 verl 的不足)
        • [3.3.1 verl 没做什么(= Uni-Agent 自己构建)](#3.3.1 verl 没做什么(= Uni-Agent 自己构建))
        • [3.3.2 具体不足分析](#3.3.2 具体不足分析)
      • [3.4 小结](#3.4 小结)
    • [0x04 Uni-Agent 设计理念与核心功能](#0x04 Uni-Agent 设计理念与核心功能)
      • [4.1 核心设计哲学](#4.1 核心设计哲学)
      • [4.2 和用户的分工](#4.2 和用户的分工)
        • [4.2.1 用户不需要做的](#4.2.1 用户不需要做的)
        • [4.2.2 用户需要做的](#4.2.2 用户需要做的)
    • [0x05 Uni-Agent 对 verl 的改造](#0x05 Uni-Agent 对 verl 的改造)
      • [5.1 核心改造原则](#5.1 核心改造原则)
        • [5.1.1 改造决策](#5.1.1 改造决策)
        • [5.1.2 核心改造手段](#5.1.2 核心改造手段)
      • [5.2 工作分类总览](#5.2 工作分类总览)
      • [5.3🔵 环境管理层 - 解决 "在哪里执行"](#5.3🔵 环境管理层 - 解决 "在哪里执行")
        • [5.3.1 解决的问题](#5.3.1 解决的问题)
        • [5.3.2 verl 为何不能解决](#5.3.2 verl 为何不能解决)
        • [5.3.3 Uni-Agent 的实现](#5.3.3 Uni-Agent 的实现)
      • [5.4 🟢 交互循环层 - 解决 "如何交互"](#5.4 🟢 交互循环层 - 解决 "如何交互")
        • [5.4.1 解决的问题](#5.4.1 解决的问题)
        • [5.4.2 verl 为何不能解决](#5.4.2 verl 为何不能解决)
        • [5.4.3 Uni-Agent 的实现](#5.4.3 Uni-Agent 的实现)
      • [5.5 🟡 工具系统层 - 解决 "如何动作"](#5.5 🟡 工具系统层 - 解决 "如何动作")
        • [5.5.1 解决的问题](#5.5.1 解决的问题)
        • [5.5.2 verl 为何不能解决](#5.5.2 verl 为何不能解决)
        • [5.5.3 Uni-Agent 的实现](#5.5.3 Uni-Agent 的实现)
        • [5.5.4 Tool 执行管道](#5.5.4 Tool 执行管道)
      • [5.6 🔴 奖励计算层 - 解决 "如何评估"](#5.6 🔴 奖励计算层 - 解决 "如何评估")
        • [5.6.1 解决的问题](#5.6.1 解决的问题)
        • [5.6.2 verl 为何不能解决](#5.6.2 verl 为何不能解决)
        • [5.6.3 Uni-Agent 的实现](#5.6.3 Uni-Agent 的实现)
      • [5.7🟣 训练集成层(胶水层)](#5.7🟣 训练集成层(胶水层))
        • [5.7.1 verl 为何不能解决](#5.7.1 verl 为何不能解决)
        • [5.7.2 Uni-Agent 的实现](#5.7.2 Uni-Agent 的实现)
      • [5.8 ⚪ 工程支撑层](#5.8 ⚪ 工程支撑层)
      • [5.9 一句话总结](#5.9 一句话总结)
    • [0xFF 参考](#0xFF 参考)

0x00 概要

本系列目的是通过解析两个强化学习框架(Uni-Agent,Miles)来梳理Agentic RL。

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 竞品对比与定位

在正式进入架构分析之前,我们先回答一个关键问题:Uni-Agent 在现有的 Agent 框架生态中处于什么位置?

1.1.1 三者定位

python 复制代码
Agent 抽象层次
    高 ▲
      │  ┌──────────────┐
      │  │  OpenHands   │ 完整 Agent 应用  
      │  └──────────────┘
      │  
      │      
      │  ┌──────────────┐   ┌──────────────────────────────┐
      │  │  SWE-Agent   │   │        Uni-Agent             │
      │  │  (学术 Agent) │   │  推理+训练一体化框架            │
      │  └──────────────┘   │  统一交互栈,大规模并发,        │
      │                     │  RL 训练原生支持               │
      │                     └──────────────┬───────────────┘
      │                                    │
      │                     ┌──────────────▼──────────────┐
      │                     │       verl (训练引擎)        │
      │                     │  AgentLoopBase/Manager      │
      │                     │  FullyAsyncRollouter        │
      │                     └─────────────────────────────┘
      └──────────────────────────────────────────────────────► 训练能力

从上图可以看出,OpenHands 定位在"完整产品"层,关注的是开箱即用的 Agent 体验;SWE-Agent 定位在"学术研究"层,侧重单任务的推理表现。而 Uni-Agent 选择了第三条路:向下集成 verl 的训练能力,向上提供可编程的 Agent 抽象。这一定位使得它成为目前唯一将 Agent 交互栈与 RL 训练引擎深度集成的开源框架。这一点也在其 README 中可以看到。

markdown 复制代码
# Uni-Agent: Build, Run, and Train Agents at Scale

Uni-Agent is a unified framework for general agents at scale.

- **All-in-one stack:** one framework for building, running, and training agents.
- **Unified agent interface:** unified abstractions for diverse and complex real-world agent scenarios.

The long-term vision is to build the backend infrastructure for next-generation agents across both inference and training, enabling them to perceive, act, and explore complex real-world tasks.

1.1.2 七维度对比表

维度 Uni-Agent OpenHands SWE-Agent
核心定位 Agent 推理+训练一体化 全栈 AI 软件工程师产品 学术研究 Agent
训练能力 原生 GRPO/GSPO RL,Fully Async + Partial Rollout
交互模式 ReAct Loop + Tool-as-Bash + 双 parser Agent-Controller 事件驱动 ReAct Loop + 自定义 DSL
工具机制 6 种内置 tool,脚本上传到容器 丰富 tool 生态 3 种 tool
环境管理 4 种部署 discriminated union + SWE-ReX Docker 为主 Docker 容器
并发规模 1000+ 并行,Ray + asyncio 单实例为主 单实例
社区生态 新生项目(2026),tool 生态早期 成熟社区 学术社区
底层依赖 verl(训练) + SWE-ReX(sandbox) 自研 Docker

1.2 Uni-Agent 架构全景

因为 Uni-Agent 深度依赖 verl,下文的一些架构图会连带着 verl 一起展示,以完整呈现二者的协作关系。

1.2.1 全景图

从全景图中可以清晰地看到 verl 和 Uni-Agent 的分工:

  • verl 负责"怎么训练"(调度、算法、参数同步)。
  • Uni-Agent 负责"训练什么"(交互逻辑、环境管理、奖励计算)。

二者的分界线是 AgentLoopBase.run() 这一个方法签名------verl 通过 hydra.utils.instantiate 创建 UniAgentLoop 实例,调用 run() 获取 AgentLoopOutput,然后将其喂入 PPO/GRPO 训练管线。

1.2.2 代码组织

python 复制代码
           Uni-Agent                               VeRL
─────────────────────────────────   ───────────────────────────────────────
uni_agent/                          verl/verl-main/verl/
├── agent_loop.py                   ├── experimental/agent_loop/
│   └─ UniAgentLoop(AgentLoopBase)  │   ├── agent_loop.py
├── interaction/                    │   │   ├── AgentLoopBase (ABC)
│   ├── interaction.py              │   │   ├── AgentLoopOutput (BaseModel)
│   │   └─ AgentInteraction         │   │   ├── AgentLoopWorker
│   ├── model.py                    │   │   ├── AgentLoopManager
│   │   ├─ AgentChatModel           │   │   └── _agent_loop_registry
│   │   └─ OpenAICompatibleChatModel│   │   ├── tool_agent_loop.py
│   ├── env.py                      │   │   │   └─ ToolAgentLoop (verl内置)
│   │   └─ AgentEnv                 │   │   ├── tool_parser.py
│   ├── tools_manager.py            │   │   │   └─ ToolParser, FunctionCall
│   │   └─ ToolsManager             │   │   └── utils.py
│   └── tool_parser.py              │   ├── fully_async_policy/
│       ├─ XMLToolParser            │   │   ├── fully_async_main.py
│       └─ HermesToolParser         │   │   ├── fully_async_rollouter.py
├── deployment/                     │   │   └── fully_async_trainer.py
│   ├── config.py                   │   ├── tools/
│   │   └─ DeployConfig (union)     │   │   ├── schemas.py
│   ├── local/, host/, modal/,      │   │   │   ├── OpenAIFunctionToolCall
│   │   vefaas/                     │   │   │   ├── ToolResponse
│   └── remote_runtime.py           │   │   │   └── ...
├── tools/                          │   │   └── tool_registry.py
│   ├── base.py                     │   └── utils/
│   │   └─ AbstractTool             │       ├── chat_template.py
│   ├── registry.py                 │       │   ├── apply_chat_template()
│   ├── execute_bash/               │       │   └── initialize_system_prompt()
│   ├── str_replace_editor/         │       ├── tokenizer.py
│   ├── finish/, submit/            │       │   └── normalize_token_ids()
│   ├── search/, search_arxiv/      │       └── profiler.py
└── reward/                         │           └── simple_timer()
    ├── base.py                     └── workers/
    │   └─ AbstractRewardSpec           └── rollout/
    ├── registry.py                         └── replica.py
    ├── swe_bench.py                            └── TokenOutput
    ├── swe_rebench.py
    ├── r2e_gym.py
    └── search.py

1.2.3 DeployConfig 四后端

deployment/config.py 使用 Pydantic v2 discriminated union 实现编译时类型安全 + 运行时自动反序列化:

后端 类型标记 底层 Runtime 适用场景
Local type: "local" Apptainer/Docker/Podman 容器 本地开发、单机训练
Host type: "host" 主机直接执行(无容器) 搜索等轻量任务
Modal type: "modal" Modal serverless sandbox 云端弹性扩缩容
veFaaS type: "vefaas" 字节跳动内部 FaaS 字节内部大规模部署

值得注意的是,RemoteRuntimedeployment/remote_runtime.py)是独立于四种后端之外的 HTTP 客户端层,所有后端都通过它与 SWE-ReX server 通信。它不在 DeployConfig 的 discriminated union 内,而是各后端的共享基础设施------这种设计意味着添加新后端时,只需新增一个 Config 类和一个 Deployment 类,无需修改 RemoteRuntime。

0x02 Agentic RL 的难点

在深入 Uni-Agent 的设计之前,我们需要先理解它要解决什么问题。Agentic RL 与传统 RLHF 的差异不是量变,而是质变。

2.1 Agentic RL vs 传统 RLHF

传统 RLHF 训练的是一问一答的对话模型:给定一个 prompt,模型生成一段 response,奖励模型打分。这是一个"单轮、短文本、无环境交互"的场景。

python 复制代码
π_θ(y | x)  →  reward_model(x, y)  →  PPO update
单轮文本生成         单标量奖励         一次参数更新

Agentic RL 则完全不同:它训练的是一个"与环境持续交互"的 Agent 模型。模型在真实环境中(代码仓库、终端、浏览器)通过多步交互完成复杂任务,训练目标是让模型学会更好的决策策略------不仅包括"说什么",更包括"何时采取什么行动"。

python 复制代码
π_θ(a_t | h_t)  →  env.step(a_t)  →  o_{t+1}  →  ...  →  terminal reward
   多轮决策            多步环境交互                           稀疏、延迟奖励

两者的本质区别在于:Agentic RL 的 trajectory 是多轮交互序列 ,包含交替的 model 输出和 environment observation token;奖励信号是稀疏且延迟的(任务完成才给分),而非每步都有即时 reward;token 序列需要精确区分 model 生成段(参与 loss)和 tool 响应段(不参与 loss)。

2.2 Agentic RL 六大核心需求

从上述差异出发,可以归纳出 Agentic RL 框架必须满足的六项核心需求:

# 需求 说明 Uni-Agent 实现
1 多轮交互 Agent 与环境进行多步 ReAct 循环;模型需要多次推理,每次根据环境反馈调整行动 AgentInteraction.run()
2 Token 级对齐/归因 prompt/response/tool observation 的 token mask 精确对齐;RL 训练需要知道每个 token 的 log probability 的梯度归属 rollout_cache["response_mask"]
3 异步大规模并发 训练需要大量轨迹数据;上千个 agent 同时与环境交互 asyncio.Semaphore + Ray worker pool
4 环境多样性 支持不同 sandbox 后端;每个 Agent 需要独立的 sandbox,执行命令并获取结果 4 种 DeployConfig discriminated union
5 异构奖励信号 不同 benchmark 的评估逻辑;不同任务有不同的评估方式 4 种 AbstractRewardSpec 实现
6 训练-推理统一 同一套代码跑推理和训练,避免训练/推理 gap AgentChatModel + OpenAICompatibleChatModel 共享 AgentInteraction

2.3 Agentic RL 十大难点

这六项需求背后是一系列工程和算法层面的难题。我们将它们归纳为十个主要难点,逐一分析 Uni-Agent 的应对策略。

难点 1: Token 对齐

python 复制代码
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ prompt [MASK=0] │ response₁ [MASK=1] │ obs₁ [MASK=0] │ response₂ [MASK=1] │ ...     |
└─────────────────────────────────────────────────────────────────────────────────────┘

核心问题:每次追加 tool response 时,应该重新 tokenize 整个对话历史,还是增量 tokenize?如果增量 tokenize,如何保证 token 序列格式与 model 训练时的格式完全一致?

Uni-Agent 的方案是增量 tokenize + message_boundary_tokens 探测,采用的差分对比法来发现 tokenizer 在消息边界插入的特殊 token。

难点 2: Token 流的混合属性

Agent 的 response 不再是纯模型输出,而是模型输出与环境反馈的交织:

python 复制代码
Token 序列: [模型思考][模型动作][环境观察][模型思考][模型动作][环境观察]...
梯度归属:   [需要]    [需要]   [不需要]   [需要]   [需要]    [不需要]

这要求框架精确标记哪些 token 参与 RL 梯度计算(模型的选择),哪些只是上下文(环境反馈)。Uni-Agent 通过 response_mask(1=参与/0=不参与)来实现这一区分。

难点 3: 交互时间极长且方差巨大

传统 RLHF 中一个 rollout 约 2 秒(生成 200 tokens),但在 Agentic RL 中,一个 rollout 可能耗时 30 秒到 60 分钟(100 步交互 + 环境执行)。Batch 内样本的执行时间差异可能超过 10 倍,训练被最慢的 Agent 阻塞(长尾效应),GPU 在等待环境执行时处于空闲状态。

难点 4: 训练-推理 Gap

推理时你可能用 OpenAI API 调用模型,但训练时 verl 需要 token IDs + log probs 来计算 PPO loss。如何保证同一套交互代码在两种不同的数据格式下都能工作?

Uni-Agent 的方案是双 Model 后端共享 AgentInteraction

难点 5: 环境资源的管理与隔离

每个 Agent 需要独立的执行环境。1000 个 Agent = 1000 个容器(内存、CPU、磁盘)。环境可能崩溃、超时、资源耗尽。不同任务可能需要不同的 Docker 镜像。容器的生命周期需要与 rollout 同步管理。Uni-Agent 通过分层错误处理 + mask_abnormal_exit_traj + timeout_budget + 探活机制来应对。

难点 6: 大规模并发稳定性

上千个 sandbox 同时运行会引发资源争抢和端口耗尽。Uni-Agent 的方案是 Semaphore 限流 + Ray worker pool + per-worker 隔离。

难点 7: Off-policy 与 Staleness(参数过期)

为了提高 GPU 利用率,推理和训练需要异步执行(Fully Async 模式下 rollout worker 和 training worker 异步运行)。但这导致 Agent 使用的模型参数可能已过期,Importance sampling ratio 的估计变得不准确,训练稳定性下降。Uni-Agent 借助 verl 的 staleness_threshold 三层防御来应对。

难点 8: Partial Rollout

慢 trajectory 不应阻塞整个 batch。verl 的 partial_rollout=True 允许部分完成的 rollout 先参与训练,Uni-Agent 对此完全透明------FullyAsyncLLMServerClient 在底层处理断点续传。

难点 9: Reward 的计算复杂性

传统 RLHF 的 reward 计算是毫秒级的(调用一次 reward model),但 Agentic RL 的 reward 计算需要在环境中跑测试套件、执行代码、对比结果,耗时为分钟级。Reward 计算本身需要环境支持,且必须在 Agent 交互完成后、环境关闭前完成。不同 benchmark 的评估逻辑完全不同。

难点 10: Credit Assignment 困难

一个 Agent 执行了 100 步,最终 reward 是 0(没解决 bug)。哪一步是关键错误?传统 RL 只有 trajectory-level 的标量 reward,无法精确归因到具体的 step 或 action。Token-level 的梯度无法区分"好的思考"和"差的行动"。这是当前整个 Agentic RL 领域尚未解决的开放问题。


0x03 verl 的能力与系统性不足

理解了 Agentic RL 要解决的难点之后,我们需要审视 Uni-Agent 的底层依赖------verl------能提供什么,不能提供什么。这决定了 Uni-Agent 必须自己构建哪些能力。

3.1 设计范式差异

verl 的定位是通用 RL 训练框架:它擅长分布式训练、GRPO/GSPO、模型并行、异步调度与推理引擎集成,但并不内建 Agent 与环境交互本身。Uni-Agent 则把注意力放在多轮交互、工具调用、环境生命周期、奖励评估时序和 token-level 轨迹构建上。两者的关系可以概括为:verl 提供骨架,Uni-Agent 负责把骨架变成可运行、可训练、可扩展的 Agent 系统。

3.1.1 本质矛盾

verl 的核心假设源自 RLHF 场景,为"单轮对话 RLHF"而生:

维度 RLHF Agentic RL
交互轮数 1 轮 10-500 轮
Response 时间 秒级 分钟到小时级
Response 长度 几百 tokens 万~十万 tokens
环境依赖 容器 / 沙箱 / 云
失败模式 只有生成问题 环境崩溃、超时、格式错误、资源耗尽
Reward 计算 调 reward model 在环境中执行验证
数据组成 纯模型输出 模型输出 + 环境反馈混合

可以看到,verl 的设计假设在 Agent 场景下被系统性打破------几乎每一个维度都发生了质变。这不是 verl 的缺陷,而是"单轮场景"和"多轮场景"之间的根本差异。

3.1.2 分工边界

因此,Uni-Agent 与 verl 的分工非常清晰:

  • verl = 训练引擎("怎么训练"):分布式调度、GPU 管理、PPO/GRPO 算法、Fully Async 策略
  • Uni-Agent = Agent 抽象栈("训练什么"):多轮交互逻辑、Tool 执行、Sandbox 管理、Reward 计算

分界线是 AgentLoopBase.run() + AgentLoopOutput------一个方法签名,一个输出结构。verl 提供 RL 训练引擎,Uni-Agent 提供 Agent 行为层(环境 + 交互 + 工具 + 奖励 + 胶水),两者通过 AgentLoopOutput 协议连接。

3.1.3 详细分工表

关注点 verl 负责 Uni-Agent 负责 边界机制
Agent 实例创建 hydra.utils.instantiate YAML 中 _target_ _agent_loop_registry
LLM 推理 LLMServerClient.generate()TokenOutput 封装为 model.query() self.server_manager
Tokenizer AutoTokenizer 加载 + 注入 tokenize/decode/boundary探测 self.tokenizer
RL 算法 PPO/GRPO/GSPO loss + optimizer 不感知 AgentLoopOutput
Fully Async FullyAsyncRollouter 全模块 不感知(训练脚本配置) verl 内部
Partial Rollout FullyAsyncLLMServerClient resume 透传 max_global_steps TokenOutput.extra_fields
多轮交互逻辑 不提供 (ToolAgentLoop 是另一实现) AgentInteraction.run() run() 内部
Tool Schema verl.tools.schemas 数据类 6 种 tool + bash 脚本 使用 verl 数据类
Tool 执行(沙箱) 不提供 ToolsManager + AgentEnv 纯 Uni
Sandbox 管理 不提供 4 种 DeployConfig 纯 Uni
Token 对齐 AgentLoopOutput.response_mask rollout_cache 增量 + boundary AgentLoopOutput
Reward 计算 reward_score 字段消费 AbstractRewardSpec 4 种 reward_score 字段
Worker 调度 AgentLoopManager Ray pool 不感知 verl 内部
数据批次 DataProto 协议 填充 raw_prompt + tools_kwargs kwargs 透传
Padding _agent_loop_postprocess() 不感知 verl 内部
异常分类 不提供 9 种 exit_reason + 5 层错误 纯 Uni
Dashboard wandb/tensorboard dashboard/ Web UI 各自独立
Tool Parse verl 内置 ToolParser XMLToolParser+HermesToolParser Uni 使用自己的 parser

3.2 verl 为 Agentic RL 做了什么

尽管 verl 的设计范式源自 RLHF,但它确实为 Agentic RL 提供了关键的专用能力。这些能力可以分为四个维度:

训练架构
• Fully Async Pipeline rollout/train 解耦,最大化 GPU 利用率
• Partial Rollout 不等最慢轨迹,快速利用已完成数据
• Parameter Sync 定期同步 + staleness 控制
算法 / 损失
• GSPO 序列级 clip,适应长轨迹
• Token-Mean Loss 按 token 加权,公平对待不同长度轨迹
• Dynamic Batch Size 按 token 总量组批,平衡 GPU 负载
• DAPO Reward Manager overlong buffer + 长度惩罚
• N-Response/Prompt Group 内多采样 + advantage normalization
推理引擎优化
• Multi-Turn Mode 推理引擎感知多轮模式
• Chunked Prefill 超长 context 分块处理,防 OOM
• Free Cache Engine rollout 后释放显存给训练
• Preemption Tracking 监控请求抢占,优化并发
• Routed Experts MoE expert 路由追踪
Staleness 控制
• staleness_threshold 数据过旧直接丢弃
• clip_ratio (极小值) 限制 importance sampling 偏差
• trigger_sync_step 控制参数传播频率

这些功能对应的实体如下表:

# 功能 Uni-Agent 使用方式
1 AgentLoopBase ABC 继承,实现 run()
2 AgentLoopOutput 填充 9 字段
3 AgentLoopMetrics 填充 metrics
4 num_turns 字段 填充 turn 数
5 extra_fields 双端通道 注入 traj_masked,exit_reason
6 multi_turn rollout 配置 enable=True
7 max_parallel_calls 设为 1
8 FullyAsyncRollouter 直接使用
9 FullyAsyncLLMServerClient 直接使用
10 staleness_threshold 训练脚本配置
11 partial_rollout 训练脚本配置
12 trigger_parameter_sync_step 训练脚本配置
13 verl.tools.schemas 使用 ToolCall 等
14 ToolResponse (多模态) 不直接使用
15 apply_chat_template() 使用(带 tools=)
16 normalize_token_ids() 5 处调用
17 AgentLoopManager 训练+推理都使用
18 _agent_loop_registry 通过 config_path 注入

3.3 verl 的不足

在充分肯定 verl 提供的能力之后,我们需要诚实地审视它的不足。verl 的"留白"正是 Uni-Agent 存在的理由。

3.3.1 verl 没做什么(= Uni-Agent 自己构建)

几个主要缺失能力举例如下:

缺失能力 为什么 Agentic RL 需要 Uni-Agent 如何补充
多轮 ReAct 循环逻辑 verl 的 ToolAgentLoop 是状态机,Uni 需要更灵活的设计 AgentInteraction.run() while 循环
Sandbox/容器管理 verl 不管理执行环境 AgentEnv + 4 种 DeployConfig
Tool 的 bash 执行 verl tool 是 Python 函数/类,无法在容器内隔离 Tool-as-Bash-Script + 容器上传
Agent 异常分类 verl 不知道 bash 超时 vs 格式错误 9 种 exit_reason + 5 层错误处理
双 Model 后端 verl 只提供 LLMServerClient AgentChatModel(训练)+ OpenAICompatible(推理)
SWE 特定 reward verl 的 reward 是通用的 AbstractRewardSpec + swe_bench/r2e_gym
实时 Dashboard verl 只有 wandb/tensorboard dashboard/ Web UI

3.3.2 具体不足分析

以下我们对 verl 的十项不足进行逐项深入分析。这些分析的目的不是批评 verl,而是帮助理解 Uni-Agent 的设计动机。

不足 1: 没有 Multi-Turn 交互的一等公民支持

verl 通过 AgentLoopBase.run() 提供了一个空接口------它的态度是"用户自己搞定多轮交互"。multi_turn.enable=True 只是一个开关,告诉引擎"这个 request 会多次追加 tokens",但不提供循环框架。Token 流的 prompt/response 分割完全由用户负责。这意味着每个 Agent RL 项目都要从头实现 multi-turn 循环,没有标准的 "step → observe → append" 模式可复用。

Uni-Agent 的补充方式:AgentInteraction 类 + rollout_cache 增量维护。

不足 2: 不理解"环境"概念

verl 的世界观里只有 Model(Actor/Critic/Ref),没有 Environment 抽象------它不知道需要启动容器、管理资源、安装工具。没有环境生命周期管理(start/run/close),也没有环境并发控制。verl 的 AgentLoopManager 只管"给 worker 派活",不管 worker 上跑了多少容器。1000 个 rollout 同时调用 env.start() 会导致资源爆炸。

Uni-Agent 的补充方式:AgentEnv + Deployment 多后端 + asyncio.Semaphore

不足 3: Rollout 时间分布假设不成立

verl 的 batch 机制(包括 asyncio.gather 等待全部完成的模式)假设所有 rollout 时间接近。但在 Agent RL 中,某些 bug 简单(3 步 submit,30 秒),某些 bug 复杂(跑满 100 步,30 分钟),batch 被最慢的 rollout 阻塞。虽然 partial_rollout=True 在 async mode 中部分解决了这个问题,但 Sync mode 中无解------必须等全部完成。

更深层的问题是:Agent 的执行时间不可预测(取决于任务难度 + 模型能力),传统 RL 的"固定步数"假设完全不适用。

不足 4: Token Credit Assignment 不够精细

verl 的 response_mask 只有 0/1 两档,能区分模型输出和环境输出,但不能区分 thought 和 action(都是 mask=1),不能提供 per-step reward(只有一个标量 reward_score),不能做时间衰减(所有 mask=1 的 token 权重相同)。GRPO 的 advantage 是 trajectory-level 的标量,所有 mask=1 的 token 共享同一个 advantage,无法告诉模型"你的第 3 步是关键错误"。

verl 目前的 workaround 是通过 loss_agg_mode="token-mean" 让不同长度的轨迹贡献正规化,但本质上还是 trajectory-level reward。

不足 5: 不支持 Action-Level Policy Optimization

verl 的 GRPO/PPO 工作在 token-level(loss = Σ_t [mask[t] * ratio[t] * advantage]),但 Agent 的决策单元不是 token,而是 action(一个 tool call 可能包含 50+ tokens)。Token-level ratio 不等于 action-level ratio;一个 action 中的 token 共享相同的"意图",但 log prob 分别计算;如果模型生成了正确的 function name 但参数有 typo,所有 token 被同样惩罚。

理想的 action-level 方案是将一个 action 中所有 token 的 logprob 求和作为一个整体来计算 ratio,但 verl 目前不支持。

不足 6: 推理引擎不为 Multi-Turn 优化

verl 的推理引擎(vLLM/SGLang)每次 generate() 调用本质上是一个独立请求,每次都传完整序列。100 步交互 = 100 次 prefill。即使有 prefix caching,cache miss 时也需要完整重算,且没有"session"概念------引擎不知道同一个 Agent 的多次 generate 是相关的。

理想的方案是 session-based generation:创建 session → 每步只传增量 → append observation,但 verl 的接口没有显式利用这种模式。

不足 7: 训练数据不均匀

verl 的 DataLoader 假设样本大小大致均匀,但 Agent 轨迹长度差异极大(2K ~ 128K tokens)。GPU 内存由最大的 sample 决定,这浪费了大量 padding。Dynamic batch size 部分缓解,但需要 per-sample padding,极长轨迹可能 OOM,但是,截断则意味着模型永远不会对长轨迹中后期的 token 计算梯度。

不足 8: 无法优雅处理 Rollout 失败

verl 的 AgentLoopManager 期望每个 rollout 都返回 AgentLoopOutput。如果失败,需要用户自行构造"empty output"(如 Uni-Agent 的 _build_empty_agent_output),没有"重试"机制,没有"替代 sample"机制。失败的 rollout 产生 garbage data(全 0 的 token ids + mask),占用了 batch 位置但不产生训练信号,高失败率 = 低训练效率。

不足 9: 参数同步对 Agent Rollout 的干扰

Fully Async 模式下,parameter sync 需要推理引擎加载新参数。如果 sync 恰好发生在 Agent 交互中间------Agent_A 正在第 50 步,推理引擎暂停 10-30 秒加载新参数,Agent_A 的环境一直在等(浪费容器时间),恢复后 Agent_A 继续第 51 步时用的是新参数。

结果:同一个 rollout 中前 50 步用参数 θ₀,后 50 步用参数 θ₁,response_logprobs 是混合的,importance sampling ratio 计算不准确。这是 async Agent RL 的固有困境,verl 对此无解。

不足 10: 缺少 Agent 特有的 Metrics

verl 的 training logger 跟踪的是通用 RL 指标(reward_score mean/std、response_length、KL divergence、loss),但 Agent RL 需要更细粒度的指标:per-task success rate、average number of turns、exit reason distribution、environment setup time vs interaction time、tool call distribution、format error rate 等。Uni-Agent 通过 extra_fields 传递了部分信息,但 verl 的 logger 不会自动展示这些,需要用户在 wandb/tensorboard 中 custom track。

3.4 小结

回顾整个分析,我们可以清晰地看到 verl 的设计范式和 Agent RL 需求之间的根本张力:

verl 的设计范式(RLHF-centric)

  1. Rollout 是短的、确定性时间的
  2. 所有 token 都是模型生成的
  3. 不需要外部环境
  4. Reward 由 reward model 即时返回
  5. 失败是少见的
  6. Batch 内样本大致等长
  7. 一次 generate 调用 = 完整 response

Agent RL 的实际需求

  1. Rollout 时间跨度大(30 秒~1 小时)
  2. Token 流是模型 + 环境交织的
  3. 需要管理容器 / 云环境的生命周期
  4. Reward 需要在环境中执行验证(耗时)
  5. 失败是常态(环境崩溃、超时、格式错误)
  6. 轨迹长度差异 >10x
  7. 需要 100+ 次 generate 调用 per rollout

verl 选择的策略是务实的:提供最小化的扩展接口(AgentLoopBase),让上层框架(如 Uni-Agent)处理所有 Agent 特有的复杂性。这让 verl 保持通用性,让专业框架处理专业问题。

verl vs Uni-Agent 能力矩阵如下


0x04 Uni-Agent 设计理念与核心功能

4.1 核心设计哲学

在理解了 verl 的边界之后,我们可以深入 Uni-Agent 自身的设计。Uni-Agent 的设计哲学是 "推理 = 训练,同一入口",具体体现在:

  1. 同一套 AgentInteraction 代码驱动纯推理(OpenAI API)和 RL 训练(verl vLLM/SGLang)两种场景
  2. 同一个 rollout_cache 数据结构在两种场景下承载不同的信息密度:训练侧有 token IDs + logprobs + mask,推理侧只有 API messages
  3. 同一套配置体系(YAML → Pydantic)覆盖从单机调试到千卡训练的规模跨度

这个哲学可以进一步拆分为五大设计理念:

理念 1: 推理即训练(Inference = Training)

同一套代码和交互逻辑,既用于大规模推理/验证,也用于 RL 训练的 rollout 阶段。不存在两套系统、两份代码。技术保障:rollout_cache 始终记录 token IDs 和 log probs,推理时忽略、训练时直接用。

理念 2: 三层解耦(Model / Tool / Env)

Agent 的三个核心要素独立定义、通过配置组合。换模型不改工具,换环境不改交互逻辑------这是"可扩展性"的工程保障。三层抽象如下:

理念 3: 配置驱动(Config-driven)

所有实验差异体现在 YAML 配置文件中。同一份代码 + 不同配置 = 不同实验。这消除了"训练脚本"和"推理脚本"的代码重复。

具体配置体系如下:

层级 来源 内容
verl Hydra ppo_megatron_trainer.yaml 训练超参、模型路径、分布式 GPU
Agent YAML agent_loop_config_path tool 列表、deployment、max_turns
运行时注入 _init_config() + tools_kwargs model client/tokenizer、镜像覆写

合并流程:verl 注入 config/tokenizer/server_manager → 读取 Agent YAML → tools_kwargs 覆写 per-sample 参数(如不同 sample 使用不同的 Docker 镜像)。

配置融合的数据流如下:

理念 4: 异步并发优先(Async-first)

核心交互循环和环境操作全部基于 asyncio。通过 Semaphore 精确控制并发度,支持 1000+ Agent 稳定运行。

并发模型如下:

python 复制代码
_semaphore: asyncio.Semaphore | None = None  # 类级别共享
worker_concurrent = max(global_concurrency // num_workers, 1)
  • 类级别 Semaphore 实现全局并发控制------同一 JVM/进程内的所有 UniAgentLoop 实例共享同一个信号量
  • Per-worker 限流 = global_concurrency / num_workers------确保每个 verl worker 的负载均匀
  • verl 的 AgentLoopManager 通过 Ray 将 worker 分布到不同节点,Uni-Agent 在 worker 内部再用 Semaphore 做精细限流,形成两层并发控制

理念 5: 最小侵入性

  • verl 作为 git submodule,不修改其源码
  • 新工具通过注册机制添加
  • verl 通过 _target_ 路径发现 Uni-Agent,零耦合

4.2 和用户的分工

核心设计哲学:用户只需定义"做什么" (tools + reward + data), Uni-Agent框架负责"怎么做" (交互循环 + 训练 + 并发)。

4.2.1 用户不需要做的

组件 为什么不需要动
AgentInteraction 交互循环 通用的 model→tool→env 循环, 任务无关
AgentChatModel / rollout_cache 自动处理 tokenization、mask、log_probs
UniAgentLoop 自动编排 env.start → interact → reward → output
verl 训练基础设施 异步训练、参数同步、GRPO/GSPO 算法都是通用的
并发控制 Semaphore 自动管理

4.2.2 用户需要做的

用户典型自定义工作量

场景 用户需要做的事 工作量
用 SWE-Bench 训练新模型 改训练脚本的 MODEL_PATH + 集群参数 ~10 分钟
用新 benchmark (如 HumanEval) 写数据预处理 + 写 Reward ~半天
加新工具 (如 web search) 写 Tool 类 + 可执行脚本 ~2-4 小时
用新沙箱平台 写 Deployment backend ~1-2 天
全新 agent 任务类型 Tool + Reward + 数据 + Config ~1-3 天

训练脚本关键配置 (train_qwen3p5_dense.sh)

配置 作用
rollout.mode=async 异步 rollout 训练和 rollout 完全解耦
rollout.multi_turn.enable=True 多轮交互 每个 rollout 是完整的 agent 轨迹
rollout.agent.agent_loop_config_path YAML 配置 指定 UniAgentLoop 的配置
partial_rollout=True 部分 rollout 不需要等所有样本完成就可开始训练
n_resp_per_prompt=8 每个 prompt 8 次采样 GRPO 需要多个 response 对比
staleness_threshold=1.0 异步训练阈值 控制 rollout 数据的"过期度"

0x05 Uni-Agent 对 verl 的改造

Uni-Agent 通过 verl 的 AgentLoopBase 抽象类 + Hydra _target_ 注册机制接入,verl 作为只读 git submodule 保持不变。

  • verl = 通用 RL 训练框架(面向单轮 RLHF 设计)
  • Uni-Agent = Agent 行为层(面向多轮 Agentic RL 设计)
  • 集成方式 = 纯扩展点利用,零代码修改 verl

5.1 核心改造原则

Uni-Agent 对 verl 的改造遵循 "最小侵入,最大复用" 原则:

python 复制代码
verl 代码:        零行修改 (以 submodule 方式引入, 不修改源码)

verl 接口使用:
  ┌─────────────────────────────────────────────────────┐
  │  agent_loop.py  ←── 唯一的 verl 强依赖点              │
  │  (AgentLoopBase, AgentLoopOutput, resolve_config)   │
  │                                                     │
  │  interaction/model.py ←── verl 工具库依赖             │
  │  (chat_template, tokenizer, profiler)               │
  │                                                     │
  │  interaction/tool_parser.py ←── verl tools.schemas  │
  │  interaction/tools_manager.py                       │
  │  interaction/interaction.py                         │
  └─────────────────────────────────────────────────────┘

其余模块 (env, reward, deployment, tools, dashboard): 零 verl 依赖, 纯 Uni-Agent 代码

这种分层的实际意义是:如果未来需要更换 RL 训练框架(如 OpenRLHF),只需要重写 agent_loop.py 中的 UniAgentLoopinteraction/ 层及以下全部可以复用。这是一种务实的"框架解耦保险"。

5.1.1 改造决策

为什么这样设计?

设计决策 原因
不修改 verl verl 是社区项目,保持上游同步能力;uni-agent 可独立升级
不继承 ToolAgentLoop verl 的 ToolAgentLoop 假设 tool = 本地 Python 函数;uni-agent 需要远程沙箱,设计基础不同
直接继承 AgentLoopBase 最小耦合面---只需实现 run(),其余自由发挥
独立的 Model 抽象 verl 直接调用 server_manager.generate(); uni-agent 需要双模式(训练+推理)+rollout cache 管理
独立的 Tool/Reward 系统 verl 的 FunctionTool 和 RewardManager 不支持沙箱内执行;uni-agent 完全重建这两层

5.1.2 核心改造手段

在此基础上,核心改造手段如下:

  • 插件式接入:不修改verl,通过扩展点注册,保持上游同步能力
  • 推理即训练:同一套AgentInteraction代码,双模式 Model抽象实现无缝切换
  • 三层解耦:Model/Tool/Env独立定义、配置组合,换一个不影响其他
  • 配置驱动:所有实验差异体现在YAML中,代码不变
  • async-first:全链路异步 +Semaphore 精确限流,支持 1000+ 并发 agent
  • 窄接口面:与verl 主要通过AgentLoopBase,AgentLoopOutput连接,另依赖少量verl 工具函数(resolve_config_path、apply_chat_template、normalize_token_ids、OpenAIFunctionTool*schemas)

因此,Uni-Agent 是 verl 的一个外挂 Agent Loop 插件,通过 Hydra + config 注册机制接入,利用 verl 的数据协议和训练引擎,但在 agent 行为层(环境、工具、交互、reward)完全自建

架构关系图如下:

5.2 工作分类总览

Uni-Agent 的工作分为 5 个核心架构层 + 2 个工程支撑层:

分类 模块 解决的问题 verl 未提供的能力
🔵 环境管理 deployment/ (Local, Modal, VeFaaS, Host) Agent 需要持久化、隔离的执行环境 verl 只有本地 Python 函数调用,无容器/云沙箱
🔵 环境管理 interaction/env.py (AgentEnv) 统一的沙箱操作接口(run_action, read_file, write_file) verl 无文件系统操作抽象
🟢 交互循环 interaction/interaction.py (AgentInteraction) 多步 model→tool→env 循环 + 超时/错误恢复 verl 的 ToolAgentLoop 太简单,无超时预算、terminal 死亡检测
🟢 交互循环 interaction/model.py (AgentChatModel) Token 级别 rollout cache + 推理/训练双模式 verl 只有 server_manager.generate() 的直接调用
🟢 交互循环 interaction/model.py (OpenAICompatibleChatModel) 推理场景复用相同交互代码 verl 的 agent loop 只支持训练模式
🟡 工具系统 tools/ (bash, editor, search, submit, finish) 真实 agent 工具(在沙箱中执行脚本) verl 的 FunctionTool 是 Python 函数,不是沙箱脚本
🟡 工具系统 interaction/tools_manager.py + tool_parser.py Tool schema 管理 + 多格式解析(XML, Hermes) verl 也有 parser,但 uni-agent 重写以适配沙箱执行模式
🔴 Reward 计算 reward/ (swe_bench, r2e_gym, swe_rebench, search) 在沙箱内运行测试得到 reward verl 的 RewardManager 是外部 reward model 打分,不支持沙箱内 eval
🟣 训练集成 agent_loop.py (UniAgentLoop) 将上述所有组件粘合到 verl 的 AgentLoopBase 接口 这是 uni-agent 的"胶水层"
⚪ 工程支撑 async_logging.py, utils.py, dashboard/ 日志、工具函数、监控面板
⚪ 使用配方 examples/ 训练/推理脚本、数据预处理、配置模板

5.3🔵 环境管理层 - 解决 "在哪里执行"

5.3.1 解决的问题

Agent 需要在持久化、隔离的执行环境中运行命令、安装依赖、编辑文件、保持状态跨步。

即,Agent 需要 cd /repo && pip install && edit file && run tests

→ 需要持久化文件系统 + 进程状态

→ 需要容器隔离(安全 + 可重现)

→ 需要支持本地/云端多种部署方式

5.3.2 verl 为何不能解决

verl 的世界观只有 Model(Actor/Critic/Reference) + 进程内工具。它:

  • 没有"环境"概念---不知道需要启动什么容器
  • 没有资源管理---不管端口分配、OOM、容器崩溃
  • 没有生命周期---不知道何时 start/close 沙箱
  • 内置的 ToolAgentLoop 支持 FunctionTool(无状态函数)和 BaseTool(有 create/execute/release 生命周期的状态工具),但所有执行都在 Python 进程内---无法操作远程文件系统或执行 bash 命令

5.3.3 Uni-Agent 的实现

python 复制代码
# 统一沙箱抽象
class AgentEnv:
    async def start()          # 启动沙箱
    async def install_tools()  # 安装工具脚本到 /usr/local/bin/
    async def run_action()     # 执行 bash 命令
    async def communicate()    # 执行命令(支持 timeout、check 级别、error 处理)
    async def read_file()      # 读取沙箱内文件
    async def write_file()     # 写入沙箱内文件
    async def close()          # 销毁沙箱

# 4 种部署后端(Pydantic Discriminated Union)
DeployConfig = Annotated[
    VefaasDeploymentConfig | LocalDeploymentConfig |
    HostDeploymentConfig | ModalDeploymentConfig,
    Field(discriminator="type")
]

具体后端如下:

后端 适用场景
Host 本地开发,无隔离
Local 本地容器
Modal 云端弹性沙箱
VeFaaS 企业级 FaaS

关键文件: uni_agent/deployment/config.py, uni_agent/deployment/{host,local,modal,vefaas}/deployment.py, uni_agent/interaction/env.py

5.4 🟢 交互循环层 - 解决 "如何交互"

5.4.1 解决的问题

  • 多步交互循环: model→parse→execute→observe→repeat,带完善的退出条件和错误恢复
  • 训练数据增量构建: 每步自动维护 token IDs、log probs、response mask
  • 推理训练一体: 同一套交互代码,底层切换 vLLM(训练)或 OpenAI API(推理)
    • 训练模式需要 token IDs + log probs + mask
    • 推理模式只需要 API 调用 + 结果
    • → 两套数据格式,同一套交互逻辑

5.4.2 verl 为何不能解决

  • verl 的内置 ToolAgentLoop 有完整的多轮状态机(PENDING→GENERATING→PROCESSING_TOOLS→TERMINATED),但它的工具执行限于进程内 Python 对象(FunctionTool 直接调用 or BaseTool 的 create/execute/release 生命周期)--- 无法执行远程 bash 命令、操作沙箱文件系统
  • verl 没有超时预算(timeout_budget)递减机制、terminal 死亡检测、format_error 恢复等面向真实环境的鲁棒性处理
  • verl 只有 server_manager.generate() 的直接调用 --- --- --- 没有推理模式的 API 抽象
  • verl 没有 message_boundary_tokens 机制 --- 不知道如何正确拼接多轮对话的 token 序列(verl 使用 apply_chat_template 全量重编码)
  • 真实 agent 交互有各种异常 → 命令超时、终端死亡、token 超限、格式错误

5.4.3 Uni-Agent 的实现

交互循环 (AgentInteraction):

python 复制代码
loop step(idx):
1. model.query(messages) → response + rollout_cache
2. tools_manager.parse_action() → tool_calls(文本解析 or 结构化)
3. tools_manager.get_tool_bash_command() → bash 命令
4. env.run_action(cmd) → observation
5. model.append_messages_to_rollout_cache() → 更新训练数据
6. 退出条件:   →   多种错误处理
   - finished: tool 名为 finish/submit
   - max_step_limit: step_idx >= max_turns
   - token_limit: MaxTokenExceededError
   - format_error: FunctionCallFormatError(可恢复,不终止循环)
   - syntax_error: ActionIncorrectSyntaxError(可恢复)
   - timeout_error: ActionTimeoutError(timeout_budget 递减,耗尽才终止)
   - terminal_not_alive: TerminalNotAliveError(不可恢复,终止)
   - unknown_error: 未预期异常(终止)

统一接口

AgentChatModel(训练)/ OpenAICompatibleChatModel(推理)用 统一接口:query() + append_messages_to_rollout_cache(),且 rollout_cache 是不透明 dict,交互层不感知内部格式

双模式模型 (AgentChatModel vs OpenAICompatibleChatModel):

接口方法 训练模式 (AgentChatModel) 推理模式 (OpenAICompatibleChatModel)
prepare_rollout_cache()
query() vLLM generate → token IDs → decode OpenAI API → text
append_messages_to_rollout_cache() tokenize + 拼 prompt_ids + mask=0 追加到 api_messages
返回值 (text, rollout_cache, info) 同上

注意: UniAgentLoop 训练路径固定使用 AgentChatModel。OpenAICompatibleChatModel 用于独立推理脚本(如 parallel_infer.py、demo.py),实现交互代码复用。

增量 Tokenize + Boundary 探测:

复制代码
@cached_property
def message_boundary_tokens(self) -> list[int]:
    # 通过差分对比自动提取消息间的分隔 token
    # tokenize("历史+新消息") - tokenize("新消息") = 分隔符

关键文件: uni_agent/interaction/interaction.py, uni_agent/interaction/model.py

5.5 🟡 工具系统层 - 解决 "如何动作"

5.5.1 解决的问题

Agent 需要通过工具(bash、文件编辑器、搜索)与环境交互,且:

  • 工具以可执行脚本形式安装到沙箱中(不是 Python 函数)
  • 不同模型有不同的 tool call 格式需要解析
  • 工具 schema 需要自动生成为 OpenAI 格式

5.5.2 verl 为何不能解决

verl 的 FunctionTool = Python 函数调用。而 真实 agent 工具 = 在沙箱中安装+执行脚本

verl 的工具系统支持两种模式:

python 复制代码
# 模式1: FunctionTool - 无状态函数调用

class FunctionTool:
    async def call(self, args: dict) -> ToolResponse: ...

# 模式2: BaseTool - 有生命周期的状态工具

class BaseTool:
    async def create(**kwargs) -> (instance_id, ToolResponse)
    async def execute(instance_id, args, agent_data) -> (ToolResponse, reward, dict)
    async def release(instance_id): ...

但两者都运行在 Python 进程内,无法满足:

  • 在远程容器中执行 bash -c "cd /repo && pytest"
  • 安装可执行脚本到 /usr/local/bin/str_replace_editor
  • 处理 bash 命令超时、输出截断、terminal 进程死亡
  • 工具以 shell 脚本形式存在于沙箱文件系统中

5.5.3 Uni-Agent 的实现

Uni-Agent 关于工具系统层的实现如下图所示。其中:

  • 内置工具: execute_bash, str_replace_editor, submit, finish, search, search_arxiv

  • 关键文件: uni_agent/tools/base.py, uni_agent/tools/registry.py, uni_agent/interaction/tools_manager.py, uni_agent/interaction/tool_parser.py

python 复制代码
解决:AbstractTool + @register_tool
每个 tool 有:
├─ __init__.py (schema 定义 + 注册)
└─ 可执行脚本(安装到沙箱 /usr/local/bin/)

ToolsManager:
├─ parse_action() → 从文本提取 tool call
└─ get_tool_bash_command() → 生成沙箱中执行的命令

# 工具注册
@register_tool("execute_bash")
class ExecuteBashTool(AbstractTool):
    @property
    def name(self) -> str: return "execute_bash"

    def get_tool_schema(self) -> dict:
        return build_tool_schema(description="...", arguments_model=ExecuteBashArguments)
    
    def get_install_command(self) -> str | None:
        return f"cp {self.local_path} /usr/local/bin/execute_bash && chmod +x ..."

# 工具解析 - 适配不同模型格式

parsers = {
    "qwen3_coder": XMLToolParser,    # <tool_call>...</tool_call>
    "hermes": HermesToolParser,       # ```json { "name": ..., "arguments": ... } ```
}

# 结构化 vs 文本解析双路径

if rollout_cache has structured_tool_calls:  # OpenAI API 返回
    parse_structured_action(tool_calls_data)
else:                                         # vLLM 纯文本输出
    parse_action(model_output)

5.5.4 Tool 执行管道

Tool 执行管道的数据流如下:

5.6 🔴 奖励计算层 - 解决 "如何评估"

5.6.1 解决的问题

Agent RL 的 reward 不是简单的模型打分,而是需要 在环境中执行验证(如运行 pytest),且:

  • 必须在 agent 修改后的同一个沙箱中运行,这是因为:
    • SWE-Bench: 需要运行测试套件,看 FAIL_TO_PASS 是否通过
    • Search: 需要对比答案正确性
  • 必须在沙箱关闭之前计算
  • 不同 benchmark 有完全不同的评估逻辑

5.6.2 verl 为何不能解决

verl 的 RewardManager(如 DAPO)假设 reward_score 已经算好了:

python 复制代码
# verl RewardManager 做的事:
# 1. 读取 rm_scores(已存在的分数)
# 2. 做 reward normalization / overlong penalty / group baseline
# 它不知道也不管 reward 是怎么来的

verl 的 reward 系统:

  • 无环境访问 --- 拿不到沙箱去跑测试
  • 计算时序错误 --- 在 rollout 结束后的训练阶段才调用,此时沙箱已关闭
  • 无注册机制 --- 不支持按 benchmark 切换评估逻辑

5.6.3 Uni-Agent 的实现

python 复制代码
# agent_loop.py: reward 在 env.close() 之前计算
async with self._semaphore:
    await self.env.start()
    await self.env.install_tools(...)
    interaction_result = await self.interaction.run()
    reward_score = await self.reward_spec.compute_reward(...)  # ← 此时沙箱还活着
    await self.env.close()  # ← reward 算完才关闭

# SWE-Bench reward 具体流程
class SWEBenchRewardSpec:
    async def compute_reward(self):
        # 1. 构造 eval 脚本(activate conda, apply test_patch, run pytest)
        # 2. 写入沙箱:env.write_file("/tmp/eval_script.sh", script)
        # 3. 在沙箱中执行:env.communicate("bash /tmp/eval_script.sh", timeout=600)
        # 4. 解析输出 → reward score:swebench log_parser → FAIL_TO_PASS 通过? → True/False
Reward 实现 评估方式 适用 benchmark
swe_bench 沙箱内运行 pytest
swe_rebench 沙箱内运行测试 + 多分支 SWE-reBench
r2e_gym 沙箱内运行 eval 脚本 R2E-Gym
search 答案字符串匹配 ASearcher

关键文件: uni_agent/reward/base.py, uni_agent/reward/registry.py, uni_agent/reward/swe_bench.py

5.7🟣 训练集成层(胶水层)

训练集成层 将上述 4 层粘合为 verl 可消费的标准 AgentLoopOutput。

5.7.1 verl 为何不能解决

verl 的 AgentLoopBase 只定义了 run() 接口 --- 具体如何编排组件、如何控制并发、如何处理异常,全部由实现者自行设计。

5.7.2 Uni-Agent 的实现

python 复制代码
class UniAgentLoop(AgentLoopBase):
    _semaphore: asyncio.Semaphore  # 类级别,per-worker-process 并发控制
	async def run(self, sampling_params, **kwargs) -> AgentLoopOutput:
        config = self._init_config(sampling_params, **kwargs)  # 三层配置合并
        self.chat_model = self._init_chat_model(...)
        self.tools_manager = self._init_tools_manager(...)
        self.env = self._init_env(...)
        self.interaction = AgentInteraction(...)
        self.reward_spec = load_reward_spec(...)

        async with self._semaphore:  # 并发限流
            try:
                await self.env.start()
                await self.env.install_tools(...)
                interaction_result = await self.interaction.run()
                reward = await self.reward_spec.compute_reward(...)
                return await self.convert_to_agent_output(interaction_result)
            except Exception:
                return await self._build_empty_agent_output("agent_loop_failed")
            finally:
                await self.env.close()

独特贡献:

  • 并发控制: Semaphore(global_concurrent // num_workers) per-worker-process 限流(注意:多个 Ray AgentLoopWorker 各自独立,非集群全局)
  • 异常降级: 失败时返回 dummy output(mask 全 0),不破坏训练 batch
  • 三层配置合并: verl config + agent_config.yaml + 样本级 tools_kwargs
  • traj_masked 标记: 让 verl 训练端可选择性忽略异常轨迹

关键文件: uni_agent/agent_loop.py

5.8 ⚪ 工程支撑层

组件 解决的问题 verl 不够的原因
async_logging.py 1000 并行 agent 的 per-run 独立日志 verl 只有全局 logger
utils.py (auto_await) sync/async 方法互操作 verl 没有此需求
dashboard/ 实时监控并行运行的状态 / 进度 / 异常 verl 的 wandb logger 不够细粒度
examples/data_preprocess/ 将 benchmark 数据转为标准 Parquet 每个 benchmark 格式不同
examples/agent_train/*.sh 完整的训练启动配方 verl 示例只有单轮 RLHF

5.9 一句话总结

verl 解决"如何高效训练 RL 模型",Uni-Agent 解决"如何让 Agent 在真实环境中完成多步交互并产出训练数据"。二者通过AgentLoopoutput协议连接:verl消费数据,Uni-Agent 生产数据。

架构全景图如下:

在此基础上,数据流满足 RL 需求的关键点如下:

RL 需求 Uni-Agent 如何满足
Token-level log probs AgentChatModel.query() 收集 token_output.log_probs → 存入 rollout_cache"response_logprobs"
Response mask (区分生成 vs 环境) response_mask: 模型生成部分=1, tool 输出/边界=0
完整 prompt + response 拼接 rollout_cache"prompt_ids" 逐步拼接所有 token
Reward signal RewardSpec.compute_reward() 在交互结束后运行测试得出分数
并行采样 asyncio.Semaphore 控制并发, verl 分发多个 prompt
异常轨迹处理 mask_abnormal_exit_traj + traj_masked 标记 → verl 训练时跳过
Partial rollout verl fully_async 模式支持不等所有 rollout 完成就开始更新

0xFF 参考