聊聊 opencode 上下文压缩: 如何做到单会话 1 亿 token 不爆不丢,acp 模型主动压缩才是唯一正道 - V2EX
请不要在回答技术问题时复制粘贴 AI 生成的内容
ranxianglei

聊聊 opencode 上下文压缩: 如何做到单会话 1 亿 token 不爆不丢,acp 模型主动压缩才是唯一正道

  •  
  •   ranxianglei 5h 24m ago 348 views

    潜水 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 ,他自己决定什么时候调用即可。

    重点说明一些基本特性:

    1. 每条消息模型都可以压缩,或者解压缩(这很重要)
    2. 模型可以把连续 N 条消息压缩成 1 条消息。
    3. 模型可以删除消息。
    4. 模型可以修改消息。

    系统做什么?

    • 在模型启动的时候告诉模型可以压缩
    • 在上下文 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 即可。

    2 replies    2026-05-20 07:02:45 +08:00
    Tmss2
        1
    Tmss2  
       4h 54m ago via Android
    我看了一下 dcp 作者回你的消息:pr 太庞大,想让你拆分一下。人还是看了的呀,还提了一些 pr 的意见。
    win8en
        2
    win8en  
       52 mins ago via Android
    @Tmss2 是的,我正想说呢,人家确实看了
    About     Help     Advertise     Blog     API     FAQ     Solana     1397 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 42ms UTC 23:55 PVG 07:55 LAX 16:55 JFK 19:55
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86