跨越协议鸿沟:Tool Use状态机从Anthropic到OpenAI兼容体系的适配要点
2026/6/12 17:38:00 网站建设 项目流程

从工程角度看,Claude Code这类Agent的核心并非普通文本生成,而是围绕工具调用构建的严格状态机。在Anthropic Messages API中,工具调用以tool_usecontent block嵌入assistant消息,工具执行结果必须作为下一条user消息的tool_resultblock回填,通过tool_use.idtool_result.tool_use_id配对。这与OpenAI-compatible function calling的设计截然不同:后者将工具请求放在assistant消息的tool_calls字段,工具结果则分配给独立role: "tool"消息。

当需要将Claude Code的工具调用协议映射到DeepSeek、Qwen等非Anthropic模型或兼容网关时,最大风险并非JSON字段名差异,而是状态机不对齐。消息角色、content block顺序、工具ID生命周期、错误语义、并行调用、停止原因和流式增量都必须整体转换。仅做input_schemaparameters的字段重命名,很容易在多轮工具调用中触发400错误、丢失工具结果、重复执行工具,或让模型把工具错误当成普通用户文本继续推理。

核心挑战:四个必须回答的问题

任何跨模型Tool Use兼容层都需要解决以下关键议题:

  1. 工具定义如何转换:Anthropic的tools[].input_schema如何映射为OpenAI-compatible的tools[].function.parameters
  2. 工具调用如何表达:assistant content中的tool_useblock如何转换为assistant消息的tool_calls
  3. 工具结果如何回填:Anthropic要求tool_result放在下一条user message content数组中;OpenAI-compatible通常要求放在role: "tool"消息内。
  4. 状态如何闭合:每个tool_use.id必须精确匹配一个工具结果,错误结果需保留机器可读状态,且下一轮模型请求必须能识别完整闭合后的历史。

这四个环节缺一不可。许多兼容层失败,不是因为模型不会调用工具,而是因为代理层将Anthropic的content block协议误判为普通聊天协议,导致工具调用链在历史中断裂。

协议差异的本质:从content block到消息角色

对比维度Anthropic Messages APIOpenAI-compatible function calling兼容层关键关注点
工具定义位置请求级tools数组请求级tools数组字段层级不同,但都位于请求级别
Schema字段名称input_schemafunction.parameters可机械映射,需保留JSON Schema约束
工具调用载体assistant消息的content[]中出现type:"tool_use"blockassistant消息的tool_calls[]Anthropic为内嵌block;OpenAI-compatible多为消息侧信道
工具名称tool_use.nametool_calls[].function.name名称应保持稳定,避免转换层改动
工具参数tool_use.input为对象tool_calls[].function.arguments通常为JSON字符串需要严格的序列化/反序列化
调用IDtool_use.idtool_calls[].id/tool_call_idID是配对主键,非展示字段
工具结果载体下一条user消息的content[]type:"tool_result"role:"tool"消息,附带tool_call_id角色和消息顺序完全不同
工具错误标记tool_result.is_error: true无统一错误字段需在结果内容中包装结构化错误
停止原因stop_reason:"tool_use"常见为finish_reason:"tool_calls"或输出function call item停止原因需进入状态机,不能只看文本是否为空
并行工具一个assistant content可包含多个tool_useblock一个assistant message可包含多个tool_calls等待所有结果闭合后再进入下一轮模型调用
顺序约束tool_result必须紧跟对应工具调用后的下一条用户消息,位于content数组前部工具消息一般紧跟assistant tool call消息顺序校验需按源协议和目标协议分别进行

从实现角度看,Anthropic更像“content block状态机”:文本、工具调用和工具结果都在消息content数组里按block排列。OpenAI-compatible则更像是“消息角色状态机”:assistant请求工具,随后若干tool role消息返回结果,再由assistant继续生成。

Anthropic的状态机模型

Anthropic的核心约束可抽象为以下状态:

状态 S0: assistant生成中 - 输出 text block → 停留在 S0 - 输出 tool_use block → 进入 S1 - stop_reason=end_turn → 本轮结束 状态 S1: 等待工具执行 - 收集一个或多个 tool_use block - stop_reason=tool_use - 应用层执行所有工具 - 进入 S2 状态 S2: 等待工具结果回填 - 下一条消息必须是 user - user.content 开头必须包含对应 tool_result block - 每个 tool_result.tool_use_id 必须匹配一个未闭合 tool_use.id - 所有 pending tool_use 闭合后进入 S3 状态 S3: 继续生成 - 将闭合后的历史发送给模型 - 模型基于结果继续 text 或再次 tool_use

