Pi 如何压缩一段过长的会话

和编码 Agent 连续工作几个小时后,对话、工具调用和命令输出会不断占用上下文。 模型的上下文窗口有上限,全部原样塞进去,迟早会超过限制。

Pi 的处理方式叫 Compaction,可以理解为把较早的会话整理成一份结构化摘要,同时保留最近一段原始消息。 完整记录仍然留在会话文件里,变化的是下一次发给模型的内容。

Compaction 不是简单删除旧消息。 它用摘要替换较早的上下文,并用一个明确的边界把摘要和近期原文接起来。

什么时候会触发 Compaction

Pi 会比较当前上下文和模型的上下文窗口:

contextTokens > contextWindow - reserveTokens

reserveTokens 默认是 16,384,可在全局或项目配置里调整。 预留这部分空间,是为了给模型接下来的回复和工具调用留出余量。

目前有三类常见触发方式:

  1. 自动检查发现上下文超过阈值。
  2. 用户手动执行 /compact [instructions]。
  3. 模型返回上下文溢出,或者输出因为长度限制提前结束时,Pi 尝试压缩并重试一次。

自动检查不只发生在一轮对话完全结束以后。 长任务里,工具执行完成、结果写入会话后,Pi 会在发起下一次模型请求前检查预计上下文大小。 这样可以在工具输出把上下文推过阈值时及时处理。

一次 Compaction 做了什么

整个过程可以拆成五步:

  1. 找到切割点。 Pi 从最新消息向前回溯,累计 token 估算值,直到最近保留的内容达到 keepRecentTokens,默认值是 20,000。
  2. 取出旧消息。 从上一个保留边界开始,到这次切割点之前的消息会进入待总结区间。
  3. 生成摘要。 Pi 调用模型生成结构化摘要。如果之前已经压缩过,上一次摘要也会参与本次总结。
  4. 写入压缩条目。 摘要和 firstKeptEntryId 会保存为新的 CompactionEntry。
  5. 重建模型上下文。 下一次请求只发送系统提示、摘要,以及 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
  }
}
参数默认值作用
enabledtrue是否启用自动压缩
reserveTokens16384给下一次模型输出预留的空间
keepRecentTokens20000压缩后继续保留的近期原始消息大小

关闭自动压缩后,/compact 仍然可以手动使用。 Pi 也支持按模型覆盖 token 配置,适合上下文窗口差异很大的模型。

摘要能延长会话,但不是无损存档

Compaction 让长任务可以跨过一次上下文窗口继续工作,但摘要一定会舍弃细节。 文件路径、已经完成的工作、关键决定和下一步动作通常会留下,某次命令的完整输出或一段临时讨论则可能被压缩掉。

需要长期保留的约束,最好写进项目文档、任务清单或代码里。 会话摘要适合维持工作连续性,不适合替代版本控制和正式文档。

参考资料