DeepSeek Harness 的 tok/s 虚高 3 到 5 倍:sessionStats 锚点怎么修

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSHsessionStatstok/sdecodeMsreasoningtoken 统计
DeepSeek Harness 底部 tok/s 虚高 3 到 5 倍,工具步可达 30 倍。根因是 decodeMs 的起点取首 token,而 isTokenDelta 不认 reasoning 的 block-start,给三条锚点修法与验收口径。

底部统计条的 tok/s 是 decodeTokens / decodeMs:分子是提供方上报的 outputTokens,包含思考 token;分母是「组装完成时间 − 首 token 时间」。 问题就出在这个「首 token」:DeepSeek Harness(DSH)只把非空的文本、思考、工具参数增量认作 token,而把思考整块下发的提供方根本不产生 reasoning-delta。锚点于是滑到第一段可见文本或第一个工具调用,分母被砍掉一大截,分子却没变(来源)。

先用日志确认:DeepSeek Harness 的 tok/s 虚高来自锚点

判据是「日志里 reasoning-chunks 为 0、只有 reasoning 的 block-start 与 block-end、统计条仍按含思考的 outputTokens 计速」。

  1. 打开出问题会话的日志,找到 assistant/message.stream 记录。预期:能看到内容块事件。
  2. 统计 reasoning-chunks 的条数。预期:为 0,说明思考没有逐字下发。
  3. 找 chunk.block-start,确认 blockType 是 reasoning。预期:存在,且后面紧跟整块的 block-end。
  4. 用同一 step 的 outputTokens 除以整步耗时,和显示值比较。预期:显示值是端到端的 3 到 5 倍。
  5. 单独看纯工具步。预期:倍率更夸张,可到 30 倍。
提供方形态reasoning-delta锚点位置tok/s
逐字下发思考有思考开始接近真实
整块下发思考无可见文本或工具调用虚高 3-5 倍
纯工具步无工具参数可到 30 倍

