LLM 的上下文窗口是有限的,但对话是无限增长的。
Pi 通过上下文压缩在输入端和历史端建立防线,让 Agent 在长时间对话中不失忆。
上下文窗口:200k tokens
┌──────────────────────────────────────────────┐
第 1 轮: │ system prompt + user msg + assistant reply │ ~2k
第 5 轮: │ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ~20k
第 20 轮: │ █████████████████████████░░░░░░░░░░░░░░░░░░░ │ ~80k
第 50 轮: │ ████████████████████████████████████████████░ │ ~180k
第 60 轮: │ ██████████████████████████████████████████████│ 爆了!
└──────────────────────────────────────────────┘
不做任何处理,对话历史会不断膨胀直到超出窗口。超出后要么报错,要么被截断——两种结果都会导致 Agent 丢失关键信息。
Pi 在两个方向管理上下文:
@startuml
skinparam backgroundColor transparent
rectangle "输入端" #E8F5E9 {
rectangle "System Prompt\n构建" as SP
rectangle "transformContext\n上下文变换" as TC
}
rectangle "历史端" #E3F2FD {
rectangle "Compaction\n上下文压缩" as CP
rectangle "Branch Summary\n分支摘要" as BS
}
rectangle "LLM 调用" as LLM #FFF3E0
SP --> LLM
TC --> LLM
CP --> LLM
BS --> LLM
note bottom of SP: 每轮重新构建\n注入最新工具/技能信息
note bottom of TC: 调 LLM 前\n可选的上下文变换管道
note bottom of CP: 旧消息压缩为摘要\n保留最近 N 轮
note bottom of BS: 切换分支时\n旧分支压缩为摘要
@enduml当消息历史接近窗口限制时,Pi 自动触发压缩。
@startuml
skinparam backgroundColor transparent
skinparam ActivityBackgroundColor #f8f9fa
start
:检测 token 使用量;
if (超过阈值?) then (yes)
:prepareCompaction();
note right
计算哪些消息要压缩
哪些消息要保留
end note
:分离消息;
fork
:待压缩消息\n(旧的对话历史);
fork again
:保留消息\n(最近 N 轮);
end fork
:用 LLM 生成摘要;
note right
把待压缩的消息
发给 LLM 做结构化摘要
end note
:创建 CompactionEntry;
:写入 Session 存储;
:替换上下文;
note right
上下文 = 摘要 + 保留的最近消息
总 token 数大幅减少
end note
else (no)
:继续;
endif
stop
@enduml压缩前(180k tokens):
┌─────────────────────────────────────┐
│ [msg1][msg2][msg3]...[msg48][msg49] │
│ ← 很早的对话 ─────────── 最近的 → │
└─────────────────────────────────────┘
压缩后(~30k tokens):
┌─────────────────────────────────────┐
│ [摘要: 3k tokens][msg45]...[msg49] │
│ ← 压缩后的历史 最近保留的 5 轮 → │
└─────────────────────────────────────┘
interface CompactionSettings {
enabled: boolean;
reserveTokens: number; // 为新的对话预留的 token 空间
keepRecentTokens: number; // 保留最近多少 token 的消息不压缩
}
// 默认配置
const DEFAULT_COMPACTION_SETTINGS = {
enabled: true,
reserveTokens: 40000, // 预留 40k
keepRecentTokens: 20000, // 保留最近 20k
};Pi 要求 LLM 生成的摘要是结构化的,不是随意的文本总结:
interface CompactResult {
summary: string; // 结构化摘要文本
firstKeptEntryId?: string; // 保留的第一条消息 ID
tokensBefore: number; // 压缩前的 token 数
retainedTail?: AgentMessage[]; // 保留的尾部消息
usage?: Usage; // 摘要生成消耗的 token
}摘要会追踪文件操作,确保 Agent 记住自己做过什么:
interface FileOperations {
read: Set<string>; // 读过的文件
written: Set<string>; // 写过的文件
edited: Set<string>; // 编辑过的文件
}在调用 LLM 前,transformContext 钩子允许对上下文做最后的处理:
const agent = new Agent({
transformContext: async (messages, signal) => {
// 例:删除超过 100 轮前的消息
// 例:注入实时检索结果
// 例:压缩中间的工具结果
return processedMessages;
},
});这与 Compaction 不同——transformContext 是每次调 LLM 前都执行的临时变换,不修改持久化的消息历史。Compaction 则是永久修改消息历史。
当用户通过 /tree 命令切换到历史分支时,当前分支的对话会被压缩为一条摘要:
@startuml
skinparam backgroundColor transparent
rectangle "Session Tree" {
rectangle "Root" as R
rectangle "Branch A\n(当前)" as A #C8E6C9
rectangle "Branch B\n(要切换到)" as B #E3F2FD
R --> A
R --> B
}
note right of A
切换前:压缩为 BranchSummaryEntry
包含对话摘要和文件操作记录
end note
@enduml分支摘要与 Compaction 的区别:
| 维度 | Compaction | Branch Summary |
|---|---|---|
| 触发时机 | 上下文快满时 | 切换分支时 |
| 作用范围 | 当前对话的旧消息 | 整个分支的对话 |
| 结果 | 替换旧消息 | 存储为独立条目 |
| 目的 | 释放窗口空间 | 保留分支记忆 |
Pi 的 system prompt 不是静态的——它在每轮对话开始前动态构建:
// AgentHarness 支持函数式 system prompt
const harness = new AgentHarness({
systemPrompt: ({ session, model, thinkingLevel, activeTools, resources }) => {
let prompt = BASE_SYSTEM_PROMPT;
// 注入可用工具描述
for (const tool of activeTools) {
prompt += `\nTool: ${tool.name} - ${tool.description}`;
}
// 注入可用 Skills
if (resources.skills?.length) {
prompt += formatSkillsForSystemPrompt(resources.skills);
}
return prompt;
},
});这确保 system prompt 始终反映 Agent 的最新状态(可用工具、技能等可能在运行时变化)。
压缩和分支摘要都涉及 LLM 调用,可能失败。Pi 内置了重试机制:
interface RetryPolicy {
maxAttempts: number; // 最大重试次数
baseDelayMs: number; // 基础延迟
maxDelayMs: number; // 最大延迟
backoffMultiplier: number; // 退避倍数
}相关事件:
{ type: "retry_scheduled", operation: "compaction", attempt: 2, maxAttempts: 3, delayMs: 2000 }
{ type: "retry_attempt_start", operation: "compaction" }
{ type: "retry_finished", operation: "compaction" }- 理解为什么需要上下文压缩
- 知道 Compaction 的触发条件和流程
- 理解压缩摘要为什么是结构化的
- 知道
transformContext和 Compaction 的区别 - 理解 Branch Summary 的用途
上一章:第六章 · 事件流
下一章:第八章 · 会话管理 — 对话的持久化与分支