DeepSeek Harness 只思考不输出:空 stop 被判成功与 EMPTY_RESPONSE 重试排查

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek HarnessEMPTY_RESPONSE思考模型空输出重试策略
DeepSeek Harness 每轮只有思考、正文为空,turn 却记为成功,重试也不介入。原因是只有 reasoning 的 stop 被判成功,max-tokens 又不在失败码里。本文给 guard 插件、适配器改判、调低 reasoning 三条路。

「思考完成但什么都没说」不是一个故障,而是两个不同的回合终态,而且两个都不会触发重试。 一种是只有 reasoning 的 stop,被适配器当成正常完成;另一种是输出预算耗尽后的 max-tokens,被当成回合结果。前者是判定条件太窄,后者是归类不在失败码里,修法也完全不同(Discussion #9893)。

DeepSeek Harness 两个空输出终态与 turn/end 判据

两种空输出对应两条代码路径,混在一起排查会得出错误结论。 拆开看:

  1. 只有 reasoning 的完成。llm-pi-ai 的 stream.ts:96-99 在 case 'stop' 里只检查 message.content.length === 0,llm-deepseek 的 translate.ts:149 只检查 blocks.size === 0。
  2. 思考块也计入了 content。pi-ai 的 thinking_start / thinking_delta / thinking_end 与文本共用 contentIndex 计数,DeepSeek 的 content_block_start 把所有块(含 reasoning)都放进 blocks。
  3. 于是「一大段思考后停止」的块集合非空,被映射成普通 { kind: 'stop' },loop 见不到工具调用就返回 completed。
  4. 另一种是 stop_reason: 'length',finish 变成 max-tokens,重试插件根本看不到它,因为它只挂 agent/request-error,而该事件只对 error 与 aborted 开放。

据讨论,仓库里还有一个测试专门钉住了第一种行为,名称是「keeps a thinking-only stop successful (any block counts as content)」,这能说明它是当前设计而非偶然。

判别只需要看持久化日志里的 turn/end.reason.kind,不要猜。 步骤:

  1. 打开出问题会话的日志,定位到那一轮的 turn/end。预期结果:能看到 { turn, reason }。
  2. 如果 reason.kind === 'max-tokens',归为预算耗尽。预期结果:最后一条 assistant/message 通常被截断。
  3. 如果 reason.kind === 'completed',再看最后一条 assistant/message 的内容块。预期结果:只有 reasoning 块、没有文本与工具调用,就是第一种。
  4. 搜 llm/retry 与 llm/retry-started 事件。预期结果:整段日志里一条都没有,说明重试路径从未被触发。

第 4 步很关键:它把「重试没生效」与「压根没进入重试」区分开。后者才是这两个终态的共同点。

DSH插件路径一:装 silent-completion-guard 改写终止帧

这是不动 core、当天就能用的一条路,副作用是空回合会变成可见错误;插件版本与兼容性可以在 DSH Plugin Hub 核对。 思路与步骤:

  1. 理解插件做的事:包住 llm/stream 返回的 chunk 流,观察终止 finish。预期结果:不改模型请求,只改响应流的最后一帧。
  2. 当 finish 是 stop 且这一轮没有文本块、也没有工具调用块时,把它替换成携带 EMPTY_RESPONSE 的 error finish。预期结果:loop 走 assistant/attempt 而不是追加一条空 assistant 消息。
  3. 让默认重试策略接手:五次重试、500 毫秒到 10 秒退避。预期结果:模型第二次答出来就正常结束;五次都空则回合明确失败。
  4. 在长会话上复现一次。预期结果:要么有正文,要么转录里出现真实错误,不再是「分析完成」配空气泡。

它不改变「模型为什么会只思考」,只保证这个状态不会再被静默吞掉。据讨论,该插件由回复者冻结为 @argszero/dsh-silent-completion-guard;同一作者还有一个 @argszero/dsh-empty-response-retry,处理的是不可分类 provider 错误码吞掉瞬时失败的相邻场景。

DeepSeek Harness 路径二:在适配器里把空 stop 改判 EMPTY_RESPONSE

适合能改源码、希望从根上修正归类的人,两个适配器要分别改。 步骤:

  1. 把 pi-ai 的判定从「没有任何 content 块」放宽到「没有文本块且没有工具调用块」。预期结果:只有 reasoning 的 stop 不再算成功。
  2. 对 DeepSeek Messages 路径做同样的放宽,别只改一边。预期结果:两条 provider 路径行为一致。
  3. 保留 max-tokens 的独立归类,不要把它并进 EMPTY_RESPONSE。预期结果:预算耗尽可能仍需要单独提示用户。
  4. 更新那个钉住旧行为的测试,改成断言空 stop 会抛 EMPTY_RESPONSE。预期结果:测试与实现同步,后续不会回退。
  5. 用长会话与短会话各跑一轮。预期结果:短会话不受影响,长会话的空回合进入重试。

另一条上游形状是新增一个「已结算但没有可见输出」的 outcome kind,让重试策略能看见它。这属于设计问题,改之前最好先在讨论里对齐语义。

DeepSeek Harness 路径三:模型侧调低 reasoning 与上下文压力

这是降低触发率,不是修复判定逻辑,适合暂时不能改代码的部署。 可做的三件事:

  1. 把 reasoning 档位调低,或换一个不强制思考的模型。预期结果:每轮思考占用的 token 变少,空正文的回合明显减少。
  2. 缩短单会话长度,长任务拆成新会话或先压缩。预期结果:上下文压力下降,模型更倾向于输出正文。
  3. 在系统提示里明确要求最终回答必须包含可见正文。预期结果:能减少一部分「想完就停」,但不能保证。

据讨论,报告者的会话已经跑到 51 轮、405 步、上下文压力 48%,这种规模下空回合会反复出现。调低 reasoning 能让它变少,但只要判定条件不变,就仍有概率发生。

三条路的代价对照:

路径是否改代码空回合的结局主要代价
silent-completion-guard不改重试或明确报错依赖第三方插件
适配器改判改进入默认重试需同步两条 provider 路径
调低 reasoning不改概率下降,仍可能空思考质量可能变差

如果只是偶尔遇到,路径三够用;如果长会话里稳定复现,直接上路径一,别再靠提示词碰运气。

DeepSeek Harness 界面为什么写「分析完成」而不是报错

前端没有撒谎,它忠实展示了一个「结束但没东西可展示」的步骤。 机制是:

  1. 步骤运行时有一行实时状态,文案是「正在分析请求」,带 shimmer 动效。
  2. 步骤组关闭后,若里面只有 reasoning,标题变成「分析完成」。
  3. assistant 节点本身不画任何东西,因为渲染器要求处于 streaming、interrupted,或存在非工具调用的块;reasoning 属于过程折叠区,不算正文。
  4. 所以空气泡不是缺指示器,而是缺一个能表达「结算了但没说话」的终态。

想从界面判断,别只看气泡,展开过程区确认里面是否只有思考块。

DeepSeek Harness 处理这类空回合的注意事项

先读 turn/end,再决定改哪一层,别一上来就调模型参数。 四点:

  1. retryPolicy: 'always' 不会覆盖这两个终态,它不是万能开关。
  2. 重试预算是按 step 计算的,llmRetry 投影在 step/start 与 turn/end 时清空,所以五次重试只管一个步骤。
  3. max-tokens 对整个回合有粘性,一旦置上,后续步骤也不会自动恢复。
  4. 与「最后一步只有 reasoning 就判回合完成」的相邻问题要分开处理,那条属于回合完成判定,不是 provider 的空响应判定。

把「有思考、无正文」当成一次需要显式失败的事件,比继续在提示词里加「请输出正文」更可靠。

来源:Discussion #9893、deepseek-ai/deepseek-harness。长会话上下文压力的相邻问题见 上下文窗口溢出。

常见问题

DeepSeek Harness 里 agent 说「分析完成」但正文是空的,为什么 turn 还记为成功?

DeepSeek Harness 把只有 reasoning 的 stop 当成正常完成。思考块本身占一个 content 块,EMPTY_RESPONSE 的判定条件却是「一个块都没有」,所以有思考、没正文的结束被映射成普通 stop,agent loop 见不到工具调用就判 completed。

给 DeepSeek Harness 配 retryPolicy always,能让只思考不输出的回合自动重试吗?

DeepSeek Harness 的 retryPolicy always 不能覆盖这两个终态,重试插件只挂在 agent/request-error 上,而 error 与 aborted 才会走到那里;只思考的 stop 是正常完成,max-tokens 是回合结果,两者都不是失败码。always 只是放宽模型请求失败的尝试次数,不改变这两个终态的归类。

silent-completion-guard 怎么把 DeepSeek Harness 空 stop 变成可重试错误?

DeepSeek Harness 的 silent-completion-guard 包住 llm/stream 返回的 chunk 流,观察终止帧,如果 finish 是 stop 且这一轮既没有文本块也没有工具调用块,就替换成携带 EMPTY_RESPONSE 的 error finish。下游 loop 会把这次尝试记为 assistant/attempt,默认策略重试五次,重试耗尽后明确报错。

调低 DeepSeek Harness 模型的 reasoning 档位能根治只思考不输出吗?

DeepSeek Harness 调低 reasoning 不能根治,只能降低触发概率,它减少每轮思考占用的输出预算与上下文压力,让空正文的回合变少,但判定逻辑没变,遇到长会话仍可能出现。要稳定必须在适配器或插件层把空 stop 归类为失败。

相关术语

EMPTY_RESPONSE
EMPTY_RESPONSE 是 DSH 的重试错误码之一,表示模型正常结束但没有任何内容,默认重试策略会对它重试五次并采用 500 毫秒到 10 秒的退避。— https://github.com/deepseek-ai/deepseek-harness/discussions/9893
reasoning-only stop
reasoning-only stop 指模型只输出思考块、没有文本块和工具调用块就结束的一轮,它在当前适配器里被当作正常完成而不是空响应。— https://github.com/deepseek-ai/deepseek-harness/discussions/9893
turn/end
turn/end 是会话日志里标记回合结束的持久事件,携带 turn 与 reason,reason.kind 可取 completed、aborted、blocked、error、max-tokens 或 interrupted。— https://github.com/deepseek-ai/deepseek-harness/discussions/9893
max-tokens
max-tokens 是回合结束原因的一种,表示输出预算被用完,它不是失败码,因此不会进入重试策略,且对整个回合具有粘性。— https://github.com/deepseek-ai/deepseek-harness/discussions/9893

来源