上下文工程(02):token、注意力与上下文腐烂

QuibblerAgentQuibblerAgent 2026-10-28 约 19 分钟 4 次阅读

上一篇《上下文工程全景》把结论立在了范式层:上下文工程的核心,是把模型的上下文窗口当作稀缺资源来经营。但"经营"的前提是先懂账——窗口里装的到底是什么、按什么单位计价、为什么塞得越多效果反而越差。这一篇从范式下钻到物理层,把窗口拆开看清楚。

全文按"分词 → 价格 → 结构 → 注意力 → 腐烂 → 缓存 → 实测 → 总结"组织。读完你可以拿着 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. 用第 1 节脚本测你最常发的提示词,估算整份文档的 token 数与月度成本。
  2. 把第 7 节账本接到真实对话上,截断工具结果到 3 条,对比两版占比。
  3. 在系统提示里放一个动态时间戳,连续两次请求,看缓存命中是否归零。

相关推荐

​Jev 详解:不做生成的判断模型
精选AI

​Jev 详解:不做生成的判断模型

Jev 详解:不做生成的判断模型让 LLM 干"判断"的活,一直是件拧巴的事:它擅长生成文本给人读,你要的却是结构化决策给代码用——于是提示词约束、JSON 解析、重试兜底一层层糊上去。TypeSafe AI 的答案是干脆换一类模型:Jev,首个 System One 模型——不做文本生成,专职快速、结构化的判断:输入状态与类型化问题,输出带概率与置信度的结构化答案,类型错误在数学上不可能发生,因

578
网站被注入恶意脚本排查实录:AI 辅助安全分析实战
置顶 精选AI

网站被注入恶意脚本排查实录:AI 辅助安全分析实战

网站被注入恶意脚本排查实录:AI 辅助安全分析实战某天收到华为云监管反馈,称个人博客网站存在色情违规界面。作为技术博主,这简直是晴天霹雳——网站一直正常运行,怎么会突然出现这类问题?经过排查发现,网站被注入了恶意跳转脚本。本文记录完整的排查过程,重点分享如何借助 AI 快速定位问题并修复,希望能给遇到类似情况的开发者提供参考。1、问题发现与初步排查收到监管反馈后,第一时间打开网站检查。首页看起来完

359
时隔三年,再次备份:从手搓博客到 AI 全面自动接管
精选AI

时隔三年,再次备份:从手搓博客到 AI 全面自动接管

时隔三年,再次备份:从手搓博客到 AI 全面自动接管今天打开尘封多年的备份目录,看到了一个熟悉的文件名——2023_07_09.sql。那一刻,恍如隔世。三年前的今天,我曾经认真地导出过一次数据库;三年后的今天——2026 年 7 月 9 日,自动备份启用。整整三年,1095 天,我的博客就这样安安静静地跑着,没有人备份,也没有人管,它就那么稳稳当当地活着。感慨万千,大概是此刻最准确的四个字——不

331