DeepSeek Harness 报 tool input is invalid JSON:整轮失败怎么修

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSHMALFORMED_RESPONSEtool callJSON重试策略llm-deepseek
DeepSeek Harness 流式工具参数出现非法 JSON 时整轮以 MALFORMED_RESPONSE 中止,连已成功的并行调用也丢掉;本文解释 translate.ts 的收尾校验,并给三种恢复与规避路径。

模型偶尔会把工具参数写成不合法的 JSON——一个值忘了加引号、数组写成了裸值、结尾多一个逗号;正常做法是只丢这一条调用,让模型重发,而 DeepSeek Harness(DSH)当前会在收尾时统一解析,任何一条失败就整轮 MALFORMED_RESPONSE。 结果是同一步里已经执行成功的 ops_bash、已经拿到的文件内容、用户等的那几十秒全部作废。这篇只解决「写坏一条之后怎么把损失降到最小」。

DeepSeek Harness 这个报错的症状对得上就不用猜

一句话结论:只有在 turn/end 的 error 里看到 DeepSeek Messages stream: tool input is invalid JSON、且同一步存在多条 tool-call 时,才是本文的场景;单条调用失败、别的调用根本没发出去的,是另一类问题。 典型日志:

{"type":"turn/end","seq":19,"data":{"turn":1,
 "reason":{"kind":"error","error":{
   "message":"DeepSeek Messages stream: tool input is invalid JSON",
   "code":"MALFORMED_RESPONSE"}}}}

