上下文裁剪与压缩¶
Pi 的上下文策略不是粗暴截断,而是结合 token 估算、合法 cut point、turn 边界、split-turn 摘要、文件操作追踪和 session reload 的完整机制。
本章涉及的 token 估算、cut point、split turn、compaction 和 branch summary 行号见源码索引。
关键源码:
packages/coding-agent/src/core/compaction/compaction.tspackages/coding-agent/src/core/compaction/branch-summarization.tspackages/coding-agent/src/core/compaction/utils.tspackages/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:
- 找最后一条非 error/aborted assistant usage。
- 用
calculateContextTokens()读取totalTokens或各项相加。 - 对 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 前缀。
压缩时会并行生成两个摘要:
- history summary:老历史摘要。
- 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 可以:
- 找旧 leaf 与目标 leaf 的共同祖先。
- 收集旧分支从 leaf 回到共同祖先之间的 entry。
- 生成摘要。
- 在导航位置追加
branch_summaryentry。
这样你从一个实验分支回到主分支时,可以把“那边做了什么”以摘要形式带回来,而不用把完整分支历史塞入上下文。
为什么不是删除历史¶
压缩不会改写或删除旧 JSONL entry。它只是追加一个 compaction entry,并让 buildSessionContext() 在当前路径上把它解析成 summary + kept messages。
优点:
- 原始历史可审计。
- fork 旧分支时仍可能看到旧路径。
- 压缩是可解释的 session 事件。
- 扩展可以通过 details 保存结构化压缩状态。
设计启发¶
- 上下文裁剪要尊重消息结构,尤其是 tool call/result 配对。
- 压缩摘要应保留目标、约束、进度、决策、下一步和关键文件。
- 多次压缩需要迭代更新旧摘要,而不是每次只总结新片段。
- 对 coding agent 来说,文件操作列表几乎和自然语言摘要一样重要。