这篇文章把「Agentic RL / LLM-RL 训练里常见的 Policy Gradient (PG) loss 到底由哪些组件拼起来」讲清楚,重点解释:

  • PPO-clip:为什么要 clip、clip 住了哪些情况、什么时候梯度为 0
  • Dual-clip:它在 PPO-clip 基础上到底“多 clip 了什么”,解决什么不稳定
  • Entropy / KL:为什么要加正则、权重怎么理解
  • 聚合(aggregate):token/sequence/group 维度的 sum/mean 会如何改变梯度尺度

本文偏“工程视角”:你看完应该能把这些项在代码里正确实现出来,并能解释训练曲线为什么会那样。

补充说明:下面不少“实现细节/监控指标/聚合模式”的表述,我会刻意对齐 verl 生态里常见写法。


0. 背景:我们在优化什么

强化学习(policy gradient)最朴素的目标是最大化期望回报:

$$
\max_\theta \ \mathbb{E}{\tau \sim \pi\theta} [R(\tau)]
$$

但直接对回报做梯度很难,于是我们引入可优化的代理目标(surrogate objective),用“概率比 + 优势函数”把“回报最大化”转成可微的 loss。

当你训练的是 LLM(token 级动作)或 Agent(工具调用/检索/规划动作),形式几乎一样,只是:

  • 动作可能是 token,也可能是工具调用、检索 query、规划步骤
  • 优势可能来自 reward model、对比评分器、规则评测、人工偏好、任务成功率等

1. 记号与核心中间量(非常重要)

一条轨迹里第 $t$ 步:

  • 状态:$s_t$(例如 prompt + history)
  • 动作:$a_t$(token / 工具调用 / 选择哪个候选等)
  • 新策略:$\pi_\theta(a_t|s_t)$
  • 旧策略:$\pi_{\text{old}}(a_t|s_t)$
  • 优势:$A_t$(“这步动作比平均水平好多少”)

重要采样比(importance ratio)

$$
r_t = \frac{\pi_\theta(a_t|s_t)}{\pi_{\text{old}}(a_t|s_t)}
= \exp(\log \pi_\theta(a_t|s_t) - \log \pi_{\text{old}}(a_t|s_t))
$$

为什么要 ratio?

  • 你的数据通常是用旧策略采样的(rollout 时的策略),但你想更新新策略
  • ratio 允许你在“旧数据”上估计“新策略”目标(近似、但有效)

2. Vanilla Policy Gradient(从这里出发)

最常见的一种 PG 代理目标(最大化形式):

$$
L_{\text{PG}}(\theta) = \mathbb{E}[\log \pi_\theta(a_t|s_t) \cdot A_t]
$$

工程里我们一般写成“最小化 loss”,所以常见写法是取负号:

$$
\text{loss}{\text{PG}} = -\mathbb{E}[\log \pi\theta(a_t|s_t) \cdot A_t]
$$

问题是:如果你直接这么做,更新会非常不稳定。

原因直观上是:$\log \pi_\theta$ 的梯度可能会推动策略一步跨得很大,尤其当 $A_t$ 大、或某些动作在旧策略里概率极小导致 ratio 爆炸时。


3. PPO-clip:用 clip 做“近似 trust region”

PPO 的核心代理目标(最大化形式):

$$
L_{\text{clip}}(\theta)=
\mathbb{E}\big[\min\big(r_t A_t,\ \text{clip}(r_t, 1-\epsilon, 1+\epsilon)A_t\big)\big]
$$

其中 $\epsilon$ 通常是 0.1 或 0.2。

在工程实现里,你几乎一定会看到最小化 loss 的等价写法(HF PPO / verl 常用):

$$
L^{\text{PPO}} = \mathbb{E}_t[\max(-r_t\hat A_t,\ -\text{clip}(r_t,1-\epsilon,1+\epsilon)\hat A_t)]
$$

它就是上面 maximize 版本取负号后,把 min 翻转成 max 得到的。

3.1 直觉:限制“新策略相对旧策略”的变化幅度

  • $r_t$ 表示“新策略把这个动作概率放大/缩小了多少”
  • clip 把 $r_t$ 限制在 $[1-\epsilon, 1+\epsilon]$

所以 PPO-clip 做的事情是:你可以变,但别变太猛

3.2 什么时候 clip 会“生效”(梯度变 0 或被截断)

