跳转至

上下文压缩(Context Compaction)

随着对话变长,历史最终会逼近模型的上下文窗口上限。Datus 通过 agent.compact 下的两种互补压缩自动管理上下文,让你在一个长会话里持续工作,减少需要手动清空会话的情况。

Minor compact Major compact
做什么 把较早的工具 I/O 归档到磁盘 把整个会话总结成摘要
驱动方式 规则(轮次开始时按 user 轮次计数;轮次中途按工具输出的新旧) LLM(一次摘要调用)
执行方式 同步,但很快(本地、无 LLM 调用) 同步、阻塞运行循环
是否触及最近轮次 否(轮次开始)/ 只触及本轮较早的工具输出(轮次中途) 是 —— 替换全部历史
可恢复 是(归档文件) 是(全历史 JSONL)

自动压缩统一在每次模型请求发送前检查,包括本轮第一次请求和重试。检查前先合并用户中途补充的消息。第一次请求可以按历史用户轮次归档,后续请求可以按工具输出的新旧归档;mid_turn_enabled 只控制后续请求。

Minor compact

触发条件 —— 在每个 user 轮次开始时,如果会话的 user 轮次超过 keep_recent_user_turns(默认 4),就会运行 minor compact。它的判据——user 轮次计数——只在轮次之间变化,因此每个轮次只判断一次。它是同步的,但由于只是纯本地的、基于规则的归档(无 LLM 调用),很快就能完成,几乎不会拖慢 agent。

做什么 —— 对于保留窗口之外的较早轮次,任何长度超过 archive_threshold 字符(默认 1000)的工具输出都会被移出实时对话并写入磁盘归档;原处留下一段简短的内联预览(archive_preview_chars,默认 1000;错误输出为 2×)并带上 [DATUS_ARCHIVED] 标记。

产生的行为

  • 最近 keep_recent_user_turns 个轮次的完整工具 I/O 保持不变 —— 轮次开始时的压缩永远不会削弱对话的活跃部分。
  • 较早的大体积输出会缩成「预览 + 指针」;当模型需要细节时,仍可通过 read_file 读取归档恢复完整内容。
  • 由于它只归档(不做摘要),不会丢失任何信息,也不消耗 LLM 调用。开销低、速度快、触发频繁。

Major compact

触发条件 —— 每次请求前,Datus 估算即将发送的输入大小,并预留回复空间。达到 major.token_threshold(默认 0.9)时,先尝试适用的归档策略,再判断是否需要摘要。有本轮最近一次调用的有效统计时,在它的基础上加上新增消息的估算;否则估算完整消息、系统指令和工具定义。手动 /compact 不受这个阈值限制。

做什么 —— 由模型把整个对话记录总结成一段摘要;随后用该摘要替换历史作为新的起点,对话由此继续。压缩前的完整历史会被转存为一个 JSONL 文件,并在摘要末尾附上指向它的指针,使 agent 能通过 read_file 找回任意具体细节。

产生的行为

  • 它是同步、阻塞的:循环会等待摘要生成后再发出下一次模型调用,因为那次调用必须看到压缩后的历史,而不是已超限的历史。
  • 可见对话被折叠 —— 较早的轮次被摘要替换。在 CLI 中会先显示 Compacting context… 提示,随后展示摘要面板;在 API/print 流中则作为一条 compact_summary markdown 消息到达。
  • 以一定的细节换取空间:摘要是精炼的,因此细粒度内容此时只存在于 JSONL 转存中(仍可通过 read_file 获取)。
  • 一次 major compact 会额外消耗一次 LLM 调用(用于摘要),因此较为少见,只在接近上下文上限时触发。

轮次中途的压缩

一个用户请求可能带来几十次工具调用,上下文往往在轮次结束前就已经填满。因此 Datus 会在正在进行的轮次里、每次模型调用之前重新评估占用 —— 此时上一轮的所有工具结果都已追加、没有工具还在运行 —— 并改写模型即将收到的历史。紧接着的这次模型调用就已经使用压缩后的上下文,任务无需任何用户干预地继续。

两个阶段,先用便宜的

  1. 归档阶段(minor) —— 达到 minor.mid_turn_token_threshold(默认 0.75)时:把本轮里比最近 keep_recent_tool_results(默认 5)个更早的工具输出移到磁盘归档,做法与轮次开始时对较早轮次的 minor 压缩完全相同。marker(路径 + 预览)不比原文短的输出不会被替换。不调用 LLM。如果这样已经把估算拉回到 major 阈值以下,就到此为止。
  2. 摘要阶段(major) —— 如果估算仍不低于 major.token_threshold:对(已归档的)对话记录做摘要,并把历史重建为
[user]      你在本轮的原始请求(原样)
[user]      你在本轮运行期间插入的消息(原样,若有)
[assistant] 摘要,附带 JSONL 恢复指针
[user]      一条简短指令:从摘要处继续,不要询问、不要重做已完成的工作

