Token 与上下文窗口:为什么会超限,如何估算与裁剪
上一篇文章我们实现了多轮对话,让程序自动记录历史消息。但随着对话越来越长,迟早会遇到一个错误——请求被拒绝,提示“token 超限”。这不是 bug,而是模型的能力边界:每一次请求的消息总量不能超过模型的上下文窗口。本篇文章将讲清楚 Token 和上下文窗口的关系,并写出可运行的估算与裁剪代码,让对话程序在长聊中也能稳定运行。
Token 是什么
大模型不直接处理自然语言文本,而是把文本切分成一个个更小的单元——Token。一个 Token 可以是一个完整的英文单词、一个单词片段、一个汉字或一个标点符号。
以 OpenAI 的 GPT-4 分词器为例:
1 | 原文:我喜欢用 Python 写代码。 |
英文单词通常 1 个 token 左右,中文一个汉字占 1—2 个 token。一条看起来不长的中文句子,token 数可能远超直觉。可以用 OpenAI 官方的 Tokenizer 工具 直观体验,也可以用 Python 库精确计算。
上下文窗口是什么
每个模型都有一个硬性的上下文窗口(Context Window),即单次请求中 messages 数组能包含的最大 token 数。常见的几个窗口值:
| 模型 | 上下文窗口 |
|---|---|
| GPT-4o-mini | 128,000 tokens |
| GPT-4o | 128,000 tokens |
| GPT-3.5-turbo | 16,385 tokens |
| DeepSeek-V3 | 128,000 tokens |
注意:上下文窗口包含输入和输出的总和。如果你发送 10,000 token 的消息,同时要求模型最多生成 4,096 token 的回复,实际消耗不会超过 14,096 token——远低于 128K,不会超限。
为什么多轮对话会超限
回顾上一篇文章:每轮对话都把全部历史消息和最新一条 user 消息一起发送。这意味着:
- 第 1 轮:system + 第 1 个问答
- 第 5 轮:system + 前 4 轮问答 + 当前问题
- 第 50 轮:system + 前 49 轮问答 + 当前问题
消息越来越长,token 数持续增长。即便每次问答只有 200 token,50 轮也约 10,000 token——对 128K 窗口不算什么,但如果每轮返回的是几千字的分析报告,或者用的是 4K/8K/16K 窗口的老模型,超限就会发生。
用 tiktoken 估算 token 数
OpenAI 开源了 tiktoken 库,可以精确计算文本在不同模型下的 token 数。安装:
1 | source .venv/bin/activate |
计算单条文本:
1 | import tiktoken |
在实际项目中,更常用的是根据消息角色估算,因为不同角色的消息计入 token 的方式略有不同(每条消息都包含元信息开销)。tiktoken 提供了一个便捷方法:
1 | import tiktoken |
更简单的方式是直接使用 API 返回的 usage 字段——它才是最终计费的依据,也最能反映实际 token 消耗。
裁剪历史:保持对话不超限
当估算的 token 数接近上下文窗口时,必须裁剪历史消息。核心原则:system 消息始终保留,旧对话从最前面开始丢弃。
裁剪策略如下:设定一个安全上限(比如模型窗口的 80%),超过上限时从最旧的 user/assistant 对开始删除,直到 token 数回到安全线以下。
最小可运行示例
1 | import tiktoken |
运行这段代码,你会看到 50 轮历史被裁剪到安全范围内。关键逻辑只有两点:保留 system 消息,从最旧的 user/assistant 对开始丢弃。
裁剪的注意事项
- 不要只删 user 不删 assistant。删除不配对的消息会破坏对话结构,可能导致模型行为异常。
- 裁剪是对信息的丢弃,模型会”忘记”被删掉的内容。如果某些旧信息仍然重要(如用户偏好、关键结论),应当在裁剪前做摘要压缩,把核心信息写回 system 消息。
- 不同模型的固定开销不同。上面的
tokens_per_msg = 3适用于 gpt-4o 系列。其他模型需查阅官方文档确认,OpenAI 对 gpt-3.5-turbo 系列是 4。
如果不用 tiktoken
并非所有模型都有官方 Token 计数库。对于其他模型,以下方法可以兜底:
- 使用 API 返回的 usage 字段。每次请求后,
response.usage.total_tokens给出实际消耗。在裁剪逻辑中,可以把最近 N 条消息的累计 token 数作为估算依据。 - 按中文字符粗略估算。一条中文消息的 token 数大致在字符数的 1.2—2.5 倍之间。这个比例不稳定,只能用于非严格场景。
- 留足安全余量。如果把安全阈值设为模型窗口的 60% 而不是 80%,即使估算有误差也不容易超限。
常见问题
tiktoken 报错 Unknown encoding。 可能是模型名写错了,或者用的是不兼容的非 OpenAI 模型。对非 OpenAI 模型直接使用 API 返回的 usage 字段来跟踪 token 消耗,不要依赖 tiktoken。
裁剪后模型回答质量下降。 裁剪导致模型丢失了重要的前置信息。可以考虑在裁剪前让另一个模型调用做一次摘要,把摘要作为 system 消息的补充。
count_tokens 的结果和 API 返回的不一致。 这是正常的。tiktoken 的本地估算和 API 服务器的实际计数可能有微小差异(通常 < 1%)。生产环境中以 API 返回值为准。
明明没超过窗口,API 还是报错。 检查 max_tokens 参数——它指的是模型最多生成多少 token。如果你同时设了很大的 max_tokens,可能会导致输入 + 输出超出窗口。调整 max_tokens 或减少输入长度。
小结
- Token 是模型处理文本的最小单元,中英文的 token 密度不同。
- 上下文窗口是模型单次请求能处理的 token 上限,包含输入和输出。
- 多轮对话随时间推移必然增长,需要主动估算和裁剪。
- 裁剪时务必保留 system 消息,从最旧的 user/assistant 对开始删除。
- 以 API 返回的 usage 为准,tiktoken 作辅助估算。
掌握了 token 估算与裁剪,你的多轮对话程序就不会再因为“聊太久”而崩溃。下一篇文章我们将讨论常用的生成参数——temperature、top_p 和 max_tokens——以及它们如何影响输出质量。