OpenCode DCP 插件是什么?拆解 Dynamic Context Pruning 的机制与取舍
用 OpenCode 跑一个稍微复杂点的任务——比如在一个不熟悉的代码库里修一个跨三个文件的 Bug——你会发现一个很常见的过程:前 20 轮顺风顺水,模型记得你之前说的每一句话;30 轮之后它开始重复读同一个文件;40 轮的时候它忘了你 10 轮前给的约束条件;然后某一轮,屏幕上闪过一段压缩摘要,模型说”根据之前的讨论……”——你知道它已经把你之前说的大部分东西都忘了。
这就是上下文窗口的限制。DCP(Dynamic Context Pruning)插件想用一种思路来缓解这个问题。
先说 OpenCode 自己是怎么处理这件事的
要理解 DCP 在做什么,得先知道 OpenCode 原生的上下文管理有多”简单粗暴”——不是贬义,是真的简单。
OpenCode 的上下文处理分两层。第一层是裁剪工具输出:从旧到新扫一遍工具调用的返回值,最近约 4 万 token 的输出不动,更老的替换成占位符。只动工具输出不动消息本身,影响最小,但能省出的空间也有限。
第二层是全量压缩(Compaction)。当 token 用量逼近窗口上限(已用 > 上限 - 2万),OpenCode 会调用模型,让它把整个对话历史浓缩成一份结构化摘要——目标是什么、做了什么、下一步干什么——然后从这份摘要重新开始。
"compaction": {
"auto": true,
"keep.tokens": 15000,
"buffer": 20000
}
一共三个配置项。
但它的问题也正在于这种简单。
它只在快满的时候才压缩。 上下文从 10% 涨到 90%,在这段过程中,模型一直在一堆越来越陈旧的信息里找你最新说的那句话。等到终于触发 Compaction 的时候,上下文已经膨胀到模型注意力最分散的程度——偏偏这时候你要它写出一份高质量的摘要。
它是全量的。 压缩不做区分,整个历史都变成一份摘要。你 5 轮前精心写的一段需求描述和 30 轮前的一次无用的 ls 输出,被同样对待。它做不到”这块任务做完了就压缩掉,正在做的那块保留”。
嵌套压缩会稀释信息。 长会话中 Compaction 可能触发多次。对一份已经是摘要的内容再做摘要,细节不可避免地逐层丢失。这件事听起来理所当然,但当模型在第 60 轮需要引用第 5 轮的一个具体错误信息时,那条信息早已被摘要了两三次,剩下的只有”之前遇到过一个相关的错误”这种程度的模糊描述。
DCP 的核心想法:让模型自己来
DCP 的思路说穿了就一句话——既然模型最清楚哪些上下文还有用、哪些已经没用,那干脆让模型自己来决定什么时候压缩、压缩哪些内容。
它通过 OpenCode 的插件系统工作。OpenCode 的插件可以在请求发给 LLM 之前通过 hook 拦截和修改消息列表——DCP 就是利用这个机制,在发请求前把它认为可以压缩的内容替换成占位符。你本地保存的会话历史不会被改动。
安装就一行(OpenCode v2 的写法,会自动加到全局配置里):
opencode plugin add @tarquinen/opencode-dcp@latest
还在用 v1 的话,命令是 opencode plugin @tarquinen/opencode-dcp@latest --global。
装完之后,模型的工具列表里多了一个叫 compress 的工具。接下来,就是让模型在合适的时机自己调用它。
Compress:不是等窗口满了再动手
想象你在做一个分三步的任务:先探索代码结构,再修改实现,最后写测试。在原生 Compaction 下,这三个阶段的所有细节都堆在上下文里,直到窗口满了一刀切。
DCP 的做法不同。当模型完成了代码探索阶段,它可以主动调用 compress,把探索过程中读过的文件内容、翻过的目录结构、走过的弯路全部压缩成一份技术摘要——“代码库使用 monorepo 结构,auth 模块在 packages/auth,依赖 Redis 做 session 存储……”——然后轻装上阵进入实现阶段。
Compress 有两种模式。Range 模式是默认的,压缩一段连续的消息范围。值得一提的是嵌套处理:如果这次压缩的范围覆盖了之前已经压缩过的内容,旧摘要会嵌套进新摘要里,而不是被丢弃重写。信息是分层保留的——第一层摘要保留了原始细节的 80%,第二层保留了第一层的 80%——递减但不是归零。这比每次都从头概括的全量压缩要好得多。
还有一个实验性的 Message 模式,可以精确到对单条消息做压缩。比如一次 cat 输出了一个 500 行的配置文件,你可以只压缩这一条,不影响周围的消息。精度很高,但目前还标注为 experimental。
当然,不是什么都能压缩。DCP 有几种保护机制:write、edit 这类修改文件的工具输出默认不能压缩(你总不希望模型忘了自己刚写了什么);可以开启 turnProtection(默认关闭)保护最近 4 轮的内容;用户消息可以选择性保护。这些保护是有道理的:你肯定不希望模型把 3 轮前你下的指令压缩掉。
Deduplication:同一个文件读了三遍,只留最后一遍
长会话里有个很常见的浪费:模型读了一个文件,改完之后又读一遍确认结果,过了 10 轮又读了一次。三次读取的参数完全相同(或者文件内容没变,只是读取的位置不同),三份完整内容却都留在上下文里占着空间。
DCP 的去重逻辑很直接——识别参数完全相同的重复工具调用,只保留最近一次的输出。为了减少对 Prompt Cache 的影响,去重只在 compress 运行时顺带执行,不会单独触发。
Purge Errors:清理失败调用的废弃输入
这个功能针对的是一个不太显眼、但确实存在的问题。工具调用失败的时候,错误信息通常就一行——“permission denied”、“file not found”。但调用本身的输入可能很大,比如一整段要写入文件的代码。错误信息有价值(模型需要知道哪里失败了),但那段写入失败的代码还留在上下文里,就是纯粹的浪费。
DCP 在错误发生 4 轮后自动清理这些废弃输入,只保留错误消息本身。一次两次看不出差别,但在反复试错的调试会话中,这些废弃输入累积起来可以有几万 token。
模型不主动压缩怎么办?Nudge 系统
把压缩权交给模型听起来很好,但实际上,模型有时候不会主动去做。它可能忙着写代码,根本没”想起来”要调用 compress,上下文就这么悄悄涨上去了。
DCP 用 nudge(提醒)来解决这个问题。它在三个时机往系统提示里注入提醒文本:
- 上下文超过 10 万 token(
maxContextLimit,默认 100000)后,每隔 5 次 API 请求提醒一次(nudgeFrequency,默认 5) - 距上次用户消息超过一定轮数后提醒(上下文需达到
minContextLimit,默认 5 万 token,这类提醒才会开启) - 模型连续自主运行超过 15 轮没有用户输入时提醒(同样受
minContextLimit控制)
提醒的强度由 compress.nudgeForce 控制,可以配成 "soft" 或 "strong"。但注意——这些只是提醒,不是强制。模型收到提醒后可以选择现在不压缩继续干活。这个设计是对的,因为有时候模型正在执行一系列步骤的中途,这时候打断去压缩反而影响工作质量。
(顺带一提,这些提醒的文本是可以自定义的。开启 experimental.customPrompts: true,DCP 会在 ~/.config/opencode/dcp-prompts/ 生成默认的提示词文件,你可以在 overrides/ 目录下放同名文件来覆盖。如果你觉得 DCP 压缩得太激进或太保守,先试试调提示词,比改配置参数更直接。)
所有人都在问的问题:Prompt Cache 怎么办?
每次聊到上下文压缩,都绕不开 Prompt Cache。原因很简单——LLM 的 Prompt Cache 基于逐字节前缀匹配,你改了中间任何一条消息的内容,从那个位置往后的缓存全部作废。DCP 压缩消息,等于在改中间的内容。
这是一个真实的代价。DCP 作者给出的测试数据:启用 DCP 后缓存命中率约 85%,不启用约 90%。
但这个对比要放在更大的背景里看。
一方面,缓存 miss 意味着你多付了一些缓存重建的成本。另一方面,DCP 声称能把长会话的整体 token 用量降低 50-70%。如果你的上下文从 20 万 token 被压缩到 8 万,即使这 8 万全部重新缓存一次,你省下的 12 万 token 的输入成本也远超缓存 miss 的代价。
不过这笔账并非在所有场景下都成立:
DCP 明显划算的情况: 按 token 计费的 API 用户、上下文已经很长、使用按请求计费的提供商(账单不随 token 数变化,cache miss 不会多收钱),或者缓存与非缓存 token 同价计费的提供商。这些场景下 DCP 省 token 基本是净收益,cache miss 的代价很小或没有。
需要犹豫的情况: 短会话(DCP 还没省出多少就先 miss 了一次)、使用 1 小时缓存 TTL 的请求(Anthropic API 需要在 cache_control 里显式设置 ttl: "1h",这种缓存写入价比 5 分钟缓存高,一次 miss 的重建成本也更高)、或者你只用模型做几轮问答而不是跑长任务。
Hacker News 上有人对此提出过更尖锐的批评:DCP 展示了 token 缩减的数据,但没有做过系统性的 eval 来证明”压缩后模型输出质量不受影响”。省了 token 但模型变笨了,这样换值不值?老实说,这个问题不只存在于 DCP,所有上下文压缩方案都有这个盲区。
和 OpenCode 原生方案的关系
两者不是二选一。DCP 运行在 Compaction 之前。如果 DCP 持续压缩把上下文控制在合理范围内,原生 Compaction 可能永远不会触发。但如果 DCP 没能及时压缩(模型没理会 nudge、或者任务确实需要那么多上下文),原生 Compaction 仍然起兜底作用。
打个不太精确的比方:原生 Compaction 类似 OOM Killer,等内存快耗尽时才出手,直接杀掉占用最多的进程。DCP 则像你自己写的内存管理代码,在合适的时候主动释放不再需要的对象。两者可以并存,你的目标是让 DCP 足够好,使 Compaction 用不上。
一些值得注意的细节
DCP 的开发重心已经转移。 作者把新功能放到了 Sleev——一个本地代理,同时支持 Claude Code 和 OpenCode。DCP 本身还维护,但不要指望它会频繁加入新功能。如果你在意长期演进方向,关注 Sleev 可能更值得。
已知有一个阈值 Bug。 有用户在 3.1.9 版本报告(GitHub issue #548):minCompressionThreshold 配成 15% 后,上下文占用只有约 4% 时仍触发了压缩。报告者猜测是系统内部逻辑绕过了配置,这个 issue 目前仍是 open 状态,根因未确认。当前版本的 README 和源码里已经没有 minCompressionThreshold,阈值相关的配置是 compress.minContextLimit 和 compress.maxContextLimit,新版本上是否还有这个问题需要自己验证。遇到类似问题的话,可以先看一下 GitHub Issues。
配置项非常多。 我前面只挑了关键的几个讲。DCP 的完整配置包括 20 多个参数,支持按模型设置不同阈值、保护特定文件模式、保护特定工具输出等等。如果你要认真用,直接看 GitHub README 的内容会比这篇文章更新。
该不该装
最后给一个简单的判断框架。
如果你的 OpenCode 使用场景是长任务、多阶段、按 token 计费,DCP 值得一试。它装上之后不能就不管了——你需要花时间调 maxContextLimit 和 minContextLimit 来匹配你常用模型的上下文大小,可能还需要调 nudge 的激进程度。但调好之后,长会话的体验和成本确实能有明显改善。
如果你的使用场景是短对话、快速问答、或者用订阅账号不太关心 token 成本,OpenCode 原生的 Compaction 已经够用了。多装一个插件、多 20 个配置项、多一层压缩逻辑——这些复杂度未必能换来你感受得到的收益。
参考:Opencode-DCP/opencode-dynamic-context-pruning · OpenCode Compaction 文档 · OpenCode 插件系统 · Context Compaction Research