DeepSeek Harness 最后一步只有 reasoning,turn 为何被判 completed?

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek HarnessreasoningEMPTY_RESPONSEagent-loopturn-stopping
模型最后一步只输出 reasoning、stop 后回合以 completed 结束,无正文无工具调用。原因在 stream 只按 content 长度判 EMPTY_RESPONSE、agent 只按有无 tool-call 判完成。给出改判定、turn-stopping 插件 steer 续跑、前端重试三条路径。

如果模型的最后一次响应只有 reasoning、finish.reason 是 stop,而界面既没有正文也没有工具调用,回合却显示 completed,那这不是模型「想完了就收工」,而是判定链把「没有回答」当成了「任务完成」。 据讨论 #9990,这类回合会静默进入 idle,用户必须再发一条消息才能推动它继续。下面按判定链、三条处理路径和复现方法拆开讲。

DeepSeek Harness 最后一步只有 reasoning 时,界面会看到什么

三个特征同时出现,基本可以锁定这一条,而不是普通的模型停住。 先看现象,再去翻会话记录:

  1. 回合结束状态是 completed,预期不是错误、也不是 aborted。
  2. 最后一条 assistant 消息只有 reasoning 块,预期没有 text,也没有 tool-call。
  3. 界面上的最终回答为空,预期任务实际没做完,比如文件没有创建、命令没有执行。
  4. 用户再发一句「继续」,预期模型能接着做完,说明能力和上下文都在,缺的只是收尾判定。

DeepSeek Harness 调用链:两个条件把「没有回答」当成「任务完成」

据讨论中社区核对源码,问题不在一个适配器,而在「适配器判空太窄 + loop 完全不看内容」的叠加。 agent loop 的整体结构可以对照 DeepSeek Harness 一轮对话链路 看,这里只留两步:

  1. llm-pi-ai 的 mapStopReason 只在 message.content.length === 0 时返回 EMPTY_RESPONSE,预期只有 reasoning 块的响应长度不为 0,于是被当成正常 stop 放行。
  2. agent-loop 在 agent.ts 的步骤收尾处只过滤 tool-call,数量为 0 就返回 { kind: 'completed' },预期完全不检查有没有可见文本。
  3. 结果是「没有输出、没有动作」和「模型真的做完了」在状态上无法区分,预期重试机制也不会被触发。

DSH插件修法:改判定、steer 续跑还是前端重试

三条路都能让空回答不再被当成完成,差别在改的是 provider 路由、插件事件还是前端展示。 只想修当前 provider 就走路径一,不想动 core 就走路径二,临时兜底走路径三。

路径一:把 reasoning-only 的 stop 并入 EMPTY_RESPONSE

这是改动最小、覆盖面只限 pi-ai 路由的做法:把「无 text 且无 tool-call」当作空响应,复用已有的重试策略。

  1. 在 packages/llm/llm-pi-ai/src/stream.ts 的 mapStopReason 里找到 case "stop",预期能定位到 stop 分支。
  2. 把判空条件从 content.length === 0 改为「没有 text 块且没有 tool-call 块」,预期 reasoning 块不再计入可见内容。
  3. 保持原有 EMPTY_RESPONSE_CODE 与 failure 结构不变,预期上层重试策略原样接管。
  4. 重跑同一条只有 reasoning 的响应,预期它被标成可重试错误,而不是正常 stop。
js
case "stop": {
  const visible = message.content.some(
    (b) => (b.type === "text" && b.text.trim() !== "") || b.type === "tool-call",
  )
  if (!visible) {
    return { kind: "error", failure: { code: EMPTY_RESPONSE_CODE } }
  }
  return { kind: "stop" }
}

路径二:用 turn-stopping 插件 steer 续跑

不想动 core,就挂在 agent/turn-stopping 上,在回合真正收尾前注入一次「继续」。 据讨论,该事件在 agent.ts 的收尾路径派发,插件有机会看到最后一条消息并调用 agent.steer。

  1. 新建一个本地插件,注册 agent/turn-stopping 监听,预期监听器会在回合收尾时被调用。
  2. 取本轮最后一条 assistant 消息,判断是否既无 tool-call 又无非空 text,预期能区分出空回答。
  3. 命中时调用 agent.steer,注入一条继续提醒,来源标为 stall-guard、形式用 notice,预期提醒带着指定来源和形式发出。
  4. 用计数器把每轮续跑限制在 2 次,超过就放行,预期不会无限空转。
  5. 用同样的故障代理重跑,预期运行从 1 步继续到第 5 步,完成文件创建并给出正常最终回答。
js
// 逻辑骨架,事件载荷字段以本机 dsh-agent-loop 的类型定义为准
if (!hasToolCall && !hasVisibleText && steered < 2) {
  steered += 1
  await agent.steer("上一步没有输出也没有动作,请继续完成任务", {
    source: { kind: "stall-guard" },
    form: "notice",
  })
}

路径三:前端提示重试,不动 core

