LLM 的 context window 有大小限制。对话、工具调用和工具输出不断累积以后,pi 不能继续把整段历史原样发送给模型,于是需要把旧内容压缩成摘要,同时保留最近的工作。
pi 的 compaction 可以先用三个问题理解。
- 什么时候触发
- 哪些内容被摘要,哪些内容被保留
- 摘要完成以后,Agent 怎样继续运行
本文参考 pi 的 Compaction 官方文档,对应实现位于 compaction.ts 和 agent-session.ts。
一次 compaction 的路径
compaction 不会删除旧的 session entry。pi 会追加一个 CompactionEntry,之后构建上下文时用它代表旧历史,再接上仍然保留的消息。
什么时候触发
自动 compaction 的判断是
contextTokens > contextWindow - reserveTokens默认值是
reserveTokens = 16384
keepRecentTokens = 20000reserveTokens 给模型响应预留空间。假设 context window 是 128k,当前上下文超过 128k 减去 16k,也就是 112k,pi 就会考虑压缩。keepRecentTokens 决定压缩后大致保留多少最近内容。
用户也可以用 /compact 手动触发。手动压缩会先中止当前 Agent 操作,然后生成摘要,不会自动恢复被中止的 turn。
配置位于 ~/.pi/agent/settings.json 或项目的 .pi/settings.json。
{
"compaction": {
"enabled": true,
"reserveTokens": 16384,
"keepRecentTokens": 20000
}
}pi 怎样估算上下文大小
如果最近一次 assistant response 带有有效的 usage,pi 会使用 provider 报告的 token 数,再估算这条响应之后新增的消息。
如果没有可用 usage,pi 会估算消息大小。文本大致按字符数除以 4,图片按固定字符预算计算,assistant 的 thinking、文本和 tool call 也会纳入估算。
这样即使 provider 返回错误,或者 usage 是 0,pi 仍然可以根据消息历史判断是否需要压缩,不会因为一次异常响应把上下文计数清零。
压缩哪些内容
pi 会在当前 session branch 上准备一份压缩计划。
prepareCompaction(pathEntries, settings)
→ 找到上一次 compaction 的保留边界
→ 估算当前 context tokens
→ 从最新消息向前寻找 cut point
→ 得到 messagesToSummarize
→ 得到 firstKeptEntryIdfirstKeptEntryId 是压缩后第一个保留 entry 的 ID。之后重建上下文时,pi 会从这个 entry 开始恢复消息。
正常情况下,pi 尽量在 turn 边界切开历史。一个 turn 从 user message 开始,包含后面的 assistant 响应和工具调用,直到下一个 user message。
有效切点可以落在 user、assistant、bashExecution 或 custom message 上。pi 不会在 tool result 上切断,因为 tool result 必须和对应的 tool call 保持完整关系。
原始历史
[旧消息 旧消息 旧消息] [最近消息 最近消息]
↓ 摘要 ↓ 保留
压缩后 context
[CompactionEntry 摘要] [最近消息 最近消息]旧消息仍然留在 session 文件中,下一次发给 LLM 的 context 使用摘要和保留消息。
Split turn
有时单个 turn 自己就超过了 keepRecentTokens。这时找不到完整的 turn 边界,只能在当前 turn 中间切开。
一个很大的 turn
user → assistant → tool → assistant → tool → assistant → tool
↑ ↑
早期部分 最近部分pi 会把当前 turn 的早期部分作为 turnPrefixMessages,并生成两部分摘要。
历史摘要
+ 当前 turn 前缀摘要
+ 当前 turn 最近消息这样最近保留的 assistant 和 tool 内容仍然有上下文,模型不会只看见后半段结果。
摘要怎样生成
pi 先用 serializeConversation() 把消息转成带标记的文本,再交给摘要模型。
[User]: 用户消息
[Assistant thinking]: 推理内容
[Assistant]: 回复文本
[Assistant tool calls]: read(path="src/index.ts")
[Tool result]: 工具输出摘要请求会要求模型只生成结构化摘要,不要继续回答原对话里的问题。默认摘要包含目标、约束、进度、关键决策、下一步和继续工作所需的上下文。
工具结果在序列化时最多保留 2000 个字符。read 和 bash 的输出可能很长,先截断再摘要可以控制 compaction 请求本身的大小。
pi 还会从被摘要消息里的 tool call 提取文件操作,把读过和修改过的文件写入摘要的 details。
read → readFiles
write → modifiedFiles
edit → modifiedFiles这些列表会在后续 compaction 中继续累积,让模型知道过去处理过哪些文件。
CompactionEntry 怎样重建 context
一个 compaction entry 主要包含
type: "compaction"
summary: string
firstKeptEntryId: string
tokensBefore: number
usage?: Usage
details?: unknownsession manager 构建当前上下文时,会找到最新的 compaction entry,然后生成
system prompt
+ compaction summary
+ firstKeptEntryId 开始的保留消息
+ compaction entry 之后的新消息被摘要的旧消息没有从 session 历史删除。它们只是不会再次进入默认的 LLM context。
Context overflow 怎样恢复
阈值 compaction 发生在请求前或一次正常响应之后。overflow recovery 则处理已经发生的请求失败或响应截断。
pi 会把失败或被截断的 assistant message 从 Agent 的实时状态中移除,再压缩并重试原 turn 一次。那条失败消息可能已经写入 session history,但不会进入这次 retry 的 context。
如果重试后又发生同类问题,pi 不会无限重试,会报告 recovery failed,让用户减少上下文或换用更大 context window 的模型。
如果响应已经正常完成,只是 context 超过配置范围,pi 会压缩历史,但不会重新执行刚刚完成的 assistant turn。
Compaction 失败
摘要生成、鉴权、模型调用或用户中止都可能让 compaction 失败。失败时不会写入半成品的 compaction entry,session 会保留当前历史并发出失败结果。
从状态角度看,compaction 是一次独立的 session 操作。
正常运行
→ 准备压缩
→ 生成摘要
→ 保存摘要 entry
→ 重建 context
→ 继续或 retry
任一步失败
→ 不保存半成品
→ 记录失败
→ 按触发原因决定后续动作Compaction 和 branch summarization
两者都会生成摘要,但解决的问题不同。
Compaction
对当前 branch 的旧消息做压缩
触发方式是 context 接近上限、overflow 或 /compact
Branch summarization
在 /tree 切换分支时保留被离开分支的上下文
触发方式是 session tree navigation本文只关注 context window compaction。branch summarization 是 session 分支切换时的另一种摘要流程。
读源码时抓住这条线
AgentSession._checkCompaction()
↓
shouldCompact() 或 context overflow 判断
↓
prepareCompaction()
↓
compact()
↓
SessionManager.appendCompaction()
↓
SessionManager.buildSessionContext()
↓
agent.state.messages 重建
↓
必要时 agent.continue()真正重要的是 firstKeptEntryId。它把摘要和原始 session tree 接起来,让 pi 可以在不删除历史的情况下,决定哪些内容继续发给模型。
小结
接近上限
→ 找到安全切点
→ 摘要旧消息
→ 保存 CompactionEntry
→ 用摘要和最近消息重建 context
→ 必要时重试被 overflow 打断的 turnpi 的 compaction 处理的重点是改变下一次 LLM 请求看到的消息集合。旧对话仍然保留在 session 历史里,Agent context 则通过 compaction entry 获得一个更小、还能继续工作的版本。