Agentic RL 强化学习 OPD OpenClaw-RL 源码阅读笔记 --- (11)--- 算法总体实现

时间:2026-07-24 07:52:50 来源:互联网

从OpenClaw-RL源码切入,系统梳理强化学习核心概念,本文聚焦算法总体实现与Binary RL基础。通过分析框架设计理念与关键细节,帮助读者理解在线强化学习在智能体工具使用场景中的应用与挑战。

  1. 0x00 概要

  2. 0x01 基础背景

    1. 1.1 架构
    2. 1.2 算法
  3. 0x02 Binary RL

    1. 2.1 设计理念
    2. 2.2 PPO
    3. 2.3 GRPO
    4. 2.4 具体配置
  4. 0x03 三种方法的损失函数

    1. 3.1 公共原语
    2. 3.2 Binary RL的损失函数
    3. 3.3 OPD 的损失函数
    4. 3.4 Combine 的损失函数: combine_loss_function
    5. 3.5 advantage 和 loss 的关系
  5. 0x04 优势的颗粒度

    1. 4.1 概念
    2. 4.2 方案
  6. 0x05 Token 级 vs 序列级:范式根因

    1. 5.1 奖励载体决定粒度
    2. 5.2 在线对话约束下的可行性
    3. 5.3 Combine 为何能融合两者
  7. 0x06 Credit Assignment

    1. 6.1 什么是Credit Assignment(信用分配)
    2. 6.2 Binary RL
    3. 6.3 OPD 解决了什么
    4. 6.4 Cascading Errors
    5. 6.5 OpenClaw对两者的应对
  8. 0xFF 参考

0x00 概要

本系列旨在通过对OpenClaw-RL源码的学习,系统梳理强化学习的相关概念与思想。因此,内容会涉及一些基础知识、扩展与发散,OpenClaw-RL仅作为一个切入点。由于整篇系列是一个整体,某些概念的解读和学习会在不同文章中分别出现,敬请理解。

OpenClaw-RL是一个面向在线强化学习(Online RL)的框架,专门针对智能体工具使用场景设计。它通过从环境反馈中提取过程奖励信号来训练语言模型,支持三种主要模式:

  1. openclaw-rl:基于二元奖励的强化学习(Binary RL / GRPO)
  2. openclaw-opd:基于后见之明提示的在线策略蒸馏(On-Policy Distillation, OPD)
  3. openclaw-combine:联合方法,在同一 PPO 更新中同时利用 RL reward 和 OPD teacher signal

img_6a62a952968c930.webp

framework

本篇主要介绍总体算法、Binary RL 的基础信息,以及一些关键的技术细节思考。

注:本文属于笔记性质,因此会涉及较多基础知识,篇幅可能较长。

0x01 基础背景

我们首先回顾一下基础背景。

1.1 架构

OpenClaw-RL 系统架构如下:

img_6a62a952b12d331.webp

11-OpenClaw-RL 系统架构

系统会从用户行为推断质量

 复制代码User sends next message → Indicates previous response was understood/useful 
User repeats question → Indicates previous response was unhelpful
 ↑
隐式信号,需要 LLM judge 解读 next_state 与 response 的关系

1.2 算法

Binary RL 采用的是 GRPO + PPO clip trick,而非完整 PPO(无 Critic)。代码中的“PPO”一词指的是 clip 机制,并非带 Critic 的完整算法。OPD 使用 on-policy distillation advantage,Combine 则是两者的加权融合。

README.md 中的描述如下:

 复制代码**Binary RL (GRPO):** A Process Reward Model scores each turn based on next-state feedback. The scalar reward is then used with GRPO advantage estimation and a PPO-style clipped surrogate loss.**On-Policy Distillation (OPD):** When the next state reveals useful hindsight, a judge model extracts a textual hint. This hint augments the original prompt to create an enhanced teacher, whose token-level log-probability gap with the student becomes a directional advantage signal richer than any scalar reward.**Combination Method:** OpenClaw-RL further combines Binary RL and OPD in a unified training recipe, leveraging the dense scalar supervision of Binary RL together with the richer token-level directional signal from OPD. This combination achieves stronger and more robust optimization than either method 
三种训练方法核心设计
Binary RLOPDCombine
优势估计GRPOon_policy_distillationGRPO(预计算)
Advantage 粒度Sequence(将标量奖励广播到所有响应token)Token(per-token差)两者线性叠加
奖励来源LLM Judge ±1/0,即PRM评分作为标量奖励(+1/-1/0)Teacher log-prob两者结合
KL 控制PPO clip only隐式(向teacher靠拢)两者叠加
Credit Assignment严重中等(级联问题)Combine对冲部分缓解
advantage 公式reward * onesteacher_lp - rollout_lpw_opd*(T-R) + w_rl*GRPO
Loss 函数Slime 内置 policy_loss_function / L=L_Pg+β_KL*L_KLSlime 内置 policy_loss_functioncombine_loss.py(自定义)
理论联系
  1. OPD

    1. ≈ Adaptive SFT (per-token weight)
    2. ≈ KD (Knowledge Distillation) with hindsight
    3. ≈ 最小化 KL(π_T(·|hint) || π_θ)
  2. Binary RL

    1. ≈ sequence-level REINFORCE (无 baseline)
  3. Combine

    1. ≈ 全局信号(GRPO)为锚 + 局部信号(OPD)做细化
    2. GRPO 的均匀惩罚隐式压制 OPD 的级联噪声
Slime支持

Slime支持的所有Advantage Estimator(OpenClaw-RL项目未全部使用)

estimator公式
grpo (Binary RL, Combine)adv[t] = reward (scalar broadcast) / GRPO 不normalize: --disable-rewards-norm
gspoGRPO 变体,全序列级 KL clip
on_policy_distillation (OPD)adv[t] = teacher_lp[t] - student_lp[t] / per-token 蒸馏,student=rollout_log_probs
step_wise多步骤 reward 按 token span 广播
ppo(未使用)需要 Critic + GAE + Value Loss / adv = GAE(V, r, v, λ)
reinforce_plus_plus reinforce_plus_plus_baselineREINFORCE++ 带 KL penalty / REINFORCE++ 带 baseline 减方差

