摘要

跨模型 tool use 兼容经常被误解成“把 Anthropic 的 tool_use schema 转成 OpenAI 的 tool_calls schema”。这只解决了工具调用的表面格式,不能解决真正影响 agent 稳定性的状态连续性问题。

在 Claude Code 这类 agent 中,一次工具调用不是普通 assistant 回复的结束,而是 assistant 回合被暂停:模型已经生成了推理状态,决定调用工具,等待外部结果后再继续同一个逻辑回合。DeepSeek V4 thinking mode 把这个约束暴露得非常直接:如果某轮 assistant 产生了 tool call,那么该轮真实返回的 reasoning_content 必须在后续请求中完整回传;如果兼容层丢失、改写、空填或摘要化这个字段,API 可能直接返回 400。

因此,跨模型兼容层的核心不是字段改名,而是状态回放:保存模型原始 thinking / reasoning 块、工具调用、工具结果、块顺序和 provider 语义,并在下一次请求中按目标模型要求重注入。

问题机制:tool call 是暂停的 assistant 回合

在 Anthropic 协议中,模型返回 tool_use 后,应用执行工具,再用 tool_result 把结果送回模型。表面上看,消息序列变成:

user -> assistant(tool_use) -> user(tool_result) -> assistant(text)

但从推理状态看,这不是两个完全独立的 assistant 回答,而是一个 assistant 回答被工具执行打断。如果上游响应中存在 Anthropic thinking block,兼容层也应把它当作原始推理状态保存和渲染;这与 tool_use / tool_result 的回放约束在状态机层面是一类问题。

DeepSeek V4 thinking mode 的约束更容易触发工程故障:

reasoning_contentcontent 同级返回。
• 没有 tool call 的普通多轮对话里,历史 reasoning_content 可以不参与后续上下文。
• 一旦 assistant 轮次产生了 tool call,该轮 reasoning_content 必须完整参与后续上下文拼接,并在所有后续用户交互中继续传回。
• 如果未正确回传,DeepSeek 文档明确说明 API 会返回 400。

这解释了为什么很多兼容 bug 不是 schema 转换错误。工具名、参数 JSON、tool_call_id 都可能是对的,但只要上一轮真实 reasoning 没被回放,目标模型看到的状态机仍然是不完整的。

为什么不是 schema 转换

schema 转换只处理“当前消息长什么样”:

语义 Anthropic 形态 OpenAI-compatible 形态
工具定义 tools[].input_schema tools[].function.parameters
工具调用 content[] 中的 tool_use block assistant message 的 tool_calls[]
工具结果 user message 的 tool_result block role: "tool" + tool_call_id
可见文本 content[] 中的 text block message content

但 thinking / reasoning 块处理的是“上一轮模型为什么停在这里”:

语义 Anthropic 形态 OpenAI / DeepSeek 形态 兼容层要求
可回传 thinking content[] 中的 thinking block reasoning_content 字段 保存原始块和顺序
安全隐藏 thinking redacted_thinking 或带 signature 的 thinking block 通常没有等价字段 作为 provider-native artifact 保存,不要伪造
streaming thinking thinking delta / signature delta delta.reasoning_content 流式累积后落库
tool call 后继续推理 thinking block + tool_use reasoning_content + tool_calls 下一请求必须重注入

这不是“Anthropic 字段 A 映射到 OpenAI 字段 B”这么简单。Anthropic thinking block 可能包含签名、redacted 数据或 omitted display;DeepSeek reasoning_content 则是 OpenAI-compatible 消息上的同级字段。兼容层应该保存 provider-native 原始材料,再根据目标后端生成请求,而不是把所有 thinking 都扁平化成一个字符串。

尤其不能用空字符串补洞:

{
  "role": "assistant",
  "content": "",
  "reasoning_content": "",
  "tool_calls": [{ "id": "call_123", "function": { "name": "read_file", "arguments": "{}" } }]
}

