DeepSeek Harness 用量数字对不上:缓存命中率、上下文占用与 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 相加,于是命中率被腰斩。
- 连发三次完全相同的请求,system prompt 大且稳定。
- 记录每轮的
input与cacheRead:社区实测三轮都是input=2433、cacheRead=2432。 - 关键观察:缓存命中时
input_tokens并没有减少,说明它是总量而不是未命中量。 - DSH 按
2432 / (2433 + 2432) ≈ 50%显示,真实值应是2432 / 2433 ≈ 99.96%。 - 长会话数据一致:
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 万,直接判溢出;跟阈值比较时又会提前压缩。
- 看实测数字:
inputTokens=345581、cacheReadTokens=345216,当前pressureTokens=690797(69.1%),真实 prompt 是 345581(34.6%)。 - 看溢出判定:
isContextOverflow在stopReason为 stop 时按input + cacheRead求和。 - 于是真实 60 万被判 120 万,超过 100 万窗口,映射成
CONTEXT_WINDOW_EXCEEDED,本轮结束。 - 更糟的是回答已经被完整生成(
stopReason: "stop"),仍被整段丢弃:社区统计到单条 1,411 字符、1,318 字符等多例。 - 重试无法脱困,下一轮用同样的历史再判一次,形成稳定死锁。
社区另给了一条独立兜底:stopReason 为 stop 且内容非空时,永远不该判为溢出——真正溢出的请求收不到完整回答。
DeepSeek Harness 对账三:压缩阈值 800,000 对真实上限 664,576
阈值公式没问题,但输出预留被当成 0:threshold = floor(min(W×0.8, W−O−H)),O 缺失时算出 80 万,而真实输入上限只有 66.4 万。
- 从上游报错反推上限:
1048576 − 384000 = 664576。 - 看公式:
W=1,000,000、O=0、H=65536,得到min(800000, 934464) = 800,000。 - 中间空档 135,424 token:会话必须先越过它,压缩才会触发,但越过它正是会话死掉的原因。
- 根因在两条链路不一致:请求确实预留了 384,000(模型的
maxTokens),预算器却按 0 算,因为适配器只在 profile 显式声明models[].maxTokens时才填defaultMaxTokens。 - 修正后阈值应为
min(800000, 550464) = 550,464,这才是能工作的值。
DeepSeek插件 对账四:tok/s 41,188 对引擎自报的 150
工具调用步骤的 decodeMs 只覆盖「首个到最后一个流块」,而这些块在调用完成时被一次性刷出,时间戳几乎相同;分子却是整步 token。
- 找一个工具调用步骤,看事件时间戳:
step/start到assistant/message真实间隔 1,390 ms。 - 看内部流块时间戳:只差约 4 ms;而
usage.outputTokens = 103。 - 于是
103 / 0.004 ≈ 25,750 tok/s被报出来,而这一步实际花了 1.39 秒。 - 统计特征:在 7,584 个工具调用步骤里,
span < 5ms且output >= 50的只有 0.3% 到 0.7%,全部是工具调用步骤,纯文本步骤 0 例。 - 所以它看起来「偶发」,实际是有明确触发条件的口径错。
会话统计的投影层 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插件 处置顺序:从能改的改起
先确认网关是否折叠,再决定是改配置还是等补丁;判定类问题优先止损。
- 判定网关类型:连发两次相同请求,若
input_tokens不随命中下降、且input ≈ cacheRead,就是折叠型。 - 能换协议就换:同一模型走不折叠的路由(例如标准 Anthropic 端点或
llm-deepseek),三类问题一起消失。 - 不能换协议就等适配器补丁,或在自定义适配层里把
cacheRead从input扣掉、同时重算totalTokens。 - 压缩阈值问题:在路由或模型配置里显式声明
models[].maxTokens,让defaultMaxTokens有值。 - tok/s 只当参考,不要用它对比模型或推理档位。
注意事项
- 别用这些数字判断计费。提供商按自己的
input_tokens计费,DSH 只是读数错。 - 别只修一半。占用与阈值方向相反,只改一边会让另一边继续错。
- 别把「stop 且内容完整」当成正常溢出。这是自相矛盾的组合,可作为独立兜底条件。
- 记录网关与协议。同一个现象在不同网关下结论不同,报问题时必须带上。
- tok/s 修复需要核心改动。客户端替换显示只是遮住一个底层仍然错的值。
如果你的网关是标准 Anthropic 端点,这四类都不该出现,直接按「模型返回异常」去查别的方向。相关排查见 DeepSeek Harness 上下文压缩异常:过早触发、切小模型卡死与 compact 报错。
常见问题
网关的 99% 更可信,DeepSeek Harness 这边的低命中率是求和口径造成的。DSH 把 uncachedInputTokens + cacheReadTokens + cacheWriteTokens 相加当分母,而折叠型网关的 input_tokens 已包含缓存读,于是分母翻倍、命中率被腰斩。连发两次相同请求即可验证。
同样是折叠求和的问题,DeepSeek Harness 的 pressureTokens 等于 inputTokens + cacheReadTokens + cacheWriteTokens,折叠时 input 已含 cacheRead,等于算了两次。显示翻倍还会让压缩阈值提前触发。
DeepSeek Harness 的静默溢出检测把 input + cacheRead 与窗口比较,折叠后这个和接近真实 prompt 的两倍。真实 60 万被判成 120 万超 100 万窗口,于是整轮失败,已经生成的回答被丢弃。
因为 DeepSeek Harness 的路由没有上报输出预留。阈值公式是 floor(min(W×0.8, W−O−H)),当 O(reservedCompletionTokens)被当成 0 时算出 800,000,而真实输入上限是 664,576,中间有 135,424 的空档。
不是,这是 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
来源
- #10627 — 网关把缓存读折叠进 input_tokens,导致误判上下文溢出· deepseek-ai(GitHub Discussions)
- #11050 — Catalog-backed pi-ai routes report no output reserve(阈值高出 135k)· deepseek-ai(GitHub Discussions)
- #11390 — tok/s reading inflated by orders of magnitude on tool-call steps· deepseek-ai(GitHub Discussions)
- deepseek-ai/deepseek-harness(源码仓库)· GitHub