Pi 中压缩的工作原理

Hacker News Top 产品

摘要

本文解释了 Pi 编码代理中压缩的工作原理,即在上下文窗口接近限制时对旧对话历史进行总结,并详细介绍了 Pi 的具体实现和触发条件。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/08/13 21:22

# Pi 中压缩的工作原理 | EARENDIL 来源:https://earendil.com/posts/compaction-in-pi/ 如果你曾在像 [Pi](https://pi.dev/)、Claude Code 或 Codex 这样的编码代理中进行过一次长时间的编码会话,你一定会触发压缩(compaction)。在这篇文章中,我们将解释压缩的工作原理,以及 Pi 何时需要压缩。 ## LLM 对话 大型语言模型(LLM)的[上下文窗口](https://en.wikipedia.org/wiki/Context_window)是有限的。上下文窗口就是模型在生成响应时能够“看到”的内容。LLM 所使用的 [Transformer 架构](https://en.wikipedia.org/wiki/Transformer_(deep_learning)) 限制了它们能处理的输入量。编码代理会话的输入包括所有先前的消息和工具调用,而这些内容会随着你的工作不断增多。一旦超出上下文窗口,LLM 就会拒绝请求。 当与像 Pi 这样的编码代理进行交互式协作时,代理会向 LLM 发送请求并接收响应。每个请求都包含系统提示、加载的文件(如 [AGENTS.md](https://agents.md/))、工具定义以及对话历史。 编码代理的第一次 LLM 请求包含这个初始上下文,以及第一条用户消息。 ``` request 1: [system][tools][user] ``` 这会开启一个回合(turn)。LLM 可能首先返回包含工具调用的助手消息。代理程序执行这些调用,并向 LLM 发送一个新的请求,其中包含完整的对话,现在还包括工具结果。我们会得到另一条助手消息。当助手完成生成输出后,这个回合才算结束。 ``` request 1 之后: [system][tools][user][assistant: tool call][tool result][assistant] <-------------------> ^ <---------> 由 LLM 返回 | 由 LLM 返回 | 由代理生成 ``` 我们继续工作,再发送一条消息。 ``` request 2: [system][tools][user][assistant: tool call][tool result][assistant][user] ^ 新的用户消息 ``` 每个回合都会扩展对话。最终,历史记录会超过上下文限制。下一个请求会返回类似 `Request exceeds the maximum size` 的错误。 ``` [system][tools][user][assistant][....][tool result][user] ^ 超出上下文窗口 ``` ## 处理上下文溢出 当我们无法按原样继续现有对话时,有两个选择。 1. 我们可以开始一个全新的空对话,不带任何累积的上下文。这会丢弃历史记录,包括先前做出的决定和未完成的工作。这仍然可能是一个好主意,因为 LLM 输出的性能会随着上下文规模的增长而下降(https://www.trychroma.com/research/context-rot)。 2. 我们可以创建对话上下文的更精简表示,因为我们想让这段对话继续进行。这就是压缩(compaction)的作用。 ## 压缩 理论上,实现压缩有很多种方法。例如,我们可以编写一个确定性函数,保留对话中的部分内容并丢弃其余部分。不过在实践中,压缩的实现通常使用一个 LLM 请求来总结对话历史。 压缩会用一种经过压缩的表示来替换部分历史记录,从而为额外的消息和工具调用腾出空间。 ``` [system][tools][compaction result][user] ^ 新的消息 ``` ## Pi 的实现 让我们更仔细地看看 Pi 具体是如何实现压缩(compaction)的(https://pi.dev/docs/latest/compaction#summary-format)。 当对话变得过长时,Pi 会使用压缩来总结较早的内容,同时保留近期的工作。当上下文限制接近上下文窗口的总大小时,就会触发自动压缩。你也可以使用 `/compact` 命令手动触发压缩。 Pi 会在一个回合结束后检查是否需要进行自动压缩。在此之前,每个请求都会扩展现有的提示(prompt),并且可以复用其缓存前缀。如果 Pi 遇到上下文溢出错误,它也可能会在回合中途进行压缩。 压缩时,Pi 会保留若干条最近的消息不变。 ``` 压缩前: [system + tools][older turns][recent retained messages] ``` 保留的消息数量并不固定,因为 Pi 使用可配置的 token 预算(https://pi.dev/docs/latest/compaction#when-it-triggers)。Pi 当前默认的 2 万个 token 大约相当于 5 到 20 个回合。在这个截断点之前的所有消息都会被提取并序列化,然后进行总结。 ## Pi 的压缩提示(Compaction Prompt) 对于编码代理来说,一个好的总结的理想效果,就像是交接班时的简报。Pi 的压缩提示关注这样一个事实:现有上下文中有很多内容已经不再相关。我们应该只保留对下一次 LLM 请求仍然重要的上下文。 因此,Pi 用于压缩的请求与常规对话的请求不同。 1. 独立的压缩请求所使用的系统提示是不同的。我们不会告诉 LLM“你是一个专业的编码助手”,而是告诉它“你是一个上下文总结助手”。(https://github.com/earendil-works/pi/blob/47610217098d9ba8f22d223fa7c1413f9f5fd759/packages/coding-agent/src/core/compaction/utils.ts#L152-L158) 2. 压缩请求中的用户消息也不同。它要求“为之后返回时提供上下文,对这个对话分支进行结构化总结”。(https://github.com/earendil-works/pi/blob/47610217098d9ba8f22d223fa7c1413f9f5fd759/packages/coding-agent/src/core/compaction/compaction.ts#L463-L498)提示中指定了目标、进度和关键决策等部分。 3. 这是一个独立的请求,不使用任何现有对话历史,这意味着它可以采用不同的 LLM 模型,而不会产生不必要的成本。 压缩的结果会作为一条压缩记录追加到 Pi 会话中,会话现在就可以继续了。压缩请求完成后,上下文已被压缩。 ``` 压缩后: [system][tools][summary][recent turns][new user message] ``` 现在对话上下文中有了更多空间,可以容纳更多消息。 Pi 将压缩总结以纯文本形式存储在会话中。这使得压缩后的上下文可读且可移植(https://earendil.com/posts/session-portability),因为我们可以在 Pi 中切换模型并继续使用该总结。 ## 压缩与提示缓存(Prompt Caching) 提示缓存(https://earendil.com/posts/prompt-caching)被 LLM 提供商用来降低同一对话中重复请求的成本。在活跃的编码会话中,对于模型已经生成过的上下文,我们支付的费用更低。这种缓存要求前缀完全匹配,因此压缩会话会破坏提示缓存。 ``` 压缩前缓存: [system][tools][older history][recent retained turns] <-------------------- 缓存前缀 --------------------> 压缩后的第一个请求: [system][tools][summary][recent retained turns][new user message] <-- 可复用 -->^ | 第一个改变的 token | +-- 此之后的所有内容都必须重新计算

相似文章

上下文压缩应该保留什么?我观察了六种智能体的处理方式[D]

Reddit r/MachineLearning

分析六种AI编程智能体(Claude Code、Codex CLI、OpenCode、Cline、Cursor、Amp)如何趋同于分层渐进式压缩以处理长上下文,它们在保护内容(用户消息、有状态工具输出)以及是否告知模型压缩方面存在差异,并在成本与准确性之间进行权衡。