Skip to content

第二章 LLM 基础

2.1 Token 与分词:模型如何"阅读"文字

承前:在第一章中,我们从宏观视角认识了 AI 大模型——它像一位博览群书又擅长推理的学者,能写文章、能回答问题、能写代码。我们了解了它的发展脉络、能力边界,以及它如何通过"预测下一个词"这一简单而深刻的机制来完成各种任务。但这位"学者"在翻开书页的那一刻,其实并不能直接看到文字。在它"思考"之前,文字首先要经历一道精密的预处理工序——分词。从这一章开始,我们拨开大模型的外衣,深入它的"血管"与"骨骼",看看文字是如何进入模型、如何在模型内部流动、又如何最终变成我们看到的回答。本章会带你走过 Token 与分词、Transformer 架构、注意力机制、训练流程等核心地基,为后续章节的提示工程、微调和 Agent 开发打下坚实的技术底座。


为什么需要分词?——模型"阅读"的第一道门槛

在深入任何算法之前,我们先想一个最基本的问题:大语言模型是"读"文字的吗?

答案是——不是。模型内部处理的是数字,而非字符串。GPU 做的是矩阵乘法,不是阅读文章。模型的"大脑"里只有 0 和 1 组成的浮点数,根本没有"字"这个概念。

打个比方:想象你面前有一堆乐高积木。你用积木可以拼出汽车、城堡、恐龙——形态千变万化,但底层都只有那么几种标准形状的积木块。大语言模型也是类似的原理:不管你喂给它的是英文论文还是中文古诗,它内部需要先把文本拆解成一个个标准化的"积木块",每个块对应一个数字编号,然后对这些编号做数学运算。

这个把文本拆成"积木块"的过程就叫分词(Tokenization),拆出来的每个基本单元就叫一个 Token

人类文本:  "我爱学习AI大模型"
              ↓  分词(Tokenization)
Token序列: [我, 爱, 学习, AI, 大, 模型]
              ↓  映射到词表ID
ID序列:    [1053, 4287, 3519, 102, 219, 5891]

如果再做一个生活类比:分词器就像一把"语言的切菜刀"。你的食材(文本)可能是整块牛排、也可能是一把青菜、也可能是一碗汤面——切菜刀需要根据食材的类型和质地,切成合适大小的块。切太大块,模型嚼不动、学不好;切太小碎,编码效率太低、序列太长。好的分词算法就是在"切多大"和"切多准"之间找到最优解。

那么,你可能马上会问一个自然的追问:既然要把文字拆开,为什么不直接按"字"拆?中文有3500个常用字,英文有26个字母,词表很小不是更简单吗?

好问题。让我们逐一来看。

为什么不按"字"分词?——"字粒度"的困境

按字符级别分词看起来是最直觉的做法:

  • 中文按字分:"我爱学习"["我", "爱", "学", "习"]
  • 英文按字母分:"hello"["h", "e", "l", "l", "o"]

但这样做有三个严重的问题。

第一,序列太长。 如果按字符分,一篇1000字的中文文章就有1000个Token。而我们知道,大模型的注意力机制计算复杂度是序列长度的平方(O(n²))——序列长度翻倍,计算量变成四倍。1000个Token和250个Token的计算开销差了16倍。更关键的是,大模型有"上下文窗口"限制(比如GPT-4o的窗口是128K Token),序列太长会直接超出窗口,丢掉关键信息。

第二,语义太碎。 单独的"学"和"习"没有意义,只有合在一起"学习"才是一个完整的概念。把字拆开,模型需要从大量的上下文中去推断哪些字应该组合成一个词,这增加了学习的难度。就好像把每个字拆成偏旁部首——"学"拆成"⺍"和"子",然后让模型猜它们应该拼在一起——显然徒增困扰。

第三,子词信息丢失。 像"unbelievable"这样的英文词,按字符分是 ["u","n","b","e","l","i","e","v","a","b","l","e"],13个Token。但其实它的结构很有规律:un(否定前缀)+ believe(词根)+ able(能力后缀)。如果能拆成 ["un","believe","able"] 三个Token,既短又保留了词法信息,模型更容易学会"un-前缀表示否定"这样的知识。

那按"词"分呢?看起来更合理——英文按空格切词,中文用分词工具切词。

为什么不按"词"分词?——"词粒度"的困境

