上下文超长时,绝大多数项目的处理方式是往提示词里加一句「请忽略无关内容」。这既不省 token 也不可控。真正的压缩发生在消息模型上,不是措辞上——pi-agent-core 把它拆成两个明确的函数,一个负责过滤,一个负责裁剪。
20 轮会话,每轮夹一条只用于界面渲染的 UI 消息。对比「只做过滤」与「过滤加剪枝」两种配置下,每一轮真实送进模型上下文的字符数。
AgentMessage 不是 LLM Message
Agent 内部维护的消息列表,比模型能看的多。除了标准的 user、assistant、toolResult,它还可以装应用自己定义的类型:渲染用的卡片、审计记录、用户点了哪个按钮、某次重试的标记。
模型只认三种角色。所以每次请求前有一个转换步骤,把 AgentMessage 数组变成 LLM Message 数组。这个转换是必经的,而且必须由你提供——因为它决定「哪些东西不该给模型看」。
链路是两段:transformContext 先裁剪和注入,convertToLlm 再过滤和转换。前者管长度,后者管可见性。分开的好处是职责清晰:长度策略可以换,可见性规则保持稳定。
两个函数,两种职责
const agent = new Agent({
// 管可见性:UI 消息永远不进模型
convertToLlm: (messages) => messages.filter((m) => m.type !== 'ui_note'),
// 管长度:留最近 8 条 + 一条滚动摘要
transformContext: async (messages) => {
if (messages.length <= 8) return messages
const dropped = messages.slice(0, messages.length - 8)
return [
{ role: 'user', content: [{ type: 'text', text: `[摘要] 前 ${dropped.length} 条已折叠` }] },
...messages.slice(-8),
]
},
})
注意:不要把这两件事混在一起做。见过把过滤逻辑塞进 transformContext 的项目,结果是压缩时把工具结果也一起裁掉了,模型突然不知道自己刚才调过什么。职责一旦混,bug 极难定位。
不压缩的代价是线性的
跑 20 轮,不做任何裁剪,送进模型上下文的字符数从第一轮的 275 涨到最后一轮的 9355,几乎是一条完美的直线。每轮增加约 450 字符,取决于单轮内容长度。
这个增长是必然的:每一轮的助手回复和工具结果都要留在上下文里,模型才能连贯。但线性增长意味着成本也线性增长,而且是在整个会话生命周期里持续放大。
加上 transformContext 之后,曲线立刻变成了平的:稳定在 1170 字符左右,只有微小抖动来自摘要文本长度。第 20 轮省下 87.5%。

20 轮会话的上下文长度(每两轮采样一次):不裁剪是直线增长,剪枝后稳定在 1173 字符附近。最后一轮差距 8182 字符。
还有一个副作用值得注意:convertToLlm 同时挡住了 UI 消息。实验里每轮插一条渲染卡片消息,如果过滤器失效,字符数会明显跳高。最终数字里没有这部分,说明过滤确实生效了。
这件事在真实项目里是隐性成本。界面上要展示的东西越多,塞进消息列表的越多,如果不做过滤,模型的上下文里会充斥着它完全不需要的展示数据。
剪枝策略怎么选
最简单的策略是留最近 N 条,加上一条摘要。它在实验里就把增长压平了,实现成本也最低。缺点是丢掉的信息无法恢复——如果模型后面需要引用早期结论,它只能重新查一遍。
更好的策略是分层:最近 8 条原文保留,8 到 30 条压成摘要,30 条以上只留主题标签。这样近处精确、远处模糊,符合模型的注意力分布。
更激进的策略是任务级重置:一个子任务结束就把中间过程全丢掉,只留结论。适合流程清晰、子任务边界明显的场景。代价是模型会失去「刚才是怎么得出这个结论的」这层上下文,出错时更难自纠。
无论选哪种,有一条硬规则:toolResult 和它对应的 toolCall 不能被拆散。只留结果不留调用,模型会不知道这个结果是谁产生的;只留调用不留结果,它会以为工具还在跑。
三种剪枝策略的取舍
| 策略 | 实现成本 | 信息损失 | 适用场景 |
|---|---|---|---|
| 最近 N 条 + 摘要 | 低 | 中 | 通用会话 |
| 分层保留 | 中 | 低 | 长任务、需要回溯 |
| 任务级重置 | 低 | 高 | 子任务边界清晰 |
为什么不能靠提示词压缩
往系统提示里写「请忽略无关内容」有几个硬伤。第一,token 已经付了——上下文超限报错不会因为你说了这句话就消失。第二,模型没有权限删消息,它只能“假装没看见”,而“假装”在长上下文里极不稳定。第三,不可观测:你不知道它忽略了什么,也不知道它有没有忽略该保留的。
把压缩做在消息层,这三个问题同时消失:token 真的少了、删除是显式的、删了什么可以记录。
这也是一个通用的判断方法:任何“让模型自己注意点”的需求,先问能不能变成接口层的强制约束。能变成约束的,就不要交给注意力。
上下文管住了,下一个问题是权限。模型有了 bash,它能做任何事——包括你不希望它做的事。第 7 篇讲钩子:为什么管控必须写在 beforeToolCall 里,而不是写在提示词里。
引用与来源
- pi-agent-core:AgentMessage 与消息流 — transformContext 与 convertToLlm 顺序
- DeepWiki:Agent Loop 与上下文处理 — 核对 2026-10-31






评论前必须登录!
立即登录 注册