摘要
跨模型 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_content 与 content 同级返回。
• 没有 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 被禁用 | 原样保存 thinking、redacted_thinking、signature |
| 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_content、content、tool_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