按词分词的思路是:

  • 英文按空格:"I love learning"["I", "love", "learning"]
  • 中文用分词工具:"我爱学习"["我", "爱", "学习"]

听起来很完美,但仔细想就会发现问题。

第一,词表太大。 中文有多少个词?中文常用词大概有5万~10万,如果加上专有名词、术语、人名地名,轻松超过50万。英文更多——各种变形、复合词、技术缩写层出不穷。这么大的词表意味着模型的 Embedding 矩阵(把Token ID映射为向量的查表操作)会极其庞大,训练和推理时都会消耗大量内存。

第二,OOV(Out-of-Vocabulary,未登录词)问题。 任何词表都不可能覆盖所有词汇。语言是活的——每天都有新词诞生:"内卷"、"躺平"、"YYDS"、"ChatGPT"、"GPT-4o"……如果词表里没有这些词,模型就完全无法处理。按词分词时,遇到词表里没有的词,要么直接报错,要么用一个特殊的 <UNK>(unknown)标记替代,这会导致大量信息丢失。

第三,变形问题。 英文中 run / running / runs / ran 是同一个词的不同形态。如果每个形态都是一个独立的词表条目,模型需要分别学习它们的含义。而对于训练时没见过的变体,比如某个新造词的 -ize 变形,更是完全束手无策。

所以,我们需要一种介于"字"和"词"之间的粒度——既能控制词表大小,又能覆盖所有可能的文本,还能尽可能保留语义信息。这就是**子词分词(Subword Tokenization)**的出发点,而 BPE、WordPiece、SentencePiece 正是三种最主流的子词分词算法。

BPE(Byte-Pair Encoding)—— 字节对编码

BPE 是当前最主流的分词算法,被 GPT 系列(GPT-2/3/4)、LLaMA 等模型广泛使用。要理解 BPE 为什么如此成功,我们需要深入它的核心机制。

核心思想:从字符级别开始,反复合并出现频率最高的相邻符号对,直到达到预设的词表大小。

这个设计非常巧妙。它保证了两个极端:

  • 高频词(如"the""的""learning")会被整体合并成一个Token——编码效率高
  • 低频词和新词会被拆成已知子词的组合——永远不会出现OOV

BPE 训练过程(以词表大小=10为例)

步骤1:初始化词表,每个字符 + 特殊结束符 </w>
词表:{a, b, c, d, e, ..., </w>}

步骤2:统计语料中所有相邻符号对的频率
语料:"low" "lower" "newest" "widest"
统计:(l,o)=2, (o,w)=2, (w,</w>)=1, (e,r)=2, (e,s)=2, ...

步骤3:合并频率最高的对 -> 加入词表
合并 (l,o) -> "lo",词表新增 "lo"
合并 (o,w) -> "ow",词表新增 "ow"
合并 (e,r) -> "er",词表新增 "er"
...

步骤4:重复步骤2-3,直到词表达预设大小

为什么 BPE 成为主流? 这里有三个深层原因。

第一,它完美解决了OOV问题。 BPE从字符开始构建,最终词表里既有完整的高频词,也有组成这些词的子片段。任何未见过的文本,最坏情况下可以退回到字符级别来编码——就像一把乐高积木,你总能用最基础的几块拼出任何形状。"ChatGPT" 可能不在词表里,但它会被拆成 ["Chat","G","PT"] 或类似的子词组合,每个部分都是模型见过的。

第二,它自带"自适应词表大小"的能力。 BPE的词表大小是一个超参数——你想让词表小一点(比如1万),它就多保留高频词、多拆低频词;你想让词表大一点(比如10万),它就合并更多词汇。这给了模型设计者极大的灵活性,可以根据计算资源和语料特点选择最优的词表大小。

第三,它在多语言场景下表现出色。 BPE不假设输入是哪种语言——它只关心"哪些字符对经常一起出现"。这意味着用中文、英文、日文混合的语料训练出来的BPE词表,会自动包含各种语言的子词。这也是为什么GPT-4虽然主要在英文语料上训练,却也能处理中文的原因。

BPE 的优势

  • 解决了 OOV(Out-of-Vocabulary,未登录词)问题:任何文本都可以拆分为已知字符的组合
  • 在词表大小和编码效率之间取得良好平衡
  • 对多种语言适应性强

