小节四:把 Compaction 接入 ContextBuilder——什么时候触发、怎么重建、怎么验证没压坏

前面我们分别讲了“压什么”和“怎么压”,到这一节就要真正接进 ContextBuilder 里了。下面这张图把触发条件、ContextBuilder 内部的拼装块,以及最终送给 LLM 的 Context 全部放在了一起,看完之后你会对接入位置和运行时机更有感觉。

图片[1]-小节四:把 Compaction 接入 ContextBuilder——什么时候触发、怎么重建、怎么验证没压坏-大赛人网

图里有一个点特别值得记住:不是每一轮都去做 Summary,而是在真的需要时才重建 Context。 也就是说,Compaction 本质上是一个按需触发的上下文重构动作,而不是每轮对话都必须执行的固定流程。

现在我们把前几节真正串起来。

当前有:

ContextUsage
ContextBuilder
WorkingMemory
Session
compact_context()

一个成熟流程可以变成:

Session
 ↓
ContextBuilder.build()
 ↓
估算 Input Token
 ↓
ContextUsage
 ↓
如果 >= 阈值
 ↓
compact_context()
 ↓
更新 historical_summary
 ↓
保留 recent_messages
 ↓
ContextBuilder 重新 build()

什么时候触发?

有三种常见策略。

第一种:

按 Token 使用率

例如:

>= 80%
触发

第二种:

按消息数量

例如:

messages > 40

第三种:

按 Tool Result 体量

例如:

累计 Tool Result > 50000 chars

真实系统通常组合使用。

为什么不要每一轮都 Summary?

因为 Compaction 自己也有成本。

如果每轮都调用一个 Summary LLM:

成本增加
延迟增加
摘要反复重写

所以更适合,达到阈值时再压缩。

一个完整最小 Compaction Manager

下面做一个完全可运行版本。

这段代码实现了:

超过阈值
 ↓
把旧消息摘要
 ↓
保留最近 6 条
 ↓
Session 中只留 compacted summary + recent

为什么 Summary 自己也要限制长度?

因为如果每次 Compaction 都:

旧 Summary + 新 Summary

无限拼接,Summary 最终也会爆。

所以 Summary 本身也需要再次压缩。

真实方案可以:

旧 Summary
+
新旧历史
 ↓
重新总结成新的统一 Summary

而不是简单字符串拼接。

课程这里用:

只保留合并摘要的最后 6000 个字符

只是演示预算概念。

Compaction 后怎么验证没有“压坏”?

压完之后,不是“看起来短了”就算结束,更关键的是确认它还能不能支持后续任务继续推进。下面这张图左边是常见的验证清单,右边则是 Build Report 建议记录的关键字段,两部分放在一起看,会非常实用。

图片[2]-小节四:把 Compaction 接入 ContextBuilder——什么时候触发、怎么重建、怎么验证没压坏-大赛人网

你会发现,真正需要检查的并不复杂:关键约束还在不在、Working Memory 有没有丢、最近几轮原文是否完整、关键事实还能不能回答、必要的 Tool Result 结论是否仍可引用。与此同时,把这些信息记录进 Build Report,后面排查“到底是压坏了,还是模型自己推错了”时会轻松很多。

这是非常重要的一步。

至少检查:

Goal 是否还在
硬约束是否还在
当前 unresolved errors 是否还在
重要文件是否还在
Current Task 是否还在
最近 Tool Result 是否还在

最好把这些放在:

Working Memory
Project Rules
Current Task

而不是只依赖 Summary。

因为 Summary 有信息损失。

压缩率高不代表压缩质量高。我们可以准备一组 Compaction 回归样例,逐项检查:硬约束召回率、未完成动作是否保留、关键数字和实体是否一致、事实与假设有没有串位、Tool 协议是否完整,以及压缩后的 Agent 能否继续完成同一任务。Token 从 80K 降到 8K 很漂亮,但如果“禁止修改 tests/”也一起消失,这次压缩仍然是不合格的。

