模組 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. 為什麼文言文比同樣字數的現代漢語"貴"?

詞表合併的是訓練語料裡常見的字元組合。現代漢語的常用詞("人工智慧"、"程式設計師")經常出現,被合成了一個詞元;文言文裡的組合("時習"、"不亦")很少見,沒有被合併,只能一個字一個詞元。詞元多,費用就高。