WordPiece —— BERT 的分词方案

WordPiece 由 Google 提出,用于 BERT 模型。其核心思想与 BPE 类似,但选择合并对的标准不同。

BPE 是"贪心"的——谁出现得多就合并谁。WordPiece 更"聪明"——它不只是看频率,而是看"合并这一对之后,整个语料的似然概率提升了多少"。如果一个字符对虽然频率高,但拆开看各自在别的语境里也很常见,那它的合并"价值"就没那么大;反之,如果一个字符对总是一起出现、拆开就很少单独出现,那合并它对提升语料概率的贡献最大。

BPE vs WordPiece 的关键区别

维度BPEWordPiece
合并标准频率最高似然增益最大
选择方式统计频率基于语言模型概率
特殊标记结束符 </w>## 前缀表示非词首
代表模型GPT系列, LLaMABERT, DistilBERT

WordPiece 的分词示例

"playing" -> ["play", "##ing"]   # ##表示该片段是前面Token的延续
"unhappiness" -> ["un", "##happiness"]

WordPiece 的 ## 前缀是一个有意思的设计。它的作用是告诉模型"这个片段不是词的开头,而是词的中间或尾部"。这样,模型在训练时就能学到 ##ing 通常表示进行时、##ed 通常表示过去式等词法规律,而不需要为 playingrunningtalking 等每个变体分别学习。

SentencePiece —— 不依赖空格的分词

SentencePiece 由 Google 提出,最大特点是将输入文本视为原始字节流,不依赖空格进行预分词。这在处理中文、日文等不使用空格分隔的语言时尤为重要。

要理解 SentencePiece 的价值,我们需要知道 BPE 和 WordPiece 有一个隐含前提:它们假设输入文本已经经过了空格分词。英文自然满足这个前提(单词之间有空格),但中文没有空格——"我爱自然语言处理"就是一串连续的字符,没有任何分隔标记。如果直接用 BPE,需要先用中文分词工具(如 jieba)预分词,这不仅引入了额外的依赖,也带来了分词错误的风险。

SentencePiece 的解决思路非常彻底——它把空格当作普通字符来处理,用特殊符号 (U+2581,一个下划线变体)替代。这样,无论输入是英文、中文还是日文,处理方式完全统一。

核心特点

  • 将空格视为普通字符(用 表示,U+2581)
  • 支持 BPE 和 Unigram 两种算法
  • 完全可逆:编码后的 Token 序列可以无损还原为原始文本

SentencePiece 分词示例

原文:  "Hello World"
分词:  ["▁Hello", "▁World"]

原文:  "我爱自然语言处理"
分词:  ["▁我", "爱", "自然", "语言", "处理"]

注意看中文的例子:SentencePiece 在句子开头自动加了一个 ,表示"这里是文本的起始位置"或"这里原本有一个空格"。这个信息在解码时会被还原——把所有Token拼接后,把 替换回空格(或省略),就能无损地还原原文。这种"完全可逆"的特性在工程上非常重要:你不需要额外的信息就能从Token序列恢复原始文本。

Llama 系列模型使用 SentencePiece 的 BPE 变体,这也是为什么 Llama 的 Tokenizer 对中文支持相对较好的原因——它从设计之初就不依赖空格预分词,中文字符可以和英文字符一样被自然地纳入合并过程。

三种分词算法对比

下面我们把三种算法放在一起做一个系统的对比:

┌─────────────────────────────────────────────────────────────┐
│                    分词算法对比                              │
├──────────────┬──────────────┬──────────────┬────────────────┤
│    特性      │     BPE      │  WordPiece   │ SentencePiece  │
├──────────────┼──────────────┼──────────────┼────────────────┤
│ 预分词方式   │ 空格分词      │ 空格分词     │ 原始字节流     │
│ 合并策略     │ 频率最高      │ 概率最大     │ BPE/Unigram    │
│ 空格处理     │ 保留空格      │ 保留空格     │ 转义为▁       │
│ 中文支持     │ 需预分词      │ 需预分词     │ 原生支持       │
│ 代表模型     │ GPT-3/4      │ BERT         │ Llama, T5      │
│ 词表大小     │ 50K-100K     │ 30K          │ 32K-128K       │
└──────────────┴──────────────┴──────────────┴────────────────┘

