KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

07. Compaction 何时触发,重启后怎样恢复? — keel 龙骨

支付事故的上下文从 60% 增长到 81%,系统开始压缩。摘要已经生成,worker 却在保存新消息前崩溃。队列随后重投同一个任务:它应该再次摘要旧历史,还是识别出上一轮已经完成?

支付事故的上下文从 60% 增长到 81%,系统开始压缩。摘要已经生成,worker 却在保存新消息前崩溃。队列随后重投同一个任务:它应该再次摘要旧历史,还是识别出上一轮已经完成?

只实现 if tokens > threshold: summarize() 还不够。生产 Compaction 是一次会改变运行状态的操作,需要触发策略、版本、事件、checkpoint、幂等和恢复顺序。否则重启后可能生成两个不同摘要、丢失最新工具结果,或者把同一历史重复压缩。

1. 三类触发信号

容量触发

当 measured tokens 超过 input budget 的某个阈值时执行。它容易实现,但只看当前占用,无法预知下一次大工具调用。

事件触发

读取大文件、完成长工具回合、阶段结束或即将切换模型时主动整理。它更接近业务状态,但需要 Harness 暴露明确事件。

周期触发

每 N 轮或每个阶段检查一次,适合增长稳定的流程。单独使用可能在两次检查之间突然超限。

实际系统可以组合:每轮做轻量计数;大结果后立即检查;阶段边界允许更彻底的结构化归档。

2. 触发不是阈值口诀

本课程示例使用:

input_budget = 70
measured = 133
if measured > budget: compact

生产中通常会在真正超限前触发,并压到更低目标形成迟滞。但触发值必须结合:

任务马上完成时,额外摘要可能纯属浪费;即将读取 2 MB 日志时,当前只有 60% 也可能需要先整理。

3. 把一次触发写成事件

运行:

python courses/foundation/context-engineering/course/project/examples/05_trigger_and_events.py

输出:

1 context.measured {'tokens': 133, 'budget': 70}
2 context.compacted {'strategy': 'summary', 'before': 133,
                     'after': 58, 'sources': ['m0', 'm1', 'm2']}

第一条事件是测量事实,第二条是状态变换结果。完整生产事件还应包含:

{
  "event_type": "context.compacted",
  "run_id": "run-42",
  "sequence": 18,
  "history_version_before": 6,
  "history_version_after": 7,
  "trigger": "above_policy_threshold",
  "strategy": "structured-summary@v3",
  "counter": "provider/model@tokenizer-version",
  "before_tokens": 6120,
  "after_tokens": 3810,
  "source_message_ids": ["m11", "m12", "tool:c1"],
  "summary_id": "summary:run-42:v7"
}

事件要说明“为什么做、对什么做、用哪个版本、得到什么”,而不是只有 compacted=true。

4. 正确的 checkpoint 顺序

一次可恢复变换可以按以下顺序执行:

读取 checkpoint(version=6)
  -> 计数并决定 strategy
  -> 生成候选摘要
  -> 校验预算、来源和工具轨迹
  -> 事务保存 checkpoint(version=7)
  -> 追加 context.compacted 事件
  -> 继续下一轮模型调用

如果事件存储与 checkpoint 不在同一事务域,需要 outbox、事件幂等键或 durable workflow 保证不会出现“状态已换但事件没写”“事件已发但状态没保存”的裂缝。

关键原则是:模型调用发生在新 checkpoint 可恢复之后。否则 worker 在调用后崩溃,系统不知道应该重放模型请求还是从新历史继续。

5. 重复投递为什么必须幂等

项目的内存 CheckpointStore 使用 (run_id, version) 保护版本:

if previous and checkpoint.version < previous.version:
    raise ValueError("checkpoint version must be monotonic")

if previous and checkpoint.version == previous.version:
    if previous != checkpoint:
        raise ValueError("same checkpoint version has different content")
    return previous

同版本、同内容重复保存直接返回;同版本、不同内容则报冲突;旧版本不能覆盖新版本。这三个分支阻止重投任务悄悄改写已经发布的摘要。

运行恢复示例:

python courses/foundation/context-engineering/course/project/examples/06_recover_after_compaction.py

输出:

run-42 3 paused_after_compaction
['[compaction summary] evidence=INC-42', '继续诊断']

示例连续保存同一 checkpoint 两次,恢复结果仍然只有版本 3。它证明幂等语义,不代表内存字典可以用于多 worker 生产环境。

6. 恢复时不能只读摘要文本

恢复组件还需要检查:

恢复不是把一串 messages 重新传给模型,而是重建一个合法运行状态,再决定下一步。

7. 三个崩溃窗口

摘要生成前崩溃

没有状态改变,安全重试;仍要遵守 run deadline。

摘要生成后、checkpoint 保存前崩溃

摘要是临时结果。重试可以重新生成,但如果摘要模型不确定性很高,最好用稳定输入 hash、策略版本和幂等操作记录,避免产生无法解释的差异。

checkpoint 保存后、下一轮调用前崩溃

恢复版本 7 继续,不应再压缩版本 6。事件重发必须使用稳定 event ID。

8. 失败注入

  1. 保存 version 3 后再保存 version 2,确认旧版本被拒绝;
  2. 对 version 3 使用不同消息保存,确认冲突;
  3. 在“摘要完成”和“checkpoint 保存”之间抛异常,确认旧 checkpoint 仍完整;
  4. 模拟取消后收到迟到模型结果,确认它不能把运行改回 running;
  5. 对同一触发事件重放两次,确认只产生一个可见版本。

运行测试:

python -m unittest courses.foundation.context-engineering.course.project.tests.test_checkpoint -v

9. 生产替换

内存 Store 要替换为事务数据库、事件存储或 durable workflow;多 worker 需要租约或乐观并发控制;摘要和 checkpoint 应有数据分类、加密、保留期与删除流程;失败和冲突要进入指标与告警,而不是只写 debug 日志。

10. 本章验收

你应能从任意 context.compacted 事件恢复出:使用了哪个输入版本、哪些消息成为来源、结果发布成哪个版本、重复投递为什么不会再生成一个不同结果,以及 worker 在三个崩溃窗口分别如何继续。

下一章:怎样证明压缩策略真的更好?

进入 keel 阅读