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 即将耗尽时,系统应该如何优雅降级?

评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个