粤ICP备2026098386号-2
上下文窗口管理:Claude Code 和 Codex 是怎么想的
返回首页

上下文窗口管理:Claude Code 和 Codex 是怎么想的

上下文窗口管理怎么做?本文从自己的设计直觉出发,结合 Claude Code 和 Codex 的实际实现,整理出一套可落地的上下文管理思路。

上下文窗口管理:Claude Code 和 Codex 是怎么想的

说实话,写这篇文章之前,我对上下文管理的理解只有一条直觉:窗口快满了,就把旧对话总结一下丢掉,最近几句留着,重要的字段单独拎出来放前面。

后来自己做 PageMind 的上下文管理模块,又去翻了 Claude Code 和 Codex 的实现,发现直觉的方向是对的,但真正做起来,细节比想象的多得多。这篇就当是折腾一圈的笔记。

为什么要管上下文

用长会话的人应该都有体会:聊到后面,模型开始"忘记"最开始的目标。你第 10 句让它记住的事,第 100 句的时候它当没发生过。这不是它变笨了,而是上下文太长,注意力被摊薄了。网上管这叫 context rot,上下文腐烂,挺形象。

有一次我让 Claude Code 改一个老项目的代码,改到后半段,它把我早就确认过的目录结构忘了,重新去读了一遍文件,读完才"哦,是这个结构"。原因就是上下文太长,前面的信息被挤得看不清了。

还有个现实问题:每轮请求都按 token 计费,对话越长,单次请求越贵越慢。我自己跑过一次全量重构,看着用量蹭蹭涨,钱包在滴血。后来学乖了,主动清理上下文,比等它自己膨胀到爆便宜得多。

所以上下文管理就三个问题:窗口快满怎么办、稳定的知识放哪、进行中的状态怎么传

我最初的直觉

没看任何实现之前,我脑子里是这样设计的:

  • 上下文超过阈值,把前面的历史做个摘要;
  • 最近几轮对话不动,原样保留;
  • 把关键信息整理成 JSON,长期放在上下文里。

现在回头看,这三条其实都踩在点上了——Claude Code 和 Codex 的主干逻辑就是这么干的。但细节上差得远。

Claude Code 的做法

Claude Code 的窗口标准是 200K token,现在还能开 1M。但窗口再大,会话总能聊爆,所以它准备了几层压缩,从便宜到贵。

最便宜的一层根本不调 API:把很久以前的工具输出(读文件的结果、跑命令的日志这类大块临时数据)直接清掉,换成一个占位符,说"这段内容被清理了"。对话的结构还在,只是把没用的重东西拿掉了,越旧的内容越优先清。

如果还不够,它会用会话过程中已经提取的记忆来当摘要,也不调 API。实在不行了,才调用模型正经做一次总结,把旧历史压成摘要,压缩完还会把最近读过的文件、项目说明这些重新塞回去,避免模型"断片"。

有个细节我印象很深:压缩之后,它会在摘要和最近的完整对话之间放一条边界标记,告诉模型"这边是别人总结的二手信息,那边是原话"。别小看这个标记——没有它,模型会把摘要里的遗漏当成事实。这个设计很小,但很聪明。

另外它还有两招"曲线救国":一是子代理,重活丢给子代理干,子代理有自己的上下文窗口,干完只把结论带回来,这比压缩划算多了;二是提示缓存,把不变的内容放在前面,每轮请求命中缓存,能省一大笔钱。

Codex 的做法

Codex 的路子不太一样,但思路殊途同归。

它的内核里有个 ContextManager 管上下文记账:每一条消息、每次工具调用有没有配对的结果、现在用了多少 token,都算得明明白白。token 数不是 API 报的,是本地估出来的,不精确但够用——估算这件事本身就很关键,总不能每次都去问模型"你现在用了多少 token"。

压缩的时候,Codex 把历史交给后端的专用接口处理,产出的摘要会被包装成一份"前任 agent 的交接文档",相当于告诉模型:这是别人留给你的交接材料,好好看。这个"交接"的框架感我觉得挺有意思,比单纯说"这是历史摘要"更让模型当真。

缓存方面 Codex 更严格。它的请求是无状态的,缓存只有前缀完全匹配才命中,所以什么东西放前面、什么东西放后面,要求非常死板,中途改一次配置都可能让整轮缓存失效。

长期记忆它分了两层:AGENTS.md 放稳定的项目约定,会话结束之后后台异步把这次会话总结成 memories 存到本地,下次要用了直接读文件——连向量检索都没上,grep 一下完事。简单粗暴,但够用。

折腾下来我的几点体会

看了两家实现,结合自己做 PageMind 的实践,我的体会是:

摘要永远比截断好。 截断是丢掉"记过的东西",摘要只是把"原文"换个说法。信息会有遗漏,但思路是连续的,模型还能接着干。

把重活移出去,比压缩更省钱。 能丢给子代理的、能写进文件的,就别塞窗口。窗口里只留必须的。

状态要重写,不是追加。 我一开始想的是 JSON 越攒越长,后来发现不行——摘要本身也是 token,越长越占地方,还稀释注意力。正确做法是每个阶段结束,把状态重新整理成一个紧凑的版本,而不是一直往上摞。

注入的位置很讲究。 不变的内容放前面,命中缓存就免费;老变的内容放后面。我在 PageMind 里就是让状态块只在关键节点更新,平时保持稳定,又省钱又不会打断模型。

能外置的就外置。 真正的长期记忆应该落在本地存储里,窗口里只留"进行中"的状态。PageMind 把历史对话全量存在 IndexedDB,恢复会话的时候压缩好再注入,就是这个思路。

最后

要说总结,就一句话:把上下文窗口当缓存,别当仓库。稳定的东西放外面,活动的东西放里面,历史按需压缩。

我最初那版直觉(阈值触发、保留最近轮次、结构化注入)撑起了大概八成的框架,剩下两成的关键细节——token 预算、边界标记、重写不追加、缓存排布——都是从 Claude Code 和 Codex 的实践里学来的。写下来也算给自己留个记录,你要是也在做类似的东西,希望能有点用。