第 07 章:上下文压缩(compaction)
每一轮对话我们都在全量重发历史,而模型的上下文窗口是有限的——长会话总有一天撞墙。这一章实现 pi 的解法:压缩(compaction)——把较旧的消息摘要成一段文字,保留最近的消息原样;摘要进入上下文,原始历史永远留在会话文件里。
pi 的压缩策略
pi 的实现(core/compaction/compaction.ts,docs/compaction.md)有几个关键决策,我们逐一对照:
pi 还处理了一些边角:单个 turn 就超预算的"split turn"(切成两段分别摘要再合并)、二次压缩从上次的保留边界继续。mini-pi 不做这些——思想一致即可,粒度差异见文末。
估算与触发
没有官方 tokenizer 时,字符数 / 4 是个够用的粗估(英文偏保守,中文偏乐观;触发判断不需要精确):
src/main.ts(新增)
压缩的实现
src/main.ts(新增)
注意三件事:
- 摘要 prompt 是写给"接手者"的交接信:目标、决定、动过的文件、跑过的命令、当前状态、下一步。这类"交接清单"比"总结一下上文"的摘要质量高得多——这也是 pi 的摘要 prompt 采用结构化格式的原因。
- 摘要本身不落盘为普通消息,而是
compaction记录;上一章的rebuildContext已经知道怎么用它:插入摘要消息、跳过被压缩的旧消息(skipUntilId逻辑,现在填坑完毕)。 - 压缩发生在每个 turn 开始前(
runAgent的 while 顶部调用maybeCompact),是"主动压缩";pi 除此之外还有"溢出后恢复"这条路。
验收
把阈值调小,人为制造长会话:
聊几轮(让它读几个文件),观察:
- 某一轮开始前打印
[compacted N older messages; …];之后问 "我们最开始要做什么?"——它能根据摘要答出来。 cat会话文件:compaction记录躺在 JSONL 里,被压缩的旧消息一条不少。压缩是有损的,但只损"给模型看的上下文",不损"事实记录"——这是 pi 会话树设计的另一半好处,配合/tree随时可以回到压缩前的完整历史。--continue恢复,输出N messages, M older compacted,接着聊——重建的上下文正是"摘要 + 保留消息"。
与 pi 的差距(以及去哪看)
- pi 按 token 预算往回找切点(保留约 20k),我们按固定条数;改成按
estimateTokens累积即可,逻辑同构。 - pi 有 split turn、二次压缩从上次保留边界继续、摘要带累计的文件操作清单(读/写/编辑过哪些文件);这些都写在 docs/compaction.md 里,建议通读。
- pi 的压缩可通过扩展完全替换(
custom-compaction示例),这正是"核心极简、扩展激进"的又一例,见设计思想:harness 层。
本章要点
- 上下文窗口是硬约束;压缩 = 摘要旧消息 + 保留最近消息,触发要留出回复余量。
- 切分必须遵守协议约束:toolResult 不能和它的 toolCall 分家。
- 摘要 prompt 写成"交接信";摘要落盘为特殊记录,原始历史永不删除。
- 至此 mini-pi 拥有了 pi 的全部核心能力:loop、四工具、流式、系统提示词、会话树、压缩——裸写部分完成。
下一章是里程碑:第 08 章:用 pi 的官方积木重构 mini-pi。