把情况按 $A_t$ 的符号拆开看最清楚:

  • 当 $A_t > 0$(这个动作是“好动作”,我们希望提升概率)
    • 如果 $r_t > 1+\epsilon$:提升得太猛了,会被 clip,等价于“这部分梯度被截掉”
  • 当 $A_t < 0$(这个动作是“坏动作”,我们希望降低概率)
    • 如果 $r_t < 1-\epsilon$:降低得太猛了,会被 clip

你会发现:PPO 的 clip 本质是在限制“朝着对优势有利的方向”更新过头

3.3 训练时看什么:reward 曲线优先,其次看这些“稳定性指标”

PPO-clip 的 loss 是 surrogate objective,不一定和“任务真的变好”单调对应;实践里更应该关注 rewards curve(你定义的任务 reward / RM reward / success rate 等),loss curve 只当作辅助信号。

除了 reward 曲线,建议至少监控这些量(很多训练框架会直接给出):

  • actor entropy:熵太快掉到很低,常见是探索不足/模式塌缩的前兆。
  • KL(对 reference / SFT / old policy 的偏离):漂移过大常见会让输出质量崩。
  • clip fraction:被 clip 的样本比例(token-level 或 seq-level,看实现)。
  • advantage 的统计量:均值/方差/分位数,尤其注意优势刻度突然变大或分布漂移。

clip fraction 怎么解读?

  • 直觉:被裁剪的样本通常“不给有效梯度”,等价于 有效 batch 变小。clip fraction 越高,更新越被少数未裁剪样本主导,学习容易停滞或变得不稳定。
  • 实践经验:0.1-0.4 往往是比较常见的区间(不是硬规则,取决于任务/优势刻度/学习率等)。

clip fraction 和 KL 的耦合(非常实用的排查思路)

  • clip fraction 高 + KL 也高:常见是步子太猛(学习率大、PPO epoch 多、优势刻度过大、reward 过陡等)。
  • clip fraction 高但 KL 不高:常见是对同一批数据复用太多(epoch 太多 / 复用过度),把 $r_t$ 推到边界但整体分布偏离并不大。
  • clip fraction 长期接近 0:常见是步子太小(学习率小、优势过小、KL/entropy 系数过大把更新压住)。

3.4 policy_loss 曲线怎么看(注意符号约定)

不同代码对 policy_loss 的符号约定不完全一致:有的记录“要最小化的 loss”(常见为负),有的记录“maximize 的 surrogate”(常见为正)。

一个可操作的解读方式是:

  • 如果 policy_loss(最小化形式)长期非常负:通常意味着优势项在推动策略快速提升,但也可能是 critic 跟不上 actor(critic 低估,adv 偏大),容易引入不稳定。
  • 如果 policy_loss(最小化形式)长期为较大的正:通常是“坏动作占比大/优势为负占主导”,策略在变差或学不到东西的信号。
  • 更理想的趋势:在 0 附近上下震荡,说明 actor/critic 在互相追赶并逐步稳定。

4. Dual-clip:为什么 PPO-clip 还不够

在实践里,很多人会再加一个 dual-clip,主要解决这样一种现象:

当 $A_t < 0$(坏动作),但 $r_t$ 可能非常大(新策略反而大幅提高了坏动作的概率)时,PPO-clip 在某些写法下对这块的“保护”不够,loss/梯度可能会被放大,导致训练震荡。

你可以把 dual-clip 理解成:对某些极端 case 再加一道上限/下限,避免目标函数在异常 ratio 下过度放大

4.1 Verl/HF 常见写法(最小化 loss 形式,对齐更容易)

先把 PPO 写成最小化 loss(忽略期望符号,只写单个样本/单个 token 的形式):

$$
L^{\text{PPO}}=\max(-r_t\hat A_t,\ -\text{clip}(r_t,1-\epsilon,1+\epsilon)\hat A_t)
$$

Dual-clip(常见引用:arXiv:1912.09729)在很多实现里会对负优势样本再加一道“上界保护”:

$$
L^{\text{dual-clip}}=\min(L^{\text{PPO}},\ -c\cdot \hat A_t), \quad c>1
$$

直觉解释:

  • 当 $\hat A_t < 0$ 时,$-c\hat A_t$ 是正数,它会把某些极端情况下的巨大正 loss 截住(避免 ratio 异常大导致 loss/梯度过度放大)。
  • 当 $\hat A_t \ge 0$ 时,这个额外项通常不启用或不会改变结果(不同代码会用条件分支实现)。

