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

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

0x00 概要

本系列的目的是:借着对 OpenClaw-RL 源码的学习,来梳理强化学习的一些相关概念和思想。所以,会有一些基础知识、扩展和发散,OpenClaw-RL 只是一个切入点。而且,因为整篇系列是一个整体,所以有些概念的解读/学习会在不同的文章中出现,还请大家谅解。

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

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

framework

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

注:本文算是笔记,所以会涉及很多基础知识,会冗长。

0x01 基础背景

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

1.1 架构

OpenClaw-RL 系统架构如下:

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 RL OPD Combine
优势估计 GRPO on_policy_distillation GRPO(预计算)
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 * ones teacher_lp - rollout_lp w_opd*(T-R) + w_rl*GRPO
Loss 函数 Slime 内置 policy_loss_function / L=L_Pg+β_KL*L_KL Slime 内置 policy_loss_function combine_loss.py(自定义)

理论联系

  • OPD
    • ≈ Adaptive SFT (per-token weight)
    • ≈ KD (Knowledge Distillation) with hindsight
    • ≈ 最小化 KL(π_T(·|hint) || π_θ)
  • Binary RL
    • ≈ sequence-level REINFORCE (无 baseline)
  • Combine
    • ≈ 全局信号(GRPO)为锚 + 局部信号(OPD)做细化
    • GRPO 的均匀惩罚隐式压制 OPD 的级联噪声

Slime支持

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

estimator 公式
grpo ✔(Binary RL, Combine) adv[t] = reward (scalar broadcast) / GRPO 不normalize: --disable-rewards-norm
gspo GRPO 变体,全序列级 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_baseline REINFORCE++ 带 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 设计理念

为什么设计成 "逐轮打分"?

  • 技术原因: 打分需要 next_state(即用户下一句话),这在当前轮结束时才能得到,天然是逐轮粒度。
  • 语义原因: PRM Judge 的 prompt 是 "given this response, did the user continue"。
  • 工程原因: 每轮独立成为一个 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 就是用户对上一轮回复的真实反应
  • 用户下一句话继续深入 → 模型回答有用 → score = +1
  • 用户重复同样的问题 → 没听懂 → score = -1
  • 用户沉默(session 结束)→ 无法判断 → score = 0

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

工程上的现实因素

问题

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

  • 问题 1:对话长度未知,因此
    • → 不知道何时该收集整个 episode 进行打分
  • 问题 2:对话可能被用户中途放弃,因此
    • → Episode 不完整,无法打 episode-level 分
  • 问题 3:批次填充问题,因此:
    • → Episode-level 到达训练队列是脉冲式的(对话结束时才来)
    • → 逐轮样本持续到达,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:优势值直接等于标量奖励\(r\),即标量奖励 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需要:

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

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

2.3 GRPO

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

代码证据:

  • 无 value model / critic—GRPO 的标志性特征:slime/slime/utils/ppo_utils.py L207: returns.append(torch.ones_like(kl[i])
  • rewards[i]) – reward 标量直接广播为 advantage,没有 GAE / value baseline
  • 运行脚本中未启用 critic:openclaw-rl/run_qwen3_4b_openclaw_rl.sh 使用 --disable-rewards-normalization, advantage = raw reward (±1 直接用)
  • 但使用了 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。

特征 PPO GRPO OpenClaw Binary RL
Value model
Advantage GAE (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
Advantage A_t = R (raw reward ±1 broadcast 到所有 token) ppo_utils.py
梯度更新 PPO-style clipped surrogate ppo_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 model GRPO 特征

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 RL reward scalar → 广播全 token 序列级 (无 credit)
OPD teacher_lp - rollout_lp Token 级 (per-token)
per-token 浮点差值
Combine w_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 的关系。关系很简单:

  • advantage 是"信号方向" (正=该增大)
  • loss = -ratio × advantage 是"优化目标" (要最小化)。
  • 符号相 反是 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 概念

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

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

任务:Put the knife in the drawer

Step 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 步骤):

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

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

4.2 方案

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

粒度 示例 信号来源
Sequence 级 标准 GRPO (单轮任务) outcome_reward
Turn 级 OpenClaw Binary RL Judge(turn) → +1/0/-1
Step 级 RLAnything Ot+(mλ)⋅∑RM(step_i)
Token 级 OpenClaw OPD Teacher 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

  • 优于 Sequence 级:Turn 粒度可以区分不同对话轮次的好坏

  • 劣于 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 的做法):

  • 每个 Step 的所有 token 使用该 Step 的综合 advantage
Step 2 advantage = -1.2         "take fork"的所有token 都被惩罚
Step 4 advantage = +0.9         "take knife"的所有token都被奖励

  • 每个Action Step独立打分(RM评分 × m次 + 结果奖励) :O_T=+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

  • 跨轨迹标准化 → 每个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 RL OPD
奖励载体: 标量 ∈ 奖励载体: 向量 ∈ 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 强度


  • OPD 告诉你"往哪个方向走"(per-token 方向性)
  • RL 告诉你"走多远/走不走"(sequence-level 强度)
  • 融合后:既有方向又有强度,信号最丰富

这正是 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ₙ 贡献了多少?


我们不清楚,因为:

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

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

类比

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

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

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


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

解决方案

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

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


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

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

AI训练

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

动作1、2、3..是铺垫(早期重要决定)
动作48、49、50...是最后的执行


问题:

这个+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"]
                                        ↑ 错误推导路径!
                                        这步错了,后面全错


解析如下:

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

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

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

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

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

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

6.4 Cascading Errors

定义

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

OPD 的 teacher log-prob 计算:

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

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

    • 输入 = student 的 step 1 文本(带有错误的上下文)
    • Teacher 的 log-prob(step 2 | student 的错误 step 1)
    • → Teacher 被迫在"错误上下文"里给出评分
    • → 这个评分可能指向"在已经犯了错之后,接下来怎么做比较好",而不是"正确策略下,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已犯错的上下文下,下一步怎么补救?"

一句话总结

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

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

6.5 OpenClaw对两者的应对

对 Credit Assignment:

  • Turn级评分(而非,Session级):
    • 每个turn独立打分→时序信用分配范围缩短到turn内部
    • 不需要跨越多个turn分配信用
  • 但Turn内部(token级)仍然是GRPO的序列级广播:
    • 这个turn里的200个token全部共享同一个advantage →Turn内部的token级信用分配 = 未解决

对级联错误:

  • 在Combine方法中(部分缓解):
    • GRPO路径(global):sequence级reward给出“这整个turn质量如何“
    • OPD路径(local):teacherlog-prob给出token级“这个词是否好“
  • GRPO的全局惩罚可以纠正OPD的级联偏差:
    • OPD-only路径(eval=0):没有GRPO纠正→级联错误完全保留
    • OPD+RL路径(eval=+1):GRPO的全局信号与OPD的局部信号共同作用
  • 但根本解决仍然缺失:没有让 teacher从正确上下文重新评估 student的错误 token

TransFormer-封面

0xFF 参考

posted @ 2026-07-23 20:48  罗西的思考  阅读(0)  评论(0)    收藏  举报