如果暂时不能改运行时,至少把静默失败变成用户看得见、点得动的状态。 这是可见性补丁,不是根修:

  1. 前端在 turn 结束时同时检查结束原因和最终文本,预期两个值在渲染时都可用。
  2. 结束原因为 completed 且最终文本为空时,不要渲染成正常完成,预期空回合不会被当成成功。
  3. 展示一条明确提示,例如「模型停止但没有输出」,并提供「重试」入口,预期用户看到可操作的提示。
  4. 用户点击后以新消息触发下一轮,预期模型重新生成而不是停留在空回答;这条路径不会自动续跑,也不改变会话记录里的 completed 状态。

DeepSeek Harness 怎么用代理稳定复现

要验证三条路径是否真的生效,需要能稳定复现,而不是等模型偶尔犯错。 讨论里给了一个确定性做法:

  1. 在 OpenAI-Responses 端点前放一个小代理,只处理第一个 /v1/responses 请求,预期只有首个请求被改写。
  2. 从 SSE 流里删掉 message、function_call 项以及 output_text、content_part 事件,只保留 reasoning,预期流里只剩推理内容。
  3. 把一个 api: openai-responses 的 provider 指向该代理,使用带推理的模型,预期请求会打到代理。
  4. 发一条「创建 a.txt 并报告完成」的任务,预期看到 1 步、turn_end 为 completed、最终文本为空、文件未创建。
  5. 换上任一条修复路径后重跑,预期完成步数与文件结果发生变化。

DeepSeek Harness 空回答的边界与验证

不是所有空回答都值得续跑,判定要卡在「最后一步、无文本、无动作」这三个条件上。 否则会把正常收尾也变成循环:

  1. 最后一步有 text 但很短,属于正常回答,预期不触发续跑。
  2. 最后一步有 tool-call,说明还在执行,预期交给工具结果推动下一轮。
  3. 用户主动中断导致的结束,预期不应被插件 steer 拉回来。
  4. 同一轮已经续跑两次仍无输出,预期停下来暴露问题,而不是继续加次数。
  5. 如果 reasoning 里出现泄漏的工具调用标记,那是另一类适配器问题,预期不能只靠续跑掩盖。

来源:Discussion #9990。

常见问题

DeepSeek Harness 里模型只输出 reasoning 就停下,turn 为什么显示 completed?

DeepSeek Harness 里判定链只看两个条件,都不检查可见文本。据讨论 #9990 中社区核对源码,llm-pi-ai 的 mapStopReason 只在 content.length === 0 时判 EMPTY_RESPONSE;content 里有 reasoning 块就返回正常 stop,agent-loop 又只按有没有 tool-call 决定是否结束,于是空回答被记成 completed。

DSH插件里把 reasoning-only stop 并入 EMPTY_RESPONSE 有何影响?

DeepSeek Harness 里不会,因为改的是「没有可见文本且没有工具调用」这一种情况,而不是所有带 reasoning 的步骤。正常步骤只要最终有 text 块或 tool-call 块就仍然走 stop;只有思考完既没写正文也没发起动作时,才进入已有的可重试错误路径。

DeepSeek Harness 的 turn-stopping 插件怎么让 agent 自动续跑,最多能续几次?

DeepSeek Harness 的 turn-stopping 插件在 agent/turn-stopping 事件里检查本轮最后一条 assistant 消息,如果没有 tool-call 也没有非空 text,就调用 agent.steer 注入一条继续提醒。据讨论里的可用实现,提醒以 stall-guard 来源、notice 形式发出,每轮最多 2 次,避免无限循环;同一条故障代理下可以跑到第 5 步并完成文件创建。

DeepSeek Harness 里不改 core,前端能不能提示重试并给按钮?

DeepSeek Harness 里可以,但这只是可见性修复,不会让当前回合自动继续。前端在收到 final 文本为空、turn 结束原因为 completed 时,展示「模型停止但没有输出」并提供重试入口,用户点一次会以新消息触发下一轮;它不能替代 loop 层的内容判定,只是把静默失败变成可操作状态。

相关术语

EMPTY_RESPONSE
EMPTY_RESPONSE 是 DSH 对「模型这一步没有产生任何内容」的可重试错误码。当前实现只在 content 数组长度为 0 时触发,只有 reasoning 块的响应不会进入该分支。— https://github.com/deepseek-ai/deepseek-harness/discussions/9990
turn(回合)
turn 是 DSH agent loop 中从接收用户消息到判定结束的一次完整执行。结束原因由最后一步是否产生 tool-call 决定,completed 表示循环认为任务已经收尾。— https://github.com/deepseek-ai/deepseek-harness/discussions/9990
reasoning block(推理块)
reasoning block 是模型返回内容中不直接展示给用户的思考片段。它可以单独构成一步响应,因此 content 非空并不等于用户看到了回答。— https://github.com/deepseek-ai/deepseek-harness/discussions/9990
steer(续跑注入)
steer 是 DSH 允许插件在回合结束前向 agent 注入提醒并继续执行的机制。turn-stopping 事件在 agent.ts 的回合收尾路径上派发,插件因此能在判定 completed 之前介入。— https://github.com/deepseek-ai/deepseek-harness/discussions/9990

来源