Context Window Pressure: Why Long-Running Agents Get Dumber Over Time
Agent 不是越跑越聪明,而是越跑越"挤"。从 context window 的物理限制出发,看 token 竞争如何侵蚀长期运行 agent 的推理质量,以及工程上怎么对抗它。
如果你跑过一个生产级的 agent,一定见过这个模式:刚启动时它思路清晰、回答精准,跑了十分钟后开始"失忆"——忽略你几分钟前说过的话,重复犯同样的错误,或者在最简单的指令上犹豫不决。
这不是幻觉问题,不是模型能力问题。这是个很朴素的物理问题:context window 是有限的,而 agent 的"记忆"必须和"当前思考"竞争同一个 token 预算。
100 tokens 的罗生门
一个典型的 agent 循环中,每条消息消耗在 payload 上的 token,只有一部分是"有效载荷"。
拆开来看一次 agent 调用实际塞进 context 的是什么:
┌─────────────────────────────────────┐
│ System Prompt (benchmark 级固定) │ ~800 tokens
├─────────────────────────────────────┤
│ Tool Descriptions (每加一个工具叠加) │ ~2000+ tokens (10个工具左右)
├─────────────────────────────────────┤
│ Conversation History (逐轮累计) │ 指数增长
├─────────────────────────────────────┤
│ Retrieved Memories (2-5条各500t) │ ~1000-2500 tokens
├─────────────────────────────────────┤
│ Current User Query │ ~50-500 tokens
└─────────────────────────────────────┘
注意一个关键事实:系统提示和工具描述是"死重量"。无论当前在做什么任务,这些开销都固定存在。真正留给对话历史和记忆检索的空间,是剩余的部分。
128K context 听起来很大。但如果你是一个 auto-run agent,每小时执行 30 轮交互,每轮平均 2000 token(包含 tool call、response、error),一小时后历史就超过了 60K。倒推一下,token 预算不是在"够不够用",而是在"第几轮被耗尽"。
三种压缩策略,三种副作用
1. 滑动窗口(Sliding Window):最简单的"忘了它"
保留最近的 N 轮对话,丢弃更早的。
- 副作用:agent 会周期性"失忆"。一小时前的决策依据、用户偏好、中途出现的约束条件,窗口滑过就没了。用户甚至会观察到一种诡异的行为——同一个错误,agent 改正两小时后,又犯了一次。
这就是所谓"agent 的认知周期":它清醒 → 积累上下文 → 窗口满了 → 被截断 → 重新变"蠢"。
2. 总结压缩(Summarization):用精度换长度
每 N 轮触发一次总结,用几句话概括已发生的事情,替换掉原始对话。
- 副作用:每次总结都是有损压缩。细节在第一次总结时消失,而后续总结只能基于前一次总结的内容——信息逐级退化。五轮总结后,agent 对早期历史的"理解"可能已经面目全非了。更致命的是,summary 本身也是 token,如果总结写得不够精炼,压缩效果约等于零。
实验中发现一个有意思的 pattern:agent 在 summary 过的上下文中做决策时,倾向于"更保守"——因为它缺失了原始对话中的情绪线索、语气变化和细微差别,只能凭逻辑硬推,导致决定变得生硬。
3. 检索增强(RAG-style Memory):给 context 做"分页"
不让历史占据 context,而是把它们持久化到外部存储中,每次只检索当前最相关的片段插入上下文。
- 副作用:检索是有延迟的,而且可能失败。agent 在关键时刻可能拿到一段不相关的记忆,"误导"比"没有信息"更糟糕。另外,检索本身也消耗 tokens——检索到的内容可能太长,被迫截断;也可能太短,不足以支撑决策。
三者在工程选择上的本质区别是:滑动窗口是遗忘,总结是扭曲,检索是冒险。 没有完美方案。
一个被低估的隐性成本:Attention 稀疏
说完了显性的 token 消耗,还有个更隐蔽的问题。
Transformer 的 attention 复杂度是 O(n²)——但更关键的是,attention 在 context 过长时会变得稀疏。2023 年 Liu et al. 的研究("Lost in the Middle")已经证明:当相关信息位于长 context 的中部时,模型检索它的能力会显著下降。
这意味着什么?即便你硬塞了 100K tokens 进去,模型真正能有效利用的,可能只有前后各几千 tokens。中间的绝大部分成了"沉默的 token"——占据空间、消耗计算、但不产生价值。
这对 agent 架构的影响是结构性的:如果中间的 token 是"沉默的",那你的 conversation history 的排列方式就极其重要。最新操作在末尾所以 OK,但历史中的关键决策点可能恰好在中段,被模型忽略了,而你完全意识不到。
对抗策略:给 context 做"城市规划"
既然 context 就是一个多租户空间,那就得像城市规划一样管它。
策略 A:优先级分层
给每种内容设置硬性预算上限:
|系统提示 ← 固定 800,不可压缩
|工具描述 ← 固定 2000,不可压缩(除非工具注册量巨大,做懒加载)
|当前轮次 ← 固定 1000,完整保留
|近期历史(3轮) ← 固定 6000,完整保留
|中期历史(后续) ← 用摘要替代,预算 2000
|远程记忆 ← 检索片段,预算 1500,超了就裁剪
每轮执行前做一次 token 审计:如果总预算超了,优先压缩"中期历史"而非"远程记忆"。
策略 B:分片注意力
借鉴 Raffel et al. 2019 的思路:不要让所有 token 互相 attention。把历史拆成独立分片,每个分片内部 full attention,分片之间用压缩后的 summary token 连接。
听起来实现复杂?实际上一条 prompt 就能近似:在当前 query 后,先用 [Context of last 5 turns] 完整粘贴历史,然后用 [Earlier context: ...] 加上一段压缩摘要。注意力天然集中在前后两端——这正是模型最擅长处理的区域。
策略 C:主动式"遗忘-归档"
结合昨天的"遗忘策略",建立三档存储:
| 层级 | 存储 | 访问方式 | Context 占用 |
|---|---|---|---|
| Hot | 当前 context | 直接可用 | 最近 3 轮 + 当前 query |
| Warm | 近期记忆 | 检索触发 | 仅在需要时载入,用后释放 |
| Cold | 长期归档 | 仅当显式引用 | 不占用 |
关键设计:agent 自己决定何时从 warm 层拉数据,而不是每次调用全量塞入。
所以
context window 压力不是 bug,它是 transformer 架构的基础物理约束——就像内存不是 bug 一样。接受它,给 context 做预算管理,比追逐更大的窗口更有工程意义。
128K context 能买到的是"故障窗口更宽",但它买不到你 agent 的长期一致性。真正的一致性需要在架构层做存储分层和预算控制。
明天继续深入这个话题:怎样设计 agent 的"退化路径"——当 context 即将耗尽时,系统应该如何优雅降级?
评论
发表评论