潜水 10 年:)
可以先把话撂在这里,acp 这个插件出现其他上下文压缩方式就可以谢幕了,甚至尝试继续扩大模型上下文的行为也变得无意义。
先说说上下文压缩插件 acp 是啥,这是一个 opencode 中的插件。为什么需要上下文压缩?用过 AI coding 的都知道,模型上下文都是有限的,哪怕你有 100w 上下文也无济于事,面对旷日持久的大项目也捉襟见肘(尤其是可怜的 glm5.1 啊啊啊)。
所以 gpt 和你聊天实际上是假聊天,你和他每次说的消息,包括他的输出,都作为上下文,每次都全部发给 gpt 处理,久而久之你们就攒满几十万甚至 100w 上下文,就要删去一些。
作为程序员,显而易见想到的方式就是把上下文提炼总结,替换掉原会话冗余的上下文。现在大部分上下文压缩都是这么做的,只不过有一些做的很粗糙(大部分),有的很精细(比如 claude 多级级联),有一些还甚至想出做向量存储相关性搜索等等。
不过最终面临的问题还是压缩效果不好的问题。
想法很简单,实现很简单:模型自己决定要不要压缩,怎么压缩
说起来一肚子气,本来这个插件是要提给原作者dcp的,我修了几十个 bug ,增加了类似 jvm 的垃圾回收算法,结果他看都不看全给我关了!!!
废话不多说先看看效果:最近上下文几乎都没有超过 50%!
模型 GLM-5.1 ( 204K context window ),ACP 阈值 55%。
指标 修复前 (5/14 - 5/19) 修复后 (5/19 - 5/20) 采样数 2,749 362 峰值 context 74.2% 45.3% 平均 context 35.9% 25.3% context <40% 的占比 64.5% 87.0% context >55% 的次数 130 次 (4.7%) 0 次 (0%)主:修复前之前还有,再之前会话超过几百条就上下文丢干净了。
这是最近几天这个会话的 token
指标 数值 总 Input Tokens 35,232,595(约 3520 万) 总 Output Tokens 616,525(约 62 万) 总 Tokens (输入+输出) 约 35,849,120(约 3585 万) 消息总数 3,762 条 会话时长 约 6 天( 5/14 - 5/20 ) 模型 GLM-5.1 ( 204,800 token 上下文窗口) ACP 压缩阈值 55%( 112,640 tokens )原理:把上下文压缩当做一个 skill
是的,就这么简单。你不需要像 claude 一样搞多级压缩(都是人工定死的,哪有模型灵活?)也不需要搞外部 api 专门压缩(外部 api 才不了解你需要哪些信息)。也不需要专门训练模型,只需要交给模型一个 skill ,他自己决定什么时候调用即可。
重点说明一些基本特性:
- 每条消息模型都可以压缩,或者解压缩(这很重要)
- 模型可以把连续 N 条消息压缩成 1 条消息。
- 模型可以删除消息。
- 模型可以修改消息。
系统做什么?
- 在模型启动的时候告诉模型可以压缩
- 在上下文 45%的时候提示模型应该压缩
- 在上下文 55%的时候提示模型必须压缩
仅此而已。
最后说说我改了啥?和原生 dcp 有啥区别
原始 DCP 有个致命问题:上下文状态全靠 msgId 列表追踪,但这些 ID 不持久化,一重启就丢,35 个压缩块全部作废,559K 字符的摘要变成废纸,3175 条原始消息全部涌回上下文,GLM-5.1 直接返回 model_context_window_exceeded。
我从头重写了核心架构,主要改了这些:
1. 独立 Block 架构 — 不再有巨型摘要
DCP 原始设计有个脑残的地方:每次压缩会把旧 block 的摘要内联展开到新 block 中。打个比方,就像你每次整理抽屉,都把之前所有整理记录的完整版塞进新记录里。经过 23 次压缩后,最新 block 的摘要膨胀到 90K 字符 — 整个会话历史的递归摘要。这个巨型摘要无法再压缩(它本身就是摘要),占据了上下文的大部分空间,会话卡在 70% context 无法继续。
ACP 改成独立 Block 架构:每个 block 独立存在,摘要只覆盖自己的范围。多个 active block 在上下文中同时存在。不再自动嵌套,(bN) 引用保留为文本标签。模型需要显式用 bN 作为 boundary 才会消费旧 block 。
2. 压缩块状态机(类似 JVM 分代 GC )
每个压缩块有 young → old 代的概念。新压缩的块是 young generation ,经历多次压缩周期后晋升为 old generation 。Old generation 的摘要会被 GC 自动截断(保留头部 + 尾部引用标记),防止老摘要无限膨胀吃掉上下文。
JVM GC ACP 触发条件 Young Gen 新创建的压缩块 每次压缩产生 Minor GC 合并最近的 young 块 55% 阈值触发 Old Gen 存活超过 N 次压缩周期的块 survivedCount ≥ 5 自动晋升 Major GC 截断 old-gen 块摘要 超过阈值自动执行 Age-based deactivation 超龄块自动停用 age > 15实测效果:126 个 active blocks ( 63K tokens 死重)→ GC 自动清理到 10 个。
3. 34 个 Bug 修复
fork 以来修了 34 个 bug( 4 CRITICAL ,15 HIGH )。每个都是真实踩到的坑,不是坐着想出来的:
最离谱的几个:
-
状态不持久化:重启后所有压缩块丢失,几千条原始消息涌回上下文,API 直接返回
model_context_window_exceeded,会话当场暴毙。这是 DCP 最大的坑,我反复踩了 N 次。 -
prune summary 静默丢失:遇到一种边界情况,原始消息被删了但摘要没注入进去 — 也就是数据直接丢了,不是压缩质量不好,是真的没了。
-
每轮 20-50 秒延迟:DCP 的 Logger 在 debug=false 时仍然执行
new Error()+Error.prepareStackTrace来获取调用栈,每次 50-100ms 。syncToolCache 对每个 tool call 调一次 logger ,500 个 tool × 100ms = 50 秒。用户以为模型在思考,实际上是 DCP 在那做无用的堆栈追踪。( PS:大概原生 dcp 上下文没这么大过,所以不会复现这个 bug 吧哈哈) -
阈值计算错误:DCP 用
inputBudget(= context limit - output limit = 73K )代替context limit( 204K )算百分比,导致 36% 就触发 CRITICAL WARNING ,模型疯狂压缩一个根本没满的上下文。 -
压缩完模型罢工:compress 工具返回后,模型说"压缩完成,接下来你想做什么?"然后停下来。正在执行的多步任务被直接中断。
-
前缀缓存被打破:DCP 把动态数据注入到对话中间的锚定消息里,GLM-5.1 的 cache 是前缀匹配,锚定消息内容每轮变化 → 后面所有内容都变成 cache miss ,命中率从 99% 持续下降到 82%。
-
npm 静默覆盖:opencode 自动安装 npm 原版 DCP ,原版缺少我的 bug fix ,加载会话时清空所有压缩块,1866 条消息未压缩直接发送。(好吧,这个不是 bug ,现在我改名了,再也不会有这个困扰了)
完整的 bug 列表太多了就不贴了,感兴趣的看 GitHub 。
增加了 300 个测试
凸(艹皿艹 ),原来 dcp bug 那么多,总计 15 个测试只有 5 个能跑,让 glm5.1 帮我写了 300 个基准测试,
竞品对比
ACP DCP 原版 Morph opencode 内置 压缩方式 模型自己决定 模型自己决定 外部专用压缩 API 被动全量摘要 额外 LLM 调用 无 idle 分析可选(费 token ) 需要 API Key 1 次摘要调用 触发时机 45% 建议,55% 必须 可配置 70% 95%(基本已经来不及了)PS:实际上 glm5.1 本身就是压缩大师了:)。
安装
opencode plugin opencode-acp@latest --global
或者
{
"plugin": ["opencode-acp@latest"]
}
配置文件 acp.jsonc:
{
"maxContextLimit": "55%", // 触发压缩的阈值
"gc": {
"enabled": true,
"interval": 300 // 每 5 分钟 GC 检查一次
}
}
链接
吐槽归吐槽,DCP 原作者的设计思路我是认可的(好吧,这句话是模型给我加的,原作者思路是对的,只不过并没有发挥到登峰造极) — 让模型自己决定压缩,而不是搞一堆规则和外部 API 。只是在工程实现上踩了太多坑,我花了几周把这些坑都填上了。
如果你也在用 opencode 做大项目,试试看。 几周 session 不用重开。
遇到 bug 就提 github 吧,我自己高频用,应该还会发现很多 bug 。你发现了在 issue 说,直接提 pr 即可。