TL;DR
Token是大语言模型处理文本的基本单位,上下文窗口决定了模型一次能处理的最大Token数量。本指南详细介绍Token化过程、主流分词算法(BPE、WordPiece、SentencePiece)、各模型上下文窗口对比、长上下文技术(RoPE、ALiBi),以及Token计数和成本优化的实用策略。
引言
当你使用ChatGPT、Claude或其他大语言模型时,是否好奇过:为什么有时候对话会被截断?为什么中文和英文的"长度"计算不一样?为什么API调用费用难以预估?
这些问题的答案都与两个核心概念相关:Token和上下文窗口(Context Window)。理解它们不仅能帮助你更高效地使用AI工具,还能显著降低API调用成本。
在本指南中,你将学到:
- Token的定义和Token化过程
- BPE、WordPiece、SentencePiece等分词算法的原理
- 什么是上下文窗口及其重要性
- 主流大模型的上下文窗口大小对比
- 长上下文技术:RoPE、ALiBi、Sliding Window
- 如何计算Token数量和估算成本
- 优化上下文使用的实用策略
什么是Token
Token是大语言模型处理文本的最小单位。在模型眼中,文本不是字符或单词的序列,而是Token的序列。
Token与字符、单词的区别
| 概念 | 定义 | 示例 |
|---|---|---|
| 字符 | 最小的文本单位 | H, e, l, l, o |
| 单词 | 由空格分隔的文本 | Hello, world |
| Token | 模型处理的基本单位 | Hello, wor, ld |
Token的粒度介于字符和单词之间。常见单词通常是一个Token,而罕见词或长词会被拆分成多个Token。
为什么使用Token而非单词
- 词汇表大小可控:英语有数十万单词,而Token词汇表通常只有3-5万
- 处理未知词:任何文本都能被分解为已知Token的组合
- 跨语言支持:同一分词器可以处理多种语言
- 子词共享:相关词汇共享子词Token,如"run"、"running"、"runner"
Token化过程详解
Token化(Tokenization)是将原始文本转换为Token序列的过程。
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4")
text = "Hello, world! 你好,世界!"
tokens = enc.encode(text)
print(f"原始文本: {text}")
print(f"Token数量: {len(tokens)}")
print(f"Token ID: {tokens}")
print(f"解码后: {[enc.decode([t]) for t in tokens]}")
输出示例:
原始文本: Hello, world! 你好,世界!
Token数量: 11
Token ID: [9906, 11, 1917, 0, 220, 57668, 53901, 3922, 244, 98220, 6447]
解码后: ['Hello', ',', ' world', '!', ' ', '你好', ',', '世', '界', '!']
不同语言的Token效率
中文和英文的Token效率差异显著:
def compare_token_efficiency(texts):
enc = tiktoken.encoding_for_model("gpt-4")
for text in texts:
tokens = enc.encode(text)
chars = len(text)
ratio = chars / len(tokens)
print(f"文本: {text}")
print(f"字符数: {chars}, Token数: {len(tokens)}, 效率: {ratio:.2f}字符/Token\n")
compare_token_efficiency([
"The quick brown fox jumps over the lazy dog.",
"敏捷的棕色狐狸跳过了懒惰的狗。",
"Transformer architecture revolutionized NLP.",
"Transformer架构彻底改变了自然语言处理领域。"
])
一般来说,英文约4个字符对应1个Token,而中文约1.5-2个字符对应1个Token。
主流分词算法
BPE(Byte Pair Encoding)
BPE是GPT系列模型使用的分词算法,通过迭代合并最频繁的字符对来构建词汇表。
def simple_bpe_demo():
"""简化的BPE演示"""
corpus = ["low", "lower", "newest", "widest"]
vocab = set()
for word in corpus:
vocab.update(list(word))
vocab.add("</w>")
print(f"初始词汇表: {sorted(vocab)}")
word_freqs = {}
for word in corpus:
chars = list(word) + ["</w>"]
word_freqs[tuple(chars)] = word_freqs.get(tuple(chars), 0) + 1
print(f"初始分词: {list(word_freqs.keys())}")
simple_bpe_demo()
BPE的优点:
- 平衡了字符级和单词级分词的优缺点
- 能有效处理未知词
- 词汇表大小可控
WordPiece
WordPiece是BERT使用的分词算法,与BPE类似但使用不同的合并策略。
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "unbelievable"
tokens = tokenizer.tokenize(text)
print(f"WordPiece分词: {tokens}")
WordPiece使用##前缀标记非首字符的子词Token。
SentencePiece
SentencePiece是一种语言无关的分词工具,将文本视为Unicode字符序列处理。
import sentencepiece as spm
sp = spm.SentencePieceProcessor()
sp.load('model.model')
text = "This is a test."
tokens = sp.encode_as_pieces(text)
print(f"SentencePiece分词: {tokens}")
SentencePiece的特点:
- 不依赖预分词(如空格分割)
- 支持BPE和Unigram两种算法
- 广泛用于多语言模型
什么是上下文窗口
上下文窗口(Context Window)是大语言模型一次能处理的最大Token数量。它决定了模型能"看到"多少信息。
上下文窗口的组成
上下文窗口包含所有输入和输出Token:
- 系统提示(System Prompt):定义模型行为的指令
- 对话历史:之前的对话轮次
- 用户输入:当前的问题或请求
- 模型输出:模型生成的回复
为什么上下文窗口很重要
| 场景 | 小上下文窗口 | 大上下文窗口 |
|---|---|---|
| 长文档分析 | 需要分块处理 | 可一次处理整个文档 |
| 多轮对话 | 容易丢失早期上下文 | 保持完整对话记忆 |
| 代码理解 | 只能看部分代码 | 理解完整代码库 |
| RAG应用 | 检索结果受限 | 可包含更多相关文档 |
主流模型上下文窗口对比
| 模型 | 上下文窗口 | 发布时间 | 备注 |
|---|---|---|---|
| GPT-3.5 Turbo | 4K / 16K | 2023 | 16K版本成本更高 |
| GPT-4 | 8K / 32K / 128K | 2023-2024 | 128K为Turbo版本 |
| GPT-4o | 128K | 2024 | 多模态支持 |
| Claude 3 Opus | 200K | 2024 | 约15万单词 |
| Claude 3.5 Sonnet | 200K | 2024 | 性能/成本平衡 |
| Gemini 1.5 Pro | 1M / 2M | 2024 | 目前最大窗口 |
| LLaMA 3 | 8K / 128K | 2024 | 开源模型 |
| Qwen 2.5 | 128K | 2024 | 中文优化 |
上下文窗口vs实际可用长度
需要注意的是,标称的上下文窗口大小并不等于实际可用长度:
def calculate_available_context(total_window, system_prompt_tokens,
history_tokens, max_output_tokens):
"""计算实际可用的输入Token数"""
available = total_window - system_prompt_tokens - history_tokens - max_output_tokens
return max(0, available)
total = 128000
system = 500
history = 10000
max_output = 4096
available = calculate_available_context(total, system, history, max_output)
print(f"总窗口: {total}")
print(f"系统提示: {system}")
print(f"历史记录: {history}")
print(f"预留输出: {max_output}")
print(f"可用输入: {available}")
长上下文技术
随着对长上下文需求的增加,研究者开发了多种技术来扩展模型的上下文处理能力。
RoPE(旋转位置编码)
RoPE通过旋转矩阵编码位置信息,支持位置外推。
import numpy as np
def rope_embedding(x, position, d_model):
"""
RoPE位置编码
x: 输入向量
position: 位置索引
d_model: 模型维度
"""
freqs = 1.0 / (10000 ** (np.arange(0, d_model, 2) / d_model))
angles = position * freqs
cos_vals = np.cos(angles)
sin_vals = np.sin(angles)
x_even = x[0::2]
x_odd = x[1::2]
rotated_even = x_even * cos_vals - x_odd * sin_vals
rotated_odd = x_even * sin_vals + x_odd * cos_vals
result = np.zeros_like(x)
result[0::2] = rotated_even
result[1::2] = rotated_odd
return result
RoPE的优势:
- 相对位置信息自然编码
- 支持长度外推
- 计算效率高
ALiBi(Attention with Linear Biases)
ALiBi通过在注意力分数中添加线性偏置来编码位置信息。
def alibi_bias(seq_len, num_heads):
"""
计算ALiBi偏置矩阵
"""
slopes = np.array([2 ** (-8 * i / num_heads) for i in range(1, num_heads + 1)])
positions = np.arange(seq_len)
bias = -np.abs(positions[:, None] - positions[None, :])
alibi = slopes[:, None, None] * bias[None, :, :]
return alibi
Sliding Window Attention
滑动窗口注意力限制每个Token只关注固定范围内的Token,降低计算复杂度。
Token计数与成本估算
准确计算Token数量对于成本控制至关重要。
使用tiktoken计算Token
import tiktoken
def count_tokens(text, model="gpt-4"):
"""
计算文本的Token数量
"""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base")
return len(encoding.encode(text))
def estimate_cost(input_tokens, output_tokens, model="gpt-4"):
"""
估算API调用成本(美元)
"""
pricing = {
"gpt-4": {"input": 0.03, "output": 0.06},
"gpt-4-turbo": {"input": 0.01, "output": 0.03},
"gpt-4o": {"input": 0.005, "output": 0.015},
"gpt-3.5-turbo": {"input": 0.0005, "output": 0.0015},
"claude-3-opus": {"input": 0.015, "output": 0.075},
"claude-3-sonnet": {"input": 0.003, "output": 0.015},
}
if model not in pricing:
return None
input_cost = (input_tokens / 1000) * pricing[model]["input"]
output_cost = (output_tokens / 1000) * pricing[model]["output"]
return input_cost + output_cost
text = """
大语言模型(Large Language Model,LLM)是一种基于深度学习的自然语言处理模型,
通过在大规模文本数据上进行预训练,学习语言的统计规律和语义知识。
"""
tokens = count_tokens(text)
cost = estimate_cost(tokens, 500, "gpt-4o")
print(f"输入Token数: {tokens}")
print(f"预估输出Token: 500")
print(f"预估成本: ${cost:.4f}")
Token计数工具类
class TokenCounter:
"""Token计数和成本估算工具"""
def __init__(self, model="gpt-4"):
self.model = model
try:
self.encoding = tiktoken.encoding_for_model(model)
except KeyError:
self.encoding = tiktoken.get_encoding("cl100k_base")
def count(self, text):
"""计算Token数量"""
return len(self.encoding.encode(text))
def count_messages(self, messages):
"""计算对话消息的Token数量"""
total = 0
for message in messages:
total += 4
for key, value in message.items():
total += self.count(value)
if key == "name":
total += -1
total += 2
return total
def truncate_to_limit(self, text, max_tokens):
"""截断文本到指定Token数量"""
tokens = self.encoding.encode(text)
if len(tokens) <= max_tokens:
return text
return self.encoding.decode(tokens[:max_tokens])
def split_by_tokens(self, text, chunk_size, overlap=0):
"""按Token数量分割文本"""
tokens = self.encoding.encode(text)
chunks = []
start = 0
while start < len(tokens):
end = min(start + chunk_size, len(tokens))
chunk_tokens = tokens[start:end]
chunks.append(self.encoding.decode(chunk_tokens))
start = end - overlap
return chunks
counter = TokenCounter("gpt-4")
long_text = "这是一段很长的文本..." * 100
chunks = counter.split_by_tokens(long_text, chunk_size=100, overlap=20)
print(f"分割成 {len(chunks)} 个块")
优化上下文使用的策略
1. 精简系统提示
verbose_prompt = """
你是一个非常有帮助的AI助手。你的任务是帮助用户解答各种问题。
你应该始终保持友好、专业的态度。当用户提问时,你需要仔细分析问题,
然后给出详细、准确的回答。如果你不确定答案,请诚实地告诉用户。
"""
concise_prompt = """
你是专业的AI助手。提供准确、简洁的回答。不确定时如实说明。
"""
counter = TokenCounter()
print(f"冗长提示: {counter.count(verbose_prompt)} tokens")
print(f"精简提示: {counter.count(concise_prompt)} tokens")
2. 对话历史管理
def manage_conversation_history(messages, max_tokens, counter):
"""
管理对话历史,保持在Token限制内
策略:保留系统消息和最近的对话
"""
system_messages = [m for m in messages if m.get("role") == "system"]
other_messages = [m for m in messages if m.get("role") != "system"]
system_tokens = counter.count_messages(system_messages)
available_tokens = max_tokens - system_tokens
kept_messages = []
current_tokens = 0
for message in reversed(other_messages):
msg_tokens = counter.count_messages([message])
if current_tokens + msg_tokens <= available_tokens:
kept_messages.insert(0, message)
current_tokens += msg_tokens
else:
break
return system_messages + kept_messages
3. 文档分块策略
def smart_chunk_document(text, max_chunk_tokens=1000, overlap_tokens=100):
"""
智能文档分块:在句子边界处分割
"""
import re
counter = TokenCounter()
sentences = re.split(r'(?<=[。!?.!?])\s*', text)
chunks = []
current_chunk = []
current_tokens = 0
for sentence in sentences:
sentence_tokens = counter.count(sentence)
if current_tokens + sentence_tokens > max_chunk_tokens:
if current_chunk:
chunks.append(''.join(current_chunk))
overlap_text = ''.join(current_chunk[-2:]) if len(current_chunk) >= 2 else ''
current_chunk = [overlap_text, sentence] if overlap_text else [sentence]
current_tokens = counter.count(''.join(current_chunk))
else:
current_chunk.append(sentence)
current_tokens += sentence_tokens
if current_chunk:
chunks.append(''.join(current_chunk))
return chunks
4. 使用摘要压缩历史
def compress_history_with_summary(messages, summarizer_fn, threshold_tokens=2000):
"""
当历史过长时,使用摘要压缩早期对话
"""
counter = TokenCounter()
total_tokens = counter.count_messages(messages)
if total_tokens <= threshold_tokens:
return messages
system_msgs = [m for m in messages if m["role"] == "system"]
other_msgs = [m for m in messages if m["role"] != "system"]
split_point = len(other_msgs) // 2
old_msgs = other_msgs[:split_point]
recent_msgs = other_msgs[split_point:]
old_text = "\n".join([f"{m['role']}: {m['content']}" for m in old_msgs])
summary = summarizer_fn(old_text)
summary_msg = {"role": "system", "content": f"之前对话摘要:{summary}"}
return system_msgs + [summary_msg] + recent_msgs
工具推荐
在处理Token和上下文相关任务时,以下工具可以提升效率:
- JSON格式化工具 - 格式化API响应和模型配置
- 文本对比工具 - 比较不同分词结果的差异
- Base64编解码 - 处理嵌入向量的编码转换
- 文本分析工具 - 快速统计文本字符和词数
- 正则表达式测试 - 测试文本分割的正则模式
总结
理解Token和上下文窗口是高效使用大语言模型的基础:
- Token是LLM的基本单位:不同于字符或单词,Token由分词算法决定
- 分词算法各有特点:BPE、WordPiece、SentencePiece适用于不同场景
- 上下文窗口限制总Token数:包括输入、输出和对话历史
- 长上下文技术不断发展:RoPE、ALiBi等技术扩展了模型能力
- 成本优化需要精确计数:使用tiktoken等工具准确估算
- 多种策略可优化使用:精简提示、管理历史、智能分块
掌握这些知识,你就能更好地控制API成本,设计更高效的AI应用。
常见问题
为什么中文消耗的Token比英文多?
这与分词算法的训练数据有关。GPT等模型主要在英文语料上训练,英文常见词通常是单个Token,而中文字符往往需要多个Token表示。例如,"人工智能"可能需要3-4个Token,而"AI"只需要1个Token。使用针对中文优化的模型(如Qwen)可以提高中文Token效率。
上下文窗口越大越好吗?
不一定。更大的上下文窗口意味着:1)更高的API成本(按Token计费);2)更长的响应时间;3)可能的注意力分散(模型可能难以聚焦关键信息)。应根据实际需求选择合适大小的上下文窗口,并使用检索增强生成(RAG)等技术优化信息利用。
如何估算一段文本的Token数量?
最准确的方法是使用对应模型的分词器。对于OpenAI模型,使用tiktoken库;对于其他模型,使用相应的tokenizer。粗略估算时,英文约4个字符=1个Token,中文约1.5-2个字符=1个Token。在线工具如OpenAI的Tokenizer也可以快速计算。
对话历史会消耗上下文窗口吗?
是的,每轮对话的输入和输出都会累积在上下文中。这就是为什么长对话可能导致早期内容被截断。建议实现对话历史管理策略:保留最近的对话、使用摘要压缩历史、或在适当时候重置上下文。
不同模型的Token是否通用?
不通用。不同模型使用不同的分词器,同一文本在不同模型中的Token数量可能不同。例如,GPT-4使用cl100k_base编码,而BERT使用WordPiece。在切换模型时需要重新计算Token数量和成本。