数值、文件路径、错误码这类信息尤其适合做确定性校验。摘要生成后可以从原记录和摘要中抽取这些关键字段进行比对;发现冲突时宁可保留更多原文,也不要让一份看起来流畅的摘要覆盖证据。

Build Report 还应该记录什么?

可以增加:

compaction_triggered
before_tokens
after_tokens
compressed_messages
summary_size

以后 Tracing 可以看到:

Before: 82K
After: 31K
Saved: 51K

这就非常有价值。

压缩之前,先列出不能丢失的东西

我们先从一段真实轨迹挑出三类材料:仍然生效的约束、已验证且影响下一步的发现、尚未完成的动作。再把重复日志和已结束的探索过程压缩。最近消息并不天然更重要,最早的用户限制可能仍控制整个任务;总结应保留它的原始来源。

工具消息也需要成组处理。模型发出两次调用后,结果与调用标识存在关联;仅按最后六条消息切片,可能保留结果却丢掉请求。压缩器应在完整交互边界切分,或按接口支持的机制处理历史,避免生成协议不合法的下一轮请求。

压缩最好按“生成候选摘要 → 校验 → 提交替换”三步完成,而不是先删除旧消息再尝试总结。校验失败、模型超时或摘要反而更长时,我们仍能继续使用旧 Context,或者退回确定性的裁剪策略。原始轨迹、摘要版本和覆盖的消息范围都写进 Trace,后面才能知道一条结论是从哪些记录压出来的。

配套 compact_messages() 已经从简单截断升级为按完整交互单元处理:System Rules 和 Working Memory 独立保留;Assistant Tool Call 与对应 Tool Result 要么一起留下,要么一起进入历史摘要;摘要作为普通上下文数据,不会被提升成 System 指令。如果工具结果失去对应的 Call ID,压缩器会明确报错,而不是构造一份协议不合法的请求。

压缩器还会比较重建前后的估算 Token。如果加上摘要标签以后反而更长,就直接返回原 Context,并把 compacted_count 保持为 0。短历史本来就没有必要硬压;“调用了 compact”不代表一定要改写状态。

它仍然是一套确定性教学摘要器,不会真正理解“哪个业务结论最重要”。生产版可以把旧交互单元交给专门的 Summarizer,但仍要由 Harness 控制输入边界、输出 Schema 和验证规则。尤其要防止摘要模型把外部日志里的指令当成自己的任务,或把“可能是依赖问题”改写成“已确认依赖问题”。

在配套项目里运行 Compaction

相关实现位于:

src/agent_harness/context/compaction.py
src/agent_harness/context/builder.py
projects/05_context_memory_session/main.py

进入 agent_harness_course_projects 后运行:

python3 --version
python3 projects/05_context_memory_session/main.py

示例先输出 Context Usage,再把较早的历史压成 COMPACTED HISTORY,同时保留 System、Working Memory 和最近交互。课程代码统一按 Python 3.9 编写。这里的 keep_recent 计算的是完整交互单元,不是裸消息条数;一次 Assistant Tool Call 加上多个 Tool Result 属于同一个单元,不会从中间切断。

动手做一份压缩前后对照

给历史加入“禁止修改 tests”、一次失败查询和一个尚未完成的工具请求,然后压缩。参考验收是:禁止项仍可见,失败原因没有被改成成功,工具请求与结果关系完整,原记录仍能找回。只比较 Token 是否减少,还不足以判断压缩质量。

如果“尚未完成”意味着 Tool Call 还没有对应结果,此时不应该为了凑出下一轮请求而伪造结果。更稳妥的做法是先等待工具完成或恢复执行检查点;只有准备发送给模型的协议消息组完整后,才进入下一次模型调用。

© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享
评论 抢沙发
头像-大赛人网
欢迎您留下宝贵的见解!
提交
头像-大赛人网

昵称

取消
昵称

    请登录后查看评论内容