三个容易被忽略的细节:

  • tool_use.id是状态机主键,而非可选调试信息。如果目标模型不返回 id,兼容层需要生成内部 id,且该 id 必须贯穿 assistant tool call、工具执行记录、tool result 回填和后续历史。

  • tool_result的位置本身就是协议语义。工具结果必须紧跟对应工具调用,并作为 user message content 数组中的 tool result block。将结果塞入普通文本,或在 tool result 前插入用户解释,都可能破坏模型对上一轮工具调用的绑定。

  • stop_reason:"tool_use"是控制信号,表示“现在该执行工具”,不是普通完成状态。忽略该信号而只检查 assistant 文本,可能会把包含工具调用的响应当作空回复。

字段映射三层拆解

1. 工具定义映射

Anthropic 形式:

{"name":"read_file","description":"Read a UTF-8 text file from the workspace.","input_schema":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}

OpenAI-compatible 形式:

{"type":"function","function":{"name":"read_file","description":"Read a UTF-8 text file from the workspace.","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}}}

此步骤虽然机械,但兼容层需做三类校验:

  • name必须满足目标模型或网关的函数名限制,且保持稳定。
  • input_schema必须是 object schema;不应将自然语言参数说明拼入 description 后冒充 schema。
  • 如果目标模型对 strict schema、additionalProperties、枚举或数组嵌套支持不完整,应在注册阶段降级,而非等模型生成非法参数后再补救。

2. 工具调用映射

Anthropic assistant 消息:

{"role":"assistant","content":[{"type":"text","text":"我需要先读取配置文件。"},{"type":"tool_use","id":"toolu_01A","name":"read_file","input":{"path":"package.json"}}],"stop_reason":"tool_use"}

OpenAI-compatible 形式:

{"role":"assistant","content":"我需要先读取配置文件。","tool_calls":[{"id":"toolu_01A","type":"function","function":{"name":"read_file","arguments":"{\"path\":\"package.json\"}"}}]}

注意arguments通常是 JSON 字符串,而 Anthropic 的input是已解析对象。兼容层应在工具执行前解析参数并做 schema 校验,同时记录原始字符串以排查流式截断、半个 JSON、重复键、数字精度和编码问题。

3. 工具结果映射

Anthropic 回填:

{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01A","content":"{\"name\":\"demo\",\"scripts\":{\"test\":\"vitest\"}}"}]}

OpenAI-compatible 回填:

{"role":"tool","tool_call_id":"toolu_01A","content":"{\"name\":\"demo\",\"scripts\":{\"test\":\"vitest\"}}"}

当工具执行失败时,Anthropic 可显式设置is_error: true

{"type":"tool_result","tool_use_id":"toolu_01A","is_error":true,"content":"File not found: package.json"}

OpenAI-compatible API 没有统一的is_error字段。不建议只返回自然语言错误,因为模型很难稳定区分“工具失败”和“工具成功但返回一段错误文本”。更稳妥的做法是在content中包装结构化结果:

{"ok":false,"error":{"type":"FileNotFound","message":"File not found: package.json","retryable":false}}

转回 Anthropic 时,将ok: false映射为is_error: true。这样错误语义不会在模型切换时丢失。

兼容层五个边界清晰的模块

1. 中间表示层 (ToolCallIR / ToolResultIR)

不要让业务逻辑直接在 Anthropic block 和 OpenAI message 之间拼 JSON。先定义内部接口:

ToolCall={id, name, input, rawArguments,source}ToolResult={toolUseId, ok, content, error}AssistantTurn={text[], toolCalls[], stopReason}

DeepSeek、Qwen 等后端或网关的 API 表层可能相似,但细节各异:有的在 streaming 中分片输出arguments,有的用 XML 标签,有的对 tool role 顺序更严格。内部 IR 将差异限制在 adapter 内,避免主逻辑依赖厂商格式。

2. 工具注册映射器

注册阶段只做确定性转换:input_schemaparameters,同时生成工具能力表:

ToolCapability={supportsParallelToolUse, supportsStrictJsonSchema, supportsToolRole, requiresXmlParser, requiresReasoningPassthrough}

能力表不应靠模型名称字符串散落在代码中判断,而应集中配置。例如同样是 Qwen,不同部署方式可能分别走 OpenAI-compatible JSON、chat template XML 或平台自定义 function calling。

3. 消息转换器

消息转换需按“轮次”处理,而非逐条孤立转换。

Anthropic → OpenAI-compatible:

assistant(content:[text, tool_use A, tool_use B],stop_reason=tool_use)user(content:[tool_result A, tool_result B, text?])⇒ assistant(content: text, tool_calls:[A, B])tool(tool_call_id=A,content=result A)tool(tool_call_id=B,content=result B)user(content: text?)// 仅当存在真实用户文本时追加

OpenAI-compatible → Anthropic:

assistant(content: text, tool_calls:[A, B])tool(tool_call_id=A,content=result A)tool(tool_call_id=B,content=result B)⇒ assistant(content:[text, tool_use A, tool_use B],stop_reason=tool_use)user(content:[tool_result A, tool_result B])

关键点:不要将 tool role 消息逐条转换为多条 Anthropic user 消息。同一轮 assistant tool calls 应聚合成下一条 user message 的 content 数组,并将tool_result放在数组前部。

4. Pending Tool Ledger

维护一个待办账本:

PendingToolUse={id, name, inputHash, createdAtTurn, status}

模型输出工具调用时登记 pending;工具结果回填时按 id 闭合。下一轮模型请求前必须通过检查:

  • 不允许存在无结果的 pending tool use。
  • 不允许出现未知tool_use_id/tool_call_id
  • 不允许同一个 id 被两个结果闭合。
  • 并行调用必须全部闭合后才能继续生成。
  • 若目标后端不支持并行,应在请求侧禁用并行,或在兼容层串行调度并保留源协议顺序。

5. 错误标准化器

所有 adapter 使用统一错误信封:

{"ok":false,"error":{"type":"CommandFailed","message":"npm test exited with code 1","retryable":true,"metadata":{"exit_code":1}}}

映射规则:

内部状态AnthropicOpenAI-compatible
成功is_error省略或 false{"ok":true,"data":...}
失败is_error: true{"ok":false,"error":...}
超时is_error: true,类型 timeoutok:falseretryable依语义决定
用户取消is_error: true,明确 cancelledok:false,避免伪装成空结果

最小正确闭环示例

Anthropic 闭环:

对应 OpenAI-compatible 闭环:

转换层必须保证第 2 步和第 3 步相邻,中间不能插入另一轮 assistant 推理。Claude Code 如果调用本地 shell、文件系统、浏览器或 MCP 服务,这些执行细节应记录在 agent 内部日志中,而非插入模型消息破坏协议闭环。

面向 DeepSeek、Qwen 等后端的适配策略

这些模型常见的接入方式是 OpenAI-compatible API,但“兼容”通常只意味着顶层 HTTP 路径和部分字段相似,不意味着工具调用状态机完全一致。设计 adapter 时应按能力假设而非按品牌:

能力问题需要探测的行为兼容策略
是否原生支持tools/tool_calls模型是否返回结构化 tool call,而非普通文本支持则用 OpenAI-compatible adapter;否则用 XML/文本 parser
是否支持并行工具调用一轮是否会返回多个 tool calls不支持时禁用并行或串行调度
streaming 参数是否稳定arguments是否按合法 JSON 增量闭合按 id 聚合 delta 后再 parse
tool role 是否严格role: "tool"必须紧跟 assistant tool_calls历史构造时做顺序校验
错误语义是否保留工具失败是否被模型误读为普通结果使用统一ok/errorenvelope
是否有额外 reasoning 字段thinking 模型要求回放 reasoning 状态adapter 单独保留并回传
是否使用 XML 工具格式Qwen 等模型可能输出<tool_call>parser 输出统一 ToolCall IR

因此,稳健的兼容层不应写成:

if model.includes("deepseek") use openaiTransform() if model.includes("qwen") use qwenTransform()

更好的结构是:

provider adapter → parse assistant output into ToolCall IR → validate pending ledger → execute tools → normalize ToolResult IR → render history into provider-specific messages

这样,即使同一个模型在云 API、本地 vLLM、llama.cpp、LM Studio 或自定义网关下表现不同,也只需替换 adapter 的 parse/render 层。

常见失败模式

失败表现根本原因修复办法
工具调用生成了但未执行,assistant 文本为空,agent 直接返回忽略了stop_reason: tool_usetool_calls将停止原因纳入状态机
API 返回缺失tool_result,Anthropic 400 或下一轮拒绝tool_use.id没有对应tool_result.tool_use_idpending ledger 强制闭合检查
工具结果无法配对,模型重复调用或误读结果id 被重写、丢失或复用id 作为不可变主键保存
Anthropic 历史格式非法,tool_result前插入了普通文本未遵守 content block 顺序聚合同轮结果,将 tool_result 放在 user content 前部
OpenAI-compatible 历史格式非法,tool 消息未紧跟 assistant tool_calls逐条消息转换时乱序按轮次转换,保持 assistant → tool* → assistant
工具错误被当成成功,模型基于错误文本继续推理OpenAI-compatible 无is_error字段使用ok:false错误信封
并行工具只回填一部分,模型丢上下文或重复调用未等待所有 tool_use 闭合并行调用使用 barrier
streaming JSON 解析失败,参数半截括号不闭合边收边 parsearguments按 tool call id 聚合完整 delta 后 parse
XML 工具调用漏解析,Qwen 类模型输出<tool_call>文本只实现 OpenAI JSON parseradapter 支持模板特定 parser
thinking 状态丢失,DeepSeek thinking mode 后续 400只转换 tool call,未保留 reasoning 字段reasoning passthrough 独立于 tool_result 管理
工具结果注入攻击,模型执行工具输出中的恶意指令将 tool_result 当成用户意图system 层明确工具输出 ≠ 用户命令,高危工具加权限边界

验证清单(摘要)

基础转换上线前,至少覆盖以下用例:

  • 单工具闭环:user → assistanttool_usetool_result→ assistant text,确认 id、name、input、result 完整。
  • 文本加工具混合输出:assistant 同时输出 text block 和 tool_use block,text 不丢失,工具执行。
  • 多工具并行:一轮返回两个以上 tool_use / tool_calls,确认所有结果聚合回填,顺序稳定。
  • 工具错误:工具抛异常、超时、权限拒绝时,Anthropic 输出is_error: true,OpenAI-compatible 输出ok:falseenvelope。
  • 未知 id:构造不存在的tool_use_id/tool_call_id,兼容层应拒绝继续请求模型。
  • 重复 id:同一个 id 回填两次,应在本地报错而非发送给模型。
  • 缺失结果:pending tool use 未闭合时,禁止进入下一轮生成。
  • 流式参数arguments分多片到达,结束后 JSON parse 和 schema validate 成功。
  • 非法 JSON 参数:模型输出半截 JSON 或类型错误,兼容层返回结构化工具调用错误,不执行工具。
  • XML parser:对使用<tool_call>模板的模型,确认 parser 能生成同一套 ToolCall IR。
  • 历史重放:20 轮工具调用历史从 Anthropic 转 OpenAI-compatible 再转回,状态机仍闭合。
  • 注入防护:工具结果含“忽略系统提示”等文本时,模型不应当作新指令执行。
  • provider 差异:DeepSeek、Qwen 及其他后端各自运行 golden transcript,不只测单轮 happy path。

总结

Anthropictool_use/tool_result到 OpenAI-compatible function calling 的转换,本质是两个协议状态机之间的映射。字段改名只是最表层:input_schemaparameterstool_use.inputfunction.argumentstool_use.idtool_call_id均可机械完成;真正决定稳定性的,是消息顺序、ID 配对、错误语义、停止原因和 content block 生命周期。

对于 Claude Code 迁移到 DeepSeek、Qwen 等非 Anthropic 模型的场景,最稳妥的实现路径是引入内部ToolCallIR/ToolResultIR,用 pending ledger 管控闭合状态,用 provider adapter 负责解析和渲染差异。只要状态机正确,模型差异可以局部适配;如果状态机错误,再强的模型也会表现成“不会用工具”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询