跳到正文
北京时间
原文
LMSYS:Blog(Chatbot Arena 团队)·· 2026-06-05精选AI 评分62

不再遗漏任何Token:解析Miles中的Token-In-Token-Out(TITO)

Blog No Token Left Behind: Demystifying Token-In-Token-Out in Miles In agentic RL, a rollout is not a single generation. It is a chain of model calls, tool outputs, harness messages, and resumed generations. Token-In-Token-Out (TITO) is a design principle that address... Miles Team: Jiajun Li, Yanbin Jiang, Mao Cheng, Shi Dong, Yusheng Su, Yueming Yuan, Zhichen Zeng, Banghua Zhu

AI 导读

Miles框架提出Token-In-Token-Out(TITO)原则,解决智能体强化学习中训练-推理不匹配:确保rollout过程token序列与训练器评估序列逐位一致。TITO将多轮轨迹视为一个连续序列(每任务一个样本),节省一个数量级计算开销并维持on-policy性。三种破坏场景:反分词-再分词不匹配、聊天模板修剪推理内容、有损模板重新渲染。Miles通过推理会话服务器、三级只追加保证、可插拔TITO分词器和序列比较器实现。典型任务(如SWE-Bench)轨迹含30-50轮。

推荐理由

LMSYS团队把agent RL里最隐秘的训练-推理不一致问题解释透了,TITO原则直接告诉你为什么之前训练不稳,做agent训练的都该看看这篇。

正文 · AI 翻译

Miles 团队:Jiajun Li、Yuzhen Zhou、Shi Dong、Yanbin Jiang、Mao Cheng、Yusheng Su、Yueming Yuan、Zhichen Zeng、Banghua Zhu

在智能体强化学习中,一次 rollout 并不是单次生成。它是一条由模型调用、工具输出、harness 消息以及恢复生成组成的链条。Token-In-Token-Out(TITO)是一项设计原则,用于解决这一过程中训练与推理不匹配的一个关键来源:训练器所评估的 token 序列,是否与推理引擎在 rollout 期间所消费和产生的 token 序列完全一致。在这篇博客文章中,我们旨在阐明我们如何定义 TITO 原则、它为何在强化学习训练中至关重要,以及这一原则如何在 Miles 框架中得以实现。

TITO 的定义

在一次智能体 rollout 中,模型会反复与外部环境交互。在一个简化设定下,模型首先接收任务描述并生成 token,其中可能包含推理内容和一次工具调用。智能体运行时解析该工具调用,将其发送至相应的环境或工具后端,并将结果作为新的观测返回。随后模型从该观测继续生成,并可能发起另一次工具调用。这一循环不断重复,直到任务完成。

请注意,该过程涉及对推理引擎的多次独立调用,人们通俗地将其定义为轮次。在每一轮中,引擎接收一个 token 序列作为提示词,并生成另一个 token 序列。我们称 TITO 原则得到满足,如果对于所有n,第n−1轮中的完整 token 序列(提示词 + 回复)是逐位完美前缀第n轮中提示词 token 序列的。这一思路如下图所示。

TITO definition diagram

为什么 TITO 很重要?

训练效率:每个任务一个样本

在智能体强化学习中,单个任务可能包含数十个回合,我们基本上有两种方式为 RL 训练器打包数据:

  1. 每回合一个样本:每个回合都被视为一个独立的训练样本。
  2. 每任务一个样本:所有回合被“粘合”成一个单一的连续序列。

让我们比较这两种方案。在方案 1 中,训练器接收的样本数量等于一条轨迹中的轮数;而在方案 2 中,无论轮数多少,训练器对每个任务实例始终只接收一个样本。对于典型的类 SWE-Bench 任务,一条轨迹包含 30-50 轮,这意味着要摄入同等数量的信息,方案 2 只需花费比方案 1 少一个数量级的计算量。计算成本如此大幅的降低,使方案 2 在扩展智能体强化学习训练方面尤其具有吸引力。

数学正确性:保持同策略性

要让一个训练样本处于 on-policy 状态,每个被采样的 token 都应当由训练器在与 rollout 期间生成它时相同的条件分布下进行评估。在 Transformer 中,该条件分布完全取决于该 token 之前的上下文。如果 TITO 被违反,就可能存在某个 token xt,使得

  • 在训练器中,模型会进行评估xt基于前面的序列x.
  • 在推理引擎中,模型会采样xt基于一个略有不同的前序序列x~.

