==============
== ACE BLOG ==
==============

解决 Claude Code 报错 The model's tool call could not be parsed

Claude Code Anthropic debugging mitmproxy

如果你在 Claude Code 里频繁看到这个报错,尤其是在模型「想了很久」之后才出现:

● The model's tool call could not be parsed (retry also failed).
✳ Churned for 5m 47s

这篇文章给你一个可以直接照做的解决办法,以及背后的原因分析。

英文版见:Fixing “The model’s tool call could not be parsed” in Claude Code


先说解决方案

这个报错通常不是网络问题,而是 extended thinking 把输出 token 预算耗尽导致的。最直接的解决办法:

方法 1:关闭 always thinking(推荐)

编辑 ~/.claude/settings.json

{
  "alwaysThinkingEnabled": false
}

或在 Claude Code 会话里用 /config,把 Always thinking 关掉。

改完要新开一个会话才生效——settings 在会话启动时读取一次,中途改文件对已经在跑的会话无效。

关掉之后模型并不会变笨:它在自己判断需要时仍会思考,只是不再每一轮都强制把 token 预算花在 thinking 上。我关闭后用 effortLevel: high 甚至 xhigh 跑同样的长任务,再没有复现过这个报错。

方法 2:降低 effort 或新开 context

  • effortLevelhigh 降到更低档(如 low),给 thinking 一个更小的预算上限
  • 或者当前对话上下文已经很大时,新开一个干净的会话,减小输入 token、给输出留出更多空间

下面是这个结论是怎么得出来的。如果你只想解决问题,到这里就够了。


现象与最初的怀疑

我用 Claude Code(Opus 4.8 1M context,alwaysThinkingEnabled: true + effortLevel: high)跑一个长任务,反复看到这个报错。规律很明显:

  • 越是长时间 thinking,越容易触发
  • retry 也失败——同一个上下文重试,第二次照样报这个错
  • 任务被卡死,无法继续

第一反应是怀疑网络——SSE streaming 是长连接,如果中途被 reset,tool call 的 JSON 可能收到一半就断,拼出残缺 JSON 而解析失败。但这只是猜测,需要拿数据验证。


怎么定性:抓原始 SSE 流

要区分「网络截断」和「模型侧问题」,可靠的办法是看 Claude Code 收到的原始 API 响应字节。Anthropic 的 /v1/messages 是 SSE 流式响应,关键在于那条流是怎么结束的。

我用 mitmproxy 在 Claude Code 和出口之间插一层,被动旁路记录每条响应(不改任何请求/响应头、保留流式,避免污染要诊断的现象本身):

Claude Code ──HTTPS_PROXY=8080──▶ mitmproxy(8080) ──▶ 上游出口 ──▶ api.anthropic.com
                                      │
                                      └─ 被动 tee 原始字节 → logs/*.sse

让 Claude Code 走 mitmproxy 的两个关键点:

  1. ~/.claude/settings.jsonenv 里把 https_proxy 指向 mitmproxy(http://127.0.0.1:8080
  2. 把 mitmproxy 的 CA 证书塞进 NODE_EXTRA_CA_CERTS(Claude Code 是 Node 应用,靠这个信任 mitmproxy 的 TLS 中间人)
{
  "env": {
    "https_proxy": "http://127.0.0.1:8080",
    "HTTPS_PROXY": "http://127.0.0.1:8080",
    "NODE_EXTRA_CA_CERTS": "/Users/you/.mitmproxy/mitmproxy-ca-cert.pem"
  }
}

注意:settings.json 的 env启动新会话时才读取。中途改文件,已经在跑的会话不受影响。

mitmproxy 流式透传时拿到的是 content-encoding 压缩后的原始字节(这里是 gzip),所以分析时要先 zlib.decompressobj(31) 解一下。


关键证据:usage 字段

复现报错后,把对应的那条 /v1/messages 响应解压出来,事件序列长这样:

message_start        model=claude-opus-4-8
content_block_start  index=0  type=thinking      ← 只有一个 thinking 块
content_block_delta  type=thinking_delta   × 31  ← 一路在思考
content_block_delta  type=signature_delta        ← thinking 块的签名
content_block_stop
message_delta        stop_reason=tool_use         ← 模型表示要调用工具
message_stop                                       ← 然后就结束了

注意:stop_reason 是 tool_use,但整条流里没有任何 content_block_start type=tool_use 事件。模型表达了「我要调用工具」的意图,但工具调用块本体从未被发送出来。

为什么?答案在 message_deltausage 里:

{
  "type": "message_delta",
  "delta": { "stop_reason": "tool_use" },
  "usage": {
    "output_tokens": 3165,
    "output_tokens_details": { "thinking_tokens": 3120 },
    "input_tokens": 37,
    "cache_creation_input_tokens": 49060,
    "cache_read_input_tokens": 0
  }
}

把账算清楚:

数值
output_tokens(本次响应总输出)3165
其中 thinking_tokens(思考消耗)3120
剩余给实际内容(含工具调用)45

一个最简单的 tool-use 块,光 content_block_start 里的结构(id / name / 空 input)就要 30–40 个 token,再加上 input JSON 内容,45 个 token 不足以容纳一个完整的工具调用

于是 API 发出了 stop_reason=tool_use(代表模型的决策),但没有剩余 token 把对应的 tool_use 内容块写出来。Claude Code 收到一个「说要调工具、却找不到工具调用」的响应,于是报 tool call could not be parsed

这也排除了网络的嫌疑:这条流的 HTTP body 是干净结束的(message_stop 完整收到、没有连接 reset),抓到的其余几十条调用也全部正常。问题在于 token 预算的分配。


为什么「想得越久越容易出错」

因果链是这样的:

  • effortLevel 决定 thinking 的预算上限
  • alwaysThinkingEnabled: true 决定每一轮都强制 thinking
  • 在一个大上下文(这里输入 + 缓存约 49K tokens)里,模型倾向于想得很深、停不下来
  • thinking 把输出 token 预算的大头吃掉,留给 tool-use 的 token 不够,工具调用块发不出来
  • retry 也失败,因为完全相同的上下文每次都复现同样的 token 耗尽——这是「确定性失败」的特征,也说明它不是随机的网络抖动

小结

  1. tool call could not be parsed 通常不是网络问题。在官方 api.anthropic.com 下,它更可能是输出 token 被 thinking 耗尽。
  2. 「retry 也失败」是一个关键线索——确定性失败指向上下文/模型侧,随机失败才更可能是网络。
  3. 抓原始 SSE 是快速定位的有效手段:mitmproxy 被动记录 + 解压 + 读 usage,很快就能判断问题在哪一层。
  4. 大上下文 + always thinking 是容易出问题的组合:上下文越大,模型越倾向深度思考,越容易把输出预算用尽。

如果这篇文章帮你省下了排查时间,欢迎转给同样遇到这个报错的朋友。