如果上一轮真实返回过 reasoning_content,空字符串不是“兼容默认值”,而是状态篡改。对 DeepSeek thinking mode 来说,后端需要的是上一轮真实 reasoning;对 Anthropic thinking 来说,thinking block 还可能携带签名或加密材料,空填同样不能恢复模型状态。

状态回放算法

推荐把 tool use 兼容层实现成事件日志和回放器,而不是一次性 message mapper。

1. 接收 assistant 响应

对每个 assistant 响应,兼容层必须同时捕获四类数据:

type AssistantTurnCapture = {
  provider: "anthropic" | "openai" | "deepseek" | "qwen" | "other";
  providerModel: string;
  turnId: string;
  rawMessage: unknown;
  visibleContent: string | null;
  toolCalls: ToolCallCapture[];
  thinkingArtifacts: ThinkingArtifact[];
  replayPolicy: "none" | "last_assistant_turn" | "all_tool_call_turns";
  createdAt: string;
};

其中 thinkingArtifacts 不应该只存字符串:

type ThinkingArtifact =
  | {
      kind: "openai_reasoning_content";
      text: string;
      sha256: string;
      mustReplayAfterToolCall: boolean;
    }
  | {
      kind: "anthropic_thinking_block";
      block: {
        type: "thinking";
        thinking?: string;
        signature?: string;
      };
      sha256: string;
      mustReplayWithToolResult: boolean;
    }
  | {
      kind: "anthropic_redacted_thinking";
      block: {
        type: "redacted_thinking";
        data: string;
      };
      sha256: string;
      mustReplayWithToolResult: boolean;
    };

关键点是:保存“原始 provider block”,再保存可校验 hash。不要只保存渲染后的文本,也不要让 UI 层、日志层或 prompt 压缩层改写它。

2. 判断是否进入 replay-required 状态

DeepSeek V4 thinking mode 下,判断条件很明确:

function needsDeepSeekReasoningReplay(turn: AssistantTurnCapture): boolean {
  return (
    turn.provider === "deepseek" &&
    turn.thinkingArtifacts.some(a => a.kind === "openai_reasoning_content" && a.text.length > 0) &&
    turn.toolCalls.length > 0
  );
}

但工程上不要只为 DeepSeek 写死特例。更稳妥的方式是把 replay 作为模型能力:

type ModelReplayCapability = {
  reasoningField: "reasoning_content" | "content_block_thinking" | "none";
  toolCallReasoningPolicy:
    | "omit_prior_reasoning"
    | "replay_last_assistant_turn"
    | "replay_all_tool_call_turns";
  acceptsSyntheticReasoning: false;
  requiresOriginalBlockOrder: boolean;
};

acceptsSyntheticReasoning 应固定为 false。兼容层不应编造 thinking,也不应在丢失后用摘要或空字符串代替。

3. 执行工具并追加结果

执行工具后,回放器生成下一次请求。以 DeepSeek OpenAI-compatible 形态为例,必须保留 assistant tool-call message 中的真实 reasoning_content

function toDeepSeekMessages(events: ConversationEvent[]): OpenAIMessage[] {
  const out: OpenAIMessage[] = [];

  for (const event of events) {
    if (event.type === "user_text") {
      out.push({ role: "user", content: event.text });
      continue;
    }

    if (event.type === "assistant_turn") {
      const reasoning = getOriginalReasoningContent(event);
      const message: OpenAIMessage = {
        role: "assistant",
        content: event.visibleContent ?? "",
      };

      if (event.toolCalls.length > 0) {
        message.tool_calls = event.toolCalls.map(toOpenAIToolCall);
      }

      if (event.requiresReasoningReplay) {
        if (!reasoning) {
          throw new Error(`missing original reasoning_content for ${event.turnId}`);
        }
        message.reasoning_content = reasoning;
      }

      out.push(message);
      continue;
    }

    if (event.type === "tool_result") {
      out.push({
        role: "tool",
        tool_call_id: event.toolCallId,
        content: event.content,
      });
    }
  }

  return out;
}

