dsh-command-context-trim:从一次断网开始的插件

一切的一切始于那场 DeepSeek API 断网故障。

DeepSeek API 断网那天,一开始我并不慌。家里服务器上跑着两张卡,本地 Qwen3.8:27b 随便用。但是,当把模型从云端 DeepSeek 切换回本地之后,我开始慌了——会话中已有的上下文超出了本地模型的上限。

DeepSeek Harness 上下文压缩机制是通过大模型来概括精简上下文,但是云端大模型不可用,上下文压缩也就不可用。就在这时,一直在角落里默不作声的 Grok 提议:“你需要的并不是上下文压缩,而是上下文裁剪。不需要大模型参与,只需要剪掉超出限额部分的上下文就好”。于是,dsh-command-context-trim 插件项目就这样开始了。

插件最一开始的功能很简单,不靠大模型,只靠脚本搜索上下文,保留头部的系统提示和工具集,从最古老的对话部分开始,将对话成对剪掉(一则用户消息和一串模型回复为一对)。原则上保留最后一对对话,这样可以保证可以继续会话内容。

就这样度过了 DeepSeek API 的断网期。很快,迎来了 DSH v0.1.7-rc 更新。这一版更新为了配合 DeepSeek 云端 API,在上下文机制和计算上引入了头部预留(headroom),固定为 65536 tokens (64K)。本地模型的 128K 上下文顿时有一半不能用了。表现现象就是上下文还没填到 50% 就触发压缩(compaction)。于是,插件里面又添加了上下文计算调优功能。

总算解决了上下文计算,新的问题又出现了—— DSH内置的上下文修剪(prune)功能会将上下文中最后一组会话剪掉! 表现出来的现象是,模型先读进一个文件 -> 文件太大触发修剪(prune)-> 刚读进来的代码被修剪掉 -> 模型再次读入同一个文件…… 如此循环。于是,插件中又多了上下文修剪调优功能。

DSH v0.1.7-rc 引入的另外一个大规模更新就是独立的插件设置页,对插件设置页的制作提出了规范化的要求。这是好事。于是,我又花了几天时间,指导AI适配新的插件设置页。

终于,在 DSH v0.2.0-rc.2 上线之际,插件的更新完成了!

(那之后插件一直在更新,现在是 0.6.x。下面讲它现在的样子。)

它现在做什么

一、/trim:免模型裁剪。 把最旧、最不值钱的那一段上下文直接丢掉,不调用模型、不产生摘要。裁剪按从旧到新的优先级进行,最后一条消息降级为“尽量不裁”,用户指令始终受保护。

二、撞墙时自动裁剪。 一个 prepend 的 agent/request-error 监听器响应 CONTEXT_WINDOW_EXCEEDED,先免模型地腾出空间,再请循环重试;实在腾不出空间,才让 DSH 自己的恢复路径(剪枝 + 摘要)接手。它只在撞墙时触发,不在普通压缩时触发——所以它不会打扰正常运行。

三、调优上下文计算。 这是上面那条「头部预留」问题的解法。DSH 的压缩触发点是:

min(thresholdRatio × contextWindow,  contextWindow − R − headroom)

headroom 固定 65536,与窗口大小无关。正是它把 128K 路由的触发点从比例想要的 80% 压到了 37.5%:

min(0.8 × 131072,  131072 − 16384 − 65536) = min(104857, 49152) = 49152   ← 37.5%

插件把这条路由的 headroom 设成 0,让比例重新说了算:

min(0.8 × 131072,  131072 − 16384 − 0) = 104857   ← 80%

同一组参数,效果是可用上下文 +55705 tokens(2.1 倍)。

四、调优剪枝阈值。 就是上面那条「读完文件就被剪掉」问题的解法。工具结果的剪枝阈值按路由派生,而不是用 DSH 固定的 8192 字符:

max(8192, min(32768, 2 × (contextWindow − maxTokens)))

一句话概括整个插件的判断:能不摘要就不摘要——免模型的裁剪先上,实在不行才让模型去总结。

实测

同一条路由,只改 headroom 这一个变量:

路由 窗口 输出预留 DSH 默认触发点 调优后
Qwen3.8:27b @ Intel Arc B70 131072 16384 49152 — 37.5 % 104857 — 80.0 %
Bonsai 2 @ RTX 5060 8GB 40960 8192 无主动触发路径 32768 — 80.0 %
deepseek-official(云) 1000000 256000 678464 — 67.8 % 744000 — 74.4 %

最后一行值得看一眼:同样的参数,在 1M 窗口的云路由上几乎不动(67.8 % → 74.4 %),在 128K 上却是翻倍。这个差异完全来自窗口大小和那个固定 headroom 的比例关系,不是模型的问题。

而压缩本身是要调一次模型的,这一调用还会卡住整个 turn。我们用本地 summarizer 实测过,一次 209 到 372 秒。所以触发点每往后推一档,省下的不只是 token,是每隔多久你要看它干等一次。

装与用

# 从 npm
dsh plugin --profile web add dsh-command-context-trim

headless / tui:host 平面拥有 agent,自动路径会触发,两个环境变量即可。

export DSH_TRIM_AUTO_TUNE=1     # 每次启动按当前路由重新调优触发点
export DSH_TRIM_PRUNER=1        # 按路由窗口派生工具结果剪枝阈值

web:会话的 agent 活在自己的 preset 域里,生命周期事件到不了 host 平面,所以调优参数要注入 preset。

/context-tune preset inplace     # 覆盖基础 preset 自己的 id,不用去新建会话界面挑

执行完需要重启 dsh 才生效。这里有个反直觉的地方:会话一旦开始,它的 preset id 就锁死了(agent-preset/locked),fork 也继承父会话被同样锁死——但锁的是 id,不是 id 背后的定义,那个定义在每次 adopt / resume 时都会重新组合。所以新会话和重启后的会话直接吃到新参数,正在跑的那个要重启。

想撤销:

/context-tune reset

它只删本插件写进补丁的那些标记块,别的工具写的块会被列出来但不动。

已知限制

小窗口路由(消息预算 ≤ 65536)的调优参数仍在调整中——这条路径上「多压缩」和「少压缩」的取舍还没有定论,欢迎带上实测数据来 issue 里讨论。另外 /trim 与 /context-tune 的完整参数、受保护内容规则和 preset 锁定语义都在 中文 README 里。


MIT 许可。

添加新评论