Skip to content
种中藏界

Compact 多轮稳定

说明 Compact 标记与投影指令如何跨轮次和会话重建模型视图,并列出版本不匹配、持久化失败与 Provider 差异的降级边界。

编写于 2026-08-16

Agent Loop 解释了 Compact 的触发规则和模型视图。本稿讨论另一个问题,Compact 不是一次性动作。长会话里它在每一轮都可能触发,会话结束后还可能跨进程恢复。多次触发、跨会话恢复之后,模型看到的视图必须保持一致——否则同一段历史,上一轮压缩后的截断效果在这一轮生效,下一轮又变回原样。

多轮稳定依赖三个能跨轮次存活的东西,

  1. Compact 标记,记录消息被排除(excluded)还是被截断(truncated)。
  2. 投影指令(projection directive),记录每条被截断消息的精确压缩方案。
  3. 消息标识,把正文、标记、指令重新对应起来的键。

三者按同一个 message_id 装配。装配发生的位置有两个,每一轮进入推理之前,以及会话恢复时。

标记代替删除,正文不动

Compact v2 的设计原则是标记代替删除,不修改消息本体,旧消息由 visible_messages() 按标记自动过滤或投影。MessageFlags 承载三个相关字段,

字段含义写入方
truncatedMicro 截断该消息micro 应用层
excludedFull Compact 排除该消息full 应用层
projection该消息的投影指令(policy_version + entries)micro 应用层

关键细节,投影指令里每个 entry 都带 message_id。指令不是散装的字符串,它和消息正文靠同一个标识对应。Micro 应用层按 message_id 把 action 分组,为每条消息生成独立的 MessageProjectionDirective,写入 flags 时同时设置 truncated。正文全文保留在存储里,模型看到的截断视图在渲染时由指令重建。

分层控制状态写入

位置职责副作用
triggermod.rsrun_compact / determine_compact_action按 budget 判定策略、执行、统计调用应用层
plannerplanner.rsplan_micro只读扫描 transcript,生成投影计划禁止写 flags
projectionprojection.rs从持久化指令重建计划、渲染 LLM 视图无(纯函数)
mutatemicro.rsmicro_compact把计划写成 per-message 投影指令唯一写标记的出口
configCompactConfig阈值与排除规则

planner 的文档注释写死了约束,只能读 MessageTranscriptCompactConfig,绝不能调用 set_truncatedset_excluded、持久化或 provider。projection 的 render_llm_view 是不触碰 transcript 的纯函数。这样无论 trigger 跑多少轮,修改 transcript 的只有 mutate 一个出口——这是视图可重复的前提。

配置控制触发范围

CompactConfig 是纯数据契约。auto_compact_threshold 在反序列化时会 clamp 到 [0.0, 1.0] 并告警,防止配置错误导致 Full 升级路径被静默绕过。以下默认值来自源码,

配置项默认值作用
micro_compact_threshold0.75budget 达到它才进入 Micro
auto_compact_threshold0.95Micro 回收不足且达到它时叠加 Full
micro_compact_stale_steps3最近 3 轮不截断
micro_excluded_toolsAgent / AskUserQuestion / goal / TodoWrite这些工具的输入输出整体保留
micro_field_threshold_chars500单字段超过它才截断
micro_field_keep_head_chars / keep_tail350 / 100截断时保留的头尾字符数
max_consecutive_failures3连续失败超限后降级跳过

smart_compact_enabled 已标记废弃,运行时按关闭处理;当前实际策略就是 Micro + Full 两级,Smart 路径仅保留兼容分支和测试。

增量处理旧消息

每一轮 Compact 阶段都会重新执行 plan_micro,但传入 skip_existing_truncated=true,已经标过 truncated 且指令版本一致的消息直接跳过。因此,

  • 第 N 轮只处理第 N 轮之前尚未被处理的消息,是增量截断;
  • 已持久化的旧截断保持不变,新轮次不会把它们改回去;
  • stale_steps=3 把最近 3 轮挡在回收区外,截断只发生在讨论已稳定的历史里。

