先说结论
Claude Messages API 的按需 compaction 不是“让模型写一段摘要,然后把旧消息删掉”。API 会返回一个签名的 compaction block;后续请求应把这个 block 放在消息历史最前面,用它替代已经被压缩的旧消息。
如果应用只保存 block 里的可读文本、修改 block、把它放错位置,或者在并发分支之间混用,恢复结果就不再可靠。
这项能力解决的是长任务 Agent 的上下文增长问题,但不会自动解决业务状态恢复、工具幂等、并发写入和审计。
它与普通摘要有什么不同
普通摘要通常由应用自己生成和保存:
任务目标:修复登录回调。
已修改:auth.ts。
剩余:补充刷新令牌测试。这种文本可以人工编辑,也可以换模型重写。signed compaction block 则是 API 返回的结构化历史状态。应用应把它视为不可修改的模型输入工件。
两者可以同时存在:
- compaction block 用于模型继续推理。
- 业务 checkpoint 用于人和程序恢复任务。
不要只保留前者。若工具已经修改文件、创建 issue 或写入数据库,模型摘要并不能证明这些副作用真实完成。
最小接入流程
按需压缩目前使用 compact-2026-09-04 beta header,并在请求顶层传入 compaction 参数。实际字段应以当前官方文档和 SDK 类型为准,不要把测试阶段的请求形状长期硬编码在多个服务里。
推荐流程:
- 正常发送完整消息历史。
- 达到应用设定的阈值后请求 compaction。
- 从响应中取出完整的 signed compaction block。
- 原子保存 block、模型 ID、会话版本和保留的近期消息。
- 后续请求把 block 放在最前,后接未被压缩的近期消息。
伪代码如下:
type AgentCheckpoint = {
revision: number
model: string
compactionBlock: unknown
recentMessages: Message[]
businessState: {
completedToolCalls: string[]
changedFiles: string[]
pendingChecks: string[]
}
}
async function resume(checkpoint: AgentCheckpoint) {
return client.messages.create({
model: checkpoint.model,
messages: [
checkpoint.compactionBlock,
...checkpoint.recentMessages
]
})
}这里的 unknown 是有意的:应用不应假设自己可以拆开、编辑或重新序列化 block 的内部语义。使用官方 SDK 类型,并完整持久化 API 返回结构。
什么时候触发压缩
不要等到请求已经超过上下文窗口才压缩。触发条件至少要考虑:
- 当前输入 token。
- 下一步工具可能返回的最大结果。
- 模型需要的输出预算。
- 重试和恢复需要保留的安全余量。
一个可操作的策略是把阈值分成三档:
| 阶段 | 动作 |
|---|---|
| 低水位 | 正常运行,记录 token 和缓存命中 |
| 中水位 | 裁剪可重取的工具结果,生成业务 checkpoint |
| 高水位 | 请求 compaction,停止启动新的大结果工具 |
阈值必须由压测决定。代码仓库大小、工具结果长度和任务类型不同,固定的“对话轮数”没有意义。
哪些近期消息应该原样保留
官方能力允许在 compaction block 后继续保留最近若干轮原文。优先保留:
- 尚未完成的用户要求。
- 最近一次工具调用及对应 tool result。
- 当前失败的测试输出。
- 等待人工批准的动作。
- 与下一步直接相关的文件片段。
可以优先压缩:
- 已经完成且有独立验证记录的探索过程。
- 能从仓库或 API 再次读取的大段内容。
- 重复的日志和搜索结果。
- 已被后续结论取代的方案讨论。
“近期”不应简单等于最后 N 条消息。一次工具调用和它的结果必须作为完整语义单元保留,不能从中间切开。
并发 Agent 最容易出错的地方
如果多个 worker 同时处理同一会话,可能出现两个 compaction block 都基于 revision 12 生成,随后互相覆盖。恢复时模型只看到其中一个分支,另一个分支完成的工具调用却已经产生副作用。
每个 checkpoint 至少要带:
conversation_idrevisionparent_revisionmodel- block 的完整值
- 保留消息的范围
- 已完成工具调用的幂等键
写入时使用乐观锁:
UPDATE agent_checkpoint
SET revision = 13, payload = :payload
WHERE conversation_id = :id AND revision = 12;受影响行数为 0 时说明状态已经被别的 worker 更新。此时应重新读取并合并业务状态,不能直接覆盖。
压缩后缓存为什么可能变化
压缩会改变请求前缀。即使语义相近,prompt cache 也可能重新建立,因此压缩后的第一个请求可能出现更高延迟或缓存写入费用。
验收时分别记录:
- 压缩请求本身的输入、输出和耗时。
- 压缩后第一次恢复请求的 cache read / cache write。
- 后续相同前缀请求的缓存命中。
- 未压缩基线的总成本。
不要只比较单次请求 token。真正要比较的是一个完整任务从开始到验收的总成本和成功率。
与 Claude Code 恢复旧会话不是同一个问题
Claude Code 恢复旧会话时重建 prompt cache,属于客户端产品如何重放历史和建立缓存。Messages API compaction 是自建 Agent 主动决定何时压缩、保存什么以及如何恢复。
两者都会表现为 token 和缓存变化,但控制面不同:
- Claude Code 用户主要检查版本、会话恢复行为和缓存账单。
- API 开发者必须设计 checkpoint、版本控制、并发冲突和工具幂等。
因此不能用“Claude Code 能恢复”证明自建 Agent 的 compaction 实现正确。
最小复现应该怎么做
准备一个不会产生真实外部副作用的测试工具:
{
"name": "record_step",
"input_schema": {
"type": "object",
"properties": {
"step_id": { "type": "string" },
"result": { "type": "string" }
},
"required": ["step_id", "result"]
}
}让 Agent 完成 20 至 30 个带唯一 step_id 的步骤,然后比较三条路径:
- 完整历史直接继续。
- 自己生成一段普通文本摘要后继续。
- 使用官方 signed compaction block 后继续。
检查它是否能准确回答:
- 哪些步骤已经完成。
- 最后一次失败发生在哪里。
- 下一步应该执行什么。
- 是否重复调用已经完成的
step_id。
重复执行测试至少三次,并保存 request ID、token、停止原因和工具调用序列。一次成功不代表恢复稳定。
故障时怎么回退
compaction 恢复失败时,不要无限重试同一个 block。推荐回退顺序:
- 停止执行有副作用的工具。
- 保存失败 block、请求 ID、模型和 revision。
- 从最后一个业务 checkpoint 新建会话。
- 重新读取仓库、数据库或任务系统中的真实状态。
- 用明确的待办清单继续,而不是要求模型猜测丢失历史。
如果切换模型,还要验证目标模型是否接受当前 block。没有明确兼容保证时,从业务 checkpoint 建立新会话更可靠。
上线验收清单
- [ ] 完整保存 API 返回的 signed compaction block,没有只取摘要文本。
- [ ] block 总是位于恢复历史最前面。
- [ ] 被压缩消息不会再次重复发送。
- [ ] 工具调用和 tool result 不会被拆开保留。
- [ ] checkpoint 带 revision,并发写入使用乐观锁。
- [ ] 有副作用的工具使用幂等键。
- [ ] 业务状态独立于模型 block 保存。
- [ ] 记录压缩前后 token、缓存、延迟和总任务成本。
- [ ] block 无法恢复时能从业务 checkpoint 新建会话。
- [ ] 日志不包含 API Key、完整私有代码或未脱敏工具结果。
常见误区
误区一:压缩后能继续回答,就说明状态完整
模型能生成连贯文本,不代表它记得所有已执行副作用。必须核对工具调用记录和业务系统状态。
误区二:把上下文窗口用满再压缩最省钱
接近上限时,大型 tool result 或一次重试就可能让请求失败。提前预留输出和工具结果空间更稳。
误区三:所有 Agent 共用同一个最新 block
并发分支的父 revision 不同。混用 block 会让模型历史与真实副作用脱节。
什么才算接入完成
接入完成不是“接口返回了 compaction block”,而是压缩、持久化、恢复、并发冲突和失败回退都能重复验证。模型上下文只是一层状态;文件、数据库、测试结果和审批记录仍必须由应用自己管理。