如果你需要在项目中选用分词方案,可以参考以下原则:

  • 如果你的应用基于现成模型:直接用模型对应的分词器即可,不需要自选。GPT系列用 tiktoken,BERT系列用 transformers 库自带的 WordPiece 分词器,Llama 系列用 sentencepiece 库。
  • 如果你要从零训练自己的模型:如果主要处理中文,优先考虑 SentencePiece(含 Unigram 算法),因为它不依赖空格预分词,对中文最友好;如果主要处理英文或混合语料,BPE 是稳妥的选择。
  • 如果你要训练一个新的 Tokenizer:可以使用 Hugging Face 的 tokenizers 库,它同时支持 BPE、WordPiece 和 Unigram 三种算法,API 统一,使用方便。

Token 计费:你的 API 账单是怎么算的?

理解了分词原理之后,我们来看一个非常实际的问题——钱。

使用商业 LLM API(如 OpenAI GPT-4o、Claude)时,费用按 Token 计费。这意味着,你发给模型的输入文本和模型返回的输出文本,都会被各自计算Token数,然后乘以对应的价格。理解Token计费模型对控制成本至关重要。

OpenAI 计费示例(2024-2025年参考价格)

模型输入价格(每百万Token)输出价格(每百万Token)
GPT-4o$2.50$10.00
GPT-4o-mini$0.15$0.60
GPT-4-Turbo$10.00$30.00

注意一个重要细节:输出Token的价格通常是输入Token的4倍。这是因为模型生成输出时的计算量(需要逐个Token自回归地预测)远大于处理输入时(可以并行编码)。

费用计算公式

总费用 = (输入Token数 × 输入单价) + (输出Token数 × 输出单价)

常见场景 Token 消耗估算

  • 一页英文(约500词):约 667 Token
  • 一页中文(约800字):约 1000~1300 Token
  • 一次简单对话(一问一答):约 200~500 Token
  • 一篇长文总结:约 3000~8000 Token

这些数字帮助你建立直觉:如果你的应用需要处理一篇5000字中文长文并生成500字的摘要,那么输入约 6500~8000 Token、输出约 650~800 Token,用 GPT-4o 大约花费 $0.02~$0.03。看起来很少,但如果你的应用每天服务1万次这样的请求,月账单就是 $6000~$9000。所以,在工程实践中,准确预估 Token 消耗并选择合适的模型,是控制成本的关键能力。

实战:使用 tiktoken 进行 Token 计数

说了这么多原理,让我们动手写代码。tiktoken 是 OpenAI 开源的 Python 库,专门用于快速、准确地计算 Token 数量。它是理解 Token 最直观的工具。

安装 tiktoken

bash
pip install tiktoken

tiktoken 是用 Rust 实现的(通过 Python 调用),速度非常快——编码百万级 Token 也就是几秒钟的事。它内置了 OpenAI 各模型对应的词表,你不需要手动下载词表文件。

完整示例:编码、解码与逐Token解析

python
# -*- coding: utf-8 -*-
"""
tiktoken 基础用法:Token 编码、解码与逐Token解析
运行前请先执行:pip install tiktoken
"""

import tiktoken

# ========= 第1步:加载编码器 =========
# tiktoken 的编码器封装了"文本 <-> Token ID"的映射关系
# 有两种加载方式:

# 方式1:按模型名称加载(推荐)
# 你告诉 tiktoken 你要用哪个模型,它会自动选择对应的词表
# 这种方式的好处是:当模型升级时,tiktoken 会自动适配新词表
encoding = tiktoken.encoding_for_model("gpt-4o")

# 方式2:按编码名称加载(更底层)
# 如果你确切知道要用哪个词表,可以直接指定编码名称
# GPT-3.5/GPT-4 使用 "cl100k_base",GPT-2 使用 "r50k_base"
# encoding = tiktoken.get_encoding("cl100k_base")

# ========= 第2步:准备测试文本 =========
# 准备一段中文和一段英文做对比
# 这两段文字表达的含义相同,但 Token 数会不同——这正是中英文分词差异的体现
text_cn = "人工智能正在改变世界,大语言模型是其中最重要的技术之一。"
text_en = "Artificial intelligence is transforming the world, and large language models are among the most important technologies."

