模块 01 · 第 1 课

词元:模型眼里的文字

用 DeepSeek 和 OpenAI 的分词器实际切一段中文、英文、文言文和代码,看模型到底把文字切成了什么,以及这为什么决定了价格,也解释了模型为什么数不清字数。

  • 约 35 分钟
  • 难度:入门
  • 实测:2026-09-14 deepseek-flash,DeepSeek V4 分词器,tiktoken o200k_base

大模型按词元(token)收费,上下文窗口按词元计算,输出上限也按词元设置。上一课你已经见过 usage 里的 prompt_tokenscompletion_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... 被切成 141592653 这样的块。模型看到的不是一个完整的数,而是几个"三位数片段"。这是模型做多位数运算容易出错的原因之一:竖式计算要逐位对齐,而它的输入根本不是逐位的。

表情符号可能被拆成字节。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 切成 strawberry 三块,而前面带空格的 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 自己的分词器。

练习

  1. 把你的名字、你所在城市的名字、一句方言,交给 tokens.py 里的两个分词器,看分别切成了什么样。
  2. count_tokens 测一测:同一段内容,写成中文和写成英文,哪个词元更多?换成你工作中真实的一段文字试试。
  3. 找一段 JSON 数据(比如一个 API 的返回结果),数一数它的词元数。再把它压缩成一行、去掉所有空格,重新数一次,词元数少了多少?这在需要把大量数据塞给模型时很有用。

自测

1. 同样一句现代汉语,DeepSeek 用的词元比 OpenAI 的分词器少。为什么?这对你有什么影响?

分词器的词表是从训练语料里统计出来的,DeepSeek 的语料中文比例更高,所以更多的中文词组被合并成了一个词元。对你的影响是:同样的中文内容,在 DeepSeek 上花的钱更少,一次能放进上下文的内容也更多。比较模型价格时,要用真实文本算出词元数再比。

2. 模型为什么容易数错一个单词里有几个某个字母?

模型看到的是词元编号,不是字母。一个常见单词往往就是一个词元,模型没法直接"看见"里面的字母,只能凭训练时的记忆回答。开启思考后,模型会先把单词逐字母写出来再数,准确率就高多了。需要精确计数时,应该用代码而不是模型。

3. 为什么文言文比同样字数的现代汉语"贵"?

词表合并的是训练语料里常见的字符组合。现代汉语的常用词("人工智能"、"程序员")经常出现,被合成了一个词元;文言文里的组合("时习"、"不亦")很少见,没有被合并,只能一个字一个词元。词元多,费用就高。