← 进阶 vibe coding模型先把文本变成 token

免费文章 · 02 理解模型怎样生成回答

模型先把文本变成 token



  当我们开始把更长的文档、代码和检索结果交给模型,很快就会遇到一个问题:到底还能放多少? 为什么看起来差不多长的两段内容,占用的上下文却不一样?要回答这些问题,只看字数并不够,因为模型真正处理的并不是“字”,而是 token。

  理解 token,我们才能判断一份材料大概会占多少上下文、什么时候需要截取,以及应该给模型的回答留出多少空间。它也是后面理解上下文窗口、API 用量和成本的基础。

  token(词元)是模型处理文本的切分单位。 tokenizer(分词器)会先把文本切成 token 并转换为编号,模型再将这些编号映射为向量进行计算,最后把生成的 token 还原成文字。

  这一节,我们先从真实的切分结果开始,看看中文、英文和代码分别会被怎样处理。

token 是切分单位,ID 是它的编号

  用 Qwen3-0.6B 配套的 tokenizer 处理 你好,世界!,会得到下面的对应关系:

文本片段

你好

,

世界

!

token ID

108386

3837

99489

6313

  六个字符最终变成了四个 token。每个 token 都会在这套 tokenizer 的词表中对应一个编号。比如 108386 只是“你好”的编号,数字本身并不表示它的含义、重要程度或与其他词的相似关系。

  到这里,模型还没有开始理解这句话。tokenizer 做的只是把文本转换成模型可以继续处理的形式;真正的计算,要从这些编号进一步转换为向量之后才开始。

为什么不直接按汉字或单词切分

  既然 token 是切分单位,一个自然的问题是:为什么不干脆按汉字或单词来切?

  如果把每个完整词都收入词表,词形变化、新名词和各种组合会让词表越来越大;如果全部拆成单个字符,词表虽然小了,但同样一段内容又会变成更长的序列。两种做法都有代价。

  因此,常见的做法是在两者之间取舍:常见的片段可以保留得长一些,不常见的内容则拆成更小的片段。BPE(字节对编码)就是一种这样的做法:构建词表时不断合并高频的相邻片段,并把这些合并结果记录下来;真正处理文本时,tokenizer 只是按照已经形成的词表和规则进行切分,并不会临时重新学习。

  实际切分还会受到预处理规则影响。比如数字是否先拆开、空格是否与后面的词一起组成片段,都可能改变结果。所以,token 的边界并不一定和我们理解中的“词”完全一致。

同一段文本,不同模型可能切得不同

  前面说到,token 的切分取决于词表和规则。换一套 tokenizer,同一段文本的切分结果也可能不同。比如输入 1234567890,Qwen3-0.6B 会把数字逐位切开,得到十个 token;DeepSeek-V3 则会切成 123 · 456 · 789 · 0,只得到四个。

  差异来自它们不同的预切分规则:Qwen 会先把数字逐位分开,DeepSeek 则允许一至三位数字组成一个片段,再继续应用 BPE。也就是说,相同的字数,并不意味着相同的 token 数。真正计算输入长度时,要使用目标模型配套的 tokenizer。

  英文和代码也是一样。Qwen3-0.6B 会把 Hello, world! 切成四个 token,其中 world 连同前面的空格组成一个片段;const total = price * 2; 则会得到八个 token。tokenizer 看到的仍然是一串文本,并不会因为它是代码,就自动按照语法结构来切分。

  所以,某段文本 token 更少,只能说明它在这套 tokenizer 下占用的序列更短。模型能力、使用成本和实际效果,还需要结合其他因素判断。

运行分词实验

沿用已准备的课程实验仓库,打开分词实验。

  1. 点击“数字”,选择“对比两种分词器”,再点“查看分词”。
  2. 对照两组token、编号与完整解码,核对前面两张图中的结果。
  3. 换成自己的文本再运行,比较切分与解码结果。

分词在浏览器本地执行,不套用聊天模板或自动添加特殊token;使用随仓库提供的两套公开规则,不代表在线产品的全部模型。

一个字符为什么会占多个 token

  一些 tokenizer 以 UTF-8 字节为基础,再把字节合并成片段。这样可以表示罕见汉字或新符号,但 token 边界可能落在一个字符的字节序列中间。

  以 龘🙂 为例,Qwen3-0.6B 把“龘”切成两个 token,“🙂”是一个;DeepSeek-V3 则把两者各切成两个。单独解码一个不完整的字节片段,可能显示为 �;将所需字节接起来后,才得到完整字符。实验页面会把跨 token 的字符合并显示,并标明实际占用数量,展开字节明细可以看到原边界。

  完整解码也要考虑 tokenizer 的规范化设置。本实验的 Qwen 配置包含 NFC:复制 café(末尾是 U+0065 U+0301,即 e 加组合重音符)到输入框,会解码为 café(末尾是预组合字符 é)。视觉内容相同,Unicode 码点序列可以不同。

纯文本计数不等于整次请求的用量

  不过,我们刚才数到的只是文本本身。真正发给聊天模型时,除了眼前这段文字,系统提示、历史对话、检索结果、工具说明等内容,也可能一起进入这次请求。因此,文本有多少 token,并不等于整次请求就用了多少 token。

  实际估算时,需要使用目标模型对应的 tokenizer 和消息格式;请求完成后,则可以通过服务返回的 usage 查看实际用量。

  这里还容易产生一个误解:把一大段内容拆成几条消息,并不会自动减少占用。只要这些内容仍然一起出现在当前请求里,它们就还在上下文窗口中。真正想减少输入,需要减少模型实际看到的内容,比如筛选相关段落、压缩较早的对话,或者舍弃已经不再需要的信息。

  同时,上下文窗口还要给模型的回答留出空间。因此,我们真正关心的不是“这一段有多少字”,而是这一整次推理,需要放进多少 token,又准备让模型生成多少 token。

一个空格会多占一个 token 吗?

  在实验页分别输入 world 和 world(第二个开头有一个空格),观察 Qwen3-0.6B 的 token 数与 ID。先预测:增加一个字符,是否一定增加一个 token?

查看答案与解析

  两者都是一个 token,但 ID 分别为 14615 和 1879。前导空格与单词共同构成了另一个词表片段;因此,字符数变了,token 数可以不变。换成 DeepSeek-V3,两者也各占一个 token,但使用的是它自己的编号。

  到这里,文本已经被转换成了一串 token 和对应的编号。接下来要看的,是模型怎样把这些离散的编号变成可以参与计算的向量。

讨论

登录后参与讨论