超长会话的上下文淘汰策略:借鉴缓存算法管理 Agent 对话历史
一个 Agent 会话跑到几百轮之后,上下文窗口迟早会被填满——这时候面对的已经不是”要不要压缩”的问题,而是”删哪些、留哪些、怎么删”的具体策略设计。市面上讲上下文压缩的文章大多聚焦”用什么方法压”(摘要、RAG 替换、结构化截断,这部分 上下文压缩 5 种策略对比 已经讲得很全),但很少有人系统讲”淘汰顺序怎么定”——这其实是一个更接近传统缓存淘汰算法的问题:上下文窗口就是一块容量有限的缓存,超长会话里的每一轮对话、每一次工具调用,都是竞争这块空间的”缓存条目”。
这篇文章借用缓存淘汰算法的思路,拆解超长 Agent 会话该按什么顺序淘汰上下文,以及淘汰、压缩、归档这三种手段各自该用在什么地方。
一、先分清三种手段,不要混为一谈
“上下文管理”经常被笼统地叫做”压缩”,但实际上是三种性质不同的操作:
| 手段 | 做法 | 信息是否可恢复 | 适用场景 |
|---|---|---|---|
| 淘汰(Eviction) | 直接从上下文中删除 | 不可恢复 | 确认无用的内容,如已完成任务的中间工具输出 |
| 压缩(Compression) | 摘要/结构化提炼后替换原文 | 部分可恢复(有损) | 早期对话历史,细节不重要但脉络需要保留 |
| 归档(Archival) | 移出上下文,存入外部存储(向量库/文件) | 完全可恢复(按需检索) | 可能还会用到但当前不需要的信息 |
三者的顺序通常是:先判断能不能归档(移出去但保留可检索性),不能归档的判断能不能压缩,都不合适的才直接淘汰。很多实现直接跳过归档、压缩,一上来就淘汰,原因通常是工程复杂度——归档需要额外的检索基础设施,压缩需要额外的模型调用,而淘汰只需要一个删除操作。超长会话场景下,淘汰仍然是用得最多的手段,所以值得单独把淘汰策略讲清楚。
二、把上下文条目按”淘汰优先级”分层
淘汰的核心问题是排序:上下文里几十上百个条目,先删哪个。可以借用缓存算法里现成的排序思路,但要结合对话场景做调整。
FIFO(先进先出):最简单但对 Agent 场景不够用
最朴素的做法是淘汰最早的内容,类似滑动窗口。问题是 Agent 会话里”最早”不等于”最不重要”——用户在对话开始时给的任务目标、约束条件,可能是全程都要参考的关键信息,用纯 FIFO 淘汰会第一个把这些删掉。
LRU(最近最少使用):比 FIFO 更适合,但要小心误判
按”多久没被引用”排序淘汰,比单纯按时间顺序更合理——如果某条早期消息在后续对话里被反复提及或依赖,它的”最后访问时间”会被刷新,不会被优先淘汰。实践中需要一个轻量的引用检测:检查后续轮次是否提到了某条历史信息涉及的实体、文件路径、变量名,作为”最近是否被使用”的代理信号。
优先级 + 钉住(Pinning):Agent 场景里最实用的组合
单纯的时间类算法(FIFO/LRU)都不区分内容的”重要性等级”,而 Agent 会话里不同类型的条目重要性差异很大。实践中更有效的做法是先分类定优先级,同优先级内再按时间淘汰:
优先级从高到低(数字越大越先被淘汰):
0 — 系统提示词、任务目标声明 [钉住,永不淘汰]
1 — 最近 N 轮的完整对话(含工具调用) [保护窗口内]
2 — 关键决策点(用户明确确认/拒绝的节点)
3 — 早期对话的文字交流
4 — 工具调用的原始输出(尤其是大段日志、文件内容)
5 — 已经被后续摘要覆盖的旧工具输出
淘汰时从优先级 5 开始清,清到腾出足够空间为止;优先级 0 和保护窗口内的内容(优先级 1)永远不参与淘汰候选。这个分层里最关键的一条经验规律是:工具调用的原始输出(优先级 4)几乎总是最先该淘汰的对象——下一节详细说原因。
三、工具输出是超长 Agent 会话里最该优先淘汰的部分
一个跑了几十轮的 Agent 会话,上下文里占比最大的往往不是用户和模型的对话文字,而是工具调用返回的原始内容:文件读取的全文、命令执行的完整日志、搜索结果的原始 JSON。这些内容有几个特点决定了它们该被优先淘汰:
- 一次性消费:工具输出通常只在紧接着的下一轮里被模型引用一次(比如读完文件内容后据此做修改),之后很少再被直接引用原文
- 体积远大于其信息密度:一次
grep或者cat的输出可能有几千 token,但真正影响后续决策的往往只是其中几行 - 有更好的替代:工具输出淘汰后,如果后续真的还需要,重新调用一次工具(比如重新读一次文件)比在上下文里一直背着它更便宜
实践中的做法是给工具调用结果加一个”消费标记”——模型在下一轮引用过某个工具输出后,就可以把这条输出标记为”已消费,可淘汰”,而不是等到窗口打满才批量清理。这比等到快满了再决定”删哪个”的被动策略更平滑,避免了满窗口时刻突然的信息断裂感。
代理性质的编程助手(比如 Claude Code、Cursor Agent 模式)在处理长任务时基本都采用这个思路:早期工具调用的完整输出会被主动折叠或替换成”这一步做了什么”的简短总结,只保留最近几步的完整细节。
四、淘汰触发时机:被动触发 vs 主动预留
被动触发:上下文快接近上限时才开始淘汰。实现简单,但触发那一刻往往需要一次性淘汰大量内容才能腾出空间,容易造成”这一轮突然反应变差”的体感断层——模型此前依赖的某些上下文被批量清掉,连贯性明显下降。
主动预留:设定一个阈值(比如上下文用量达到 70% 就开始淘汰),持续小批量清理,永远给窗口留出缓冲空间。这样每次淘汰的量小,对连贯性的影响被摊薄,不会有明显的”断层时刻”。代价是要更频繁地做淘汰判断,工程实现上多一层调度逻辑。
超长会话场景(几百轮起步)建议用主动预留,因为一次性大批量淘汰对这类场景的连贯性伤害是复合的——早期决策被清掉之后,后续每一轮的判断都会受影响,而不是只影响当下这一轮。
五、淘汰后要不要留”墓碑记录”
直接删除还是留一条极简的”墓碑”(比如”[已淘汰:关于 XX 文件的讨论,共 N 轮]”)?建议保留墓碑,原因是:
- 模型如果在后续对话里被问到”我们之前是不是聊过 XX”,看到墓碑至少知道”聊过但细节没了”,可以如实告诉用户去查历史记录,而不是编造或者装作没聊过
- 墓碑的 token 成本极低(一行文字),换来的是避免幻觉的价值
墓碑不是压缩,它不承担”恢复信息”的功能,只承担”标记这里发生过什么但已被清空”的功能,这也是它和压缩摘要的本质区别。
六、和长期记忆系统的边界
淘汰策略处理的是单次会话内的上下文管理,不要和长期记忆系统混淆——用户跨会话的偏好、历史事实这类需要长期保留的信息,应该走独立的记忆存储和检索机制,不应该依赖”这一次没被淘汰所以还在”这种脆弱的方式来保证持久性。长期记忆的保留周期和清理规则是另一套独立的问题,可以参考 Agent 记忆保留与清理策略。两者的关系是:淘汰策略决定”这次对话还记不记得”,记忆系统决定”下次对话还记不记得”。