上下文压缩(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_summarymarkdown 消息到达。 - 以一定的细节换取空间:摘要是精炼的,因此细粒度内容此时只存在于 JSONL 转存中(仍可通过
read_file获取)。 - 一次 major compact 会额外消耗一次 LLM 调用(用于摘要),因此较为少见,只在接近上下文上限时触发。
轮次中途的压缩¶
一个用户请求可能带来几十次工具调用,上下文往往在轮次结束前就已经填满。因此 Datus 会在正在进行的轮次里、每次模型调用之前重新评估占用 —— 此时上一轮的所有工具结果都已追加、没有工具还在运行 —— 并改写模型即将收到的历史。紧接着的这次模型调用就已经使用压缩后的上下文,任务无需任何用户干预地继续。
两个阶段,先用便宜的
- 归档阶段(minor) —— 达到
minor.mid_turn_token_threshold(默认0.75)时:把本轮里比最近keep_recent_tool_results(默认 5)个更早的工具输出移到磁盘归档,做法与轮次开始时对较早轮次的 minor 压缩完全相同。marker(路径 + 预览)不比原文短的输出不会被替换。不调用 LLM。如果这样已经把估算拉回到 major 阈值以下,就到此为止。 - 摘要阶段(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:
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)。