Pi 官方文档
上下文压缩
上下文压缩与分支摘要
LLM 的上下文窗口有限。对话变长后,Pi 会使用上下文压缩,把较早的内容总结起来,同时保留最近的工作内容。本文介绍自动上下文压缩和分支摘要这两种机制。
源文件 (pi-mono):
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- 条目类型(CompactionEntry、BranchSummaryEntry)packages/coding-agent/src/core/extensions/types.ts- 扩展事件类型
如果你想查看项目里的 TypeScript 定义,请检查 node_modules/@earendil-works/pi-coding-agent/dist/。
概览
Pi 有两种摘要机制:
| 机制 | 触发条件 | 作用 |
|---|---|---|
| 上下文压缩 | 上下文超过阈值,或执行 /compact | 将旧消息总结成摘要,腾出上下文 |
| 分支摘要 | /tree 导航 | 切换分支时保留上下文 |
两者都使用同一种结构化摘要格式,并且会累积追踪文件操作。
上下文压缩
何时触发
自动上下文压缩在以下情况触发:
contextTokens > contextWindow - reserveTokens
默认情况下,reserveTokens 是 16384 tokens(可在 ~/.pi/agent/settings.json 或 <project-dir>/.pi/settings.json 中配置)。这样会为 LLM 的回复预留空间。
你也可以手动执行 /compact [instructions],其中可选的 instructions 用来聚焦摘要内容。
工作机制
- 找到截断点:从最新消息开始向前回溯,累加 token 估算值,直到达到
keepRecentTokens(默认 20k,可在~/.pi/agent/settings.json或<project-dir>/.pi/settings.json中配置) - 提取消息:收集上一次保留边界(或会话起点)到截断点之间的消息
- 生成摘要:调用 LLM 生成结构化摘要;如果有上一版摘要,就把它作为迭代上下文传入
- 追加条目:保存带有摘要和
firstKeptEntryId的CompactionEntry - 重新加载:会话重新加载时,使用摘要 + 从
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)
After compaction (new entry appended):
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
What the LLM sees:
┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐
│ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │
└────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘
↑ ↑ └─────────────────┬────────────────┘
prompt from cmp messages from firstKeptEntryId
在重复执行上下文压缩时,被摘要的区间会从上一次上下文压缩保留的边界(firstKeptEntryId)开始,而不是从上下文压缩条目本身开始;如果在路径中找不到那个保留条目,就回退到上一次上下文压缩之后的条目。这样可以把上一轮上下文压缩后仍然保留下来的消息,也一起纳入下一轮摘要。Pi 还会在写入新的 CompactionEntry 之前,先根据重建后的会话上下文重新计算 tokensBefore,这样 token 数就能反映实际被替换的压缩前上下文。
拆分轮次
一个“轮次”从用户消息开始,包含之后所有 assistant 回复和 tool call,直到下一条用户消息为止。通常情况下,上下文压缩会在轮次边界处截断。
当单个轮次超过 keepRecentTokens 时,截断点会落在轮次中部的一条 assistant 消息上。这就是“拆分轮次”:
Split turn (one huge turn exceeds budget):
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)
isSplitTurn = true
messagesToSummarize = [] (no complete turns before)
turnPrefixMessages = [usr, ass, tool, ass, tool, tool]
对于拆分轮次,Pi 会生成两份摘要并合并它们:
- 历史摘要:之前的上下文(如果有)
- 轮次前缀摘要:被拆分轮次的前半部分
截断点规则
有效的截断点包括:
- User messages
- Assistant messages
- BashExecution messages
- Custom messages (custom_message, branch_summary)
不要在 tool 返回结果处截断(它们必须和对应的 tool call 保持在一起)。
CompactionEntry 结构
定义在 session-manager.ts 中:
interface CompactionEntry<T = unknown> {
type: "compaction";
id: string;
parentId: string;
timestamp: number;
summary: string;
firstKeptEntryId: string;
tokensBefore: number;
fromHook?: boolean; // true if provided by extension (legacy field name)
details?: T; // implementation-specific data
}
// Default compaction uses this for details (from compaction.ts):
interface CompactionDetails {
readFiles: string[];
modifiedFiles: string[];
}
扩展可以把任何可 JSON 序列化的数据存到 details 里。默认的上下文压缩会跟踪文件操作,但自定义扩展实现可以使用自己的结构。
实现请看 prepareCompaction() 和 compact()。
分支摘要
何时触发
当你使用 /tree 切换到另一个分支时,pi 会提示你总结即将离开的工作。这会把左侧分支的上下文带入新分支。
工作机制
- 找到共同祖先:旧位置和新位置共享的最深节点
- 收集条目:从旧叶子节点向上回溯到共同祖先
- 按预算准备:按 token 预算包含消息,优先保留最新的
- 生成摘要:用结构化格式调用 LLM
- 追加条目:在切换点保存
BranchSummaryEntry
Tree before navigation:
┌─ B ─ C ─ D (old leaf, being abandoned)
A ───┤
└─ E ─ F (target)
Common ancestor: A
Entries to summarize: B, C, D
After navigation with summary:
┌─ B ─ C ─ D ─ [summary of B,C,D]
A ───┤
└─ E ─ F (new leaf)
累积式文件跟踪
上下文压缩和分支摘要都会累积跟踪文件。在生成摘要时,pi 会从以下来源提取文件操作:
- 正在被总结的消息里的 tool call
- 之前的上下文压缩或分支摘要
details(如果有)
这意味着文件跟踪会跨越多次上下文压缩或嵌套的分支摘要持续累积,保留读写过的文件完整历史。
BranchSummaryEntry 结构
定义在 session-manager.ts 中:
interface BranchSummaryEntry<T = unknown> {
type: "branch_summary";
id: string;
parentId: string;
timestamp: number;
summary: string;
fromId: string; // Entry we navigated from
fromHook?: boolean; // true if provided by extension (legacy field name)
details?: T; // implementation-specific data
}
// Default branch summarization uses this for details (from branch-summarization.ts):
interface BranchSummaryDetails {
readFiles: string[];
modifiedFiles: string[];
}
和上下文压缩一样,扩展也可以把自定义数据存到 details 里。
实现请看 collectEntriesForBranchSummary()、prepareBranchEntries() 和 generateBranchSummary()。
摘要格式
上下文压缩和分支摘要都使用同一种结构化格式:
## Goal
[What the user is trying to accomplish]
## Constraints & Preferences
- [Requirements mentioned by user]
## Progress
### Done
- [x] [Completed tasks]
### In Progress
- [ ] [Current work]
### Blocked
- [Issues, if any]
## Key Decisions
- **[Decision]**: [Rationale]
## Next Steps
1. [What should happen next]
## Critical Context
- [Data needed to continue]
<read-files>
path/to/file1.ts
path/to/file2.ts
</read-files>
<modified-files>
path/to/changed.ts
</modified-files>
消息序列化
在总结之前,消息会先通过 serializeConversation() 序列化为文本:
[User]: What they said
[Assistant thinking]: Internal reasoning
[Assistant]: Response text
[Assistant tool calls]: read(path="foo.ts"); edit(path="bar.ts", ...)
[Tool result]: Output from tool
这样可以避免模型把它当成一段要继续下去的对话。
序列化时,tool result 会被截断到 2000 个字符。超出部分会被替换成一个标记,说明有多少字符被截断。这样可以把摘要请求控制在合理的 token 预算内,因为 tool result,尤其是 read 和 bash 的结果,通常是占用上下文最多的部分。
通过扩展定制摘要
扩展可以拦截并定制上下文压缩和分支摘要。事件类型定义请看 extensions/types.ts。
session_before_compact
在自动上下文压缩或 /compact 之前触发。可以取消,或提供自定义摘要。类型定义请看 types 文件里的 SessionBeforeCompactEvent 和 CompactionPreparation。
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, signal } = event;
// preparation.messagesToSummarize - messages to summarize
// preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)
// preparation.previousSummary - previous compaction summary
// preparation.fileOps - extracted file operations
// preparation.tokensBefore - context tokens before compaction
// preparation.firstKeptEntryId - where kept messages start
// preparation.settings - compaction settings
// branchEntries - all entries on current branch (for custom state)
// signal - AbortSignal (pass to LLM calls)
// Cancel:
return { cancel: true };
// Custom summary:
return {
compaction: {
summary: "Your summary...",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
details: { /* custom data */ },
}
};
});
将消息转成文本
如果你想用自己的模型生成摘要,可以先用 serializeConversation 把消息转成文本:
import { convertToLlm, serializeConversation } from "@earendil-works/pi-coding-agent";
pi.on("session_before_compact", async (event, ctx) => {
const { preparation } = event;
// Convert AgentMessage[] to Message[], then serialize to text
const conversationText = serializeConversation(
convertToLlm(preparation.messagesToSummarize)
);
// Returns:
// [User]: message text
// [Assistant thinking]: thinking content
// [Assistant]: response text
// [Assistant tool calls]: read(path="..."); bash(command="...")
// [Tool result]: output text
// Now send to your model for summarization
const summary = await myModel.summarize(conversationText);
return {
compaction: {
summary,
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
}
};
});
完整示例请看 custom-compaction.ts,其中使用了不同的模型。
session_before_tree
在 /tree 导航前触发。无论用户是否选择摘要,都会触发。可以取消导航,或者提供自定义摘要。
pi.on("session_before_tree", async (event, ctx) => {
const { preparation, signal } = event;
// preparation.targetId - where we're navigating to
// preparation.oldLeafId - current position (being abandoned)
// preparation.commonAncestorId - shared ancestor
// preparation.entriesToSummarize - entries that would be summarized
// preparation.userWantsSummary - whether user chose to summarize
// Cancel navigation entirely:
return { cancel: true };
// Provide custom summary (only used if userWantsSummary is true):
if (preparation.userWantsSummary) {
return {
summary: {
summary: "Your summary...",
details: { /* custom data */ },
}
};
}
});
请参见 types 文件中的 SessionBeforeTreeEvent 和 TreePreparation。
设置
在 ~/.pi/agent/settings.json 或 <project-dir>/.pi/settings.json 中配置上下文压缩:
{
"compaction": {
"enabled": true,
"reserveTokens": 16384,
"keepRecentTokens": 20000
}
}
| 设置 | 默认值 | 说明 |
|---|---|---|
enabled | true | 启用自动上下文压缩 |
reserveTokens | 16384 | 为 LLM 响应预留的 token 数 |
keepRecentTokens | 20000 | 保留最近的 token(不做摘要) |
使用 "enabled": false 关闭自动上下文压缩。你仍然可以通过 /compact 手动执行上下文压缩。