上下文裁剪与压缩

Pi 的上下文策略不是粗暴截断,而是结合 token 估算、合法 cut point、turn 边界、split-turn 摘要、文件操作追踪和 session reload 的完整机制。

本章涉及的 token 估算、cut point、split turn、compaction 和 branch summary 行号见源码索引

关键源码:

  • packages/coding-agent/src/core/compaction/compaction.ts
  • packages/coding-agent/src/core/compaction/branch-summarization.ts
  • packages/coding-agent/src/core/compaction/utils.ts
  • packages/coding-agent/docs/compaction.md

触发条件

自动压缩使用:

contextTokens > contextWindow - reserveTokens

默认设置在 DEFAULT_COMPACTION_SETTINGS

参数 默认值 含义
enabled true 是否启用自动压缩
reserveTokens 16384 给下一次模型输出预留空间
keepRecentTokens 20000 压缩后尽量保留的近期上下文

AgentSession 在 prompt 前和 agent run 后都会检查 compaction,因此既能预防超窗,也能在 overflow 错误后恢复。

Token 估算

Pi 优先使用最近 assistant message 的真实 usage:

  1. 找最后一条非 error/aborted assistant usage。
  2. calculateContextTokens() 读取 totalTokens 或各项相加。
  3. 对 usage 之后的新消息用 estimateTokens() 估算。

如果完全没有 usage,则所有消息都用字符数除以 4 的保守估算。图片按固定字符量估算,避免视觉输入被当成零成本。

Cut Point 规则

findCutPoint() 从最新消息往前累计 token,直到达到 keepRecentTokens。但它只允许切在合法位置:

  • user message
  • assistant message
  • bashExecution message
  • custom message
  • branch_summary / custom_message entry

它绝不切在 tool result 上,因为 tool result 必须跟着对应 tool call。如果切在带 tool call 的 assistant message,后续 tool result 会被保留。

Split Turn

如果单个 turn 本身超过 keepRecentTokens,切点可能落在这个 turn 中间。Pi 会识别 split-turn:

  • turnStartIndex: 当前超大 turn 的起始 user/bashExecution。
  • firstKeptEntryIndex: 从哪里开始保留 suffix。
  • turnPrefixMessages: 被切掉的 turn 前缀。

压缩时会并行生成两个摘要:

  1. history summary:老历史摘要。
  2. turn prefix summary:超大 turn 前缀摘要。

最终合并为一个 compaction summary。这样保留的 suffix 仍然能理解本 turn 开头用户到底要求了什么。

迭代式摘要

如果之前已经有 compaction,新的 prepareCompaction() 会:

  • 找最后一个 compaction entry。
  • 取它的 summary 作为 previousSummary
  • firstKeptEntryId 找到上一次保留下来的边界。
  • 新摘要用 update prompt 合并旧摘要和新消息。

这避免多次压缩后摘要只覆盖最近一段,而丢失更早目标、约束和关键决策。

文件操作追踪

压缩摘要不只写自然语言,还会追加:

<read-files>
...
</read-files>

<modified-files>
...
</modified-files>

文件列表来自:

  • 被压缩消息里的 read/edit/write/bash 等工具调用。
  • 前一次 compaction details。
  • split-turn 前缀消息。

这是 coding agent 很重要的上下文:继续工作时,模型需要知道读过哪些文件、改过哪些文件。

Branch Summarization

分支摘要和压缩是姐妹机制。用户在 /tree 中离开某个分支时,Pi 可以:

  1. 找旧 leaf 与目标 leaf 的共同祖先。
  2. 收集旧分支从 leaf 回到共同祖先之间的 entry。
  3. 生成摘要。
  4. 在导航位置追加 branch_summary entry。

这样你从一个实验分支回到主分支时,可以把“那边做了什么”以摘要形式带回来,而不用把完整分支历史塞入上下文。

为什么不是删除历史

压缩不会改写或删除旧 JSONL entry。它只是追加一个 compaction entry,并让 buildSessionContext() 在当前路径上把它解析成 summary + kept messages。

优点:

  • 原始历史可审计。
  • fork 旧分支时仍可能看到旧路径。
  • 压缩是可解释的 session 事件。
  • 扩展可以通过 details 保存结构化压缩状态。

设计启发

  • 上下文裁剪要尊重消息结构,尤其是 tool call/result 配对。
  • 压缩摘要应保留目标、约束、进度、决策、下一步和关键文件。
  • 多次压缩需要迭代更新旧摘要,而不是每次只总结新片段。
  • 对 coding agent 来说,文件操作列表几乎和自然语言摘要一样重要。