跨越协议鸿沟:Tool Use状态机从Anthropic到OpenAI兼容体系的适配要点
从工程角度看Claude Code这类Agent的核心并非普通文本生成而是围绕工具调用构建的严格状态机。在Anthropic Messages API中工具调用以tool_usecontent block嵌入assistant消息工具执行结果必须作为下一条user消息的tool_resultblock回填通过tool_use.id与tool_result.tool_use_id配对。这与OpenAI-compatible function calling的设计截然不同后者将工具请求放在assistant消息的tool_calls字段工具结果则分配给独立role: tool消息。当需要将Claude Code的工具调用协议映射到DeepSeek、Qwen等非Anthropic模型或兼容网关时最大风险并非JSON字段名差异而是状态机不对齐。消息角色、content block顺序、工具ID生命周期、错误语义、并行调用、停止原因和流式增量都必须整体转换。仅做input_schema到parameters的字段重命名很容易在多轮工具调用中触发400错误、丢失工具结果、重复执行工具或让模型把工具错误当成普通用户文本继续推理。核心挑战四个必须回答的问题任何跨模型Tool Use兼容层都需要解决以下关键议题工具定义如何转换Anthropic的tools[].input_schema如何映射为OpenAI-compatible的tools[].function.parameters。工具调用如何表达assistant content中的tool_useblock如何转换为assistant消息的tool_calls。工具结果如何回填Anthropic要求tool_result放在下一条user message content数组中OpenAI-compatible通常要求放在role: tool消息内。状态如何闭合每个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_useblockassistant消息的tool_calls[]Anthropic为内嵌blockOpenAI-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_resultrole: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_reasonend_turn → 本轮结束 状态 S1: 等待工具执行 - 收集一个或多个 tool_use block - stop_reasontool_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_schema→parameters同时生成工具能力表ToolCapability{supportsParallelToolUse, supportsStrictJsonSchema, supportsToolRole, requiresXmlParser, requiresReasoningPassthrough}能力表不应靠模型名称字符串散落在代码中判断而应集中配置。例如同样是 Qwen不同部署方式可能分别走 OpenAI-compatible JSON、chat template XML 或平台自定义 function calling。3. 消息转换器消息转换需按“轮次”处理而非逐条孤立转换。Anthropic → OpenAI-compatibleassistant(content:[text, tool_use A, tool_use B],stop_reasontool_use)user(content:[tool_result A, tool_result B, text?])⇒ assistant(content: text, tool_calls:[A, B])tool(tool_call_idA,contentresult A)tool(tool_call_idB,contentresult B)user(content: text?)// 仅当存在真实用户文本时追加OpenAI-compatible → Anthropicassistant(content: text, tool_calls:[A, B])tool(tool_call_idA,contentresult A)tool(tool_call_idB,contentresult B)⇒ assistant(content:[text, tool_use A, tool_use B],stop_reasontool_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 后再 parsetool role 是否严格role: tool必须紧跟 assistant tool_calls历史构造时做顺序校验错误语义是否保留工具失败是否被模型误读为普通结果使用统一ok/errorenvelope是否有额外 reasoning 字段thinking 模型要求回放 reasoning 状态adapter 单独保留并回传是否使用 XML 工具格式Qwen 等模型可能输出tool_callparser 输出统一 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_use或tool_calls将停止原因纳入状态机API 返回缺失tool_resultAnthropic 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 闭合并行调用使用 barrierstreaming JSON 解析失败参数半截括号不闭合边收边 parsearguments按 tool call id 聚合完整 delta 后 parseXML 工具调用漏解析Qwen 类模型输出tool_call文本只实现 OpenAI JSON parseradapter 支持模板特定 parserthinking 状态丢失DeepSeek thinking mode 后续 400只转换 tool call未保留 reasoning 字段reasoning passthrough 独立于 tool_result 管理工具结果注入攻击模型执行工具输出中的恶意指令将 tool_result 当成用户意图system 层明确工具输出 ≠ 用户命令高危工具加权限边界验证清单摘要基础转换上线前至少覆盖以下用例单工具闭环user → assistanttool_use→tool_result→ assistant text确认 id、name、input、result 完整。文本加工具混合输出assistant 同时输出 text block 和 tool_use blocktext 不丢失工具执行。多工具并行一轮返回两个以上 tool_use / tool_calls确认所有结果聚合回填顺序稳定。工具错误工具抛异常、超时、权限拒绝时Anthropic 输出is_error: trueOpenAI-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_schema到parameters、tool_use.input到function.arguments、tool_use.id到tool_call_id均可机械完成真正决定稳定性的是消息顺序、ID 配对、错误语义、停止原因和 content block 生命周期。对于 Claude Code 迁移到 DeepSeek、Qwen 等非 Anthropic 模型的场景最稳妥的实现路径是引入内部ToolCallIR/ToolResultIR用 pending ledger 管控闭合状态用 provider adapter 负责解析和渲染差异。只要状态机正确模型差异可以局部适配如果状态机错误再强的模型也会表现成“不会用工具”。