ChatGPTClaudeGrok / X / CursorGemini

DeepSeek Harness 用量数字对不上:缓存命中率、上下文占用与 tok/s 排查

故障排查发布于 2026-10-30作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSHtoken缓存命中率上下文占用tok/s压缩阈值
DeepSeek Harness 显示缓存命中率 49.9% 而实际 99.8%,上下文占用 69% 却只有 34.6%,tok/s 出现四位数。用四组数字对账分清是显示错、判定错还是计量口径错,再对症处理。

同一台机器上,缓存命中率显示 49.9%、后台网关显示 99.8%;上下文占用显示 69.1%、真实 prompt 只有 34.6%;tok/s 偶尔跳到 41,188,而引擎自己报的是约 150。 这三个数字不是一个 bug,是三套口径各自出错。对账的价值在于:先把「显示错」和「判定错」分开,因为判定错会直接杀掉整轮(#10627、#11390)。

DeepSeek Harness 对账一:命中率 49.9% 对 99.8%

分母被算了两遍:input 已包含缓存读,DSH 又把它和 cacheRead 相加,于是命中率被腰斩。

  1. 连发三次完全相同的请求,system prompt 大且稳定。
  2. 记录每轮的 input 与 cacheRead:社区实测三轮都是 input=2433、cacheRead=2432。
  3. 关键观察:缓存命中时 input_tokens 并没有减少,说明它是总量而不是未命中量。
  4. DSH 按 2432 / (2433 + 2432) ≈ 50% 显示,真实值应是 2432 / 2433 ≈ 99.96%。
  5. 长会话数据一致:input=312035、cacheRead=311680,显示 49.9%,实际 99.9%。

对照口径写在 packages/llm/llm/src/types.ts:计数是互斥的(DISJOINT),提供方把缓存折进总 prompt 的,适配器要减回来。llm-pi-ai 的 mapUsage() 只处理了 reasoning 折进 output 这一种情况,没做减法。

DSH插件 对账二:占用 69% 对 34.6%,以及 120 万判超 100 万

同一个折叠在判定侧方向相反:跟窗口比较时会把真实 60 万算成 120 万,直接判溢出;跟阈值比较时又会提前压缩。

  1. 看实测数字:inputTokens=345581、cacheReadTokens=345216,当前 pressureTokens=690797(69.1%),真实 prompt 是 345581(34.6%)。
  2. 看溢出判定:isContextOverflow 在 stopReason 为 stop 时按 input + cacheRead 求和。
  3. 于是真实 60 万被判 120 万,超过 100 万窗口,映射成 CONTEXT_WINDOW_EXCEEDED,本轮结束。
  4. 更糟的是回答已经被完整生成(stopReason: "stop"),仍被整段丢弃:社区统计到单条 1,411 字符、1,318 字符等多例。
  5. 重试无法脱困,下一轮用同样的历史再判一次,形成稳定死锁。

社区另给了一条独立兜底:stopReason 为 stop 且内容非空时,永远不该判为溢出——真正溢出的请求收不到完整回答。

DeepSeek Harness 对账三:压缩阈值 800,000 对真实上限 664,576

阈值公式没问题,但输出预留被当成 0:threshold = floor(min(W×0.8, W−O−H)),O 缺失时算出 80 万,而真实输入上限只有 66.4 万。

  1. 从上游报错反推上限:1048576 − 384000 = 664576。
  2. 看公式:W=1,000,000、O=0、H=65536,得到 min(800000, 934464) = 800,000。
  3. 中间空档 135,424 token:会话必须先越过它,压缩才会触发,但越过它正是会话死掉的原因。
  4. 根因在两条链路不一致:请求确实预留了 384,000(模型的 maxTokens),预算器却按 0 算,因为适配器只在 profile 显式声明 models[].maxTokens 时才填 defaultMaxTokens。
  5. 修正后阈值应为 min(800000, 550464) = 550,464,这才是能工作的值。

DeepSeek插件 对账四:tok/s 41,188 对引擎自报的 150

工具调用步骤的 decodeMs 只覆盖「首个到最后一个流块」,而这些块在调用完成时被一次性刷出,时间戳几乎相同;分子却是整步 token。

  1. 找一个工具调用步骤,看事件时间戳:step/start 到 assistant/message 真实间隔 1,390 ms。
  2. 看内部流块时间戳:只差约 4 ms;而 usage.outputTokens = 103。
  3. 于是 103 / 0.004 ≈ 25,750 tok/s 被报出来,而这一步实际花了 1.39 秒。
  4. 统计特征:在 7,584 个工具调用步骤里,span < 5ms 且 output >= 50 的只有 0.3% 到 0.7%,全部是工具调用步骤,纯文本步骤 0 例。
  5. 所以它看起来「偶发」,实际是有明确触发条件的口径错。

会话统计的投影层 session-stats/src/projection.ts 里,decodeMs 由首个流块时间累加,而 decodeTokens 是整步输出,两者不是同一个区间。修复方向有两条:把分母改为 step/start 到 assistant/message 的事件包络,或把字段语义改名成「整步 tokens/秒」并如实标注。

DeepSeek Harness 四张表说明了两类问题

显示类错误只是难看,判定类错误会丢回答;先按这张表定性,再决定要不要动手。

现象数字对账性质影响
缓存命中率约 50%2432/(2433+2432)显示错只影响观感
占用率翻倍690797 对 345581显示错 + 提前压缩上下文被白截断
完整回答被判溢出120 万对 100 万判定错整轮失败、回答丢弃
压缩阈值高于上限800000 对 664576配置/上报缺失压缩救不回
tok/s 上万103 / 0.004s显示错无法横向比较

DSH插件 处置顺序:从能改的改起

先确认网关是否折叠,再决定是改配置还是等补丁;判定类问题优先止损。

  1. 判定网关类型:连发两次相同请求,若 input_tokens 不随命中下降、且 input ≈ cacheRead,就是折叠型。
  2. 能换协议就换:同一模型走不折叠的路由(例如标准 Anthropic 端点或 llm-deepseek),三类问题一起消失。
  3. 不能换协议就等适配器补丁,或在自定义适配层里把 cacheRead 从 input 扣掉、同时重算 totalTokens。
  4. 压缩阈值问题:在路由或模型配置里显式声明 models[].maxTokens,让 defaultMaxTokens 有值。
  5. tok/s 只当参考,不要用它对比模型或推理档位。

注意事项

  1. 别用这些数字判断计费。提供商按自己的 input_tokens 计费,DSH 只是读数错。
  2. 别只修一半。占用与阈值方向相反,只改一边会让另一边继续错。
  3. 别把「stop 且内容完整」当成正常溢出。这是自相矛盾的组合,可作为独立兜底条件。
  4. 记录网关与协议。同一个现象在不同网关下结论不同,报问题时必须带上。
  5. tok/s 修复需要核心改动。客户端替换显示只是遮住一个底层仍然错的值。

如果你的网关是标准 Anthropic 端点,这四类都不该出现,直接按「模型返回异常」去查别的方向。相关排查见 DeepSeek Harness 上下文压缩异常:过早触发、切小模型卡死与 compact 报错。

ChatGPTClaudeGrok / X / CursorGemini

常见问题

DeepSeek Harness 显示缓存命中率只有 50%,但网关后台显示 99%,哪个是真的?

网关的 99% 更可信,DeepSeek Harness 这边的低命中率是求和口径造成的。DSH 把 uncachedInputTokens + cacheReadTokens + cacheWriteTokens 相加当分母,而折叠型网关的 input_tokens 已包含缓存读,于是分母翻倍、命中率被腰斩。连发两次相同请求即可验证。

上下文占用显示 69%,实际 prompt 只有 34%,为什么 DeepSeek Harness 会算差一倍?

同样是折叠求和的问题,DeepSeek Harness 的 pressureTokens 等于 inputTokens + cacheReadTokens + cacheWriteTokens,折叠时 input 已含 cacheRead,等于算了两次。显示翻倍还会让压缩阈值提前触发。

报 CONTEXT_WINDOW_EXCEEDED 但模型明明完整回答了,DeepSeek Harness 这是怎么回事?

DeepSeek Harness 的静默溢出检测把 input + cacheRead 与窗口比较,折叠后这个和接近真实 prompt 的两倍。真实 60 万被判成 120 万超 100 万窗口,于是整轮失败,已经生成的回答被丢弃。

为什么 DeepSeek Harness 的压缩阈值会比提供商的准入上限还高?

因为 DeepSeek Harness 的路由没有上报输出预留。阈值公式是 floor(min(W×0.8, W−O−H)),当 O(reservedCompletionTokens)被当成 0 时算出 800,000,而真实输入上限是 664,576,中间有 135,424 的空档。

tok/s 出现 41,188 这种数字,是 DeepSeek Harness 的模型真的变快了吗?

不是,这是 DeepSeek Harness 的统计口径问题,模型并没有真的变快。工具调用步骤的 decodeMs 从首个流块算起,而这些块在调用完成时被一次性刷出,时间戳几乎相同;分子却是整步 token,于是分母几毫秒、比值上万。

相关术语

DISJOINT 计数约定
DISJOINT 计数约定是 DeepSeek Harness 对 TokenUsage 的约定:inputTokens 只含未缓存输入,缓存单独计为 cacheReadTokens / cacheWriteTokens。适配器遇到把缓存折进总 prompt 的提供商时要减回去。— https://github.com/deepseek-ai/deepseek-harness/discussions/10627
cacheReadTokens
cacheReadTokens 是本轮命中缓存的输入 token 数,与未缓存输入分开计数。折叠型网关会把两者合并在 input_tokens 里,导致所有求和口径都翻倍。— https://github.com/deepseek-ai/deepseek-harness/discussions/10627
reservedCompletionTokens
reservedCompletionTokens 是压缩阈值公式里的输出预留量,取自请求的 maxTokens 或模型的 defaultMaxTokens。路由未上报时按 0 计算,阈值会明显偏高。— https://github.com/deepseek-ai/deepseek-harness/discussions/11050
decodeMs
decodeMs 是会话统计里解码耗时的累加字段,当前实现从首个流块时间算到最后一个块时间。工具调用步骤的块时间戳被压缩,导致这个分母极小。— https://github.com/deepseek-ai/deepseek-harness/discussions/11390

来源