AI实践(3)Token与上下文窗口


Author: Once Day Date: 2026年3月2日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…

漫漫长路,有人对你微笑过嘛…

全系列文章可参考专栏: AI实践成长_Once-Day的博客-CSDN博客

参考文章:


1. 令牌(Token)

Token 是大语言模型(LLM)在理解和生成文本时的最小处理单元。它是模型将语言拆分成的"小块"数据,以便作为原始信息进行处理。需要注意的是,Token 并不完全等同于日常理解中的"字"或"词",而是介于两者之间的"子词(subword)“或"字节序列(byte sequence)”。

子词分词算法遵循的核心原则是:高频词不应被拆分为更小的子词,而低频词则应被分解为有意义的子词片段。例如,英文单词 tokenization 可能会被拆分为 tokenization 两个 Token,而常见的短词 hello 通常会被保留为一个完整的 Token。每个 Token 会被映射为一个唯一的数值 ID,形成一个数字序列供模型进行数学运算,因为神经网络无法直接"阅读"字母或文字,只能对数字进行运算。

1.1 分词器(Tokenizer)

将原始文本转化为 Token 序列的工具称为分词器(Tokenizer。常见的分词算法包括字节对编码(Byte Pair Encoding, BPE)、WordPiece 等,它们基于训练数据中发现的统计模式来对文本进行切分。以 BPE 为例,它是一种数据压缩算法,通过反复合并最频繁出现的字符或字节对来构建子词词汇表,使分词过程既高效又灵活,被广泛应用于 GPT 系列模型中。下面是一个使用 OpenAI 提供的 tiktoken 库进行分词的简单示例:

import tiktoken

# 加载 GPT-4o 使用的分词器
enc = tiktoken.encoding_for_model("gpt-4o")

# 对文本进行编码
text = "Hello, tokenization is great!"
tokens = enc.encode(text)
print(tokens)   # 输出 Token ID 列表,如 [13225, 11, 5765, 2065, 382, 2294, 0]

# 将 Token ID 解码回文本
print(enc.decode(tokens))  # "Hello, tokenization is great!"

不同模型厂商和模型系列使用的分词器存在差异,其词汇表(vocabulary)也各不相同。例如,OpenAI 使用 tiktoken,而 LLaMA 使用 SentencePiece,二者对同一段文本的拆分方式和产生的 Token 数量可能不同。当前主流模型的词汇表大小通常在 50K~100K 个 Token 左右,这是在序列压缩效率与词汇覆盖范围之间取得的平衡点。因此,在进行跨模型的 Token 数量估算时,必须使用与目标模型匹配的分词器,否则结果可能出现偏差。

1.2 不同语言的 Token 差异

分词效率在不同语言之间存在显著差异。英文是使用成本最低的语言,因为它采用拉丁字母,词形变化相对简单,并且绝大多数训练数据都是英文。在英文中,100 个单词大约对应 130 个 Token。而对于中日韩等非拉丁字母语言,情况则复杂得多。中文的 Token 数甚至可能超过其字符数,这是因为 BPE 算法基于字节而非字符进行操作,而单个中文字符在 UTF-8 编码下占用多个字节。

以下表格展示了不同语言在分词效率上的大致对比(以英文为基准):

语言类别 代表语言 相对英文的分词效率 说明
拉丁字母语言 英文 1x(基准) 训练数据最多,压缩率最高
其他拉丁字母语言 西班牙语、波兰语 ~1/2x 词形变化更丰富
非拉丁字母语言 俄语、希伯来语 ~1/3x 字符编码占用更多字节
CJK 语言 中文、日文 ~1/2x 字符字节数多,但单字信息密度高

分词器使用 UTF-8 编码,拉丁字母生成的字节序列更短,而非拉丁字符可能编码为 2~3 个字节,导致某些语言的 Token 数量甚至超过字符数量。这一差异直接影响 API 调用成本和上下文窗口的有效利用率。

1.3 令牌计数的作用

在实际开发中,准确的 Token 计数具有多方面的实用价值。首先,它可以用于优化提示词,确保输入内容不超过模型的上下文长度限制,避免文本被截断而导致信息丢失。由于 OpenAI 等平台的计费方式是按 Token 数量收费的,预先对文本进行分词可以有效估算 API 调用成本。

此外,在构建复杂的 AI 应用时,Token 计数还可以用于请求路由——将较短的提示词分配给更快速的轻量模型处理,从而优化整体响应延迟和成本。对于包含图片、PDF 等多模态内容的输入,Token 计数尤为重要,因为这些内容的 Token 占用量无法简单地通过字符数来估算。

1.4 令牌计数 API

虽然可以在客户端使用 tiktoken 等库进行本地 Token 估算,但这种方式存在局限性。系统提示词、工具定义(tool schemas)以及安全包装层等都会在服务端自动注入隐藏的 Token,这些额外开销无法在客户端完全预知。因此,各模型厂商通常提供服务端的 Token Count API,支持对完整的消息结构(包括工具定义、图片和文档)进行精确的 Token 计数。

AnthropicClaude 为例,调用令牌计数 API 的方式如下:

import anthropic

client = anthropic.Anthropic()

response = client.messages.count_tokens(
    model="claude-sonnet-4-20250514",
    system="You are a helpful assistant.",
    messages=[{
        "role": "user",
        "content": "What is the capital city of France?"
    }],
)
print(response.input_tokens)  # 输出精确的输入 Token 数

通过在推理之前获取精确的 Token 数量,开发者能够更准确地预估成本,并对 AI 模型的使用获得更大的透明度和控制力。这也有助于主动管理 Token 限额、避免意外的速率限制,并确保请求内容适配模型的上下文长度限制,从而实现更高效的提示词优化。

2. 上下文窗口(ContextWindows)

随着对话轮次的增加,输入内容会不断累积,最终逼近模型的上下文窗口限制。上下文窗口(Context Window)是指语言模型在生成响应时能够引用的全部文本范围,包括系统提示词、用户输入、对话历史以及模型自身的输出,所有内容都必须容纳在这个窗口之内。上下文窗口本质上是模型的"工作记忆"(Working Memory),它与模型在训练阶段所学习的大规模语料库截然不同。训练语料赋予模型通用的语言理解和知识储备,而上下文窗口则决定了模型在当前会话中能"看到"多少信息。

在这里插入图片描述

上下文窗口的大小以 Token 为单位来衡量。例如 GPT-4.1 的上下文窗口约为 128,000 个 Token,按照平均每个 Token 约 3.5 个字符来估算,大约对应 450,000 个字符。近年来,各主流模型的上下文窗口规模持续扩张。自 2023 年中以来,最大的上下文窗口长度以每年约 30 倍的速度增长。下表列出了当前主流模型的上下文窗口大小:

模型 上下文窗口
GPT-4o 128K tokens
GPT-4.1 1M tokens
Claude Sonnet 4 1M tokens
Gemini 2.5 Pro/Flash 1M tokens
Llama 4 Scout 10M tokens
DeepSeek R1/V3 128K tokens

上下文窗口的扩展是 LLM 领域最重要的进步之一,更大的窗口使得用户能够处理日益复杂的高容量任务,无论是分析代码库、处理大量法律文档,还是管理多轮客户交互。

需要特别注意的是,更大的上下文窗口并不意味着更好的效果。研究人员在对多个模型进行大规模测试后发现,模型标称的最大上下文窗口(MCW)与实际有效上下文窗口(MECW)之间存在显著差异,且有效窗口大小还会随任务类型的不同而变化。即使是 2025 年的最新研究也持续验证了"中间遗失"(Lost-in-the-Middle)现象——模型对上下文开头和结尾处的信息检索效果较好,但对埋藏在中间位置的数据则表现不佳。

这种随上下文长度增长而产生的性能下降现象被称为"上下文腐烂"(Context Rot)。Context Rot 是指随着输入上下文变长,模型的输出会变得更不准确、更不可靠——即便上下文窗口远未被填满。Chroma 在 2025 年的一项研究中测试了包括 GPT-4.1Claude 4Gemini 2.5 等在内的 18 个前沿模型,发现每一个模型都会随着输入长度的增加而性能下降。

Context Rot 的根源在于 Transformer 架构的固有特性。LLM 基于 Transformer 架构,其中每个 Token 都需要与上下文中的其他所有 Token 计算注意力分数,对于 n 个 Token 会产生 n² 个成对关系。随着上下文长度增加,模型捕捉这些成对关系的能力被"摊薄",在上下文规模与注意力聚焦之间形成天然的张力。Context Rot 有三个主要成因:首先是"中间遗失"效应,模型对上下文开头和结尾的 Token 注意力强,对中间部分则明显不足;其次是注意力的二次方扩展,更多的 Token 导致需要追踪的成对关系呈指数级增长;第三是语义干扰项的干扰,与主题相关但实际无关的内容会混淆模型对关键信息的识别。

这一现象的实际影响十分直观:一项斯坦福的研究发现,仅包含 20 篇检索文档(约 4,000 个 Token)时,模型的准确率就可能从 70-75% 下降到 55-60%。在另一项基准测试中,模型在短提示上的得分超过 95%,但在含有语义相关干扰内容的长上下文中降至 60-70%。

因此,上下文应当被视为一种有限资源,其边际收益是递减的。与人类有限的工作记忆类似,LLM 拥有一个"注意力预算"(Attention Budget),每引入一个新的 Token 都会在一定程度上消耗这个预算,这使得精心筛选进入上下文的 Token 变得至关重要。这一思路催生了"上下文工程"(Context Engineering)这一新兴实践方向。Context Engineering 是指在 LLM 推理过程中,对最优 Token 集合进行筛选和维护的方法论。与关注单次输入的提示词工程不同,上下文工程管理的是多轮交互中的整个上下文状态——包括何时添加信息、何时移除信息以及何时进行摘要压缩。

对于开发者而言,关键的启示在于:精心管理上下文中的内容与扩大上下文空间同样重要。在实践中,应当优先考虑将最相关、最高质量的信息放入上下文窗口,而非简单地追求"把所有内容都塞进去"。

3. 压缩上下文(Compaction)

在实际使用大语言模型的过程中,多轮对话会导致上下文不断累积。每一轮的用户输入和模型回复都会作为历史消息保留在上下文窗口中,随着对话轮次增加,Token 消耗也持续增长。当上下文逼近窗口上限时,模型要么拒绝继续生成,要么被迫丢弃早期信息,这两种情况都会严重影响对话质量。压缩上下文(Compaction)正是为了解决这一问题而提出的实践策略。

Compaction 的核心思路并不复杂:当对话内容接近上下文窗口的容量限制时,利用模型自身的能力对已有对话进行摘要提炼,然后用这份精简的摘要替换原始的冗长历史,开启一个新的上下文窗口继续对话。这一过程可以类比为"读书笔记"——不需要逐字保留整本书的内容,只需记录关键信息和结论,就能在后续讨论中维持连贯性。

具体的实现流程通常如下:

[对话轮次 1~N] → 上下文接近窗口上限
       ↓
调用模型对历史对话生成摘要(高保真压缩)
       ↓
以摘要作为新上下文的起点,继续后续对话
       ↓
[对话轮次 N+1 ~ M] → 再次接近上限时重复上述过程

这种策略在长期运行的智能体(Agent)场景中尤为重要。智能体往往需要跨越数十甚至上百轮交互来完成复杂任务,如果不做任何压缩处理,上下文窗口很快就会耗尽。Compaction 通常被视为上下文工程(Context Engineering)中首先考虑的手段,因为它在实现上相对简单,且效果立竿见影。

当然,压缩必然伴随信息损失,关键在于如何在压缩率和信息保真度之间取得平衡。实践中常见的做法是在摘要时保留几类核心要素:用户的关键意图、已确认的事实和决策、尚未完成的任务状态,以及重要的约束条件。而那些寒暄内容、重复确认、中间推理的试错过程等,则可以安全地舍弃。不同场景下的压缩粒度需要根据任务特性来调整——编程辅助场景可能需要保留更多代码细节,而日常问答场景则可以压缩得更为激进。

从成本角度看,Compaction 带来的收益十分显著。压缩后的上下文通常只有原始长度的 10%~30%,这意味着后续每一轮对话的输入 Token 数量大幅减少,直接降低了 API 调用费用。同时,更短的输入也意味着更低的推理延迟,模型的响应速度会有明显改善。虽然执行压缩本身需要额外消耗一次模型调用,但与持续携带膨胀的完整历史相比,总体开销通常更低。

4. 提示词缓存

在调用大语言模型的 API 时,每次请求通常都携带大量重复内容。例如,一个客服助手的系统提示(System Prompt)可能长达数千个 Token,包含角色设定、行为规范、输出格式要求等,而这部分内容在每一次用户提问时都完全相同。如果每次请求都让模型从头处理这些相同的前缀内容,无疑是对算力和费用的浪费。提示词缓存(Prompt Caching)正是针对这一问题的优化机制。

Prompt Caching 的基本原理是:当 API 服务端检测到当前请求的提示前缀与近期某次请求完全一致时,可以直接复用之前的计算结果,而无需重新处理这部分内容。以 OpenAI 为例,其平台会将 API 请求路由到最近处理过相同提示前缀的服务器节点上,从而跳过重复的前缀计算阶段。这种机制对调用方是透明的,不需要额外的代码改动,平台会自动判断是否命中缓存。AnthropicClaude 同样提供了类似的缓存功能,但需要开发者通过特定的 API 参数显式标记可缓存的内容段。

需要注意的是,缓存命中的条件是精确的前缀匹配。这意味着只有从提示开头起连续一致的部分才能被缓存复用,一旦某个位置出现差异,该位置之后的所有内容都需要重新计算。因此,提示的结构编排对缓存效率有直接影响。推荐的做法是将提示内容按照稳定性从高到低排列:

┌─────────────────────────────┐  ← 最前面:系统指令、角色设定(几乎不变)
├─────────────────────────────┤
│ 少样本示例(Few-shot Examples) │  ← 中间:示例和工具定义(偶尔调整)
├─────────────────────────────┤
│  工具定义(Tools / Functions)  │
├─────────────────────────────┤
│  用户消息、对话历史             │  ← 最后面:每次请求都不同的内容
└─────────────────────────────┘

按照这种结构组织,静态的系统提示和示例部分始终位于前缀位置,能够在多次请求间稳定命中缓存。而每次变化的用户输入放在末尾,不会破坏前面的缓存匹配。同样的原则也适用于提示中包含的图片和工具定义——它们在请求之间必须保持一致的顺序和内容,否则会导致缓存失效。

从实际收益来看,提示词缓存在两个维度上带来改善。其一是成本降低,以 OpenAI 的定价为例,缓存命中部分的 Token 费用通常为正常输入价格的 50%,Claude 的缓存读取价格则低至原价的 10%。其二是延迟优化,跳过前缀计算意味着模型可以更快地开始生成回复,对于包含长系统提示的场景,首个 Token 的响应时间可以缩短数百毫秒甚至更多。对于需要频繁调用 API 且系统提示较长的应用场景,合理利用提示词缓存是一项投入极低但回报可观的优化措施。

5. Token 计数实践

在实际开发中,准确掌握提示内容所消耗的 Token 数量是成本控制和上下文管理的基础。由于不同模型采用不同的分词器(Tokenizer),同一段文本在不同模型下的 Token 数量可能存在差异,仅凭字数或词数进行估算往往不够可靠。因此,借助专用的分词工具进行精确计数是更为稳妥的做法。

OpenAI 官方提供的 tiktoken 是目前最常用的 Token 计数库,使用 Python 编写,安装和调用都非常简单:

pip install tiktoken

以下示例展示了如何针对不同模型计算一段文本的 Token 数量:

import tiktoken

# 根据模型名称获取对应的编码器
enc = tiktoken.encoding_for_model("gpt-4o")

text = "大语言模型通过 Token 来理解和生成文本。"
tokens = enc.encode(text)

print(f"文本内容: {text}")
print(f"Token 数量: {len(tokens)}")
print(f"Token 列表: {tokens}")
print(f"Token 解码: {[enc.decode([t]) for t in tokens]}")

运行上述代码,可以直观地观察到中文文本被拆分成 Token 的具体方式。一个值得注意的现象是,中文字符通常比英文单词消耗更多的 Token——一个常见的英文单词往往对应 1 个 Token,而一个中文汉字则可能消耗 1~2 个 Token。这也是为什么同等语义长度下,中文提示的费用通常略高于英文提示。

在实际工程中,Token 计数常用于以下场景:在发送 API 请求之前检查是否超出模型的上下文窗口限制;在多轮对话中决定何时触发上下文压缩;以及预估一次调用的费用开销。一个典型的用法是编写预检函数:

import tiktoken

def check_token_limit(messages, model="gpt-4o", max_tokens=128000):
    """检查消息列表是否超出上下文窗口限制"""
    enc = tiktoken.encoding_for_model(model)
    total = 0
    for msg in messages:
        # 每条消息本身有固定的格式开销(约 4 个 Token)
        total += 4
        total += len(enc.encode(msg["content"]))
    return total, total < max_tokens

除了 tiktoken 之外,还有一些其他途径可以获取 Token 计数信息。OpenAIAPI 响应中包含 usage 字段,会返回本次请求实际消耗的输入和输出 Token 数量,可用于事后统计和账单核对。AnthropicClaude API 同样在响应中提供类似的用量信息。对于不方便编写代码的场景,OpenAI 官方还提供了在线的 Tokenizer 工具,可以在浏览器中直接粘贴文本查看分词结果。

对于使用 Claude 系列模型的开发者,Anthropic 官方提供了独立的 Token 计数 API,可以在不实际执行推理的情况下获取消息的 Token 数量,这在需要精确预算但又不想浪费推理调用的场景下尤为实用。养成在开发阶段关注 Token 消耗的习惯,有助于在项目早期就建立合理的成本模型,避免上线后出现超预期的费用增长。







Alt

Once Day

也信美人终作土,不堪幽梦太匆匆......

如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注!

(。◕‿◕。)感谢您的阅读与支持~~~

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