重构 agent 的上下文管理

2026-06-17

我自己写了一个 coding agent,目前日常使用都是用的它。 最近我又打算把上下文管理的实现方式重构了一版,这已经是第三次做大的重构了, 这篇博客会把每个版本改的啥,以及为什么那样改,记录下来。

其实前面多多少少已经提到过了,最开始是一个非常基础的版本, 就是用上下文窗口用满了就 compact。compact 会把最近的若干条消息保留,其它的都 summary。这个做法是版本 0,相当于什么策略优化都没有的版本。

然后是版本 1 的重构。版本 1 的主要思想是这个 即我想让 agent 自主决定它自己的上下文管理,我只提供它需要的一些指标,由它来决定做什么样的上下文管理操作。

实际实现的时候,我提供了三个工具,分别是 truncate tool output,llm context update,compact。 truncate 用于把工具的输出裁剪掉,只留下 [xxx chars truncated] 类似的信息。 然后 llm context update 是让 agent 把当前的上下文总结到一个文件,这个文件内容会被注入到 context 中。 具体的格式就有点像 plan 更新,当前任务是什么,已经完成哪些,接下来要做哪些,关键的文件改动,决策等等。 两者结合,就是一个轻量的上下文管理,不停地把工具输出这种信息密度较低的干掉,而保留总结性的 llm context 文本在上下文中。 最后 compact 工具就是一个传统的 compact,既用来兜底,也提供给 agent 决策自己去主动调用,比如一个大的 plan 中,刚刚完成了一个 task,就可以先 compact 了再处理下一个。

版本 1 做完,实际使用中就撞了个大墙。主要是指令遵守问题。我在前面文章中已经描述过了。 就是我一边要求模型完成它自己的主线任务,另一边又同时要求它自己来管理上下文。这样它的注意力就很分散,心智负担特别重。 为了让它遵守要求,自主管理上下文,我还在 agent 里面注入奖励惩罚机制,越发加重它的心智负担。

所以版本 2 的大重构,就是把普通模式,和上下文管理模式,完全隔离开了。 我在普通模式下,使用的就是正常的 system prompt,也不注入任何需要关注的上下文管理的指标,也不提供各种工具了。 然后普通模式运行一段时间,当 agent 检测到上下文使用比例比如 20% 之后,就开始触发进入上下文管理模式。

只有进入到上下文管理模式之后,LLM 才需要去关注上下文管理的事情,还是使用那些工具,去做决策调用哪些。 这比同一模式下耦合两件事情,心智负担要小得多。不管是工具,还是 system prompt,都是专门的,提供给这个模式下使用的。

为了让这个 agent 自主管理的效果好,我还专用用 auto research 的方式来打磨过 context management 模式下的工具和 prompt。 具体做法是,从许多的历史 session 中挑选,专门挑那些比较长的 session,尤其是发生 compact 的时刻的那些。 挑出来之后,构建测试集,输入就是这样的 session。然后让 agent 决定 context management 下的动作,观察它的行为是否合理。 评价方式就是用 llm as judger,看它执行 context manage 之前,跟之后,上下文内容对比。看看是否执行之后的,上下文质量更高。

truncate tool output 和 llm context update 是更低风险的操作,而 compact 是更高风险。观察到的一些现象就是,compact 之后崩溃的,丢失了太多信息然后导致任务走偏。 所以这个打磨过程就是用科学的方式,让 agent 自主上下文管理能够维护最佳的上下文内容。

版本 2 做完之后用了很长时间,基本满意。然后就到了今天的话题,为什么又双叕重构呢?

其实核心的点在于主流 SOTA 模型,上下文从 200K 升到 1M 这个范式变更。 deepseek v4 出来的时候,国产模型首次支持到 1M 的上下文。然后到 6.15 号,GLM 正式发布了 glm5.2 也开始支持到 1M 上下文。

我之前的专注点一直在于:如何在有限的上下文中,维护尽可能最优质量的内容。 不管是 truncate 掉低价值的工具输出,还是维护结构体的 llm context 信息,都是这样的方向。 我可以更频繁地 compact,用 llm context 摘要来精简上下文,只留下任务最重要的信息,以及若干最近消息。

而 1M 上下文之后,这个逻辑完全变化了!尤其是价格模型完全变化了。 1M 上下文可以维护更多的内容,不用再用得那么扣扣搜搜的,与其追求更好的上下文质量,不如追求更高的缓存命中。 以 deepseek v4 为例,flash 的缓存命中,和不命中,价格差距是 50 倍。而对于 pro,更是命中和不命中相差了 120 倍!

而之前无论版本 1 不是版本 2,都不算缓存友好的。 我明显能感受到版本 2 中那些不好的点,比如 truncate tool output 这个工具,如果我们把比较早期的内容 truncate 掉之后,从那个前缀开始,就是全部缓存未命中的。 所以这种 truncate 历史消息特别不缓存友好。

llm context update 这个取决于我们把 llm context 文本注入的位置,如果总是保持注入到最近一轮 user 消息的前面,然后 agent 做了特别多的活,比如好多轮的工具调用, 然后 user 再发一条消息,那 llm context 的注入位置就变化了,缓存前缀失效!

