跳转到内容

历史压缩

长会话会被有策略地压缩,而不是简单粗暴地截断。有两个触发点,一个开销很小、在每次调用模型之前都会跑 一遍,还有一条针对 provider 直接拒绝请求时的应急恢复路径。

触发点 检查时机 自动模式下的默认值
轮次之间(--compact-threshold 每次给 provider 发消息之前 上下文窗口的 75%
批次内部(--compact-batch-threshold 每个工具调用批次之后、同一轮里下一次调 provider 之前 85%——故意设得更高,不像轮次之间的检查那么急于打断一个工具调用循环

两者语义一致:0 = 自动(用上面那个百分比,可以用 --compact-auto-pct 调)、<0 = 关闭、 >0 = 一个具体的 token 数。token 计数优先用 provider 实际报告的用量,而不是 octo 自己的估算—— 一旦有真实数字就用真实数字。

不管是自动触发还是手动跑 /compact,永远是这个顺序:

  1. 回收陈旧的工具结果——不调模型。 最近的 6 个 tool_result block 永远原样保留。 更老的、超过 4KB 的会被替换成一个占位符,说明内容被省略了,重新跑一遍工具就能再看到。 如果这一步就已经把估算 token 数拉到阈值以下,压缩到此为止——不调用总结模型,不花钱。
  2. 总结最老的那些轮次——调一次模型。 只有回收之后还是超阈值才会走到这一步。从最新往最老走, octo 会把完整的用户轮次不断加进“保留”的尾巴里,直到再加下去就会超过一个 keep 预算(窗口的 30%,同时还有个上限,不超过阈值本身的一半)。比这更老的内容会被折叠成一份摘要。 拆分点永远不会落在一个轮次中间,所以一次工具调用和它的结果不会被拆散。

不管会话涨到多大,最近的小块工具输出永远不会被动——只有“旧 + 大”这个组合才会被回收。

如果要折叠的那部分占历史总量不到 15%,自动路径会直接跳过这次总结,而不是为了折叠一小撮内容 去花一次模型调用——这种情况通常说明真正的体量卡在保留的尾巴里(一个带了一大堆工具调用的巨型轮次), 而不是老历史里,如果不加这个防抖,接下来几乎每一轮都会重新触发一次收效甚微的压缩。

这个防抖只在自动路径生效:手动跑 /compact 永远会尝试折叠——既然你主动要求了,哪怕收益很小也是收益。

如果一次发送因为 provider 认为请求太长而失败,octo 会在同一轮里重试,按需要逐级升级:

  1. 先回收,再原样重试——如果错误信息里带了具体差多少(很多 provider 会给),并且光靠回收 就腾出了足够的空间,直接重试,不调用总结模型。
  2. 弹出最新一条消息,总结剩下的,重试——只丢最新那一条(尽量保住 provider 的 prompt cache),压缩剩下的部分,把弹出的那条重新接回去。
  3. 弹出大约一半的历史,总结,重试——只有第 2 步还是不够时才会走到这一步;牺牲 cache, 换取这次重试一定能成功。

这条路径是被动触发、每轮最多一次——和上面的主动触发不同,它只在真正发送失败之后才会启动, 并且它瞄准的是错误信息里解析出来的确切差额,而不是笼统的 30% keep 预算。

不管是哪个触发点,还是手动 /compact,只要发生了一次真正的总结调用,并且是在一个会持久化的会话里 (CLI 或 Web UI,headless 单发和 IM 不会)——被折叠的原始消息会先被存成一份可读的 Markdown 记录, 然后才被摘要替换掉。留在历史里的那份摘要会带一个指向归档路径的提示,之后如果某个细节真的重要, 模型可以用 read 工具把原文读回来。回收(上面第 1 步)和“上下文太长”恢复都不会归档——只有真正 跑了一次总结调用才会。

/compact 和自动压缩在所有传输方式上推的都是同一套三个事件(开始、进度、完成),所以 TUI、 Web UI、IM 渠道看到的表现完全一致:

  • 只做了回收:✦ reclaimed stale tool output · ~Xk → ~Yk tokens
  • 真的总结了:✦ compacted context · folded N message(s) · ~Xk → ~Yk tokens
  • 没有明显效果:什么都不打印,而不是假装做了什么有用的事。

config.yml 里设置 lite_model,总结调用会先跑在这个更便宜 的模型上,只有 lite 调用失败才回退到你的主模型——这是在一个长会话里控制压缩本身开销的主要手段。

下一步:一次回复本身(不是输入)超长导致的中途错误是另一套机制——见 Agent 循环里“从截断的回复里恢复”一节。