这里的失败策略应该是 fail closed:如果 replay-required turn 的真实 reasoning 丢了,直接报兼容层错误,不要发一个“看起来像”的请求给模型。否则问题会从本地状态损坏变成远端 400,甚至变成更隐蔽的推理漂移。

4. 后续用户轮次继续回放

DeepSeek 文档的重点不止是“tool result 紧接着那一次要回传”。它要求发生 tool call 的 reasoning_content 在所有后续用户交互中继续传回。换句话说,兼容层做历史裁剪时必须把这些 assistant tool-call turns 标记为不可普通清理:

type ConversationEvent = {
  id: string;
  type: "user_text" | "assistant_turn" | "tool_result";
  retentionClass:
    | "normal"
    | "protocol_required"
    | "reasoning_replay_required"
    | "audit_only";
};

历史压缩可以摘要普通工具结果,但不能把 reasoning_replay_required 的原始 reasoning 从请求历史中删除,除非目标模型能力明确允许,或者开启了 provider 官方的 context editing 策略并且经过回归验证。

兼容层数据结构

一个可落地的兼容层至少需要三层数据,而不是只维护 messages[]

规范化事件层

type NormalizedConversation = {
  conversationId: string;
  events: ConversationEvent[];
  providerCapabilities: Record<string, ModelReplayCapability>;
};

事件层表达跨模型共同语义:用户输入、assistant 输出、工具调用、工具结果、thinking artifact、错误状态。

原始块存储层

type RawBlockStoreRecord = {
  id: string;
  provider: string;
  turnId: string;
  blockKind:
    | "assistant_raw_message"
    | "reasoning_content"
    | "anthropic_thinking"
    | "anthropic_redacted_thinking"
    | "tool_call"
    | "tool_result";
  payload: unknown;
  sha256: string;
  createdAt: string;
  retention: "request_replay" | "audit" | "debug";
};

这一层解决“不能被 UI 或 agent 框架剥离”的问题。很多 Claude Code / agent 框架会在内部把消息转成更简单的 { role, content } 结构,或者只保留 text block、丢掉 unknown block。对普通聊天这可能没事;对 thinking + tool use,这会直接破坏协议。

后端渲染层

type ProviderRequestRenderer = {
  targetProvider: "anthropic" | "openai-compatible" | "deepseek";
  render(events: ConversationEvent[], store: RawBlockStore): unknown[];
  validate(messages: unknown[]): ValidationResult;
};

渲染层负责把同一份事件日志投影成目标 API 格式。它必须在发送前做协议校验,例如:

• assistant tool_calls 后是否存在匹配的 tool result。
• DeepSeek tool-call assistant turn 是否带真实 reasoning_content
• Anthropic thinking / redacted_thinking block 是否保持原顺序、原内容。
• 是否出现空字符串伪造 reasoning。
• 是否把 thinking block 放进了用户可编辑文本或摘要文本。

失败模式

失败模式 触发原因 现象 修复策略
DeepSeek 返回 400 tool call 后未回传真实 reasoning_content 请求在工具结果之后失败 捕获 assistant 原始 message;标记 replay-required;发送前校验
空字符串补洞 mapper 发现字段缺失后填 "" 仍然 400,或模型状态漂移 缺失即本地报错,不允许 synthetic reasoning
Claude thinking block 被剥离 框架只保留 text / tool_use block Anthropic 多轮 tool use 报 thinking block modified,或 thinking 被禁用 原样保存 thinkingredacted_thinkingsignature
block 顺序被重排 序列化层按类型排序 content blocks provider 校验失败或推理连续性破坏 以原始 content 数组为准,不按类型重排
历史清理误删 reasoning context 压缩只认为 tool result 是协议必需 多轮后才失败,难以定位 retention class 区分 reasoning_replay_required
Anthropic 到 DeepSeek 扁平化过度 把 signed thinking 当普通文本写入 reasoning_content 失去签名语义,无法再路由回 Anthropic provider-native artifact 与目标字段分开保存
streaming 丢首尾片段 只监听 text delta,忽略 reasoning_content delta 或 signature delta 非流式正常,流式失败 流式事件按 block 累积并在 stop 时落库校验
agent UI 泄露 thinking 为了回放把 thinking 放入可见 transcript 安全和产品风险 request replay store 与用户可见 transcript 分离