即便训练器和推理引擎共享完全相同的权重,条件概率 π(xt∣x) 也可能与 π(xt∣x~) 出现巨大偏差。这种偏差最终可能导致更新不稳定,危及 RL 训练的稳定性。

TITO 可能如何失效

尽管 TITO 原则在概念上很简单,但它很脆弱。在下文中,我们给出了众多场景中的三个常见场景,在这些场景中该原则可能被违反。

场景 1:反分词-重分词不匹配

在多轮 RL rollout 中,人们可能会将模型生成的 token 反分词为字符串以便存储,随后在构建第 n 轮的提示词时再对其重新分词。这可能会破坏 TITO 原则,因为模型生成的 token 未必能在反分词-重分词的往返过程中保持不变。

根本原因在于分词器编码文本的方式与模型生成 token 的方式之间存在不对称:

  • encode(文本 → token)是一对一的:对于给定的输入字符串,分词器总是选择一种标准切分方式(通常是贪心 / 最长匹配)。
  • decode(token → 文本)是多对一的:多个不同的 token 序列可以解码为完全相同的字符串。模型可以、有时也确实会生成一个合法但非标准的 token 序列。

Detokenize-retokenize mismatch

示例:假设模型生成了两个独立的 token Hel(3) 和 lo(7)。将它们解码会得到字符串 "Hello"。然而,当你重新编码 "Hello" 时,tokenizer 会按规范将其编码为单个 token Hello(4)。原始的 Hel(3) + lo(7) 序列就此永远丢失,导致训练器评估的是一个模型实际上从未采样过的 token 序列。

场景 2:推理内容被聊天模板裁剪

聊天模板会把一个类似 JSON 的消息列表转换成单个提示词字符串,发送给推理引擎。一些推理模型的模板引入了我们称之为思考截断边界的东西:即对话中的某个点,在该点之前的历史助手推理会从渲染后的提示词中被移除。在 Qwen3 和 Kimi K2 等推理模型的默认聊天模板中,这条边界由最后一条 User 消息决定。当模板渲染对话时,它会丢弃出现在最后一条 Assistant 消息之前的 User 推理,只保留该边界之后的推理。

然而,智能体执行框架经常会在任务中途注入 User 消息——例如,Terminus-2 框架用 User 来承载终端输出,而其他框架则用它来处理引擎重试,比如“Parse failed”。每一次注入都会把思考截断边界向前推,悄无声息地抹掉模型实际采样出的推理内容,破坏轮次之间逐比特完全一致的前缀。这一行为如下图所示。

Cut-think boundary breakage

场景 3:有损的聊天模板重新渲染

许多推理引擎接受一个消息列表,并在每次调用时重新应用聊天模板和 tokenizer 来构建提示词。这很方便,但也很危险:聊天模板是在字符串层面工作的——空白裁剪、转义处理、推理内容重新打包——因此它们为给定消息输出的 token ID 可能取决于该消息是何时以及与什么一起被渲染的。

因此,在消息层面重新应用聊天模板也会引入意外的文本漂移。以下是一个具体的失败模式。在第n−1轮中,助手发出一个工具调用,其流式 token 解码为一个紧凑的 JSON 主体——逗号或冒号后没有空格,键按模型选择的顺序排列:

{"name":"bash","arguments":{"cmd":"ls"}}

引擎会解析这个字符串,并将其作为结构化tool_calls字段存储在助手消息上。反过来n,当模板重新渲染对话时,它会序列化tool_calls再经过一个tojson过滤器。由于这种先解析再序列化的往返过程本质上会丢弃原始的字节级格式(空格、换行),该过滤器会应用自己的默认间距并输出:

{"name": "bash", "arguments": {"cmd": "ls"}}

注意每个逗号和冒号后面多出的空格。语义相同,字节不同,token ID 也不同。这会破坏逐位完全一致的前缀。

TITO 在 Miles 中是如何实现的

Miles 用四个组件来实现 TITO,其设计使得核心不变量能够被机械验证,并且新模型的接入成本很低。

