【Agentic RL / 强化学习 / OPD】Hermes & OPD 源码阅读笔记

【Agentic RL / 强化学习 / OPD】Hermes & OPD 源码阅读笔记

目录

  • [【Agentic RL / 强化学习 / OPD】Hermes & OPD 源码阅读笔记](#【Agentic RL / 强化学习 / OPD】Hermes & OPD 源码阅读笔记)
    • [0x00 概要](#0x00 概要)
    • [0x01 OPD 机制原理 ------ "OPD 是什么,怎么工作"](#0x01 OPD 机制原理 —— "OPD 是什么,怎么工作")
      • [1.1 OPD 的完整数据流](#1.1 OPD 的完整数据流)
      • [1.2 vLLM](#1.2 vLLM)
      • [1.3 Teacher Scoring vs Teacher Forcing](#1.3 Teacher Scoring vs Teacher Forcing)
        • [1.3.1 两者的本质区别](#1.3.1 两者的本质区别)
        • [1.3.2 总结](#1.3.2 总结)
      • [1.4 OPD 对于推理的特殊需求](#1.4 OPD 对于推理的特殊需求)
        • [1.4.1 使用 vLLM](#1.4.1 使用 vLLM)
        • [1.4.2 精确的 token IDs](#1.4.2 精确的 token IDs)
        • [1.4.3 双重角色](#1.4.3 双重角色)
      • [1.5 OPD 蒸馏完整管线详细分析](#1.5 OPD 蒸馏完整管线详细分析)
        • [1.5.1 Step 1: 标准 Rollout (继承自 HermesAgentBaseEnv)](#1.5.1 Step 1: 标准 Rollout (继承自 HermesAgentBaseEnv))
        • [1.5.2 Step 2: 提取 Turn Pairs - _extract_turn_pairs()](#1.5.2 Step 2: 提取 Turn Pairs - _extract_turn_pairs())
        • [1.5.3 Step 3: LLM Judge 提取 Hint - _extract_hint()](#1.5.3 Step 3: LLM Judge 提取 Hint - _extract_hint())
        • [1.5.4 Step 4: 构建增强 Prompt + VLLM Scoring - _opd_for_sequence()](#1.5.4 Step 4: 构建增强 Prompt + VLLM Scoring - _opd_for_sequence())
        • [1.5.5 Step 5: 打包训练信号 - _apply_opd_pipeline()](#1.5.5 Step 5: 打包训练信号 - _apply_opd_pipeline())
        • [1.5.6 训练端如何使用新字段?](#1.5.6 训练端如何使用新字段?)
      • [1.6 论文定义的四个关键机制](#1.6 论文定义的四个关键机制)
    • [0x02 实现对比 ------ "同一算法,两套系统"](#0x02 实现对比 —— "同一算法,两套系统")
      • [2.1 从代码看 OPD 实现](#2.1 从代码看 OPD 实现)
      • [2.2 OpenClaw-RL 异步Proxy 架构 vs Hermes Batch 架构](#2.2 OpenClaw-RL 异步Proxy 架构 vs Hermes Batch 架构)
        • [2.2.1 OpenClaw-RL 架构:在线异步Proxy](#2.2.1 OpenClaw-RL 架构:在线异步Proxy)
        • [2.2.2 Hermes架构:离线Batch环境](#2.2.2 Hermes架构:离线Batch环境)
        • [2.2.3 设计差异对比](#2.2.3 设计差异对比)
        • [2.2.4 三个最重要的架构差异](#2.2.4 三个最重要的架构差异)
      • [2.3 logprobs & loss & advantage](#2.3 logprobs & loss & advantage)
      • [2.4 架构级差异](#2.4 架构级差异)
      • [2.5 一句话总结](#2.5 一句话总结)
    • [0x03 训练端分歧 --- "传单值 vs 传分布"](#0x03 训练端分歧 — "传单值 vs 传分布")
      • [3.1 通俗类比](#3.1 通俗类比)
        • [3.1.1 传单值(OpenClaw-RL的做法)](#3.1.1 传单值(OpenClaw-RL的做法))
        • [3.1.2 传分布(Hermes/Atropos的做法)](#3.1.2 传分布(Hermes/Atropos的做法))
        • [3.1.3 训练时的区别](#3.1.3 训练时的区别)
        • 一句话总结
      • [3.2 Reward vs Advantage 概念澄清](#3.2 Reward vs Advantage 概念澄清)
        • [3.2.1 术语澄清](#3.2.1 术语澄清)
        • [3.2.2 OPD区别](#3.2.2 OPD区别)
        • [3.2.3 训练](#3.2.3 训练)
        • [3.2.4 总结](#3.2.4 总结)
      • [3.3 KL散度蒸馏 loss 的实际实现](#3.3 KL散度蒸馏 loss 的实际实现)
        • [3.3.1 Reverse KL](#3.3.1 Reverse KL)
        • [3.3.2 核心算法:"Top-K + Tail Trick"](#3.3.2 核心算法:"Top-K + Tail Trick")
        • [3.3.3 实际代码解读](#3.3.3 实际代码解读)
    • [0x04 设计哲学 --- "为什么选这条路"](#0x04 设计哲学 — "为什么选这条路")
      • [4.1 选择原因](#4.1 选择原因)
        • [4.1.1 原因 1:解耦架构的必然结果](#4.1.1 原因 1:解耦架构的必然结果)
        • [4.1.2 原因 2:Trainer 可替换性](#4.1.2 原因 2:Trainer 可替换性)
        • [4.1.3 原因3:数据格式的约束](#4.1.3 原因3:数据格式的约束)
        • [4.1.4 原因4:工程简洁性](#4.1.4 原因4:工程简洁性)
      • [4.2 RL 训练全流水线](#4.2 RL 训练全流水线)
      • [4.3 OPD 蒸馏管线](#4.3 OPD 蒸馏管线)
      • [4.4 数据交付](#4.4 数据交付)
    • [0x05 Top-K 解读](#0x05 Top-K 解读)
      • [5.1 为什么需要 Top-K 蒸馏?单值 OPD 的局限](#5.1 为什么需要 Top-K 蒸馏?单值 OPD 的局限)
      • [5.2 Top-K 蒸馏在 OpenClaw 中的位置](#5.2 Top-K 蒸馏在 OpenClaw 中的位置)
      • [5.3 核心机制:Tail Trick + Reverse KL](#5.3 核心机制:Tail Trick + Reverse KL)
        • [5.3.1 问题:15 万词表 vs 50 个 top-K](#5.3.1 问题:15 万词表 vs 50 个 top-K)
        • [5.3.2 Tail 的数值计算](#5.3.2 Tail 的数值计算)
        • [5.3.3 Reverse KL 散度](#5.3.3 Reverse KL 散度)
      • [5.4 完整数据流:从 API Server 到 Loss](#5.4 完整数据流:从 API Server 到 Loss)
      • [5.5 关键设计选择:Teacher Top-K vs Student Top-K](#5.5 关键设计选择:Teacher Top-K vs Student Top-K)
        • [5.5.1 为什么选 Teacher Top-K?](#5.5.1 为什么选 Teacher Top-K?)
        • [5.5.2 Student Top-K 的优势(OpenClaw 放弃的)](#5.5.2 Student Top-K 的优势(OpenClaw 放弃的))
      • [5.6 数值风险](#5.6 数值风险)
      • [5.7 两种 OPD 路径对比总表](#5.7 两种 OPD 路径对比总表)
      • [5.8 常见问题](#5.8 常见问题)
    • [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

Hermes Agent 中也涉及到了 OPD,事实上,Hermes OPD=OpenClaw OPD 的"Atropos 移植版"

python 复制代码
相同(100%一致):
    Judge prompt 文本
    投票策略(选最长positivehint)
    Hint 插入格式([user'shint/ instruction])
    Parse 逻辑 (\boXed{} + [HINT_START]...[HINT_END]) 
    Top-K teacher scoring 思路
    
不同
	(架构适配):在线vs离线
    SGLang vs VLLM
    单turn vs 全 trajectory 
    有student logprobs vs 无 
    reward=1.0 vs 三信号加权
    advantage 融合 vs 分离式 loss

本质:相同的OPD算法,不同的工程实现。

  • Hermes适配了Atropos 解耦架构(环境-trainer 分离),
  • OpenClaw适配了SLIME一体化架构(环境-trainer 紧耦合)。

0x01 OPD 机制原理 ------ "OPD 是什么,怎么工作"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1.1 OPD 的完整数据流

OPD 的完整数据流如下图所示。

python 复制代码
1.学生模型rollout → 生成agent对话(含tool calls + results)
            ↓
2.找到 (assistant_turn,next_state)对
  例如:assistant写了有bug的代码 → tool返回pytest失败信息
            ↓
3.LLM Judge 从 next_state 提取 hint
  例如:"应该用dict而不是list来存储映射关系" 
            ↓
4.构建enhanced prompt=原始上下文+hint
            ↓
5.关键步骤:用VLLM的get_logprobs()计算:
  - 输入:enhanced_prompt+学生原始回复的tokens
  - 输出:模型在"知道hint"的条件下,对学生每个token的top-k logprobs 
            ↓
6.得到 distill_token_ids[pos][k] 和 distill_logprobs[pos][k]
  这就是 "teacher distribution"---------带有后验知识的概率分布 
            ↓    
7.Trainer计算逐token优势:
  A_t = teacher_logprob(token_t) - student_logprob(token_t)
  正值→teacher认可这个token→上调权重
  负值→teacher不认可→下调权重

1.2 vLLM

OPD 使用vLLM(Managed Server)进行推理。VLLM get_logprobs完整技术链如下:

python 复制代码
OPD 环境(agentic_opd_env.py)
   └────self.server.get_logprobs(input_ids=enhanced_ids, top_k=50) 
			↓    
   ManagedServer.get_logprobs() (managed_server.py)
   	- 如果传入 messages→用tokenizer.apply_chat_template 转为 prompt 
   	- 委托给底层server
			↓ 
    VLLMServer._get_logprobs_wrapper() (vllm_server.py)
    	- 构建请求:{"prompt":{"prompt_token_ids":[...]}, "prompt_logprobs": 50} 
    	- 发送HTTP POST 到VLLM的/generate端点
    	- 解析返回的prompt_logprobs,归一化为统一格式 
    		↓ 
    VLLM/generate端点(VLLM服务进程)
    	- 接收prompt_token_ids(精确的token 序列)
    	- 对每个位置计算条件概率 P(token_i | token_0, ..., token_{i-1})
    	- 返回每个位置的 top-K token IDs+logprobs

VLLM/generate的关键请求参数如下。其中的关键设计:max_tokens=1一这个请求几乎不生成新文本。它的目的纯粹是让 VLLM 对输入的 prompt token 序列进行一次完整的前向传播,返回每个位置的prompt_logprobs(即条件概率)。

python 复制代码
 request_data = {
    "prompt":{"prompt_token_ids":prompt_tokens},# 精确 token 序列
    "prompt_logprobs":,top_k, # 每个位置返回top-k个logprobs
    "n":1,                    # 只要1个序列
    "temperature":0.0,        # 确定性(不需要采样)
    "top_p":1.0,
    "max_tokens":1,           # 几乎不生成新token(只要promptlogprobs)
}

返回数据格式

python 复制代码
{
    "prompt_tokens":[101,2023,3854,...], #输入 token IDs
    "prompt_topk_token_ids":[  #每个位置的 top-K token IDs
        [3854,7142,9821,...] #位置0
        [2001,5431, 1234,...] #位置1
   		 ...
    ],
	"prompt_topk_logprobs":[   #对应的logprobs  
		[-0.12,-1.45,-2.31,...],#位置0
		[-0.05,-0.89,-1.67,...],#位置1
        ...
    ],
}

为什么是prompt_logprobs而不是生成log probs?

这是整个设计中最精妙的部分。普通的 LLM API 返回的 logprobs是模型自己生成的 token 的概率。但OPD 需要的不是这个一它需要:给定enhanced prompt(含hint),模型对学生已经生成的每个token的评分是多少?

这在技术上叫做teacher scoring:你把"增强后的上下文+学生原始回复" 拼接成一个完整序列,让模型做一次前向传播,读取prompt_logprobs。这告诉你:如果模型看到了 hint,它在每个位置会给学生的 token 多高的概率?

python 复制代码
增强prompt=[系统提示]+[任务]+[hint:"应该用dict"]+[学生的原始代码]
                                                    ↑
										VLLM在这里的prompt_logprobs 就是teacher 的评分

1.3 Teacher Scoring vs Teacher Forcing

OPD 数据流的核心动作是第 5 步的 Teacher Scoring。OpenClaw的"teacher scoring"本质上是:

  • 用 hint 增强后的 teacher 视角,对 student 的 response 做概率评估,
  • 得到的 teacher_log_probs 用于OPD advantage 计算。

在深入技术细节之前,有必要先厘清两个看起来很像的概念:Teacher Scoring 与 Teacher Forcing。

Teacher Scoring vs Teacher Forcing 这是两个完全不同的概念,虽然名字相似。 前者是"评估已有答案的概率",后者是"用正确答案训练"。两者都利用了全序列并行的 forward pass,但目的不同。

Teacher Forcing(经典 NLP 技术)

python 复制代码
训练时:
    输入:[The,cat,sat,on]    ← 真实ground truth tokens
    预测: [cat,sat,on,the]    ← 模型预测下一个token

关键:每步输入是ground truth token,不是模型自己上一步的输出
    →避免error accumulation(错误滚雪球) → 所有SFT/预训练都用teacher forcing

Teacher Scoring(OpenClaw OPD 中使用的技术)

python 复制代码
不是"forcing",是"scoring":

给teacher一个完整序列(prompt+hint+学生的response)
让teacher 做 forward pass,输出每个 token 的 log-prob。

teacher不生成任何新token,只是"打分"(评估每个token的概率)

1.3.1 两者的本质区别

Teacher Forcing Teacher Scoring
目的 训练(提供 ground truth 输入) 评估(获取概率分布)
谁的 token Ground truth 数据 Student 已生成的 response
产出 模型的预测 + loss 每个 token 的 log-prob
是否生成 不生成(但计算 loss) 不生成(只计算 prob)
并行性 ✓ 全序列并行 ✓ 全序列并行
用途 SFT/预训练 知识蒸馏(OPD)

1.3.2 总结

  • Teacher Forcing:"我告诉你正确答案,你学着预测"
  • Teacher Scoring:"你已经写好了答案,我来评估每个词写得好不好"。Teacher Scoring 本质就是:给一个完整序列做 forward pass,返回 log-probs,不生成新 token。

两者都利用了"已知完整序列 → 一次 forward pass"的并行性,但目的完全不同。

1.4 OPD 对于推理的特殊需求

1.4.1 使用 vLLM

为什么 OPD 必须是 VLLM(或者类似的工具,比如SGLang)而不是 OpenAI API?这是因为OPD需要:

  • Tokenizer ---- 将 enhanced prompt + response 编码为 token IDs
  • prompt_logprobs --- 计算增强分布下对每个response token的概率
  • 精确token 对齐 ---- 将teacher logprobs映射回学生序列的正确位置

这三个都是VLLM独有的。OpenAI API只能告诉你"模型生成了什么",但OPD需要知道"如果模型看到了hint,它会对学生的每个token 给出怎样的概率"。

这里的关键是token 对齐。OPD 需要知道"位置 37的 teacher logprob 对应学生 token 序列的位置 37"。这要求输入是精确的prompt_token_ids 不是文本(文本会被重新tokenize,可能产生不同的分词)

这就是为什么VLLM是OPD的硬依赖一一一整个teacher scoring pipeline建立在VLLM独有的prompt_logprobs 能力之上,需要token 级别的精确控制。

1.4.2 精确的 token IDs

RL 训练为什么需要精确的 token IDs。下面从 RL 训练的数学本质开始分析。

核心原因:RL训练的梯度计算发生在token级别,不是文本级别。

1. 训练目标公式

RL训练(GRPO/PPO)的核心 loss 是:

python 复制代码
L=-E[A(s,a)·log π_θ(a_t | s_t)] 

其中:

python 复制代码
π_θ(a_t | s_t)=模型在位置t生成token a_t的概率
A(s,a) = advantage(这个 rollout 有多好)

因此,计算 logπ_θ(a_t| s_t)需要知道a_t是哪个 token ID。

2. 如果只有文本会怎样?

python 复制代码
    文本:"Hello world"
        │ 自己tokenize
        ▼
    Token IDs:[15496,995]  ←  你用你的tokenizer编码的

但因为 tokenizer 版本的原因,模型实际生成时的token可能是:

python 复制代码
 Token IDs:[15496,995]  ←  恰好一样(简单词) 
 
 or [1544,7799,995]     ←  如果tokenizer版本不同

另外,对简单文本可能对得上,但 agentic 场景充满特殊 token:

python 复制代码
<tool_call>terminal</tool_call>{"command":"ls"} 

这段文本的tokenization方式取决于:

  • 是否有特殊token<tool_call>→可能是1个token或多个
  • 模型的chat template如何处理
  • BPE merge 在上下文中的行为

因此,只有推理服务器本身产出的token IDs才是"ground truth"。

3. Mask 也需要精确对齐

训练时只对 assistant产出的 token 计算loss:

python 复制代码
System:... User:...    Assistant:Hello world <tool_call>...
[mask] [mask] [mask]            [TRAIN] [TRAIN] [TRAIN][TRAIN]

如果你事后tokenize,mask的边界会错位→训练信号错误。

Managed Server的Sequence Nodes天然记录了哪些token是生成的(需要训练),哪些是prompt(需要 mask)。

4. OPD更极端

OPD需要计算teacher的逐token logprob分布:

python 复制代码
Teacher distribution at position t: 
    token_3847 → -0.23 (logprob)
    token_9182→-1.05 
    token_5521→-2.31
    ...Top-50 tokens

Student要模仿这个分布→KL(student || teacher)

如果tokenIDs对不上,student在位置t看到的分布就对不上实际的token序列,KL loss完全无意义。

5.一句话总结

场景 需要什么 为什么
SFT 文本就够 目标明确("照着说"),框架自行tokenize
RL(GRPO/PPO) 精确 token IDs + mask Loss = per-token logprob x advantage
OPD 精确token IDs + 逐token logprob 分布 Loss = per-token KL divergence

注:SFT可以事后tokenize,是因为目标序列是"标准答案"。RL的目标序列是模型自己生成的 rollout---必须和模型内部token完全一致,否则梯度计算就是垃圾。

1.4.3 双重角色

OPD 设计上的巧妙之处在于:OPD用同一个VLLM服务器同时扮演两个角色:

  • 学生(Phase 2 rollout):通过 ManagedServer 生成 trajectory,获取精确 token +logprobs
  • Teacher(hint-augmented scoring):通过 get_logprobs()评估学生token,提供密集蒸馏信号

这意味着自蒸馏(self-distillation)---teacher 和 student 是同一个模型,但 teacher 拥有"后验知识"(hint)。随着训练推进,student逐渐内化这些hints,不再需要显式的工具反馈就能生成更好的代码。

1.5 OPD 蒸馏完整管线详细分析

总览: 5 步管线:

python 复制代码
Rollout → 提取 Turn Pairs → LLM Judge 提取 Hint → 构建增强 Prompt → VLLM Scoring → 训练信号

1.5.1 Step 1: 标准 Rollout (继承自 HermesAgentBaseEnv)

collect_trajectories() 先调用 super().collect_trajectories(item) 执行标准的 Phase 2 rollout, 得到ScoredDataGroup (包含 tokens/masks/scores)。OPD 是在此基础上的后处理增强。

1.5.2 Step 2: 提取 Turn Pairs - _extract_turn_pairs()

遍历对话历史, 找到所有 (assistant_turn, next_state) 对:

python 复制代码
messages = [system, user, assistant₁, tool₁, tool₂, assistant₂, tool₃, assistant₃(final)]
                          ^^^^^^^^^^^^^^^^^^^^^^    ^^^^^^^^^^^^^^^^^^
                           turn pair 1              turn pair 2

每个 pair 包含:

  • context_messages - 该 assistant turn 之前的所有消息
  • assistant_text - assistant 的回复内容
  • next_state_text - 后续的 tool 结果/user 回复 (多个结果用 --- 连接)
  • next_state_role - "tool" 或 "user"

长工具输出按 hint_max_next_state_chars 截断。

1.5.3 Step 3: LLM Judge 提取 Hint - _extract_hint()

核心思想: next_state 包含事后才知道的信息 (hindsight), 例如:

  • 测试失败的错误信息 → 揭示了代码 bug
  • 命令执行的输出 → 揭示了环境状态
  • 用户的纠正 → 揭示了需求理解偏差

Judge Prompt :

python 复制代码
System: "You are a process reward model used for hindsight hint extraction..."
User:  "## Assistant response (turn t)\n{response}\n## Next state (turn t+1) [role: {role}]\n{next_state}"

关键设计: Judge 被明确告知 role 的语义---role='tool' 意味着这是 assistant调用工具后才产生的结果, 不应视为 assistant事先应该知道的信息。成功的工具调用意味着 assistant 的行为是合理的。

多数投票:

  • 并发发送 prm_votes (默认 3) 次 judge 查询
  • 每次 temperature=0.7
  • 输出格式: \boxed{1} + HINT_START...HINT_END 或 \boxed
  • _select_best_hint(): 从所有 score=1 的投票中选最长的 hint (信息量最大)
  • 至少一个投票为正且 hint 长度 > 10 字符才返回

1.5.4 Step 4: 构建增强 Prompt + VLLM Scoring - _opd_for_sequence()

对每个有效 hint:

4a. 构建增强消息 - _append_hint_to_messages() (288-310):

python 复制代码
# 深拷贝 context_messages, 在最后一个 user 消息末尾附加:
"\n\n[user's hint / instruction]\n{hint}"

4b. Tokenize 增强 Prompt:

python 复制代码
enhanced_prompt = tokenizer.apply_chat_template(enhanced_messages, add_generation_prompt=True)
enhanced_full = enhanced_prompt + assistant_text  # 原始 response 追加在后面
enhanced_ids = tokenizer(enhanced_full)["input_ids"]

4c. VLLM get_logprobs 调用:

python 复制代码
logprob_result = await self.server.get_logprobs(
    input_ids=enhanced_ids,    # 增强 prompt + student response
    top_k=k,                   # distill_topk (默认 5)
    split="eval",              # 使用 eval semaphore, 不阻塞训练
)

返回每个 token 位置的 top-K 预测: {prompt_topk_token_ids, prompt_topk_logprobs}

4d. 提取 response 部分的 teacher 分布 只取最后 response_len 个位置的

logprobs, 这些对应 assistant 回复的token 在增强分布下的概率。

4e. 映射回全序列:通过 _find_token_span() 在学生的完整 token 序列中定位该 assistant

turn 的位置, 将 teacher 的top-K logprobs 写入对应位置。

1.5.5 Step 5: 打包训练信号 - _apply_opd_pipeline()

最终在 ScoredDataGroup 上设置两个新字段:

python 复制代码
group["distill_token_ids"]    # shape: [group_size][seq_len][top_k]
group["distill_logprobs"]     # shape: [group_size][seq_len][top_k]

失败的序列用零填充 [[0]*k]*seq_len, 保证 shape 一致。

1.5.6 训练端如何使用新字段?

Atropos trainer 读取这些字段计算逐 token 优势:

python 复制代码
A_t = teacher_logprob(token_t) - student_logprob(token_t)
    -A_t>O:teacher(知道hint后).认为这个token更好→upweight
    -A_t<O:teacher认为这个token不好→downweight
    -A_t=O(零填充位置):没有蒸馏信号→退化为标准RL

数据流图如下

python 复制代码
Rollout (Phase 2)
    |
    | messages + tokens
    |
    ▼
[ _extract_turn_pairs ]
    |
    | turn pairs[]
    |
    ▼
[ _extract_hint (per turn pair) ] <-- LLM Judge x prm_votes (majority vote)
    |
    | hint string
    |
    ▼
[ _append_hint_to_msgs + tokenize ] <-- context + hint -> enhanced prompt
    |
    | enhanced_ids
    |
    ▼
[ server.get_logprobs (teacher scoring) ] <-- VLLM prompt_logprobs
    |
    | top-K teacher dist
    |
    ▼
[ _find_token_span + merge into array ] <-- map back to student sequence
    |
    | distill_token_ids[seq_len][k]
    | distill_logprobs[seq_len][k]
    |
    ▼
[ ScoredDataGroup + distill fields ] --> Atropos Trainer
                                         A_t = teacher_lp - student_lp

1.6 论文定义的四个关键机制

OPD 论文将上述管线中的核心设计抽象为四个关键机制。这是从 OPD 机制走向实际系统实现的桥梁。

python 复制代码
OPD → token 级 teacher logprobs → 逐token指导 → 独立于reward运行

OpenClaw-RL论文的设计:

  • Evaluative signal(标量reward):通过 PRM Judge打分,基于 next_state评价 action 好坏
  • Directive signal(OPD 蒸馏):从 next_state提取hint→teacher logprobs→ token 级指导
  • 两者组合为 hybrid RL objective
机制 论文描述
Hint 提取 PRM Judge 从 next-state 中提取 textual hint
Teacher scoring 用 hint 增强 prompt -> 计算 token-level logprob gap
Overlap-guided hint selection 当 teacher/student 分布差异过大时,选择与 student top-K 重叠度最高的 hint
Log-prob-difference clip 裁剪逐 token 的 advantage 估计,防止 teacher/student 严重不一致时训练不稳定

以上是 OPD 的机制原理------它是什么、怎么工作、依赖什么基础设施。

而 Hermes 和 OpenClaw-RL 实现了相同的 OPD 算法,但架构路线完全不同。

0x02 实现对比 ------ "同一算法,两套系统"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Hermes 的 OPD 实现本质上就是 OpenClaw-RL 官方代码的移植,适配到了 Atropos/VLLM 的技术栈上。

最大的架构差异是OpenClaw-RL作为异步proxy server 运行(拦截实时对话),而Hermes 作为离线batch env 运行(收集 rollout 后再做 OPD)

Hermes/Tinker (分离式) OpenClaw (融合式)
选择原因 解耦架构约束 + Trainer 可替换 单进程控制所有数据
数据要求 只需 teacher logprobs 需要 teacher + student logprobs
Trainer 接口 标准化(可选蒸馏字段) 定制化(必须理解 combined advantage)
优点 灵活、可插拔、易调试 训练更稳定、无 loss 量级冲突
缺点 两个 loss 可能方向冲突、需调 α 环境-trainer 耦合更紧
适用场景 多 trainer 生态(Tinker/SLIME/Axolotl 都能接) 自研一体化系统

2.1 从代码看 OPD 实现

从代码层面的对比可以得出结论:Hermes 的 OPD 算法实现与 OpenClaw-RL 高度相似,核心逻辑(Judge Prompt、投票策略、Hint 插入方式、Teacher scoring 原理)完全相同。真正的差异在系统架构层面。

2.1.1 逐项对比

以下逐项对比。

  1. Hint 选择策略 ------ 两者一致,都是选最长的

OpenClaw-RL 的 _select_best_hint() 使用"选最长 hint"策略(openclaw_opd_api_server.py:112-119)。Hermes 推断使用相同策略。

  1. Judge Prompt ------ 完全相同

两者的 _HINT_JUDGE_SYSTEM prompt 几乎逐字相同,包括:

  • \boxed{1} / \boxed{-1} 决策格式
  • HINT_START / HINT_END 标记
  • role='tool' 的特殊说明
  1. Hint 插入方式 ------ 完全相同

两者的 _append_hint_to_messages() 逻辑相同:深拷贝 → 找最后一个 user 消息 → 追加 \n\n[user's hint / instruction]\n{hint}

  1. Teacher scoring ------ 架构差异(本质相同)
OpenClaw-RL Hermes
后端 SGLang /generate VLLM /generate (via atroposlib)
调用方式 _compute_teacher_log_probs() 直接 HTTP server.get_logprobs() 通过 ManagedServer
Top-K 可选 (--distill-topk 50) 默认 50
返回格式 input_token_logprobs + input_top_logprobs prompt_topk_token_ids + prompt_topk_logprobs

本质相同:都是将 (enhanced_prompt + response) tokenize → 发给推理引擎 → 获取 response 部分每个位置的 logprobs。

  1. 关于 "overlap-guided" ------ 论文理论 vs 实际代码

论文摘要/方法论中提到 overlap-guided hint selection,但官方代码中并没有实现 ------ 它用的是和 Hermes 一样的"选最长 hint"启发式。这可能是:

  • 论文中描述的是一个更理想的方案,代码中做了工程简化
  • 或者 overlap-guided 在某个未公开的实验分支中
  1. Log-prob clip ------ 在 trainer (SLIME) 端实现

OpenClaw-RL 的 env 端(openclaw_opd_api_server.py)也没有做 clip,clip 逻辑在 SLIME trainer 的 PPO loss 计算中。Hermes 将 distill fields 传给 Atropos trainer 处理,推断同理。

2.1.2 结论

完全一致的部分
环节 说明
Judge Prompt 逐字相同---\boxed{1/-1} + HINT_START/END,含 role='tool' 说明
投票策略 并发 m 次查询(默认3),temperature 0.6-0.7
Hint 选择 从 score=1 且长度>10 的投票中选最长的
Hint 插入 深拷贝消息 -> 最后一个 user 消息末尾追加 user's hint / instruction
Teacher scoring tokenize(enhanced_prompt + response) -> 推理引擎获取 response 部分的 prompt logprobs
Top-K 蒸馏 支持,默认 K=50
Advantage 公式 A_t = teacher_lp - student_lp
关键差异
维度 OpenClaw-RL Hermes
运行模式 在线 proxy(拦截实时对话) 离线 batch(先跑 rollout 再做 OPD)
OPD 触发 实时---下一条消息到达触发上一轮 批量---整个 rollout 结束后遍历
推理后端 SGLang /generate VLLM /generate (via atroposlib)
Reward 固定 1.0(有 hint 即正样本) 多信号加权(correctness+efficiency+tool_usage)
权重更新 连续异步(SLIME trainer 后台更新) 同步批次(Atropos trainer)
Session 管理 有状态(pending dict + session_done 清理) 无状态(每次重新遍历 messages)
数据来源 真实用户交互 合成编程任务rollout
PRM Eval 独立的 evaluative PRM judge(可选) 集成在 compute_reward 中的程序化验证
Turn 分类 main/side标记(side跳过) 无---所有 assistant turn 都处理
Logprob clip 交给 SLIME trainer 交给 Atropos trainer

2.2 OpenClaw-RL 异步Proxy 架构 vs Hermes Batch 架构

2.2.1 OpenClaw-RL 架构:在线异步Proxy

python 复制代码
User/Environment <--> OpenClaw Proxy (FastAPI) <--> SGLang Policy Server
                              |
                    (intercepts live chat)
                              |
                              v
              PRM/Judge Server       SLIME Trainer
              (hint + teacher lp)    (PPO updates)

核心设计:proxy模式,拦截实时对话

  • 用户正常使用模型(通过OpenAI-compatibleAPI/v1/chat/completions)
  • Proxy透明转发请求到SGLang,同时获取responselogprobs
  • Turn分类:turn_type="main"(可训练)vs"side"(跳过)
  • 延迟触发OPD:当下一条消息到达时,才对上一条回复做OPD(因为此时才有next_state)
  • 即时提交Sample→trainer在后台持续更新权重
  • submission_enabled event控制权重更新时暂停提交

2.2.2 Hermes架构:离线Batch环境

python 复制代码
Atropos Trainer --> HermesAgentBaseEnv.collect_trajectories()
                              |
                              v
                    group_size 个并行 rollout
                              |
                              | (完整 rollout 结束后)
                              v
              AgenticOPDEnv._apply_opd_pipeline()
                              |
                              v
                        per-serquence | OPD 处理
                              |
                              v
                        ScoredDataGroup + distill fields → Trainer      

核心设计:先跑完rollout,再做OPD

  • Rollout全部完成后,得到完整messages+tokens+reward

  • 一次性遍历整个对话,提取所有turn pairs

  • 批量做OPD:每个turnpair 的 hint 提取 + teacher scoring

  • 打包成ScoredDataGroup →返回给Atropos trainer

2.2.3 设计差异对比

维度 OpenClaw-RL (Proxy) Hermes (Batch)
训练模式 在线持续学习 离线 batch 训练
数据来源 真实用户交互 合成 rollout
OPD 触发时机 实时---下一条消息到达时 延迟---整个 rollout 结束后
延迟 秒级(对话间隔) 分钟级(rollout 周期)
并发模型 每个 session 独立的 asyncio Task group_size 并行 + 顺序 OPD
状态管理 session_id 为 key 的 pending dict 无状态(每次重新遍历 messages)
权重更新 连续(trainer 后台更新) 同步(batch 提交后 trainer 更新)
策略新鲜度 高---用最新权重生成 中---一批 rollout 共享同一权重
适用场景 有真实用户流量的部署 研究/实验/离线 fine-tuning
reward 固定 1.0(hint 存在即为正样本) 程序化验证(0.0~1.0 连续值)
session 管理 支持 session_done 信号清理状态 不需要---每个 rollout 自动隔离

2.2.4 三个最重要的架构差异

在线 vs 离线-本质区别
  • OpenClaw-RL 的核心价值是**"边用边练"**:用户正常使用 AI assistant,每一次对话都在后台产生训练信号。这是论文标题 "Train Any Agent Simply by Talking" 的精髓。

  • Hermes 是传统 RL 研究范式:先收集大量合成 rollout,再一批训练。优点是可控性强、可复现,缺点是 policy 更新不够及时。

数据流时序

OpenClaw-RL的精妙之处一延迟一步:

python 复制代码
turn 1:user msg → proxy → policy →response1 (save pending)
turn 2:user msg → proxy 触发 opD(response1,user_msg2)→ policy → response2 (save pending)
turn 3:user msg →proxy 触发 OPD(response2,user_msg3)→...
session_done→丢弃最后一个pending(无next_state) 

Hermes----次性回顾如下,Hermes的方式更简单(不需要维护session 状态),但丧失了在线学习的能力。

python 复制代码
rollout 完成 → messages =[sys, user, asst1, tool1,asst2,tool2,asst3]
遍历 → pair1=(asst1,tool1),pair2=(asst2,tool2) 
并行 OPD → distill fields
Reward信号来源

OpenClaw-RL:

  • 每个有效hint的样本reward固定为1.0(sample.reward={"score": 1.0})
  • 论文依赖OPD的 token级信号,不需要精细的 trajectory-level reward
  • 可选的PRM eval mode提供额外标量评分

Hermes:

  • 有独立的多信号reward函数(correctness + efficiency + tool_usage)
  • OPD distill fields 和reward 共存在ScoredDataGroup 中
  • Trainer同时使用两者

两者信号不同,是因为 Hermes 有真实沙箱可以执行测试 →能产出有意义的连续 reward→没理由浪费它。OPD 的 KL loss 告诉模型"每个token怎么选",reward 告诉模型"整体策略对不对"一两者互补。OpenClaw用 reward=1.0是因为它的 OPD 纯模式不需要RL信号一它只想蒸馏 teacher,不评判好坏。

2.3 logprobs & loss & advantage

2.3.1 logprobs

Student logprobs 的两种获取时机
python 复制代码
时机 A: Rollout 时截获 (OpenClaw 的做法)
模型生成 token → 同时记录每个 token 的 logprob → 存入 sample.rollout_log_probs
用途: combine_loss 中计算 teacher_adv = teacher_lp - student_lp(rollout时的)

时机 B: Training forward pass 时重算 (Hermes/Tinker 的做法)
Trainer 拿到 tokens → 做一次 forward pass → 得到当前模型对每个 token 的 logprob
用途: KL loss 中计算 KL(student_current || teacher)

Hermes 不存 rollout-time student logprobs, 但 Trainer 会重算(推断,因为没有tinker代码)

python 复制代码
# topk_distillation_loss.py 的逻辑 (Tinker 大概率类似)
# 1. Trainer 做 forward pass → 得到 student 的 logits
student_logits = model(tokens)  # [T, vocab_size]

# 2. 用 distill_token_ids 从 student logits 中取对应位置的 logprob
#    只看 teacher 认为重要的那 K=50 个 token
student_topk = student_logits.gather(dim=-1, indices=distill_token_ids)  # [T, K]

# 3. 构建 teacher 的 K+1 分布 (来自 distill_logprobs)
teacher_topk = distill_logprobs  # [T, K]
teacher_tail = log(1 - sum(exp(teacher_topk)))  # tail bin

# 4. 构建 student 的 K+1 分布
student_tail = log(1 - sum(exp(student_topk)))

# 5. KL(student || teacher) over K+1 bins
loss = F.kl_div(teacher_dist, student_dist, log_target=True)
关键区别
OpenClaw combine_loss Hermes/Tinker Top-K KL
需要什么 rollout 时的 student logprob (旧的) training 时的 student logprob (当前的)
什么时候算 rollout 时存好 training forward pass 时重算
数据里需要存吗 ✔ 必须存 rollout_log_probs ✘ 不需要 (训练时现算)
用途 teacher_adv = teacher_lp - old_student_lp KL(current_student
为什么两种方式有不同的语义?
python 复制代码
OpenClaw combine_loss:
"teacher 在位置 t 觉得该生成 token X 的信心 - 当初 student 生成 X 的信心"
→ 正值 = teacher 比 student 当初更确信 → 应该增大 X 的概率
→ 这是一个 advantage (方向信号)

Hermes/Tinker Top-K KL:
"当前 student 的分布 和 teacher 的分布 有多远"
→ 直接最小化分布距离
→ 这是一个 loss (距离度量)
小结

Hermes 的 OPD 不需要存储 rollout-time 的 student logprobs, 因为它用的是 "让当前模型逼近 teacher 分布" (KL loss), student logprobs 在训练时通过 forward pass 实时计算。OpenClaw combine 需要存储是因为它用的是 "teacher 比旧student强多少"(advantage信号),需要知道rollout时的旧值。

2.3.2 loss

openclaw-tinker

openclaw-tinker/ 完整暴露了 Tinker 上训练 OPD 的方式。它和 OpenClaw-RL 的 SLIME 版本用的是同一种融合式设计!

Tinker 上 OPD 的 Loss (从 data_formatter.py 确认)

python 复制代码
# data_formatter.py L106-123: RL / OPD 模式
def sample_to_datum(sample, advantage, kl_penalty_coef=0.0):
    # 基础 advantage = GRPO 标量 reward
    resp_advantages = [advantage * mask for mask in sample.loss_mask]

    # OPD: 叠加 reverse-KL penalty
    if sample.teacher_logprobs is not None and kl_penalty_coef > 0:
        for i in range(len(resp_advantages)):
            student_lp = sample.response_logprobs[i]  # rollout 时的 student logprob
            teacher_lp = sample.teacher_logprobs[i]  # 有 hint 的 teacher logprob
            kl_i = student_lp - teacher_lp            # per-token KL
            resp_advantages[i] += -kl_penalty_coef * kl_i  # 加到 advantage 上

# data_formatter.py L146-178: Combined 模式
def sample_to_datum_combined(sample, w_opd=1.0, w_rl=1.0, kl_penalty_coef=0.0):
    for i in range(len(sample.response_tokens)):
        rl_adv = w_rl * sample.reward * mask
        opd_adv = w_opd * (-kl_penalty_coef * (student_lp - teacher_lp)) * mask
        resp_advantages[i] = rl_adv + opd_adv  # 融合!

关键发现: Tinker 版和 SLIME 版本本质相同,两者都是 advantage 融合式:

combined_advantage = w_rl × reward + w_opd × (-kl_coef × (student_lp - teacher_lp))

而不是分离式:L = L_GRPO + α × L_OPD

Hermes 的 OPD

现在问题是: Hermes 环境产出的数据能走 openclaw-tinker 的路径吗?

看 TrainingSample 的字段:

python 复制代码
@dataclass
class TrainingSample:
    prompt_tokens: list[int]      # ← Hermes 有 (ScoredDataItem.tokens 的 prompt 部分)
    response_tokens: list[int]   # ← Hermes 有 (ScoredDataItem.tokens 的 response 部分)
    response_logprobs: list[float] # ← Hermes ✘ 没有! (不存 student rollout logprobs)
    loss_mask: list[int]         # ← Hermes 有 (ScoredDataItem.masks)
    reward: float                # ← Hermes 有 (ScoredDataItem.scores)
    teacher_logprobs: list[float] # ← Hermes ⚠️ 有 Top-K 但不是单值

矛盾!

字段 Hermes 产出 openclaw-tinker 需要 兼容?
tokens
masks
reward ✔ 连续 0--1 ✔ 标量
response_logprobs ✘ 不存 ✔ 需要 ✘ 不兼容
teacher_logprobs Top-K 分布 T50 单值 T ✘ 格式不同
最终结论
python 复制代码
openclaw-tinker (reference/ 中的代码):
  - 使用 advantage 融合式 (和 SLIME combine_loss 相同)
  - 需要: student rollout logprobs + teacher 单值 logprobs
  - OPD advantage = -kl_coef × (student_lp - teacher_lp)

Hermes 环境产出:
  - 不存 student rollout logprobs
  - teacher 是 Top-K 分布 [T][K=50], 不是单值

→ Hermes 的数据格式和 openclaw-tinker 不兼容!
→ Hermes 配套的真正 Tinker (nousresearch/tinker-atropos)
  大概率用的是 Top-K KL 方式 (直接消费 distill_token_ids/distill_logprobs)
  而不是 openclaw-tinker 的 advantage 融合方式
一句话

OpenClaw-RL 项目中的 openclaw-tinker/ 确实用了 advantage 融合式 (和 SLIME combine_loss 相同), 但这条路需要 student rollout logprobs 和 teacher 单值 logprobs---而 Hermes 环境不提供这些。所以 NousResearch 的 nousresearch/tinker-atropos (私有仓库) 大概率有一条不同的 Top-K KL loss 路径来消费 Hermes' 的 distill_token_ids[T][50] + distill_logprobs[T][50] 格式的数据。

换句话说:

  • openclaw-tinker = OpenClaw 团队为 Tinker 云平台适配的训练代码 (用他们自己的数据格式)
  • nousresearch/tinker-atropos = NousResearch 为 Hermes 环境适配的训练代码 (消费 Hermes 的 Top-K 分布格式)

两者都跑在 Tinker 平台上, 但数据格式和 loss 实现不同。

2.3.3 Hermes Reward vs OpenClaw-RL Reward

Hermes Reward

Hermes 的 Reward: 三信号连续打分 (0.0--1.0)

python 复制代码
# agentic_opd_env.py compute_reward()
reward = correctness_weight × correctness       # 默认 0.7
       + efficiency_weight × efficiency         # 默认 0.15
       + tool_usage_weight × tool_usage         # 默认 0.15
# clamp 到 [0.0, 1.0]

信号如下:

信号 量化方式 值域
correctness 实际执行测试: exit_code=0 + "passed" → 1.0; exit=0 → 0.8; assertion fail → 0.2; crash → 0.1; 完全失败 → 0.0 0, 1
efficiency turns_used ≤ 3 → 1.0; ≤ half → 0.8; ≤ 3/4 → 0.5; else → 0.2
tool_usage terminal + write_file → 1.0; terminal only → 0.6; 有工具 → 0.3; 无 → 0.0

特点:

  • ✔ 真正的环境执行 (跑 python test_solution.py)
  • ✔ 区分部分正确 / 完全正确
  • ✔ 鼓励高效解题 (少用 turns)
  • ✔ 鼓励正确行为模式 (用 terminal + 写文件)
  • ✔ 连续值, 梯度信号更丰富
OpenClaw-RL 的 Reward: 两套独立系统

A) OPD 模式: 硬编码 reward = 1.0

复制代码
# openclaw_opd_api_server.py L884
sample.reward = {"score": 1.0}  # 所有 OPD 样本统一 reward

为什么 OPD 不需要 reward? 因为 OPD 的训练信号来自 teacher logprobs (KL loss), 不来自 reward。reward=1.0 只是个占位值, 让 GRPO advantage 不置零。

B) RL 模式 (PRM): LLM 二元判决 (+1 / -1)

复制代码
# scorers.py PRMScorer:
# 用 teacher LLM 做 3 次投票 → majority_vote → {+1, -1, 0}
# +1 = 助手成功完成了用户意图
# -1 = 助手失败
# 0 = 投票无效 (所有投票都失败)

特点:

  • ✘ 不执行代码 (纯 LLM 判断)
  • ✘ 二元 (无 "部分正确" 概念)
  • ✔ 通用性强 (不需要 test_solution.py)
  • ✔ 成本可控 (LLM 调用 vs 沙箱执行)

C) Combined 模式: PRM (+1/-1) + OPD (teacher logprobs) 同时作用

维度 Hermes OpenClaw-RL
reward 来源 实际执行 (真沙箱) LLM 判断 (PRM 投票)
值域 连续 0, 1 离散
信号密度 3 维组合 (正确性+效率+工具) 1 维 (成功/失败)
部分奖励 ✔ 有 (0.1, 0.2, 0.5...) ✘ 无
OPD 时 reward 连续值 (和 RL 相同) 固定 1.0
可扩展性 需要 test_solution.py 只需下一轮对话
成本 沙箱执行 (便宜但需 Docker) Teacher LLM 调用 (贵)
GRPO 梯度信号 丰富 (连续差异产生精细 advantage) 粗糙 (只有好/坏两极)
设计哲学差异

Hermes 理念:

  • "我们有完整的工具沙箱 → 直接跑测试 → 最精确的 reward"
  • "连续 reward + 多信号 = GRPO advantage 更有区分度"

OpenClaw 理念:

  • "通用性优先 → 不是所有任务都有 test case → LLM PRM 万能评估"
  • "OPD 的真正信号在 teacher logprobs, reward 只是 GRPO 的权重因子"
  • "二元 PRM 够用 → RL 本身就是 contrastive (好/坏对比)"

对GRPO的影响

  • GRPO advantage =(sample_reward - group_mean) / group_std
  • Hermes:连续reward→group内差异丰富→advantage分布平滑
  • OpenClawRL:二元reward→group 内只有+1/-1→advantage 是01开关

OpenClaw OPD:全是 1.0 → advantage 全 = 0 → GRPO 无效,只有 KL loss 训练!

这就解释了为什么 OpenClaw OPD 模式把 reward 设为 1.0 ------ 它故意让 GRPO 的 advantage 归零,只依赖 teacher KL 做蒸馏。

2.4 架构级差异

  • Hermes 选分离式是因为它的核心价值观是"解耦"---环境和 Trainer 通过标准数据格式通信,任何一方都可以独立替换。
  • OpenClaw 选融合式是因为它是自研一体化系统(SLIME + 环境紧耦合),追求训练效果最优。
维度 Hermes (agentic_opd_env.py) OpenClaw-RL (openclaw_opd_api_server.py)
运行模式 离线批处理:先跑完整个 rollout,再回溯做 OPD 在线代理:实时拦截每个 turn,流式做 OPD
集成方式 Atropos BaseEnv 子类,collect_trajectories() 覆写 FastAPI 中间代理,拦截 /v1/chat/completions
Turn pair 提取 Rollout 完成后遍历 messages 数组 实时 session 状态追踪,turn 计数器
并发模型 asyncio 协程 (Atropos event loop 内) asyncio + httpx + FastAPI
Token 来源 VLLM ManagedServer.get_logprobs() SGLang /generate endpoint

2.5 一句话总结

Hermes 的OPD是OpenClaw-RL 官方代码的直接移植,核心算法(hint提取 → teacher scoring →token advantage)完全一致。区别仅在系统架构层:OpenClaw-RL是面向真实部署的在线持续学习系统,Hermes 是面向研究的离线 batch 训练环境。

0x03 训练端分歧 --- "传单值 vs 传分布"

有一个非常大的分歧:OpenClaw-RL 是传单值,Hermes 是传分布。

从 Hermes 数据到 Loss 的完整链路如下:

3.1 通俗类比

这是传分布 VS 传单值的通俗解释。

场景类比:想象你是一个厨师学徒(学生模型),正在跟大厨(老师模型)学做菜。

3.1.1 传单值(OpenClaw-RL的做法)

大厨品尝完你做的菜后,只告诉你:"这道菜我打7.2分"

你知道大厨觉得还行,但不知道他觉得:一盐放多了还是少了?一火候偏大还是偏小?-该加醋还是加糖?

对应OPD:对于学生 生成的每个token,老师只给一个数字:

python 复制代码
 token "print"→teacher_logprob=-0.3(老师觉得不错) 
 token "echo"→teacher_logprob=-2.1(老师觉得不太好)

Trainer 能算出advantage =teacher_logprob-student_logprob,知道"这个token 老师喜不喜欢",但不知道老师更喜欢什么替代品。

3.1.2 传分布(Hermes/Atropos的做法)

大厨不仅打了分,还告诉你他心目中的Top-50选择排名:"我觉得最好的是加白胡椒(概率35%),其次是黑胡椒(概率25%),再次是花椒(概率15%)你用的辣椒粉排第12位(概率2%)

现在你不仅知道自己的选择不太好,还知道:

  • 老师最偏好什么
  • 各个替代方案的相对优劣
  • 你的选择离最优有多远

对应OPD:则是对于每个token位置,老师给出Top-K=50的完整分布:

python 复制代码
位置[5]:
    token_id 1234 ("print") → logprob -0.3 (老师首选) 
    token_id 5678 ("println") → logprob -1.2
    token_id 9012 ("write") → logprob -1.8
    ......
共50个候选

3.1.3 训练时的区别

单值 分布
能算什么loss 只能算A=teacher_lp-student_lp(一个标量差值) 可以算KL散度(两个分布之间的距离)
训练信号丰富度 只知道"老师喜不喜欢这个 token" 知道"老师喜欢什么、不喜欢什么、你离最优差多远"
类比 考试只告诉你对错 考试告诉你标准答案+每个选项的得分理由

具体来说如下:

单值训练(简单advantage)
python 复制代码
loss ≈ -advantage x log P_student(token) = -(teacher_lp -student_lp) x log P_student(token)

方向:如果老师比学生更喜欢这个 token →鼓励;反之→抑制。

分布训练(KL散度/reverse KL):
python 复制代码
loss  KL(P_teacher_top50 || P_student)
= ∑ P_teacher(t) x [log P_teacher(t) -log P_student(t)] 

方向:让学生的概率分布整体靠近老师的分布,不只是在当前 token 上。

一句话总结

传单值=告诉你"这个答案好不好"(打分)

传分布=告诉你"所有答案中哪些好哪些差"(给答案册)

Hermes选择传分布,是因为它给了trainer更多自由度一trainer 可以选择只用单值(退化为OpenClaw-RL 的做法),也可以用完整分布做更精细的蒸馏。

3.2 Reward vs Advantage 概念澄清

3.2.1 术语澄清

  1. Reward 函数 (compute_reward) - env 端计算,返回一个标量分数
  2. Advantage 函数 - trainer 端计算,决定每个 token 的梯度方向和大小
  3. 这两个是不同层级的东西

3.2.2 OPD区别

OpenClaw-RL的做法

在OpenClaw-RL中:

  • env 端(openclaw_opd_api_server.py)计算teacher_log_probs(每个token一个值)和

    rollout_log_probs(student,每个token一个值)

  • 两者都放入 Sample对象提交给SLIME trainer

  • SLIME trainer 内部做 A_t=teacher_lpt-rollout_lpt

  • 然后用PPO clipped loss:L=L_pg(A)+β_KL*L_KL

Hermes 如何处理OPD advantage

Hermes 不计算也不传递 student logprobs 给 trainer。它传递的是:

复制代码
group ["distill_token_ids"]       # [seq_len][top_k=50] - teacher 的 top-50 token IDs
group ["distill_logprobs"]        # [seq_len][top_k=50] - teacher 的 top-50 logprobs
关键区别

注意关键区别:

  • OpenClaw-RL 传的是 单值 teacher_log_probs T (每位置一个 logprob)
  • Hermes 传的是 top-K 分布 [T][50] (每位置 50 个候选 token + 对应 logprobs)
  • 这意味着 tinker-atropos trainer 端的 advantage 计算方式不同于 SLIME

3.2.3 训练

Trainer 拿到 distill_token_ids [t][k]distill_logprobs [t][k] 后,有两种用法:

python 复制代码
方法 A (Token-level OPD, 等价于 OpenClaw-RL):
    对位置 t, student 生成了 token_t
    在 distill_token_ids [t] 中找到 token_t 的位置
    k_idx = distill_token_ids [t].index (token_t)
    teacher_lp = distill_logprobs [t][k_idx] # teacher 给 token_t 的 logprob
    student_lp = model.forward (tokens [:t+1]).logprob [t] # student 当前策略的 logprob
    A_t = teacher_lp - student_lp

方法 B(Top-K reverse KL,SDFT/SDPO 风格):
    #不针对单 token,而是用完整 top-k分布做 KL散度
    L_distill = KL(student_topk Il teacher_topk) # reverse KL over K+1 bins 

3.2.4 总结

Hermes的设计比 OpenClaw-RL 更通用:

  • OpenClaw-RL 硬编码了A =teacher_lp -student_lp(单值)
  • Hermes 传递了完整 top-K teacher 分布,让 trainer 可以选择用简单差值 / top-K KL 散度 / 其他方法
OpenClaw-RL (SLIME) Hermes (Tinker-Atropos)
Env 提供 teacher_lpT + rollout_lpT distill_token_ids[T][K] + distill_logprobs[T][K]
Student logprob env 端获取 (response 时带回) trainer 端自己 forward 一遍获取
Advantage 公式 A_t = teacher_lpt - rollout_lpt trainer 需从 top-K 中查找 student token, 取对应 teacher logprob, 减去 student logprob
等价性 简单差值 更灵活:可以用 top-K reverse KL (如 SDFT/SDP0), 也可以退化为简单差值

3.3 KL散度蒸馏 loss 的实际实现

3.3.1 Reverse KL

为什么选 Reverse KL(学生→老师)而不是 Forward KL(老师→学生)?因为Reverse KL 更适合 agent 训练:我们希望学生专注学好老师最强的行为模式,而不是试图模仿所有可能性(那会分散注意力)。

3.3.2 核心算法:"Top-K + Tail Trick"

问题:老师给了 Top-50 token 的概率,但词表有 ~15 万个 token。剩下的 14.995 万个 token 怎么办?

解法:Tail Trick(尾部合并技巧),把词表从 15 万个 token 压缩为 K+1 = 51 个 bin。

python 复制代码
原始词表(~150K tokens):
  token_1: P=0.35, token_2: P=0.25, ..., token_50: P=0.001
  token_51: P=0.0003, token_52: P=0.0002, ..., token_150000: P=0.000001

压缩后(K+1 = 51 bins):
  bin_1: P=0.35 (top-1 token)
  bin_2: P=0.25 (top-2 token)
  ...
  bin_50: P=0.001 (top-50 token)
  bin_51: P=? (tail bin = 所有其他 token 的总概率)

Tail 的计算方法:
    # 1. Top-K 的概率总和(在 log 空间)
    log_sum_topk = logsumexp(topk_logprobs)    # log(P1 + P2 + ... + P_K)
    # 2. 剩余概率 = 1 - sum_topk(在 log 空间)
    tail_logprob = log(1 - exp(log_sum_topk))  # = log(1 - 0.95) = log(0.05)
    # 3. 拼出 K+1 维的简化分布
    distribution = [topk_logprobs..., tail_logprob]  # shape: [K+1]

图示如下:

3.3.3 实际代码解读

python 复制代码
# ===== 第一步:收集学生在老师 Top-K 位置的 logprobs =====
for i, (logits_chunk, tokens_chunk) in enumerate(get_responses(...)):
    t_logps = teacher_topk_logprobs[i]   # 老师的 Top-K logprobs [R, K]
    t_indices = teacher_topk_indices[i]  # 老师的 Top-K token IDs [R, K]

    # 对每个 k,计算学生在"老师选的第 k 个 token"上的 logprob
    for k in range(K):
        lp_k = compute_log_probs(logits_chunk, t_indices[:, k])
        student_logps_k.append(lp_k)

# ===== 第二步:Tail Trick =====
# 学生的 tail
student_log_s = logsumexp(student_topk)      # 学生 Top-K 总概率
student_tail = log(1 - exp(student_log_s))   # 学生剩余概率

# 老师的 tail
teacher_log_s = logsumexp(teacher_topk)      # 老师 Top-K 总概率
teacher_tail = log(1 - exp(teacher_log_s))   # 老师剩余概率

# 拼出 K+1 维分布
student_with_tail = cat([student_topk, student_tail])  # [T, K+1]
teacher_with_tail = cat([teacher_topk, teacher_tail])  # [T, K+1]

# ===== 第三步:计算 Reverse KL =====
per_token_kl = F.kl_div(
    input=teacher_with_tail,      # log Q (老师)
    target=student_with_tail,     # log P (学生)
    log_target=True,
).sum(dim=-1)
# 注意:F.kl_div(log Q, log P) = Σ P * (log P - log Q) = KL(P||Q) = KL(student||teacher)

loss = sum_of_sample_mean(per_token_kl)  # 按 sample 平均

0x04 设计哲学 --- "为什么选这条路"

Hermes-agent在整个RL训练流水线中扮演的是**环境(Environment)**角色一它生成任务、运行agent 对话(rollout)、计算奖励、准备蒸馏数据,然后把打包好的数据交给外部训练器。Hermes不包含任何训练代码。没有loss计算、没有梯度更新、没有权重修改。训练由独立的trainer 组件完成(tinker-atropos 或SLIME)

4.1 选择原因

4.1.1 原因 1:解耦架构的必然结果

OpenClaw的融合式需要:

python 复制代码
    teacher_advantages = teacher_log_probs - student_log_probs
    #                                          ↑ 需要 student 在 rollout 时的精确 logprob

Hermes的设计哲学:环境HTTP API →Trainer(完全解耦)

python 复制代码
环境端产出的 ScoredDataItem:
{
    "tokens":[...],
    "masks": [...],
    "scores":0.85,                       ← reward标量
    "distill_token_ids": [[...]...],     ← OPD Top-K token IDs
    "distill_logprobs": [[...]...],      ← OPD Top-K logprobs
}
但 Hermes 的 ScoredDataItem 里只有 teacher 的 logprobs,没有 student 的。

为什么?因为 Hermes 的 OPD 是在 rollout 之后做的,不是在线,所以可以解耦:

python 复制代码
Hermes OPD 流程:
    1. Student rollout → 得到 trajectory (tokens + masks + reward)
    2. 事后:提取 turn pairs → LLM Judge → 生成 hint
    3. 事后:VLLM teacher scoring (同模型+hint) → teacher logprobs
    4. 打包:tokens + masks + reward + teacher_logprobs

OpenClaw 的流程不同,因为 OpenClaw 用 SGLang 同步 rollout + scoring,数据全在同一个进程里:

python 复制代码
OpenClaw OPD 流程:
    1. Student rollout → 同时记录 student_logprobs
    2. Teacher scoring → teacher_logprobs
    3. teacher_advantage = teacher_logprobs - student_logprobs ← 都需要有

4.1.2 原因 2:Trainer 可替换性

Hermes 的架构目标:任何 trainer 都能接入。

python 复制代码
如果 OPD 信号已经融入 advantage:
    → Trainer 必须理解 "这个 advantage 包含蒸馏成分"
    → 破坏了标准 GRPO/PPO 的接口契约
    → 换一个 trainer 就要重新实现融合逻辑

如果 OPD 是独立的 loss 项:
    → Trainer 收到 (tokens, masks, reward) 做标准 GRPO
    → 额外收到 (distill_token_ids, distill_logprobs) 做蒸馏 loss
    → 两者独立,trainer 可以选择性实现
    → 甚至可以只用 GRPO 不用 OPD(忽略 distill 字段即可)

4.1.3 原因3:数据格式的约束

python 复制代码
Hermes 传给 Atropos API 的是 ScoredDataGroup: 
{
	items: [ScoredDataItem,...]
}
Atropos API是通用的数据仓库,要服务多种环境:

- 有的环境只有reward(纯GRPO)
- 有的环境有distill数据(GRPO+OPD)
- 有的环境可能未来有别的辅助信号

通用格式→辅助数据只能作为可选附加字段 → Trainer 看到有 distill 字段就加一个loss项 → 如果看不到,就可以采用纯GRPO

4.1.4 原因4:工程简洁性

  • 分离式:
    • 环境:只管产数据(各字段独立)
    • Trainer:各算各的 loss 再加起来
    • 调试:可以分别关闭 GRPO 或 OPD,看各自效果
  • 融合式(OpenClaw):
    • 环境:要同时给出 teacher + student logprobs
    • Trainer:先融合 advantage,再做 PPO
    • 调试:combined_advantage 里两个信号耦合,不好拆解

OpenClaw 的好处:一个目标函数 → 不存在 loss 量级冲突

4.2 RL 训练全流水线

4.3 OPD 蒸馏管线

OPD (On-Policy Distillation) 在标准 rollout 之后额外执行:

python 复制代码
Step 1: 提取 turn pairs
    从 rollout 历史中找 (assistant回复,下一状态) 对
    下一状态 = 紧跟的 tool 返回结果
    [assistant: "让我运行测试"] -> [tool: "3 passed, 2 failed"]
          ↑ response                        ↑ next_state

Step 2: LLM Judge 提取 hint
    把 (response, next_state) 发给 judge 模型 (3 次投票)
    judge prompt: "你是一个 AI 助手训练师...给出一句话的 hint"
    投票策略:选最长的 hint (不是真正的多数投票)

Step 3: 构建增强 prompt
    原始用户消息 + 插入的 hint:
    "请修复这个 bug。[user's hint / instruction]
    在修复时注意检查边界条件。"

Step 4: Teacher scoring
    把增强 prompt + 学生的原始回复 -> 拼接 -> tokenize
    发送给 VLLM 的 prompt_logprobs 接口
    得到:老师在每个 token 位置上的 Top-K=50 分布

Step 5: 打包 distill fields
    distill_token_ids [序列数][序列长度][K=50] - 每位置老师 Top-50 token ID
    distill_logprobs  [序列数][序列长度][K=50] - 每位置老师 Top-50 logprob
    附加到 ScoredDataGroup -> 发送给 Atropos API -> Trainer 消费

OPD 的直觉

  • 没有 hint -> 老师和学生看一样的 prompt -> teacher logprobs 提供有限的额外信息
  • 有 hint -> 老师看到了"未来会发生什么"的线索 -> 老师的判断更好 -> 蒸馏信号更有价值

类比:开卷考试(老师)vs 闭卷考试(学生),让学生学会不用打开书也能做出和老师一样好的判断。

4.4 数据交付

环境最终产出的是 ScoredDataGroup(atroposlib 定义的 TypedDict):

python 复制代码
ScoredDataGroup = {
    "tokens":            [[int]],      # 每条轨迹的 token ID 序列
    "masks":             [[int]],      # 1=assistant生成, 0=非assistant
    "scores":            [float],      # 每条轨迹的 reward
    "distill_token_ids": [[[int]]],    # OPD: 老师 Top-K token IDs (可选)
    "distill_logprobs":  [[[float]]],  # OPD: 老师 Top-K logprobs (可选)
}

数据通过 HTTP POST 发送到 Atropos API Server,训练器从 API Server 拉取 batch 进行训练。

环境 --POST /scored_data--> Atropos API -----GET /batch---> Trainer
                           (缓冲+组 batch)                (loss+梯度+权重更新)
                                                             |
                                                             v
                                                           VLLM (热更新权重)
                                                             |
                                                             v
                                                          下一轮rollout

0x05 Top-K 解读

5.1 为什么需要 Top-K 蒸馏?单值 OPD 的局限

标准 OPD(token-level)的工作方式是:对每个 token 位置,teacher 给一个 scalar logprob,trainer 计算 A_t = teacher_lp - student_lp,然后走 PPO。

这个方案有一个根本的信息瓶颈:teacher 对每个位置只传了一个数字 。teacher 知道"第 5 个 token 应该是 print",但它也知道"如果不用 printwrite 也比 echo 好得多"------这个排序信息在单值传输中完全丢失了。

问题 单 token OPD Top-K OPD
信息量 每位置 1 个 scalar 每位置 K+1 维分布
teacher 的第 2,3...K 选择 丢失 保留偏好排序
尾部 token 的处理 完全不知道 tail bin 保底
存储 [T] [T, K](K=50 时 ×50)
学生遗忘风险 只看 teacher 最强 token,可能过拟合 K+1 维 KL 更温和

Top-K 蒸馏本质上是从"teacher 告诉你哪个 token 对"升级为"teacher 给你一份参考答案册"------不仅告诉你标准答案,还告诉你每个备选答案能得多少分。这个想法来自 SDFT (arXiv:2601.19897) 和 SDPO (arXiv:2601.20802) 两篇工作,它们的共同思路是:用 teacher 的 top-K logits + tail bin 做 KL 散度蒸馏,而不是传统的全词表 KL 或单 token CE。


5.2 Top-K 蒸馏在 OpenClaw 中的位置

Top-K 蒸馏不是替代标准 OPD,而是作为 OPD 的可选增强。两者的关系如下:

python 复制代码
                         OPD
                          │
       ┌──────────────────┼──────────────────┐
       ▼                  ▼                  ▼
   标准 OPD            Top-K OPD           RL-only
   teacher_lp         teacher_topk_lp      无 teacher
                      teacher_topk_idx
                          │
                          ▼
               topk_distillation_loss.py
               (含 tail bin + reverse KL)

                         Combine
                          │
                       (继承 OPD)
                          │
                    可以用 Top-K 路径

两种模式通过 shell 脚本区分:

标准 OPD Top-K OPD
Shell 脚本 run_qwen3_4b_openclaw_opd.sh run_qwen3_4b_openclaw_opd_topk.sh
传输字段 teacher_log_probs [T] teacher_topk_log_probs [T,K] + teacher_topk_indices [T,K]
Advantage / Loss --advantage-estimator on_policy_distillation (Slime 内置 PPO) --custom-loss-function-path topk_distillation_loss (自定义 reverse KL)
数据量 ~300MB/sample(全 vocab logprobs) ~200KB/sample(K+1 维分布)

Binary RL 完全不涉及 Top-K------它只有 reward ±1 + GRPO advantage,没有任何蒸馏。


5.3 核心机制:Tail Trick + Reverse KL

5.3.1 问题:15 万词表 vs 50 个 top-K

teacher 只传了 Top-50 token 的 logprobs,但词表有 ~150K 个 token。剩下的 149,950 个 token 怎么办?

解法:Tail Trick。把词表从 150K 维压缩为 K+1 = 51 维------前 50 个 bin 各对应一个 teacher 选的 token,第 51 个 bin("tail")是所有剩余 token 的概率之和。

复制代码
原始词表 (~150K tokens):
  token_1: P=0.35, token_2: P=0.25, ..., token_50: P=0.001
  token_51: P=0.0003, ..., token_150000: P=0.000001
       ↓ Tail Trick
压缩为 K+1 = 51 bins:
  bin_1: P=0.35, bin_2: P=0.25, ..., bin_50: P=0.001
  bin_51 (tail): P = 1 - Σ(top-50)  ← 所有剩余 token 的总概率

5.3.2 Tail 的数值计算

实际计算在 log 空间进行,代码位于 openclaw-opd/topk_distillation_loss.py:83-89

python 复制代码
# 1. Top-K 概率之和(log 空间)
log_sum_topk = logsumexp(topk_logprobs)          # log(P1 + P2 + ... + P_K)

# 2. 数值稳定:clamp 防止 log_sum_topk 接近 0 导致下溢
log_sum_topk = clamp(log_sum_topk, max=-1e-7)

# 3. 用 expm1 计算 tail,避免 log(1-x) 的灾难性抵消
tail_logprob = log(-expm1(log_sum_topk))         # = log(1 - exp(log_sum_topk))

# 4. 拼出 K+1 维分布
distribution = [topk_logprobs..., tail_logprob]  # shape: [K+1]

关键细节:clamp(max=-1e-7) 确保 log_sum_topk < 0,防止 exp(log_sum_topk)=1 导致 log(0) = -infexpm1 是比 1-exp(x) 更数值稳定的实现。

5.3.3 Reverse KL 散度

有了 student 和 teacher 各自的 K+1 维分布后,计算 reverse KL:

python 复制代码
# openclaw-opd/topk_distillation_loss.py:94-99
per_token_kl = F.kl_div(
    input=teacher_with_tail,      # teacher 的 log-prob
    target=student_with_tail,     # student 的 log-prob
    log_target=True,
    reduction="none",             # 不聚合,保留逐 token 维度
).sum(dim=-1)                    # 对 K+1 个 bin 求和

loss = sum_of_sample_mean(per_token_kl)

PyTorch 约定:F.kl_div(input, target, log_target=True) 计算的是 D_KL(target || input)。代码中 input=teacher, target=student,因此计算的是 D_KL(student || teacher)------反向 KL。

选反向 KL 的原因是它的 mode-seeking 特性:

Forward KL: KL(teacher||student) Reverse KL: KL(student||teacher)
公式 Σ P_teacher × log(P_teacher/P_student) Σ P_student × log(P_student/P_teacher)
行为 均值寻求(mean-seeking) 众数寻求(mode-seeking)
效果 student 试图覆盖 teacher 的所有模式 student 集中学 teacher 最强的模式
Agent 场景 分散注意力 精确模仿最优行为

在 agent 训练中,我们希望学生专注学好 teacher 最强的行为模式,而不是试图模仿所有可能性。反向 KL 恰好鼓励这种行为。

可选:Entropy 正则化

python 复制代码
if args.entropy_coef != 0.0:
    entropy = -(student_probs * student_logprobs).sum(dim=-1)
    loss = loss - entropy_coef * entropy  # 减去 → 鼓励探索

防止学生分布坍塌到单点(太确定),保持探索能力。完整 loss:

复制代码
L = KL(P_student || P_teacher)_{K+1 bins}  -  λ × H(P_student)

5.4 完整数据流:从 API Server 到 Loss

Step 1 --- API Server 获取 teacher top-K (openclaw_opd_api_server.py):

python 复制代码
_compute_teacher_topk_logprobs(input_ids, response_len)
  ├─ payload: max_new_tokens=0, return_logprob=True, top_logprobs_num=K
  │           (只做 forward pass, 不生成新 token)
  ├─ 返回: teacher_topk_log_probs [T, K]  ← 每个位置 top-K 的 log P
  │       teacher_topk_indices  [T, K]  ← 对应的 token ID
  └─ 并发控制: self._teacher_lp_semaphore (max=3)

Step 2 --- 传入 Slime 训练管道 (slime/ray/rollout.py):

python 复制代码
train_data["teacher_topk_log_probs"] = [sample.teacher_topk_log_probs ...]
train_data["teacher_topk_indices"]  = [sample.teacher_topk_indices  ...]

Step 3 --- 损失计算 (openclaw-opd/topk_distillation_loss.py):

python 复制代码
# A. 收集 student 在 teacher 所选 K 个 token 上的 log-prob
for k in range(K):
    lp_k = compute_log_probs(logits, t_indices[:, k])
student_topk = stack(student_logps_k, dim=-1)        # [T, K]

# B. Tail Trick(teacher 同理)
student_log_s = logsumexp(student_topk, dim=-1)       # Top-K 概率之和
student_log_s = clamp(student_log_s, max=-1e-7)       # 数值稳定
student_tail  = log(-expm1(student_log_s))            # 1 - Σtop_K

# C. 拼成 K+1 维分布
student_with_tail = cat([student_topk, student_tail]) # [T, K+1]
teacher_with_tail = cat([teacher_topk, teacher_tail]) # [T, K+1]

# D. Reverse KL
per_token_kl = F.kl_div(teacher, student, log_target=True,
                         reduction="none").sum(dim=-1)  # [T]

# E. 聚合
loss = sum_of_sample_mean(per_token_kl)

5.5 关键设计选择:Teacher Top-K vs Student Top-K

OpenClaw 做了一个有别于原版 SDFT/SDPO 的修改:使用 Teacher Top-K(查 teacher 概率最高的 K 个 token,再看 student 在这些位置上的概率),而非 Student Top-K(查 student 的 top-K,再看 teacher 在这些位置上的概率)。代码注释中也提到:"we use teacher top K instead of student top K, see issue #7."

python 复制代码
# Top-K 的原版 SDFT/SDPO: Student Top-K
Step 1: Student forward → 取 student 概率最高的 K 个 token
Step 2: 查 teacher 在这 K 个位置上的 logprob
Step 3: D_KL(student_K+1 || teacher_K+1)

# OpenClaw 修改: Teacher Top-K
Step 1: Teacher forward → 取 teacher 概率最高的 K 个 token(API 端完成)
Step 2: Student forward → 查 student 在这 K 个位置上的 logprob(训练端完成)
Step 3: D_KL(student_K+1 || teacher_K+1)

代码中的体现(topk_distillation_loss.py):

python 复制代码
t_indices = teacher_topk_indices[i]     # teacher 选的 K 个 token ID
for k in range(K):
    lp_k = compute_log_probs(logits, t_indices[:, k])
    # ↑ 查 student 在 teacher 位置上的 prob,而非用 student 自己的 argmax top-K

5.5.1 为什么选 Teacher Top-K?

原因一:架构约束(最根本)

Teacher Top-K 只需 1 次 HTTP 往返------SGLang 原生支持 top_logprobs_num=K 参数,teacher 一次返回 top-K logprobs + indices。Student Top-K 需要 2 次:先 student forward 取 top-K indices,再发给 teacher,teacher 返回对应位置的 logprob。

在 OpenClaw 的异步解耦架构中(teacher 在 GPU 6-7,trainer 在 GPU 0-3,中间只传 [T, K] 个数字),Teacher Top-K 的实现成本远低于 Student Top-K。

原因二:teacher >> student 场景更合理

OpenClaw 的实际场景是 teacher 强于 student(teacher 有 hint 辅助)。Student 的 top-K 可能指向"垃圾模式",teacher 在这些位置的分布不含有效信息。Teacher top-K 直接回答"应该学什么",而非"我哪里和 teacher 不同"。

原因三:Tail Bin 更有区分度

复制代码
Teacher Top-K 的 student_tail = 1 - Σ student_log_prob(teacher 选的 K 个 token)
    → student 在 teacher 重视的位置上概率可能很低
    → tail 很大,有信息量:"student 的大部分概率不在 teacher 重视的地方"

Student Top-K 的 student_tail = 1 - Σ student_log_prob(student 自己选的 K 个 token)
    → student 自己的 top-K 已捕获大部分概率质量
    → tail 很小,信息量低

当 student 和 teacher 分布差异大时,Teacher Top-K 的 tail bin 差异更大,信号更强。

维度 Teacher Top-K (OpenClaw) Student Top-K (原版 SDFT/SDPO)
实现难度 简单(1 次 API 调用) 复杂(2 次往返)
适合场景 teacher >> student teacher ≈ student
信息方向 "学 teacher 重视什么" "发现自己哪里错"
自我强化风险
Tail bin 精度 高(差异大时) 低(student 自身 top-K 已占大部分质量)
K 的合适大小 可以较大(K=50) 较小(K=10-20)即可

5.5.2 Student Top-K 的优势(OpenClaw 放弃的)

  • 梯度更直接:student 优化的就是自己概率最高的 token,梯度信号直指"把我的 top token 调整得更像 teacher"
  • 更好的 mode coverage:如果 student 有一个 teacher 没有的高概率 mode,Student Top-K 能发现并用 KL 惩罚修正
  • 自适应采样区域:随着训练推进,student 的 top-K 会变化

这些优势在 teacher ≈ student 的后期训练中更明显,但在 teacher 明显强于 student 的蒸馏场景下,Teacher Top-K 更合理。


5.6 数值风险

当 student 在 teacher 的 top-K 位置上概率之和接近 1 时(训练后期,student 已经学得很好),1 - exp(log_sum_topk) → 0,tail → -∞。代码通过 clamp(log_sum_topk, max=-1e-7) 做了上界保护,但 没有 clamp tail 的下界,极端情况下可能产生 NaN。这是已知的工程取舍------训练初期 student 分布与 teacher 差异大时 tail 的行为是正常的,而后期 student 收敛后通常不再需要继续 Top-K 蒸馏。


5.7 两种 OPD 路径对比总表

维度 标准 OPD (token-level) Top-K 蒸馏 (SDFT/SDPO 风格)
存储字段 teacher_log_probs [T] teacher_topk_log_probs [T,K] + teacher_topk_indices [T,K]
Advantage / Loss A_t = teacher_lp - rollout_lp,融入 PPO clip 独立 `D_KL(student
代码路径 Slime loss.py 内置 on_policy_distillation 分支 topk_distillation_loss.py 自定义 loss
信息量 每位置 1 个值 每位置 K+1 维分布
可并存性 字段不冲突 字段不冲突
适用场景 大多数 OPD 训练场景(默认推荐) 需要更丰富 teacher 信号的研究/高级蒸馏
Shell 启用 默认 --custom-loss-function-path topk_distillation_loss

5.8 常见问题

Q1: 反向 KL 在 Top-K 蒸馏中有什么潜在问题?何时该切换?

反向 KL 是 mode-seeking 的------student 一旦放弃 teacher 的某个 mode(某 bin 的 q_k → 0),梯度 q_k·log(q_k/p_k) → 0,永远学不回来。训练后期 student ≈ teacher 时,考虑改用 JSD(对称、有界)或混合 KL:(1-α)·D_KL(Q||P) + α·D_KL(P||Q),α 从 0 渐增到 0.5。

Q2: 从全词表 KL 到 Top-K+Tail 蒸馏,信息损失有多大?

tail bin 把 |V|-K 个 token 聚合为一个标量,丢失了 tail 内部的分布结构。可忽略的条件:teacher 分布足够 peaked(top-K 已捕获 >95% 概率质量)、K 足够大(K=50 对 |V|=150K 通常足够)、目标只是蒸馏(只需排序,不需精确匹配)。不可忽略的条件:teacher 分布平坦(高 entropy)或 K 太小(如 K=5)。

Q3: Top-K 蒸馏中的 Reverse KL 是 D_KL(student || teacher) 还是 D_KL(teacher || student)?

D_KL(student || teacher)。代码中 F.kl_div(input=teacher, target=student, log_target=True),PyTorch 定义为 D_KL(target || input),即 D_KL(student || teacher)。teacher 在 input 位置,student 在 target 位置。

0xFF 参考