验证清单

• 构造 DeepSeek V4 thinking mode + 单次 tool call:确认下一次请求 assistant message 含原始 reasoning_contentcontenttool_calls
• 构造 DeepSeek V4 thinking mode + 多次 tool call:确认每个产生 tool call 的 assistant turn 都能在后续请求中继续回放。
• 构造普通 DeepSeek thinking 对话但无 tool call:确认兼容层不会错误地把所有历史 reasoning 都塞回请求。
• 构造 reasoning_content 丢失场景:发送前应本地失败,不能补空字符串。
• 构造 Anthropic thinking + tool_use:确认 thinking block、redacted_thinking block、signature 原样保存并按原顺序回传。
• 构造框架中间层只支持 { role, content } 的场景:测试应能发现 thinking artifact 被剥离。
• 构造 streaming 响应:确认 reasoning_content delta、thinking block delta、signature delta 全量累积。
• 构造历史压缩:确认 reasoning_replay_required 事件不会被摘要替换或移出请求历史。
• 构造跨后端切换:从 Anthropic-native、OpenAI-compatible、DeepSeek 三种输入分别渲染,确认 provider-native artifact 不被错误伪造。

工程建议

第一,把模型响应保存为“原始消息 + 规范化事件”,不要只保存框架抽象后的 chat message。Claude Code / agent 框架为了统一 UI、日志和上下文,经常会过滤未知 content block;这类过滤必须发生在展示层,不能发生在协议层。

第二,把 replay 校验放在发送请求前,而不是等远端 API 报错。只要存在 tool-call assistant turn,就检查目标模型是否要求 thinking / reasoning 回放;如果要求,必须能从原始块存储中取回真实内容和 hash。

第三,区分三种 transcript:用户可见 transcript、模型请求 transcript、审计 transcript。thinking / reasoning 可能需要进入模型请求,但不一定应该进入用户可见 UI;完整原始块则应进入审计存储,方便复现。

第四,不要把 provider 的 thinking 语义混成一个无来源字符串。Anthropic 的 signed / redacted thinking、DeepSeek 的 reasoning_content、OpenAI-compatible 的扩展字段具有不同校验和回放规则。兼容层可以提供统一接口,但底层必须保留 provider identity。

结论

reasoning / thinking 块重注入是跨模型 tool use 兼容的核心断点。工具调用 schema 可以转换,工具参数可以重排,工具结果可以包装;但模型在 tool call 前生成的推理状态不能靠兼容层猜测、摘要或空填。

对 DeepSeek V4 thinking mode 来说,tool call 后回传上一轮真实 reasoning_content 是协议要求。对 Anthropic thinking block 形态来说,兼容层也不应把原始 thinking 内容当成可随意丢弃的展示文本。Claude Code 这类 agent 如果在中间层剥离 thinking / reasoning block,就会把一个看似正常的工具调用链变成不可继续的半截状态机。

稳健的设计是:把 tool use 兼容层当作状态回放系统来实现。保存原始 thinking / reasoning artifact,给需要回放的 assistant turn 打上 retention 标记,在每次请求前按目标模型能力渲染和校验。只有这样,跨模型 agent 才能从“字段兼容”走向“执行语义兼容”。

参考链接

DeepSeek Thinking Mode
DeepSeek Tool Calls
DeepSeek Reasoning Model
Anthropic: Handle tool calls
Vercel AI issue: DeepSeek thinking mode tool calling needs reasoning_content passthrough
OpenCode issue: DeepSeek V4 thinking mode + tool call reasoning_content 400
NousResearch Hermes Agent issue: DeepSeek V4 thinking mode + tool call 400
OpenClaw issue: prior assistant reasoning_content replay failure
deepseek-cursor-proxy
Cursor Forum: DeepSeek tool-call reasoning passthrough discussion