我在版本 2 里面把普通模式和 context management 模式彻底隔离,这也是一个很不缓存友好的行为。我不仅是两个模式下使用了完全不一样的 system prompt 和工具,并且还有一些特殊操作。 我要把普通模式的内容提取出来,对第个 tool call 注入一个编号,然后成为新的 context management 模式下的消息。 之前这么做的原因是,truncate tool output 需要有 tool call 的 id,才能知道 truncate 哪一条。我是把这个决策权给到 LLM 自主决策的。 但是按照正常的 openai 或者 anthropic API,LLM 其实它是拿不到 tool call id 的信息的,所以我要把内容改写成 "[tool call id: xxx] 原始内容" 再发给 LLM。

这就导致每次进入 context managent 模式,完全是全新的内容,缓存用不上需要重建。这个操作就很慢,明显慢于普通的 user prompt 操作。 还有更不爽的是,有可能进入 context management 后,LLM 给的决策是 do nothing! 因为 LLM 自主决策,当它认为当前上下文不需要调整,质量足够好,它就保持不操作。 这完全亏了,进入 context management 模式的代价那么高,却啥也不做。

还有就是触发进入 context management 模式的初始值。在 200K 上下文的时候,我设置的是 20% 开始触发。即 20% 以下,也就是 40K 以内上下文使用,完全不用管。 上下文使用率再往上去,使用率越高,会越频繁进入 context management 模式。比如达到 20% / 33% / 50% 使用率之后,我会每隔 15 / 10 / 7 次的 tool call,就进入到 context management 模式。 这些数值是我在 200K 使用和 tuning 感受下来的,而到了 1M 之后,20% 才进入,已经太晚了。然后进入之后,上下文已经很长,进入 context management 模式的代价很高。

我之前测试 deepseek 的时候,就已经意识到缓存命中这些问题了。没有急于修改的原因是 GLM 用的 coding plan 嘛,白嫖 API,反正也不是 token plan 计价格式。 缓存命中不命中,无所谓的。但是现在不一样了,GLM 也支持到 1M 的上下文了。这意味着我的 agent 也必须跟着调整了。

当前叫版本 3 吧。版本 3 的思路,主要几个考量的点:

  1. 必须考虑缓存友好性
  2. 最优性价比,而不是在有限上下文长度的最佳质量

缓存友好性,就意味着,任何已经发送给 LLM 过的历史内容,就尽可能不要改了。即使改,也是尽量只改动到最近的消息内容,这样前缀缓存还能保住。 那么前面版本 2 里面的 truncate tool output 最好就不要了,llm context 也尽量不要是文件注入了。

但是并不意味着一味追求缓存友好是最佳选择。如果一味追求缓存友好,那么做法最佳是版本 0,什么都不做,当上下文要爆了,才去 compact 一下。 有一个开源的 deepseek-reasonix 的项目,卖点追求的就是缓存命中率,但是一味追求这个点就是错的。为什么是错的?

  • 第一,当上下文没满的时候,其实已经衰退得厉害了。不要听信模型说支持 1M 就真的可以长上下文运行,质量下降得厉害。所以上下文的质量一定是考虑因素。
  • 第二,假设我们一直不做 compact 也不做任何上下文管理操作。那么,前面有 1 条消息的时候,再发一条新消息,token 缓存 1/2 等于命中率 50%。等 9 条消息的时候,再发一条消息,前 9 条都命中缓存, 这时缓存命中率就到了 90%...越往后,缓存命中率是越高的,也就是一味堆积上下文,啥都不做的情况缓存命中率最高
  • 第三,缓存命中,只表示成本低,但是也是有成本的。假设我们有 50 条消息,假设命中和不命中的价格是 50 倍。那么,我们不 compact 的情况,下一轮的价格是 50命中+1不命中。如果 compact 呢?变成两条不命中,价格一样等于 2不命中。但是下下轮呢,未来n轮呢?

采取 compact 的时候,后面轮次的成本就变成了 2命中+1不命中,而不 compact 则是 51命中+1不命中。经过几轮之后,compact 是会回本的!虽然 compact 的那一轮不命中,但是后面轮大大减少了上下文的 token 量。

这里最好是能预测,当前的上下文,未来还要经历多少轮次,才能计算最佳的性价比。单纯只算缓存命中,其实不是最优的。 当然,这个事情并不容易预测。所以我还是给 LLM 自主决定 context management 的触发。

最终版本 3 我的想法是,用 handoff 的方式实现上下文管理。可以把它想象成,有两个 agent,主 agent 把根据自己的 session 当前信息,写一个 handoff.md 文件,然后让子 agent 加载这个 handoff 文件。 如果只是这一步,就跟 compact 没有太大的本质区别。最最核心的点在于:我会留一个 QA 过程。让子 agent 看完 handoff 文件之后,如果有不清晰的,它可以向主 agent 提问!等 QA 环节完成之后,才算上下文管理完成。 这跟传统的 compact 最大的区别,就是在于崩溃恢复的韧性。compact 是一个不可逆的过程,是很容易搞崩上下文的,而在 handoff 模式下,有一个交接的过程,这会让质量提高不少。

这个版本的上下文管理,就是一个把 handoff 做成自动化的实现。fork resume rewind 这些还是要支持,不过这些都是实现细节了。

context managementAIagent