第二章 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 的关键区别:
| 维度 | BPE | WordPiece |
|---|---|---|
| 合并标准 | 频率最高 | 似然增益最大 |
| 选择方式 | 统计频率 | 基于语言模型概率 |
| 特殊标记 | 结束符 </w> | ## 前缀表示非词首 |
| 代表模型 | GPT系列, LLaMA | BERT, DistilBERT |
WordPiece 的分词示例:
"playing" -> ["play", "##ing"] # ##表示该片段是前面Token的延续
"unhappiness" -> ["un", "##happiness"]WordPiece 的 ## 前缀是一个有意思的设计。它的作用是告诉模型"这个片段不是词的开头,而是词的中间或尾部"。这样,模型在训练时就能学到 ##ing 通常表示进行时、##ed 通常表示过去式等词法规律,而不需要为 playing、running、talking 等每个变体分别学习。
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:
pip install tiktokentiktoken 是用 Rust 实现的(通过 Python 调用),速度非常快——编码百万级 Token 也就是几秒钟的事。它内置了 OpenAI 各模型对应的词表,你不需要手动下载词表文件。
完整示例:编码、解码与逐Token解析
# -*- 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 一般遵循以下流程:
- 安装:
pip install tiktoken- 选择编码器:用
tiktoken.encoding_for_model("模型名")按模型加载,或用tiktoken.get_encoding("编码名")按名称加载- 编码:
encoding.encode(text)→ 返回 Token ID 列表- 计数:
len(encoding.encode(text))→ 得到 Token 数- 解码:
encoding.decode(token_ids)→ 还原文本- 逐Token查看:
encoding.decode_single_token_bytes(token_id)→ 查看单个Token的字节内容一个常用技巧:如果你只需要知道 Token 数量而不需要 Token ID 列表,直接调用
len(encoding.encode(text))即可,简洁高效。
实战:计算 API 调用费用
理解了 Token 计数之后,我们就可以编写一个费用估算工具——在实际项目中,这个工具能帮你在调用 API 之前就预估成本,选择最经济的模型。
# -*- 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 的核心:位置编码、注意力机制、前馈网络,看看大模型的"大脑"到底是怎么运转的。
参考资料
📄 OpenAI Cookbook - How to Count Tokens with TiktokenOpenAI官方指南,详细介绍tiktoken的使用方法https://cookbook.openai.com/examples/how_to_count_tokens_with_tiktoken
📄 OpenAI Tokenizer 在线工具在线可视化Token分词结果,直观理解Token划分https://platform.openai.com/tokenizer
📄 Hugging Face Tokenizers 文档Hugging Face的Tokenizers库,支持BPE/WordPiece/Unigram等多种算法https://huggingface.co/docs/tokenizers/index
📄 SentencePiece GitHubGoogle SentencePiece项目主页,含详细技术文档https://github.com/google/sentencepiece
📄 BPE算法原始论文 (Neural Machine Translation of Rare Words with Subword Units)Sennrich et al., 2016,BPE应用于NMT的开创性论文https://aclanthology.org/P16-1162/
📄 OpenAI 定价页面最新的API定价信息,实时了解Token计费标准https://openai.com/api/pricing/