在 Agent RL(尤其是基于 LLM 的 RL,如 RLHF、RLAIF、Tool-use RL、Long-horizon Agent RL)中,Retokenize Drift 指的是:

同一段文本在 rollout 阶段和训练阶段经过不同的 tokenizer/template 处理后,产生了不同的 token 序列,导致 RL 更新时计算的 logprob、advantage、KL 等对应不到原来的采样轨迹。

本质上是一种:

$$ \text{sampled trajectory} \neq \text{training trajectory}
$$

的问题。


1. 关键概念:同一句话,两条 token 路径不一定相等

在 agent RL 训练里,每个 step 你需要两样东西:

  1. 训练用的 token 序列(input_ids + response),喂给 trainer 算 loss
  2. rollout 时模型真正生成的 token(raw_output_ids),即采样时实际走过的那条 token 路径

问题的根源是:tokenizer 不是可逆双射。模型生成了一串 token id,你把它 decode 成文本,再 encode 回去,得到的 token 序列不一定一样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
            模型采样出的真实路径                    Retokenize 后的路径
        ┌───────────────────────┐            ┌───────────────────────┐
        │ raw_output_ids        │            │ encode(decode(ids))   │
        │ [451, 1029, 88, 320]  │  decode    │ "Hello world!"        │
        │         │             │ ────────►  │         │             │
        │         ▼             │            │         ▼  encode     │
        │   "Hello world!"      │            │ [451, 1030, 320]      │ ← 少了/变了 token!
        └───────────────────────┘            └───────────────────────┘
                  ▲                                      │
                  │   policy 的 log_prob 是基于左边算的   │
                  └──────────  ✗ 对不上  ◄───────────────┘

为什么会变(merge/normalization 导致的边界漂移):

1
2
3
4
5
6
7
8
真实生成(逐 token 采样,可能踩在“非最优” merge 上):
   "data" + "set"   ->  [ "data" ][ "set" ]      2 tokens

Retokenize(贪心 BPE,倾向最长匹配 merge):
   "dataset"        ->  [ "dataset" ]            1 token   ← 边界漂移!

下游连锁反应:
   token 数变了 -> 序列错位 -> response_mask / gen_steps / log_prob 全部对不齐

2. PPO 中为什么危险

PPO 要计算:

$$ R_t =
\frac{\pi_{\theta}(a_t)}
{\pi_{old}(a_t)}
$$

这里要求:

1
action_t

必须是 rollout 时真正采样出来的 token。


例如 rollout:

1
2
token_1 = "Hello"
token_2 = "world"

对应:

1
old_logprob

重新 tokenize 后:

1
2
3
token_1 = "<assistant>"
token_2 = "Hello"
token_3 = "world"

此时:

1
new_logprob[0]

对应的已经不是原来的 action。

于是:

1
2
3
ratio = exp(
    new_logprob - old_logprob
)

完全错位。

得到的 PPO 更新没有意义。


3. Agent RL 为什么更容易出现

单轮 RL 里,retokenize 漂移最多让结尾几个 token 错位。但 agent RL 是多轮拼接的,每一轮的 assistant 输出会变成下一轮的 prompt 历史,误差会逐轮累积放大。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
multi-turn 轨迹,trainer 侧拼成一条长序列:

  turn0:  [系统+user prompt][assistant gen0]
  turn1:                    └─►[同样内容当 prompt][assistant gen1]
  turn2:                                          └─►[...][assistant gen2]

如果每轮的 assistant 段用「retokenize」而不是「真实 raw_output_ids」:

  turn0 gen0 漂移 +1 ─┐
  turn1 prompt 含 gen0 → 又漂移 ─┐ 误差累加
  turn2 prompt 含 gen0+gen1 → 再漂移 ─┐  💥 越往后错得越多

错位之后,最直接的后果是 mask 和 log_prob 贴错位置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
理想(用真实 ids,response_mask 精确盖住 assistant 生成的 token):
  tokens :  P  P  P  | A  A  A  A | P  P | A  A
  mask   :  0  0  0  | 1  1  1  1 | 0  0 | 1  1     ← 1 = 计算 policy loss
  gen_step: -1 -1 -1 | 0  0  0  0 |-1 -1 | 1  1

漂移后(retokenize 让 assistant 段长度从 4 变 3,整体右移):
  tokens :  P  P  P  | A  A  A | P  P | A  A
  mask   :  0  0  0  | 1  1  1  1| 0  0| 1     ← ✗ mask 盖到了 prompt token,
                                  ▲              又漏掉了一个 assistant token
                                  └─ 在错误的 token 上回传梯度 / importance ratio 爆炸

importance sampling ratio π_new/π_old 假定分子分母在同一个 token 序列上。一旦序列因 retokenize 而错位,ratio 就失去意义,PPO/GRPO 的梯度会被污染 → 训练不稳甚至崩。

4. Long Horizon Agent 最典型的例子

假设 rollout 时:

1
Action: search("Tesla")

Tokenizer:

1
["Action",":"," search","(","Tesla",")"]

存盘后:

1
2
3
4
{
  "tool":"search",
  "arg":"Tesla"
}

训练时恢复:

1
2
3
<tool_call>
search(Tesla)
</tool_call>

Token 变成:

1
2
3
4
5
6
["<tool_call>",
 "search",
 "(",
 "Tesla",
 ")",
 "</tool_call>"]

已经完全不是原轨迹。


PPO 的:

1
old_logprob

对应的是第一种 token。

而 forward 算出来的是第二种 token。

结果:

1
2
3
KL
ratio
advantage

全部失真。


5. 业界怎么解决

方法 1:保存原始 token(最可靠)

Rollout 时直接存:

1
2
3
input_ids
response_ids
old_logprobs

训练时不再 tokenize:

1
2
3
dataset
    
directly use input_ids

例如:

  • OpenAI PPO
  • DeepMind PPO
  • VeRL PPO
  • PRIME
  • DAPO

基本都倾向这样做。


方法 2:Hash 检查

重新 tokenize 后:

1
2
3
assert(
    new_tokens == old_tokens
)

或者:

1
hash(tokens)

校验。

发现 drift:

1
drop sample

方法 3:Frozen Chat Template

训练期间固定:

1
2
3
tokenizer.json
chat_template.jinja
special_tokens

不允许变化。


方法 4:Token-level Replay

保存:

1
2
3
action_mask
position_ids
attention_mask

训练时完全重放 rollout。

Megatron-RL、veRL 新版本大量采用这种方案。


一句话总结:

Retokenize Drift 是指 Agent RL 中 rollout 时采样得到的 token 序列与训练时重新 tokenize 得到的 token 序列不一致,导致 PPO/KL/Advantage 计算对应不到原始动作轨迹的问题。本质上是 trajectory reconstruction error,在长轨迹、多轮 Tool Use、Agent RL 场景下尤其严重,因此现代 RL 框架越来越倾向于直接保存 token-level trajectory,而不是保存文本后再 retokenize。

参考