PiPi
快速开始
使用 Pi
  • 简体中文
  • English
快速开始
使用 Pi
  • 简体中文
  • English
  • 从这里开始

    • 快速开始
    • 使用 Pi
    • 提供商
    • 安全
    • 容器化
    • 设置
    • 键位绑定
    • 会话
    • 压缩与分支总结
  • 自定义

    • 扩展
    • 技能
    • 提示词模板
    • 主题
    • Pi 包
    • 自定义模型
    • 自定义提供商
  • 参考

    • 会话文件格式
  • 编程式使用

    • SDK
    • RPC 模式
    • JSON 事件流模式
    • TUI 组件
  • 平台设置

    • Windows 设置
    • Termux(Android)设置
    • tmux 设置
    • 终端设置
    • Shell 别名
  • 开发

    • 开发

压缩与分支总结

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 个 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. 追加条目:保存带有 summary 和 firstKeptEntryId 的 CompactionEntry
  5. 重新加载:会话重新加载,使用 summary + 从 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 计数会反映实际被替换的压缩前上下文。

拆分轮次

一个“轮次”从用户消息开始,并包含直到下一条用户消息之前的所有助手响应和工具调用。通常,压缩会在轮次边界处切分。

当单个轮次超过 keepRecentTokens 时,切点会落在轮次中间的某条助手消息处。这称为“拆分轮次”:

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. 轮次前缀总结:拆分轮次的较早部分

切点规则

有效切点包括:

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

绝不在工具结果处切分(它们必须与对应的工具调用保留在一起)。

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[]
}

扩展可以在 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 会从以下来源提取文件操作:

  • 被总结消息中的工具调用
  • 先前压缩或分支总结的 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

这会防止模型将其视为要继续的对话。

工具结果在序列化期间会被截断到 2000 个字符。超出该限制的内容会被替换为一个标记,说明有多少字符被截断。这可以让总结请求保持在合理的 token 预算内,因为工具结果(尤其来自 read 和 bash)通常是上下文大小的最大贡献者。

通过扩展自定义总结

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

session_before_compact

在自动压缩或 /compact 之前触发。可以取消或提供自定义总结。请参阅类型文件中的 SessionBeforeCompactEvent 和 CompactionPreparation。

pi.on('session_before_compact', async (event, ctx) => {
  const { preparation, branchEntries, customInstructions, reason, willRetry, 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)
  // reason - "manual" (/compact), "threshold", or "overflow"
  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)
  // 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 */},
      },
    }
  }
})

请参阅类型文件中的 SessionBeforeTreeEvent 和 TreePreparation。

设置

在 ~/.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 手动压缩。

最近更新:: 2026/7/6 09:32
Contributors: seepine
Prev
会话