词元:模型眼里的文字
用 DeepSeek 和 OpenAI 的分词器实际切一段中文、英文、文言文和代码,看模型到底把文字切成了什么,以及这为什么决定了价格,也解释了模型为什么数不清字数。
- 约 35 分钟
- 难度:入门
- 实测:2026-09-14 deepseek-flash,DeepSeek V4 分词器,tiktoken o200k_base
大模型按词元(token)收费,上下文窗口按词元计算,输出上限也按词元设置。上一课你已经见过 usage 里的 prompt_tokens 和 completion_tokens。可词元到底是什么?一个汉字是一个词元吗?一个英文单词呢?
这不只是算账的问题。模型有一些奇怪的毛病,比如有时数不清一句话有几个字,根源也在词元上。这一课我们把文字实际切开来看。
模型读不到文字,只读得到数字
神经网络只能处理数字。所以文字进入模型之前,要先经过一个叫分词器(tokenizer)的程序:它有一张固定的词表,把文字切成词表里有的片段,每个片段对应一个编号。模型看到的是一串编号,输出的也是编号,最后再由分词器翻译回文字。
这些片段就是词元。它可能是一个字、一个词、半个英文单词,也可能是一个标点或者几个空格。DeepSeek 的词表里有 129,280 个词元,OpenAI 的 GPT-4o 这一代用的 o200k_base 词表有 200,019 个。
词表是在训练模型之前,根据大量文本统计出来的:经常一起出现的字符组合,就合并成一个词元。第 09 模块第 1 课会从零实现这个算法。现在只需要记住结论:常见的组合会被合成一个词元,罕见的会被拆碎。
实际切一下
DeepSeek 公开了它的分词器,可以下载到本地使用。下载地址,解压后得到一个 deepseek_v4_tokenizer 目录。再装两个包:
uv add tokenizers tiktoken
tokenizers 用来加载 DeepSeek 的分词器文件,tiktoken 是 OpenAI 的分词器。下面的脚本把同样几段文字交给两个分词器,打印切出来的每个片段:
import tiktoken
from tokenizers import Tokenizer
deepseek = Tokenizer.from_file("deepseek_v4_tokenizer/tokenizer.json")
openai_enc = tiktoken.get_encoding("o200k_base") # GPT-4o 等 OpenAI 模型用的分词器
samples = [
"人工智能正在改变软件开发的方式,越来越多的程序员开始用大模型写代码。",
"Artificial intelligence is changing how software is built, and more programmers now write code with large language models.",
"学而时习之,不亦说乎?有朋自远方来,不亦乐乎?",
"def add(a, b):\n return a + b\n",
"3.1415926535897932384626",
"😀",
]
def deepseek_pieces(text):
ids = deepseek.encode(text, add_special_tokens=False).ids
return [deepseek.decode([i]) for i in ids]
def openai_pieces(text):
pieces = []
for i in openai_enc.encode(text):
raw = openai_enc.decode_single_token_bytes(i)
# 一个汉字或表情可能被拆成几个字节,单独拿出来不是合法的 UTF-8
pieces.append(raw.decode("utf-8", errors="replace"))
return pieces
for text in samples:
ds, oa = deepseek_pieces(text), openai_pieces(text)
print(f"原文({len(text)} 个字符):{text!r}")
print(f" DeepSeek {len(ds):3d} 个词元:{ds}")
print(f" OpenAI {len(oa):3d} 个词元:{oa}")
print()
运行结果:
原文(34 个字符):'人工智能正在改变软件开发的方式,越来越多的程序员开始用大模型写代码。'
DeepSeek 15 个词元:['人工智能', '正在', '改变', '软件开发', '的方式', ',', '越来越多的', '程序员', '开始', '用', '大', '模型', '写', '代码', '。']
OpenAI 21 个词元:['人工', '智能', '正在', '改变', '软件', '开发', '的', '方式', ',', '越来越', '多', '的', '程序', '员', '开始', '用', '大', '模型', '写', '代码', '。']
原文(122 个字符):'Artificial intelligence is changing how software is built, and more programmers now write code with large language models.'
DeepSeek 20 个词元:['Artificial', ' intelligence', ' is', ' changing', ' how', ' software', ' is', ' built', ',', ' and', ' more', ' programmers', ' now', ' write', ' code', ' with', ' large', ' language', ' models', '.']
OpenAI 20 个词元:['Artificial', ' intelligence', ' is', ' changing', ' how', ' software', ' is', ' built', ',', ' and', ' more', ' programmers', ' now', ' write', ' code', ' with', ' large', ' language', ' models', '.']
原文(23 个字符):'学而时习之,不亦说乎?有朋自远方来,不亦乐乎?'
DeepSeek 22 个词元:['学', '而', '时', '习', '之', ',', '不', '亦', '说', '乎', '?', '有', '朋', '自', '远方', '来', ',', '不', '亦', '乐', '乎', '?']
OpenAI 21 个词元:['学', '而', '时', '习', '之', ',不', '亦', '说', '乎', '?', '有', '朋', '自', '远', '方', '来', ',不', '亦', '乐', '乎', '?']
原文(32 个字符):'def add(a, b):\n return a + b\n'
DeepSeek 12 个词元:['def', ' add', '(a', ',', ' b', '):\n', ' ', ' return', ' a', ' +', ' b', '\n']
OpenAI 12 个词元:['def', ' add', '(a', ',', ' b', '):\n', ' ', ' return', ' a', ' +', ' b', '\n']
原文(24 个字符):'3.1415926535897932384626'
DeepSeek 10 个词元:['3', '.', '141', '592', '653', '589', '793', '238', '462', '6']
OpenAI 10 个词元:['3', '.', '141', '592', '653', '589', '793', '238', '462', '6']
原文(1 个字符):'😀'
DeepSeek 2 个词元:['�', '�']
OpenAI 1 个词元:['😀']
这一屏输出里有好几个值得细看的地方。
现代汉语,DeepSeek 切得更粗。"人工智能"、"软件开发"、"越来越多的"在 DeepSeek 的词表里都是一个词元,同一句话它用 15 个词元,OpenAI 用 21 个。词表是按训练语料统计的,DeepSeek 的语料里中文更多,所以更多的中文词组被合并了。同样的中文内容,词元越少,花钱越少,一次能塞进上下文的内容也越多。
英文,两家一模一样。常见的英文单词基本都是一个词元,而且空格是跟着后面的单词走的:' intelligence' 前面带了一个空格。122 个字符切成 20 个词元,差不多一个单词一个。
文言文,几乎一个字一个词元。"学而时习之"在两个词表里都是五个单字。现代汉语里常见的是"学习"、"时间"这样的组合,"时习"、"不亦"这种组合太少见,没被合并。同样是 23 个字,文言文用的词元比现代汉语多一倍。
代码里的空格也是钱。缩进的四个空格里,三个空格成了一个单独的词元。Python 代码缩进层级越深,空格占的词元越多。
数字被切成三位一段。3.1415926... 被切成 141、592、653 这样的块。模型看到的不是一个完整的数,而是几个"三位数片段"。这是模型做多位数运算容易出错的原因之一:竖式计算要逐位对齐,而它的输入根本不是逐位的。
表情符号可能被拆成字节。DeepSeek 把 😀 切成了两个词元,单独解码出来都是乱码 �,因为每个词元只是这个表情编码的一部分字节,拼在一起才是完整的字符。
算钱时怎么估计词元数
最准的办法有两个:用官方分词器在本地数,或者直接看 API 返回的 usage。
不方便的时候,可以粗略估计。DeepSeek 官方文档给的换算是:1 个英文字符约 0.3 个词元,1 个中文字符约 0.6 个词元。我用 API 实测了几段文字,结果比官方的数字低:
| 文本 | 字符数 | 词元数 | 每字符词元数 |
|---|---|---|---|
| 现代汉语(上面那句) | 34 | 15 | 0.44 |
| 英文(上面那句) | 122 | 20 | 0.16 |
| 文言文(上面那句) | 23 | 22 | 0.96 |
| Python 代码 | 32 | 12 | 0.38 |
| 5 个表情 | 5 | 10 | 2.00 |
测法是这样的:同一个问题设置 max_tokens=1,只看 prompt_tokens。先发一个只有字母 a 的消息,记下它的输入词元数作为基准(格式标记的开销),再发要测的文字,两者相减再加一,就是这段文字本身的词元数。
def count_tokens(text):
r = client.chat.completions.create(
model="deepseek-flash",
messages=[{"role": "user", "content": text}],
max_tokens=1,
extra_body={"thinking": {"type": "disabled"}},
)
return r.usage.prompt_tokens
base = count_tokens("a") # 格式标记加上字母 a 本身,我测到的是 5
print(count_tokens("学而时习之,不亦说乎?有朋自远方来,不亦乐乎?") - base + 1)
官方文档的数字偏保守,拿来做预算正好,不容易超支。但要记住,词元数取决于内容:古文、生僻字、表情、少见的语言都会比日常中文"贵"。
为什么模型数不清字数
有个出名的例子:问模型 "strawberry 里有几个字母 r",很多模型答 2,正确答案是 3。
看看分词器怎么处理这个词:DeepSeek 把单独的 strawberry 切成 st、raw、berry 三块,而前面带空格的 strawberry(在句子中间出现时就是这样)则是一整个词元。模型拿到的是一个编号(79430),它并不直接"看见"里面的字母。它知道这个词里有几个 r,只能靠训练时见过类似的讨论。
不过这个例子太出名了,现在的模型大多见过。我让 deepseek-flash 回答了几种问法,每种关掉思考和开着思考各问三次:
| 问题 | 关掉思考 | 开着思考 |
|---|---|---|
| 英文问 strawberry 里有几个 r | 3, 3, 3 | 3, 3, 3 |
| 中文问 strawberry 里有几个字母 r | 2, 3, 3 | 3, 3, 3 |
| 中文问 raspberry 里有几个字母 r | 3, 2, 3 | 3, 3, 3 |
| "秋天的叶子一片片落下"这句话有几个字 | 10, 10, 11 | 10, 10, 10 |
那个老问题用英文问已经全对了,换成中文问、换一个词、改成数汉字,关掉思考时就会偶尔出错。开着思考全对,因为模型在思考过程里会把单词一个字母一个字母地写出来再数,相当于自己把词元拆开了。
实际开发中,凡是需要精确计数的事情,比如字数统计、字符串长度、文本截断,别交给模型,用代码做。len("秋天的叶子一片片落下") 永远等于 10。
常见问题
不同模型的词元数能直接比较吗? 不能。同样一段中文,DeepSeek 和 OpenAI 的词元数差了三成。比较两个模型的价格时,要用同一段真实的业务文本,分别算出词元数再乘以各自的单价,不能只比单价。
我在代码里用 tiktoken 数 DeepSeek 的词元,可以吗? 英文和代码基本一样,中文会偏多。要准确就用 DeepSeek 自己的分词器。
练习
- 把你的名字、你所在城市的名字、一句方言,交给
tokens.py里的两个分词器,看分别切成了什么样。 - 用
count_tokens测一测:同一段内容,写成中文和写成英文,哪个词元更多?换成你工作中真实的一段文字试试。 - 找一段 JSON 数据(比如一个 API 的返回结果),数一数它的词元数。再把它压缩成一行、去掉所有空格,重新数一次,词元数少了多少?这在需要把大量数据塞给模型时很有用。
自测
1. 同样一句现代汉语,DeepSeek 用的词元比 OpenAI 的分词器少。为什么?这对你有什么影响?
分词器的词表是从训练语料里统计出来的,DeepSeek 的语料中文比例更高,所以更多的中文词组被合并成了一个词元。对你的影响是:同样的中文内容,在 DeepSeek 上花的钱更少,一次能放进上下文的内容也更多。比较模型价格时,要用真实文本算出词元数再比。
2. 模型为什么容易数错一个单词里有几个某个字母?
模型看到的是词元编号,不是字母。一个常见单词往往就是一个词元,模型没法直接"看见"里面的字母,只能凭训练时的记忆回答。开启思考后,模型会先把单词逐字母写出来再数,准确率就高多了。需要精确计数时,应该用代码而不是模型。
3. 为什么文言文比同样字数的现代汉语"贵"?
词表合并的是训练语料里常见的字符组合。现代汉语的常用词("人工智能"、"程序员")经常出现,被合成了一个词元;文言文里的组合("时习"、"不亦")很少见,没有被合并,只能一个字一个词元。词元多,费用就高。