免费文章 · 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 下占用的序列更短。模型能力、使用成本和实际效果,还需要结合其他因素判断。
一个字符为什么会占多个 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。
在实验页分别输入 world 和 world(第二个开头有一个空格),观察 Qwen3-0.6B 的 token 数与 ID。先预测:增加一个字符,是否一定增加一个 token?
查看答案与解析
两者都是一个 token,但 ID 分别为 14615 和 1879。前导空格与单词共同构成了另一个词表片段;因此,字符数变了,token 数可以不变。换成 DeepSeek-V3,两者也各占一个 token,但使用的是它自己的编号。
到这里,文本已经被转换成了一串 token 和对应的编号。接下来要看的,是模型怎样把这些离散的编号变成可以参与计算的向量。
讨论
登录后参与讨论