Skip to content

[Bug] DeepSeek 返回空 thinking + tool_use 时未被缓存回传,下一轮请求稳定触发 400(AI 建议) #279

Description

@JayHome137

问题描述

Claude Code 通过 CCX 接入 DeepSeek Anthropic Messages 兼容接口并开启 thinking 后,会间歇性出现以下错误:

API Error: 400 The `content[].thinking` in the thinking mode must be passed back to the API.

目前已确认,该问题不是普通的 thinking 缓存失效,而是:
DeepSeek 返回了一个“存在但内容为空”的 thinking block,CCX 将其当成“不存在 thinking block”并丢弃,导致下一轮工具结果请求没有完整回传原始 assistant content。

因此,这个错误主要发生在:
thinking="" + tool_use
普通文本回复、非空 thinking,以及没有工具调用的请求通常不会触发,所以表面上看起来是“偶发错误”。

环境
CCX:v2.9.37
Claude Code:2.1.215
系统:Windows 10.0.26200.8875
reasoningMapping:各模型均为 high

稳定触发链路
1. DeepSeek 返回 assistant response
结构类似:
{
  "role": "assistant",
  "content": [
    {
      "type": "thinking",
      "thinking": ""
    },
    {
      "type": "tool_use",
      "id": "tool_xxx",
      "name": "some_tool",
      "input": {}
    }
  ]
}
注意:这里并不是没有 thinking block,而是:
thinking block 存在,但 thinking 字段为空字符串
2. Claude Code 执行工具
Claude Code 执行对应工具,然后发起下一轮包含 tool_result 的请求。
3. CCX 处理历史 assistant content
CCX 当前会过滤掉:
{
  "type": "thinking",
  "thinking": ""
}
于是发给 DeepSeek 的历史 assistant content 中只剩下 tool_use。
4. DeepSeek 拒绝下一轮请求
DeepSeek 检测到 thinking 模式下,前一轮 assistant response 的 thinking block 没有被完整回传,因此返回:
HTTP 400
The `content[].thinking` in the thinking mode must be passed back to the API.

期望行为
CCX 应区分以下三种状态:
1. response 中完全没有 thinking block
2. response 中存在 thinking block,但 thinking == ""
3. response 中存在非空 thinking block
目前第 1 和第 2 种状态被折叠成了同一个状态。
当 DeepSeek 原始 assistant response 中确实存在:
{
  "type": "thinking",
  "thinking": ""
}
并且后面跟有 tool_use 时,下一轮包含 tool_result 的请求应继续原样携带该 thinking block。
期望发送给 DeepSeek 的历史 assistant content 类似:
{
  "role": "assistant",
  "content": [
    {
      "type": "thinking",
      "thinking": ""
    },
    {
      "type": "tool_use",
      "id": "tool_xxx",
      "name": "some_tool",
      "input": {}
    }
  ]
}
而不是只保留:
{
  "role": "assistant",
  "content": [
    {
      "type": "tool_use",
      "id": "tool_xxx",
      "name": "some_tool",
      "input": {}
    }
  ]
}
建议实现
1. Collector 记录 thinking block 是否实际出现
ClaudeStreamCollector 不应只通过最终拼接后的 thinking 字符串是否为空来判断 thinking 是否存在。
建议额外记录类似状态:
ThinkingBlockSeen bool
ThinkingText      string
收到 thinking block 时:
ThinkingBlockSeen = true
即使:
ThinkingText == ""
也必须保留“该 block 确实出现过”的事实。
2. 缓存区分 absent 和 present-but-empty
缓存结构需要能够表达:
absent
present-empty
present-non-empty
实现方式可以是:
增加 thinking_block_present 字段
使用 nullable 状态
使用明确的结构体字段
使用内部 sentinel,但序列化回请求时恢复为空字符串
不建议继续仅通过:
strings.TrimSpace(thinking) != ""
判断是否应该缓存。
3. 下一轮按原始结构回传
当满足以下条件时:
- assistant response 中实际出现过 thinking block
- response 同时包含 tool_use
- 下一轮请求包含对应 tool_result
- session 和 assistant/tool-use fingerprint 匹配
CCX 应在历史 assistant content 中恢复:
{
  "type": "thinking",
  "thinking": ""
}
并尽可能保持原始 content block 顺序。
4. 不要对完全缺失 thinking 的响应伪造 block
以下情况:
{
  "role": "assistant",
  "content": [
    {
      "type": "tool_use"
    }
  ]
}
与空 thinking block 不同。
如果上游响应里完全没有 thinking block,CCX 不应仅因为出现 tool_use 就自动伪造:
{
  "type": "thinking",
  "thinking": ""
}
修复重点应是保留 block 的“存在性”,而不是为所有工具调用添加空 thinking。
5. 不要让通用空文本清理误删 thinking block
stripEmptyTextBlocks 可以继续处理普通的:
{
  "type": "text",
  "text": ""
}
但 thinking 是协议状态的一部分,不应与普通空 text block 使用完全相同的过滤语义。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions