No Token Left Behind: Demystifying Token-In-Token-Out in Miles

发表时间: 2026-06 · Blog post by Miles Team: Jiajun Li, Yuzhen Zhou, Shi Dong, Yanbin Jiang, Mao Cheng, Yusheng Su, Yueming Yuan, Zhichen Zeng, Banghua Zhu (lmsys.org)

原文: https://lmsys.org/blog/2026-05-13-no-token-left-behind

作者/机构:Miles Team: Jiajun Li, Yuzhen Zhou, Shi Dong, Yanbin Jiang, Mao Cheng, Yusheng Su, Yueming Yuan, Zhichen Zeng, Banghua Zhu

速读

一句话结论
本文在 Miles 框架中提出并实现了 Token-In-Token-Out (TITO) 原则,通过在多轮智能体强化学习中维持严格的增量追加机制,确保了训练和推理阶段的 Token 序列实现比特级一致,从而在保证策略同分布数学正确性的同时,将长轨迹任务的训练计算成本降低了一个数量级。

要解决什么问题
在智能体强化学习中,模型完成一个任务通常需要与环境进行 30 到 50 轮的交互。如果将每一轮作为独立的训练样本(One Sample Per Turn),计算成本极高;但如果将所有轮次拼接成一个长序列(One Sample Per Task),就会面临严重的训练与推理不匹配问题。这种不匹配破坏了强化学习的同分布(On-Policy)假设:如果训练器看到的历史上下文 $\mathbf{x}$ 与推理引擎实际生成的上下文 $\tilde{\mathbf{x}}$ 存在微小差异,即使模型权重完全相同,条件概率 $\pi(x_t|\mathbf{x})$ 也会与 $\pi(x_t|\tilde{\mathbf{x}})$ 产生巨大分歧,导致梯度更新异常并破坏训练稳定性。这种上下文差异主要由三种隐蔽机制引发:第一是反分词再分词的失真,分词器的编码是单射而解码是多射,模型生成的非标准 Token 序列(如 Hel 加 lo)在解码为文本再重新分词后,会被合并为单个标准 Token(如 Hello),导致原始采样的 ID 永久丢失;第二是聊天模板的截断,Qwen3 和 Kimi K2 等推理模型的模板存在“思考截断边界”,当外部框架在任务中途注入新的用户消息(如终端报错)时,模板会静默抹除该边界前的历史思考内容;第三是有损的模板重渲染,引擎在解析并重新序列化 JSON 格式的工具调用时,会引入额外的空格或换行,导致相同语义的文本产生完全不同的 Token ID。

怎么做的
核心思路是确立 Token-In-Token-Out (TITO) 原则,即强制要求第 $n-1$ 轮的完整 Token 序列(提示词加回复)必须是第 $n$ 轮提示词序列的比特级完美前缀。为了绕开上述重分词和模板渲染带来的卡点,Miles 框架通过四个核心部件实现了严格的增量追加机制。首先是推理会话服务端(Inference session server),它为每条轨迹维护一个不断增长的 Token 缓存区,所有轮次的交互都在此缓存区上原地追加,直接保留每个样本精确的 Token 级信息(如对数概率)。其次是三级追加写入设计:在消息列表层,新消息只在尾部追加,绝不修改历史字典;在模板渲染层,提供禁用了思考截断的定制 Jinja 模板(设置 clear_thinking: false),并根据允许追加的角色自动选择前缀稳定的模板配置;在 Token 序列层,彻底放弃对全局提示词的重新分词,只对新增消息进行分词。第三是可插拔的 TITO 分词器,它负责计算需要拼接到缓存区的增量 Token。具体做法是构建一个最小上下文,分别在包含和不包含新消息的情况下渲染模板,对两者的字节差异进行编码。此外,它还负责处理拼接点补丁,例如 Qwen3 停止于 <|im_end|> 但模板要求以 <|im_end|>\n 结尾,分词器会先补齐换行符再拼接;而 GLM-4.7 可能会在拼接处留下错误的角色边界 Token,分词器会将其覆写为正确 Token,并将该位置的损失掩码清零以防被计入训练。最后是 Token 序列比较器,用于在每次 rollout 后进行校验。它采用混合检查策略:先对消息边界的特殊 Token 进行严格的结构比对,再将特殊 Token 之间的片段解码为文本进行字符串比对,以此放行无害的重分词差异,同时精准拦截破坏 TITO 原则的真实 Bug。

效果如何
实验在 CPU 往返测试和真实的 SGLang GPU 会话中进行,验证了该机制在 R3、OPD 以及 zero-KL 对齐等算法中所必需的 Token 精确性。对比基线为传统的“每轮一独立样本(One Sample Per Turn)”路线,TITO 代表的“每任务一拼接样本(One Sample Per Task)”路线在典型的 SWE-Bench 类任务中,能在摄入同等信息量的前提下,将计算成本降低整整一个数量级。该方案目前已原生支持 Qwen(Qwen3 等)、GLM(GLM-4.7 等)、Kimi(Kimi K2 等)、Nemotron-3、Minimax 以及 Deepseek(Deepseek-v3.2、Deepseek-v4)等模型家族。作者也指出了该方法的局限性:由于模型生成的助手文本可能偏离模板预期(例如未闭合的 <think> 标签、工具调用边界的异常空格等),助手文本区域的 Token 错位是不可避免的,系统目前只能将其记录为非致命错误;此外,Deepseek 系列模型目前仅支持 {tool} 角色的中途注入,尚未完全覆盖用户和系统消息的复杂注入场景。

一、主要贡献

在智能体强化学习(Agentic RL)中,一次 rollout 并非单一的生成过程,而是一系列模型调用、工具输出、框架消息注入以及恢复生成的调用链。在这个过程中,一个关键的训练-推理不匹配(training-inference mismatch)来源是:训练器在评估时,是否使用了与推理引擎在 rollout 期间消耗和生成的完全相同的 token 序列。为了解决这一核心问题,本文提出了 Token-In-Token-Out (TITO) 设计原则,并详细阐述了该原则在 Miles 框架中的具体实例化方案。

在智能体 rollout 循环中,模型首先接收任务描述并生成 token(可能包含推理过程和工具调用),随后智能体运行时解析工具调用,将其发送给外部环境或工具后端,并将结果作为新的观察返回。模型接着从该观察继续生成,这一循环重复直至任务完成。该过程涉及对推理引擎的多次独立调用,通常被称为轮次(turns)。如果对于所有轮次 $n$,第 $n-1$ 轮的完整 token 序列(提示词 + 响应)是第 $n$ 轮提示词 token 序列的完美按位匹配前缀(bit-perfect prefix),则称 TITO 原则得到了满足。

TITO定义示意图
TITO定义示意图

二、背景知识与设计挑战

TITO在训练效率上的重要性
在智能体强化学习中,单个任务可能包含数十个对话轮次。为了向强化学习训练器打包数据,本质上有两种选择:一是将每一轮作为一个独立的训练样本(One Sample Per Turn);二是将所有轮次“粘合”成一个单一的、连续的序列,即每个任务作为一个样本(One Sample Per Task)。在第一种方案中,训练器接收的样本数与轨迹中的轮数相等;而在第二种方案中,无论轮数多少,训练器每次任务实例化只接收一个样本。对于典型的类似 SWE-Bench 的任务,一条轨迹包含 30 到 50 轮。这意味着为了摄取相同的信息量,第二种方案的计算成本比第一种方案低一个数量级。这种巨大的计算成本缩减使得单任务单样本方案在扩展智能体强化学习训练时极具吸引力。2026年8月18日的更新指出,Miles 正在基于 TITO 为诸如 Claude Code 和 Codex 等黑盒智能体框架开发训练方案。跨模型家族的 CPU 往返和真实的 SGLang GPU 会话验证了 R3、OPD 和零 KL 对齐所需的 token 精确性。子智能体和上下文压缩使得每个任务的轨迹计数动态化。Session Server V2 记录了包含精确 token ID、对数概率(logprobs)和损失掩码的完整树,然后尽力将样本与其掩码合并;可能会保留多个样本。训练阶段会在由此产生的动态全局批次上对损失进行归一化。

保持同策略(On-Policyness)的数学正确性
为了确保训练样本符合同策略要求,每一个被采样的 token 都必须在训练器中按照其在 rollout 期间生成时的相同条件分布进行评估。在 Transformer 模型中,该条件分布完全依赖于 token 的前置上下文。如果违反了 TITO 原则,可能会存在某个 token $x_t$,在训练器中,模型基于前置序列 $\mathbf{x}$ 评估 $x_t$;而在推理引擎中,模型则是基于一个略微不同的前置序列 $\tilde{\mathbf{x}}$ 采样 $x_t$。即使训练器和推理引擎共享完全相同的权重,条件概率 $\pi(x_t|\mathbf{x})$ 也可能与 $\pi(x_t|\tilde{\mathbf{x}})$ 发生显著偏离。这种差异最终会导致参数更新不稳定,从而危及强化学习训练的稳定性。

反序列化与重新序列化导致的不匹配(场景1)
在多轮强化学习 rollout 中,一种常见的做法是将模型生成的 token 反序列化(detokenize)为字符串进行存储,随后在构建第 $n$ 轮的提示词时重新对其进行序列化(retokenize)。这种做法极易破坏 TITO 原则,因为模型生成的 token 不一定能在一个反序列化-重新序列化的往返过程中无损存活。其根本原因在于分词器编码文本和模型生成 token 之间存在不对称性:encode(文本到 token)是一对一的,对于给定的输入字符串,分词器总是选择一种标准的分割方式(通常是贪婪/最长匹配);而 decode(token 到文本)是多对一的,多个不同的 token 序列可以解码成完全相同的字符串。模型能够且有时确实会生成有效但非标准的 token 序列。例如,假设模型生成了两个独立的 token Hel(3) 和 lo(7),解码它们会产生字符串 "Hello"。然而,当重新编码 "Hello" 时,分词器会规范地将其编码为单个 token Hello(4)。原始的 Hel(3) + lo(7) 序列将永远丢失,导致训练器评估一个模型实际上从未采样过的 token 序列。

反序列化-重新序列化不匹配
反序列化-重新序列化不匹配

聊天模板裁剪推理过程(场景2)
聊天模板(Chat templates)负责将类似 JSON 的消息列表转换为单个提示字符串以发送给推理引擎。一些推理模型的模板引入了“截断思考边界”(cut-thinking boundary):即对话中的一个节点,在此节点之前历史助手的推理过程会从渲染的提示词中被移除。在 Qwen3 和 Kimi K2 等推理模型的默认聊天模板中,这个边界由最后一个 User 消息决定。当模板渲染对话时,它会丢弃出现在最后一个 User 消息之前的 Assistant 推理,仅保留该边界之后的推理。然而,智能体框架经常在任务中途注入 User 消息——例如,Terminus-2 框架使用 User 角色输出终端信息,而其他框架使用它进行诸如“解析失败”的引擎重试。每一次注入都会将截断思考边界向前推进,悄无声息地擦除模型实际采样的推理过程,打破了轮次之间的完美按位前缀匹配。

截断思考边界破坏
截断思考边界破坏

聊天模板重新渲染导致的信息丢失(场景3)
许多推理引擎接受一个消息列表,并在每次调用时重新应用聊天模板和分词器来构建提示词。这种做法虽然方便,但非常危险:聊天模板在字符串级别进行工作(包括去除空格、处理转义、重新打包推理内容),因此它们为给定消息生成的 token ID 可能取决于消息渲染的时间和伴随的上下文。因此,在消息级别重新应用聊天模板也会引入意想不到的文本漂移。一个具体的失败模式是:在第 $n-1$ 轮中,助手发出一个工具调用,其流式传输的 token 解码为一个紧凑的 JSON 主体——逗号或冒号后没有空格,键的顺序由模型选择:

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

引擎解析这个字符串并将其作为结构化的 tool_calls 字段存储在助手消息中。在第 $n$ 轮中,当模板重新渲染对话时,它通过 tojson 过滤器将 tool_calls 序列化回去。因为这种先解析后序列化的往返过程固有地丢弃了原始的字节级格式(如空格、换行符),过滤器会应用其自己的默认间距并输出:

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

此时每个逗号和冒号后面多出了额外的空格。虽然语义相同,但字节不同,生成的 token ID 也不同,这彻底打破了完美按位前缀匹配。

三、方法细节

推理会话服务器的设计与作用
一个推理会话(inference session)代表了单条轨迹与推理引擎的交互过程,即属于同一任务的轮次序列共享一个不断增长的 token 缓冲区。Miles 构建了一个轻量级的推理会话服务器层,通过会话 ID(session id)键值来维护每条轨迹的状态。在每个 ID 下,服务器持有一个不断增长的 token 缓冲区 P,该缓冲区在每一轮都会进行原地追加(appended in place)。该 token 缓冲区保留了每个样本精确的 token 级别信息(包括对数概率 logprobs、路由专家 routed experts),因此它可以被直接发送到训练环节。

推理会话服务器架构
推理会话服务器架构

在三个层面确保仅追加(Append-only)机制
仅追加意味着每一轮都在扩展上一轮的数据,而绝对不重写任何早期的字节。Miles 在三个层面上强制执行此机制:

  • 消息列表层面(Level 1):第 $n$ 轮的消息列表通过在尾部附加新消息来扩展第 $n-1$ 轮的列表,早期的消息字典永远不会发生突变。
  • 聊天模板渲染层面(Level 2):聊天模板可能会通过修剪早期内容或根据框架追加的消息角色进行不同的渲染来破坏仅追加原则。为了防止修剪,Miles 提供了固定的 Jinja 模板,通过传入 clear_thinking: false 关键字参数来禁用截断思考,从而在不同轮次间保留历史推理记录。为了防止依赖于角色的渲染漂移,用户需要通过 --tito-allowed-append-roles 声明预期的追加角色,Miles 会自动为该角色集选择一个前缀稳定的模板配置。
  • Token序列层面(Level 3):对上述渲染结果进行分词必须产生符合 TITO 定义的完美按位匹配 token 前缀。即使在 Level 2 成立的情况下,简单的重新分词也会打破这一点。Miles 的策略是完全避免重新分词:在每一轮中,只有新追加的消息会被分词,并且生成的 ID 被原地追加。可插拔的 TITO 分词器正是实现这种仅追加分词的核心组件。

可插拔的 TITO 分词器实现机制
TITO 分词器负责在框架追加新的非助手消息时,扩展由推理会话服务器维护的每轨迹 token 缓冲区 P。它负责计算要拼接到 P 上的增量 token,以及某些模型在拼接点处需要的边界补丁。其基本思想是基于虚拟前缀的增量分词(dummy-prefix incremental tokenize):首先构建一个合成的最小上下文;接着使用新消息和不使用新消息分别渲染一次聊天模板;最后对两者的字节差异进行编码。产生的增量给出了新的序列化内容,Miles 从中推导出要追加到 P 的增量 token。例如,假设 P 已经保存了直到第 $n-1$ 轮的分词前缀,并且框架现在追加了一个工具响应:

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。

