你和 AI 编程助手聊了 30 轮。突然,它报错了。不是你的代码有问题,是对话历史太长,token 超过了上下文窗口限制。
这是所有 LLM 编程助手都必须面对的问题。Earendil 官方博客最近发表了一篇技术文章,详细解释他们的编码助手 Pi 是如何处理这个问题的——通过一个叫「compaction」的机制。
两个选择,都不完美
当上下文窗口满了,你只有两条路。
第一条:扔掉所有历史,重新开始。简单粗暴,但你会丢失之前的所有决策、修复记录和未完成的工作。而且 LLM 的输出质量会随着上下文增长而下降——这个现象被称为「context rot」。
第二条:压缩。把一部分对话历史替换成摘要,腾出空间给新消息。Pi 选择的是这条路。
Pi 的 compaction 怎么工作
触发时机有三:每次对话回合结束后,Pi 检查上下文是否接近 token 限制(自动触发);用户可以手动/compact;如果中途遇到上下文溢出错误,也会触发紧急压缩。
关键设计是:不是压缩全部历史,而是保留最近的一段对话不动,只压缩更早的部分。
默认保留 20,000 token 的「最近消息」不压缩,换算下来大约 5-20 个回合。这个阈值是可配置的。
request 1:
[system][tools][user]
after request 1:
[system][tools][user][assistant: tool call][tool result][assistant]
<-------------------> ^ <--------->
returned by LLM | returned by LLM
|
produced by the agent
request 2:
[system][tools][user][assistant: tool call][tool result][assistant][user]
^
new user message
[system][tools][user][assistant][....][tool result][user]
^
exceeds context window
压缩本身是一个完全独立的 LLM 请求。它不是把当前对话的 system prompt 拿来用,而是换了一套专门的 prompt——告诉 LLM 你现在是「上下文摘要助手」,而不是编程助手。要求输出按「目标、进展、关键决策」三段组织的结构化摘要。
这个设计的巧妙之处在于:因为压缩请求是独立的,它可以用一个不同的模型来跑,不需要增加主对话模型的成本。你想用便宜的模型做摘要、用贵的模型写代码,完全可行。
摘要以纯文本形式存储在会话中,这意味着它跨模型迁移时不会有格式兼容问题。
Prompt Cache 的代价
Compaction 有一个副作用:它会破坏 prompt cache。
Prompt cache 的工作原理是复用之前计算过的上下文前缀。Compaction 把一段历史替换成了摘要,前缀变了,缓存就失效了。但好处是 compaction 之后的请求又会重新享受缓存加速——因为前缀又稳定了。
和 Claude Code 的 compaction 有什么不同?
熟悉 Claude Code 的用户可能会注意到差异。Claude Code 的 compaction 是把整个对话——包括工具调用结果——压缩成一段摘要,然后继续。Pi 的做法更细粒度:保留最近 N 轮对话原封不动,只压缩更早的部分。
两种策略各有优劣。Pi 的方案在「保留上下文连续性」和「释放 token 空间」之间做了一个折中。Claude Code 的方案更激进,但压缩后的摘要可能丢失工具调用的细节。
可扩展的设计
Earendil 强调 Pi 的设计原则是「extensible and malleable」——用户可以替换 compaction 机制。如果你觉得默认的摘要 prompt 不够好,可以写一个扩展,用自己的 prompt 来生成摘要。
这种设计思路和 Pi 整体定位一致:它不是一个黑盒产品,而是一个你可以修改和定制的工具。
参考来源: