AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

Token 与上下文窗口:先算清楚,再放进模型

我的学习 →

正在读取学习记录…

token 是模型处理文本时使用的片段;上下文窗口是一次请求能够容纳的 token 范围。发给模型的内容不只有用户最后一句话,还可能包括指令、历史和工具结果;回答也需要预留空间。

把一次请求想成容量有限的箱子。我们先用分词器观察一段文字怎样拆成 token,再给一条正常请求分配输入与输出预算。看懂这份账后,再分析为什么 Agent 调用几次工具就可能塞满窗口。练习容量设为8192;接入模型时,从对应接口文档读取窗口大小和计数规则。

0. 为什么 Agent 比普通聊天更容易把窗口塞满

一次普通问答可能只有系统指令和一条用户消息。Agent 每执行一步,还会追加工具定义、工具请求、工具返回、检索资料和中间状态;下一轮又把其中一部分重新发送给模型。窗口是单次请求可容纳的计算范围,不是永久记忆,更不是装得越满越聪明。

两位工程师把指令、对话、工具资料和检索文件分区装入有限箱子,并为输出保留空位的静态插画
图1:把上下文当作容量有限的箱子,先给回答留位置,再决定放入哪些资料。
Agent 请求中的内容为什么会增长完全丢掉会怎样
系统与业务规则通常每轮都要保留模型可能越权或偏离任务
对话与决策历史每轮继续追加忘记用户限制或重复提问
工具参数规则(Schema)工具越多定义越长模型缺少参数填写规则
工具返回日志、网页和 JSON 可能很大无法根据真实结果继续
检索资料每轮问题可能召回新片段回答缺少依据
输出预留需要提前留出输入装满后没有生成空间

工具参数规则像一张填写说明:查询天气要填城市,查询订单要填订单号。Schema 描述有哪些字段、各填什么类型,以及哪些必须填写。这些说明也要发给模型,因此同样占用窗口。

请求材料进入有限上下文窗口,已计数输入占据主要空间,另为模型输出和误差保留位置
图2:先为输出与余量留位置,再决定放入多少检索材料。

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 tokenso200k_base tokens
hello world111122
请先校验工具参数,再执行请求。15451310
print("hello")141444
🙂1421

同一句中文在两种编码中分别是13和10,不是“一字一token”。单个emoji也可能拆成多个token。这个小表只说明这些文本在指定编码下的结果,不能据此判断哪个模型整体更省钱。

先创建并进入实验目录,准备独立环境:

PowerShell
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.0
macOS 终端命令
Bash
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 能找到这个包:

Bash
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:

Python
"""使用指定的分词规则,显示文字拆成的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))
Bash
python -B -X utf8 count_tokens.py --cache-dir .cache/tiktoken

运行时会输出文本、token 数量及其编号。下面列出记录中的各组编号:

编码输入文本token 数量token 编号
cl100k_basehello world215339, 1917
cl100k_base请先校验工具参数,再执行请求。1315225, 61826, 15308, 94, 42462, 49792, 77913, 33765, 3922, 88356, 76217, 35959, 1811
cl100k_baseprint("hello")41374, 446, 15339, 909
cl100k_base🙂29468, 19044
o200k_basehello world224912, 2375
o200k_base请先校验工具参数,再执行请求。1010394, 12231, 25336, 22047, 69771, 41513, 130554, 65988, 54875, 788
o200k_baseprint("hello")41598, 568, 24912, 1405
o200k_base🙂137459

两种编码分别按各自的分词规则计算文本片段;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 能创建,随后输出计数

检查解释器和安装工具的位置:

Bash
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。先按分项查看占用,再选择缩短历史、压缩资料或减少候选数量,就知道空间是从哪里省出来的。

总容量8192的背包分给已有输入3328、回答1024、余量256和资料3584,比例条对应实际数字
图:已有输入共3328,扣除回答和余量后,检索资料还能占3584。

把数字换成符号:总容量记作 W,回答预留记作 O,余量记作 M,输入各项记作 I。sum(I) 表示把这些输入相加,因此剩余资料容量为 W - O - M - sum(I),也就是上面这笔减法。

这个表只有算术意义。真实项目应保存各分项计数来源与模型版本,并在最后对完整请求重新计数。分开编码的分项之和不一定精确等于拼接后一次编码的数量。

4. 把剩余容量算出来

将 budget.py 与 test_budget.py 按下方代码逐个保存到同一目录:

保存为 budget.py:

Python
"""把已有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
Python
# 核对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)
Bash
python -X utf8 budget.py
python -X utf8 -m unittest -v test_budget