针对特定模型的拼接点补丁(Splice-point patches)
上述基本增量分词配方假设引擎放入 P 中的 token 已经与规范模板在该点渲染的内容完全匹配。然而,真实模型通常会违反这一假设,需要在拼接点进行针对特定模型的小补丁修复,Miles 通过其 TITO 分词器中的钩子(hook)来处理这些情况:

  • Qwen3模型修复:模型在生成 <|im_end|> 处停止,但规范的聊天模板在每轮结束时带有 <|im_end|>\n,因此 P 缺少尾随的换行符 token。Miles 会在拼接下一条消息的增量 token 之前,主动将缺失的 \n token 追加到 P 中。
  • GLM-4.7模型修复:该模型采样 <|user|> 或 <|observation|> 作为停止和下一条消息开始的 token,但智能体框架接下来可能会注入不同的角色,从而在 P 的末尾留下错误的边界 token。Miles 在拼接增量 token 之前,会用传入角色的正确边界 token 覆盖那个错误的边界 token,同时将该被替换位置的损失掩码(loss mask)置为零,以确保该交换操作不会被用于训练。

Qwen3拼接点补丁
GLM-4.7拼接点补丁

通过 Token 序列比较器进行正确性验证
在每次 rollout 之后,系统必须检查 token 缓冲区 P 是否与从头开始渲染完整消息列表时聊天模板生成的内容相匹配。如果 P 存在差异,说明训练器正在读取模型实际上从未见过的上下文——这意味着训练会悄无声息地偏离策略。Miles 提供了 TokenSeqComparator 来运行此检查。设 actual 为 P,expected 为通过聊天模板重新渲染消息列表得到的 token。系统不能仅仅逐个 token 进行比较:因为相同的文本可以编码为不同的 token ID,严格的相等性检查会将无害的重新分词标记为失败。为了跳过这些无害的重新分词同时捕获真正的错误,Miles 采用了一种混合的文本-token检查方法:

  1. 结构检查(Structural check):在消息边界特殊 token(例如 Qwen3 的 <|im_start|> / <|im_end|>,但不包括 <think> / </think>)处拆分 actual 和 expected,并严格验证两个特殊 token 序列是否匹配。
  2. 文本检查(Textual check):将特殊 token 之间的片段解码回文本并比较字符串,这样解码为相同文本的重新分词差异就能顺利通过检查。

尽管有上述检查机制,模型仍然可能产生与聊天模板预期格式相背离的输出。助手文本一致性可能被破坏的几种常见异常情况包括:工具调用边界周围存在空格或换行符;未闭合的 <think> 块;<think> 标签周围被剥离或产生了额外的换行符。

助手文本异常
助手文本异常

由于此类情况的存在,助手 token 中的不匹配是不可避免的;Miles 会记录它们并将其标记为非关键(non-critical)。所有其他不匹配——即特殊 token 序列和非助手文本——必须严格保持为零,因为这些区域是确定性的,任何差异都表明 TITO 管道中存在真正的错误。Miles 部署了两个验证脚本跨所有支持的(模型,追加角色集)组合运行此比较器:一个是在渲染的 token 序列上运行的 CPU/快速层,另一个是在真实模型推理下重复检查的 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 经验证可处理智能体框架在第一个助手轮次之后可能追加的以下消息角色组合:

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

目前 Deepseek-v3.2 和 Deepseek-v4 仅支持 {tool} 接口。

五、结论

Miles 框架通过引入 TITO 原则及其四核心组件(推理会话服务器、三级仅追加机制、可插拔 TITO 分词器、混合验证器),彻底解决了智能体强化学习中训练与推理环境由于 token 不匹配导致的策略偏离问题。拓宽现有模型的支持范围或接入新模型,通常只需要配置一个固定的 Jinja 模板加上一个少量的 merge_tokens 覆盖代码。这种低成本扩展性正是 TITO 设计的核心目标:在保持接入成本低廉的同时,确保每一条用于训练的 rollout 轨迹都实现完美的按位匹配,真正做到“不遗漏任何一个 token”。