讨论中的报告者用 47 个会话离线复算,按 provider 与 api 分组后,整块下发思考的组别放大最明显(#9899)。

根因:DeepSeek Harness 的分母起点为什么会滑走

「首 token」这个锚点依赖一个没有写进契约的前提:内容必须以增量形式到达。

  1. 投影在 assistant/message 分支取 open.firstTokenTime,没有就调用 assistantStreamFirstTokenTime。预期:两条路径都找增量。
  2. 这个函数遍历流记录,只对 isTokenDelta 为真的片段返回时间。预期:其余片段被跳过。
  3. isTokenDelta 接受非空 text-delta、reasoning-delta、tool-call-delta。预期:block-start 永远不算。
  4. 思考整块下发时没有 reasoning-delta,锚点后移。预期:落到第一段文本或第一个工具调用参数。
  5. decodeMs 从那里才开始累计。预期:分母明显偏小。

还有第二个方向的误差:如果某个 step 既没有文本增量也没有工具参数增量,它的 outputTokens 会因为缺少锚点而整段不计入分子。分母偏小与分子漏算同时存在,看总数无法区分,必须按 step 拆开。

路径一:用 Math.min 取更早的 DeepSeek Harness 锚点

最小改动是把「首 token」与「首个 block-start」取较早者作为解码起点,同时让 TTFT 继续用原来的 token 锚点。

  1. 在流读取模块里加一个只找 block-start 的函数,返回首个块开始时间。预期:整块流与普通流都能返回。
  2. 在 message 分支同时取 tokenAnchor 与 blockStart,用 Math.min 得到 decodeStart,并处理 undefined。预期:正常流结果不变。
  3. decodeMs 改用 decodeStart,ttftMs 与 ttftSteps 仍用 tokenAnchor。预期:TTFT 数字不变,tok/s 下降。
  4. 三个实现点一起改:主机投影、客户端 turn-metrics 镜像、统计条展示。预期:界面与日志口径一致。
  5. 用历史日志复算一组会话。预期:修改后接近端到端吞吐。
ts
const tokenAnchor = open.firstTokenTime ?? assistantStreamFirstTokenTime(event.data.stream) ?? null
const blockStart = assistantStreamFirstBlockStartTime(event.data.stream)
const decodeStart = tokenAnchor !== null && blockStart !== undefined
  ? Math.min(blockStart, tokenAnchor)
  : tokenAnchor

为什么不直接把分母换成整步耗时:整步耗时混入排队、重试与工具等待,口径比解码耗时更宽,直接替换会从虚高变成偏低。真要用端到端口径,必须让分子与分母覆盖同一组 step,并在标签上写明。

路径二:在投影里按 block 类型取 DeepSeek Harness 锚点

如果不想新增全局读取器,可以在投影里按块类型决定:只有整块下发思考的 step 才把 reasoning 的 block-start 当起点。

  1. 在 step 打开时记录首个 reasoning block 的开始时间与是否出现过 reasoning-delta。预期:两类提供方能被区分。
  2. 只在该 step 没有 reasoning-delta、但存在 reasoning block 时,把 block-start 纳入候选锚点。预期:逐字下发的会话数值不变。
  3. 不要把这个新锚点镜像到 assistant/attempt 分支。预期:重试步仍以第一次尝试的首 token 为准。
  4. 给重试场景补测试:第二次尝试的 block-start 晚于第一次的首 token。预期:取到的是首 token。
  5. 用两条不同提供方的会话做对照。预期:两条都落在合理区间。

这条路的语义更窄,回归面也小。代价是投影里多了一个条件分支,读代码的人要先理解「为什么要区分提供方形态」。

路径三:前端先不展示 DeepSeek Harness 的不可信值

如果只是使用者、改不了宿主,最诚实的做法是让界面别把不可信的数字当精确值展示。

  1. 在客户端判断当前 step 是否属于「有 reasoning 块但没有 reasoning 增量」。预期:能标记出可疑 step。
  2. 对可疑 step 改用整步耗时为分母的端到端速度,并标注口径。预期:数字与用户手算一致。
  3. 或者折叠为「—」,把 token 总量保留。预期:不再给出误导性的高值。
  4. 在 tooltip 里说明「估算」二字。预期:读者知道这不是精确解码速率。
  5. 用 DSH插件 做展示层替换时保留原字段。预期:不破坏其他消费方。

不要用「outputTokens 减 reasoningTokens」当通用修法:reasoningTokens 是可选字段,只有部分提供方上报,混用会让跨提供方比较失去意义。

验证与边界:DeepSeek Harness 三个实现点复查

改完后要同时看三件事:tok/s 回落到端到端量级、TTFT 不变、纯工具步不再出现极端倍率。

  1. 用同一批历史会话复算,不要只测一条新会话。预期:整块与逐字两组的差距消失。
  2. 确认分母与分子覆盖同一组 step。预期:不再出现只算一半的情况。
  3. 对比修改前后的 TTFT。预期:数字一致。
  4. 看一条纯工具步。预期:倍率从 30 倍回到个位数。
  5. 检查三个实现点是否都生效。预期:日志、回退路径、统计条显示同一个值。
层作用漏改后果
主机投影日志与统计的主来源日志里的值仍虚高
客户端 turn-metrics投影不可用时的回退掉线或旧会话显示不一致
统计条展示第二处速度入口同屏出现两个 tok/s

边界有三条:统计口径的说明文档要同步改,写清「首 token」的适用前提;不要只测一条新会话就宣布修好;讨论里没有官方修复版本。状态栏展示项的另一个问题见 状态栏只剩 tok/s。

常见问题

DeepSeek Harness 的 tok/s 比总输出 token 除以总耗时高好几倍,是统计 bug 吗?

这是 DeepSeek Harness 的口径缺陷,不是你的算错。sessionStats 的分子含思考 token,分母 decodeMs 却从首 token 开始算;思考整块下发时锚点滑到可见文本,分母变短,速度就虚高。

为什么只有某些模型或网关的 DeepSeek Harness tok/s 虚高,逐字输出思考的模型却正常?

DeepSeek Harness 的缺陷依赖一个未写进契约的前提:思考必须以增量下发。逐字下发会产生 reasoning-delta,锚点落在思考开始处;整块下发只有 block-start 与 block-end,锚点只能后移。

修 DeepSeek Harness 的 tok/s 只要改一处代码吗,还是主机和前端都要改?

DeepSeek Harness 这条规则写在三个地方:主机投影、客户端 turn-metrics 镜像、统计条展示。只改主机侧,前端回退路径仍会显示虚高值,所以三处要一起改。

把 reasoningTokens 减掉,能得到正确的 DeepSeek Harness tok/s 吗?

DeepSeek Harness 的 reasoningTokens 是可选字段,不能直接相减。只有部分提供方上报;在上报处相减、不上报处留空,会破坏跨提供方可比性。更稳的是修分母锚点。

相关术语

decodeMs
decodeMs 是 DeepSeek Harness 会话统计里的解码耗时,按「组装完成时间减去首 token 时间」累计,用作 tok/s 的分母。它的起点选择决定速度口径。— https://github.com/deepseek-ai/deepseek-harness/discussions/9899
isTokenDelta
isTokenDelta 是 DeepSeek Harness 判断一个流片段是否承载 token 的函数,只认非空的 text-delta、reasoning-delta 与 tool-call-delta。block-start、block-end、usage、finish 都不算。— https://github.com/deepseek-ai/deepseek-harness/discussions/9899
block-start
block-start 是流式响应里标记一个内容块开始的片段,带 blockType,例如 reasoning 或 text。它不承载 token,因此不会被计为首 token,但它标志着生成已经开始。— https://github.com/deepseek-ai/deepseek-harness/discussions/9899

来源