预算器使用 Python 自带工具;跳过前面的 tiktoken 安装,也能在保存这两个文件的目录运行。它接收已经数好的数量,按表中算式求剩余容量:

TXT
available for retrieval: 3584
after 3000 retrieval tokens: 584
rejected: context budget exceeded by 416 tokens

核心函数只接收已计数的整数,不接收文本:

Python
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 参与加法,但预算接口不接受这种隐式转换。

TXT
窗口 8192
  - 输出预留 1024
  - 安全余量 256
  - 固定输入 3328
= 检索可用 3584

代码返回 3584,而不是直接替你截断资料。这样负责内容选择的上层才能基于来源、优先级和完整语义块做决定。把预算计算和删内容混在一个函数里,测试很难判断究竟删了什么。

余量为0表示分配的数量刚好装满;真实调用时还要为协议包装和计数差异留出空间。

5. 按完整语义片段裁剪内容

库中的truncation可按配置截断token序列;padding用于把不同长度样本补到指定形状,两者不是同一回事。padding不会增加有效知识,截断可能删除关键信息。填充与截断文档

对于Agent,直接保留最后N个字符尤其危险。可能切掉JSON括号、工具调用ID、用户否定词或系统约束;工具请求与返回不成对时,还可能破坏接口格式。

策略适合处理什么风险与检查
删除过时完整轮次无后续依赖的闲聊检查当前任务是否仍引用其中决定
按块挑选检索资料带来源的独立段落保留来源与版本,不能断章取义
摘要历史长时间执行的背景摘要可能遗漏或改写事实,需原记录引用
限制工具输出字段大JSON、长日志保留错误码、关键证据和后续查询入口
拆成多个任务一个请求确实装不下跨步状态与最终汇总要可追踪

这些策略不是随意删指令的理由。不可变约束应单独保留,并且工具来源的文本不能因为进入摘要就变成高优先级指令。选择完后重新校验协议和计数,再调用模型。

6. 工具调用后要重新算

某一轮预留了检索空间,不代表下一轮也能装下。工具可能返回数万行日志或整个网页;如果原样追加,之前的预算立即失效。

工具适配层可以返回有界结构:状态、关键字段、截取范围、完整结果引用。截取必须明确告知,不要把前20条说成全部结果。模型需要更多信息时,让它按页或范围继续查询,而不是一次塞入所有数据。

还应给循环设置总时间、调用次数与累计费用上限。上下文容量只限制一次请求,不能防止Agent重复请求一百次。缓存可能节约部分计算或计费,但不应未经文档确认就认为缓存内容不占窗口。

7. 检查模型是否找到了关键资料

把文档放进窗口只是必要条件,不保证模型能定位所有关键细节。用有明确答案和证据位置的任务测试,观察关键资料放在不同位置时,模型是否仍能引用正确内容。

一种可复查的做法是固定问题与文档集合,只改变无关材料比例或关键证据位置,比较回答正确性、引用与成本。测试中避免把答案直接写在提示的总结里,否则测到的不是长文定位能力。记录模型、处理方式和样本范围,不把一个例子泛化为所有任务。

信息不足时应允许继续检索、要求补充或报告未找到,而不是让模型补出被截掉的事实。

8. 练习与实践作品

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v2.0.0

选择题 · 长度预算能否只看字数?

同一段中文换了模型的分词器,token数量一定不变吗?

对应知识点:L0-tokens

选择一个答案
练习 2 / 4 · v2.0.0

简答题 · 为什么分项相加还不够?

你分别统计了系统指令、用户问题和历史正文的token,合计未超限,但完整请求仍被拒绝。列出可能漏掉的内容,并说明最后应怎样校验。

对应知识点:L0-tokens

练习 3 / 4 · v2.0.0

代码题 · 给检索留下多少空间?

窗口8192,输出预留1024,余量256,输入分项为512、128、2048、640。写预算函数并验证检索加入3000和4000时的结果,超限不能静默裁剪。

对应知识点:L0-tokens

练习 4 / 4 · v2.0.0

场景题 · 日志把上下文挤满了

工具返回两万行日志导致下一轮超限。有人建议保留最后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 编码/解码、目标模型聊天模板、填充/截断,再到生成长度控制。

下一篇:Embedding 与 Reranker 入门。

参考资料

图片查看器
100%

文章配图 · 按 Esc 关闭