01 · Tokens & Tokenization#
LLM 不读「字」,也不读「词」,它读的是 Token。
理解 Token 是理解 LLM 一切行为的基础:
为什么有「上下文窗口」限制?
为什么 API 按 Token 计费?
为什么中文比英文「贵」?
为什么模型有时会「数不清字数」?
本章用代码把这些现象都跑一遍。
1. Token 是什么#
Token 是模型处理文本的最小单位,介于字符和单词之间。
现代 LLM 普遍使用 BPE(Byte Pair Encoding) 算法生成词表: 从单个字节出发,反复合并最高频的相邻对,直到词表达到目标大小(GPT-4 用 ~100k,Claude 类似)。
结果是:
常见词 → 单个 token(
hello= 1 token)罕见词 → 被拆开(
tokenization= 2–3 tokens)空格通常附着在下一个 token 前面(
hello≠hello)
我们用 OpenAI 开源的 tiktoken 库来观察这个过程(Claude 的 tokenizer 未开源,但行为类似)。
import tiktoken
# cl100k_base 是 GPT-4 / text-embedding-3 使用的编码
enc = tiktoken.get_encoding("cl100k_base")
text = "Tokenization is not splitting by spaces!"
token_ids = enc.encode(text)
print(f"原文: {text}")
print(f"Token IDs: {token_ids}")
print(f"Token 数量: {len(token_ids)}")
原文: Tokenization is not splitting by spaces!
Token IDs: [3404, 2065, 374, 539, 45473, 555, 12908, 0]
Token 数量: 8
# 把每个 token ID 解码回文字,看清边界
tokens_decoded = [enc.decode([t]) for t in token_ids]
print(tokens_decoded)
['Token', 'ization', ' is', ' not', ' splitting', ' by', ' spaces', '!']
2. 可视化 Token 边界#
用颜色区分每个 token,直观看清边界在哪里。
import html
from IPython.display import HTML
COLORS = ["#FFB3BA", "#FFDFBA", "#FFFFBA", "#BAFFC9", "#BAE1FF", "#E8BAFF"]
def visualize_tokens(text: str, encoding_name: str = "cl100k_base") -> HTML:
enc = tiktoken.get_encoding(encoding_name)
ids = enc.encode(text)
parts = [enc.decode([t]) for t in ids]
spans = []
for i, part in enumerate(parts):
color = COLORS[i % len(COLORS)]
safe = html.escape(part).replace(" ", " ").replace("\n", "↵<br>")
spans.append(
f'<span title="id={ids[i]}" '
f'style="background:{color};padding:3px 5px;border-radius:4px;'
f'margin:2px;font-family:monospace;font-size:14px">{safe}</span>'
)
summary = f'<p style="font-size:12px;color:#666">{len(ids)} tokens</p>'
return HTML(f'<div style="line-height:3">{" ".join(spans)}</div>{summary}')
visualize_tokens("Hello, world! How are you today?")
9 tokens
# 注意:同一个词,有无前导空格,token 不同
visualize_tokens("hello Hello hello")
4 tokens
# 罕见词被拆分
visualize_tokens("Supercalifragilisticexpialidocious antidisestablishmentarianism")
17 tokens
# 代码也是 token
visualize_tokens("def fibonacci(n):\n return n if n <= 1 else fibonacci(n-1) + fibonacci(n-2)")
return n if n <= 1 else fibonacci (n - 1 ) + fibonacci (n - 2 )
24 tokens
3. Token 的几个反直觉特点#
用实验来验证这些特点,而不是死记。
# 特点 1:空格是 token 的一部分(前置空格)
cases = ["hello", " hello", "Hello", " Hello"]
for c in cases:
ids = enc.encode(c)
print(f"{repr(c):10} → ids={ids}")
'hello' → ids=[15339]
' hello' → ids=[24748]
'Hello' → ids=[9906]
' Hello' → ids=[22691]
# 特点 2:数字被逐位或小组 tokenize,这就是为什么 LLM 不擅长数学
visualize_tokens("1 + 1 = 2, 1234567890, 3.14159265")
22 tokens
# 特点 3:同一概念,格式不同 → token 数不同
formats = {
"ISO date": "2024-03-14",
"US date": "March 14, 2024",
"Chinese": "2024年3月14日",
"JSON": '{"date": "2024-03-14"}',
}
for fmt, s in formats.items():
n = len(enc.encode(s))
print(f"{fmt:12} | {n:2} tokens | {repr(s)}")
ISO date | 6 tokens | '2024-03-14'
US date | 7 tokens | 'March 14, 2024'
Chinese | 7 tokens | '2024年3月14日'
JSON | 11 tokens | '{"date": "2024-03-14"}'
4. 中英文 / 代码 / Emoji 效率对比#
chars/token 越高 = 同样的 token 携带更多信息 = 越「高效」。
samples = {
"English": "Artificial intelligence is transforming every industry.",
"中文": "人工智能正在改变每一个行业。",
"日本語": "人工知能はあらゆる産業を変革しています。",
"Python": "result = [x**2 for x in range(10) if x % 2 == 0]",
"JSON": '{"name": "Alice", "age": 30, "active": true}',
"Emoji": "I ❤️ AI 🤖🧠✨💡🚀",
"Repeated": "ha" * 20,
}
print(f"{'Category':10} | {'Chars':>6} | {'Tokens':>6} | {'Chars/Token':>11}")
print("-" * 44)
for name, text in samples.items():
chars = len(text)
tokens = len(enc.encode(text))
ratio = chars / tokens
print(f"{name:10} | {chars:6} | {tokens:6} | {ratio:11.2f}")
Category | Chars | Tokens | Chars/Token
--------------------------------------------
English | 55 | 8 | 6.88
中文 | 14 | 13 | 1.08
日本語 | 20 | 22 | 0.91
Python | 48 | 22 | 2.18
JSON | 44 | 17 | 2.59
Emoji | 13 | 17 | 0.76
Repeated | 40 | 19 | 2.11
观察结论:
英文约 4 chars/token,是 token 效率最高的语言之一
中文约 1.5–2 chars/token,同样语义需要更多 token
这意味着:同样的上下文窗口,中文能装的信息量少于英文
Emoji 效率极低,每个 emoji 可能占 2–3 tokens
5. 实际意义:成本 & 上下文限制#
# 5.1 发送前计算 token 数(避免超出上下文窗口)
CONTEXT_LIMITS = {
"claude-sonnet-4-6": 200_000,
"gpt-4o": 128_000,
"gemini-2.0-flash":1_000_000,
}
def check_context(text: str, model: str = "claude-sonnet-4-6") -> dict:
n = len(enc.encode(text))
limit = CONTEXT_LIMITS.get(model, 128_000)
return {
"tokens": n,
"limit": limit,
"usage_pct": round(n / limit * 100, 1),
"fits": n < limit,
}
sample = "Hello, world! " * 1000
result = check_context(sample)
print(result)
{'tokens': 4001, 'limit': 200000, 'usage_pct': 2.0, 'fits': True}
# 5.2 估算 API 调用成本
# 价格单位:USD / 1M tokens(2025 年参考值,请以官网为准)
PRICING = {
"claude-sonnet-4-6": {"input": 3.0, "output": 15.0},
"claude-haiku-4-5": {"input": 0.8, "output": 4.0},
"gpt-4o": {"input": 2.5, "output": 10.0},
"gpt-4o-mini": {"input": 0.15, "output": 0.6},
}
def estimate_cost(
prompt: str,
estimated_output_tokens: int = 500,
model: str = "claude-sonnet-4-6",
) -> None:
input_tokens = len(enc.encode(prompt))
price = PRICING.get(model, PRICING["claude-sonnet-4-6"])
input_cost = input_tokens / 1_000_000 * price["input"]
output_cost = estimated_output_tokens / 1_000_000 * price["output"]
total = input_cost + output_cost
print(f"Model: {model}")
print(f"Input tokens: {input_tokens:,} → ${input_cost:.6f}")
print(f"Output tokens: {estimated_output_tokens:,} (est) → ${output_cost:.6f}")
print(f"Estimated cost: ${total:.6f}")
prompt = "Summarize the history of artificial intelligence in detail."
estimate_cost(prompt, estimated_output_tokens=800)
Model: claude-sonnet-4-6
Input tokens: 11 → $0.000033
Output tokens: 800 (est) → $0.012000
Estimated cost: $0.012033
# 同样的 prompt,不同模型的成本差异
print("Cost comparison across models:")
print("-" * 50)
for model in PRICING:
estimate_cost(prompt, model=model)
print()
Cost comparison across models:
--------------------------------------------------
Model: claude-sonnet-4-6
Input tokens: 11 → $0.000033
Output tokens: 500 (est) → $0.007500
Estimated cost: $0.007533
Model: claude-haiku-4-5
Input tokens: 11 → $0.000009
Output tokens: 500 (est) → $0.002000
Estimated cost: $0.002009
Model: gpt-4o
Input tokens: 11 → $0.000027
Output tokens: 500 (est) → $0.005000
Estimated cost: $0.005027
Model: gpt-4o-mini
Input tokens: 11 → $0.000002
Output tokens: 500 (est) → $0.000300
Estimated cost: $0.000302
6. 用 LLM 验证理解#
现在用真实的 API 调用来感受 token 的存在。
from utils.llm_client import chat
prompt = "用3个要点解释 LLM 中的 token 是什么,每个要点一句话。"
response = chat(prompt)
print(response)
print("---")
print(f"Prompt: {len(enc.encode(prompt))} tokens")
print(f"Response: ~{len(enc.encode(response))} tokens (tiktoken 估算,实际由模型计)")
- Token 是模型处理文本的基本单位,通常是字符、子词或字节层面的片段(不等同于自然语言里的“词”)。
- 每个 token 被编码为一个 ID 并映射为向量,模型以这些 token 的序列进行训练与预测下一个 token。
- 不同的分词/tokenization 方法和词表大小会影响分割结果、上下文长度与使用成本(中文、数字或特殊符号可能产生不同数量的 token)。
---
Prompt: 26 tokens
Response: ~146 tokens (tiktoken 估算,实际由模型计)
# 问模型:你刚才的回答用了多少 token?(看它能不能数清楚)
followup = chat(
f"你刚才回答了这段文字:\n\n{response}\n\n它包含多少个字符?多少个词?"
)
print(followup)
print("---")
print(f"实际字符数: {len(response)}")
我把你引述的三句话(不含每行开头的“- ”标记,也去掉行末多余空格)当作一个文本来计算:
- 字符数(按 Unicode 字符计,包含中文、英文字母、标点和中间的空格):181 个。
- “词”的数量(按空白字符分割,即以空格为分隔符):13 个。
如果你希望我用另一种定义来统计(例如把每个中文“词”用分词工具统计,或把开头的“- ”也算入字符),我可以按那种方式再算一次。
---
实际字符数: 193
结论: LLM 对「数字符 / 数词」这类任务往往不准确,因为它看到的是 token,不是字符或词。 这正是本章的核心洞察。
小结#
概念 |
要点 |
|---|---|
Token |
模型处理文本的最小单位,由 BPE 算法生成 |
空格 |
通常是 token 的前缀, |
中文效率 |
~1.5 chars/token,英文 ~4,同窗口中文装的内容更少 |
成本 |
API 按 input + output token 分开计费 |
计数盲区 |
LLM 不擅长数字符/数词,因为它看的是 token |
下一章 → Embeddings:token 如何变成向量,语义如何被编码进数字