# ========= 第3步:文本编码为 Token ID 列表 =========
# encode() 方法接收一个字符串,返回一个整数列表
# 每个整数代表词表中的一个 Token ID
tokens_cn = encoding.encode(text_cn)   # 例如:[103287, 64323, ...]
tokens_en = encoding.encode(text_en)   # 例如:[453, 18021, ...]

# 打印编码结果
print(f"中文原文:{text_cn}")
print(f"中文Token数:{len(tokens_cn)}")
print(f"中文Token序列:{tokens_cn}\n")

print(f"英文原文:{text_en}")
print(f"英文Token数:{len(tokens_en)}")
print(f"英文Token序列:{tokens_en}\n")

# 注意:中文的 Token 数通常比英文多(表达相同含义时)
# 这是因为 GPT 系列的词表以英文为主,中文需要更多的子词来拼出

# ========= 第4步:Token 解码还原 =========
# decode() 方法接收一个整数列表,返回原始字符串
# 这是 encode 的逆操作,可以验证编码是否正确
decoded_cn = encoding.decode(tokens_cn)
decoded_en = encoding.decode(tokens_en)

print(f"中文还原:{decoded_cn}")
print(f"英文还原:{decoded_en}")
# 如果还原结果与原文一致,说明编解码是可逆的

# ========= 第5步:逐个查看每个 Token 对应的文本 =========
# decode_single_token_bytes() 可以查看单个 Token ID 对应的原始字节
# 这对理解"模型到底看到了什么"非常有帮助
print("\n中文Token逐个解析:")
for token_id in tokens_cn:
    # 获取该 Token 的原始字节
    token_bytes = encoding.decode_single_token_bytes(token_id)
    # 将字节解码为可读文本(errors='replace' 防止某些特殊字节报错)
    token_text = token_bytes.decode('utf-8', errors='replace')
    print(f"  ID={token_id:>5} -> '{token_text}'")

print("\n英文Token逐个解析:")
for token_id in tokens_en:
    token_bytes = encoding.decode_single_token_bytes(token_id)
    token_text = token_bytes.decode('utf-8', errors='replace')
    print(f"  ID={token_id:>5} -> '{token_text}'")

运行这段代码,你会看到中文和英文的 Token 拆分方式截然不同。中文往往被拆成更细的子词——比如"人工智能"可能被拆成 ["人工", "智能"] 甚至更小的碎片;而英文 Artificial 可能整体是一个Token。这种差异直接影响成本:用中文做输入通常比英文花更多的 Token。

tiktoken 使用步骤详解

在实际开发中,使用 tiktoken 一般遵循以下流程:

  1. 安装pip install tiktoken
  2. 选择编码器:用 tiktoken.encoding_for_model("模型名") 按模型加载,或用 tiktoken.get_encoding("编码名") 按名称加载
  3. 编码encoding.encode(text) → 返回 Token ID 列表
  4. 计数len(encoding.encode(text)) → 得到 Token 数
  5. 解码encoding.decode(token_ids) → 还原文本
  6. 逐Token查看encoding.decode_single_token_bytes(token_id) → 查看单个Token的字节内容

一个常用技巧:如果你只需要知道 Token 数量而不需要 Token ID 列表,直接调用 len(encoding.encode(text)) 即可,简洁高效。

实战:计算 API 调用费用

理解了 Token 计数之后,我们就可以编写一个费用估算工具——在实际项目中,这个工具能帮你在调用 API 之前就预估成本,选择最经济的模型。

python
# -*- coding: utf-8 -*-
"""
API 费用估算器
功能:根据输入文本和输出文本,估算不同模型的 API 调用费用
"""

import tiktoken

