这是"每日一个开源项目"系列的第121篇文章。今天的主角是 tiktoken——OpenAI 开源的官方分词器。

在调用 OpenAI API 之前,几乎每个开发者都会遇到同一个问题:这段文本会消耗多少 token?超出上下文限制了吗?费用怎么估算?这些问题的答案,都藏在 tokenization 这一步里。
tiktoken 不只是一个"数 token 的工具",它是 GPT 系列模型在训练和推理时真实使用的分词器。理解它,就是理解模型真正"看到"的输入形态。
tiktoken 是 OpenAI 发布的开源 BPE(Byte Pair Encoding,字节对编码)分词器库。它的核心职责是将文本字符串转化为 token 序列(整数列表),供语言模型消费;也可以将 token 序列还原为原始文本。
这不是一个"实验性"工具——它是 GPT-3.5、GPT-4、GPT-4o 等模型真实使用的分词器。当你通过 API 发送一段文字时,模型"看到"的其实是 tiktoken 生成的 token 序列。
tiktoken 做三件事:
这三件事看起来简单,但在 LLM 应用开发中至关重要:
API 调用前的 Token 预算控制
max_tokens 限制,避免截断或报错长文档的智能分块(Chunking)
多轮对话的上下文窗口管理
精确成本估算与监控
Fine-tuning 数据预处理
pip install tiktoken
import tiktoken# 方式一:按编码名获取(推荐用于新项目)
enc = tiktoken.get_encoding("o200k_base")# 方式二:按模型名获取(自动匹配模型对应的编码)
enc = tiktoken.encoding_for_model("gpt-4o")# 编码:文本 → token ID 列表
tokens = enc.encode("Hello, tiktoken!")
print(tokens) # [13225, 11, 384, 4963, 0]
print(len(tokens)) # 5 ← 这就是 token 数# 解码:token ID 列表 → 文本
text = enc.decode(tokens)
print(text) # "Hello, tiktoken!"# 可逆性验证
assert enc.decode(enc.encode("任意文本都可以还原")) == "任意文本都可以还原"
高性能 Rust 核心
GPT2TokenizerFast)可逆无损编码
decode(encode(text)) == text 始终成立,token 序列可完整还原原始文本通用性覆盖任意文本
高压缩率
子词感知
ing、tion、pre-),帮助模型理解词法规律多编码支持
o200k_base(GPT-4o)、cl100k_base(GPT-4/3.5-turbo)等主流编码特殊 Token 扩展
<|im_start|> 等特殊控制 token,适配 Chat 格式教育模块
_educational 模块,可视化 BPE 合并过程,适合学习算法原理| 对比维度 | tiktoken | HuggingFace Tokenizers | SentencePiece |
|---|---|---|---|
| 速度 | 最快(Rust核心) | 快(Rust核心) | 中等(C++) |
| 与 OpenAI 模型对齐 | 官方一致 | 近似 | 不支持 |
| Python 接口简洁度 | 极简 | 中等 | 中等 |
| 支持模型范围 | OpenAI 系列 | 通用 | 通用 |
| 自定义编码 | 支持 | 支持 | 支持 |
| 依赖包大小 | 小 | 中 | 中 |
为什么选择 tiktoken?
BPE(Byte Pair Encoding,字节对编码)是 tiktoken 的核心算法。理解 BPE 的 4 个特性,就能理解 tiktoken 的能力边界:
① 可逆无损(Lossless Reversibility)
token 序列可以 100% 还原为原始文本,没有任何信息丢失。这是 BPE 的基础承诺:
original = "GPT-4o 使用 o200k_base 编码"
assert enc.decode(enc.encode(original)) == original # 永远成立
② 通用性(Open Vocabulary)
tiktoken 基于字节级 BPE,词表从单个字节(256个)开始,通过统计频率逐步合并,最终覆盖高频子词。任何 Unicode 字符都能被分词——即使是模型从未见过的词:
# 训练集之外的新词、表情符号、代码,全部可以正常分词
enc.encode(" tiktoken-v99 新词") # 不会报错
③ 高压缩率(High Compression)
平均每个 token 约对应 4 字节,这意味着序列长度大幅缩短,降低计算开销:
text = "The quick brown fox jumps over the lazy dog"
tokens = enc.encode(text)
print(f"字符数: {len(text)}, token数: {len(tokens)}")
# 字符数: 43, token数: 9 → 压缩率约 4.8:1
④ 子词感知(Subword Awareness)
BPE 能识别词根、词缀等语言规律,帮助模型泛化到未见过的组合:
# "encoding" → ["encod", "ing"] 模型可以理解 "encod-" 的含义
# "tokenization" → ["token", "ization"]
print(enc.decode([b]) for b in enc.encode("encoding"))
选错编码会导致 token 计数与实际 API 调用不符。以下是完整对照表:
| 编码名称 | 适用模型 | 词表大小 |
|---|---|---|
o200k_base | GPT-4o, GPT-4o-mini | 200,000 |
cl100k_base | GPT-4, GPT-3.5-turbo, text-embedding-3-* | 100,000 |
p50k_base | text-davinci-003 等旧版模型 | 50,000 |
r50k_base | GPT-3 (davinci) 等 | 50,000 |
import tiktokendef count_tokens(text: str, model: str = "gpt-4o") -> int:
"""精确计算指定模型的 token 消耗"""
enc = tiktoken.encoding_for_model(model)
return len(enc.encode(text))# 测试
print(count_tokens("Hello, world!")) # 4
print(count_tokens("你好,世界!")) # 6
Chat 模型(如 gpt-3.5-turbo)使用特殊 token 来界定角色边界。你可以扩展现有编码来支持这些控制符:
import tiktokencl100k_base = tiktoken.get_encoding("cl100k_base")# 创建带 Chat 格式特殊 token 的扩展编码
enc = tiktoken.Encoding(
name="cl100k_im", # 自定义名称
pat_str=cl100k_base._pat_str,
mergeable_ranks=cl100k_base._mergeable_ranks,
special_tokens={
**cl100k_base._special_tokens,
"<|im_start|>": 100264, # 角色开始
"<|im_end|>": 100265, # 角色结束
}
)# 现在可以编码包含特殊 token 的 Chat 格式文本
text = "<|im_start|>usernWhat is BPE?<|im_end|>"
tokens = enc.encode(text, allowed_special={"<|im_start|>", "<|im_end|>"})
print(f"token 数: {len(tokens)}")
这是 tiktoken 最高频的使用场景——在发送 API 请求前,检查并裁剪消息列表:
import tiktokendef trim_messages_to_budget(
messages: list[dict],
model: str = "gpt-4o",
max_tokens: int = 8000,
) -> list[dict]:
"""
裁剪消息历史,确保 token 总数不超过预算。
保留 system prompt,从最旧的 user/assistant 消息开始删除。
"""
enc = tiktoken.encoding_for_model(model) def count(msgs):
# 每条消息有 4 token 的固定开销(角色、分隔符等)
total = sum(4 + len(enc.encode(m.get("content", ""))) for m in msgs)
return total + 2 # 回复前置的 2 token system = [m for m in messages if m["role"] == "system"]
others = [m for m in messages if m["role"] != "system"] while count(system + others) > max_tokens and others:
others.pop(0) # 删除最旧的消息 return system + others# 使用示例
messages = [
{"role": "system", "content": "你是一个助手。"},
{"role": "user", "content": "第一个问题"},
{"role": "assistant", "content": "第一个回答"},
# ... 更多历史消息
]trimmed = trim_messages_to_budget(messages, max_tokens=4096)
tiktoken 内置了一个教育用的简化版 BPE 实现,适合学习算法原理:
from tiktoken._educational import SimpleBytePairEncoding# 使用真实的 cl100k_base 合并规则训练一个简化编码
enc = SimpleBytePairEncoding.from_tiktoken("cl100k_base")# 可视化分词过程
result = enc.encode("hello world aaaaaaaaaaaa")
# 输出会显示每一步合并的过程
tiktoken 之所以比同类工具快 3-6 倍,关键在于其 Python + Rust 混合架构:
tiktoken/
├── tiktoken/
│ ├── __init__.py ← Python 接口层
│ ├── core.py ← 主 API(Encoding 类)
│ ├── model.py ← 模型名 → 编码名的映射表
│ ├── registry.py ← 编码注册与缓存
│ └── _educational.py ← 教育用纯 Python BPE 实现
│
└── src/ (Rust)
└── lib.rs ← 高性能 BPE 核心逻辑(通过 PyO3 暴露给 Python)
性能关键点:
tiktoken 的价值不只在于"数 token",它是连接开发者与 GPT 模型之间的翻译层。掌握 tiktoken,意味着你真正理解了模型的输入形态,能够精确控制上下文、估算成本、构建健壮的 LLM 应用。
Rust 核心 + Python 接口的设计选择,也是一个值得借鉴的工程范式:把性能关键路径交给系统语言,把易用性和灵活性留给动态语言。
欢迎来我的个人主页找到更多有用的知识和有趣的产品