上一篇《上下文工程全景》把结论立在了范式层:上下文工程的核心,是把模型的上下文窗口当作稀缺资源来经营。但"经营"的前提是先懂账——窗口里装的到底是什么、按什么单位计价、为什么塞得越多效果反而越差。这一篇从范式下钻到物理层,把窗口拆开看清楚。
全文按"分词 → 价格 → 结构 → 注意力 → 腐烂 → 缓存 → 实测 → 总结"组织。读完你可以拿着 tiktoken,给自己的 Agent 对话算出第一份 token 账单。
1、token 与分词直觉
模型不读字符,也不读词,它读 token——分词器(tokenizer)切出来的子词片段。主流方案是 BPE(Byte Pair Encoding,字节对编码):从字符出发,把语料里高频共现的片段反复合并进词表,直到词表封顶。于是"上下"能合成一个 token,"新"却可能单过。
光说没意思,直接测。tiktoken 是 OpenAI 的开源分词器,o200k_base 是 GPT-4o 起使用的词表。下面用同一段业务描述的中英文版各测一次,再附一行典型代码。
import tiktoken
enc = tiktoken.get_encoding("o200k_base") # GPT-4o 起的词表
zh = "上下文工程是一门新兴的工程学科,研究如何为大语言模型提供恰当的工作记忆。"
en = ("Context engineering is an emerging discipline that studies "
"how to provide large language models with appropriate working memory.")
code = "def fetch_user_orders(user_id: int, limit: int = 20) -> list[Order]:"
for name, text in [("中文", zh), ("英文", en), ("代码", code)]:
n = len(enc.encode(text))
print(f"{name}: {n} token | {len(text)} 字符 | {len(text)/n:.2f} 字符/token")
跑完脚本,再顺手把中文的前几个 token 逐个解码,就能看到 BPE 的切分现场。运行结果如下。
中文: 27 token | 36 字符 | 1.33 字符/token
英文: 19 token | 128 字符 | 6.74 字符/token
代码: 21 token | 68 字符 | 3.24 字符/token
三个结论直接从数字里长出来。第一,切分现场是 ['上下', '文', '工程', '是一', '门', '新']——token 不是字也不一定是词,中文常见二字词能并成一个 token,生僻字单独成段。按这个密度换算,一篇 5000 字的中文技术文约 3750 token,乘上单价就是它的输入成本。第二,同义内容,中文比英文多耗约四成 token:同一句话中文 27 个、英文 19 个,每 token 承载的字符数差 5 倍。英文语料在训练分布里占比大,切得更碎、压得更密,这是很多中文开发者意料之外的账。第三,代码居中偏贵:类型注解、下划线、标点把串切散,一行函数签名 21 个 token,按字符算密度只有英文的一半。
还有个工程提醒:tiktoken 只代表 OpenAI 一家的词表。Claude、Gemini、DeepSeek 各有自己的分词器,同样文本的 token 数会有 10%~30% 出入;给 Claude 计数应使用官方 Messages API 的 count_tokens 接口,估算时留出 10%~30% 的富余量即可。
2、主流窗口与价格速查
先看 2026 年 10 月这轮快照(每 1M token 价格,美元,取各家官方定价页)。
| 模型 | 窗口 | 输入 | 输出 | 长输入加价 |
|---|---|---|---|---|
| Claude Sonnet 5.5 | 1M | $2.00 | $10.00 | 全窗口同价 |
| Claude Haiku 5.5 | 1M | $0.10 | $0.50 | 超 100K 输入涨至 $0.50/$2.50 |
| GPT-6 Astra | 1.05M | $10.00 | $50.00 | 超 272K 输入加价 |
| GPT-5.1 | 400K | $1.25 | $10.00 | 超 200K 输入翻倍 |
| Gemini 3 Pro | 1M | $2.00 | $12.00 | 超 200K 输入加价 |
| DeepSeek V4 Flash | 1M | $0.15~0.30 | $0.60~1.20 | 分峰谷定价 |
数据来源:OpenAI 定价页、Claude 官方文档、Gemini API 定价、DeepSeek 定价。Claude 同代还有 Opus 5.5($4/$20)与旗舰 Fable 5.1($10/$50),同样 1M 窗口。
两个结构性事实值得记住。一是窗口大小已经不再稀缺:GPT-6 Astra、Claude、Gemini、DeepSeek 旗舰全部站上 1M,连 DeepSeek 的低价档都是 1M 起步。二是账单差异全在梯度上:厂商用"标准段 + 加价段"把长上下文的推理成本显性化了——Haiku 5.5 过 100K 输入价格涨 5 倍,Astra 过 272K 输入加价,Gemini 3 Pro 过 200K 加价,DeepSeek 则按峰谷时段浮动。标称窗口是"装得下"的上限,不是"用得起"的承诺,系列第 13 篇会专门算这笔账。
加价本身并不冤:Transformer 的注意力计算量随序列长度平方级膨胀,KV 缓存的显存占用也线性堆高,超长前缀对服务商是真实的推理成本,梯度定价只是把这笔成本显性化。
3、窗口的真实结构
聊窗口常被简化成"对话历史",但一次真实的 API 调用里,窗口由四块构成:system 提示、tools 定义、messages、以及混在 messages 里的工具结果。以 Anthropic Messages API 的请求体为例。
{
"model": "claude-sonnet-5-5",
"max_tokens": 1024,
"system": [
{ "type": "text", "text": "你是运维助手,负责查询线上服务状态……",
"cache_control": { "type": "ephemeral" } }
],
"tools": [
{ "name": "search_logs", "description": "按服务名与时间范围检索日志",
"input_schema": { "type": "object",
"properties": { "service": { "type": "string" },
"minutes": { "type": "integer" } },
"required": ["service"] } }
],
"messages": [
{ "role": "user", "content": "aiblog 错误率升高,帮我查一下原因" },
{ "role": "assistant", "content": [
{ "type": "tool_use", "id": "toolu_01", "name": "search_logs",
"input": { "service": "aiblog", "minutes": 30 } } ] },
{ "role": "user", "content": [
{ "type": "tool_result", "tool_use_id": "toolu_01",
"content": "[24 条日志 JSON,实测约 914 token]" } ] }
]
}
这四块全都在窗口里、全都按输入 token 计费,这是多数人第一处算错账的地方。tools 定义是"隐形的系统提示":模型要先读完全部工具的 JSON Schema 才知道能做什么,几十个 MCP 工具挂上去,光 schema 就可能吃掉几千 token。工具结果则常是最大的一块——第 7 节会实测它的占比。另外注意 cache_control 标记和请求体的渲染顺序:服务商按 tools → system → messages 的顺序拼出最终前缀,这个顺序是理解下一节缓存的关键伏笔。OpenAI 侧略有差异:超过 1024 token 的请求自动启用前缀缓存,无需手动标记。
4、注意力的位置偏置
窗口是装进去了,但模型对窗口各段的"注视强度"并不均匀。Lost in the Middle 论文(Liu 等,TACL 2024)做了个干净实验:多文档问答里固定 20 篇文档,只挪动那篇含正确答案的文档的位置。结果是稳定的 U 形曲线——答案在开头或结尾时准确率最高,挪到中段明显下滑,首尾与中间能差出十几个百分点。模型对开头(注意力锚点)和结尾(邻近生成位置)的信息利用率天然更高。
这对信息排布是直接可执行的指导,不是玄学:
- 硬约束、角色定义写进 system 提示——它天然在最前。
- 当前任务、关键材料放在最后一条用户消息附近,贴着生成位置。
- 长文档中段的内容要么提炼成摘要放头尾,要么分多轮注入。
- 唯一的"钥匙"别埋在几十万 token 的中段。
第 1 篇讲的信息架构原则,物理依据就在这里:排布位置本身就是一种注意力预算。
5、上下文腐烂实证
位置偏置之外还有更狠的现象:上下文腐烂(context rot)——任务准确率随上下文长度系统性衰减,哪怕信息位置不变。三份公开研究互相印证。
RULER 论文(NVIDIA,2024)指出,一批标称 32K~128K 窗口的模型,在多变长度的检索与聚合任务上,有效上下文普遍缩水到 4K~32K。Chroma 的 context rot 研究(2025)横测了十类任务,几乎全部呈随长度单调下滑的形态,任务越复杂、衰减来得越早。NoLiMa 论文(2025)则给出机制层面的解释:超过训练长度的约 1/8 之后,模型需要"跨距离关联"的能力快速崩塌,八个模型平均只保留基线不到 10% 的表现——本质上退化成了局部字面匹配。
标称窗口是"装得下",不是"看得清"。上下文既是资产也是负债:塞进去的一切都在稀释模型对每一段的注意力。
所以"窗口 1M"和"1M 都用好"是两回事。系列第 4 篇的 token 预算、第 5 篇的压缩,都是在这个物理约束下做取舍。
6、前缀缓存的成本反直觉
前面埋的伏笔在这里兑现。各家 API 都提供 prompt caching:服务端把请求前缀的 KV 计算结果缓存起来,下次请求前缀完全一致就直接复用。原理只有四个字:前缀匹配——逐字节比对,从第一个字节到最后一个 cache_control 断点,任何一处不同,其后全部失效。Anthropic 写入约收 1.25 倍价、命中约收 0.1 倍价(Sonnet 5.5 命中 $0.20/MTok,默认保留 5 分钟);GPT-6.1 Sol 缓存输入 $0.10;DeepSeek V4 Flash 命中低至 $0.003——是未命中价的 1/50。细节见 Anthropic 缓存文档。
缓存直接推翻了"压缩一定省钱"的直觉。算笔账:Agent 会话里有一份 8000 token 的项目背景,方案 A 压成 800 token 摘要,每次请求付 800 × $2/1M ≈ $0.0016;方案 B 原文保留、缓存命中,每次付 8000 × $0.20/1M ≈ $0.0016。成本打平,但 A 丢了细节,摘要本身还要先花一次输出 token(输出价是输入的 5 倍),多轮会话里 B 稳赢。缓存命中时,保留原文可能比压缩摘要更便宜,还更快——这正是系列第 6 篇"压缩 vs 保留"要展开的核心权衡。
注意:前缀里一个字节的变动——系统提示里的时间戳、每次随机的请求 ID、未排序的 JSON key、临时增删的工具——都会让其后所有缓存作废。稳定性是缓存的第一公民。
7、给 Agent 对话算 token 账
道理讲完,落一个能跑的工具。把一次典型的 Agent 往返(提问、工具调用、工具结果)按四块拆开,用 tiktoken 逐块计数。消息结构按 OpenAI 风格示意,各家字段名不同,构成完全一致。
import json, tiktoken
enc = tiktoken.get_encoding("o200k_base")
system = "你是运维助手。负责查询线上服务状态、解读日志并给出修复建议。"
tools = [
{"name": "search_logs", "description": "按服务名与时间范围检索日志",
"parameters": {"type": "object", "properties": {
"service": {"type": "string"}, "minutes": {"type": "integer"}},
"required": ["service"]}},
{"name": "get_metrics", "description": "查询服务的 CPU、内存、QPS 与错误率",
"parameters": {"type": "object", "properties": {
"service": {"type": "string"}}, "required": ["service"]}},
]
logs = [{"ts": f"2026-10-25 09:{i:02d}", "level": "ERROR",
"msg": "upstream timeout after 3000ms path=/api/posts"} for i in range(24)]
messages = [
{"role": "user", "content": "aiblog 最近半小时错误率升高,帮我查一下原因"},
{"role": "assistant", "tool_calls": [{"function": {"name": "search_logs",
"arguments": '{"service": "aiblog", "minutes": 30}'}}]},
{"role": "tool", "name": "search_logs", "content": json.dumps(logs)},
]
parts = {"system 提示": system, "tools 定义": json.dumps(tools),
"对话消息": json.dumps(messages[:2]), "工具结果": messages[2]["content"]}
total = sum(len(enc.encode(t)) for t in parts.values())
for name, text in parts.items():
n = len(enc.encode(text))
print(f"{name} {n} token ({n / total:.0%})")
print(f"合计 {total} token")
这份账本的运行结果如下。
system 提示 22 token (2%)
tools 定义 191 token (15%)
对话消息 122 token (10%)
工具结果 914 token (73%)
合计 1249 token
数字很刺眼:24 条日志的 JSON 就吃掉 73% 的窗口,对话本身只占一成。真实 Agent 一次跑几十轮、每轮塞几 KB 工具输出,账单和"腐烂"都从这里来。优化优先级因此清晰——先测量,再优化,砍工具结果(限行数、裁字段、截断到 Top N)的收益远大于抠系统提示的措辞。这正是系列第 8 篇"工具结果治理"的主题。
8、总结
这一篇把上下文窗口拆到了物理层:token 是 BPE 切出来的子词,中文比英文多耗约四成;1M 窗口已成标配但长输入普遍加价;窗口由 system、tools、messages、工具结果四块构成且全部计费;注意力呈 U 形偏置,中段最弱;context rot 让准确率随长度衰减,标称不等于有效;前缀缓存则把"压缩省钱"的直觉改写成了"稳定复用更省钱"。第 7 节的账本脚本随时可以跑在你自己的对话上。
对于刚开始做上下文治理的团队,建议本周就做三件事:给自己的 Agent 跑一次 token 账本,找出占比最大的一块;把系统提示与工具列表冻结成稳定前缀,打开缓存;把关键信息从头到尾重排一遍,避开中段盲区。
练习三题:
- 用第 1 节脚本测你最常发的提示词,估算整份文档的 token 数与月度成本。
- 把第 7 节账本接到真实对话上,截断工具结果到 3 条,对比两版占比。
- 在系统提示里放一个动态时间戳,连续两次请求,看缓存命中是否归零。