效果上,多轮 Compact 像一个只前不退的快照序列,每轮在旧截断的基础上向前推进,而不是重新洗牌。

从持久化指令重建视图

每个轮次进入 LLM 推理前,Reason 阶段按固定顺序选择可见消息的来源,

flowchart TD
    A[Reason 阶段取得 visible_messages] --> B{plan_from_persisted_directives}
    B -->|成功| C[render_llm_view 持久化指令]
    B -->|版本不匹配| D[plan_micro skip=false 重新规划]
    B -->|无任何指令| D
    D --> E{新计划有变更?}
    E -->|是| F[render_llm_view 重新规划视图]
    E -->|否| G[原始可见消息]
    C -->|渲染失败| G
    F -->|渲染失败| G

主路径是第一个分支,只要标记和指令还在,模型每轮看到的是同一个投影视图,不重新规划。指令重建时会做两类校验,

  • 指令的 policy_version 必须等于当前投影策略版本(PROJECTION_POLICY_VERSION,当前为 2),不匹配直接报 DIRECTIVE_VERSION_MISMATCH
  • 指令 entry 的 message_id 必须等于当前消息 id,错位即 fail-closed。

消息被标 truncated 却没有任何指令时,返回 CORRUPTED_PROJECTION 而不是猜测兜底。另外,指令里固化了下手时的 keep_head / keep_tail,所以之后配置再改,也不影响已持久化指令渲染出的样子。

跨会话装回标记

会话恢复时,先装入消息正文,再按阶段从存储读回 flags,用 message_id 批量装回 transcript。标记和指令与正文同一条标识对应,新会话直接走上文的主路径。

标记加载失败会记录诊断并继续运行——让整个会话打不开会扩大存储故障的影响。此时没有可用指令,Reason 阶段会落到重新规划分支。这里要说明白,重新规划是重建,不是还原。标记丢失时,模型视图不会和关闭前完全相同,缺失层的降级结果是可观察的差异,不是无损恢复。

完整压缩后的状态

Full Compact 是另一种状态切换,会进入后续轮次,全部旧消息标 excludedvisible_messages() 自动过滤;追加一条带 CONTINUATION_HINT 的摘要 Human 消息;随后按预算 re-inject 最近读取的文件与 Skill 定义。excluded 同样持久化,后续轮次不再处理这些历史。

失败语义也在多轮视角下成立,budget 高位时先应用 Micro 再叠加 Full;Full 失败时代码对外报 MicroAppliedThenFullFailed,但 Micro 的截断标记已经持久化——这一轮的压缩收益不会因为 Full 失败而丢掉,下一轮从同样的状态继续。max_consecutive_failures(默认 3)兜底防死循环,连续失败超限后跳过压缩,不阻塞主循环。

测试固定可见行为

  • trigger_test.rs 固定触发判定、回收不足升级、失败上限、shadow mode;
  • projection_test.rs 固定指令重建的空指令、版本不匹配、旧数据无指令直通、stale config 仍渲染等分支;
  • transcript_test.rs 固定投影指令的持久化 roundtrip;
  • Micro 集成测试固定标志写入后恢复一致、多轮后旧标记保留、首次运行不清理标记。

这些用例共同兜住一个承诺,标记和指令一旦写入,任一后续轮次或恢复后的新会话都能重建并渲染出同一视图。

限制与边界

  • 没有实测数据,截断实际节省的 token 百分比没有发布过数字,估计函数是字符数除以 4 的粗估;可引用的只有上表的配置默认值。
  • 这不是无损压缩,投影会移除 Image/Document 的 Base64 payload,Full 更是用摘要代替原文。
  • Provider 能力参与渲染,比如带签名的 reasoning 在部分 Provider 协议下必须整体保留;换 Provider 后,旧指令可能渲染出不同的视图。
  • 降级路径只保证可用,不保证一致,标记缺失或版本不匹配时的重新规划是近似重建,跨 Provider、跨配置修改时同样如此。

相关实现