2.2 上下文窗口:Agent 的真正瓶颈
一句话结论:上下文窗口不只是一条「装不下」的容量线,它还是一台只有一半的检索引擎——模型极擅长从上下文里"捞出"相关内容,却不会自动替你"数清楚、总结好"。这一半的缺失,是大量 Agent 行为诡异的根源。
读这篇之前
需要先读 2.1(Token 与上下文窗口的基本概念)和 1.3(上下文是能力天花板)。这一篇比前一篇更深一层:不只讲"装多少",还讲"装进去的东西怎么被使用"。
一、先纠正一个常见的理解偏差
大多数人把上下文窗口理解成一个"容器":装得下就没事,装不下就截断。
这个理解只对了一半。
真正决定 Agent 表现的,除了装多少,还有装进去之后模型怎么用它。而模型的注意力机制有一个非常特殊的行为特性:
上下文学习更像"检索",而不是"推理"。[原文 4668]
意思是:模型擅长从已有内容里查找相关信息——你问什么,它能从成千上万 Token 里捞出相关片段,效果相当于把 RAG 内置进了每一次前向传播。
但模型不擅长主动归纳总结。上下文里的东西,永远不会被自动"数一遍、建个索引、就地总结成结论"。
原文给了一个极精准的比喻:
上下文窗口是一台只有一半的检索引擎。 检索那一半非常强,但缺少了"提炼层"。[原文 4668]
二、这"一半缺失"会造成什么
理解了这个特性,很多 Agent 的诡异行为立刻有了解释。
案例:为什么 Agent 会忘记"已经打了三次"
设一个场景:Agent 打电话处理业务,系统提示词里明确要求「每个商家最多打 3 次」。
但实际运行中,Agent 打了 3 次之后经常数不清,又打了第 4 次,甚至陷入循环。[原文 4668]
为什么?不是因为模型不识字,也不是因为它不遵守指令。根源是:
「已经打了几次」这个知识没有自动提炼出来,而是分散在原始通话记录的向量表示里。
模型每次需要判断"还能不能再打",都得从那些原始记录里现算一遍。而这个计算的代价随内容量 N 上涨——记录越多,越算不准。
修复方法极其简单却极有效:在每次工具调用结果里直接标明累计次数。
本次是第 3 次呼叫该商家(上限 3 次)
加上这一行之后,模型立即发现已达限制,错误率大幅降低。[原文 4668]
从这个案例提炼出的原则
把分散在上下文各处的隐式状态,提炼为可以直接读取的显式信息。
这是一条能反复复用的设计原则。它解释了为什么高水平的 Agent 系统里,总能看到类似这些设计:
| 设计 | 解决什么问题 |
|---|---|
| 系统状态栏 | Agent 不感知执行环境、用户时间、工作状态 |
| 任务清单外显 | Agent 忘记做到哪一步了 |
| 每轮标注已用次数 / 剩余额度 | Agent 数不清、超限 |
| 进度摘要 | 长任务中模型丢失全局 |
它们的共同点是:把"需要算"的变成"直接读"的。
三、为什么长上下文并不等于更强
由上面这层理解,可以推出一条反直觉但极其重要的结论:
上下文越长,不一定越好;超过某个点,反而更差。
原因有三条,逐条说清:
原因一:关键信息被稀释
窗口里塞了 20 万 Token,其中真正相关的可能只有 2 千。注意力要在这 20 万里捞出那 2 千——虽然它擅长检索,但信噪比下降会让准确率下降。
原因二:"现算"的代价随内容量上涨
回到那个"打了几次"的例子:上下文越长,需要现算的原始记录越多,模型越容易算错。上下文里每增加一份原始记录,都在增加"需要提炼"的负担。
原因三:污染风险
上下文里任何一段内容都可能影响模型判断。工具返回的网页正文、文件里的注释、甚至日志里的一行报错,都可能夹带误导性文字(第 14 章「提示注入」)。
上下文越多,攻击面越大。
四、于是,上下文怎么装就成了核心工程问题
综合下来,上下文工程要回答的不是"能塞多少",而是这五个问题:
| 问题 | 手段 | 详见 |
|---|---|---|
| 该装什么 | 优先级分层:系统规则 > 任务目标 > 相关证据 > 历史 | 2.3、5.1 |
| 装不下怎么办 | 压缩、摘要、丢弃低价值内容 | 5.2 |
| 怎么让它"能被提炼" | 状态栏、显式计数、结构化摘要 | 本篇第二节 |
| 怎么省钱省时 | KV Cache 复用、前缀稳定 | 5.3 |
| 怎么防污染 | 隔离不可信输入、标注来源 | 5.4、14.2 |
一个实用的分层模板
把上下文按"重要性"和"稳定性"两个维度分层,是实践中最好用的结构:
┌─────────────────────────────────────┐
│ ① 系统规则(最稳定,几乎不变) │ ← 放最前,可被缓存
├─────────────────────────────────────┤
│ ② 任务目标与约束(一次任务内不变) │
├─────────────────────────────────────┤
│ ③ 显式状态(每轮更新:进度/次数/时间)│ ← 提炼层,关键
├─────────────────────────────────────┤
│ ④ 相关证据(工具结果、检索片段) │ ← 最易膨胀,需管理
├─────────────────────────────────────┤
│ ⑤ 历史对话(最易稀释,可压缩或丢弃) │
└─────────────────────────────────────┘
这个分层有两个好处:稳定性高的放前面(有利于缓存复用,见 5.3);膨胀风险大的放后面(好截断,且不破坏前缀)。
五、给 Agent 装一只"状态栏"
原文提到一个很实用的技术:系统状态栏(status bar)。
它要解决的问题很具体:Agent 常常出现状态遗忘、目标偏离,根源在于缺乏对环境当前状态的感知和任务进展的跟踪。状态栏的做法是在上下文中嵌入结构化元信息,为 Agent 补上自我感知机制。[原文 4668]
写起来很简单,就是每轮在消息里附一段结构化文本:
【当前状态】
- 任务:处理账单争议
- 进度:第 3/10 个商家
- 本轮动作:已呼叫「商家 A」,累计 3 次(上限 3)
- 剩余预算:7 次调用
- 当前时间:2026-10-25 14:30
为什么这么有效? 因为它把"需要模型从原始记录里现算"的东西,变成了"模型一眼能读到"的东西——正好补上了那"缺失的一半提炼层"。
判断标准:如果你的 Agent 需要"数数"(第几次、还剩几个、做了多少),就应该外显;如果需要"记得"某个不容易从原始记录推出的结论,也应该外显。
六、上下文里到底装了哪些东西
要管理上下文,先得知道它由什么构成。一次 Agent 调用里,进入窗口的内容通常有六类:[原文 4568]
| 类别 | 具体内容 | 特点 | 管理策略 |
|---|---|---|---|
| 系统提示 | 身份、行为规则、约束 | 最稳定、优先级最高 | 放最前,尽量不变(利于缓存) |
| 工具定义 | 每个工具的名字、描述、参数 schema | 数量与体积容易失控 | 按需加载(第 9 章 Skills、第 4.5 工具爆炸) |
| 任务目标 | 用户请求、当前子目标 | 一次任务内基本不变 | 明确、简短、可复述 |
| 工具结果 | 读到的文件、API 返回、检索片段 | 最容易膨胀 | 截断、摘要、只留关键字段 |
| 历史对话 | 之前轮次的问答 | 长任务中迅速堆积 | 压缩、折叠、只留结论 |
| 环境状态 | 时间、目录、运行状态、进度 | 模型无法自行感知 | 必须显式外显(状态栏) |
这张表最有价值的地方在第 4、5、6 行:膨胀最快的三类,恰恰是最不该无限增长的。而第 6 行是最容易被忽略的——很多团队把注意力全放在"塞知识"上,却忘了告诉 Agent「现在是几点、你在哪个目录、任务做到第几步」。
一个量化视角
假设一个 Coding Agent 每轮平均消耗:
- 系统提示 + 工具定义:约 3K Token(固定)
- 本次读入的文件:约 8K Token
- 历史对话:第 1 轮 1K,第 10 轮可能 15K
那么到第 10 轮,光"历史"就占了总预算的一大半,而它对当下决策的价值远低于"当前文件内容"。
这就是为什么压缩的对象总是历史对话,而不是系统规则。
七、一个反直觉的自检清单
把这一篇的结论落成五个问题,用来体检你自己的 Agent:
| # | 问题 | 如果答案是"没有" |
|---|---|---|
| 1 | 我有没有把当前进度/次数/时间显式写进上下文? | 模型会数不清、忘记做到哪 |
| 2 | 我的系统提示是否足够稳定(不随轮次变化)? | 前缀缓存失效,成本翻倍 |
| 3 | 工具结果有没有截断或摘要策略? | 几轮之后就爆窗口 |
| 4 | 历史对话有没有压缩或折叠机制? | 长任务必然崩 |
| 5 | 我在上下文里区分了可信与不可信来源吗? | 易被注入攻击 |
这五个问题里,第 1 条最便宜也最有效,第 5 条最容易被忽略但后果最严重。
八、动手:给你的 Agent 做一次上下文体检
理论讲完,最重要的是一次实际检查。
五步体检法
| 步 | 动作 | 看什么 |
|---|---|---|
| 1 | 打印每轮实际发送的完整 messages | 总量与各段占比 |
| 2 | 统计各类内容的 Token 占比 | 谁最占地方 |
| 3 | 找出"每轮重复出现"的内容 | 哪些可以缓存或引用 |
| 4 | 找出"从不被引用"的内容 | 哪些是白占地方 |
| 5 | 检查状态信息是否外显 | 模型能不能"读到"进度 |
一个典型体检结果
总输入 12,400 Token
├─ 系统提示 1,500 (12%)
├─ 工具定义 2,000 (16%)
├─ 历史对话 5,800 (47%) ← 最多
├─ 本轮文件内容 2,600 (21%)
└─ 环境状态 0 ( 0%) ← 缺失!
这张表立刻给出两个结论:
| 发现 | 行动 |
|---|---|
| 历史占 47% | 该压缩(5.2) |
| 环境状态 0% | 该加状态栏(2.2 第五节) |
第 3 步和第 4 步最容易被跳过,但它们的收益最大。
一个常见发现:工具定义从未变化却重复发送
10 个工具 2,000 Token,每次调用都发一遍。如果前缀稳定,这部分应该被缓存(5.3)。
如果没命中缓存,通常是这两个原因:
| 原因 | 检查 |
|---|---|
| 工具顺序不固定 | 用字典遍历导致顺序随机 |
| 系统提示里有动态内容 | 时间、用户名 |
第一个很隐蔽:很多语言的字典遍历顺序不稳定,导致每次拼出的前缀都不同。
九、上下文腐化的实测信号
5.2 讲了"腐化"这个概念,但怎么发现它?
| 信号 | 说明 |
|---|---|
| 重复纠结已解决的问题 | 说明它找不到之前的结论 |
| 反复调同一个工具 | 说明结果没被正确利用 |
| 前后自相矛盾 | 说明注意力被分散 |
| 越到后面越差 | 典型的腐化曲线 |
最简单的检测:对比"第 2 轮的回答质量"与"第 8 轮的回答质量"(同样难度的问题)。
如果明显下降,就是腐化——该压缩了。
十、常见误区
误区一:窗口越大越好,干脆全塞进去。
不行。信噪比下降、现算代价上升、污染风险上升,三个问题会同时恶化。
误区二:模型会自己总结上下文。
不会。它擅长检索,不擅长提炼。"一共几条""进行到哪步"这类结论,每次都要现算,而且容易算错。
误区三:只要提示词写清楚"最多 3 次",模型就会遵守。
指令清楚是必要条件,但不充分。把"当前是第几次"也告诉它,遵守率才会显著改善。
误区四:长上下文能力是新模型的卖点,所以旧问题消失了。
窗口变大解决的是"装不下",不解决"算不准"和"信噪比"。窗口越大,后两个问题反而更突出。
误区五:上下文工程就是压缩。
压缩只是其中一环。完整的上下文工程包括**选择(装什么)、组织(怎么排)、提炼(状态外显)、维护(压缩与丢弃)**四件事。
十一、下一篇
现在我们知道了模型的"容器"有多大、也知道了它的"检索强、提炼弱"。
下一个问题更棘手:既然模型只是在预测下一个 Token,那它为什么会一本正经地说错话?幻觉到底从哪来,有没有可能彻底消除?
下一篇专门讲幻觉。