重开会话,还是压缩历史?Pi 是如何解决模型上下文自动压缩问题的
一次长任务不会因为模型“记性好”而持续。它只是不断把过去重新塞进下一次请求;装不下时,Pi 会重写自己的工作记忆。
- Compacting context 是把较早工作改写成更短的状态摘要,为长任务腾出上下文空间。
- Pi 默认保留最近约 20k tokens,给下一次回答预留 16384 tokens;这些数值是 Pi 的实现,不是行业通则。
- Compaction 是有损迁移:它保住工作连续性,却会丢细节,并让旧 prompt cache 暂时失效。
如果你让 Pi、Claude Code 或 Codex 连续改代码、查日志、跑测试,任务足够长时,多半见过类似 compacting context 的提示。
它不是在压缩代码,也不是在删除文件。它表示当前会话快要装满模型一次能够读取的信息空间,coding agent 正在把较早的工作记录改写成更短的版本,好让任务继续。
这个有限空间叫 context window,也就是上下文窗口。模型每次回答时,只能参考被放进这个窗口里的内容。长任务不断加入新消息、代码、日志、工具调用和执行结果,窗口便会越来越满。compacting context 出现,是因为 agent 必须在撞上容量上限之前,为后续工作腾出空间。
Earendil Engineering 专门写了一篇《How Compaction Works in Pi》,拆开 Pi 的处理方式。Pi 是一款运行在终端里的 coding agent。下面讲的是它的具体实现;其他 coding agent 也面对同一个通用问题,但触发阈值、保留量与摘要方法未必相同。
模型没记忆,只重放上下文
假设你让 agent 修一个支付故障,第一句只说“查一下 checkout 为什么返回 502”。模型真正收到的却不止这句话:还有 system prompt、工具定义、项目里的规则文件。它调用日志工具后,工具结果也会进入历史;你再补一条线索,下一次请求又会带上此前全部内容。
Agent 不是在某个隐藏脑区里永久记住了工作。它是在每次请求时,把目前的工作现场重新摆到模型面前。
每一步都合理,但累积结果只有一个:历史单向增长,任务却可能还没完成。上下文装不下时,agent 不能继续把过去原封不动地带走,它必须决定哪些内容还值得保留。
重开会话,还是压缩历史
一旦现有会话无法原样继续,agent 有两条基本路线。它可以新开会话,立刻拿回全部空间,但目标、已做工作、关键决定和未解决问题也一起离场。它也可以做 compaction:把一部分旧历史换成更短的表示,再沿着当前任务继续。
Pi 选择第二条路。这里的“压缩”并不像压缩包,之后还能无损还原。Pi 通常让模型为旧历史写摘要,空间来自取舍:细节会丢失,措辞会被概括,某些只出现过一次的信息可能逐渐变淡。
所以 compaction 更准确的名字是有损的工作状态迁移。它不追求保留每一句话,而是让下一次模型仍然知道:目标是什么、已经做了什么、哪些决定不能推翻、现在卡在哪里、下一步先做什么。
Pi 如何切分新旧上下文
Pi 不会把整段对话一股脑概括。它先从最新消息往回数,划出一条切线。按当前官方文档和固定源码版本,当 contextTokens > contextWindow - reserveTokens 时会触发自动压缩。默认给即将生成的回答预留 16,384 tokens,并尽量让最近 20,000 tokens 保持原样。
20k 是预算,不是固定消息数。若最近几轮塞了大量日志和代码,能留下的轮数就会更少。原文将它粗略换算为 5–20 轮,但这不是保证。
默认给新回答预留 16,384 tokens;最近 20k 是预算而非固定轮数,两项均属于 Pi 的可配置实现。
切线以前的内容被抽取、序列化并总结;切线以后仍按原顺序进入新请求。Pi 通常等一轮结束后再检查自动压缩。如果模型已经在一轮中途返回超限错误,它才可能当场压缩并重试。
用户也可以主动运行 /compact [instructions],在需要时提前压缩,并用附加说明提醒摘要器保留某些信息。这个入口能改变时机和重点,却仍不能把有损摘要变成无限记忆。
摘要是一份工作交接
普通摘要关心“刚才聊了什么”。工作交接关心的是:下一棒能不能直接开工。
Pi 会发起一次独立的模型请求,把角色改成 context summarization assistant,要求它不要继续当前对话,而是输出结构化 checkpoint。当前提示词要求保留六类信息:
- Goal:用户最终要完成什么。
- Constraints & Preferences:不能违反的限制与偏好。
- Progress:已完成、进行中与阻塞项。
- Key Decisions:关键选择及其理由。
- Next Steps:接下来按什么顺序推进。
- Critical Context:路径、函数、报错和其他精确信息。
生成结果会作为纯文本 CompactionEntry 写回 session。新的请求因此变成:
[system] [tools] [summary] [recent turns] [new user]
纯文本的好处很朴素:人可以检查摘要有没有漏掉关键决定,Pi 也能切换模型后继续使用它。被迁移的是可见的工作状态,不是某个模型内部不可检查的记忆。
但这份交接单只能保住摘要器识别到的东西。重要约束如果只在很早以前出现过一次,仍可能在反复压缩中变淡。真正不能丢的项目规则,最好写进 AGENTS.md、任务状态、代码或其他持久记录。
前缀一变,旧缓存就失效
Compaction 腾出了空间,也改动了每次请求最敏感的部分:前缀。
模型供应商的 prompt caching 通常依赖精确前缀匹配。压缩前,下一次请求大多只是沿着旧历史继续追加;压缩后,较早历史被一份新摘要替代。即使最近几轮一个字没变,它们前面的内容已经不同,旧缓存也无法从原位置继续复用。
失效不是永久的:压缩后的结构会成为新前缀,后续请求可以在它上面重新积累缓存。
这不是永久失去缓存。压缩后的结构会成为新的稳定前缀,后续请求又能在它上面继续积累缓存。这也解释了 Pi 为什么尽量在一轮结束后压缩:先让本轮连续请求享受旧前缀的缓存收益,再改写上下文。
压缩后能否直接开工
Pi 的取舍现在完整了:保留全部历史,会撞上上下文上限;压缩历史,会丢失细节并付出一次缓存重算;彻底新开会话,则连工作连续性也放弃。
判断一次 compaction 好不好,可以问四个问题:最近多少工作保持原样?摘要是否保留目标、决定、进度和下一步?结果能否被人检查并在模型之间迁移?触发时机是否在剩余空间、溢出风险和缓存成本之间取得合理平衡?
好的 compaction 不是“记住所有过去”。它是接受遗忘不可避免,然后尽量把真正影响下一步的工作状态交给下一棒。
Pi Compaction 一页看懂
长任务会把消息、代码、日志和工具结果不断带入下一次请求。Pi 不会清空一切,而是把较早历史改写成工作交接,保留最近内容继续推进。
为什么会出现 compacting context
模型每次回答都要重新读取当前上下文。历史只增不减,最终会逼近窗口上限;compaction 是在任务完成之前主动腾出空间。
重开会话与压缩历史
- 重开会话立即拿回全部空间,但目标、进度、决定和未解决问题一起离场。
- 压缩历史保住工作连续性,但旧细节会被概括,属于有损迁移。
Pi 实际怎样切
- 预留回答空间当 contextTokens 超过 contextWindow 减 reserveTokens 时触发;默认预留 16,384 tokens。
- 保留最近工作尽量让最近 20,000 tokens 原样进入新请求;这是预算,不是固定轮数。
- 概括较早历史切线以前的内容被序列化并写成结构化摘要。
- 继续当前任务新请求由 system、tools、summary、recent turns 和 new user 组成。
摘要不是聊天梗概,而是工作交接
- 目标最终要完成什么
- 限制与偏好不能违反什么
- 进度完成、进行中与阻塞
- 关键决定选择及其理由
- 下一步接下来怎样推进
- 关键上下文路径、函数和报错
换来的空间并不免费
- 细节损失摘要器没识别到的信息可能消失,反复压缩后尤其明显。
- 缓存重算旧历史换成新摘要后,精确前缀发生变化,旧 prompt cache 暂时无法命中。
- 实现不通用16,384 与 20,000 是 Pi 当前可配置默认值,不能外推到其他 coding agent。
判断 compaction 好不好
看它是否保住最近工作、目标、决定、进度和下一步,是否能被人检查并跨模型迁移,以及触发时机是否平衡了剩余空间、溢出风险和缓存成本。好的 compaction 不是记住所有过去,而是把真正影响下一步的工作状态交给下一棒。
一次长任务,怎样把工作现场交给下一棒
开发者查一下 checkout 为什么返回 502。
Pi我收到的不只这一句话,还包括整个当前工作现场。
Agent 的连续性来自每次重放上下文,不是隐藏记忆。
窗口任务还没结束,我却快装满了。
每一步都合理,累计起来却会撞上容量上限。
Pi重开能拿回全部空间,却会丢掉工作连续性;我选择把旧历史改写得更短。
Compaction 是有损的工作状态迁移,不是可无损还原的压缩包。
摘要器我把较早历史整理成结构化 checkpoint。
最近消息我按原顺序继续进入新请求。
Pi 默认给回答预留 16,384 tokens,并尽量保留最近 20,000 tokens;两者都是可配置预算。
上一棒目标、限制、进度、决定、下一步和关键路径都在这里。
下一棒我不需要重演全部对话,也能接着开工。
保留下来的不是逐字历史,而是可检查、可迁移的工作状态。
Prompt cache最近消息虽然没变,开头变了,我就不能沿用旧命中。
缓存之后会在新前缀上重新积累;真正不能丢的规则仍应写进持久文件,而不是只依赖摘要。