4.2 参数 $c$ 怎么选

经验上很多实现会用 2 到 5 的量级(比如 3)。它不是越大越好:

  • $c$ 太小:保护太强,学习可能变慢
  • $c$ 太大:保护太弱,起不到稳定作用

5. Entropy:为什么要鼓励“更随机”

entropy bonus 常见形式(加入到最小化 loss):

$$
\text{loss} \leftarrow \text{loss} - \beta \cdot H(\pi_\theta)
$$

5.1 从 logits 计算 token-level entropy(verl 常见写法)

很多实现会直接从 logits 计算每个 token 的熵(注意输出仍然是 $[B,T]$,后面要走 agg_loss 聚合):

$$
H = \log\sum \exp(\text{logits}) - \sum p\cdot \text{logits}, \quad p=\text{softmax}(\text{logits})
$$

1
2
3
4
5
6
import torch

def entropy_from_logits(logits: torch.Tensor):
pd = torch.nn.functional.softmax(logits, dim=-1)
entropy = torch.logsumexp(logits, dim=-1) - torch.sum(pd * logits, dim=-1)
return entropy # [B, T]

直觉:

  • 在早期训练,鼓励探索(别过早塌缩到某一种输出模式)
  • 在 LLM 训练里,entropy 太低会导致输出变得呆板,甚至出现 mode collapse

怎么调参:

  • $\beta$ 太大:模型“太随机”,reward 上不去
  • $\beta$ 太小:容易塌缩或过拟合到奖励漏洞

6. KL:为什么用 KL 约束,而不是 JS

很多 LLM-RL(尤其 RLHF / RLAIF / 各种 agentic RL 变体)都会加 KL 正则,典型是约束当前策略别偏离参考策略 $\pi_{\text{ref}}$(常见是 SFT 模型或某个 frozen baseline):

$$
\text{loss} \leftarrow \text{loss} + \beta \cdot \text{KL}(\pi_\theta ;||; \pi_{\text{ref}})
$$

6.1 KL 在工程上“很好算”

在 token-level 的实现里,你往往只需要:

$$
\text{KL} \approx \mathbb{E}[\log \pi_\theta(a|s) - \log \pi_{\text{ref}}(a|s)]
$$

(实际会有更精细的估计方式,但核心是:你只要能拿到 logprob 就行)

而 JS 散度需要混合分布:

$$
\text{JS}(P||Q) = \tfrac{1}{2}\text{KL}(P||M) + \tfrac{1}{2}\text{KL}(Q||M),\ \ M=\tfrac{1}{2}(P+Q)
$$

这意味着你要显式构造/评估 $M$,工程上更麻烦、也更贵。

6.2 KL 更符合“单向约束”的需求

在 RLHF 里,你常见的真实需求是:

“在提高 reward 的同时,别偏离 reference 太远。”

这本质是一个单向的、带约束的优化问题,KL 非常自然。

JS 虽然对称、有界,但它的对称性并不一定是你想要的,而且在高维动作空间(比如大词表)里,JS 的数值/梯度性质也未必更稳定。

6.3 更重要的一点:PPO/TRPO 的理论也经常围绕 KL

TRPO 的 trust region 约束是直接写 KL 的;PPO 也是在用 clip 近似这个约束。

所以当你把 PPO-clip + KL penalty 放在一起看,会发现它们在做同一类事情:

  • PPO-clip:约束“这一步更新别太大”
  • KL penalty:约束“整体别偏离 reference 太远”

6.4 KL 放在 reward 里 vs 放在 loss 里(RLHF/LLM-RL 很常见)

在 LLM-RL 里,KL 有两种非常常见的“落点”:

  1. 加在 loss 里policy_loss += kl_loss * kl_coef(verl/HF 里常见)。
  2. 写进 token-level reward(reward shaping):把 KL 看成一个“密集惩罚”,例如常见形式

$$
r_t = r_{\text{RM}} - \beta \log\frac{\pi_\theta(a_t|s_t)}{\pi_{\text{SFT/ref}}(a_t|s_t)}
$$

其中 $r_{\text{RM}}$ 往往只在序列最后一个 token 非零(或以序列级 reward 形式给出),而 KL 惩罚是 token 级的“每步都扣分”。两者目标一致:限制偏离,但优化动态、日志解读、以及和 advantage/GAE 的耦合会有差异。


