跳转到内容

压缩与分支摘要

LLM 的上下文窗口有限。当对话变得过长时,pi 会使用压缩来摘要较早的内容,同时保留最近的工作。本页涵盖自动压缩和分支摘要。

源文件pi-mono):

要在项目中查看 TypeScript 定义,请检查 node_modules/@earendil-works/pi-coding-agent/dist/

Pi 有两种摘要机制:

机制触发条件用途
压缩上下文超过阈值,或 /compact摘要旧消息以释放上下文
分支摘要/tree 导航切换分支时保留上下文

两者使用相同的结构化摘要格式,并累积跟踪文件操作。

自动压缩在以下情况触发:

contextTokens > contextWindow - reserveTokens

默认情况下,reserveTokens 为 16384 个 token(可在 ~/.pi/agent/settings.json<project-dir>/.pi/settings.json 中配置)。这为 LLM 的响应留出空间。

你也可以使用 /compact [instructions] 手动触发,其中可选的 instructions 用于聚焦摘要内容。

  1. 查找切分点:从最新消息向后遍历,累积 token 估算,直到达到 keepRecentTokens(默认 20k,可在 ~/.pi/agent/settings.json<project-dir>/.pi/settings.json 中配置)
  2. 提取消息:收集从上一个保留边界(或会话开始)到切分点的消息
  3. 生成摘要:调用 LLM 以结构化格式生成摘要,若存在则传递上一次摘要作为迭代上下文
  4. 追加条目:保存包含摘要和 firstKeptEntryIdCompactionEntry
  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)
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 调用,直到下一条用户消息。通常,压缩在轮次边界处切分。

当单个轮次超过 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 生成两个摘要并合并:

  1. 历史摘要:之前的上下文(如有)
  2. 轮次前缀摘要:分割轮次的早期部分

有效的切分点包括:

  • 用户消息
  • Assistant 消息
  • BashExecution 消息
  • 自定义消息(custom_message、branch_summary)

切勿在 tool 结果处切分(它们必须与对应的 tool 调用保持在一起)。

定义于 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[];
}

扩展可以在 details 中存储任何可 JSON 序列化的数据。默认压缩跟踪文件操作,但自定义扩展实现可以使用自己的结构。

实现请参阅 prepareCompaction()compact()

当你使用 /tree 导航到不同分支时,pi 会提示摘要你即将离开的工作。这会将左侧分支的上下文注入到新分支中。

  1. 查找公共祖先:旧位置和新位置共享的最深节点
  2. 收集条目:从旧叶节点回溯到公共祖先
  3. 按预算准备:在 token 预算内包含消息(优先最新)
  4. 生成摘要:以结构化格式调用 LLM
  5. 追加条目:在导航点保存 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 调用
  • 之前的压缩或分支摘要 details(如有)

这意味着文件跟踪在多次压缩或嵌套分支摘要中累积,保留已读和已修改文件的完整历史。

定义于 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 结果会被截断至 2000 个字符。超出部分会被替换为标记,说明截断了多少字符。这有助于将摘要请求控制在合理的 token 预算内,因为 tool 结果(尤其是来自 readbash 的)通常是上下文大小的主要贡献者。

扩展可以拦截并自定义压缩和分支摘要。事件类型定义请参阅 extensions/types.ts

在自动压缩或 /compact 之前触发。可以取消或提供自定义摘要。请参阅类型文件中的 SessionBeforeCompactEventCompactionPreparation

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

/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 */ },
}
};
}
});

请参阅类型文件中的 SessionBeforeTreeEventTreePreparation

~/.pi/agent/settings.json<project-dir>/.pi/settings.json 中配置压缩:

{
"compaction": {
"enabled": true,
"reserveTokens": 16384,
"keepRecentTokens": 20000
}
}
设置默认值说明
enabledtrue启用自动压缩
reserveTokens16384为 LLM 响应保留的 token
keepRecentTokens20000保留的最近 token(不摘要)

使用 "enabled": false 禁用自动压缩。你仍可使用 /compact 手动压缩。