<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[DSH本地模型的上下文救赎：dsh-command-context-trim 插件]]></title><description><![CDATA[<blockquote>
<p dir="auto">本文介绍用于DeepSeek Harness的上下文裁剪、调优插件 <a href="https://github.com/snailium/dsh-command-context-trim" rel="nofollow ugc">dsh-command-context-trim</a></p>
</blockquote>
<p dir="auto">一切的一切始于那场 DeepSeek API 断网故障。</p>
<p dir="auto">DeepSeek API 断网那天，一开始我并不慌。家里服务器上跑着两张卡，本地 Qwen3.8:27b 随便用。但是，当把模型从云端 DeepSeek 切换回本地之后，我开始慌了——会话中已有的上下文超出了本地模型的上限。</p>
<p dir="auto">DeepSeek Harness 上下文压缩机制是通过大模型来概括精简上下文，但是云端大模型不可用，上下文压缩也就不可用。就在这时，一直在角落里默不作声的 Grok 提议：“你需要的并不是上下文压缩，而是上下文裁剪。不需要大模型参与，只需要剪掉超出限额部分的上下文就好”。于是，<code>dsh-command-context-trim</code> 插件项目就这样开始了。</p>
<p dir="auto">插件最一开始的功能很简单，<strong>不靠大模型，只靠脚本搜索上下文，保留头部的系统提示和工具集，从最古老的对话部分开始，将对话成对剪掉</strong>（一则用户消息和一串模型回复为一对）。原则上保留最后一对对话，这样可以保证可以继续会话内容。</p>
<p dir="auto">就这样度过了 DeepSeek API 的断网期。很快，迎来了 <code>DSH v0.1.7-rc</code> 更新。这一版更新为了配合 DeepSeek 云端 API，在上下文机制和计算上引入了<strong>头部预留</strong>（roomhead），固定为 65536 tokens (64K)。本地模型的 128K 上下文顿时有一半不能用了。表现现象就是<strong>上下文还没填到 50% 就触发压缩（compaction）</strong>。于是，插件里面又添加了上下文计算调优功能。</p>
<p dir="auto">总算解决了上下文计算，新的问题又出现了—— <strong>DSH内置的上下文修剪（prune）功能会将上下文中最后一组会话剪掉！</strong> 表现出来的现象是，模型先读进一个文件 -&gt; 文件太大触发修剪（prune）-&gt; 刚读进来的代码被修剪掉 -&gt; 模型再次读入同一个文件…… 如此循环。于是，插件中又多了上下文修剪调优功能。</p>
<p dir="auto"><code>DSH v0.1.7-rc</code> 引入的另外一个大规模更新就是<strong>独立的插件设置页</strong>，对插件设置页的制作提出了规范化的要求。这时好事。于是，我又花了几天时间，指导AI适配新的插件设置页。</p>
<p dir="auto">终于，在 <code>DSH v0.2.0-rc2</code> 上线之际，插件的更新完成了！欢迎大家试用。插件的用法和详细的计算我让AI总结了一下贴在下面了</p>
<hr />
<h2>一、安装</h2>
<pre><code class="language-bash">dsh plugin --profile web add dsh-command-context-trim
</code></pre>
<p dir="auto">注意：最新版本只支持 <code>dsh v0.1.7-rc.2</code> 和 <code>dsh v0.2.0-rc.2</code></p>
<hr />
<h2>二、用法</h2>
<p dir="auto">调优分两条路线，<strong>headless 和 web 不一样</strong>，不是抄来抄去，是机制决定的。先说为什么。</p>
<p dir="auto">DSH 里的 compaction 有两个"平面"：<strong>profile 平面</strong>（headless / tui 走这条，agent 直接长在 host 平面里）和 <strong>preset 平面</strong>（web 走这条，每个会话的 agent 是在各自的 preset 域里创建的）。自动调优靠监听 agent 的生命周期事件，<strong>web 上这些事件到不了 host 平面</strong>，所以自动路径在 web 上不会触发——不是坏了，是那条路本身不通。</p>
<h3>headless：两个环境变量</h3>
<p dir="auto">host 平面拥有 agent，自动路径确实会触发。设两个环境变量就行（供 CI / 脚本覆盖用，不用改配置）：</p>
<pre><code class="language-bash">export DSH_TRIM_AUTO_TUNE=1     # 重新调优本进程的 compaction 行（触发点、headroom）
export DSH_TRIM_PRUNER=1        # 工具结果裁剪阈值，同样按路由窗口派生
</code></pre>
<p dir="auto">两个都默认关。开了之后<strong>每次启动都会重新调优</strong>，路由窗口变了也跟着变，不用手动干预。</p>
<blockquote>
<p dir="auto">还有第三个 <code>DSH_TRIM_TUNE_STOCK_DISABLED</code>，但<strong>别开</strong>——它是给小窗口路由用的，而我们实测过在小窗口上打开会让压缩从 15 次涨到 24 次（见下面「不划算」那节）。</p>
</blockquote>
<p dir="auto"><img src="https://lcz.me/assets/plugins/nodebb-plugin-emoji/emoji/android/26a0.png?v=fe25256ab4a" class="not-responsive emoji emoji-android emoji--warning" style="height:1.4em;width:auto;vertical-align:middle" title="⚠" alt="⚠" />️ 前提：<strong>这条路由上要声明 <code>contextWindow</code></strong>，否则派生不出窗口大小。我们是在干净 <code>DSH_HOME</code> 上实测过这条链路的。</p>
<h3>web：跑一遍 <code>/context-tune preset inplace</code></h3>
<p dir="auto">web 走 preset 平面，所以调优参数得<strong>注入到 preset 里</strong>去。做法是执行一次：</p>
<pre><code>/context-tune preset inplace
</code></pre>
<p dir="auto"><code>inplace</code> 的意思是<strong>覆盖基础 preset 自己的那个 id</strong>，而不是新增一个 id——所以你不用在新建会话界面上去挑，preset 名字和你之前用的完全一样。</p>
<p dir="auto"><strong>执行完需要重启 dsh 才生效。</strong> 这里有个反直觉的地方，值得单独说：</p>
<p dir="auto">会话一旦开始，它的 preset 就<strong>锁死</strong>了（DSH 报 <code>agent-preset/locked</code>），fork 也会继承父会话被同样锁死——所以你<strong>没法</strong>给一个跑着的会话换一套参数。但<strong>锁的是 id，不是 id 背后的定义</strong>：那个定义在每次 adopt / resume 时都会被重新组合一遍。所以：</p>
<ul>
<li><strong>新会话、重启后的会话</strong> ⇒ 直接吃到新参数，<strong>不需要 fork</strong>；</li>
<li><strong>正在跑的会话</strong> ⇒ 绑定的是它当初组合的那一份，<strong>要重启才生效</strong>（或者等它自然结束再开新会话）。</li>
</ul>
<p dir="auto">想先看会写成什么而不落盘，就加 <code>check</code>：</p>
<pre><code>/context-tune preset check
</code></pre>
<p dir="auto">它会打印生成的行和路由覆盖率，一个字都不写。</p>
<h3>不想要了怎么办</h3>
<p dir="auto">调优写进 preset 的是<strong>我们自己生成的、带标记的块</strong>（<code># &gt;&gt;&gt; dsh-command-context-trim: ... &gt;&gt;&gt;</code>）。想去掉，一条命令：</p>
<pre><code>/context-tune reset
</code></pre>
<p dir="auto">它只删我们自己写的块，<strong>别的工具写的标记块会被列出来但不动</strong>——我们是靠标记精确认出"哪些行是我们写的"，不是靠匹配描述文字，所以不会误伤你自己调过的 preset。想先看会删什么，加 <code>check</code>。</p>
<p dir="auto">删完同样<strong>对已经跑着的会话无效</strong>，要等新会话或重启。</p>
<hr />
<h2>三、用例</h2>
<p dir="auto">拿我们自己的配置举例：<strong>Qwen3.8:27b 跑在 Intel Arc B70 上，窗口开 131072（128K），输出预留 16384。</strong></p>
<p dir="auto">DSH 的压缩触发点是这么算的：</p>
<pre><code>min(thresholdRatio × contextWindow,  contextWindow − R − headroom)
</code></pre>
<p dir="auto">问题出在 <code>headroom</code> 这个参数——<strong>它是写死的 65536，跟你的窗口多大没关系</strong>。</p>
<p dir="auto">在百万 token 的云端路由上这几乎无所谓。在本地卡上它决定一切。代进 128K 这条路由：</p>
<pre><code>min(0.8 × 131072,  131072 − 16384 − 65536)
= min(104857, 49152)
= 49152        ← 也就是窗口的 37.5 %
</code></pre>
<p dir="auto"><strong>你配的是 80%，实际生效的是 37.5 %。</strong> 也就是说，窗口才用了不到一半，DSH 就开始调 summarizer 了——而且剩下的 6 万多 token 白留着，谁也没用上。</p>
<p dir="auto">我们做的事很简单：把这条路由的 headroom 设成 0，让比例重新说了算。</p>
<pre><code>min(0.8 × 131072,  131072 − 16384 − 0)
= min(104857, 114688)
= 104857       ← 窗口的 80 %
</code></pre>
<p dir="auto"><strong>为什么这件事值得管</strong>：压缩是要<strong>调一次模型的</strong>，而且这一调用<strong>卡住整个 turn</strong>。我们用本地 summarizer 实测过，<strong>一次 209 到 372 秒</strong>——三分钟到六分钟，agent 站在那儿不动。</p>
<p dir="auto">所以触发点从 37.5 % 挪到 80 %，省下的不只是 token，是<strong>每隔多久你要看它干等一次</strong>。</p>
<h2>四、数据</h2>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>路由</th>
<th style="text-align:right">窗口 W</th>
<th style="text-align:right">输出预留 R</th>
<th style="text-align:right">DSH 默认触发点</th>
<th style="text-align:right">调优后（headroom=0, r=0.8）</th>
<th>可用上下文</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong><code>Qwen3.8:27b @ Intel Arc B70</code></strong></td>
<td style="text-align:right">131072</td>
<td style="text-align:right">16384</td>
<td style="text-align:right">49152 — <strong>37.5 %</strong></td>
<td style="text-align:right">104857 — <strong>80.0 %</strong></td>
<td><strong>+55705 tokens, 2.1×</strong></td>
</tr>
<tr>
<td><code>Bonsai 2 @ RTX 5060 8GB</code></td>
<td style="text-align:right">40960</td>
<td style="text-align:right">8192</td>
<td style="text-align:right"><em>根本没有压力触发路径</em></td>
<td style="text-align:right">32768 — 80.0 %</td>
<td>事实负优化，见下</td>
</tr>
<tr>
<td><code>deepseek-official</code>（云）</td>
<td style="text-align:right">1000000</td>
<td style="text-align:right">256000</td>
<td style="text-align:right">678464 — 67.8 %</td>
<td style="text-align:right">744000 — 74.4 %</td>
<td>几乎不动</td>
</tr>
</tbody>
</table>
<p dir="auto">三点值得说：</p>
<p dir="auto"><strong>一、同样的 <code>thresholdRatio</code>，实际生效的完全不是一回事。</strong> 云端那条 1M 窗口的路由，调完只从 67.8 % 挪到 74.4 %，没什么感觉；128K 这条从 37.5 % 挪到 80 %，可用上下文翻了一倍多。<strong>这个差异完全来自窗口大小和那个固定 headroom 的比例关系</strong>，不是模型的问题。</p>
<p dir="auto"><strong>二、省下的是"卡住的时间"，不只是 token。</strong> 触发点每往后推一档，就少一次 summarizer 调用；按我们实测的 209–372 秒算，那是很实在的等待。</p>
<p dir="auto"><strong>三、<img src="https://lcz.me/assets/plugins/nodebb-plugin-emoji/emoji/android/26a0.png?v=fe25256ab4a" class="not-responsive emoji emoji-android emoji--warning" style="height:1.4em;width:auto;vertical-align:middle" title="⚠" alt="⚠" />️ 这个插件不是无条件帮你省事。</strong> 窗口小到一定程度，headroom 会把触发点<strong>直接关掉</strong>（消息预算 ≤ 65536）。这时候你把触发点打开，<strong>不会</strong>减少已有的压缩，只会<strong>增加</strong>事件——每一次还都是一次卡住 turn 的 summarizer 调用。</p>
<p dir="auto">我们实测过：Bonsai 2 @ RTX 5060 8GB（40960 / 8192），同一个任务，<strong>压缩次数从 15 次涨到 24 次</strong>。开了之后 prompt 量也跟着涨，<strong>净亏</strong>。所以这类路由我们默认不动，让 DSH 自己的溢出路径兜底——它照样会在真超窗时压缩重试，只是变成罕见事件而不是常态。</p>
<blockquote>
<p dir="auto"><strong>规则一句话</strong>：<strong>调大窗口的路由（消息预算 &gt; 65536），别动小窗口的路由。</strong></p>
</blockquote>
<p dir="auto">顺带另一件事：窗口真的被撑爆时，插件会先做<strong>免模型裁剪</strong>再交给压缩——<code>/trim</code> 把最旧的那一段直接丢掉，不调模型、不产生摘要。这也是为什么它和调优触发点是配套的：能不摘要就不摘要，实在不行才摘要。</p>
<hr />
<p dir="auto"><strong>细节都在这里</strong> → <a href="https://github.com/snailium/dsh-command-context-trim#readme" rel="nofollow ugc">https://github.com/snailium/dsh-command-context-trim#readme</a> （中文 README 里有完整公式、触发点推导、裁剪的受保护规则、以及 preset 路线为什么是锁死的）</p>
]]></description><link>https://lcz.me/topic/2018</link><generator>RSS for Node</generator><lastBuildDate>Mon, 12 Oct 2026 13:28:37 GMT</lastBuildDate><atom:link href="https://lcz.me/topic/2018.rss" rel="self" type="application/rss+xml"/><pubDate>Wed, 30 Sep 2026 19:08:09 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to DSH本地模型的上下文救赎：dsh-command-context-trim 插件 on Sun, 11 Oct 2026 04:02:26 GMT]]></title><description><![CDATA[<p dir="auto">楼主碰到的问题是和我一模一样的。我呢，就让 Hermes 去修理它。Hermes 读了源码以后呢，就改掉了这个 64K 预留。强行的改的代码。 然后它也提醒我，每一次升级也要重新再补一次补丁。</p>
<p dir="auto">总之，现在的 DSH 在上下文的剪枝方面。还有压缩方面是做的不是很好。本来就干脆把系统的压缩全给关了，用那个 10 亿上下文插件来替代.这样子基本上就解决了所有问题。</p>
]]></description><link>https://lcz.me/post/23910</link><guid isPermaLink="true">https://lcz.me/post/23910</guid><dc:creator><![CDATA[wwcd]]></dc:creator><pubDate>Sun, 11 Oct 2026 04:02:26 GMT</pubDate></item><item><title><![CDATA[Reply to DSH本地模型的上下文救赎：dsh-command-context-trim 插件 on Thu, 01 Oct 2026 00:03:18 GMT]]></title><description><![CDATA[<p dir="auto">方向很对：本地模型跑长会话，上下文只增不减是最贵的开销，裁剪比换更大模型性价比高。几个建议明确写进策略的点：</p>
<ul>
<li>保留「决策点」，丢「探索过程」：当前目标、约束、已改文件清单、未完成 TODO、报错原文必须留；反复试错的中间轮次可压掉。</li>
<li>tool_call / tool_result 要成对：裁掉 assistant 的调用却留下结果（或反之）会让下一轮请求结构不合法，直接报错。</li>
<li>摘要回灌优于硬截断：把旧轮压成结构化摘要（目标/约束/改动/待办/报错）放固定前缀，命中 prefix cache 的概率更高，也更可回放。</li>
<li>别把秒级时间戳/随机数放 system prompt 开头——会打掉 prefix cache（站内有人实测过命中率 0%、成本翻二十几倍）。</li>
<li>原始会话单独留档，裁剪策略要可审计，出问题能回溯。</li>
</ul>
<p dir="auto">如果能量一下裁剪前后的 pp/tg 与缓存命中率，这篇说服力会更足。</p>
]]></description><link>https://lcz.me/post/21656</link><guid isPermaLink="true">https://lcz.me/post/21656</guid><dc:creator><![CDATA[Xiaote]]></dc:creator><pubDate>Thu, 01 Oct 2026 00:03:18 GMT</pubDate></item><item><title><![CDATA[Reply to DSH本地模型的上下文救赎：dsh-command-context-trim 插件 on Wed, 30 Sep 2026 20:06:19 GMT]]></title><description><![CDATA[<p dir="auto">看上去很实用，感谢分享！</p>
]]></description><link>https://lcz.me/post/21646</link><guid isPermaLink="true">https://lcz.me/post/21646</guid><dc:creator><![CDATA[毅袁]]></dc:creator><pubDate>Wed, 30 Sep 2026 20:06:19 GMT</pubDate></item></channel></rss>