0x02 Binary RL

Binary RL = GRPO + PPO clip + DAPO clip-higher,无value model、无KL 惩罚、无entropy bonus。

Binary RL对每一轮(turn)独立打分,而非对整个多轮对话统一打分。

 复制代码多轮对话(conversation):
    Turn 1: reward₁ (独立打分)
    Turn 2: reward₂ (独立打分)
    Turn 3: reward₃ (独立打分)单轮 Turn 1:
    token₁ token₂ token₃ ... tokenₙ
    [r₁] [r₁] [r₁] ... [r₁] ← GRPO 在一轮内广播 reward₁ 到所有 token

2.1 设计理念

为什么设计成“逐轮打分”?

  1. 技术原因: 打分需要 next_state(即用户下一句话),这在当前轮结束时才能得到,天然是逐轮粒度。
  2. 语义原因: PRM Judge 的 prompt 是 “given this response, did the user continue”。
  3. 工程原因: 每轮独立成为一个 Sample 进入训练队列,Megatron 处理的 batch 可以来自不同 session 的不同 turn,互相不依赖。
对话任务的奖励结构
场景适合哪种打分
游戏(推箱子)/ 代码执行Episode-level(只有最终成功/失败)
定理证明Episode-level(证明完整才有意义)
开放式对话(OpenClaw)Turn-level(每轮都有独立价值)
SWE bug 修复Episode-level(代码能运行才算)
技术原因