(1) 推理会话服务器

一个推理会话是单条轨迹与推理引擎的交互——即属于同一任务的轮次序列,共享一个不断增长的 token 缓冲区。推理会话服务器是一个轻量的服务器层,按 session id 维护每条轨迹的状态。在每个 id 下,它持有一个不断增长的 token 缓冲区P,每一轮都会就地追加。该 token 缓冲区保留了每个样本精确的 token 级信息(logprobs、路由到的专家),因此可以直接发送给训练。

Inference session server architecture

TITO 推理会话服务器流程。

(2) 在三个层面确保仅追加

仅追加意味着每一轮都在扩展上一轮的数据,而不重写任何更早的字节。Miles 在三个层面强制执行这一点:

第 1 层——消息列表。第 n 轮的消息列表在第 n−1 轮的基础上于尾部追加新消息;更早的消息字典永远不会被修改。

第 2 层——chat-template 渲染。 chat template 可能通过裁剪先前内容(场景 2)或根据 harness 已追加的消息角色进行不同渲染,从而破坏仅追加特性。为防止裁剪,Miles 提供了固定的 jinja 模板,通过 clear_thinking: false kwarg 禁用 cut-thinking,从而在跨轮次时保留历史推理内容。为防止依赖角色的渲染漂移,用户通过 --tito-allowed-append-roles 声明预期追加的角色,Miles 会为该角色集自动选择前缀稳定的模板配置。

第 3 层——token 序列。 对这些渲染结果进行 tokenize 时,必须按照 TITO 的定义产生逐位完美的 token 前缀。即使第 2 层成立,朴素的重新 tokenize 也会破坏这一点。Miles 完全避免了重新 tokenize:每一轮只对新追加的消息进行 tokenize,并将得到的 ID 原地追加。下一节所述的可插拔 TITO tokenizer 正是让这种仅追加 tokenize 得以工作的关键。

(3) 可插拔 TITO tokenizer

TITO tokenizer 负责在 harness 追加新的非 assistant 消息时扩展 P——即由推理会话服务器维护的每条轨迹的 token 缓冲区。它计算要拼接到 P 上的增量 token,以及某些模型在拼接点所需的分界补丁。

基本思路——dummy-prefix 增量 tokenize。 该方案(受这篇博客文章启发)如下:

  1. 构建一个合成的极简上下文。
  2. 用新消息渲染一次聊天模板,再不带新消息渲染一次。
  3. 对字节差异进行编码。

由此得到的增量给出了新的序列化内容,Miles 从中推导出要追加到 P 的增量 token。

例如,假设 P 已经保存了截至第 n−1 轮的 token 化前缀,而 harness 现在追加了一个工具响应:

old_messages = [system, user, assistant]
new_messages = old_messages + [
    {"role": "tool", "content": "file1.txt\nfile2.txt"},
]

以 Qwen3 的模板为例,该工具响应的字节差异为:

<|im_start|>user
<tool_response>
file1.txt
file2.txt
</tool_response><|im_end|>

对其进行编码即可得到要追加到 P 上的增量 token。

拼接点补丁。 上文介绍的基本方法假设引擎放入 P 的 token 已经与规范模板在该位置渲染出的内容一致。然而,真实模型常常违反这一假设,需要在拼接点进行针对每个模型的小型补丁,Miles 通过我们的 TITO tokenizer 中的一个 hook 来处理这一点:

  • Qwen3。 模型停在 <|im_end|>,但聊天模板在每一轮结尾都会以 <|im_end|>\n 结束,因此 P 缺少了末尾的换行 token。Miles 在拼接增量 token 之前,先追加缺失的 \n token。
  • GLM-4.7。 该模型会将 <|user|> 或 <|observation|> 采样为停止 token 和下一消息起始 token,而 harness 接下来可能注入不同的角色,从而在 P 末尾留下错误的边界 token。Miles 会在拼接增量 token 之前,将该错误的边界 token 覆盖为与即将到来的角色相匹配的正确 token——同时将被替换位置的 loss mask 置零,使这一替换不参与训练。

以下图示说明了 Qwen3 和 GLM-4.7 的拼接点补丁是如何工作的。

Qwen3 splice-point patch