7. 聚合(Aggregate):token / sequence / group 的差别

LLM 的 logprob、advantage 很多时候都是 token-level 的张量(形状 $[B, T]$)。

你最终需要把它聚合成一个标量 loss 才能 backward()。常见聚合方式:

  • token mean:对所有有效 token 取平均
  • sequence mean:先对每个序列求平均,再对 batch 平均
  • token sum:对 token 求和(注意会放大梯度尺度)
  • group mean(GRPO 常见):先在 group 内聚合,再在 group 间聚合

核心结论:聚合方式会改变梯度的等效尺度,从而影响你对学习率、clip、KL 系数等超参的感受。

如果你把 mean 换成 sum,很多时候你会发现“同样的超参突然不稳定了”,原因往往就是梯度整体变大了。

7.1 常见 loss_agg_mode 的数学定义(带 mask)

设 loss 矩阵 $L\in\mathbb{R}^{B\times T}$,mask $M\in{0,1}^{B\times T}$(为 1 的位置计入损失):

  • token-mean
    • $$\mathcal{L}=\frac{\sum_{i=1}^{B}\sum_{j=1}^{T}L_{i,j}M_{i,j}}{\sum_{i=1}^{B}\sum_{j=1}^{T}M_{i,j}}$$
  • seq-mean-token-sum
    • $$\mathcal{L}=\frac{1}{B}\sum_{i=1}^{B}\Big(\sum_{j=1}^{T}L_{i,j}M_{i,j}\Big)$$
  • seq-mean-token-mean(很多论文把它当成“sample-level loss”)
    • $$\mathcal{L}=\frac{1}{B}\sum_{i=1}^{B}\Big(\frac{\sum_{j=1}^{T}L_{i,j}M_{i,j}}{\sum_{j=1}^{T}M_{i,j}}\Big)$$
  • seq-mean-token-sum-norm(一种“按长度做归一化”的 token-sum)
    • $$\mathcal{L}=\frac{\sum_{i=1}^{B}\sum_{j=1}^{T}L_{i,j}M_{i,j}}{T}$$

7.2 和 GRPO / DAPO / DrGRPO 的对应(为什么会影响长 CoT 稳定性)

以 verl 文档/实现里的口径总结(不同实现会有细微差异,但核心直觉一致):

  • GRPO 原始论文常用 seq-mean-token-mean(sample-level),在长 CoT 场景可能更不稳定。
  • DrGRPO 倾向使用 seq-mean-token-sum-norm(降低长度偏置/数值不稳)。
  • DAPO 倾向使用 token-mean(直接在 token 维度平均,梯度尺度更直观)。

如果你的任务输出长度差异很大(例如 deep research 报告长短不一),聚合方式几乎一定会影响训练是否稳定,甚至会直接改变“模型更偏好长输出还是短输出”(长度偏置)。

7.3 GSPO/GRPO:序列级 ratio、长度偏置与 stop-gradient trick

很多 LLM-RL 的不稳定来自两个因素叠加:

  • token-wise ratio 在长序列上更容易数值爆炸/塌缩
  • 长序列天然贡献更多 token,导致“长输出占更多梯度份额”(长度偏置)

以 GSPO 的一种常见写法为例,会先定义序列级的归一化 ratio

$$
s_i(\theta)=\Big(\frac{\pi_\theta(y_i|x)}{\pi_{\text{old}}(y_i|x)}\Big)^{1/|y_i|}
=\exp\Big[\frac{1}{|y_i|}\sum_t \log\frac{\pi_\theta(y_{i,t}|x,y_{i,<t})}{\pi_{\text{old}}(y_{i,t}|x,y_{i,<t})}\Big]
$$

再把它“分摊回 token 级”,并用 stop_gradient(记作 sg[·])避免破坏梯度结构:

$$
\log s_{i,t}(\theta)=\text{sg}[\log s_i(\theta)] + \log\pi_\theta(y_{i,t}|\cdot) - \text{sg}[\log\pi_\theta(y_{i,t}|\cdot)]
$$

直觉:让每个样本(序列)在 ratio 上先被长度归一化,再决定 token 级梯度怎么分配,从而缓解长度偏置和 token-wise 数值问题。


9. PyTorch 伪代码:把这些项拼起来(可直接对照你用的框架)

