Token 与上下文窗口:先算清楚,再放进模型
正在读取学习记录…
token 是模型处理文本时使用的片段;上下文窗口是一次请求能够容纳的 token 范围。发给模型的内容不只有用户最后一句话,还可能包括指令、历史和工具结果;回答也需要预留空间。
把一次请求想成容量有限的箱子。我们先用分词器观察一段文字怎样拆成 token,再给一条正常请求分配输入与输出预算。看懂这份账后,再分析为什么 Agent 调用几次工具就可能塞满窗口。练习容量设为8192;接入模型时,从对应接口文档读取窗口大小和计数规则。
0. 为什么 Agent 比普通聊天更容易把窗口塞满
一次普通问答可能只有系统指令和一条用户消息。Agent 每执行一步,还会追加工具定义、工具请求、工具返回、检索资料和中间状态;下一轮又把其中一部分重新发送给模型。窗口是单次请求可容纳的计算范围,不是永久记忆,更不是装得越满越聪明。

| Agent 请求中的内容 | 为什么会增长 | 完全丢掉会怎样 |
|---|---|---|
| 系统与业务规则 | 通常每轮都要保留 | 模型可能越权或偏离任务 |
| 对话与决策历史 | 每轮继续追加 | 忘记用户限制或重复提问 |
| 工具参数规则(Schema) | 工具越多定义越长 | 模型缺少参数填写规则 |
| 工具返回 | 日志、网页和 JSON 可能很大 | 无法根据真实结果继续 |
| 检索资料 | 每轮问题可能召回新片段 | 回答缺少依据 |
| 输出预留 | 需要提前留出 | 输入装满后没有生成空间 |
工具参数规则像一张填写说明:查询天气要填城市,查询订单要填订单号。Schema 描述有哪些字段、各填什么类型,以及哪些必须填写。这些说明也要发给模型,因此同样占用窗口。
1. 字符、字节、token是三个数量
Python 的 len(text) 回答“这段字符串有多少个字符位置”,UTF-8 编码后的字节数又是另一种数量。模型计费和窗口使用的 token 数还取决于分词规则:中文、英文、代码、空格和 emoji 的切法可能不同。因此,在要做容量检查的地方,请使用目标模型对应的计数方法;不要把“字符数除以 4”当作精确公式。
token计数应使用目标模型匹配的分词器和序列化方式。只编码正文可能漏掉聊天模板、角色标记、工具描述和特殊token;图片、音频等多模态输入还可能采用专门计数规则。模型服务返回的usage可用于事后核对,但请求前仍需要预算。
| 数量 | 用途 | 不能替代什么 |
|---|---|---|
| 字符或字节 | 输入限制、存储、网络大小 | 精确模型token数 |
| 输入token | 模型请求容量与部分计费依据 | 输出token或工具响应耗时 |
| 输出token | 生成长度与部分计费依据 | 任务完成质量 |
| 账单金额 | 按供应商规则结算 | 简单把全部token乘同一个价格 |
亲自比较两种真实编码
下面结果来自本地 tiktoken 0.9.0 的实际运行,不是字符数估算。使用 cl100k_base 和 o200k_base 编码同一段原始文本,每条还检查了解码后能否还原原文。tiktoken 官方项目与编码示例
| 原始文本 | Python码点数 | UTF-8字节数 | cl100k_base tokens | o200k_base tokens |
|---|---|---|---|---|
| hello world | 11 | 11 | 2 | 2 |
| 请先校验工具参数,再执行请求。 | 15 | 45 | 13 | 10 |
| print("hello") | 14 | 14 | 4 | 4 |
| 🙂 | 1 | 4 | 2 | 1 |
同一句中文在两种编码中分别是13和10,不是“一字一token”。单个emoji也可能拆成多个token。这个小表只说明这些文本在指定编码下的结果,不能据此判断哪个模型整体更省钱。
先创建并进入实验目录,准备独立环境:
mkdir agentdepth-token-lab
Set-Location agentdepth-token-lab
py -3.13 -m venv .venv
$env:Path = "$PWD\.venv\Scripts;$env:Path"
python -c "import sys; print(sys.executable)"
python -m pip install tiktoken==0.9.0macOS 终端命令
mkdir agentdepth-token-lab
cd agentdepth-token-lab
python3.13 -m venv .venv
source .venv/bin/activate
python -c "import sys; print(sys.executable)"
python -m pip install tiktoken==0.9.0安装结束后,确认当前 Python 能找到这个包:
python -c "import importlib.metadata; print(importlib.metadata.version('tiktoken'))"应显示 0.9.0。接下来保存并运行计数程序。安装包和首次运行是两个联网步骤:安装时获取 Python 库,第一次使用某种编码时还会获取该编码的数据,并保存在下面指定的 .cache/tiktoken 目录。
然后复制下方完整代码,保存为 count_tokens.py,保存到刚创建的 agentdepth-token-lab 目录;运行 Get-ChildItem count_tokens.py 确认位置,再执行下面的脚本。
保存为 count_tokens.py:
"""使用指定的分词规则,显示文字拆成的Token及数量。"""
import argparse
import importlib.metadata
import json
import os
from pathlib import Path
import tiktoken
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--cache-dir', required=True, help='Explicit cache location; first run may download encoding data')
args = parser.parse_args()
cache = Path(args.cache_dir).resolve()
cache.mkdir(parents=True, exist_ok=True)
os.environ['TIKTOKEN_CACHE_DIR'] = str(cache)
samples = ['hello world', '请先校验工具参数,再执行请求。', 'print("hello")', '🙂']
rows = []
for name in ['cl100k_base', 'o200k_base']:
encoding = tiktoken.get_encoding(name)
for text in samples:
ids = encoding.encode(text)
assert encoding.decode(ids) == text, 'round-trip failed'
rows.append({'encoding': name, 'text': text, 'codepoints': len(text),
'utf8_bytes': len(text.encode('utf8')), 'tokens': len(ids), 'ids': ids})
print(json.dumps({'tiktoken_version': importlib.metadata.version('tiktoken'),
'scope': 'raw text only, not provider request or bill', 'rows': rows}, ensure_ascii=False, indent=2))python -B -X utf8 count_tokens.py --cache-dir .cache/tiktoken运行时会输出文本、token 数量及其编号。下面列出记录中的各组编号:
| 编码 | 输入文本 | token 数量 | token 编号 |
|---|---|---|---|
| cl100k_base | hello world | 2 | 15339, 1917 |
| cl100k_base | 请先校验工具参数,再执行请求。 | 13 | 15225, 61826, 15308, 94, 42462, 49792, 77913, 33765, 3922, 88356, 76217, 35959, 1811 |
| cl100k_base | print("hello") | 4 | 1374, 446, 15339, 909 |
| cl100k_base | 🙂 | 2 | 9468, 19044 |
| o200k_base | hello world | 2 | 24912, 2375 |
| o200k_base | 请先校验工具参数,再执行请求。 | 10 | 10394, 12231, 25336, 22047, 69771, 41513, 130554, 65988, 54875, 788 |
| o200k_base | print("hello") | 4 | 1598, 568, 24912, 1405 |
| o200k_base | 🙂 | 1 | 37459 |
两种编码分别按各自的分词规则计算文本片段;DeepSeek 请求还包含聊天模板、工具 Schema 等内容。做真实账单对账时,应使用目标模型及供应商的计数方式,不能把文本片段的数字直接当作完整请求用量。
安装或首次运行遇到报错时
先看错误发生在哪一步,再按对应的一行处理:
| 出现的提示 | 原因与操作 | 修复后看什么 |
|---|---|---|
| 安装时下载超时或出现 SSL 错误 | 检查网络,按组织要求配置代理或可信包源,再重新执行安装命令 | 运行上面的版本检查,显示 0.9.0 |
No module named tiktoken | 安装和运行可能用了不同的 Python。运行下面两条命令确认位置,然后用当前 python -m pip install tiktoken==0.9.0 安装 | 版本检查能找到包 |
包版本能显示,get_encoding 却连接超时 | 编码数据首次下载尚未完成。恢复网络后,原样重跑带 --cache-dir 的计数命令 | 程序输出8组计数,中文分别为13和10 |
缓存目录出现 PermissionError | 当前目录不可写。将整个学习文件夹放到可写位置,在该文件夹重新运行 | .cache/tiktoken 能创建,随后输出计数 |
检查解释器和安装工具的位置:
python -c "import sys; print(sys.executable)"
python -m pip --version第一条指向学习目录中的 .venv\Scripts\python.exe,第二条的包路径应在同一个 .venv 内。若位置不同,先在学习目录重新执行 $env:Path = "$PWD\.venv\Scripts;$env:Path",再安装和检查版本。
网络暂时不可用时,可以先继续第3节的容量预算:budget.py 使用 Python 自带的工具,按给定数字做加减。分词实验恢复后再运行,观察文字实际拆成了多少 token。
2. 上下文窗口怎样分配输入和输出
对许多自回归模型,可用上下文需要同时容纳输入与生成部分。服务还可能另设最大输入、最大输出、推理token等约束,必须逐项读取模型接口,不能只取一个宣传数字。
把窗口想成容量固定的背包:问题、历史和工具说明已经占了一部分,回答也要提前留位置,剩下的空间才装检索资料。先看下一节的具体数字,再把这笔账写成公式。
在 Transformers 中,max_new_tokens 限制新生成的长度,也就是背包里“留给回答”的部分。输入提示的长度由输入容量约束管理。生成长度参数
3. 把一次请求拆成预算表
假设教学窗口8192,预留输出1024,余量256:
| 分项 | 假设token数 | 为什么不能漏 |
|---|---|---|
| 固定指令 | 512 | 行为约束也是输入 |
| 当前问题 | 128 | 本次任务的内容 |
| 保留历史 | 2048 | 前文结果与决策 |
| 工具定义与包装 | 640 | 名称、参数Schema和协议字段 |
| 输出预留 | 1024 | 不能把所有容量先塞满 |
| 安全余量 | 256 | 覆盖未精确建模的开销 |
| 留给检索内容 | 3584 | 由上述各项扣除后得到 |
加入3000 tokens的资料,还剩584;加入4000则超出416。先按分项查看占用,再选择缩短历史、压缩资料或减少候选数量,就知道空间是从哪里省出来的。
把数字换成符号:总容量记作 W,回答预留记作 O,余量记作 M,输入各项记作 I。sum(I) 表示把这些输入相加,因此剩余资料容量为 W - O - M - sum(I),也就是上面这笔减法。
这个表只有算术意义。真实项目应保存各分项计数来源与模型版本,并在最后对完整请求重新计数。分开编码的分项之和不一定精确等于拼接后一次编码的数量。
4. 把剩余容量算出来
将 budget.py 与 test_budget.py 按下方代码逐个保存到同一目录:
保存为 budget.py:
"""把已有Token数量相加,计算上下文窗口还能放多少内容。"""
def remaining(window, output, margin, parts):
counts = [window, output, margin, *parts.values()]
if any(type(x) is not int or x < 0 for x in counts) or window == 0:
raise ValueError('non-negative integer counts and positive window required')
available = window - output - margin - sum(parts.values())
if available < 0:
raise ValueError(f'context budget exceeded by {-available} tokens')
return available
if __name__ == '__main__':
parts = {'system': 512, 'question': 128, 'history': 2048, 'tools': 640}
print('available for retrieval:', remaining(8192, 1024, 256, parts))
print('after 3000 retrieval tokens:', remaining(8192, 1024, 256, {**parts, 'retrieval': 3000}))
try:
remaining(8192, 1024, 256, {**parts, 'retrieval': 4000})
except ValueError as error:
print('rejected:', error)完整代码:test_budget.py
# 核对Token 与上下文窗口的正常结果和错误分支;断言失败时会显示对应的测试。
import unittest
from budget import remaining
class BudgetTest(unittest.TestCase):
def test_reserves_output_and_overhead(self):
self.assertEqual(remaining(8192, 1024, 256, {'system': 512, 'question': 128, 'history': 2048, 'tools': 640}), 3584)
def test_exact_fit_is_zero(self):
self.assertEqual(remaining(100, 20, 10, {'messages': 70}), 0)
def test_overflow_is_not_silently_truncated(self):
with self.assertRaises(ValueError):
remaining(100, 20, 10, {'messages': 71})
def test_invalid_counts_are_rejected(self):
for value in [-1, True, 1.5, '12']:
with self.subTest(value=value), self.assertRaises(ValueError):
remaining(100, 10, 5, {'messages': value})
def test_counted_text_is_not_mutated(self):
parts = {'messages': 50}
remaining(100, 10, 5, parts)
self.assertEqual(parts, {'messages': 50})
if __name__ == '__main__':
unittest.main(verbosity=2)python -X utf8 budget.py
python -X utf8 -m unittest -v test_budget预算器使用 Python 自带工具;跳过前面的 tiktoken 安装,也能在保存这两个文件的目录运行。它接收已经数好的数量,按表中算式求剩余容量:
available for retrieval: 3584
after 3000 retrieval tokens: 584
rejected: context budget exceeded by 416 tokens核心函数只接收已计数的整数,不接收文本:
available = window - output - margin - sum(parts.values())
if available < 0:
raise ValueError(f'context budget exceeded by {-available} tokens')
return available五项测试覆盖预算预留、刚好装满、超限、非法计数以及输入不被修改。函数超限时拒绝,不擅自删除消息;“要删什么”需要语义策略,不能由一个加减法函数决定。
把四行函数读懂
parts 是一个字典,例如 {'system': 512, 'history': 2048};键是预算类别,值是已经由上游计数得到的 token 数。parts.values() 只取这些整数,sum(...) 求出已占用输入。函数先拒绝负数、浮点数和布尔值:Python 中 True 虽然能当作 1 参与加法,但预算接口不接受这种隐式转换。
窗口 8192
- 输出预留 1024
- 安全余量 256
- 固定输入 3328
= 检索可用 3584代码返回 3584,而不是直接替你截断资料。这样负责内容选择的上层才能基于来源、优先级和完整语义块做决定。把预算计算和删内容混在一个函数里,测试很难判断究竟删了什么。
余量为0表示分配的数量刚好装满;真实调用时还要为协议包装和计数差异留出空间。
5. 按完整语义片段裁剪内容
库中的truncation可按配置截断token序列;padding用于把不同长度样本补到指定形状,两者不是同一回事。padding不会增加有效知识,截断可能删除关键信息。填充与截断文档
对于Agent,直接保留最后N个字符尤其危险。可能切掉JSON括号、工具调用ID、用户否定词或系统约束;工具请求与返回不成对时,还可能破坏接口格式。
| 策略 | 适合处理什么 | 风险与检查 |
|---|---|---|
| 删除过时完整轮次 | 无后续依赖的闲聊 | 检查当前任务是否仍引用其中决定 |
| 按块挑选检索资料 | 带来源的独立段落 | 保留来源与版本,不能断章取义 |
| 摘要历史 | 长时间执行的背景 | 摘要可能遗漏或改写事实,需原记录引用 |
| 限制工具输出字段 | 大JSON、长日志 | 保留错误码、关键证据和后续查询入口 |
| 拆成多个任务 | 一个请求确实装不下 | 跨步状态与最终汇总要可追踪 |
这些策略不是随意删指令的理由。不可变约束应单独保留,并且工具来源的文本不能因为进入摘要就变成高优先级指令。选择完后重新校验协议和计数,再调用模型。
6. 工具调用后要重新算
某一轮预留了检索空间,不代表下一轮也能装下。工具可能返回数万行日志或整个网页;如果原样追加,之前的预算立即失效。
工具适配层可以返回有界结构:状态、关键字段、截取范围、完整结果引用。截取必须明确告知,不要把前20条说成全部结果。模型需要更多信息时,让它按页或范围继续查询,而不是一次塞入所有数据。
还应给循环设置总时间、调用次数与累计费用上限。上下文容量只限制一次请求,不能防止Agent重复请求一百次。缓存可能节约部分计算或计费,但不应未经文档确认就认为缓存内容不占窗口。
7. 检查模型是否找到了关键资料
把文档放进窗口只是必要条件,不保证模型能定位所有关键细节。用有明确答案和证据位置的任务测试,观察关键资料放在不同位置时,模型是否仍能引用正确内容。
一种可复查的做法是固定问题与文档集合,只改变无关材料比例或关键证据位置,比较回答正确性、引用与成本。测试中避免把答案直接写在提示的总结里,否则测到的不是长文定位能力。记录模型、处理方式和样本范围,不把一个例子泛化为所有任务。
信息不足时应允许继续检索、要求补充或报告未找到,而不是让模型补出被截掉的事实。
8. 练习与实践作品
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 长度预算能否只看字数?
同一段中文换了模型的分词器,token数量一定不变吗?
对应知识点:L0-tokens
简答题 · 为什么分项相加还不够?
你分别统计了系统指令、用户问题和历史正文的token,合计未超限,但完整请求仍被拒绝。列出可能漏掉的内容,并说明最后应怎样校验。
对应知识点:L0-tokens
代码题 · 给检索留下多少空间?
窗口8192,输出预留1024,余量256,输入分项为512、128、2048、640。写预算函数并验证检索加入3000和4000时的结果,超限不能静默裁剪。
对应知识点:L0-tokens
场景题 · 日志把上下文挤满了
工具返回两万行日志导致下一轮超限。有人建议保留最后2000字符。请给出更可核验的处理方案,说明必须保留哪些信息以及处理后如何验证。
对应知识点:L0-tokens
打开 budget.py,把检索资料的3000改为3500,先算一算应该剩多少,再运行:应剩84。再改为3600,运行会以 ValueError: context budget exceeded by 16 tokens 结束,表示预算检查按预期拒绝了超额输入;这次没有剩余额度输出。恢复3000后重新运行程序与测试,应再次得到584和 OK。
用预算表检查两件事:各部分相加是否超过容量;取舍资料后,问题和对应的工具结果是否仍完整。表里的数字用于练习预算算法;实际请求的 token 数由所选模型的分词器或用量接口取得。
9. 总结与下一步
8192 的练习容量减去1024的回答预留、256的安全余量和已有输入后,还能放3584;再塞入4000的检索片段,就超出416。先取舍材料,再重新计数。
这里的 token 数是练习输入;真实请求要按目标模型的规则计算完整消息。直接按字符数截半篇资料,可能把证据和出处拆开。你可以附自己的预算表、超限时舍弃了什么,以及重计后的结果。截断概念可查 Hugging Face 文档。
开源阅读顺序是 tiktoken 编码/解码、目标模型聊天模板、填充/截断,再到生成长度控制。
参考资料
- tiktoken 官方仓库:原始文本编码、解码与扩展入口。
- Transformers 生成配置:输入长度与新增生成长度相关参数。
- Transformers 填充与截断:padding、truncation 和最大长度的区别。
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。