Qwen3 拼接点补丁。引擎在 <|im_end|> 处停止,但规范的聊天模板以 <|im_end|>\n 结束每一轮对话。Miles 会在拼接下一条消息的增量 token 之前,将缺失的 \n 追加到 P。

GLM-4.7 splice-point patch

GLM-4.7 拼接点补丁。该模型会将 <|user|> 或 <|observation|> 采样为停止 token 和下一消息起始 token,但 harness 接下来可能注入不同的角色。Miles 会在拼接增量 token 之前,将 P 中错误的边界 token 覆盖为与即将到来的角色相匹配的正确 token(通过 loss mask 置零,使其不参与训练)。

(4) 通过 token 序列比较器进行验证

每次 rollout 之后,我们都会检查 token 缓冲区 P 是否与从头渲染完整消息列表时聊天模板会生成的结果一致。如果 P 不一致,说明训练器读取的是模型从未真正见过的上下文——训练会悄无声息地偏离策略。Miles 提供了 TokenSeqComparator 来执行这项检查。

设 actual 为 P,而 expected 为我们将消息列表通过聊天模板重新渲染后得到的 token。我们不能简单地逐 token 比较它们:根据场景 1,同一段文本可能编码为不同的 token ID,因此严格的相等性检查会把无害的重新分词标记为失败。

为了跳过这些无害的重新分词,同时仍能捕获真正的 bug,Miles 采用了一种文本-token 混合检查:

  1. 结构检查:在消息边界特殊 token 处切分 actual 和 expected(例如 Qwen3 的 <|im_start|> / <|im_end|>,但不是 <think> / </think>),并验证这两个特殊 token 序列是否匹配。
  2. 文本检查:将特殊 token 之间的片段解码回文本并比较字符串,这样即使重新分词方式不同、但解码后文本相同的情况也能通过。

即便有上述检查,模型仍可能产生偏离聊天模板预期格式的输出。Assistant 文本一致性被破坏的几种常见方式包括:

  • 工具调用边界周围的空格或换行(场景 3)
  • 未闭合的 <think> 块
  • <think> 周围被剥离的换行或多余换行

Assistant text anomalies

由于这类情况,assistant token 的不匹配无法避免;Miles 会记录它们并将其标记为非关键。所有其他不匹配——特殊 token 序列和非 assistant 文本——必须保持为零,因为这些区域是确定性的,任何偏差都表明 TITO 中存在真正的 bug。

两个验证脚本会在所有受支持的(模型、append-role-set)组合上运行此比较器:一个 CPU/快速层作用于渲染后的 token 序列,另一个 GPU/端到端层会在真实模型推理下重复该检查。任一失败都会在变更进入训练之前将其阻止。

支持的模型

TITO 流水线目前原生支持以下模型(包括思考与非思考变体):

  • Qwen:Qwen3、Qwen3.5、Qwen3-Next
  • GLM:GLM-4.7、GLM-5、GLM-5.1
  • Kimi:Kimi-K2、Kimi-K2.5、Kimi-K2.6
  • Nemotron:Nemotron-3
  • Minimax:Minimax-M2.5、Minimax-M2.7
  • Deepseek:Deepseek-v3.2、Deepseek-v4

对于每个模型(Deepseek-v3.2 和 Deepseek-v4 除外),TITO 已验证能够处理 harness 在首个 assistant 轮次之后可能追加的以下消息角色组合:

  • {tool}:仅注入工具输出的 harness。
  • {tool, user}:还会注入 User 角色消息的 harness,例如终端输出(如 Terminus-2)或解析器重试提示词。
  • {tool, user, system}:还会在任务中途注入 System 角色提醒的 harness。

Deepseek-v3.2 和 Deepseek-v4 目前都只支持 {tool} 这一接口面;扩展它们——就像接入任何新模型一样——通常只需要一个固定的 Jinja 模板加上一处小小的 merge_tokens 覆盖。这种低成本正是关键所在:TITO 让每一次 rollout 在训练时都保持逐 bit 精确,同时扩展成本低廉,因此不会遗漏任何一个 token。

如果你想在 miles 中试用 TITO,请查看此处的文档。

来源:LMSYS:Blog(Chatbot Arena 团队) · lmsys.org