下面给一个“最常见”的结构示例,假设你已经有:

  • logp_new: 当前策略对采样动作的 logprob,形状 [B, T]
  • logp_old: rollout 时旧策略 logprob(需要 detach),形状 [B, T]
  • advantages: 优势(需要 detach),形状 [B, T]
  • mask: 有效 token mask(padding 位置为 0),形状 [B, T]
  • entropy: 可选,形状 [B, T]
  • logp_ref: 参考策略 logprob(可选),形状 [B, T]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import torch

def masked_mean(x, mask, eps=1e-8):
return (x * mask).sum() / (mask.sum() + eps)

def ppo_pg_loss(
logp_new, logp_old, advantages, mask,
clip_eps=0.2,
dual_clip=None, # e.g. 3.0
ent_coef=0.0,
entropy=None,
kl_coef=0.0,
logp_ref=None,
):
# ratio = exp(log pi_new - log pi_old)
ratio = torch.exp(logp_new - logp_old)

# PPO clipped surrogate (minimize loss)
pg1 = -advantages * ratio
pg2 = -advantages * torch.clamp(ratio, 1.0 - clip_eps, 1.0 + clip_eps)
pg = torch.max(pg1, pg2) # because we minimize negative of the "min" objective

# Dual-clip: cap extreme loss on negative-adv samples
# Note: different codebases implement slightly different variants.
if dual_clip is not None:
# Common (verl-like) form: for advantages < 0, cap the loss by min(L_ppo, -c * A)
cap = (-dual_clip) * advantages
pg = torch.where(advantages < 0, torch.min(pg, cap), pg)

loss = masked_mean(pg, mask)

# Entropy bonus (encourage exploration)
if ent_coef != 0.0:
assert entropy is not None
loss = loss - ent_coef * masked_mean(entropy, mask)

# KL penalty (keep close to reference)
if kl_coef != 0.0:
assert logp_ref is not None
kl = (logp_new - logp_ref) # common estimator on sampled actions
loss = loss + kl_coef * masked_mean(kl, mask)

return loss

9.1 读代码时最容易踩坑的点

  1. 最大化 vs 最小化:论文写 maximize,代码写 minimize;min/max 会翻转。
  2. advantages / logp_old 要 detach:它们是“常数”,不应对它们反传。
  3. mask 一定要做:padding token 不能算进 loss。
  4. 聚合方式会改变梯度尺度:mean/sum 会影响你对 lr/clip/kl 的直觉。

9.2 如果你用的是 verl:如何“真的改到代码里”

modern_genai_bilibili 的笔记里给了一个很实用的路线图(这里总结成可执行的 checklist):

  1. 全局搜 .backward(),先找到训练主循环真正反传的 loss(通常是 loss.backward())。
  2. core_algos.py(或同名文件)里找 compute_policy_loss,它通常是 “PG loss 的注册中心”。
  3. 通过类似 @register_policy_loss("xxx") 的注册机制添加新的 pg_loss 变体(例如把 entropy/KL/特殊 ratio 写法组合进去)。
  4. 如果你要改的是 training logic(比如动态采样、rollout 管理、异步 agent loop),通常需要自定义 Trainer(笔记里提到的 RayDAPOTrainer 就属于这类)。

10. 这和 Agentic RL / Deep Research 有什么关系

如果你做的是“deep research agent”(检索 + 阅读 + 归纳 + 写作),你不一定要微调 LLM 本体。

更现实的路线是:

  • LLM 用 API(当成 black-box policy 或 planner)
  • 你训练的是 agent 的决策层:何时检索、检索什么、读哪些文档、如何分解任务、如何分配预算
  • reward 来自任务指标:答案质量、引用覆盖、事实一致性、时间/成本、用户满意度等

这时 PPO/GRPO 这些 loss 仍然是核心工具,只是动作空间从“token”扩展到了“工具与策略”。

如果你具体做的是“deep research 报告型任务”,建议尽早把评测闭环定下来:

  • RACE:评估报告质量(覆盖度/洞察/指令遵循/可读性),并用相对评分避免“打分膨胀”。
  • FACT:逐条核查“每一个引用是否支撑对应陈述”,把 citation trustworthiness 从纯生成质量里拆出来单独评。

11. 小结

  • PPO-clip 的本质:用 ratio + clip 做稳定的 surrogate objective
  • dual-clip 的本质:对极端 case 再加一道保护,减少震荡
  • entropy/KL 的本质:一个防塌缩,一个防漂移
  • aggregate 的本质:改变梯度尺度,影响所有超参的手感