再往前翻一条 step/*,你会看到同一步里有两条以上 tool-call。按下面三步确认:

  1. 在 turn/end 的 error 里搜索 tool input is invalid JSON;预期能看到 code: MALFORMED_RESPONSE。
  2. 回看同一步的 step/* 事件;预期有两条以上 tool-call,而不是单条。
  3. 对比各条 arguments 的形态;预期其中一条存在值没加引号、数组裸值或结尾多逗号。

整轮失败后,合法那条的结果也不会回灌给模型。适配器层的其他问题可以先看 LLM 适配器开发。

为什么一条坏调用能带走整轮 DeepSeek Harness 请求

一句话结论:解析发生在流翻译层的 message_stop 分支,抛的是 LlmError,而这条路径没有「部分成功」的出口。 据讨论里对 llm-deepseek/src/translate.ts 的核对,逻辑等价于:

ts
if (reason.kind !== "max-tokens") for (const { content } of blocks.values()) {
  if (content.type !== "tool-call") continue
  let parsed
  try {
    parsed = JSON.parse(content.arguments)
  } catch (_invalidProviderToolJson) {
    return malformed("tool input is invalid JSON")
  }
  object(parsed)
}

malformed() 在 :25-26 直接 throw new LlmError(..., "MALFORMED_RESPONSE")。注意 :148 先要求所有 block 都已 closed,也就是说:执行顺序是「先跑完能跑的,再在收尾时统一校验」,坏的那条一抛,usage 和 finish 都不会发出。catch 的形参名 _invalidProviderToolJson 说明这种输入是被预料到的,只是当时选择了整轮失败。

DeepSeek Harness 的三种修复路径

一句话结论:生产环境先改配置加白名单,不想动配置就装守卫插件,要提交上游就改收尾校验;三条按代价从小到大排列。

修法一:把 MALFORMED_RESPONSE 加进重试白名单

这是改动最小、最适合先止血的做法,代价是可能多花一次完整请求的 token。 重试由 DSH 的 retry-policy 统一处理,不需要改模型代码。

  1. 找到当前 profile 的 cordis.patch.yml,确认 llm-deepseek-account 或你实际使用的 provider 行 id;预期能定位到唯一的目标行。
  2. 在该行配置里加 retryableCodes,把 MALFORMED_RESPONSE 追加进去,保留原有错误码;预期配置解析通过。
  3. 重启 DSH,构造一次会触发多条并行工具调用的请求;预期请求有失败后重试的机会。
  4. 观察日志;预期出现 retry 记录且第二次成功。
  5. 如果连续两次都坏,重试会停止,此时回到修法二或三;预期不会无限重试。

这条修法只重试整轮请求,不是单条调用;已经执行过的工具可能被再执行一次,带副作用的命令要小心。坏 JSON 稳定复现时,重试只会浪费时间,需要改 prompt 或换模型。

修法二:装社区守卫插件,按错误码拦截

不想改 profile 配置时,用 @argszero/dsh-malformed-tool-json-guard 在 llm/stream 瀑布上做一次统一拦截,效果与修法一接近,但升级时不会被配置覆盖。 具体做法:

  1. 按插件市场的方式安装该插件;预期它在插件列表里处于 active。
  2. 打开插件配置,确认拦截的错误码包含 MALFORMED_RESPONSE,重试上限设为 1 或 2;预期配置能保存。
  3. 重放一次出问题的会话;预期第一次失败后自动重发并成功。
  4. 如果插件与已有 provider 配置冲突,先禁用插件再重启;预期不会出现两个重试层叠加。

修法三:改收尾校验,隔离坏调用

这是唯一能让「合法调用照常返回、坏调用单独回灌」的方案,适合你能改核心代码或维护 fork 的情况。 目标是让失败粒度从「整轮」降到「单条」。

  1. 在 translate.ts 的 JSON.parse 失败分支,不再 return malformed(...),改为给该 block 记 invalidArguments: true;预期解析失败不再直接抛错。
  2. 在 message_stop 结束时,把带标记的 block 转成一条工具结果,内容为「参数无效,请重新生成」,并带上工具名与原始片段;预期坏调用有明确的回灌内容。
  3. 让工具层识别这种结果:不要执行工具,只把它作为下一次模型请求的输入;预期工具不会被误执行。
  4. 其余没有标记的 block 按原逻辑执行,usage 和 finish 正常发出;预期整轮以 completed 结束。
  5. 补两条测试:一条单坏、一条好坏并存;预期好调用有结果、坏调用有回灌、整轮不失败。

DSH 插件修复后怎么验证与避坑

一句话结论:准备一条正常请求和一条人为写坏的请求,分别走「重试」和「隔离」两条路径,才能验证修复没有误伤合法调用。 验证步骤:

  1. 构造一个同一步包含两条工具调用的请求,人为把其中一条的 arguments 改成裸值;预期请求能稳定复现 MALFORMED_RESPONSE。
  2. 用修法一/二时,确认日志里出现一次重试且最终成功;预期 turn/end 是 completed。
  3. 用修法三时,确认好调用结果保留、坏调用收到「参数无效」回灌;预期整轮仍是 completed 而不是 error。
  4. 再跑一次正常请求;预期没有把合法的工具调用误判为坏 JSON。

避坑要点:

  • 不要把 MALFORMED_RESPONSE 设成无限重试,否则一个稳定坏的 prompt 会无限循环烧 token。
  • 带副作用的工具要加幂等保护:整轮重试可能让 git commit、rm 这类命令执行两次。
  • 错误信息里没有工具名和字节位置,这是当前实现的短板;修法三顺手把这两项补进 message,后续排查会快很多。
  • 讨论里没有给出官方修复版本,0.2.1-alpha.4 与 rc.2 均可复现;升级后以实际 release notes 为准,别把 DSH插件 的旧版本行为当成已修。

DeepSeek Harness 工具 JSON 常见问题

Q:DeepSeek Harness 报 tool input is invalid JSON,为什么整轮都失败? A:DeepSeek Harness 报 tool input is invalid JSON 时整轮失败,是因为校验收在 message_stop 之后统一做。任何一条 tool-call 的 arguments 解析失败都会抛 LlmError,已经执行成功的调用结果也进不了 turn/end 的成功分支。

Q:DeepSeek Harness 的 MALFORMED_RESPONSE 为什么不自动重试,重发却能好? A:DeepSeek Harness 的 MALFORMED_RESPONSE 默认不在 retryableCodes 里,所以被当成确定性错误。触发它的往往是模型偶发的 JSON 语法抖动,把错误码补进重试白名单或装一个守卫插件,就能把硬失败降成一次自动重试。

Q:怎么让 DeepSeek Harness 只丢弃坏的那条工具调用,而不是整轮失败? A:要让 DeepSeek Harness 只丢坏调用,需要改 translate.ts 的收尾分支:解析失败时不要抛 LlmError,而是给该 block 记一个 invalid-arguments 标记,交给工具层生成参数无效的工具结果回灌给模型。其余合法调用照常执行,模型下一轮可以重发坏掉的那一条。

Q:DeepSeek Harness 的非法 JSON 报错不带工具名,怎么快速定位? A:DeepSeek Harness 的非法 JSON 报错不带工具名时,先在会话日志里找 turn/end 错误之前最后一个 step 的 tool-call 块,对比各条 arguments 的形态。常见是值没加引号、数组写成裸值或结尾多逗号;长期解决是在 malformed 错误里补上工具名和 JSON.parse 位置。

常见问题

DeepSeek Harness 报 tool input is invalid JSON,为什么整轮都失败?

DeepSeek Harness 报 tool input is invalid JSON 时整轮失败,是因为校验收在 message_stop 之后统一做。任何一条 tool-call 的 arguments 解析失败都会抛 LlmError,已经执行成功的调用结果也进不了 turn/end 的成功分支。

DeepSeek Harness 的 MALFORMED_RESPONSE 为什么不自动重试,重发却能好?

DeepSeek Harness 的 MALFORMED_RESPONSE 默认不在 retryableCodes 里,所以被当成确定性错误。触发它的往往是模型偶发的 JSON 语法抖动,把错误码补进重试白名单或装一个守卫插件,就能把硬失败降成一次自动重试。

怎么让 DeepSeek Harness 只丢弃坏的那条工具调用,而不是整轮失败?

要让 DeepSeek Harness 只丢坏调用,需要改 translate.ts 的收尾分支:解析失败时不要抛 LlmError,而是给该 block 记一个 invalid-arguments 标记,交给工具层生成参数无效的工具结果回灌给模型。其余合法调用照常执行,模型下一轮可以重发坏掉的那一条。

DeepSeek Harness 的非法 JSON 报错不带工具名,怎么快速定位?

DeepSeek Harness 的非法 JSON 报错不带工具名时,先在会话日志里找 turn/end 错误之前最后一个 step 的 tool-call 块,对比各条 arguments 的形态。常见是值没加引号、数组写成裸值或结尾多逗号;长期解决是在 malformed 错误里补上工具名和 JSON.parse 位置。

相关术语

MALFORMED_RESPONSE
MALFORMED_RESPONSE 是 DeepSeek Harness 对「上游返回的响应结构或内容无法解析」的统一错误码,流式工具参数 JSON 损坏是最常见的一种。它默认不在重试白名单内,因此会被当作终止性错误。— https://github.com/deepseek-ai/deepseek-harness/discussions/10195
message_stop
message_stop 是流式响应里的收尾事件,表示本轮所有内容块都已结束。DeepSeek Harness 在收到它之后统一校验 tool-call 的 arguments,而不是在增量到达时逐条校验。— https://github.com/deepseek-ai/deepseek-harness/discussions/10195
retryableCodes
retryableCodes 是 DeepSeek Harness 重试策略里允许自动重试的错误码集合,默认包含网络类与限流类错误,不包含 MALFORMED_RESPONSE。可以通过 profile 配置为特定 provider 追加。— https://github.com/deepseek-ai/deepseek-harness/discussions/10195

来源