L1 第一篇讲过:模型能可靠利用的上下文远小于它标称的长度,而且长上下文按 token 计费、线性拖慢 TTFT。这一篇从另一头看同一个问题:一次请求里模型到底看到了什么。多数工程师第一次把发给模型的完整请求体打印出来时都会吃惊——用户那句 20 个字的问题外面裹着两万 token 的 system prompt、工具定义、历史、检索结果与工具返回,而其中一半是上一步某个工具返回的原始 JSON。
要管理上下文,先要能解剖它。本篇建立一个七层模型:每层是什么、由谁维护、多大、变化多快、能不能压缩——后面五篇的每一种策略(预算、压缩、缓存、版本)都是对某几层的操作。实例用两个公开材料:Claude Code 文档里的”上下文窗口时间线”(一个会话从启动到压缩每一步进了什么),以及 Manus 公开的两个数字(一个任务约 50 次工具调用、输入输出 100 : 1)。
本篇要回答的核心问题是:
模型在这一步看到的上下文由哪几层组成,各由谁维护、多大、变化多快?1 一个 agent 会话的上下文是怎样长起来的,为什么不管理就一定漂移?2 “注意力预算”有限这件事的机制是什么,对设计意味着什么?3
一、总览
1. 七层
层 内容 变化频率 维护者
──────────────────────────────────────────────────────────────────────────
① 系统指令 角色 · 规则 · 输出格式 · 安全约束 发布时 开发者
② 工具与技能定义 name / description / schema · 技能描述行 发布时 开发者 / 平台
③ 长期记忆 CLAUDE.md / AGENTS.md · 偏好 · 跨会话事实 会话间 用户 / 运行时
④ 对话历史 此前每轮 user / assistant(含 tool_call) 每轮追加 运行时
⑤ 检索结果 本轮取回的片段 每轮 运行时
⑥ 工具返回 上一步工具执行的结果(往往最大) 每步 运行时
⑦ 当前输入 用户这一轮的话 / 触发事件 每轮 用户
──────────────────────────────────────────────────────────────────────────
↑ 稳定、可缓存、放前面 不稳定、每轮变、放后面 ↓
七层不是供应商 API 的字段(API 只有 system、messages、tools 三个位置),是应用侧的逻辑分层:①② 放在 API 的 system / tools 里,③ 通常拼进 system 末尾或第一条 user 消息,④⑤⑥⑦ 都在 messages 里。分层的价值在右边两列:变化频率决定缓存断点放哪(第五篇),维护者决定谁有权改、改了要跑什么评测(第六篇);再加一列可压缩性(第四篇)——工具返回可以卸载到文件、历史可以摘要、系统指令不能动。
2. 本文的章节安排
第二章用 Claude Code 的时间线走一个真实会话;第三章逐层讲七层的性质;第四章讲 agent 会话的增长规律与漂移;第五章讲注意力预算的机制;第六章给出实践建议。
二、一个会话的时间线
Claude Code 的文档有一条交互式时间线,展示一个会话的上下文怎样填满。把它按七层重新标注:
| 时刻 | 进入上下文的东西 | 层 | 量级 | 备注 |
|---|---|---|---|---|
| 启动,用户还没敲字 | system prompt(含输出风格、--append-system-prompt) |
① | 几千 token | 不在消息历史里,压缩不动它 |
| CLAUDE.md(项目根目录 + 用户级) | ③ | 几百到几千 | 从磁盘读;压缩后重新注入 | |
| 自动记忆 | ③ | ≤ 200 行或 25 KB | 有硬上限——这是一个预算决定 | |
| MCP 工具名与技能描述 | ② | 每个工具 / 技能几十到几百 | 工具的完整 schema 默认延迟加载,只有名字常驻 | |
| 第一个 prompt | 用户的话 | ⑦ | 几十 | |
| 工作中,每读一个文件 | 文件内容作为工具返回 | ⑥ | 一个中等源文件 2–5K | 十个文件就是几万 |
| 读到匹配路径的文件时 | 带 paths: 前置元数据的规则 |
③ | 几百 | 按需加载;压缩后丢失直到再读匹配文件 |
| 每次编辑后 | PostToolUse hook 的输出 | ⑥ | 几十 | |
| 需要研究一个问题 | 子 agent 在自己的窗口里读大量文件 | — | 主窗口只收到摘要 + 小段元数据 | 隔离是最有效的预算手段 |
| 窗口接近上限 | /compact 或 auto-compact |
④⑥ → 一段摘要 | 历史被替换 |
|
这条时间线说明三件事。第一,你敲第一个字之前已经花了几千到上万 token——①②③ 是常驻成本,每一轮都在传(缓存让它便宜,第五篇)。第二,增长的主体是 ⑥:几十次文件读取与命令输出,每一次几千 token,几十步之后占到窗口的大半。第三,压缩对各层不平等:① 完全不受影响,③ 的一部分靠”从磁盘重读”存活,④⑥ 被摘要替代,③ 的按需部分直接丢失——第四篇会把这张”压缩后的命运”表展开。
Claude Code 的两个硬数字值得记:自动记忆上限 200 行 / 25 KB,是对 ③ 的预算;auto-compact 在窗口约 83.5% 处触发、预留约 33K 给摘要生成,是对整体的预算(一个 200K 窗口的 16.5%)。这些不是随手选的——它们是”留多少给模型回答”与”多久压缩一次”的权衡结果,第四篇讨论。
%% 图:一个 Claude Code 会话的上下文怎样填满——启动时 ①②③ 已占几千到上万 token;每读一个文件 ⑥ 加 2–5K;子 agent 的大量读取在另一个窗口里、主窗口只收摘要;到约 83.5% 处 auto-compact 把 ④⑥ 换成一段摘要,① 不动、③ 的根目录部分重注入、按需部分丢失
flowchart TB
S0["启动:还没敲字<br/>① system 几千 · ② 工具名与技能描述行 · ③ CLAUDE.md + 自动记忆(≤ 25 KB)<br/>≈ 5–15K token"]
S0 --> S1["第一个 prompt<br/>⑦ 几十 token"]
S1 --> S2["工作中:每读一个文件<br/>⑥ +2–5K;十个文件就是几万"]
S2 --> S3["需要研究一个问题<br/>子 agent 在自己的窗口里读几十个文件<br/>主窗口只收到一段摘要"]
S3 --> S2
S2 --> S4{"窗口到 83.5%?<br/>(200K 窗口预留 33K 给摘要)"}
S4 -->|"否"| S2
S4 -->|"是"| S5["auto-compact<br/>④⑥ → 一段摘要"]
S5 --> S6["压缩后的命运<br/>① 不变 · ③ 根目录 CLAUDE.md 从磁盘重注入 · ③ 带 paths: 的规则丢失 · ④⑥ 只剩摘要"]
S6 --> S2
classDef res fill:#eefaf0,stroke:#4d9a5c,color:#222
classDef grow fill:#fff7e0,stroke:#c98a00,color:#222
classDef dec fill:#eef6ff,stroke:#5b8fd6,color:#222
classDef cmp fill:#fdecea,stroke:#c0392b,color:#222
class S0,S1 res
class S2,S3 grow
class S4 dec
class S5,S6 cmp
三、逐层的性质
1. 系统指令
开发者写的常驻文本:角色、任务、规则、输出格式、安全约束。性质:最稳定(随发布变)、最靠前(缓存前缀的头)、不可压缩(压缩不碰它)、每轮全量传(所以要精炼——不是短,是每一句都有用)。它是 prompt 工程传统意义上的对象,第二篇讲怎么写。一个常被忽略的事实:它在模型眼里只是上下文的一段,与 ⑥ 里的一句注入文字在同一个注意力空间里竞争(L1 第一篇第四章)。
2. 工具与技能定义
每个工具的名字、描述、参数 schema;每个技能的描述行。性质:稳定、靠前、按数量线性增长——几十个 MCP 工具的完整 schema 可以是几千到上万 token。两种应对:Claude Code 与 OpenAI 的 tool search 把完整 schema 延迟加载(常驻的只有名字或一行描述,模型需要时再取——代价是这些按需加载的内容不在缓存前缀里);Agent Skills 标准把这个思路做成规范——SKILL.md 只有 description(≤ 1,024 字符)常驻,正文与脚本用到时才读。这叫渐进披露(progressive disclosure),是这一层的核心设计原则。
%% 图:渐进披露——常驻在上下文里的只是每个工具的名字和每个技能的一行描述(几十 token),完整的 schema 与 SKILL.md 正文放在外面,模型判断要用时再加载;30 个工具的完整定义从常驻的 9K 变成常驻 1K + 按需 300
flowchart LR
subgraph RES["常驻层 ②:每轮都传、在缓存前缀里"]
direction TB
R1["工具名 + 一行描述 × 30<br/>≈ 1K token"]
R2["技能 description × n<br/>每个 ≤ 1,024 字符"]
end
subgraph EXT["上下文之外:磁盘 / 注册表"]
direction TB
E1["30 个完整的参数 schema<br/>≈ 9K token"]
E2["SKILL.md 正文 + 脚本<br/>几千到几万 token"]
end
R1 -.->|"模型决定用某个工具 → tool search 取回它的 schema(+300)"| E1
R2 -.->|"模型决定用某个技能 → 读正文"| E2
classDef res fill:#eefaf0,stroke:#4d9a5c,color:#222
classDef ext fill:#f0f0f0,stroke:#888,color:#222
class R1,R2 res
class E1,E2 ext
3. 长期记忆
跨会话存活的东西:项目说明(CLAUDE.md、AGENTS.md)、用户偏好、运行时自动记下的事实。性质:会话间变化、由用户或运行时维护、通常有硬上限(Claude Code 的 25 KB)。它是一种特殊的检索——从磁盘或数据库取出来放进上下文——L4 的 memory 一篇讨论怎么写入与遗忘;本系列关心的是它占多少预算、放在哪、压缩后怎么恢复(从磁盘重读,所以它的”真身”不在上下文里而在文件里——这是第四篇”可恢复压缩”的雏形)。
4. 对话历史
之前每一轮的消息,包括模型发出的工具调用与思考块(L1 第三篇:思考块要原样保留)。性质:只追加(修改会破坏缓存与推理状态)、随轮数线性增长、可摘要——但摘要是有损的且要在思考链的边界做。它是多轮成本二次增长的来源(L1 第四篇),也是压缩的主要对象。
5. 检索结果
本轮从知识库、文件、网页取回的片段。性质:每轮变、由运行时组装、数量与顺序是设计决定——放几条、按相关性排、最相关的放末尾(位置效应)。L3 讲怎么取,本系列讲取回来之后占多少、放哪、要不要在下一轮保留(多数情况下不保留:上一轮的检索结果对这一轮多半是噪声,应清理)。
6. 工具返回
上一步工具执行的结果:文件内容、命令输出、API 响应、搜索结果、数据库行。性质:最大、最不可预测(一次 SQL 可能返回 200 行也可能 20 万行)、多数可恢复(文件还在磁盘上、URL 还能再访问)、时效短(模型读过一次、做了决定之后,原始内容对后续步骤价值急降)。这三条性质决定了第四篇的处理顺序:卸载(留路径不留内容)→ 清理(旧的删掉)→ 压缩(最后手段)。Deep Agents 的 20,000 token 卸载阈值、Anthropic API 的 clear_tool_uses,都是对这一层的。
7. 当前输入
用户这一轮说的话,或触发这一步的事件(定时、webhook、上一步的工具返回本身)。性质:最小、最靠后、最重要——它是这一步的任务。位置效应告诉我们它应该在末尾;Manus 的”复述”技巧(第四篇)本质上是把 ⑦ 的一个副本(当前目标)不断重新写到末尾。
四、agent 会话的增长规律
1. 两个数字
Manus 公开的生产统计:一个典型任务约 50 次工具调用,输入与输出 token 的比例约 100 : 1。第二个数字的含义是:agent 的成本几乎全在输入侧——每一步都把累积的上下文重新传一遍、prefill 一遍,而模型每步只产出几百 token 的决定。这与聊天产品(输入输出比例接近 1 : 1 到 10 : 1)完全不同,也是为什么 Manus 把 KV-cache 命中率列为第一指标(第五篇)。
2. 增长的形状
设常驻层(①②③)为 \(S\),每步新增的工具返回平均 \(r\)、模型输出 \(o\),第 \(k\) 步的上下文约为 \(S + (k-1)(r + o)\)。\(r\) 主导:一次文件读取 3K、一次命令输出 1K、一次网页 5K,取 \(r = 3{,}000\)、\(o = 300\)、\(S = 15{,}000\),第 50 步是 \(15{,}000 + 49 \times 3{,}300 \approx 177{,}000\) token——一个 200K 窗口在任务结束前就满了;即使 1M 窗口,L1 第一篇的有效长度问题在几万 token 就开始起作用。不管理的上下文在长任务上必然触顶或退化,这不是模型能力问题,是算术。
%% 图:上下文随步数线性增长——常驻 15K、每步工具返回 3K + 输出 300 时,第 50 步到 177K,一个 200K 窗口在 83.5%(167K)处约第 47 步触发压缩;把工具返回卸载到平均 800 后每步只长 1.1K,50 步 69K,整个任务不用压缩
%%{init: {"xyChart": {"width": 760, "height": 340, "plotReservedSpacePercent": 60}, "themeVariables": {"xyChart": {"plotColorPalette": "#c0392b, #5b8fd6, #888888"}}}}%%
xychart-beta
title "第 k 步的上下文大小(千 token):S = 15K,o = 300"
x-axis "步数 k" [1, 10, 20, 30, 40, 50]
y-axis "千 token" 0 --> 200
line [15, 45, 78, 111, 144, 177]
line [15, 25, 36, 47, 58, 69]
line [167, 167, 167, 167, 167, 167]
红线 r = 3,000(不管理),蓝线 r = 800(工具返回卸载后),灰线是 200K 窗口的 auto-compact 触发线 167K。红线在第 47 步左右撞线;蓝线到任务结束还不到一半。
3. 漂移
增长带来第二个问题:原始任务漂到窗口中间。第 1 步时用户的目标在末尾(⑦),第 30 步时它前面有 ①②③、后面有 29 步的工具返回——正是 Lost in the Middle 里准确率最低的位置。Manus 的观察是模型开始”忘记”或”偏离”目标,他们的解法是让 agent 维护一个 todo.md 并在每步末尾重写它——把目标从中间搬回末尾。Anthropic 的博客把同样的现象归到”注意力预算”的耗尽。这就是为什么第四篇说压缩不只是省钱:它也是把目标拉回注意力焦点的手段。
%% 图:目标的漂移与复述——第 1 步时用户的目标在窗口末尾(注意力最好的位置);第 30 步时它前面是常驻层、后面是 29 步的工具返回,正好落在 Lost in the Middle 的谷底;Manus 的解法是每步把 todo.md 重写到末尾,把目标的副本搬回焦点
flowchart TB
subgraph K1["第 1 步"]
direction LR
A1["①②③ 常驻 15K"] --> A2["⑦ 用户目标 ← 在末尾,注意力最好"]
end
subgraph K30["第 30 步:不管理"]
direction LR
B1["①②③ 常驻 15K"] --> B2["⑦ 用户目标 ← 漂到中间,U 形的谷底"] --> B3["29 步的工具返回与输出 ≈ 96K"]
end
subgraph K30R["第 30 步:每步复述"]
direction LR
C1["①②③ 常驻 15K"] --> C2["原始目标(仍在中间)"] --> C3["29 步的工具返回…"] --> C4["todo.md:目标 + 已完成 / 待做 ← 重写到末尾"]
end
K1 ~~~ K30 ~~~ K30R
classDef res fill:#eefaf0,stroke:#4d9a5c,color:#222
classDef goal fill:#eef6ff,stroke:#5b8fd6,color:#222
classDef bad fill:#fdecea,stroke:#c0392b,color:#222
classDef grow fill:#fff7e0,stroke:#c98a00,color:#222
class A1,B1,C1 res
class A2,C4 goal
class B2 bad
class B3,C2,C3 grow
五、注意力预算
1. 机制
Transformer 的注意力对上下文里每一对 token 计算一个关系,\(n\) 个 token 是 \(n^2\) 对。模型在训练时见到的长序列远少于短序列,对”在几十万 token 里精确定位一条信息”的能力是外推出来的,不是训练充分的(共享《Transformer 与 LLM》系列讲位置编码与长度外推的机制)。Anthropic 的博客用一个比喻:每个新 token 都在消耗一份有限的”注意力预算”,上下文是有边际递减的资源,不是桶。
2. 实证
L1 第一篇引过的四项研究是这个约束的测量:Lost in the Middle(位置效应的 U 形)、RULER(多数模型在到达标称长度前跌破门限)、NoLiMa(非词面匹配时 32K 处 11/13 模型掉到基线一半以下)、Context Rot(18 个模型在极简任务上随长度不均匀退化)。它们共同说:上下文里每多一段无关内容,都在降低模型找到相关内容的概率,而且这个效应在远小于标称长度的位置就开始了。
3. 对设计的含义
- 每一段都要为它占的预算辩护。”放进去总没坏处”是错的:无关的检索结果、过期的工具返回、用不到的工具 schema 都在消耗预算。
- 相关的放末尾,稳定的放开头。两端是注意力最好的位置,也分别是”当前任务”与”缓存前缀”的位置——两个目标恰好一致。
- 隔离胜过压缩。子 agent 把探索性的大量读取放在另一个窗口里,主窗口只收摘要——这比事后压缩更省预算,因为噪声从未进来。
- 窗口变大不解除约束。1M 的窗口让你能放更多,不让模型能用更多;它改变的是压缩的触发点,不是预算的存在。
六、实践建议
- 打印一次完整请求体。选你的应用一个典型的第 10 步请求,把请求体存下来,按七层标注每一段、数 token。多数团队第一次做会发现 ⑥ 占了一半以上,① 里有一段早已无效的规则,③ 里有重复的内容。
- 画预算饼图并定上限。每一层给一个 token 上限(第四篇有参考值),超过上限的层就是下一步要处理的对象。
- 给工具返回做时效标记。在 trace 里记每个工具返回在后续几步里被模型引用过没有——多数在下一步之后就不再被引用,这是清理策略的依据。
- 检查目标的位置。在第 20 步的请求里找用户的原始任务在哪;如果在中间,考虑复述。
- 常驻层用渐进披露。工具的完整 schema、技能的正文、长文档的内容都不该常驻,常驻的是”它们存在、什么时候用”的一行描述。
七、本文小结
- 上下文有七层:系统指令、工具与技能定义、长期记忆、对话历史、检索结果、工具返回、当前输入;API 只有三个位置,分层是应用侧的逻辑模型,价值在变化频率、维护者、可压缩性三列。
- Claude Code 的时间线:敲第一个字前已有几千到上万 token 的常驻层;增长主体是工具返回;压缩对各层不平等(system 不变、根目录 CLAUDE.md 重注入、路径规则丢失);自动记忆上限 25 KB,auto-compact 在约 83.5% 处、预留 33K。
- agent 会话约 50 次工具调用、输入输出 100 : 1,成本几乎全在输入侧;上下文按 \(S + (k-1)(r+o)\) 增长,\(r\) 主导,50 步就填满 200K;原始目标漂到中间是算术的必然。
- 注意力是 \(n^2\) 的两两关系、长序列能力是外推的,预算有限且边际递减;每一段都要为占的预算辩护,相关的放末尾、稳定的放开头,隔离胜过压缩,窗口变大不解除约束。
八、自测
-
一个 agent 的请求体里有:system 4K、30 个 MCP 工具的完整 schema 9K、CLAUDE.md 2K、20 轮历史 40K、本轮检索 6K、上一步工具返回 25K、用户消息 0.1K。按七层标出哪一层最该先动、用什么手段。
答案
⑥ 工具返回 25K 最大且时效最短——先卸载(存文件留路径与预览)或在下一步后清理;其次 ② 9K 的完整 schema——改为渐进披露(tool search,常驻只留名字);④ 40K 的历史是压缩的对象但代价最高,放在卸载与清理之后。①③⑤ 量级合理。详见第三章。
-
Claude Code 压缩后,为什么根目录的 CLAUDE.md 还在、而带
paths:的规则不在了?这说明长期记忆层有什么性质?答案
压缩把消息历史替换成摘要;根目录 CLAUDE.md 与自动记忆的”真身”在磁盘上,压缩后从磁盘重新注入;带
paths:的规则是读到匹配文件时才加载的,压缩后要等再读匹配文件才回来。说明 ③ 的可恢复性来自它有一个上下文之外的持久存储——这是”可恢复压缩”的原型:上下文里只需要能找回内容的引用。详见第二章。 -
常驻层 12K,每步工具返回 4K、输出 300。200K 窗口、预留 33K 给输出时,大约第几步触发压缩?如果把工具返回卸载到平均 800,第几步?
答案
触发线 200K − 33K = 167K。\(12{,}000 + (k-1) \times 4{,}300 \ge 167{,}000 \Rightarrow k - 1 \ge 36.05\),约第 38 步。卸载后每步 1,100:\((k-1) \ge 140.9\),约第 142 步。卸载让同一个任务多跑近四倍的步数才需要压缩。详见第四章。
-
为什么”1M 窗口的模型不需要做上下文管理”是错的?给两个独立的理由。
答案
(1)有效长度远小于标称:NoLiMa、RULER、Context Rot 表明模型在几万 token 处就开始退化,窗口大只让你能放、不让模型能用;(2)成本与延迟:输入按 token 计费、prefill 线性变慢,agent 的成本 100 : 1 在输入侧,且超过 272K / 200K 有加价门限(L1 第四篇)。窗口变大只改变压缩的触发点。详见第五章。
-
把工具完整 schema 改为按需加载(tool search)之后,缓存命中率反而下降了。为什么?
答案
常驻的完整 schema 虽然大,但稳定、在前缀里、每轮命中缓存;按需加载的 schema 在模型需要时才进入上下文,位置在历史中间、每次可能不同,这部分不在稳定前缀里。命中率下降是把”大而可缓存”换成了”小但不可缓存”——总输入 token 减少了,但命中率这个比例指标可能变差。要看的是绝对的未缓存 token 数与成本,不只是命中率。详见第三章。
-
七层:① 系统指令(开发者,发布时变,几千 token,不可压缩);② 工具与技能定义(开发者 / 平台,发布时变,按数量线性增长,用渐进披露控制);③ 长期记忆(用户 / 运行时,会话间变,通常有硬上限如 Claude Code 的 25 KB,真身在磁盘可重注入);④ 对话历史(运行时,每轮追加,只追加不修改,可摘要);⑤ 检索结果(运行时,每轮变,数量与顺序是设计决定,下一轮多半应清理);⑥ 工具返回(运行时,每步变,最大最不可预测、多数可恢复、时效短);⑦ 当前输入(用户,每轮,最小最重要,应在末尾)。稳定的在前可缓存,不稳定的在后。详见第一章、第三章。 ↩
-
敲第一个字前常驻层已占几千到上万 token;之后每步的工具返回(文件 3K、网页 5K)是增长主体,上下文按 \(S + (k-1)(r+o)\) 线性增长,\(r\) 主导;Manus 的统计是一个任务约 50 次工具调用、输入输出 100 : 1。取 \(S = 15K\)、\(r + o = 3.3K\),第 50 步约 177K,200K 窗口在任务结束前填满。漂移是算术的必然:第 1 步目标在末尾,第 30 步它前有常驻层、后有 29 步返回,正落在 Lost in the Middle 准确率最低的中间位置——Manus 用每步重写 todo.md 把目标搬回末尾。详见第二章、第四章。 ↩
-
注意力对 \(n\) 个 token 计算 \(n^2\) 对关系,长序列能力是从训练分布外推的,所以每个新 token 消耗一份有限、边际递减的”注意力预算”(Anthropic 的说法);实证是 Lost in the Middle、RULER、NoLiMa、Context Rot——退化在远小于标称长度处开始。设计含义:每一段要为占的预算辩护(无关内容降低找到相关内容的概率);相关的放末尾、稳定的放开头(两端注意力最好,也分别是当前任务与缓存前缀的位置);隔离(子 agent)胜过事后压缩;窗口变大只改变压缩触发点,不解除约束。详见第五章。 ↩
本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/anatomy-of-the-context-window.html)的前提下,欢迎各种形式的转载、翻译或商业引用。
COMMENTS
评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。