<?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[[巨坑？] DeepSeek Harness v0.1.7 的上下文压缩/修剪策略会降低本地模型的可用性]]></title><description><![CDATA[<p dir="auto">自从 <code>dsh v0.1.7-rc.2</code> 上线之后，我花了好几天的时间做插件适配。在这个过程中，切换到本地模型的任务给人感觉越来越不对劲，<strong>上下文占用基本上不会超过50%就会触发上下文压缩</strong>，而且简单的任务执行起来变迟缓了。于是，我决定给我的 <code>dsh-command-context-trim</code> 插件里面加上“上下文调优”功能。过程中暴露了dsh v0.1.7的上下文机制问题，还挺严重的，覆盖面不仅仅是 <strong>上下文压缩（compaction）</strong>，还有 <strong>上下文修剪（prune）</strong>。</p>
<p dir="auto">补充说明一下，新发布的 <strong>DeepSeek Harness桌面版</strong> 用的就是 v0.1.7-rc2 版本</p>
<p dir="auto">目前插件的基本调优功能已经完成了，还在优化中，<code>dsh-command-context-trim@0.3.1</code>只是预览发布。等插件优化完毕我会单独开一个帖子介绍。</p>
<p dir="auto">下面是我用AI总结的这两天的发现。</p>
<hr />
<p dir="auto">Intel Arc B70 上跑 dsh + Qwen3.8-27B：128K 窗口只留给对话 33K，两天的排查记录</p>
<p dir="auto">同样一条 131072 的路由、同一个 agent 任务，一轮 2 小时超时、报告没写出来；另一轮 8 分钟做完，还输出了 215 行报告。差别只有一个：压缩阈值从哪里触发。</p>
<p dir="auto">把过程写出来的原因很土：这类"窗口标着 128K、实际用不到一半"的坑，光看配置文件根本看不出来。整件事前后折腾了两天：第一天先赔了一轮 2 小时超时，才回头去读源码；第二天把 0.1.5 的同一个文件翻出来做对照，又照着 session 记录把 meter 的计费方式和 pruner 的调用顺序逐条对完，才敢把上面那句结论写下来。</p>
<blockquote>
<p dir="auto"><strong>我们量下来的结论：dsh 0.1.7 的压缩阈值里有一项固定 65536 的 headroom，再加上工具 schema 折算出来的 16,201，一条 131072 的路由，压缩在对话量只有 32,951 tokens（窗口的 25.1 %）时就触发。压缩一旦在这个位置触发，pruner 又会把刚读进来的整份源码裁掉，agent 只能重读 —— 这是一个自我维持的循环。</strong></p>
</blockquote>
<p dir="auto"><strong>文中用来做对照的调优插件、以及跑这两轮实验的容器镜像，都是我们自己的项目</strong>：</p>
<ul>
<li>插件 <a href="https://github.com/snailium/dsh-command-context-trim" rel="nofollow ugc"><code>dsh-command-context-trim</code></a>：它只做一件事 —— 从路由真实的窗口和请求预留算出那个"刚好让比例项赢"的 headroom，写进 profile 补丁，并留一条可对账的记录。</li>
<li>容器镜像 <code>ghcr.io/snailium/dsh-container/dsh</code>：dsh 本体加常用插件、后端测试脚本都装好了，一条 <code>docker run</code> 就能起一个隔离 home 跑 agent 任务（本文两轮数据就是它跑出来的，两轮的唯一差别是 <code>DSH_TRIM_AUTO_TUNE</code> 开关）。</li>
</ul>
<hr />
<h2>一、先说清楚 dsh 是什么，不然下面的数字没有参照</h2>
<p dir="auto">dsh（DeepSeek Harness）是一个 agent harness：你给它一个模型端点（本地 llama.cpp 也行），它像 Claude Code 那样跑一个会自己读文件、执行命令、写报告的小 agent。它有两个和我们这篇文章直接相关的机制：</p>
<ul>
<li>compaction（压缩）：对话变长时，把较老的一段历史交给模型摘要成一条"压缩检查点"，替换掉原文，腾出上下文。</li>
<li>tool-result pruner（工具结果裁剪）：对单条过长的工具输出做"保留头部 + 中间省略 + 保留尾部"的裁剪，默认 <code>thresholdChars: 8192</code>（约 2K tokens）。</li>
</ul>
<p dir="auto">两个机制都发生在"上下文压力"判断之后，而这一次的问题就出在那个判断上。</p>
<h2>二、现象</h2>
<p dir="auto">同一个 A1 任务（让 agent 对我们自己的 Go 项目做安全审查），同一个 B70 SYCL 后端、同一条 <code>lc / Qwen3.8-27B-Q4_K_M.gguf</code> 路由：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th>第一轮（stock）</th>
<th>第二轮（调优后）</th>
</tr>
</thead>
<tbody>
<tr>
<td>墙钟</td>
<td>2 小时超时（exit 124）</td>
<td>8 分 1 秒（exit 0）</td>
</tr>
<tr>
<td>报告</td>
<td>没有（最后一句卡在"我再确认几个细节就写报告"）</td>
<td>215 行、11 条发现</td>
</tr>
<tr>
<td>工具调用</td>
<td>100</td>
<td>29</td>
</tr>
<tr>
<td>不同文件</td>
<td>11</td>
<td>11</td>
</tr>
<tr>
<td>压缩次数</td>
<td>69</td>
<td>0</td>
</tr>
<tr>
<td>模型侧上报的输入 tokens（见第七节：这个数不等于 prompt 大小）</td>
<td>1,978,300</td>
<td>57,990</td>
</tr>
</tbody>
</table>
<p dir="auto">第一轮里 <code>web.go</code> 被读了 26 次、<code>yamlstore.go</code> 24 次，100 次调用只覆盖 11 个文件 —— 典型的"读完就丢、丢了再读"。</p>
<h2>三、源码定位：0.1.7 的公式，和 0.1.5 的公式</h2>
<p dir="auto">下面这两段代码对照，是这两天里最值钱的产出 —— 下面所有数字都是从它们推出来的。</p>
<p dir="auto">以下行号来自 npm 上装出来的包（<code>0.1.7-rc.2</code> 与 <code>0.1.5-rc.3</code>），不是 master 源码，读者可以直接对照自己 <code>node_modules</code> 里的同一文件。</p>
<p dir="auto">0.1.7-rc.2，<code>@deepseek-ai/dsh-compaction-basic/lib/index.js</code>：</p>
<pre><code class="language-js">:63   const headroomTokens = config.headroomTokens ?? 65536;

:124  function resolveCompactSpec(policy, contextWindow, reservedCompletionTokens) {
:128    const messageBudgetTokens = contextWindow - reservedCompletionTokens;
:130    const pressureBudgetTokens = messageBudgetTokens - policy.headroomTokens;
:131    if (pressureBudgetTokens &lt;= 0) throw new TargetPressureConfigError(...);
:132    const thresholdTokens = Math.floor(Math.min(contextWindow * policy.thresholdRatio, pressureBudgetTokens));
</code></pre>
<p dir="auto"><code>:757</code> 定义了预留量是哪来的：</p>
<pre><code class="language-js">function reservedCompletionTokens(agent, defaultMaxTokens) {
  return agent.session.requestHeader()?.config.maxTokens ?? defaultMaxTokens ?? 0;
}
</code></pre>
<p dir="auto">也就是说，0.1.7 的触发点是 <code>min(窗口 × 比例, 窗口 − 预留 − 65536)</code>，而且那个 65536 是绝对值，不随窗口缩放。</p>
<p dir="auto">0.1.5-rc.3 的同一个文件则简单得多：</p>
<pre><code class="language-js">:108  function resolveCompactSpec(policy, contextWindow) {
:111    const thresholdTokens = Math.floor(contextWindow * policy.thresholdRatio);
</code></pre>
<p dir="auto">没有预留项，没有 headroom 项 —— <code>thresholdRatio: 0.8</code> 就是窗口的 80 %。这就是"0.1.5 上没这个问题"的根源。</p>
<h2>四、把公式代进我们这条路由</h2>
<p dir="auto">路由声明 <code>contextWindow: 131072</code>，请求的 <code>maxTokens</code> 是 16384（从 session 的 <code>request/header</code> 里读出来的），比例 0.8：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th>计算</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td>0.1.5（无预留、无 headroom）</td>
<td><code>floor(131072 × 0.8)</code></td>
<td>104857（80 %）</td>
</tr>
<tr>
<td>0.1.7 stock（<code>headroom = 65536</code>）</td>
<td><code>floor(min(104857, 131072 − 16384 − 65536))</code></td>
<td>49152（37.5 %）</td>
</tr>
<tr>
<td>0.1.7 调优后（<code>headroom = 9831</code>）</td>
<td><code>floor(min(104857, 131072 − 16384 − 9831))</code></td>
<td>104857（80 %）</td>
</tr>
</tbody>
</table>
<p dir="auto">9831 就是"让比例项刚好赢"的最大 headroom：<code>(131072 − 16384) − floor(131072 × 0.8)</code>。也就是说，在 0.1.7 上，<code>thresholdRatio: 0.8</code> 这条设置本身几乎不生效，真正决定触发点的是那个 65536。</p>
<h3>把四种情况排在一起看</h3>
<p dir="auto">同一套公式，窗口不同，代价完全不同。下表里 O 是各自请求的 <code>maxTokens</code>，比例统一取 0.8，<code>headroomTokens</code> 取 stock 的 65536；"留给对话"一列扣掉了上面量到的 16,201 固定开销。</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>场景</th>
<th style="text-align:right">窗口 W</th>
<th style="text-align:right">请求预留 O</th>
<th style="text-align:right">stock 触发点</th>
<th style="text-align:right">stock 占窗口</th>
<th style="text-align:right">调优后触发点</th>
<th style="text-align:right">调优后占窗口</th>
<th>留给对话（stock → 调优后）</th>
</tr>
</thead>
<tbody>
<tr>
<td>deepseek 官方（1M / 256K 预留）</td>
<td style="text-align:right">1,000,000</td>
<td style="text-align:right">256,000</td>
<td style="text-align:right">678,464</td>
<td style="text-align:right">67.8 %</td>
<td style="text-align:right">744,000</td>
<td style="text-align:right">74.4 %</td>
<td>662,263 → 727,799</td>
</tr>
<tr>
<td>我们本地（128K / 16K 预留）</td>
<td style="text-align:right">131,072</td>
<td style="text-align:right">16,384</td>
<td style="text-align:right">49,152</td>
<td style="text-align:right">37.5 %</td>
<td style="text-align:right">104,857</td>
<td style="text-align:right">80.0 %</td>
<td>32,951 → 88,656</td>
</tr>
<tr>
<td>同一张卡放大到 256K</td>
<td style="text-align:right">262,144</td>
<td style="text-align:right">16,384</td>
<td style="text-align:right">180,224</td>
<td style="text-align:right">68.8 %</td>
<td style="text-align:right">209,715</td>
<td style="text-align:right">80.0 %</td>
<td>164,023 → 193,514</td>
</tr>
<tr>
<td>Bonsai 2 小窗口（40K / 8K 预留）</td>
<td style="text-align:right">40,960</td>
<td style="text-align:right">8,192</td>
<td style="text-align:right">无压力触发</td>
<td style="text-align:right">—</td>
<td style="text-align:right">32,768</td>
<td style="text-align:right">80.0 %</td>
<td>16,567（溢出前的对话预算）</td>
</tr>
</tbody>
</table>
<p dir="auto">三个结论：</p>
<ol>
<li>这套默认值明显是为大窗口准备的，代价落在本地小窗口上。 同样是 65536 的 headroom，在 1M 的官方窗口上只让触发点从 74.4 % 掉到 67.8 %（-6.6 个点，几乎无感）；换到 131072 的本地窗口，它把触发点从 80 % 压到 37.5 %（-42.5 个点），留给对话的预算只剩 32,951 tokens。</li>
<li>窗口越大，同一个 headroom 越无害；越接近 64K，越由它说了算。 把同一张卡放大到 256K，stock 就是 68.8 %，调优只需再拿回约 11 个点；到 40K 那一档，压力预算直接变成负数 —— stock 状态下压根没有"提前压缩"这条路径。</li>
<li>40K 这一档反过来不能调（这一档我们单独在第六节展开）。 stock 没有压力触发，压缩只在真正溢出时发生（很少）；把触发打开（headroom 设 0）只是给每一次越线都加一次压缩 —— 我们另一台 8GB 卡实测 15 次涨到 24 次。这一档该做的是抬高 pruner 的 <code>thresholdChars</code>，让溢出路径和 trim 去兜。</li>
</ol>
<p dir="auto">一句话概括：官方 1M 和本地 256K 这两档，调优是"拿回被 headroom 吃掉的比例"（收益分别是 6.6 和 11 个点，越大的窗口越无感）；本地 128K 是收益最大的一档（+42.5 个点）；40K 反过来，调优是"给本来没有的触发加负担"。</p>
<p dir="auto">最后留意"留给对话"那一列的占比：16,201 的固定开销在 1M 窗口上占 1.6 %，在 128K 上占 12.4 %，在 40K 上占 39.6 % —— 工具面越重的 harness，小窗口越吃亏，这一条和模型本身无关。</p>
<h2>五、固定开销有多大：工具 schema 是算进阈值的</h2>
<p dir="auto">阈值比较用的是 dsh 自己的度量（<code>@deepseek-ai/dsh-token-meter</code>），而它把工具 schema 也算进去：</p>
<pre><code class="language-js">:83  function estimateToolsTokens(header) {
:85    return Math.ceil(JSON.stringify(header.tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
</code></pre>
<p dir="auto">这次 harness 的工具面是 66 个工具，schema JSON 共 60,327 字符；system prompt 4,458 字符。按 dsh 自己的折算（<code>CHARS_PER_TOKEN = 4.0</code>、<code>BLOCK_OVERHEAD = 4</code>）：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>项</th>
<th style="text-align:right">字符</th>
<th style="text-align:right">折算 tokens</th>
</tr>
</thead>
<tbody>
<tr>
<td>工具 schema（66 个）</td>
<td style="text-align:right">60,327</td>
<td style="text-align:right">15,086</td>
</tr>
<tr>
<td>system prompt</td>
<td style="text-align:right">4,458</td>
<td style="text-align:right">1,115</td>
</tr>
<tr>
<td>固定开销合计</td>
<td style="text-align:right"></td>
<td style="text-align:right">16,201</td>
</tr>
</tbody>
</table>
<p dir="auto">于是在 stock 的 49152 阈值下，留给对话内容的只有约 32,951 tokens（窗口的 25.1 %）；而触发那一刻真实请求大约是 49152 + 16384 ≈ 65536 tokens，只有窗口的一半 —— 离"撞墙"还差得远。</p>
<h2>六、40K 这一档单独说：它的病和上面相反</h2>
<p dir="auto">上面 128K 的问题是"压缩得太早"，40K 这一档（我们的 Bonsai 2：窗口 40960、预留 8192）恰好相反 —— stock 状态下根本没有提前压缩，而把它打开只会更慢。</p>
<p dir="auto">先看它为什么没有：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>项</th>
<th style="text-align:right">数值</th>
</tr>
</thead>
<tbody>
<tr>
<td>窗口 W</td>
<td style="text-align:right">40,960</td>
</tr>
<tr>
<td>请求预留 O（<code>maxTokens</code>）</td>
<td style="text-align:right">8,192</td>
</tr>
<tr>
<td>messageBudget = W − O</td>
<td style="text-align:right">32,768</td>
</tr>
<tr>
<td>stock headroomTokens</td>
<td style="text-align:right">65,536</td>
</tr>
<tr>
<td>pressureBudget = messageBudget − headroom</td>
<td style="text-align:right">-32,768</td>
</tr>
</tbody>
</table>
<p dir="auto">压力预算为负，<code>resolveCompactSpec</code> 直接抛 <code>TargetPressureConfigError</code>；这个异常在 <code>agent/pre-step</code> 的监听里被 catch 住，只记一条 warning（每个路由只报一次），然后 <code>return next()</code> —— 回合照常继续，只是这条路由没有压力触发。压缩只能等真实溢出（<code>agent/request-error</code> + <code>CONTEXT_WINDOW_EXCEEDED</code>）才发生，很少。</p>
<p dir="auto">所以这一档有两种做法，我们两种都跑过：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>做法</th>
<th>触发点</th>
<th>实测（同一个 A1 任务）</th>
</tr>
</thead>
<tbody>
<tr>
<td>保持 stock（我们现在的默认）</td>
<td>没有提前触发，只在真溢出时压</td>
<td>0–15 次压缩，随任务长度变化</td>
</tr>
<tr>
<td>打开压力触发（headroom 设 0）</td>
<td><code>min(0.8 × 40960, 32768)</code> = 32,768</td>
<td>压缩次数 15 → 24，更慢</td>
</tr>
</tbody>
</table>
<p dir="auto">那次对照是在 40960 / 16384 预留的配置下跑的；换成现在的 8192 预留，具体次数会变，但机制不变：stock 本来没有的触发，打开它只是给每次越线都加一次压缩。</p>
<p dir="auto">还有一条纯算术上的坑，跟模型无关：40K 的窗口里，16,201 的工具面固定开销占掉 39.6 %，再扣掉 8,192 的预留，真正能放对话的只有 16,567 tokens（约 40 % 的窗口）。所以这一档的优化重点不在阈值，而在两处：</p>
<ol>
<li>把 pruner 的 <code>thresholdChars</code> 抬高（8192 → 32768），别让一份整文件读被裁掉 —— 在"读完就丢、丢了再读"的场景里，这一条最直接。</li>
<li>让模型无关的 trim 去修墙。压缩要调模型、还要等；trim 是纯文本操作，不占上下文预算，也不打断回合。</li>
</ol>
<p dir="auto">一句话：40K 这一档不是"阈值调高一点"能救的，它的 stock 行为已经是最省的；能做的是别再让 pruner 把刚读进来的东西拿走。</p>
<h2>七、session 里的实测数字</h2>
<p dir="auto">证据都在 dsh 自己写的 session 记录里（<code>session.v4.jsonl.zstd</code>，一条 <code>zstd -dc</code> 就能解开）。第一轮：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>指标</th>
<th>数值</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>tool/call</code></td>
<td>100（其中 <code>read</code> 96）</td>
</tr>
<tr>
<td>不同文件</td>
<td>11</td>
</tr>
<tr>
<td><code>web.go</code> 读取次数</td>
<td>26</td>
</tr>
<tr>
<td><code>compaction/start</code></td>
<td>69</td>
</tr>
<tr>
<td><code>compaction/prune</code></td>
<td>16（每次只裁一条，<code>shadowedRange.start == end</code>）</td>
</tr>
<tr>
<td>触发时对话内容量（由"内容 + 16,201 固定开销 ≈ 49,152"反推）</td>
<td>约 3 万 tokens 量级</td>
</tr>
<tr>
<td>溢出证据（<code>llm/retry</code> / <code>agent/request-error</code>）</td>
<td>0 / 0</td>
</tr>
</tbody>
</table>
<p dir="auto">顺带一句方法上的话：我们一开始想直接重建"触发那一刻的对话内容"来交差，但独立重建比 dsh 的 meter 低约 20–30 %（差异来自它逐节点计费、并把工具 schema 与结构化块一并计入），所以这里只给量级，不给假精度。</p>
<p dir="auto">"溢出证据"那一行是关键：整个 run 里一次真实上下文溢出都没有发生（0 次重试、0 次请求错误），窗口上界 131072 从没被触到，所以 16 次裁剪全部来自"压力路径"，也就是第五节那个被压到 49152 的阈值。</p>
<p dir="auto">链路是这样的：</p>
<pre><code>agent 读一份 8.5–27 KB 的 Go 源码
  → 对话内容 + 16,201 固定开销越过 49152（对话其实只有 ~31K）
  → 压缩流程先跑 pruner：8192 阈值 → 保留 4096 头 + 1024 尾，中间挖空
  → agent 刚拿到的代码没了 → 重新读同一个文件
  → 再越过 49152 → 再裁 —— 自我维持
</code></pre>
<h2>八、两个坑，踩过的可以直接跳过</h2>
<p dir="auto">坑一：provider 上报的 <code>inputTokens</code> 不是 prompt 大小，别拿它当证据。 这台后端在 KV 缓存命中时只上报"本次新评估"的 token：第一轮 97 条模型响应里，这个数字是 445、801、938 … 偶尔跳到 32,739（恰好是 surface 刚变过、缓存失效的那几次）。我们一开始也被它带偏过，用它算出来的"触发点中位数"完全对不上公式；真正能对上的量是"活 surface 内容 + 固定开销"。</p>
<p dir="auto">坑二：prune 之后如果因此不再压缩，内容就白丢了。 <code>compactIfNeeded</code> 的顺序是先判阈值、再裁剪、再重新测量：</p>
<pre><code class="language-js">if (measurement.totalTokens &lt; spec.thresholdTokens) return null;   // 没到阈值：不裁
if (prune !== void 0) { prune.pruneSession(agent.session); … }      // 到了阈值：先裁
if (measurement.totalTokens &lt; spec.thresholdTokens) return null;   // 裁完降到阈值下：不压缩，但裁掉的不还
</code></pre>
<p dir="auto">也就是说，agent 可能既拿不到摘要、又丢掉了刚读的代码，而这两件事在日志里看不出区别。我们的做法是把 pruner 的 <code>thresholdChars</code> 抬高一点（比如 32768），让一份整文件读不至于被裁；在"读完就丢、丢了再读"的场景里，这一条比换模型或调采样参数都有效。</p>
<h2>九、怎么规避（以及一个反例）</h2>
<p dir="auto">三条路，按优先级：</p>
<ol>
<li>把 headroom 设成"刚好让比例项赢"的值（这条路由是 9831）。我们自己的插件会从路由的 <code>contextWindow</code> 和请求的 <code>maxTokens</code> 算出这个数并写进 profile 补丁，同时把结果记成一条 <code>context-trim/tuned</code> 记录，事后能对上账。</li>
<li>或者显式压低它：<code>- id: compaction-basic</code> + <code>config: { headroomTokens: 8192, maxTokens: 8192 }</code>，阈值就从 37.5 % 回到接近 80 %。</li>
<li>抬高 pruner 的 <code>thresholdChars</code>（比如 32768），让一份整文件读不再被裁。</li>
</ol>
<p dir="auto">对照实验（同一任务、同一后端、fresh home）：</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th></th>
<th>第一轮（headroom 65536）</th>
<th>第二轮（headroom 9831）</th>
</tr>
</thead>
<tbody>
<tr>
<td>触发点</td>
<td>37.5 %</td>
<td>80 %</td>
</tr>
<tr>
<td>压缩</td>
<td>69</td>
<td>0</td>
</tr>
<tr>
<td>裁剪</td>
<td>16</td>
<td>0</td>
</tr>
<tr>
<td><code>read</code> 次数</td>
<td>96（11 个文件）</td>
<td>11（11 个文件，各一次）</td>
</tr>
<tr>
<td>结果</td>
<td>2 小时超时</td>
<td>8 分钟、215 行报告</td>
</tr>
</tbody>
</table>
<p dir="auto">有一点必须先说清楚：两轮各只跑了一次，而且第二轮恰好采用了更省上下文的策略（用 <code>grep</code>/<code>sed</code> 定点查，而不是整文件读），所以"8 分钟 vs 2 小时"不能当作同策略的纯粹对比。能确定的是机制：阈值低 → 压缩频繁 → 刚读进来的代码被裁掉 → 只能重读。</p>
<p dir="auto">反例，必须说：调优不是万能的。小窗口上它的作用是相反的（第六节单独讲了这一档）。我们另一台 8GB 卡上的 Bonsai 2（40960 / 8192）实测：把压力触发"打开"之后，压缩次数从 15 涨到 24 —— 因为那条路由的 message budget（32768）本来就低于 65536，stock 状态下压根没有压力触发，你把它打开，只是给每一次越线都加了一次压缩。那条路由我们的默认行为是保持不动，让溢出路径兜底。</p>
<h2>十、我们踩完之后改了三件事</h2>
<p dir="auto">都不是什么高级操作，只是下次不会再踩：</p>
<ol>
<li>headroom 按路由算，不再拿一个固定值套所有卡。大窗口上它能把触发点从 37.5 % 拉回 80 %；但小窗口上方向是反的 —— 我们另一台 8GB 卡（Bonsai 2，40960/8192）实测，把压力触发打开反而让压缩从 15 次涨到 24 次，因为那条路由本来就没有压力触发，打开只是给每次越线都加了一次压缩。现在我们的默认是：小窗口保持 stock，让溢出路径兜底。</li>
<li>不再用 provider 上报的 <code>inputTokens</code> 判断"离上限还有多远"。改成两段式心算：固定开销（工具 schema + system prompt）+ 对话内容。这样一眼就能看出"窗口 128K，但真正能放对话的只有 33K"。</li>
<li>长任务开跑前先量一次基线：数 <code>compaction/start</code> 和 <code>compaction/prune</code>。如果压缩次数和 <code>read</code> 次数一起飙，先去看阈值，而不是先去调采样参数 —— 我们第一轮就是先怀疑后端，白折腾了半天。</li>
</ol>
<h2>十一、可以照搬的一点</h2>
<p dir="auto">这套量法不依赖我们的工具，任何人对着自己的 session 记录几条命令就能复现：读 <code>request/header</code> 里的 <code>config.maxTokens</code> 得到预留 O，用窗口和 <code>headroomTokens</code> 代入 <code>min(W × 比例, W − O − headroom)</code> 算出真实触发点，再数一下 <code>compaction/prune</code> 和重复的 <code>read</code>。</p>
<p dir="auto">如果算出来发现"留给对话的预算"远小于你以为的窗口大小，那问题不在模型、也不在后端 —— 在阈值这一层。窗口 128K 的卡上，我们这里能用的只有 32,951 tokens。</p>
<p dir="auto">想直接跑起来看自己的路由属于哪一种：用上面那个镜像起一个隔离 home，先读一次 <code>request/header</code> 里的 <code>config.maxTokens</code>，跑一遍任务，再 <code>zstd -dc</code> 数一下 <code>compaction/prune</code> —— 三次以内就能判断你需不需要调 headroom。</p>
]]></description><link>https://lcz.me/topic/1971</link><generator>RSS for Node</generator><lastBuildDate>Mon, 12 Oct 2026 13:28:41 GMT</lastBuildDate><atom:link href="https://lcz.me/topic/1971.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 27 Sep 2026 16:59:42 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to [巨坑？] DeepSeek Harness v0.1.7 的上下文压缩/修剪策略会降低本地模型的可用性 on Mon, 12 Oct 2026 03:13:00 GMT]]></title><description><![CDATA[<p dir="auto">在吗 我用dsh 0.2rc 也发现了和你一样的问题，我运行本地qwen27B 很多任务都做不下来，dsh一边压缩信息，一边大规模阅读我本地信息，导致很长时间上下文都被占满，再压缩。但是我换成codex 或者hermes就正常了。</p>
]]></description><link>https://lcz.me/post/24153</link><guid isPermaLink="true">https://lcz.me/post/24153</guid><dc:creator><![CDATA[suboyang]]></dc:creator><pubDate>Mon, 12 Oct 2026 03:13:00 GMT</pubDate></item><item><title><![CDATA[Reply to [巨坑？] DeepSeek Harness v0.1.7 的上下文压缩/修剪策略会降低本地模型的可用性 on Wed, 30 Sep 2026 10:53:33 GMT]]></title><description><![CDATA[<p dir="auto">谢谢.dsh 升级很容易升一堆 的坑.<br />
特别是用本地 vllm 的.<br />
主要是插件各种矛盾</p>
]]></description><link>https://lcz.me/post/21588</link><guid isPermaLink="true">https://lcz.me/post/21588</guid><dc:creator><![CDATA[wwcd]]></dc:creator><pubDate>Wed, 30 Sep 2026 10:53:33 GMT</pubDate></item><item><title><![CDATA[Reply to [巨坑？] DeepSeek Harness v0.1.7 的上下文压缩/修剪策略会降低本地模型的可用性 on Sun, 27 Sep 2026 22:48:29 GMT]]></title><description><![CDATA[<p dir="auto">感谢分享，已经修复巨坑！</p>
]]></description><link>https://lcz.me/post/21196</link><guid isPermaLink="true">https://lcz.me/post/21196</guid><dc:creator><![CDATA[毅袁]]></dc:creator><pubDate>Sun, 27 Sep 2026 22:48:29 GMT</pubDate></item><item><title><![CDATA[Reply to [巨坑？] DeepSeek Harness v0.1.7 的上下文压缩/修剪策略会降低本地模型的可用性 on Sun, 27 Sep 2026 17:08:00 GMT]]></title><description><![CDATA[<p dir="auto">难怪，第一次我跑了24小时，也是没完没了最终停在一个没有报告的重点，但是第二轮就快了，要什么有什么。。。</p>
]]></description><link>https://lcz.me/post/21188</link><guid isPermaLink="true">https://lcz.me/post/21188</guid><dc:creator><![CDATA[imbiplaza ASUS]]></dc:creator><pubDate>Sun, 27 Sep 2026 17:08:00 GMT</pubDate></item></channel></rss>