next_state 本身就是对话级的隐式信号。用户聊天不是“赢”或“输”,每一轮回复的质量都独立地影响用户体验。

 复制代码Turn N response → [PRM judge] ← Turn N+1 (user's next message)
          ↑
        next_state 就是用户对上一轮回复的真实反应
  1. 用户下一句话继续深入 → 模型回答有用 → score = +1
  2. 用户重复同样的问题 → 没听懂 → score = -1
  3. 用户沉默(session 结束)→ 无法判断 → score = 0

这比 episode-level binary signal 信息量更大:能在每轮感知用户体验,而非等整个对话结束。

工程上的现实因素
问题

在线学习的核心要求:尽快更新。而在工程上遇到了如下问题:

  1. 问题 1:对话长度未知,因此

    1. → 不知道何时该收集整个 episode 进行打分
  2. 问题 2:对话可能被用户中途放弃,因此

    1. → Episode 不完整,无法打 episode-level 分
  3. 问题 3:批次填充问题,因此:

    1. → Episode-level 到达训练队列是脉冲式的(对话结束时才来)
    2. → 逐轮样本持续到达,GPU 利用率更平稳

比如下图所示,延迟越长,更新时的权重越 off-policy,PPO clip 的约束越容易被违反。

 复制代码逐轮打分: Turn 1 → scored → 进训练队列 → 模型更新
         Turn 2 → scored → 进训练队列 → 模型更新
         ...(持续在线)
         
Episode 打分:Turn 1 ___________________________|
    Turn 2 ___________________________|
    Turn N → 对话结束 → scored → 一批更新 ←┘
    延迟 = 整个对话时长
广播机制

因此,Binary RL使用了广播机制,将单个标量值复制到响应序列中的所有 token位置。

在强化学习中,优势值 SA_t衡量的是在时间步 t 采取特定动作相对于平均策略的优劣程度。在 OpenClaw-RL的上下文中: SA_t=rS:优势值直接等于标量奖励,即标量奖励 rS:来自 PRM(过程奖励模型)的评分,通常为 +1、-1或0。

由于 PRM只能对整个助手响应进行整体评分,而训练需要在每个 token 级别计算梯度,因此必须将标量奖励广播到所有相关 toke n。

OpenClaw-RL的优势值广播机制是一种巧妙的设计,它将复杂的强化学习问题简化为可操作的训练流程。通过将标量PRM评分厂播到所有响应 token,系统能够在保持计算效率的同时,有效地利用自然用户交互作为训练信号。这种设计体现了 OpenClaw框架在实用性、效率性和可扩展性之间的平衡,使其能够适应真实世界的大规模部署需求

2.2 PPO

虽然代码里policy 更新用的是PPO-style clipped surrogate objective:

 复制代码    ratio = exp(log T_new -log T_old)
    pg_loss = -min(ratio * adv, clip(ratio,1-e,1+e_high) * adv)

但这只是“PPO clip技巧”,不是完整PPO。

完整PPO需要:

  1. ①一个独立的Critic网络估计V(s)
  2. ②GAE (Generalized Advantage Estimation):A_t=δ_t+γλδ_{t+1} + ...,
  3. ③Value Loss 训练 Critic

OpenClaw-RL 没有 Critic;reward直接广播到所有token → 这是GRPO/REINFORCE 风格

2.3 GRPO

因此,OpenClaw 的 Binary RL 使用的是 GRPO(不是 PPO)。

代码证据:

  1. 无 value model / critic—GRPO 的标志性特征:slime/slime/utils/ppo_utils.py L207: returns.append(torch.ones_like(kl[i])
  2. rewards[i]) – reward 标量直接广播为 advantage,没有 GAE / value baseline
  3. 运行脚本中未启用 critic:openclaw-rl/run_qwen3_4b_openclaw_rl.sh 使用 --disable-rewards-normalization, advantage = raw reward (±1 直接用)
  4. 但使用了 PPO 的 clip 机制:eps_clip=0.2 / eps_clip_high=0.28 (DAPO 风格 clip-higher)

所以准确说是:GRPO advantage + PPO-style clip + DAPO clip-higher = “GRPO with DAPO tricks”, 不是原版 PPO。

特征PPOGRPOOpenClaw Binary RL
Value model
AdvantageGAE(R-μ)/σraw R (±1 broadcast)
Surrogate clip ε 对称 ε 对称 ε 非对称(DAPO)
KL penalty β>0通常 coef=0

2.4 具体配置

Binary RL = GRPO(无 value model,组内相对比较) + PPO clip(信任域约束) + DAPO clip-higher(恢复低概率正样本梯度)。

不是标准 PPO(没有 value model),不是纯 REINFORCE(有 clip 约束),不是纯 GRPO(有 DAPO 的非对称 clip 改进) = GRPO with DAPO tricks

GRPO + DAPO clip-higher,具体配置:

组件具体实现代码位置
算法框架GRPO (Group Relative Policy Optimization)--advantage-estimator grpo
AdvantageA_t = R (raw reward ±1 broadcast 到所有 token)ppo_utils.py
梯度更新PPO-style clipped surrogateppo_utils.py
Clip 范围下界 ε=0.2, 上界 ε_high=0.28 (DAPO 非对称 clip)--eps-clip 0.2 --eps-clip-high 0.28
KL 惩罚计算但不参与梯度 (coef=0.0)--kl-loss-coef 0.0
Reward 标准化关闭 (raw ±1 直接用)--disable-rewards-normalization
Entropy bonus关闭--entropy-coef 0.0
Value modelGRPO 特征

0x03 三种方法的损失函数

我们接下来看看三种方法的损失函数。

先看看源码中的注释:

 复制代码"""Joint loss for mixed RL (GRPO) + OPD (distillation) batches.Both branches use the same PPO-style clipped policy gradient objective,
matching SLIME's ``policy_loss_function``, but with different advantages:- OPD samples: advantage = teacher_logp - old_logp  (token-level distillation)
- RL  samples: advantage = reward broadcast          (GRPO-style)OPD samples carry reward=0 so GRPO advantage=0; RL samples carry
teacher_logp ≈ rollout_logp so teacher advantage ≈ 0.  The combined
advantage is simply their sum, and each branch naturally dominates for
its own sample type.
"""

三种方法的损失函数对比

advantage 来源信号粒度
Binary RLreward scalar → 广播全 token序列级 (无 credit)
∈ {+1.0, 0.0, -1.0}
OPDteacher_lp - rollout_lpToken 级 (per-token)
per-token 浮点差值
Combinew_opd*(teacher-old)两级信号叠加
+ w_rl*grpo_adv
共同的 loss 形式PPO clip + 熵正则 + (KL)完全相同

三种方法共用同一个 PPO clip 框架,区别仅在于 combined_advantages 张量里装的内容不同。

3.1 公共原语

以下是公共部分。

 复制代码@torch.compile(dynamic=True)
def compute_policy_loss(
    ppo_kl: torch.Tensor,
    advantages: torch.Tensor,
    eps_clip: float,
    eps_clip_high: float,
    eps_clip_c: float | None = None,
):
    # ppo_kl = old_log_probs - new_log_probs
 # ratio=π_θ(a|s)/ π_old(a|s),即重要性采样比率
    ratio = (-ppo_kl).exp()
    # 无clip的 PGloss:-ratio * A
    pg_losses1 = -ratio * advantages
    # clip后的PGloss:ratio被截断到[1-e,1+e_high] 再乘A
    # e=0.2,e_high=0.28(asymmetric clip,允许轻微超过)
    pg_losses2 = -ratio.clamp(1 - eps_clip, 1 + eps_clip_high) * advantages
    # 取两者的逐元素最大值(即取悲观值),这是PPOclip的核心
    # 当A>O:ratio太大时被截断,防止步子过大
 # 当A<O:ratio太小时被截断,防止对负样本惩罚过轻
    clip_pg_losses1 = torch.maximum(pg_losses1, pg_losses2
    # clipfrac:记录有多少token触发了clip(用于监控)
    clipfrac = torch.gt(pg_losses2, pg_losses1).float() # dual-clip(可选):当A<0时进一步约束下界,避免负advantage样本
    # 驱使ratio无限增大;eps_clip_c>1.0
    if eps_clip_c is not None:
        assert (
            eps_clip_c > 1.0
        ), f"The lower bound of the clip_ratio_c for dual-clip PPO should be greater than 1.0, but get the value: {eps_clip_c}."
        pg_losses3 = -eps_clip_c * advantages
        clip_pg_losses2 = torch.min(pg_losses3, clip_pg_losses1)
        pg_losses = torch.where(advantages < 0, clip_pg_losses2, clip_pg_losses1)
    else:
        pg_losses = clip_pg_losses1    return pg_losses, clipfrac #shape:[T_total],每 token 一个值

3.2 Binary RL的损失函数

RL方法使用 Slime 内置的policy_loss_function,advantage 由 compute_advantages_and_returns 中的 GRPO分支预先计算好。

 复制代码# ==============================================
# Step0(前置):GRPO advantage计算在rollout阶段完成
# ==============================================
def get_grpo_returns(
    rewards: torch.Tensor,
    kl: list[torch.Tensor],
):
    returns = []
    for i in range(len(rewards)):
        # rewards[i] 是标量(±1.0 或 0.0)
        #kl[i]是长度为T_i的per-tokenKL向量
        #torch.ones_like(kl[i])* rewards[i]:
        # 把标量reward广播为全token 都相同的advantage
        # →序列级信号,无credit assignment
        returns.append(torch.ones_like(kl[i]) * rewards[i])
    return returns# ==============================================
# Step1:前向传播,计算当前策略的log-probs
# ==============================================
# policy_loss_function -RL 路径
advantages = torch.cat(batch["advantages"], dim=0)
# advantages shape:[T_total],每个token的值都等于其所在序列的reward
old_log_probs=batch["rollout_log_probs"# rollout 时记录的旧策略log-probs
# 调用Megatron 的 fused cross entropy计算当前log-probs
_,log_probs_and_entropy = get_log_probs_and_entropy(
    logits,...
 # logits:[1,T,V],Megatron tensor-parallel 分片的 vocab logits
    # 内部调用 fused_vocab_parallel_cross_entropy
)                                                    # ==============================================
# Step2:计算per-token KL(即IS ratio的log形式)
# ==============================================
old_log_probs = torch.cat(old_log_probs,dim=0)
log_probs = torch.cat(log_probs_and_entropy["log_probs"], dim=0)# ppo_kl =logπ_old(a|s)-log π_θ(a|s)
#        =log(π_old / π_θ)=-log(π_θ/ π_old)=-log(ratio)
ppo_kl = old_log_probs - log_probs # shape:[T_total]# ==============================================
# Step3:PPO clip→聚合→加熵正则
# ==============================================
pg_loss,pg_clipfrac = compute_policy_loss(ppo_kl,advantages,
           args.eps_clip,#0.2
           args.eps_clip_high) # 0.28
# sum_of_sample_mean:先对每个样本内的token求均值,再对样本求和
# 等价于对有效token(loss_mask=1)的加权平均 
pg_loss =sum_of_sample_mean(pg_loss)
# entropy_coef通常>0,鼓励探索
# 用-entropy_coef*H(π)作为正则项
entropy_loss =sum_of_sample_mean(entropy)
loss = pg_loss - args.entropy_coef * entropy_loss
# 可选:KL散度惩罚(本项目kl_coef=0,不启用)
# 可选:ref_modelKLloss(use_kl_loss=True时启用)

3.3 OPD 的损失函数

OPD的损失函数分两步:advantage 计算在compute_advantages_and_returns的on_policy_distillation 分支,loss计算仍走 policy_loss_function(同一个函数)

 复制代码# ======================================================
# Step 0(前置):OPD advantage = teacher_lp - student_1p
# ======================================================
#loss.py:433-on_policy_distillation 分支
elif args.advantage_estimator == "on_policy_distillation":
 student_log_probs=log_probs # rollout时SGLang返回的旧策略log-probs
 teacher_log_probs =rollout_data.get("teacher_log_probs")
    # 来自OpenClawOPDAPIServer._compute_teacher_log_probs():
 # 教师forwardpass(max_new_tokens=0),返回response位置的per-tokenlog-probs 
    
    teacher_log_probs =[t.to(device) for t in teacher_log_probs]
 # teacher_log_probs 的原始长度=prompt+response(含promptlogprobs)
    # 这里截取最后response_length个token,对齐response位置 
    teacher_log_probs=[
        t_lp[-response_length:]
        for t_lp, response_length in zip(teacher_log_probs, response_lengths)
    ]
    # per-token advantage =log π_teacher(a_t|s,a_<t)-log π_old(a_t|s,a_<t)
    # 含义:教师比学生"更喜欢"这个token的程度
    # >0 → 教师倾向此token → 策略更新朝此方向
    # <0 → 教师不倾向此token → 策略更新远离此方向 
    advantages =[
        teacher_lp -student_lp
        for teacher_lp,student_lp in zip(teacher_log_probs,student_log_probs)
    ]
    returns = advantages# ======================================================
# Step1-3:与RL完全相同的policy_loss_function
# ======================================================
# OPD 也走 policy_loss_function, 代码完全相同
# 区别仅在于 batch["advantages"] 里装的是 token-level teacher-student diff
# 不是 scalar broadcast 的 reward# 效果等价于:
# ∇L_OPD ≈ E_t [ (π_teacher/π_old - clipped) * ∇log π_θ(a_t) ]
# 即用教师 log-prob 差值作为 per-token 的重要性权重                             

3.4 Combine 的损失函数: combine_loss_function

Combine 跳过 Slime 的 advantage 估算, 自己在 loss 函数内合并两路 advantage。

 复制代码def combine_loss_function(args, batch, logits, sum_of_sample_mean):
    # —— 1. 读取 GRPO advantages (Slime 预计算, 序列级广播) ——————————————————
    grpo_advantages = torch.cat(batch["advantages"], dim=0)
    # shape: [T_total], 每 token = 该序列的 reward scalar    # —— 2. 决定用哪组旧策略 log-probs ——————————————————
    old_log_probs_list = (
        batch["rollout_log_probs"]   # rollout 时 SGLang 实时记录的
        if args.use_rollout_logprobs
        else batch["log_probs"]      # Megatron 前一轮 forward 的 (更精确但开销大)
    )
    
    # —— 3. 当前策略 forward pass → new_log_probs ——————————————————
    _, log_probs_and_entropy = get_log_probs_and_entropy(logits, ...)
    # 调用 Megatron fused_vocab_parallel_cross_entropy, 支持 TP 分片
    new_log_probs = torch.cat(log_probs_and_entropy["log_probs"], dim=0)
    old_log_probs = torch.cat(old_log_probs_list, dim=0)    # —— 4. 教师 advantage (OPD 分支) ——————————————————
    teacher_log_probs_list = batch.get("teacher_log_probs")
    if teacher_log_probs_list is not None:
        teacher_advantages = torch.cat(
            [
                t.to(device) - o.to(device)  # teacher_lp - rollout_lp (旧策略)
                for t, o in zip(teacher_log_probs_list, old_log_probs_list)
            ],
            dim=0,
        )
        # 注意: Combine 中 RL-only 样本 teacher_lp == rollout_lp
        # → teacher_advantages ≈ 0, 只有 GRPO 项有效
    else:
        teacher_advantages = torch.zeros_like(grpo_advantages)    # —— 5. 核心公式: 线性加权合并两路 advantage ——————————————————
    w_opd = float(os.getenv("OPENCLAW_COMBINE_W_OPD""1.0"))
    w_rl  = float(os.getenv("OPENCLAW_COMBINE_W_RL",  "1.0"))    combined_advantages = (
        w_opd * teacher_advantages   # token-level: 教师对每个 token 的打分
        + w_rl  * grpo_advantages    # sequence-level: 人类对整条轨迹的打分
    )
    # 两路信号对齐后相加: OPD 提供密集 per-token 方向, RL 提供稀疏 trajectory 幅度
   
    # —— 6. PPO clip (与 RL/OPD 完全相同) ——————————————————
    ppo_kl = old_log_probs - new_log_probs  # = log(π_old/π_θ) = -log(ratio)
    pg_loss, pg_clipfrac = compute_policy_loss(
        ppo_kl, combined_advantages, args.eps_clip, args.eps_clip_high
    )
    pg_loss = sum_of_sample_mean(pg_loss)    # —— 7. 熵正则 ——————————————————
    # 若 entropy_coef != 0: 用本次 forward 的 entropy (有梯度)
    # 若 entropy_coef == 0: 在 no_grad 下单独计算, 仅用于监控
    entropy_loss = ...
    loss = pg_loss - args.entropy_coef * entropy_loss    # —— 8. KL 正则 (可选, kl_loss_coef 控制强度) ——————————————————
    if args.use_kl_loss:
        ref_log_probs = torch.cat(batch["ref_log_probs"], dim=0)
        kl = compute_approx_kl(new_log_probs, ref_log_probs, kl_loss_type=...)
        loss = loss + args.kl_loss_coef * sum_of_sample_mean(kl)    # —— 9. 空序列保护 (梯度连通性) ——————————————————
    if new_log_probs.numel() == 0:
        loss = loss + 0 * logits.sum()
        # 0 梯度但保持计算图连通, 避免 DDP all-reduce 挂起    return loss, reported_loss        

3.5 advantage 和 loss 的关系

我们再看看advantage 和 loss 的关系。关系很简单:

  1. advantage 是“信号方向” (正=该增大)
  2. loss = -ratio × advantage 是“优化目标” (要最小化)。
  3. 符号相 反是 RL 标准做法: maximize reward ↔ minimize negative reward。
推导
 复制代码# PPO loss 公式:
loss = -ratio * advantage# 其中:
advantage = teacher_lp - student_lp
ratio = exp(new_lp - old_lp)# 第一次 forward pass 时 (还没更新权重):
# new_lp ≈ old_lp → ratio ≈ 1# 所以: 
loss ≈ -1 * (teacher_lp - student_lp) 
    = -(teacher_lp - student_lp)
    = student_lp - teacher_lp
# 符号反转是 RL 的标准约定
# advantage > 0 → 好动作 → 应该增大概率 → loss 应该负 → 梯度下降时增大概率
# advantage < 0 → 坏动作 → 应该减小概率 → loss 应该正 → 梯度下降时减小概率
# 所以:
# loss = -advantage (乘以 ratio 做稳定)

关系图如下

 复制代码advantage = teacher_lp - student_lp  (信号: teacher 比 student 多喜欢多少)
    |
    | × (-ratio)
    ↓
loss = -ratio × advantage            (优化目标: 要最小化)
     ≈ -(teacher_lp - student_lp)    (当 ratio≈1)
     = student_lp - teacher_lp        (化简)
     ≈ D_KL(student || teacher) 的    (在采样 token 上的估计)
       per-token contribution
    |
    | ∇_θ (梯度下降)
    ↓
效果: 减小 student_lp - teacher_lp
    = 让 student_lp 接近 teacher_lp
    = 缩小 Reverse KL
实现
 复制代码# loss.py 中的 on_policy_distillation 分支:
elif args.advantage_estimator == "on_policy_distillation":
    student_log_probs = log_probs  # 当前 forward pass 的 log_probs   
    teacher_log_probs = rollout_data.get("teacher_log_probs")  # π_teacher logprobs [T]    # 对齐到 response 长度(取最后 response_length 个 token)
    teacher_log_probs = [
        t_log_prob[-response_length:]
        for t_log_prob, response_length in zip(teacher_log_probs, response_lengths)
    ]    # 核心计算:per-token 差值
    advantages = [
        teacher_log_prob - student_log_prob
        for teacher_log_prob, student_log_prob in zip(teacher_log_probs, student_log_probs)
    ]
    returns = advantages
总结

一句话总结:advantage = “应该怎么调” (正=增大, 负=减小) ;loss = “优化器要最小化的目标” = -advantage。两者符号相反,是 RL 的标准做法: maximize(advantage) ↔ minimize(loss = -advantage)。

0x04 优势的颗粒度

我们从 OpenClaw-RL 展开,看看优势函数的颗粒度。

4.1 概念

  1. Sequence(完整时序序列,整段上下文) :一条完整多轮对话、system 提示词 + 全部历史 turn + 当前待生成上下文,合起来叫一条 Sequence。一条 Sequence 包含 N 个 Turn、M 个 Step、海量 Token。
  2. Turn(一轮交互,用户↔模型单次往返)。大语言模型交互场景下,Turn特指用户与模型的一次完整往返交互,即「用户输入→模型输出」构成一个独立Turn,这一术语在对话系统、多轮对话微调任务中被普遍采用。
  3. Step(强化学习单步决策,时序动作单元):从强化学习决策视角出发,模型单次生成一段Token序列的决策过程对应1个Step,是RL中智能体(即大模型)执行动作、接收反馈的最小时序单元,和PPO等强化学习算法中的“步”定义完全对应。

以一个ALFWorld任务为例:“把刀放进抽屉”,轨迹内容如下:

 复制代码任务:Put the knife in the drawerStep 1 (look):"I see a counter with:a fork, a knife,a cup"       →正确:找到了物体
Step 2 (take):"take fork from counter"                           →→→→→→→→→→错误:拿错了
Step 3 (put):"put fork in/on counter"                            →纠正:放回去
Step 4 (take):"take knife from counter"                          →正确:拿到刀
Step 5 (goto):"go to drawer 1"                                   →正确:走对了
Step 6 (put):"put knife in/on drawer 1"                          →正确:任务完成最终结果:SUCCESS(O_T=+1)

对应的层级如下。

 复制代码对话任务的层级:
  Session (多轮对话)
    ├── Turn 1 (第1轮, 含 user + model)
    ├── Turn 2 (第2轮)
    └── Turn n (第n轮)
        └── tokens (每个词)Agentic 任务的层级:
  Trajectory (完整任务执行)
    ├── Step 1 (look)
    ├── Step 2 (take fork)
    ├── Step 3 (put fork)
    └── Step n (put knife → done)
        └── tokens (每个词)

所以,OpenClaw实际上是 Turn 级(非 Sequence 级),每个对话轮次独立打分。RLAnything 是 Step 级(Agent action 步骤):

  1. OpenClaw 的 “Turn 级” = 每轮对话的所有 token 共享一个 reward;
  2. RLAnything 的 “Step 级” = Agent 的每个动作步骤的所有 token 共享一个 RM 评分。

两者都是在其各自任务结构的 “自然切割粒度”上操作的。

4.2 方案

我们接下来对几种方案进行对比。

粒度示例信号来源
Sequence 级标准 GRPO (单轮任务)outcome_reward
Turn 级OpenClaw Binary RLJudge(turn) → +1/0/-1
Step 级RLAnythingOt+(mλ)⋅∑RM(step_i)
Token 级OpenClaw OPDTeacher log-prob - Student log-prob
方案 A:Sequence 级(标准GRPO)

GRPO的做法:所有token都用同一个advantage(+1or-1)不区分哪个token贡献了正确结果。

 复制代码奖励信号:+1(任务成功),所有 Token 共享同一个reward:Step 1 tokens:    [+1,+1,+1, +1, +1, +1,+1,+1,+1,+1,...]  
Step 2 tokens:    [+1,+1,+1, +1, +1, +1,+1,+1,...]   ←   拿错了!但仍是+1
Step 3 tokens:    [+1,+1,+1, +1, +1, +1,+1,+1,...]
Step 4 tokens:    [+1,+1,+1, +1, +1, +1,+1,+1,...]
Step 5 tokens:    [+1,+1,+1, +1, +1,+1,...]
Step 6 tokens:    [+1,+1,+1, +1, +1, +1,+1,+1,+1,...]
                                ↑
所有token 都是+1,包括“take fork”这个错误步骤

问题:Policy学不到“step 2是错误的”。如果改变随机性,“拿fork”的概率反而可能升高。

方案 B:Turn 级(OpenClaw Binary RL)

Binary RL:每个Turn内所有token共享同一个advantage,不同Turn之间有各自的advantage(这是“Turn级”比“Session级”好的地方)

单个对话轮次统一打分:(在对话系统中,整个agent响应是一个turn),Judge评估整个turn的质量:→ 用户got their knife into drawer → score=+1

  1. 优于 Sequence 级:Turn 粒度可以区分不同对话轮次的好坏
  2. 劣于 Step 级:同一 Turn 内所有步骤仍共享相同loss_mask
 复制代码所有步骤对应的loss_mask=[1]: Step 1:loss_mask = 1
Step 2:loss_mask = 1    ←  拿错了,但turn最终成功,仍参与训练
Step 3:loss_mask = 1 
Step 4:loss_mask = 1 
Step 5: loss_mask = 1 
Step 6: loss_mask = 1
方案 C:Step 级(RLAnything)

Step级(RLAnything 的做法):

  1. 每个 Step 的所有 token 使用该 Step 的综合 advantage
 复制代码Step 2 advantage = -1.2         "take fork"的所有token 都被惩罚
Step 4 advantage = +0.9         "take knife"的所有token都被奖励
  1. 每个Action Step独立打分(RM评分 × m次 + 结果奖励) :O_T=+1(任务最终成功)

因此:

  1. RM对每步的平均评分S_mean:
 复制代码Step 1 (look):        S_mean = +0.9  → R1=0_t+入·S_mean =1+0.9=+1.9
Step 2 (take fork):   S_mean = -0.7  → R2=1+(-0.7)=+0.3   ← 降低了!
Step 3 (put fork):    S_mean = +0.1  → R3=1+0.1=+1.1
Step 4 (take knife):  S_mean = +0.8  → R4=1+0.8=+1.8
Step 5 (goto):        S_mean = +0.6  → R5=1+0.6=+1.6
Step 6 (put knife):   S_mean = +1.0  → R6=1+1.0=+2.0
  1. 跨轨迹标准化 → 每个step的 advantage:
 复制代码step 1: +0.6
step 2: -1.2         ← step 2获得负advantage!Policy 学到“拿fork是错的”
step 3: +0.2 
step 4: +0.9 
step 5: +0.4 
step 6: +1.1

效果:即使轨迹整体成功,错误步骤(step2)仍然被惩罚

方案 D:Token 级(OpenClaw OPD)

OPD 使用 Teacher 模型对每个 token 的概率进行处理。

针对 Step 2 中 “take fork from counter”的每个 token:

Teacher(知道正确做法)的 token 概率分布:

 复制代码P_teacher("take") =0.85          ←  拿东西是对的
P_teacher("fork") = 0.03         ←  Teacher 不会选fork
P_teacher("knife") =0.88         ←  Teacher 更偏向 knife
P_teacher("from") = 0.92
P_teacher("counter") = 0.89

Student 的 token 概率分布:

 复制代码P_student("take")=0.80 
P_student("fork")=0.45          ←   Student更可能选fork!
P_student("knife")=0.35

Token 级 advantage = log(Teacher/Student):

 复制代码"take":     log(0.85/0.80)= +0.06    ←  方向对
"fork":     log(0.03/0.45)= - 2.71   ←  极强负信号!"fork"是错误token
"knife":    log(0.88/0.35) = +0.92   ←  如果选这个会有强正信号
"from":     +0.05
"counter":  +0.02

效果:因为Policy对P(“fork”)的梯度方向是向下的,P(“knife”)向上,所以,不仅在step级别知道错了,而且精确到“fork”这个词的选择是问题所在。

四种粒度对比总结
 复制代码                    Step 2中错误"fork"的处理
──────────────────────────────────────────────────────────────────────
Sequence 级:       advantage=+1.0  Policy强化了这个错误
                     ↓ 学到的:take fork from counter 是好的 (错误!)
Turn 级:           loss_mask =1   参与训练,但无法区分 fork vs knife
                     ↓ 学到的:整个turn的模式是好的,包括 fork(隐式正强化)
Step级:            advantage =-1.2   整个 step 被惩罚
                       ↓ 学到的:在这个上下文里,"take fork from counter"是差的
                                但不知道具体是"fork"这个词还是"take"的方向错了
Token 级 (OPD):     "fork"token的advantage =-2.71
                       ↓ 学到的:精确是选择“fork”这个词是错的
                       P(fork)下降,P(knife)上升

0x05 Token 级 vs 序列级:范式根因

0x04 我们用 ALFWorld 例子对比了四种粒度的效果差异。这一章我们追问一个更根本的问题:为什么 Binary RL 只能是序列级、OPD 只能是 token 级?这种粒度差异是偶然选择,还是范式决定?

答案是:粒度由奖励载体决定,而非主观选择

5.1 奖励载体决定粒度

Binary RLOPD
奖励载体: 标量 ∈ {-1, 0, +1}奖励载体: 向量 ∈ R^T(每个 token 一个值)
来源: 人类行为反馈(无结构)来源: 语言模型 log-prob(自带 token-level 结构)
信息粒度: trajectory level信息粒度: token
信用分配: 无法解决(平均分配是唯一选择)信用分配: 天然解决(teacher LP 在每个位置都有答案)
类比: 电影评分(1-5)对整部电影类比: 逐句校对批注,告诉你每个词该如何改

Binary RL 是强化学习范式——通过环境反馈学习,只能获得稀疏的标量奖励;OPD 是监督/蒸馏学习范式——通过教师模型指导学习,能够获得密集的 token 级监督信号。这是范式层面的根本差异,粒度只是其外在表现。

5.2 在线对话约束下的可行性

即便理论上 token 级更优,在线对话场景对它也有苛刻约束。Step 级需要“在模型生成每个 action 时立刻打分”,对对话 LLM 而言“step”=每个 token,相当于需要实时 token-by-token RM 评分,延迟不可接受。

OPD 的巧妙之处在于:用 Teacher 的 一次 forward pass 代替显式的逐 token RM 评分——Teacher 一次前向就给出所有 token 的 log_prob,没有逐 token 评分的延迟。这就是 OPD 能用 token 级同时保持在线可行的原因。

5.3 Combine 为何能融合两者

 复制代码combined_adv[t] = w_opd * (teacher_lp[t] - old_lp[t])   ← per-token 方向
                + w_rl  * reward                         ← sequence-level 强度
  1. OPD 告诉你“往哪个方向走”(per-token 方向性)
  2. RL 告诉你“走多远/走不走”(sequence-level 强度)
  3. 融合后:既有方向又有强度,信号最丰富

这正是 Combine 方法的核心价值——两种范式互补,而非简单叠加。

0x06 Credit Assignment

OPD是“部分解决”而非“完全解决”credit assignment,即,程度比Binary RL轻得多。

6.1 什么是Credit Assignment(信用分配)

问题

我们最终最求的本质:给定最终奖励,哪个时刻的动作应该“得到信用”(被奖励/惩罚)?

 复制代码轨迹:s₁ →a₁→ s₂ →a₂→ s₃ →a₃→ ... →aₙ→ 最终结果 R

而目前问题是,面对上面的序列,我们可以回答如下嘛?

 复制代码R = +1(成功)→ a₁ 贡献了多少?a₂ 贡献了多少?... aₙ 贡献了多少?

我们不清楚,因为:

  1. a₃ 是关键动作 → 但 R 在 n 步后才出现
  2. a₁ 也影响了后续所有状态 → 间接贡献是多少?
  3. 中间有些动作是噪声 → 真正贡献是 0?

核心挑战:长轨迹 + 稀疏奖励 → 无法确定哪个动作应该被强化

类比

想象你和三个朋友一起打篮球,你们队赢了。比赛结束后,教练说:“你们打得很好!”

但问题来了—到底是谁的功劳?

 复制代码第一节:小明投进了一个三分球
第二节:小红传了一个漂亮的助攻
第三节:小刚把对方最厉害的球员挡住了
第四节:你投进了压哨一球最终结果:你们赢了

“赢了”这个奖励是在最后才出来的。但中间每个人都做了贡献—只是贡献的时间和大小都不一样。

解决方案

Credit Assignment 就是在回答这个问题:

 复制代码赢球这件好事,应该有多大比例算小明的?多大比例算小红的?多大比例算小刚的?多大比例算你的?

如果你奖励所有人一样多,小刚(只是在防守但没得分)可能会觉得不公平。

如果你只奖励最后投球的你,小明三分球的功劳就被忽视了。

AI训练

在AI训练中:AI做了50个动作,最后完成了任务,拿到了奖励+1。

 复制代码动作1、23..是铺垫(早期重要决定)
动作48、4950...是最后的执行

问题:

 复制代码这个+1应该“分配”给哪些动作?那些早期的铺垫有没有功劳?如果不管哪个动作都平等地拿到相同的奖励信号  → AI就不知道哪个动作真正重要  → 可能会保留了错误的习惯,丢掉了正确的习惯

Credit Assignment就是:“怎么公平地把功劳分给中间每一步”。

这个问题越到AI Agent(要做几十步才能完成任务)就越难 ---- 就像一场球赛越长,越难说清楚到底是哪个决定决定了胜负。

6.2 Binary RL

Binary RL 的 Credit Assignment 问题(对比基线),其问题如下:前 7 个 token 无辜受罚,梯度信号嘈杂。

 复制代码response: ["The""answer""is""42""because""sqrt(1764)""=""43"]
                                                                       ↑ 错误!
reward = -1 (PRM 打了负分)GRPO advantage: [-1, -1, -1, -1, -1, -1, -1, -1]
                所有 token 被同等惩罚,包括正确的前半段

6.3 OPD 解决了什么

成绩

OPD 做到了:teacher 知道错误所在,应在关键 token 位置集中梯度。

 复制代码# 有 hint: "注意 sqrt(1764) = 42, 不是 43"
teacher_lp: [..., -0.1, -0.2, -0.3, -5.8, -0.1, -0.1, -0.1, -0.05]
rollout_lp: [..., -0.1, -0.2, -0.3, -0.1, -0.1, -0.1, -0.1, -5.5]
                                     ↑                       ↑
advantage:  [...,  0.0,  0.0,  0.0, -5.7,  0.0,  0.0,  0.0, +5.45]
                       惩罚 "because"后的推导            奖励正确答案 "42"
问题

OPD 仍未解决的 Credit Assignment 如下。

 复制代码问题根源:auto-regressive 条件化teacher_lp[t] = P_teacher(w_t | w_1...w_t-1, hint)
                                  ↑
                       条件化在学生之前已犯的错误上

具体场景:

 复制代码response: ["Let", "me""compute""1764÷2=882""then""sqrt(882)≈29.7"]
                                        ↑ 错误推导路径!
                                        这步错了,后面全错

解析如下:

Tokenteacher 评分问题
“1764÷2=882”低(路径不对)正确被惩罚
“then”中(给定错误步骤后较合理)被“过度原谅”
“sqrt(882)≈29.7”高(给定 882,这步正确)错误 token 反而得到奖励

核心问题:teacher 无法做反事实推理 —— 它不知道“如果 token 4 不同,token 5-8 会更好”。

为何 OPD 在实践中仍然有效?

尽管存在级联问题,OPD 仍然 work 的原因:

  1. Hint 注入改变了条件:teacher 见过 hint,对整段 response 的评分有了全局视角,能在关键位置集中低概率
  2. 长期统计上自纠正:级联误导在大量样本上随机,均值接近零;真正错误 token 的信号是系统性的
  3. 比 Binary RL 方差更低:即使有噪声,per-token 信号的方差明显小于 broadcast 标量

类比:OPD 相当于“带高亮的批改作业”(知道哪段错),Binary RL 相当于“只给总分”。前者仍有局部歧义,但比后大得多。

6.4 Cascading Errors

定义

级联错误(Cascading Errors)是 OpenClaw OPD 里一个更具体的问题,但发生机制不同:

OPD 的 teacher log-prob 计算:

  1. Student 生成了:“用户想要 A,我先给 B,再...” ← step 1(有错误:应该直接给 A)

  2. Teacher 计算 step 2 的 log-probs 时:

    1. 输入 = student 的 step 1 文本(带有错误的上下文)
    2. Teacher 的 log-prob(step 2 | student 的错误 step 1)
    3. → Teacher 被迫在“错误上下文”里给出评分
    4. → 这个评分可能指向“在已经犯了错之后,接下来怎么做比较好”,而不是“正确策略下,step 2 应该是什么”

问题:OPD 梯度 = teacher_lp - student_lp,但 teacher_lp 是在“错误上下文”下计算的,→ 梯度方向不是指向“正确策略”,而是“在已犯错的基础上最优补救”。

关键区别
 复制代码       信用分配问题                                  级联错误问题
───────────────────────────────────────────────────────────────────────────
信号是否存在?                                信号存在,但质量有问题
    → 奖励信号模糊/稀疏                           → Teacher log-probs 被污染方向是否正确?                                方向可能错误
    → 方向正确,但不知道应该分配给哪个时刻         → 梯度方向本身可能是 biased 的解决思路                                     解决思路
    → 使用 PRM/step-level reward                 → 让 teacher 从正确上下文重新计算
      让每步有独立信号                            但这需要 teacher 的"重生成",很贵    → 使用 GAE 做时序折扣                         → 或者:GRPO RL 路径来纠正
      (γλ)^t 折扣远期贡献                         (Combine 方法的设计目的之一)

两者关系图

 复制代码Credit Assignment问题(一般问题)
    │
    ├───────── 时序信用分配(Temporal Credit Assignment)
    │             "哪一步应该得到奖励?"
    │             → OpenClaw的解法:Turn级评分(每轮独立打分)
    │               减少了需要分配的时序跨度
    │                                                     
    └───────── 反事实信用分配(Counterfactual Credit Assignment)
                  "如果step t做了不同选择,结果会怎样?"
                   → 级联错误是这里的一个子问题:
                     "teacher的评估是否真的告诉了我step t的最佳选择?"
                     "还是"只告诉了我在student已犯错的上下文下,下一步怎么补救?"
一句话总结

级联错误可以被理解为信用分配问题的一个加重版本:不仅难以归因,连用来归因的参考信号本身都因为早期决策的影响而不可信了。

  1. Credit Assignment=“功劳/过失应该算谁头上?”(归因问题)
  2. 级联错误=“参考信号本身因早期错误而被污染了”(信号质量问题)

6.5 OpenClaw对两者的应对

对 Credit Assignment:

  1. Turn级评分(而非,Session级):

    1. 每个turn独立打分→时序信用分配范围缩短到turn内部
    2. 不需要跨越多个turn分配信用
  2. 但Turn内部(token级)仍然是GRPO的序列级广播:

    1. 这个turn里的200个token全部共享同一个advantage →Turn内部的token级信用分配 = 未解决

对级联错误:

  1. 在Combine方法中(部分缓解):

    1. GRPO路径(global):sequence级reward给出“这整个turn质量如何”
    2. OPD路径(local):teacherlog-prob给出token级“这个词是否好”
  2. GRPO的全局惩罚可以纠正OPD的级联偏差:

    1. OPD-only路径(eval=0):没有GRPO纠正→级联错误完全保留
    2. OPD+RL路径(eval=+1):GRPO的全局信号与OPD的局部信号共同作用
  1. 但根本解决仍然缺失:没有让 teacher从正确上下文重新评估 student的错误 token

img_6a62a952c05f632.webp

TransFormer-封面

0xFF 参考

本文从源码角度完整解析了OpenClaw-RL的算法实现,深入探讨了三种训练方法的原理与差异,为理解在线强化学习在智能体场景中的应用提供了系统参考。