【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (15)--- Combine模式

【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (15)--- Combine模式

目录

  • [【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (15)--- Combine模式](#【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (15)--- Combine模式)
    • [0x00 概要](#0x00 概要)
    • [0x01 架构](#0x01 架构)
      • [1.1 架构:继承而非重写](#1.1 架构:继承而非重写)
      • [1.2 并行评估流水线:两个任务合一](#1.2 并行评估流水线:两个任务合一)
      • [1.3 小结](#1.3 小结)
    • [0x02 流程](#0x02 流程)
      • [2.1 阶段一](#2.1 阶段一)
      • [2.2 阶段二](#2.2 阶段二)
      • [2.3 阶段三](#2.3 阶段三)
      • [2.4 阶段四](#2.4 阶段四)
    • [0x03 Combined Loss](#0x03 Combined Loss)
      • [3.1 计算公式](#3.1 计算公式)
      • [3.2 样本](#3.2 样本)
      • [3.3 Combine vs 独立运行两个方法](#3.3 Combine vs 独立运行两个方法)
      • [3.4 OPD loss 和 GRPO loss 的联合训练](#3.4 OPD loss 和 GRPO loss 的联合训练)
        • [方案 A: Loss 混合](#方案 A: Loss 混合)
        • [方案 B: Advantage 混合(OpenClaw 的选择)](#方案 B: Advantage 混合(OpenClaw 的选择))
        • 为什么这样设计?
          • 梯度方向一致性
          • 信任域语义
          • [PPO clip 是关于 r_t 的,不是关于 A 的](#PPO clip 是关于 r_t 的,不是关于 A 的)
          • [与 multi-objective RL 的关系](#与 multi-objective RL 的关系)
      • [3.5 Combine 的 custom loss 如何拦截并混合?](#3.5 Combine 的 custom loss 如何拦截并混合?)
    • [0x04 三路分发逻辑](#0x04 三路分发逻辑)
      • [4.1 三路分发逻辑](#4.1 三路分发逻辑)
      • [4.2 3-Way Dispatch 为何是必要的](#4.2 3-Way Dispatch 为何是必要的)
      • [4.3 与"有条件分支"的对比](#4.3 与“有条件分支”的对比)
      • [4.4 hint+eval_score 如何被利用](#4.4 hint+eval_score 如何被利用)
        • dispatch决策树
        • 每种情况的Sample构造与梯度效果
          • [情况 A: OPD-only(_submit_turn_sample with reward=0.0)](#情况 A: OPD-only(_submit_turn_sample with reward=0.0))
          • [情况B: RL-only(_submit_rl_turn_sample)](#情况B: RL-only(_submit_rl_turn_sample))
          • [情况 C: DROP(两信号都无)](#情况 C: DROP(两信号都无))
          • [情况D: OPD+RL(_submit_turn_sample with reward=±1.0)](#情况D: OPD+RL(_submit_turn_sample with reward=±1.0))
      • [4.5 信息流总结图](#4.5 信息流总结图)
      • [4.6 权重参数的语义](#4.6 权重参数的语义)
    • [0x05 讨论](#0x05 讨论)
    • [0xFF 参考](#0xFF 参考)

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

OpenClaw-Combine:同时使用OPD提示和RL奖励。两个优势互补:OPD提供方向性指导,RL提供精细调优

这种设计体现了OpenClaw框架的灵活性和可扩展性,能够根据不同任务需求选择最适合的训练策略,同时保持统:一的架构接口和数据流规范。

0x01 架构

1.1 架构:继承而非重写

OpenClawCombineAPIServer 继承了 OpenClawOPDAPIServer,继承的核心组件:

  • _opd_evaluate() :hint提取 + teacherlog-probs(来自OPD,未修改)
  • query_judge_once() :judge LLM调用(来自OPD,未修改)
  • fire_opd_task() :触发异步评估任务(来自OPD,未修改)
python 复制代码
    class OpenClawCombineAPIServer(OpenClawOPDAPIServer):
        #继承OPD的全部基础设施:
        #-turn 拦截、记录、next_state 等待
        #-hintjudge机制(完整保留)
        #-teacherlog-probs计算(完整保留)
        #-top-K蒸馏(完整保留)
        #新增:override_submit_turn_sample和_maybe_submit_ready_samples
        #使其同时执行eval评分(来自OPD的eval_mode功能)
        
        #重写了_submit_turn_sample()和_maybe_submit_ready_samples()
        #其余(hint提取、teacher log-probs、流量代理等)完全复用父类 

1.2 并行评估流水线:两个任务合一

在 combine 模式下,Turn N+1 到达时, 会触发对 Turn N 的评估:

python 复制代码
Turn N+1到达后,触发_fire_opd_task()(继承自OPD)
                ↓   
_opd_evaluate() 同时执行:
┌────────────────────────────────────────────────┐
│ Task A: m次并发 hint-judge 调用(asyncio.gather)│
│   → 决定 hint 是否有用                           │
│   → 提取最长 hint                               │
│   → 若 accepted: 计算 teacher log-probs         │
│   → votes:[{score:1,hint:"..."},...]          |
│   → _select_best_hint()→选最长hint/None         |  
│                                                │
│ Task B: m次并发 eval 调用 (eval_mode=True)       │
│   → 决定 response 质量 ±1/0                     │
│   → majority vote → eval_score                 │
│   → eval_scores:[+1,+1,-1]                     |
│   → _prm_eval_majority_vote() → eval_score:+1/-1/0
└────────────────────────────────────────────────┘
                ↓
_maybe_submit_ready_samples() 调度

注意:Task A 和 Task B 在代码中是顺序执行的 (先 hint, 再 eval),但两者都是并发的 gather, 整体并发度 = 2m 次 LLM 调用

1.3 小结

下面是三种模式的对比。

对比维度 Binary RL 只 OPD Combine
样本覆盖率 所有评分 turn 只有 hint 接受 Max(4 种情况尽量收集)
信号粒度 token 统一 per-token 混合
对隐式反馈 ✅ 支持 ❌ 不支持 ✅ 支持
对显式纠正 ❌(仅支持 ±1 打分) ✅(hint 文本精准纠正) ✅ 全部支持
单 turn 最大样本 1 1 1(同样只产生 1 个)
权重可调 N/A N/A w_rl, w_opd(默认均为 1.0)
API Server 类 OpenClawAPIServer OpenClawOPDAPIServer OpenClawCombineAPIServer
Judge 调用次数/turn m=3 (PRM) m=3 (Hint Judge) + 1次 Teacher LP m=3 (Hint) + m=3 (Eval) + 1次Teacher LP
loss_mask 控制 0 或 1(按 score) 始终 1 始终 1
reward 字段 ±1 / 0 固定 0 ±1 / 0(来自 eval judge)
Advantage 类型 REINFORCE-like (scalar) per-token (teacher-studentdiff) 两者加权求和
Drop 条件 score=0 且无 at-least-one hint 被拒绝 accepted=F 且 eval=0
丢弃率
at-least-one guarantee × ×
teacher_log_probs 真实 teacher LP 真实(OPD路径)/ rollout LP(RL路径)

0x02 流程

2.1 阶段一

2.2 阶段二

python 复制代码
-------------------------------------------------------------------------------
【阶段二】三路分发 (_maybe_submit_ready_samples)
-------------------------------------------------------------------------------

+----------------+------------+-----------------------------------------------+
| hint accepted? | eval ±1?   | 路径                                          |
+----------------+------------+-----------------------------------------------+
| ✓              | ✓          | OPD+RL  -> _submit_turn_sample(reward=±1)     |
| ✓              | × (=0)     | OPD-only -> _submit_turn_sample(reward=0)     |
| ×              | ✓          | RL-only -> _submit_rl_turn_sample(±1)         |
| ×              | × (=0)     | DROP -> 不产生 sample                         |
+----------------+------------+-----------------------------------------------+

2.3 阶段三

2.4 阶段四

python 复制代码
-------------------------------------------------------------------------------
【阶段四】combine_loss.py - Combined Advantage 公式
-------------------------------------------------------------------------------

Megatron 训练时 (combine_loss_function):

① GRPO advantage (预计算, 来自 Slime ppo_utils):
    grpo_adv[i] = reward[i] * ones_like(tokens)  (scalar broadcast)

② OPD advantage (per-token, 在 loss 函数中计算):
    teacher_adv[i] = teacher_log_probs[i] - old_log_probs[i]

③ Combined advantage:
    combined_adv = w_opd * teacher_adv + w_rl * grpo_adv
                     ↑                    ↑
           OPENCLAW_COMBINE_W_OPD=1.0   OPENCLAW_COMBINE_W_RL=1.0

④ PPO clipped loss:
    ppo_kl = old_log_probs - new_log_probs  (new = 当前前向)
    ratio  = exp(-ppo_kl)
    pg_loss = -min(ratio * adv, clip(ratio, 1±ε) * adv)
    ε=args.eps_clip (default 0.2), ε_high=args.eps_clip_high (default 0.2 or 0.28, depending on shell flags)

⑤ 最终 loss:
    loss = pg_loss - entropy_coef * entropy
    if args.use_kl_loss and batch.get("ref_log_probs") is not None:
        loss += kl_loss_coef * KL(new||ref)

─────────────────────────────────────────
各路径 advantage 等效性验证:

OPD+RL:  teacher_adv ≠ 0  +  grpo_adv ≠ 0  →  双信号
OPD-only: teacher_adv ≠ 0  +  grpo_adv = 0   →  纯蒸馏
RL-only:  teacher_adv ≈ 0  +  grpo_adv ≠ 0   →  纯RL
          (teacher=rollout → teacher-old=0)
─────────────────────────────────────────

0x03 Combined Loss

3.1 计算公式

给定一个 sample 的每个 token t,对应的计算公式如下:

python 复制代码
1. GRPO advantage:
   A_t^grpo = R  (reward 标量 broadcast)

2. OPD teacher advantage:
   A_t^opd = log π_teacher(a_t|s_t) - log π_old(a_t|s_t)
   
3. 混合:
	A_t^combined = w_rl · A_t^grpo + w_opd · A_t^opd
	
4. PPO ratio:
	r_t = exp(log π_new(a_t|s_t) - log π_old(a_t|s_t))
	
5. Clipped surrogate:
	L_t = -min(r_t · A_t^combined, clip(r_t, 1-ε, 1+ε_high) · A_t^combined)
	
6. Total loss:
	L = mean_over_tokens(L_t)   

Combine 不在 Slime 的 advantage_estimator 枚举内,而是绕过了 Slime 的 advantage 计算!

  • 它用 custom_loss_function_path,直接在 loss 函数中合并两种信号

  • Slime 还是会调用 GRPO 的 get_grpo_returns,但 combine_loss 覆盖了最终使用的 advantages

  • Slime 原生流程:

    • rewards → get_grpo_returns() → batch"advantages" ← GRPO advantages(被 Combine 复用)
  • teacher_log_probs → 存入 batch"teacher_log_probs" ← OPD 信号(Combine 手动计算差值)

  • custom_loss_function (combine_loss)的流程:
    • 取出两者,线性组合,再做 PPO clipping

具体如下:

python 复制代码
#combine_loss.py中的核心计算:

# Step1: GRPO advantages(预计算,由Slime框架注入batch / reward 广播)
grpo_advantages = torch.cat(batch["advantages"], dim=0)  # reward 广播到所有response tokens
# RL样本:GRPO 用 reward(±1) 广播给所有 token
# OPD样本:reward=0 → GRPO advantage = 0

# Step 2: OPD teacher advantages(per-token)
# teacher_log_probs_list / old_log_probs_list 按 sample 组织, 需逐 sample 计算差值后 cat
teacher_advantages = torch.cat([
    t.to(device=device) - o.to(device=device)  # teacher_lp - old_lp (per-token)
    for t, o in zip(teacher_log_probs_list, old_log_probs_list)
], dim=0)
# OPD样本:真实 hint → 非零 per-token advantage
# RL-only样本:teacher=rollout → teacher_advantage ≈ 0

# Step 3:线性组合
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 + w_rl * grpo_advantages

# Step 4: PPO截断surrogate loss(与Slime 标准policy_loss_function 完全一致)
ppo_kl = old_log_probs - new_log_probs
pg_loss, pg_clipfrac = compute_policy_loss(ppo_kl, combined_advantages,
    eps_clip=args.eps_clip, eps_clip_high=args.eps_clip_high)
loss = pg_loss - entropy_coef * entropy_loss
if args.use_kl_loss and batch.get("ref_log_probs") is not None:
    loss = loss + kl_coef * kl_loss

3.2 样本

具体样本如下:

python 复制代码
┌──────────┬──────────────────┬─────────────────────┬───────────────┬────────────────┐
│ 样本类型  │ teacher_log_probs│ teacher_adv         │ grpo_adv      │ combined_adv   │
│          │ 内容              │                     │               │                │
├──────────┼──────────────────┼─────────────────────┼───────────────┼────────────────┤
│ OPD+RL   │ 真实 teacher LP   │ teacher - rollout ≠ 0│ reward ±1    │ w_opd*(t-r) +  │
│          │                  │                     │ broadcast     │w_rl*grpo       │
├──────────┼──────────────────┼─────────────────────┼───────────────┼────────────────┤
│ OPD-only │ 真实 teacher LP  │ teacher - rollout ≠ 0│ 0 (reward=0) │ w_opd*(t-r)    │
├──────────┼──────────────────┼─────────────────────┼───────────────┼────────────────┤
│ RL-only  │ rollout_lp(假)  │ 0(对消)            │ reward ±1     │ w_rl * grpo    │
│          │                  │                     │ broadcast     │                │
└──────────┴──────────────────┴─────────────────────┴───────────────┴────────────────┘

各样本类型的优势值矩阵

各样本类型的优势值矩阵如下:

样本类型 teacher_advantages (per-token) grpo_advantages (per-token) combined (per-token)
OPD+RL sample hint_logp -- old_logp ±1.0(broadcast) 两者叠加
OPD-only sample hint_logp -- old_logp 0.0(reward=0) 只有 OPD 信号
RL-only sample ≈ 0(teacher=rollout) ±1.0(broadcast) 只有 RL 信号
直觉 方向性纠正:"下一步往哪走" 好/坏评价:"这步走的方向对不对"

三种样本的优势向量对比

假设response ="None","check","is","added" (T=4 tokens)

python 复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│ RL-only 样本 (hint=None, eval=+1)                                           │
│                                                                             │
│ rollout_log_probs  = [-0.3, -1.2, -0.1, -0.8]                               │
│ teacher_log_probs  = [-0.3, -1.2, -0.1, -0.8] <- 故意=rollout               │
│ reward             = +1.0                                                   │
│                                                                             │
│ teacher_adv_t = teacher - old = [0.0, 0.0, 0.0, 0.0]                        │
│ grpo_adv_t    = [+1.0, +1.0, +1.0, +1.0]                                    │
│                                                                             │
│ combined_adv = 1.0*[0,0,0,0] + 1.0*[1,1,1,1]                                │
│              = [+1.0, +1.0, +1.0, +1.0]                                     │
│                                                                             │
│ 效果: 所有 token 均匀鼓励 (Binary RL 行为)                                     │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ OPD-only 样本 (hint=接受, eval=0)                                            │
│                                                                             │
│ rollout_log_probs  = [-0.3, -1.2, -0.1, -0.8]                               │
│ teacher_log_probs  = [-0.1, -0.3, -0.1, -0.2] <- 真实 teacher               │
│ reward             = 0.0                                                    │
│                                                                             │
│ teacher_adv_t = [-0.1-(-0.3), -0.3-(-1.2), -0.1-(-0.1), -0.2-(-0.8)]        │
│               = [+0.2, +0.9, 0.0, +0.6]                                     │
│ grpo_adv_t    = [0.0, 0.0, 0.0, 0.0] (reward=0)                             │
│                                                                             │
│ combined_adv = 1.0*[0.2,0.9,0.0,0.6] + 1.0*[0,0,0,0]                        │
│              = [+0.2, +0.9, 0.0, +0.6]                                      │
│                                                                             │
│ 效果: "check" 这个 token 最受鼓励 (teacher 认为最关键)                          │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ Combined 样本 (hint=接受, eval=+1)                                           │
│                                                                             │
│ rollout_log_probs  = [-0.3, -1.2, -0.1, -0.8]                              │
│ teacher_log_probs  = [-0.1, -0.3, -0.1, -0.2] <- 真实 teacher               │
│ reward             = +1.0                                                   │
│                                                                             │
│ teacher_adv_t = [+0.2, +0.9, 0.0, +0.6]                                     │
│ grpo_adv_t    = [+1.0, +1.0, +1.0, +1.0]                                    │
│                                                                             │
│ combined_adv = 1.0*[0.2,0.9,0.0,0.6] + 1.0*[1.0,1.0,1.0,1.0]                │
│              = [+1.2, +1.9, +1.0, +1.6]                                     │
│                                                                             │
│ 效果: 全局鼓励 (来自RL) + per-token 差异化增强 (来自OPD)                         |
│ "check" 的梯度信号最强 (teacher最在意的token 叠加了两个正信号)                    │
└─────────────────────────────────────────────────────────────────────────────┘

直觉理解

token 1 token 2 token 3 token 4 token 5
GRPO advantage: +1 +1 +1 +1 +1 ← 全部一样(reward=+1)
OPD advantage: +0.3 -0.1 +0.5 +0.8 -0.2 ← 每个不同
Combined: +1.3 +0.9 +1.5 +1.8 +0.8 ← w=1,1时

含义:

  • token 1: GRPO 说"整体好",OPD 说"teacher也喜欢" → 大力增强
  • token 2: GRPO 说"整体好",OPD 说"teacher不太喜欢" → 温和增强
  • token 5: GRPO 说"整体好",OPD 说"teacher不太喜欢" → 温和增强

GRPO 给出全局方向(好/差),OPD 细化每个 token 的权重(哪些 token 更好/差)。

PPO Clipping 如何作用于 Combined Advantage

Combined 优势对截断的影响:

python 复制代码
A_t 放大 (如 +1.9 for "check"):
    -> ratio 鼓励增大 π_θ("check" | context)
    -> clipping 防止单步更新过大 (ε_high = 0.28)

A_t 接近 0 (如 +1.0 for "is"):
    -> 更新量较小, 更保守

具体代码如下:

python 复制代码
# ppo_utils.py 中的 compute_policy_loss():
# ratio = exp(new_log_prob - old_log_prob) = π_θ(a_t)/π_old(a_t)
ppo_kl = old_log_probs - new_log_probs  # 近似 KL
ratio = torch.exp(-ppo_kl)              # 策略比率

pg_losses1 = -ratio * advantage         # 未截断
pg_losses2 = -ratio.clamp(1-0.2, 1+0.28) * advantage  # 截断
pg_loss = torch.max(pg_losses1, pg_losses2) # 取保守的 (更大的损失)

3.3 Combine vs 独立运行两个方法

Combine的优势:

  • turn最多提交1个样本(节省databuffer容量)
  • 同一个teacher forward pass服务两个目标(节省PRM GPU调用)
  • 梯度信号互补(序列级评价+token级方向)

Combine的代价:

  • OPD judge和eval judge各需要m次调用(总共2m次;默认m=1)
  • accepted率低时(无hint),仍需额外的eval调用

3.4 OPD loss 和 GRPO loss 的联合训练

这就是 Combine 方法的核心。它们不是两个独立 loss 相加,而是在 advantage 层面混合后共享一个 PPO loss。

不是这样(两个 loss 加权求和),我们定义为方案 A:

python 复制代码
# ✗ 错误理解
total_loss = w_rl * PPO_loss(grpo_advantage) + w_opd * OPD_loss(teacher_advantage)

而是这样(advantage 混合 → 单一 PPO loss),我们称之为方案 B:

python 复制代码
# ☑ 实际实现(combine_loss.py)
combined_advantage = w_opd * (teacher_lp - old_lp) + w_rl * grpo_advantage
total_loss = PPO_clip_loss(combined_advantage)    # 只有一个 loss!

两种方案的形式化定义如下。

方案 A: Loss 混合

python 复制代码
L = w_rl · L_PPO(A^grpo) + w_opd · L_PPO(A^opd)

其中:L_PPO(A) = -E_t[min(r_t · A_t, clip(r_t) · A_t)]

两个独立的 PPO loss,各自有 clip,最后加权求和。

方案 B: Advantage 混合(OpenClaw 的选择)

python 复制代码
A^combined = w_rl · A^grpo + w_opd · A^opd
L = L_PPO(A^combined)

一个 PPO loss,只是 advantage 是混合的。

为什么这样设计?

下表展示了设计原因。

关键理论差异如下。

梯度方向一致性
python 复制代码
方案 A 的梯度:

    ∇L = w_rl · ∇L_PPO(A^grpo) + w_opd · ∇L_PPO(A^opd)

    两个 clip 可能在同一个 token 上做出相反决定:
    - L_PPO(A^grpo) 的 clip 认为 r_t 太大 → clip 住
    - L_PPO(A^opd) 的 clip 认为 r_t 合适 → 不 clip

    → 同一步更新中,一个力推一个力拉 → 梯度冲突

方案 B 的梯度:

    ∇L = ∇L_PPO(A^combined)

    只有一个 clip 判断:A^combined 确定了方向,r_t 与 clip 的关系只判断一次
    → 梯度方向一致
信任域语义

PPO clip 的本质是:限制 policy 每步最多改变多少。

python 复制代码
方案 A:两个独立信任域
    Trust Region for GRPO (r ∈ [0.8, 1.28])
    Trust Region for OPD (r ∈ [0.8, 1.28]) 

    问题:r_t 同时满足两个约束 → 实际约束可能过松或过紧

方案 B:单一信任域
    Trust Region for combined A (r ∈ [0.8, 1.28])

	r_t 只需满足一个约束 → 语义清晰
PPO clip 是关于 r_t 的,不是关于 A 的

这是理解的关键:

python 复制代码
    # PPO loss 公式:
    L = -min(r_t · A_t, clip(r_t, 1-ε, 1+ε_high) · A_t)
    # r_t = π_new / π_old (策略变化幅度)
	# A_t 只决定方向和大小,clip 约束的是 r_t
	
    # 方案 A:
    L = w1 · [-min(r·A1, clip(r)·A1)] + w2 · [-min(r·A2, clip(r)·A2)]
    = [-min(r·A1, clip(r)·A1)] * w1 + [-min(r·A2, clip(r)·A2)] * w2

    # 如果 A1 > 0 且 A2 < 0(矛盾方向):
    # 第一项想增大 r,第二项想减小 r
    # clip 无法协调,梯度互相抵消
    
    # 方案 B:
    A_combined = w1·A1 + w2·A2 ← 先解决方向冲突
    L = -min(r · A_combined, clip(r) · A_combined)
    # 方向已确定,clip 只管步长
与 multi-objective RL 的关系
范式 公式 理论依据
Reward 混合(最标准) R = w1·R1 + w2·R2 → 单一 RL 线性 scalarization of Pareto front
Advantage 混合(OpenClaw) A = w1·A1 + w2·A2 → 单一 PPO advantage 是 reward 的函数,混合传递
Loss 混合(方案 A) L = w1·L1 + w2·L2 多任务学习(MTL),不是标准 RL

Loss 混合则更接近多任务学习(如 Transformer 同时做 MLM + NSP),不保证收敛到 Pareto 最优。

而OpenClaw 的方案等价于:先把两个信号在 advantage 空间统一成一个目标,然后用标准单目标 RL 优化。这符合 RL 理论中的线性 scalarization 范式。但是,该方案存在一个理论风险:两个分量的量纲/尺度不同:

  • teacher_lp - old_lp: 典型值 [-2, +2](per-token log-prob 差)
  • reward: {-1, 0, +1}(离散标量 broadcast)

如果 w_opd = w_rl = 1: → OPD 项可能系统性地比 GRPO 项大/小 → 实际权重不等于名义权重

这是为什么 OPENCLAW_COMBINE_W_OPD 和 OPENCLAW_COMBINE_W_RL 需要仔细调参的原因。

3.5 Combine 的 custom loss 如何拦截并混合?

Combine 的巧妙之处:它声称自己是 GRPO,让 Slime 先算好 GRPO advantage,然后 custom loss function 拦截并混合 OPD advantage。

Shell 脚本配置

复制代码
--advantage-estimator grpo               ← ① 告诉 Slime 用 GRPO 算 advantage
--loss-type custom_loss                  ← ② 告诉 Slime 用自定义 loss 函数
--custom-loss-function-path combine_loss.combine_loss_function ← ③ 指向哪个函数

执行流程

Slime 内部流程如下:

python 复制代码
Step 1: advantage 阶段 (loss.py L400-420)
    rewards = [±1, 0, ±1, 0, ...] ← API Server 设的
    advantages = get_grpo_returns(rewards) ← GRPO: broadcast reward to all tokens
    batch["advantages"] = advantages ← 存入 batch 字典
        │ 
        │ 
        ▼
Step 2: loss 阶段 (loss.py L944-952)
    match args.loss_type:
        case "policy_loss": func = policy_loss_function ← Binary RL / OPD 走这里
        case "custom_loss": func = load_function(path) ← Combine 走这里!
        │ 
        │ 
        ▼
Step 3: combine_loss.py 被调用
	接收 batch 字典 → 读取 GRPO advantages + teacher_log_probs → 混合

combine_loss.py 核心逻辑 (逐段解读)如下:

python 复制代码
def combine_loss_function(args, batch, logits, sum_of_sample_mean):

① 读取 GRPO 已算好的 advantage:

    grpo_advantages = torch.cat(batch["advantages"], dim=0)
    # 这就是 Slime 在 Step 1 算好的 GRPO advantage (每 sample 一个标量 broadcast)
    # 对于 reward=±1 的样本: 全 +1 或全 -1
    # 对于 reward=0 的样本 (OPD-only) : 全 0
    
② 当前 forward pass 得到新 log-probs:

    new_log_probs = get_log_probs_and_entropy(logits, ...) # 当前策略的 log π(a|s)
    old_log_probs = batch["rollout_log_probs"] # rollout 时旧策略的 log π_old

③ 计算 OPD teacher advantage:

    teacher_advantages = torch.cat([
        t.to(device) - o.to(device) # teacher_lp - old_lp (per-token!)
        for t, o in zip(teacher_log_probs_list, old_log_probs_list)
    ], dim=0)
 
④ 混合---这是整个 Combine 的核心公式:

    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 + w_rl * grpo_advantages

⑤ 用混合 advantage 走标准 PPO clip:

    ppo_kl = old_log_probs - new_log_probs       # ratio 的 log 形式
    pg_loss = compute_policy_loss(ppo_kl, combined_advantages, eps_clip, eps_clip_high)

拦截的精妙之处

Combine 的精妙在于:不用 if/else,而是通过数据设置让数学自动分流。GRPO 先算好 advantage,custom loss 拦截后叠加 OPD teacher:

  • OPD 样本的 GRPO 项为 0(因为 reward=0)
  • RL 样本的 OPD 项为 0(因为 teacher=rollout)

能这么做的原因是:三条路径用同-个公式 A = w_rl * grpo_adv + w_opd *(teacher_lp - rollout_lp), 不需要条件分支。 dispatch 只决定"给 teacher_lp 和reward赋什么值",loss函数本身是统一的。

在combine_loss.py 里只用一行代码实现了整个"RL-only样本零OPD"的效果,不需要任何条件判断。其数学效果如下:

python 复制代码
# RL-only样本:
teacher_advantage = teacher_lp - old_lp
				  = response_logprobs - response_logprobs #同---个值! 
				  = 0 ← OPD信号被完美消除
# OPD-only样本:
grpo_advantage = broadcast(reward)= broadcast(0.0) = 0 ← GRPO 信号被完美消除

# OPD+RL样本:
teacher_advantage = real_teacher_lp-old_lp ≠ 0 ←OPD信号存在
grpo_advantage = broadcast(±1) ≠ 0 ←GRPO信号存在
combined = w_opd * teacher_adv +w_rl * grpo_adv ← 两者都贡献

细节如下:

python 复制代码
                  OPD-only 样本                    RL-only 样本
                  ─────────────                    ────────────
reward            0                                 ±1
grpo_advantages   全 0 ← reward broadcast           全 ±1
teacher_lp        真实 teacher 的 log-probs          = rollout_lp (API Server 设的!)
old_lp (rollout)  旧策略 log-probs                   旧策略 log-probs

teacher_adv =     teacher_lp - old_lp ≠ 0           rollout_lp - old_lp ≈ 0 †
                  ↑ 真正的蒸馏信号                   ↑ 自消了!

combined_adv =    w_opd * teacher_adv + w_rl * 0    w_opd * 0 + w_rl * grpo_adv
                  = 纯 OPD                           = 纯 GRPO

† RL-only 样本中,API Server 把 teacher_log_probs 设为 rollout_log_probs,所以 teacher_lp - old_lp ≈ 0。

注意,Combine 的 combine_loss.py 中,teacher_advantages 和 grpo_advantages 分别如下:

  • grpo_advantages:Slime 标准流程计算的 GRPO advantage(标量reward broadcast),来自batch"advantages"
  • teacher_advantages:teacher_log_probs-rollout_log_probs(per-token 差异),在 loss 函数内部计算两者加权求和:A=w_rlgrpo_advantages+w_opdteacher_advantages

样例(看看如何处理RL only 样本)如下:在 _submit_turn_sample 函数中,分别有设置:

python 复制代码
# openclaw-combine\openclaw_combine_api_server.py
    # OPD 样本(含 hint 的):
    sample.teacher_log_probs = teacher_forward_pass(hint + original_response)
    # ↑ 真实的 teacher 分布(包含 hint 信息)

# openclaw-opd\openclaw_opd_api_server.py    
    # RL-only 样本(无 hint):
    sample.teacher_log_probs = torch.tensor(response_logprobs)  # = rollout_log_probs
    # ↑ 设置为 rollout log-probs 自身!
    # 这样 teacher_advantage = teacher_lp - old_lp ≈ 0
	# RL-only 样本的 OPD 贡献 ≈ 0,由 GRPO advantage 主导   

这个巧妙设计通过让 teacher_log_probs = rollout log-probs,RL only 样本自然地在 combined loss 中贡献 0 的OPD 信号。

0x04 三路分发逻辑

路径 条件 训练信号
OPD+RL hint被采纳 + (eval=±1) teacher_log_probs + reward
OPD-only hint被采纳 + (eval=0) teacher_log_probs + (reward=0)
RL-only hint被拒绝 + (eval=±1) (teacher_lp ← rollout_lp) + reward
Drop hint被拒绝 + (eval=0) 丢弃

4.1 三路分发逻辑

python 复制代码
    # _maybe_submit_ready_samples() 中的分发逻辑:
    opd_accepted = opd_result.get("accepted")    # hint 被接受?
    has_valid_rl = self._is_valid_rl_score(eval_score)  # eval 有效?

    if opd_accepted and has_valid_rl:
        → _submit_turn_sample(reward=eval_score)   # 合并样本
    elif opd_accepted:
        → _submit_turn_sample(reward=0.0)          # OPD-only
    elif has_valid_rl:
        → _submit_rl_turn_sample(eval_score)       # RL-only
    else:
        → 丢弃(无任何信号)
        
┌───────────┬─────────────┬─────────────────────────────────────────────┐
│ hint 接受?│ eval = ±1?  │ 提交类型 & 优势信号                             │
├───────────┼─────────────┼─────────────────────────────────────────────┤
│ ✅        │ ✅          │ Combined: A_t = w_opd*(teacher-old) + w_rl*r│
│ ✅        │ ❌ (0/None) │ OPD-only: A_t = w_opd*(teacher-old) + 0     │
│ ❌        │ ✅          │ RL-only:  A_t = 0 + w_rl*r (均匀广播)        │
│ ❌        │ ❌          │ 丢弃                                         │
└───────────┴─────────────┴─────────────────────────────────────────────┘        

4.2 3-Way Dispatch 为何是必要的

dispatch 的理论意义:它保证了两路信号在合并时互不污染。OPD 信号只在"有高质量 hint"时激活;RL 信号只在"有清晰质量判断"时激活。不满足条件的路径,对应信号被零化,而非被随机噪声替代。

四种情况的具体触发条件和设计逻辑如下(4.1 给出了分发表,这里展开每种情况的设计意图):

情况 触发条件 teacher_log_probs reward.score loss 主导
OPD-only acc + 0 真实教师 LP T 0.0 OPD 主导
RL-only !acc + ±1 rollout_lp T(代入) ±1.0 (PRM Eval) GRPO 主导
DROP !acc + 0 (不提交) (不提交)
OPD+RL acc + ±1 真实教师 LP T ±1.0 (PRM Eval) 两路叠加

情况 A(OPD-only):hint accepted + eval=0。人类信号不确定(中性),但教师有明确的改进建议 → 用 r=0 → grpo_adv=0 → 只走 OPD 路径,避免 RL 噪声污染好的蒸馏信号。

情况 B(RL-only):hint rejected + eval=±1。教师没能提炼出 hint(可能回复本来就很好)→ 令 teacher_lp=rollout_lp → teacher_adv≈0 → 纯 GRPO 训练,奖罚清晰,防止强行蒸馏一个没有 hint 的样本。

情况 C(DROP):hint rejected + eval=0。双无信号,什么都不做。

情况 D(OPD+RL):hint accepted + eval=±1。两路信号都可信,联合优化,这是信号最丰富的情况。

四种情况的 Sample 构造细节和梯度效果见 4.4 节。

4.3 与"有条件分支"的对比

朴素实现(有分支):

python 复制代码
if sample_type == "rl_only":    
	loss = rl_loss(r, pi_theta, pi_old) 
elif sample_type == "opd_only":    
	loss = opd_loss(pi_T, pi_theta, pi_old) 
elif sample_type == "opd_rl":    
	loss = rl_loss(...) + opd_loss(...) 

Combine 实现(无分支):

python 复制代码
#combine_loss.py
# old_log_probs 来自 rollout_log_probs (默认) 或 log_probs (依据 args.use_rollout_log_probs 标志)
combined_adv = w_opd * (teacher_lp - old_log_probs) + w_rl * grpo_adv
#样本类型编码在数值里,而非代码逻辑里

这类似于L1正则化(用绝对值的次梯度统一处理W=0和W≠0,而非分情况讨论),将控制流转化为数值代数,使得:

  • GPU执行路径统一:所有样本走同一段CUDA kernel,无条件分支带来的warp divergence
  • 批次可以混合三种样本:同一个mini-batch里OPD样本和RL样本共存,梯度累加后自然叠加
  • 解耦是严格的:不是"约等于零",而是代数上精确为零(π_T=π_old时差为精确0)

4.4 hint+eval_score 如何被利用

dispatch决策树

关键注意:eval_score=0 被判定为 has_valid_rl=False(因为 _is_valid_rl_score 只接受 ±1)。这意味着中性信号不参与 RL 训练,与 Binary RL 的设计理念(loss_mask=0 跳过)有所不同。

python 复制代码
opd_accepted =opd_result.get("accepted") # HintJudge 是否接受了hint
has_valid_rl =self._is_valid_rl_score(eval_score)#eval_score E{+1,-1}.(0不算)

# 情况A
if opd_accepted and has_valid_rl:
	_submit_turn_sample(..., reward=float(eval_score)) # OPD+RL
    
# 情况B 
elif opd_accepted:
	_submit_turn_sample(...,reward=0.0) # OPD-only
    
# 情况C 
elif has_valid_rl:
	_submit_rl_turn_sample(...,eval_score #RL-only
                           
# 情况D:两者都不满足 → 什么都不做(DROP)

每种情况的Sample构造与梯度效果

情况 A: OPD-only(_submit_turn_sample with reward=0.0)
  • 触发时机:HintJudge给出了有价值的hint(说明回复有改进空间),但PRMEval给出的是中性评分(o),即"看不出这条对话成功与否"。

  • 设计逻辑:此时用·RLreward=0会引入方差(零信号但与teacher_adv叠加可能方向不稳定),干脆令grpo_adv=0,只让教师引导token-level 改进。

python 复制代码
	sample.teacher_log_probs = torch.tensor(teacher_log_probs)#教师 LP正常 
	sample.reward ={"score":0.0} #RL信号被置零
	
	#combine_loss.py中的效果:
	grpo_advantages = broadcast(0.0) = 0  # RL 分支贡献 = 0
	teacher_advantages =teacher_lp-rollout_lp  #OPD分支正常
	
	combined_adv_t.=w_opd* teacher_adv_t+0.0
情况B: RL-only(_submit_rl_turn_sample)
  • 触发时机:Hint Judge 找不到有价值的 hint(可能回复本身已经很好),但 PRM Eval 有明确的质量判断(+1 或 -1)。
  • 设计妙点:不是"不传 teacher_log_probs"(这样代码要分叉),而是令 teacher_log_probs = rollout_log_probs,使教师优势 在 loss 中自然消失。combine_loss.py 的代码路径完全不变。
python 复制代码
    #关键行:# 注意:用的是rollout_log_probs,不是教师的!
	sample.teacher_log_probs =torch.tensor(response_logprobs,dtype=torch.float32)
	
	sample.reward ={score":float(eval_score)} #±1.0←PRM Eval 有效
    
    #combine_loss.py中的效果:
    teacher_advantages= teacher_lp-rollout_lp
    				  =rollout_lp-rollout_lp  #teacher=rollout=相同张量!
                      ≈0 #教师优势约为零(数值精度内)
    combined_adv_t = w_opd * 0 + w_rl * eval_score
    			   = w_rl * eval_score # 等价于纯 GRPO
情况 C: DROP(两信号都无)

hint rejected (accepted=False) + eval_score = 0

  • → 既没有改进建议,也不知道对话好坏
  • → 无有效信号,直接 continue(不进队列)
情况D: OPD+RL(_submit_turn_sample with reward=±1.0)

这是最核心的情况:RL保证方向,OPD在内部精细化每个token的权重。

python 复制代码
    #调用_submit_turn_sample(重写自父类)
    sample.teacher_log_probs= torch.tensor(teacher_log_probs) #教师 forward pass 结果
    sample.reward = {"score": float(eval_score)}  # ±1.0 来自 PRM Eval

    #combine_loss.py 中的效果:
    teacher_advantages=teacher_lp-rollout_lp #token-level:教师对每个token 的改进建议
    grpo_advantages=broadcast(reward) #sequence-level:eval_score广播

    combined_adv_t=w_opd * teacher_adv_t+w_rl *eval_score 

两个信号同时起作用的token示例:

python 复制代码
    假设 eval_score=+1(PRM评为正例)
		teacher_1p_t3-ro1lout_1p_t3=+0.5 #教师特别喜欢第3个token
    
    combined_adv_t3=1.0*0.5+1.0*1.0=+1.5 #→双重强化

    teacher_lp_t7-rollout_lp_t7=-0.3 # 教师不喜欢第7个token,即使整体是正例
                                           
    combined_adv_t7=1.0*(-0.3)+1.0*1.0=+0.7 # →削弱,但仍正

4.5 信息流总结图

这个设计的核心是信号有效性门控: 只有当 hint 可信时才启用 OPD, 只有当 eval 可信时才启用 RL, 两者独立判定, 不互相干扰。

python 复制代码
_opd_evaluate() 返回:
{
  accepted: True/False,    <- Hint Judge 多数投票结果
  teacher_log_probs: [...], <- 教师 forward pass (仅 accepted=True 时有值)
  eval_score: 1/0/-1/None,  <- PRM Eval 多数投票 (eval_mode 下)
}
           │
           │ 
           ▼

_maybe_submit_ready_samples() 3-way dispatch:
|
|-- accepted=True, eval_score=±1  ---------------------> OPD+RL Sample
|   teacher_lp = 教师的                                  reward = ±1
|                                                       combined_adv = OPD+GRPO
|
|-- accepted=True, eval_score=0/None -------------------> OPD-only Sample
|   teacher_lp = 教师的                                  reward = 0
|                                                       combined_adv = OPD only
|
|-- accepted=False, eval_score=±1 ----------------------> RL-only Sample
|   teacher_lp = rollout_lp (零化 OPD)                  reward = ±1
|                                                       combined_adv = GRPO only
|
|-- accepted=False, eval_score=0/None -----------------> DROP

4.6 权重参数的语义

关键洞察:因为两种样本类型"天然解耦"(RL样本的OPD贡献=0,OPD样本的RL贡献为0),权重对各分支的独立样本没有交叉干扰,只有在同一turn同时产生两种信号时(OPD+RL合并样本)才真正合并。

python 复制代码
OPENCLAW_COMBINE_W_OPD=1.0 #OPD分支的权重 
OPENCLAW_COMBINE_W_RL=1.0 #RL分支的权重

w_rl=1.0,w_opd=0.0 → 退化为纯BinaryRL
w_rl=0.0,w_opd=1.0 → 退化为纯OPD
w_rl=1.0,w_opd=1.0 → Combined(默认,实验显示最优)

权重 w_rl/w_opd 的调参建议

场景 建议配置 原因
用户反馈简短隐式 w_rl=1.5, w_opd=0.5 OPD hint 少,RL 更可靠
用户反馈详细纠正 w_rl=0.5, w_opd=1.5 OPD hint 质量高,信号更丰富
平衡场景 w_rl=1.0, w_opd=1.0 默认推荐

问题

问题:两个分量的尺度天然不同,A^opd 的方差 >> A^grpo 的方差。这会导致:→ OPD 信号在实际中主导梯度方向 → 名义权重 1:1,实际影响力可能是 3:1

三种确定比例的方法

方法 1:方差匹配(理论最优起点)

原理:确保两个信号的 "话语权"等量,然后再通过额外的全局比例调节偏好。

python 复制代码
# 运行时统计两个分量的标准差
σ_grpo = std(A^grpo across all tokens)    # 通常 ≈ 1.0(因为 reward ∈ {-1,+1})
σ_opd  = std(A^opd across all tokens)     # 需要实际测量

# 归一化使两者方差相等:
w_rl  = 1 / σ_grpo
w_opd = 1 / σ_opd

# 或等价地:
w_rl  = 1.0
w_opd = σ_grpo / σ_opd    # 如果 σ_opd = 2.0, σ_grpo = 1.0 → w_opd = 0.5

方法 2:梯度范数匹配(更精确)

这是多任务学习(GradNorm, PCGrad)的经典方法,但计算开销较大。

python 复制代码
# 不看 advantage,直接看两个信号对梯度的贡献
g_grpo = ∇_θ L_PPO(w_rl * A^grpo)      # GRPO 信号产生的梯度
g_opd  = ∇_θ L_PPO(w_opd * A^opd)      # OPD 信号产生的梯度

# 调节使 ||g_grpo|| ≈ ||g_opd||

方法 3:课程调参

原理:

  • 早期 model 很弱 → teacher 信号(密集 per-token)比 ±1 reward 更有信息量
  • 后期 model 接近 teacher → OPD 信号越来越弱,应让 RL 接管
训练阶段 w_opd w_rl 理由
早期 (step<100) 1.0 0.1 OPD 主导:先学 teacher 的分布
中期 (100-500) 0.5 0.5 均衡:同时学分布和 reward 信号
后期 (500+) 0.1 1.0 RL 主导:fine-tune reward 优化

0x05 讨论

5.1 符号定义

我们先进行符号定义,设 token 序列长度为 T,对第 t 个 token:

符号 含义
π_θ(a_t) 当前策略(本次前向传播的 log-probs)
π_old(a_t) rollout 时的旧策略(rollout_log_probs)
π_T(a_t) 教师策略(teacher_log_probs)
r PRM judge 给出的 reward(±1 或 0)

5.2 不同路径的贡献

对于RL-only样本:

python 复制代码
teacher_log_probs=rollout_log_probs(故意赋值) 
    → teacher_advantages=teacher-old 0
    → combined_adv=w_opd*0+w_rl*grpo_adv=w_rl*grpo_adv

对于OPD-only样本:

python 复制代码
reward=0.0
    → grpo_advantages=0(GRPO 计算时 reward=0 广播为 0)
    → combined_adv=w_opd*(teacher-old)+w_rl*0=w_opd*(teacher-old) 

对于Combined样本:

python 复制代码
teacher_log_probs#rollout_log_probs(真实teacher) 
	→ reward=±1
	→ combined_adv=w_opd *(teacher-old)+w_rl*r (两个分支都贡献) 

5.3 Combine为何能同时收益于OPD+RL

我们看看Combine为何能同时收益于OPD+RL。

理论解释:两种信号的互补视角

两路信号测量的是不同维度

RL信号(Binary RL / GRPO):
  • r ∈ {+1, 0, -1} 来自人类行为
  • 测量:这条对话轨迹整体是否成功?是从全局(整体response质量)视角出发
  • 粒度:序列级,无credit assignment
  • 优点:无级联;
  • 缺点:无法区分哪些token有责任

具体问题如下:

python 复制代码
轨迹 [a1, a2, ..., aT] 获得 reward = +1
    ↓
gradient = +1 * Σ_t ∇ log π_θ(a_t)
    ↓
所有 token 同等被强化,包括:
  ✓ 真正好的 token (理应强化)
  × 平庸/随机 token (被误强化 → 方差来源
无法区分哪些token有责任
OPD信号(Teacher LP)
  • A_t=log π_T(a_t|s,hint)-log π_old(a_t|s) → 来自教师 forward pass

  • 测量:第t个token是否可以被教师改善?

  • 粒度:Token 级,自带credit assignment

  • 局部(token条件概率)视角

  • 优点:细粒度

  • 缺点:级联污染

python 复制代码
teacher_adv_t = log π_T(a_t|hint) - log π_old(a_t)
    ↓
无论对话整体质量如何,都在"教"学生模仿教师
    ↓
即使对话很糟糕 (RL=-1),OPD 仍在强化某些 token
    → 可能强化"表达方式好但方向错误"的回复
Combined

上述两路信号在信息论层面不余。RL知道"好不好"但不知道"哪里好";OPD知道"哪里可以改"但不知道"整体值不值得改"。

Combine 的协同效应:

  • 全局信号为锚,局部信号做细化,RL提供"方向正确"的保证,OPD在内部精细化credit

  • ∂L/∂θ=OPD梯度(精准但有噪声)+ GRPO梯度(粗糙但无偏)

  • 两者在级联位置方向相反→自然对消

python 复制代码
    combined_adv_t = w_rl * r + w_opd * (teacher_lp_t - old_lp_t)
    
    ------ OPD+RL 样本 (hint accepted, eval=+1) ------------------
    
    RL 说:这条轨迹是正例 (r = +1)
    OPD 说:token t1 教师特别喜欢,token t2 无差异,token t3 教师不喜欢
    
    combined_adv:
        t1: w_rl*(+1) + w_opd*(big positive) → 双重强化
        t2: w_rl*(+1) + w_opd*(≈0) → 仅被 RL 适度强化
        t3: w_rl*(+1) + w_opd*(negative) → 对冲,净效果小

方差一偏差权衡

为什么Combine偏差低:OPD的教师hint是针对"知道next_state"的反事实,与真实对话质量有相关但不相同。RL的reward直接来自人类行为(user的下一步操作)。Combine中RL扮演"质量门控"角色,防止OPD 在差回复上进行无效蒸馏。

python 复制代码
			偏差(Bias)   方差(Variance)
----------------------------------------
Binary RL   低            高(all tokens receive same signal)
OPD         中            低(smooth per-token signal) 
Combine     低            较低(RL约束偏差,OPD降方差)

最优控制视角:Q函数的近似分解

在RL理论中,最优Q函数可以写成:Q*(s,a_1:T)=累积折扣奖励的期望。实践中无法直接计算,需要估计。

Combine隐式地做了这样的分解:

python 复制代码
Q(S,a_1:T) = R_RL                ---> RL提供轨迹级return估计
              + ∑_t A_OPD(a_t)   ---> OPD提供per-step局部优势估计

这与Advantage Actor-Critic(A2c)的结构相似:

  • A2C 中的Critic·提供V(s)来减小方差
  • OPD中的教师提供per-token的"局部价值"信号

区别:

  • A2C的Critic是训练的Value网络,OPD的"Critic"是固定的教师模型+hindsight信息。
  • OPD 不用维护额外参数,且信号是"反事实已知结果",在某种意义上比Critic估计更准确(不是估计,而是观测到的真实 next_state)。

解耦的本质:信号空间的正交分解

可以把 combined advantage 写成如下,其中:A_opd = log π_T - log π_old (教师优势, per-token);A_rl = r (RL 优势,序列级广播):

复制代码
A = w_opd · A_opd + w_rl · A_rl 

即

a_t=w_opd⋅(logπ_T(a_t)−logπ_old(a_t)) ← OPD 项
       +wrl · ⋅r                      ← GRPO 项(标量广播到每个 token)

三路样本的激活矩阵:

复制代码
            A_opd ≠ 0?    A_rl ≠ 0? 
OPD+RL        √                √    → 联合信号 
OPD-only      √                ×    → 仅 OPD 
RL-only       ×                √    → 仅 RL 
Drop          ×                ×    → 丢弃(不入队) 

关键:× 不是通过 if-else 实现的,而是通过数值赋值让对应项代数归零,即通过trick让公式自然退化:

  • RL-only:teacher_log_probs=rollout_log_probs→OPDadvantage=0→只剩GRPO
  • OPD-only:reward=0→GRPOadvantage=0→只剩OPD OPD+RL:两者都非零→加权求和

梯度分解

PPO clip 损失(忽略 clip 边界,分析梯度方向):

复制代码
∂L/∂θ = -a_t · ∂logπ_θ(a_t)/∂θ
      = -[w_opd·(logπ_T - logπ_old) + w_rl·r] · ∂logπ_θ(a_t)/∂θ

三路样本的梯度分解

python 复制代码
Case 1:OPD+RL(hint 被接受+eval=±1)
    π_T ≠ π_old,r=±1
    At=w_opd·(logπ_T-logπ_old)+w_rl·r
    dL/dθ = -[w_opd·(logπ_T - logπ_old) + w_rl·r] · ∇logπ_θ
    两个信号都有非零贡献,方向由叠加决定。 

Case2:OPD-only(hint 被接受+eval=0)
    π_T ≠ π_old, r=0→grpo_adv=0
    At=w_opd·(logπ_T-logπ_old)+w_r1·0 =w_opd·(logπ_T-log π_old)
    dL/dθ = -w_opd·(logπ_T - logπ_old) · ∇logπ_θ
                                      ↑
                               仅教师信号驱动梯度,RL项完全消失

Case 3:RL-only(hint 被拒绝+ eval=±1)
    π_T←π_o1d,r=±1
    At = w_opd·(log π_old-logπ_old)+w_rl·r
        =w_opd.0                     +w_rl·r
        =w_rl·r
    
    dL/dθ = -w_rl·r · ∇logπ_θ
 					   ↑
               仅reward驱动梯度,教师信号完全消失(代数精确,非近似)

直觉总结

比喻:学生(Policy)、老师(Teacher)、用户反馈(RL Reward)

  • 纯RL: 用户说"这次对话很棒"学生强化所有行为(含运气)
  • 纯OPD: 老师说"第3句可以这样改" → 学生模仿但不知整体质量
  • Combine: 用户验证"对话成功"+老师指出"具体改善点" → 学生知道"朝对的方向,而且知道在哪个词上发力"

5.4 Combine 方法是否缓解了 OPD 级联错误问题

结论:

  • 有,通过"GRPO 全局惩罚 vs OPD 局部级联"的互补对冲,部分缓解---但仅在 OPD+RL 路径下有效。

  • 不是人为消除级联,而是让GRPO的全局信号作为隐式正则,自动压制OPD的局部级联噪声。

具体场景分析

假设最终 PRM 打分 = -1 (Bad) :

python 复制代码
	response: ["Let", "me", "compute", "1764÷2=882", "then", "sqrt(882)≈29.7"]
                                                ↑ 错误!

具体效果如下:

OPD 信号(有级联问题):

python 复制代码
teacher_adv: [0.0, 0.0, 0.0, -5.7, +0.8, +2.1]
							   ↑ 级联导致后续 token 被错误正向强化
						     正确惩罚

GRPO 信号(无级联,但粗粒度):

python 复制代码
grpo_adv: [-1, -1, -1, -1.0, -1.0, -1.0]
                          ↑
                          均匀惩罚所有token

Combined(对冲!):GRPO 的负向均值把 OPD 的错误级联信号向下拉了回来。

python 复制代码
combined = w_opd * teacher_adv + w_rl * grpo_adv
         = 1.0 * [-5.7, +0.8, +2.1] + 1.0 * [-1.0, -1.0, -1.0]
         = [-6.7, -0.2, +1.1]  ← 级联正向信号被大幅压制
                   ↑        ↑
            从+0.8→-0.2  从+2.1→+1.1(还有残余,但小了很多)

Combine 3-way Dispatch 中的不对称缓解

python 复制代码
---------------------|----------------------------------------------------|
| Dispatch 路径      | 级联错误缓解效果                                      |
|--------------------|----------------------------------------------------|
| OPD + RL           | ✓ 有缓解:GRPO 全局信号对抗 OPD 局部级联               |
| (hint接受+eval≠0)  |   负向样本:GRPO 拉低 OPD 级联正值(如上例)             |
|                    |   正向样本:GRPO 拉高 OPD 级联负值(方向对称)           |
|--------------------|----------------------------------------------------|
| OPD only           | × 无缓解:eval=0 无 GRPO 项,级联错误 100% 保留         |
| (hint接受+eval=0)  |   (恰恰是"模型表现平平"的样本,teacher 还不确定对错)     |
|--------------------|----------------------------------------------------|
| RL only            | - 无 OPD 信号(teacher_lp=rollout_lp → 差值≈0)      |
| (hint拒绝+eval≠0)  |   退化为纯 GRPO,无级联问题(但也无细粒度)              |
---------------------|----------------------------------------------------|

残余问题

即使Combine.也无法完全解决的情况:

  • eval=0的OPD-only样本:hint被接受但模型表现中性,此时恰好级联问题最严重而GRPO缺席
  • W_r1设置过小时:GRPO信号太弱,无法有效对抗级联
  • 长CoT开头的错误:越早的级联,污染的token越多,GRPO的均匀惩罚越难精确定位

0xFF 参考