def estimate_cost(
    input_text: str,
    output_text: str,
    model: str = "gpt-4o",
) -> dict:
    """
    估算一次 API 调用的费用

    参数:
        input_text: 发送给模型的输入文本(prompt)
        output_text: 模型返回的输出文本
        model: 模型名称

    返回:
        包含 Token 数和费用明细的字典

    价格参考(2024-2025,单位:美元/百万Token):
        gpt-4o:       输入 $2.50,  输出 $10.00
        gpt-4o-mini:  输入 $0.15,  输出 $0.60
        gpt-4-turbo:  输入 $10.00, 输出 $30.00
    """
    # 模型定价表(美元/百万Token)
    # 实际使用时,建议从 API 官方页面获取最新价格
    pricing = {
        "gpt-4o": {"input": 2.50, "output": 10.00},
        "gpt-4o-mini": {"input": 0.15, "output": 0.60},
        "gpt-4-turbo": {"input": 10.00, "output": 30.00},
    }

    # 检查模型是否在定价表中
    if model not in pricing:
        raise ValueError(f"未知模型: {model},支持: {list(pricing.keys())}")

    # 获取该模型对应的编码器
    # 注意:即使使用不同的模型,tiktoken 会自动选择正确的词表
    encoding = tiktoken.encoding_for_model(model)

    # 分别计算输入和输出的 Token 数
    # 输入 = 你发给模型的 prompt(包括系统提示、用户消息等)
    # 输出 = 模型生成的回复文本
    input_tokens = len(encoding.encode(input_text))
    output_tokens = len(encoding.encode(output_text))

    # 计算费用
    # 公式:Token数 / 1,000,000 × 单价 = 该部分的费用
    price = pricing[model]
    input_cost = (input_tokens / 1_000_000) * price["input"]
    output_cost = (output_tokens / 1_000_000) * price["output"]
    total_cost = input_cost + output_cost

    # 返回费用明细
    return {
        "model": model,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
        "total_tokens": input_tokens + output_tokens,
        "input_cost": round(input_cost, 6),
        "output_cost": round(output_cost, 6),
        "total_cost": round(total_cost, 6),
    }


# ===== 模拟一次真实的 API 调用场景 =====

# 用户提问
user_input = "请用中文总结一下Transformer架构的核心思想,大约500字。"

# 模型返回的回答(这里用一段预设文本来模拟)
assistant_output = """Transformer架构的核心思想是摒弃了传统的循环神经网络(RNN)结构,
完全基于注意力机制(Attention Mechanism)来建模序列数据。其关键创新包括:
1. Self-Attention机制:让序列中的每个位置都能直接关注到所有其他位置...
2. Multi-Head Attention:通过多个注意力头并行计算...
3. 位置编码:为序列注入位置信息...
4. 残差连接与层归一化:确保深层网络的稳定训练..."""

# 估算不同模型的费用并打印对比
# 实际开发中,你常常需要在多个模型之间做权衡:
# - gpt-4o:质量最好但较贵
# - gpt-4o-mini:便宜很多,简单任务完全够用
print("=" * 50)
print("API 调用费用估算对比")
print("=" * 50)
for model_name in ["gpt-4o", "gpt-4o-mini"]:
    result = estimate_cost(user_input, assistant_output, model=model_name)
    print(f"\n模型:{result['model']}")
    print(f"  输入Token:{result['input_tokens']}")
    print(f"  输出Token:{result['output_tokens']}")
    print(f"  总Token:{result['total_tokens']}")
    print(f"  预估费用:${result['total_cost']:.6f}")
    print(f"  输入费用:${result['input_cost']:.6f}")
    print(f"  输出费用:${result['output_cost']:.6f}")

运行这段代码,你会看到 gpt-4o 和 gpt-4o-mini 的费用差距非常明显——对于简单任务,mini 模型的费用可能只有 o 模型的6%。这就是为什么在实际工程中,一个常见的策略是:用便宜的模型(如 gpt-4o-mini)做初筛和简单任务,只在需要高质量输出时才调用昂贵的模型。

常见误区

在理解了分词原理之后,让我们来澄清几个常见的误区。这些误区在实际开发中经常导致 bug 或成本超支。

误区一:"一个Token等于一个字(或一个词)"

这是最常见的误解。Token的粒度取决于分词器的训练语料和词表大小,和"字"或"词"没有一一对应关系。一个Token可能是半个字(比如某个中文字的偏旁)、一个完整的字、一个完整的词、甚至一个短语。不同模型对同一个词的分词结果可能不同——"学习" 在 GPT-4o 的词表中可能是1个Token,在 GPT-3.5 中可能是2个。所以,永远不要用字数来估算Token数,一定要用 tiktoken 之类的工具实际计算。

误区二:"中文分词和英文一样"

中文和英文的分词逻辑有本质差异。英文天然有空格分隔,分词器在空格基础上做子词合并即可;中文没有空格,分词器需要自己决定切分边界。结果是:GPT系列模型(使用BPE,假设空格预分词)对中文的处理效率较低——一个中文字可能需要2~3个Token才能编码。而使用SentencePiece的模型(如Llama)对中文相对更友好。这也解释了为什么同样的文本,不同模型的Token计数差异可能很大。

