FIELD NOTE · CONTEXT ENGINEERING
Agent 上下文管理:Compact 阈值计算与 TUI 显示
同一套 GPT OAuth,Codex 显示 272K,OpenCode 显示 500K。数字并不只是大小不同,它们代表的上下文口径也不同。
同一个GPT OAuth,为什么codex显示上下文272k,opencode显示500k?
今天被 OpenCode 和 Codex 的上下文数字搞得有点烦。一路追下来才发现,我最初问的问题和最后真正想清楚的问题,其实不是同一件事。
使用 GPT-5.6(ChatGPT OAuth)时,OpenCode 显示 500K,Codex 显示 272K,可用输入大约 258K。我的第一反应是:OpenCode 支持 500K 上下文,牛逼。查了一圈才知道,OpenCode 当时是按 Codex 早期的 372K 输入预算,再加上 128K 最大输出,拼成一个 500K 的总窗口展示给用户。后来 Codex 改回 272K,OpenCode 的模型数据没有同步,于是出现了现在的情况:用户看到 500K,以为能稳定用到接近 500K 的上下文,实际上大约 350K 左右就可能触发 compact。
这引出一个更基础的问题:为什么要把输出上限也加进「上下文窗口」?一开始我以为,这是为了在计算剩余空间时提前扣掉即将生成的回复,避免历史已经很大,下一轮再输出一段就把总窗口撑爆。听起来合理,细想又不太对。用户看到 “Context Usage” 时,一般会自然理解成当前对话已经占用了多少输入上下文。既然如此,为什么 OpenCode 还要把本轮 output tokens 加进这个数字?
看完源码后才知道,OpenCode 实际上维护了不止一个限制:context 为 500K,input 为 372K,output 为 128K。其中 500K 主要用于模型目录和界面展示;真正的 compact 阈值由 input limit 计算,再减去 reserved。默认情况下 OpenCode 还有一个 20K 的压缩预留,因此 compact 阈值大约是 372K 减去 20K,也就是 352K。换句话说,对这个模型而言,500K 主要是一个「输入加输出的总 envelope」,真正影响 compact 时机的却是约 352K。
于是问题变成了:一个不直接决定 compact 时机的数字,为什么要被拿来当 Context Usage 的分母?OpenCode 的 App、TUI 和 ACP 都会用 tokens / limit.context 来展示占用率。这里的 tokens 又不只是历史输入。它取最后一次 Assistant 的 usage snapshot,把 input、output、reasoning、cache.read 和 cache.write 全部加起来,再除以 500K。因此它真正展示的是:当前这一轮请求的 prompt 输入,加上这一轮已经生成的 completion 输出,占模型总窗口的比例。从这个口径看,用 500K 做分母是自洽的。
假设某轮调用使用了 320K 的 input 与 cache,再加上 32K 的 output 与 reasoning,合计 352K。OpenCode TUI 会显示 352K / 500K = 70%。但下一次发起请求前,runtime 会拿同一份 usage snapshot 去和约 352K 的 compact 阈值比较。结果就是 TUI 还显示 Context Usage 70%,runtime 却已经认为应该 compact。数学上没有问题,产品语义却很容易误导用户。看到 70% 的进度条,大多数人的第一反应都是「还有 30% 可以用」。实际上这 30% 并不是可继续使用的 prompt budget,下一条消息发出去之前,runtime 可能已经需要先压缩会话。
但500K 并非完全没有机会接近。假设上一轮 input 已经达到 351K,刚好还在 352K compact 阈值以下,这一轮模型又真的生成了 128K output,那么当前 usage 可能来到 479K,TUI 可以短暂显示 96%。但这不是一个可以继续稳定使用的状态。下一条用户消息发出时,runtime 会在请求模型之前发现已经远超 352K 阈值,于是先 compact,再发送请求。虽然我还是觉得这种显示方式不合理,但至少它的内部逻辑是自洽的:它展示的是 total request window,而不是可用 prompt budget。
再看 Claude Code,它采用了另一种显示口径。主状态栏的 Context 只计算 input 与 cache,不包含本轮刚生成的 output。从用户直觉来看,这个数字更接近「产生当前回复时使用了多少 prompt input」。但 Claude Code 内部判断 compact 时,又会把本轮 output 加回来,再加上 API 响应后新增消息的估算。于是同一个状态,在状态栏和 compact 内部会呈现出两套不同的占用数字,但是呈现给用户的口径一直是“你还能塞多少上下文进来”。
- 预测尝试
- 217K
- 完整 compact
- 239K
- 硬阻断
- 249K
- 有效窗口
- 252K
到这里,两种实现的差异就比较清楚了。OpenCode 的主 TUI 展示的是当前 input、cache、output 与 reasoning 之和,除以 total request window;Claude Code 的主状态栏展示的是当前 input 与 cache,除以原始上下文窗口。但它们做 compact 时,都会考虑当前 output,因为这一轮 output 在下一轮会变成对话历史的一部分。真正不同的,主要是用户看到的指标:OpenCode 展示当前请求完成后的总 envelope,Claude Code 展示产生当前回复时使用的 prompt input;OpenCode 的 TUI 会立即反映本轮 output,Claude Code 的主状态栏不会,但内部 compact 已经把它计算进去了。这两个指标都能自圆其说,但如果都只叫 “Context Usage”,用户依然很难知道自己真正还能使用多少。
我更希望 Agent 直接把关键数字摊开:总窗口用了多少、prompt input 用了多少、compact 阈值在哪里、当前 output 是多少、本次实际请求的 output 预算又是多少。这样用户就不需要自己猜:眼前这个大数字到底是总窗口还是输入窗口,真正的输入硬上限在哪,为什么进度条还没到顶却已经要 compact,当前 output 有没有进入上下文计算,以及下一条消息发送前到底会发生什么。
回到最初的问题:OpenCode 显示 500K,到底算不算错?如果它展示的是当前 input 加当前 output 占 total request window 的比例,那么 500K 作为分母在数学上没有错。它真正不合理的地方,是把这个指标笼统地命名成 Context Usage,却没有同时告诉用户真正决定下一步行为的 input limit 和 compact threshold。如果让我自己做 Agent,我不会只展示一个看起来很大的 Context 数字。我更倾向于直接告诉用户:现在实际用了多少、还能输入多少、什么时候会 compact,以及下一条消息发送前到底会发生什么。模型规格可以复杂,但用户不应该靠猜。