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
生产中通常会在真正超限前触发,并压到更低目标形成迟滞。但触发值必须结合:
- 下一轮工具结果的 P95 体积;
- 本轮输出预留;
- 摘要调用延迟和费用;
- 任务质量随压缩频率的变化;
- provider 是否提供服务端 compaction;
- 当前运行是否即将结束。
任务马上完成时,额外摘要可能纯属浪费;即将读取 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. 恢复时不能只读摘要文本
恢复组件还需要检查:
- checkpoint 是否属于当前 run 和 tenant;
- history version 与下一事件序号是否连续;
- summary 使用的策略和模型版本是否仍可接受;
- source IDs 是否仍可回查或已经按保留策略删除;
- 未决工具调用、审批和 wake-up 是否完整;
- 运行是否已 cancelled、failed 或 completed;
- 迟到结果是否仍能改变当前版本。
恢复不是把一串 messages 重新传给模型,而是重建一个合法运行状态,再决定下一步。
7. 三个崩溃窗口
摘要生成前崩溃
没有状态改变,安全重试;仍要遵守 run deadline。
摘要生成后、checkpoint 保存前崩溃
摘要是临时结果。重试可以重新生成,但如果摘要模型不确定性很高,最好用稳定输入 hash、策略版本和幂等操作记录,避免产生无法解释的差异。
checkpoint 保存后、下一轮调用前崩溃
恢复版本 7 继续,不应再压缩版本 6。事件重发必须使用稳定 event ID。
8. 失败注入
- 保存 version 3 后再保存 version 2,确认旧版本被拒绝;
- 对 version 3 使用不同消息保存,确认冲突;
- 在“摘要完成”和“checkpoint 保存”之间抛异常,确认旧 checkpoint 仍完整;
- 模拟取消后收到迟到模型结果,确认它不能把运行改回 running;
- 对同一触发事件重放两次,确认只产生一个可见版本。
运行测试:
python -m unittest courses.foundation.context-engineering.course.project.tests.test_checkpoint -v
9. 生产替换
内存 Store 要替换为事务数据库、事件存储或 durable workflow;多 worker 需要租约或乐观并发控制;摘要和 checkpoint 应有数据分类、加密、保留期与删除流程;失败和冲突要进入指标与告警,而不是只写 debug 日志。
10. 本章验收
你应能从任意 context.compacted 事件恢复出:使用了哪个输入版本、哪些消息成为来源、结果发布成哪个版本、重复投递为什么不会再生成一个不同结果,以及 worker 在三个崩溃窗口分别如何继续。