改写后的历史以你的请求开头、以一条指令结尾,因此模型会接着做任务,而不是去回应摘要。这条指令是合成的,不会出现在 CLI 与 API 展示的对话历史里。

你会看到什么

  • 改写后状态栏的上下文占用显示为 0,直到下一次模型响应提供实际输入 token 数;Compacting context… 提示与摘要面板会在轮次中途出现。
  • 本轮之后的调用都沿用压缩后的视图,并在其后追加新的工具回合。压缩前的工具输出不会再次发送,但完整记录保存在 JSONL 转存里,归档的输出保存在归档目录里。
  • 轮次中途压缩之后按 ESC 取消,会话会回滚到压缩后的视图(请求加摘要),而不是被清空。
  • 会话文件会被改写成与压缩后视图一致,因此之后的 resume 从它开始。

安全限制 —— 连续三次失败(例如摘要调用报错)或压缩后的视图本身仍高于阈值时,轮次中途的压缩会在本轮剩余时间里停止尝试,轮次以未压缩的上下文继续。摘要失败绝不会丢弃归档阶段的成果。两个阶段都可以用各自的 mid_turn_enabled 单独关闭,不影响轮次开始时的压缩。

自动 vs. 手动

  • 自动 —— 两种压缩在会话过程中自行运行,你无需做任何事。major 在接近上下文上限时触发,minor 随轮次增长、或随长轮次累积的工具输出触发。
  • 手动 /compact —— 无论当前用量如何,都会立即执行一次 major。适合在开始一项较大的新任务前使用,获得一个干净、已摘要的起点。

参数

配置项 类型 默认值 说明
compact.major.enabled bool true 启用 LLM 驱动的全历史摘要。
compact.major.token_threshold float 0.9 上下文窗口占用比例达到该值时执行 major(轮次开始与轮次中途均适用)。
compact.major.mid_turn_enabled bool true 允许摘要阶段在轮次中途运行。
compact.minor.enabled bool true 启用规则驱动的归档压缩。
compact.minor.keep_recent_user_turns int 4 轮次开始:保留最近 N 个 user 轮次的原始工具 I/O;更早的轮次可被归档。
compact.minor.archive_threshold int 1000 工具输出超过该字符数时转存到磁盘。
compact.minor.archive_preview_chars int 1000 归档标记中保留的内联预览长度(错误输出为 2×)。
compact.minor.mid_turn_enabled bool true 允许归档阶段在轮次中途运行。
compact.minor.mid_turn_token_threshold float 0.75 轮次中途:估算占用达到该比例时运行归档阶段。
compact.minor.keep_recent_tool_results int 5 轮次中途:本轮最近 N 个工具输出永不归档。
agent.yml
agent:
  compact:
    major:
      enabled: true
      token_threshold: 0.9
      mid_turn_enabled: true
    minor:
      enabled: true
      keep_recent_user_turns: 4
      archive_threshold: 1000
      archive_preview_chars: 1000
      mid_turn_enabled: true
      mid_turn_token_threshold: 0.75
      keep_recent_tool_results: 5

如需完全关闭自动压缩,将 major.enabled 与 minor.enabled 都设为 false。即使 major.enabled 为 false,仍可手动运行 /compact。

注意事项

  • 旧占用量记录没有有效性标记,可能包含升级前的估算值,因此升级后按未知处理,等待下一次模型响应刷新。
  • 显示的上下文占用来自最近一次模型响应的输入 token 数,包含缓存输入。context_usage_ratio 是该数值除以当前模型的上下文窗口。累计输入、输出用量单独统计,不会因压缩而减少。
  • 会话改写会同时使内存和持久化记录中的旧占用量失效。恢复会话后也显示 0,等待下一次实际统计。自动检查使用的估算只用于内部判断,不会作为实际占用量显示或落盘。
  • 压缩 API 的 new_token_count、tokens_saved、compression_ratio 返回 null。摘要输出的 token 数不能代表压缩后的请求大小;摘要面板中的 token 数仅表示摘要调用的输出量。
  • 当前容量估算约按每 4 个字符一个 token 计算,并包含系统指令、工具定义和回复预留。这只是估算,不能保证所有请求都不会超限;仍无法容纳的请求由服务商错误处理路径处理。
  • 改写缓存前缀可能降低下一次请求的缓存复用率。这就是归档阶段等到 75% 才运行、而不是持续运行的原因。
  • 上下文窗口大小未知时,跳过基于 token 的判断;第一次请求前的历史归档和手动 /compact 仍然可用。
  • Minor compact 依据会话的 user 轮次计数判断轮次开始时是否触发,因此在 resume 之后仍能正确工作。
  • 归档的工具 I/O 与全历史 JSONL 存放在会话的数据目录下;路径见 存储(Storage)。