误区三:"Token越多,模型理解越差"

不完全正确。Token多只意味着序列长、计算量大、费用高,但不意味着模型理解更差。事实上,更细的分词粒度有时反而能帮助模型捕捉更多细节。真正的权衡是"编码效率"和"信息保留"之间的平衡,而非简单的"多就是坏"。

误区四:"所有模型的Token计数方式相同"

不同的模型使用不同的分词器和词表,因此同样一句话在不同模型上的Token数可能不同。例如,"你好世界" 在 GPT-4o 的 cl100k_base 编码下可能是4个Token,但在 Llama 2 的 SentencePiece 编码下可能是3个。在预估费用时,一定要使用对应模型的分词器来计数。

误区五:"分词器可以完美还原所有文本"

大多数情况下,分词是可逆的(BPE和SentencePiece都支持无损还原)。但有一些边缘情况:某些特殊Unicode字符、不可见字符、或者超长的连续无空格文本,可能导致分词结果不完美。在实际工程中,建议在关键场景下做编解码的往返测试(round-trip test),确保 decode(encode(text)) == text 成立。

本节小结

回顾这一节的内容,我们从"模型如何阅读文字"这个最基本的视角出发,深入了解了分词的原理和实践:

  • Token是LLM的最小处理单元。模型不读文字,它读的是数字。文本需要先分词为Token,再映射为ID才能被模型处理。分词器就是"语言的切菜刀",把连续的文本切成模型能消化的标准块。

  • 不能按字分词——序列太长、语义太碎、子词信息丢失。也不能按词分词——词表太大、OOV问题严重、变形处理困难。子词分词(Subword Tokenization)是"字粒度"和"词粒度"之间的最优解。

  • BPE:频率驱动的合并策略。从字符开始,反复合并最高频的字符对。解决OOV、自适应词表大小、多语言友好。GPT系列、LLaMA使用。

  • WordPiece:基于似然增益的合并策略。不只看频率,还看合并后对整个语料概率的提升。用 ## 标记非词首片段,帮助模型学习词法规律。BERT使用。

  • SentencePiece:不依赖空格的字节流分词。把空格转义为 ,完全可逆。原生支持中文等无空格语言。Llama、T5使用。

  • Token计费:按输入/输出Token分别计费,输出通常比输入贵4倍。用 tiktoken 工具准确计数,是控制API成本的基础能力。

理解了分词,你就理解了大模型"消化"信息的第一步。但Token只是"食材",模型如何"加工"这些食材、如何理解Token之间的关系、如何生成下一个Token——这就要进入模型的核心架构了。


启后:这一节我们解决了"文字如何进入模型"的问题——分词把文本变成了Token,Token变成了数字ID。但拿到一串数字ID之后,模型要怎么"理解"它们?这些ID是如何变成富含语义的向量表示的?模型又是如何从这些向量中"看出"词与词之间的关系、最终预测出下一个词的?答案藏在 Transformer 架构里——这是2017年那篇石破天惊的论文"Attention Is All You Need"给世界带来的礼物。下一节,我们将深入 Transformer 的核心:位置编码、注意力机制、前馈网络,看看大模型的"大脑"到底是怎么运转的。


参考资料

  1. 📄 OpenAI Cookbook - How to Count Tokens with TiktokenOpenAI官方指南,详细介绍tiktoken的使用方法https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken

  2. 📄 OpenAI Tokenizer 在线工具在线可视化Token分词结果,直观理解Token划分https://platform.openai.com/tokenizer

  3. 📄 Hugging Face Tokenizers 文档Hugging Face的Tokenizers库,支持BPE/WordPiece/Unigram等多种算法https://huggingface.co/docs/tokenizers/index

  4. 📄 SentencePiece GitHubGoogle SentencePiece项目主页,含详细技术文档https://github.com/google/sentencepiece

  5. 📄 BPE算法原始论文 (Neural Machine Translation of Rare Words with Subword Units)Sennrich et al., 2016,BPE应用于NMT的开创性论文https://aclanthology.org/P16-1162/

  6. 📄 OpenAI 定价页面最新的API定价信息,实时了解Token计费标准https://openai.com/api/pricing/