Pi 如何压缩一段过长的会话
和编码 Agent 连续工作几个小时后,对话、工具调用和命令输出会不断占用上下文。 模型的上下文窗口有上限,全部原样塞进去,迟早会超过限制。
Pi 的处理方式叫 Compaction,可以理解为把较早的会话整理成一份结构化摘要,同时保留最近一段原始消息。 完整记录仍然留在会话文件里,变化的是下一次发给模型的内容。
Compaction 不是简单删除旧消息。 它用摘要替换较早的上下文,并用一个明确的边界把摘要和近期原文接起来。
什么时候会触发 Compaction
Pi 会比较当前上下文和模型的上下文窗口:
contextTokens > contextWindow - reserveTokens
reserveTokens 默认是 16,384,可在全局或项目配置里调整。
预留这部分空间,是为了给模型接下来的回复和工具调用留出余量。
目前有三类常见触发方式:
- 自动检查发现上下文超过阈值。
- 用户手动执行
/compact [instructions]。 - 模型返回上下文溢出,或者输出因为长度限制提前结束时,Pi 尝试压缩并重试一次。
自动检查不只发生在一轮对话完全结束以后。 长任务里,工具执行完成、结果写入会话后,Pi 会在发起下一次模型请求前检查预计上下文大小。 这样可以在工具输出把上下文推过阈值时及时处理。
一次 Compaction 做了什么
整个过程可以拆成五步:
- 找到切割点。 Pi 从最新消息向前回溯,累计 token 估算值,直到最近保留的内容达到
keepRecentTokens,默认值是 20,000。 - 取出旧消息。 从上一个保留边界开始,到这次切割点之前的消息会进入待总结区间。
- 生成摘要。 Pi 调用模型生成结构化摘要。如果之前已经压缩过,上一次摘要也会参与本次总结。
- 写入压缩条目。 摘要和
firstKeptEntryId会保存为新的CompactionEntry。 - 重建模型上下文。 下一次请求只发送系统提示、摘要,以及
firstKeptEntryId之后保留的原始消息。
压缩前,消息大致是这样排列的:
Before compaction:
entry: 0 1 2 3 4 5 6 7 8 9
┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┐
│ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│
└─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘
└────────┬───────┘ └──────────────┬──────────────┘
messagesToSummarize kept messages
↑
firstKeptEntryId (entry 4)
压缩完成后,原始条目没有消失,末尾只是多了一个 cmp 条目。
After compaction:
entry: 0 1 2 3 4 5 6 7 8 9 10
┌─────┬─────┬─────┬──────┬─────┬─────┬──────┬──────┬─────┬─────┬─────┐
│ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │
└─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘
└──────────┬──────┘ └──────────────────────┬───────────────────┘
not sent to LLM sent to LLM
↑
starts from firstKeptEntryId
模型下一次看到的是重建后的版本:
┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐
│ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │
└────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘
↑ ↑ └─────────────────┬────────────────┘
prompt from cmp messages from firstKeptEntryId
这套结构保留了两种信息:摘要负责记录较早的目标、进度和决定,近期原文则保留仍在进行中的细节。 工具调用和工具结果也不会被随意拆开,切割点不能落在一条 tool result 上。
多次压缩如何衔接
长任务可能经历不止一次 Compaction。
第二次压缩时,待总结区间会从上一次的 firstKeptEntryId 开始,而不是从上一个 CompactionEntry 开始。
这点看起来有些绕,但原因很直接:上次被保留的近期消息,到了下一次压缩时可能已经变成旧消息,它们也应该被纳入新的摘要。 如果当前分支已经找不到原来的保留边界,Pi 才会退回到上一个压缩条目之后继续处理。
写入新的压缩条目前,Pi 还会基于重建后的上下文重新计算 tokensBefore。
这个值代表实际被摘要替换的上下文大小,而不是会话文件里所有历史条目的总和。
一轮对话太长时怎么办
一轮对话从用户消息开始,包含下一条用户消息出现前的所有助手回复和工具调用。 正常情况下,Pi 会尽量在轮次边界切开,这样一段任务的输入和执行过程可以完整保留。
单个轮次也可能超过 keepRecentTokens。
比如 Agent 连续读取大量文件、运行测试并修复问题,期间没有新的用户消息,切割点就只能落在这个轮次中间。
Pi 把这种情况称为 split turn。
Split turn:
entry: 0 1 2 3 4 5 6 7 8
┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐
│ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │
└─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘
↑ ↑
turnStartIndex = 1 firstKeptEntryId = 7
│ │
└──── turnPrefixMessages (1-6) ───────┘
└── kept (7-8)
split turn 会生成两部分摘要:
- 历史摘要,记录这轮对话之前的重要上下文。
- 轮次前缀摘要,记录这轮对话中已经被切走的前半段。
两部分最后合并到同一个结构化摘要里。 切割点可以落在用户消息、助手消息、Pi 自己的 BashExecution 消息,以及扩展注入的自定义消息上,但不会落在工具结果上。
可以调整哪些参数
Pi 的全局配置位于 ~/.pi/agent/settings.json,项目也可以通过 .pi/settings.json 覆盖它。
{
"compaction": {
"enabled": true,
"reserveTokens": 16384,
"keepRecentTokens": 20000
}
}
| 参数 | 默认值 | 作用 |
|---|---|---|
enabled | true | 是否启用自动压缩 |
reserveTokens | 16384 | 给下一次模型输出预留的空间 |
keepRecentTokens | 20000 | 压缩后继续保留的近期原始消息大小 |
关闭自动压缩后,/compact 仍然可以手动使用。
Pi 也支持按模型覆盖 token 配置,适合上下文窗口差异很大的模型。
摘要能延长会话,但不是无损存档
Compaction 让长任务可以跨过一次上下文窗口继续工作,但摘要一定会舍弃细节。 文件路径、已经完成的工作、关键决定和下一步动作通常会留下,某次命令的完整输出或一段临时讨论则可能被压缩掉。
需要长期保留的约束,最好写进项目文档、任务清单或代码里。 会话摘要适合维持工作连续性,不适合替代版本控制和正式文档。