06 上下文压缩与分支摘要
06 上下文压缩与分支摘要
Pi 有两套总结机制,很容易混。先分清:compaction 为“腾出上下文”而总结旧消息;branch summarization 为“换分支时不丢工作”而总结被舍弃的路径。
1. 两套机制对比
| 机制 | 触发 | 目的 |
|---|---|---|
| Compaction | 上下文超阈值,或 /compact | 总结旧消息以释放上下文 |
| Branch summarization | /tree 导航 | 切分支时保留被舍弃分支的上下文 |
两者使用相近的结构化格式,并累计追踪文件操作。两类的总结请求都禁用 prompt-cache 写入(这类一次性 prompt 不太可能被重用)。
2. 自动压缩何时触发
contextTokens > contextWindow - reserveTokens
reserveTokens 默认 16384,为 LLM 回复留空间。触发检查时机:
- 多轮 run 中,工具结束、结果追加后,开始下一次 assistant 响应前。若越线,就在
prepareNextTurn里压缩,然后执行既有的 catch-up steering 轮询,再到turn_start。已完成工具批终止 run 且无排队消息时跳过该检查。 - 新 user prompt 之前。
- 低层 run 结束后的最终 overflow 恢复。
provider 报上下文溢出,或提前以 stopReason: "length" 结束时,可选择一次 compact-and-retry 恢复尝试。带 tool call 的 length 响应会保留其合成的失败工具结果,并走普通工具/队列调度,而不是强制结束 run。
手动触发:/compact [instructions],可选指令用来引导摘要偏向某个话题。
3. 压缩的五个步骤
- 找切点:在已定稿的 session projection 上向前回走,累计 token 估算,直到达到
keepRecentTokens(默认 20000)。 - 提取消息:从上一个保留边界(或会话开始)到切点之间的投影消息。
- 生成摘要:调 LLM,按结构化格式总结;有上一份摘要时作为迭代上下文传入。
- 追加 entry:写入
CompactionEntry,带摘要与firstKeptEntryId。 - 重建上下文:下一次请求用“摘要 + 从
firstKeptEntryId起的消息”。
压缩前: hdr usr ass tool usr ass tool tool ass tool
└──── 待总结 ────┘ └──────── 保留 ────────┘
↑ firstKeptEntryId
压缩后: ... tool │ cmp(追加在末尾)
LLM 看到的: system │ summary │ usr ass tool ass tool
原始 entry 一条不删,只是不再发给模型。
重复压缩的行为
再次压缩时,被总结的区间从上次压缩的保留边界(firstKeptEntryId)开始,而不是从压缩 entry 本身开始(若该保留 entry 在路径上找不到,则退到上次压缩 entry 之后的那一条)。这样上次存活下来的消息会再次被纳入总结,不会出现“越压缩越丢中间信息”的缺口。retain-none 压缩以自身 ID 作为 firstKeptEntryId,重复压缩从它之后开始。
写入新 CompactionEntry 前,Pi 会从重建且已应用 context_edit 的投影重新计算 tokensBefore,保证它反映的是真实被替换的 pre-compaction 上下文。被省略的原始 entry 仍存储,但不影响切点、摘要、检查点与 token 估算。
4. 溢出 / length 恢复的顺序(很关键)
恢复保留既有生命周期与队列顺序:已完成的尝试仍能被 turn_end、agent_end 看到;run 后的恢复在重试前修好持久化的模型上下文。
持久化最终 assistant 响应
→ 扩展/公共 turn_end
→ 扩展/公共 agent_end
→ 为选中的尝试追加 context_edit 省略
→ 溢出/length:跑 session_before_compact,成功则追加 compaction
→ 以全新 run 开始重试
若恢复压缩失败或被取消:保留省略编辑,不追加 compaction,也不安排内部重试;既有排队工作仍按普通 steering / follow-up 规则走。agent_before_settle 看到的是恢复处理后的投影。原始转录历史、导出、计费总数与历史搜索扩展仍可看到被省略的那次尝试。
5. 切点规则与 split user-message span
合法切点:user 消息、assistant 消息、BashExecution 消息、custom 消息(custom_message / branch_summary)。
绝不在 tool result 切——它必须跟随它的 tool call。
当单个 user-message span 本身就超过 keepRecentTokens 时,切点会落到该 span 内部的某条 assistant 消息上,这就是 split user-message span:
entry: hdr │ usr ass tool ass tool tool ass tool
↑ ↑
turnStartIndex=1 firstKeptEntryId=7
└── turnPrefixMessages(1-6) ──┘
isSplitTurn = true
messagesToSummarize = [] (没有更早的 span)
turnPrefixMessages = [usr, ass, tool, ass, tool, tool]
此时 Pi 生成两种摘要并合并:1)历史摘要(之前上下文),2)该 span 的前缀摘要。
保留边界的推进限制
只有当“上下文不可见的后缀”里包含被省略的 assistant 尝试、且没有未省略的上下文产出 entry 时,准备阶段才能推进保留边界。恢复用的 context_edit 省略满足此条件;纯元数据不能单独推进切点,新追加的 custom message 也不能。若替换编辑影响了候选输入或被总结的前缀,也会阻断推进(因为被省略的 assistant 回答的是编辑前的输入)。
6. 产物结构
interface CompactionEntry<T = unknown> {
type: "compaction";
id: string;
parentId: string | null;
timestamp: string;
summary: string;
firstKeptEntryId: string;
tokensBefore: number;
usage?: Usage; // 生成摘要的用量,计入会话总用量
fromHook?: boolean; // 旧字段名,扩展提供时为 true
details?: T; // 实现自定数据
}
interface CompactionDetails { readFiles: string[]; modifiedFiles: string[] }
BranchSummaryEntry 同构,但用 fromId(导航来的那一条)代替 firstKeptEntryId,details 为 BranchSummaryDetails。
累计文件追踪
默认压缩与分支摘要都会从被总结消息的 tool call 里抽取文件操作,并累计传递:压缩会携带上一次 Pi 生成的压缩的文件列表;分支摘要会携带它所总结的 entry 中 Pi 生成的分支摘要的文件列表。因此它在多层嵌套上会累积。注意:fromHook: true 的扩展生成摘要不会被自动携带文件列表,由扩展自己管 details。
7. 摘要格式
两种格式都包含 Goal、Constraints & Preferences、Progress、Key Decisions、Next Steps;压缩摘要额外包含 Critical Context,分支摘要到 Next Steps 就结束。文件列表在相关时追加。
## Goal
[用户想达成什么]
## Constraints & Preferences
- [用户提到过的要求]
## Progress
### Done
- [x] ...
### In Progress
- [ ] ...
### Blocked
- ...
## Key Decisions
- **[决定]**:[理由]
## Next Steps
1. ...
## Critical Context
- [继续所需的数据]
<read-files>
path/to/file1.ts
</read-files>
<modified-files>
path/to/changed.ts
</modified-files>
消息序列化
总结前用 serializeConversation() 把消息转成文本,目的是防止模型把它当成一段继续的对话:
[User]: 说了什么
[Assistant thinking]: 内部推理
[Assistant]: 回复文本
[Assistant tool calls]: read(path="foo.ts"); edit(path="bar.ts", ...)
[Tool result]: 工具输出
工具结果在序列化时截断到 2000 字符,超出部分换成标明截断字符数的标记——因为 read 与 bash 的结果通常是上下文的体积大头。
8. 用扩展插手总结
session_before_compact
在自动压缩或 /compact 前触发,可取消或提供自定义摘要:
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;
// preparation.messagesToSummarize / turnPrefixMessages / previousSummary
// preparation.fileOps / tokensBefore / firstKeptEntryId / settings
// reason: "manual" | "threshold" | "overflow"
// willRetry: 被中止的 turn 是否会在压缩后重试
if (shouldSkip) return { cancel: true };
return {
compaction: {
summary: "你的摘要...",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
// usage: summaryResponse.usage,
details: { /* 自定义数据 */ },
},
};
});
用自己的模型总结时,导出 convertToLlm 与 serializeConversation 来准备文本:
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
const conversationText = serializeConversation(convertToLlm(preparation.messagesToSummarize));
完整例子见官方 examples/extensions/custom-compaction.ts。
session_compact_failed
手动或自动压缩失败/被中止时触发,用于把 session_before_compact 尝试与最终结果配对(典型是遥测):字段 reason、errorMessage(非 abort 失败才有)、aborted、willRetry、fromExtension。
session_before_tree
/tree 导航前触发,无论用户是否选择总结都会触发;可取消导航或提供自定义摘要(仅当 preparation.userWantsSummary 为真时使用):preparation 含 targetId、oldLeafId、commonAncestorId、entriesToSummarize、userWantsSummary。
9. 相关设置
{
"compaction": { "enabled": true, "reserveTokens": 16384, "keepRecentTokens": 20000 },
"branchSummary": { "reserveTokens": 16384, "skipPrompt": false }
}
逐模型覆盖
{
"compaction": {
"modelOverrides": { "some-provider/big-model": { "reserveTokens": 400000 } }
}
}
要点:
- key 是精确、区分大小写的
provider/modelId(模型 ID 内部的/也算)。 reserveTokens与keepRecentTokens各自独立回退:模型覆盖 → 普通设置 → 内置默认。- 0.5 内置约束:值必须是非负安全整数;模型覆盖条目必须是对象;匹配到的覆盖里有非法值会在读取时报错(只有省略的字段才回退),而普通 token 设置含非法值即使当前模型有合法覆盖也会报错。
enabled是全局的,不逐模型。- 解析后的值同时用于手动压缩、所有自动阈值检查、overflow 恢复,以及扩展可见的
preparation.settings。切模型只影响之后的检查与压缩,不会改普通设置;进行中的压缩使用它开始时捕获的模型与设置。分支摘要设置不受影响。 - 全局与项目设置会递归合并再查表:全局里的模型特定值优先于项目里的全局回退值,要改它必须在项目里覆盖同一个模型条目。
关掉自动压缩("enabled": false)后,仍可手动 /compact;provider 不可用或无法接受总结请求时压缩会失败,修好后重跑即可。
10. 读代码入口
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/src/core/session-manager.ts(entry 类型)packages/coding-agent/src/core/extensions/types.ts(扩展事件类型)prepareCompaction()/compact()、generateSummary()/generateSummaryWithUsage()、collectEntriesForBranchSummary()/prepareBranchEntries()/generateBranchSummary()
溯源
- 官方
docs/compaction.md、docs/sessions.md、docs/settings.md