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的序列。

graph LR subgraph "Token化过程" A["原始文本"] --> B["分词器 Tokenizer"] B --> C["Token序列"] C --> D["Token ID"] D --> E["嵌入向量 Embedding"] end subgraph "示例" T1["Hello world"] --> T2["Hello, world"] T2 --> T3["[15496, 995]"] end

Token与字符、单词的区别

概念 定义 示例
字符 最小的文本单位 H, e, l, l, o
单词 由空格分隔的文本 Hello, world
Token 模型处理的基本单位 Hello, wor, ld

Token的粒度介于字符和单词之间。常见单词通常是一个Token,而罕见词或长词会被拆分成多个Token。

为什么使用Token而非单词

  1. 词汇表大小可控:英语有数十万单词,而Token词汇表通常只有3-5万
  2. 处理未知词:任何文本都能被分解为已知Token的组合
  3. 跨语言支持:同一分词器可以处理多种语言
  4. 子词共享:相关词汇共享子词Token,如"run"、"running"、"runner"

Token化过程详解

Token化(Tokenization)是将原始文本转换为Token序列的过程。

python
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]}")

输出示例:

code
原始文本: Hello, world! 你好,世界!
Token数量: 11
Token ID: [9906, 11, 1917, 0, 220, 57668, 53901, 3922, 244, 98220, 6447]
解码后: ['Hello', ',', ' world', '!', ' ', '你好', ',', '世', '界', '!']

不同语言的Token效率

中文和英文的Token效率差异显著:

python
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系列模型使用的分词算法,通过迭代合并最频繁的字符对来构建词汇表。

graph TB subgraph "BPE训练过程" S1["初始: 所有字符"] --> S2["统计字符对频率"] S2 --> S3["合并最频繁的对"] S3 --> S4["更新词汇表"] S4 --> S5{"达到目标 词汇表大小?"} S5 -->|否| S2 S5 -->|是| S6["完成"] end
python
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类似但使用不同的合并策略。

python
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字符序列处理。

python
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数量。它决定了模型能"看到"多少信息。

graph LR subgraph "上下文窗口示意" direction TB W["上下文窗口 (如: 128K tokens)"] W --> P["系统提示 System Prompt"] W --> H["对话历史 History"] W --> I["用户输入 User Input"] W --> O["模型输出 Output"] end

上下文窗口的组成

上下文窗口包含所有输入和输出Token:

  1. 系统提示(System Prompt):定义模型行为的指令
  2. 对话历史:之前的对话轮次
  3. 用户输入:当前的问题或请求
  4. 模型输出:模型生成的回复

为什么上下文窗口很重要

场景 小上下文窗口 大上下文窗口
长文档分析 需要分块处理 可一次处理整个文档
多轮对话 容易丢失早期上下文 保持完整对话记忆
代码理解 只能看部分代码 理解完整代码库
RAG应用 检索结果受限 可包含更多相关文档

主流模型上下文窗口对比

graph TB subgraph "2024-2026 上下文窗口演进" G3["GPT-3.5 4K/16K"] --> G4["GPT-4 8K/32K/128K"] G4 --> G4T["GPT-4 Turbo 128K"] C2["Claude 2 100K"] --> C3["Claude 3 200K"] G1["Gemini 1.0 32K"] --> G15["Gemini 1.5 1M/2M"] end
模型 上下文窗口 发布时间 备注
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实际可用长度

需要注意的是,标称的上下文窗口大小并不等于实际可用长度:

python
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通过旋转矩阵编码位置信息,支持位置外推。

python
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通过在注意力分数中添加线性偏置来编码位置信息。

python
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,降低计算复杂度。

graph LR subgraph "滑动窗口注意力" T1["Token 1"] --> W1["窗口 1-3"] T2["Token 2"] --> W2["窗口 1-4"] T3["Token 3"] --> W3["窗口 1-5"] T4["Token 4"] --> W4["窗口 2-6"] T5["Token 5"] --> W5["窗口 3-7"] end

Token计数与成本估算

准确计算Token数量对于成本控制至关重要。

使用tiktoken计算Token

python
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计数工具类

python
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. 精简系统提示

python
verbose_prompt = """
你是一个非常有帮助的AI助手。你的任务是帮助用户解答各种问题。
你应该始终保持友好、专业的态度。当用户提问时,你需要仔细分析问题,
然后给出详细、准确的回答。如果你不确定答案,请诚实地告诉用户。
"""

concise_prompt = """
你是专业的AI助手。提供准确、简洁的回答。不确定时如实说明。
"""

counter = TokenCounter()
print(f"冗长提示: {counter.count(verbose_prompt)} tokens")
print(f"精简提示: {counter.count(concise_prompt)} tokens")

2. 对话历史管理

python
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. 文档分块策略

python
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. 使用摘要压缩历史

python
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和上下文相关任务时,以下工具可以提升效率:

总结

理解Token和上下文窗口是高效使用大语言模型的基础:

  1. Token是LLM的基本单位:不同于字符或单词,Token由分词算法决定
  2. 分词算法各有特点:BPE、WordPiece、SentencePiece适用于不同场景
  3. 上下文窗口限制总Token数:包括输入、输出和对话历史
  4. 长上下文技术不断发展:RoPE、ALiBi等技术扩展了模型能力
  5. 成本优化需要精确计数:使用tiktoken等工具准确估算
  6. 多种策略可优化使用:精简提示、管理历史、智能分块

掌握这些知识,你就能更好地控制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数量和成本。