OpenAI 协议和 Anthropic 协议到底差在哪里?从一条请求走到工具调用

9,451 字#OpenAI #Anthropic #LLM #Agent #API

你把模型供应商从 OpenAI 换成 Anthropic,最初通常只改一个 base_url 和一个 model。真正接入工具调用、流式输出和多模态之后,问题就出现了:同样叫 messages 的字段,为什么内容结构不一样?OpenAI 返回 tool_calls,Anthropic 返回 tool_use,它们能不能直接互换?

答案是:它们解决的是同一个问题,但不是同一个协议。 本文真正比较的主线,是 OpenAI Responses API、OpenAI Chat Completions 和 Anthropic Messages API;这里的“OpenAI 协议”不是一套统一标准。Embeddings 只在后文作为向量接口边界说明,不参与工具调用闭环。

更准确地说,OpenAI 和 Anthropic 都提供了基于 HTTP、JSON 和流式事件的模型 API;社区常说的“OpenAI 协议”“Anthropic 协议”,通常是对各自 API 请求、响应和事件约定的简称,而不是一个由双方共同维护的网络标准。

本文用一条任务贯穿对比:

用户问:“帮我查询北京今天的天气,如果需要调用天气工具就调用它,然后给我一句简短建议。”

文章不只比较 OpenAI Responses API 和 Anthropic Messages API。OpenAI 至少要分清三类接口:Responses、Chat Completions 和 Embeddings。本文按新项目优先使用 Responses 来组织示例;Chat Completions 主要用于维护已有代码、兼容现有网关,或明确依赖其 messages / choices 结构的场景。Embeddings 则负责把文本转换成向量,不能因为它们都使用 JSON 就把它们当成同一种“聊天协议”。

本文的主线仍然是一次工具调用,但会以 OpenAI Responses 与 Anthropic Messages 为主对照,再解释 Chat Completions 的兼容差异和 Embeddings 的独立边界。OpenAI Responses API 使用自己的 input、output item、response event 和工具模型,不能把 Chat Completions 的字段机械套过去。官方入口:

先建立一个正确的分层模型

一次模型调用至少有四层:

你的 Agent 业务代码
        ↓  把内部消息转换成供应商格式
OpenAI Responses / Anthropic Messages
        ↓  HTTPS + JSON,请求和响应
供应商的生成模型服务
        ↓
文本、工具调用、图片内容、流式事件

Chat Completions 是另一种 OpenAI 生成接口,主要承担兼容与迁移场景。

文档检索链路另行使用 Embeddings API:
文本 → 向量 → 向量数据库 → 检索结果 → 生成接口

这里容易混淆三件事:

名称它解决的问题例子
API 协议约定请求和响应字段怎么组织messages、input、tool_calls
向量接口约定文本如何变成可检索的向量input、model、embedding
传输方式数据怎样到达服务端HTTPS、JSON、SSE 流式响应
Agent Loop工具调用后如何继续任务模型请求 → 执行工具 → 把结果发回模型

模型不会直接读取你的数据库,也不会真的执行本地函数。模型只能返回一段文字或结构化的工具调用意图;真正执行工具的是你的程序。两套协议的差异,主要发生在“如何描述消息”和“如何表达这个意图”这两个位置。

OpenAI 的三类接口:Responses、Chat Completions 与 Embeddings

这三个名字经常同时出现在 SDK 里,但职责完全不同。对于新写的 OpenAI Agent 或多工具应用,可以优先从 Responses API 开始;Chat Completions 仍然有大量存量用户和兼容层,不能当作已经消失的接口。

接口输入模型输出模型典型用途
Responsesinput,也可以带消息和内容块output[],由多个 output item 组成新项目、工具、内置工具和更丰富的响应事件
Chat Completionsmessageschoices[].message 或流式 choices[].delta存量对话、兼容网关、已有 Agent
Embeddings一段或多段文本data[].embedding 向量数组语义搜索、RAG、聚类、相似度计算

Chat Completions:兼容性更强的消息接口

Chat Completions 的核心是 messages。调用方把 system、user、assistant 和 tool 消息整理成一段对话历史,模型返回一个或多个 choice。它的典型读取路径是:

response.choices[0].message.content
response.choices[0].message.tool_calls

Chat Completions 仍适合大量已有的聊天和 Agent 代码,也适合需要兼容 OpenAI 风格 /v1/chat/completions 网关的场景。但对于新项目,不应因为旧 SDK 示例很多就默认选择它。它的消息角色和返回结构不能代表 Responses API 的 item 结构。

OpenAI Responses:新项目优先考虑的响应接口

Responses API 更像一个统一的响应容器。本文用它代表 OpenAI 新项目的一种组织方式,而不是把“Responses 优先”当成所有项目都必须遵守的规则。简化后的请求可以写成:

{
  "model": "gpt-4.1",
  "instructions": "你是一个简洁的天气助手。",
  "input": "查询北京今天的天气,并给我一句建议。",
  "tools": [
    {
      "type": "function",
      "name": "get_weather",
      "description": "查询指定城市的天气",
      "parameters": {
        "type": "object",
        "properties": { "city": { "type": "string" } },
        "required": ["city"]
      }
    }
  ]
}

Responses 的结果不是 Chat Completions 的 choices[0].message,而是 output 中的 item。例如,文本输出可以是 type: "message" 的内容,函数调用则是 type: "function_call" 的 item。工具执行后,程序再把 function_call_output 作为后续输入交给模型。SDK 提供的 output_text 是便捷读取方式,不应当被误认为底层响应只有一个字符串。

因此,Chat Completions 和 Responses 至少有这些差异:

语义Chat CompletionsResponses
主要输入messagesinput,另有 instructions 等字段
文本读取choices[].message.contentoutput 中的 message content,SDK 可读 output_text
函数调用message.tool_calls[]output 中的 function_call item
工具结果role: "tool" + tool_call_idfunction_call_output 输入项,对应调用 ID
流式模型completion chunkresponse / output item 事件

这也是为什么适配器不能只写一个 OpenAIAdapter,然后默认 Chat 和 Responses 共用一套序列化代码。正确做法是先统一成内部的 AssistantTurn、ToolCall 和 ToolResult,再分别实现 Chat adapter 与 Responses adapter。

Embeddings:向量接口,不是聊天接口

Embeddings 解决的问题不是“让模型回答什么”,而是把输入文本映射成固定维度的浮点数向量。典型请求是:

{
  "model": "text-embedding-3-small",
  "input": [
    "北京今天会下雨吗?",
    "北京今日天气预报"
  ],
  "encoding_format": "float"
}

响应的核心结构是:

{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [0.0123, -0.0456, 0.0789]
    }
  ],
  "model": "text-embedding-3-small",
  "usage": {
    "prompt_tokens": 12,
    "total_tokens": 12
  }
}

实际向量会有更高维度,上面的数组只是示意。Embedding 的数据流通常是:

原始文档
  → 切分 chunk
  → 调用 Embeddings API
  → 得到向量
  → 写入向量数据库
  → 查询文本也向量化
  → 计算相似度并取回文档
  → 再交给 Chat 或 Responses 生成答案

它和 Chat、Responses 的边界很重要:

  • Embeddings 没有 messages、tool_calls 或对话历史。
  • Embeddings 不会生成自然语言答案,也不会执行工具。
  • Embeddings 通常不走 SSE 文本流,而是一次返回向量结果。
  • 建库和查询必须使用兼容的 embedding 模型、预处理方式和向量维度。
  • 换 embedding 模型后,旧向量通常不能直接与新向量混用,需要重新生成索引。
  • 向量接口的成功,只说明向量生成成功,不说明 RAG 的召回质量或最终回答正确。

Anthropic 的 Messages API 是生成式消息接口;Anthropic 官方 API 并没有一个可以直接等同于 OpenAI /v1/embeddings 的通用 Embeddings endpoint。需要使用 Anthropic 模型做生成、另一个供应商做向量化时,Embedding provider 应在架构中单独抽象,而不是假设每个 LLM provider 都同时提供向量服务。

同一个任务,两个请求长什么样

OpenAI Responses API

下面用新项目优先的 Responses API 表达天气查询。字段删去了鉴权、超时和其他可选参数,只保留理解协议所需的部分:

{
  "model": "gpt-4.1",
  "instructions": "你是一个简洁的天气助手。",
  "input": "查询北京今天的天气,并给我一句建议。",
  "tools": [
    {
      "type": "function",
      "name": "get_weather",
      "description": "查询指定城市的天气",
      "parameters": {
        "type": "object",
        "properties": {
          "city": { "type": "string", "description": "城市名" }
        },
        "required": ["city"],
        "additionalProperties": false
      }
    }
  ],
  "tool_choice": "auto"
}

Responses API 的典型结构是:

  • instructions 承担系统级指令,input 承担本轮输入或消息内容。
  • 工具定义直接位于 tools 数组中,函数工具使用 name、description 和 parameters。
  • 模型返回的是 output item,而不是 Chat Completions 的 choices[].message。
  • 模型发起函数调用时,响应中通常会有 type: "function_call" 的 output item。
  • 程序执行完工具后,用 function_call_output 输入项把结果关联回原调用。

Anthropic Messages

Anthropic Messages API 的请求表达相似任务时,结构更偏向“消息由内容块组成”:

{
  "model": "claude-sonnet-4-5",
  "max_tokens": 1024,
  "system": "你是一个简洁的天气助手。",
  "messages": [
    {
      "role": "user",
      "content": "查询北京今天的天气,并给我一句建议。"
    }
  ],
  "tools": [
    {
      "name": "get_weather",
      "description": "查询指定城市的天气",
      "input_schema": {
        "type": "object",
        "properties": {
          "city": { "type": "string" }
        },
        "required": ["city"]
      }
    }
  ]
}

Anthropic 的关键特点是:

  • system 通常是顶层字段,而不是 messages 中一个 role: "system" 的元素。
  • 一条消息的 content 可以是字符串,也可以是内容块数组。
  • 工具定义直接包含 name、description 和 input_schema。
  • 模型发起工具调用时,assistant 的 content 中会出现 type: "tool_use" 的内容块。
  • 程序返回工具结果时,通常继续发送一条 role: "user" 的消息,其中包含 type: "tool_result" 内容块。

因此,Responses 和 Anthropic Messages 虽然语义对应,却不能直接复制字段:

语义OpenAI ResponsesAnthropic Messages
系统提示instructions顶层 system
本轮输入inputmessages[].content
工具名称namename
工具参数 Schemaparametersinput_schema
模型发起调用output[] 中的 function_callassistant.content[] 中的 tool_use
调用参数arguments,通常是 JSON 字符串input,通常已经是对象
调用 IDcall_idtool_use.id
工具结果function_call_output 输入项role: user + tool_result 内容块

最重要的差异:角色模型不同,内容模型也不同

OpenAI 更像“消息角色 + 可选字段”

在 Chat Completions 中,开发者通常先看消息的 role,再根据角色读取字段:

{
  "role": "assistant",
  "content": "我先查询一下。",
  "tool_calls": [
    {
      "id": "call_123",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\":\"北京\"}"
      }
    }
  ]
}

注意两个细节:

  1. content 可能为空,但 tool_calls 不为空。不能用“没有文本”判断模型没有返回内容。
  2. arguments 经常是 JSON 字符串。解析失败、流式拼接不完整、模型输出非法 JSON,都需要由适配层处理。

Anthropic 更像“角色消息 + 内容块数组”

Anthropic 的 assistant 消息可能是这样:

{
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "我先查询一下。"
    },
    {
      "type": "tool_use",
      "id": "toolu_123",
      "name": "get_weather",
      "input": {
        "city": "北京"
      }
    }
  ]
}

这套结构的好处是文本、图片、工具调用和其他内容可以放在同一条消息中;代价是调用方不能只写一个 message.content 字符串处理所有情况。

工程上不要把 Anthropic 的 content 强行压扁成字符串,也不要把 OpenAI 的 tool_calls 假设成普通文本。适配器应该先把供应商响应转换成自己的中间表示:

struct AssistantTurn {
    text: Option<String>,
    tool_calls: Vec<ToolCall>,
    stop_reason: StopReason,
}

struct ToolCall {
    id: String,
    name: String,
    arguments: JsonValue,
}

供应商字段只在边界层出现,Agent Loop 只认识 AssistantTurn。这比在业务代码里到处写 if openai ... else anthropic ... 更容易维护。

工具调用:模型提议,程序执行,结果再回传

两套协议都遵循同一个闭环:

sequenceDiagram
    participant U as 用户
    participant A as Agent 程序
    participant M as 模型 API
    participant T as 工具

    U->>A: 查询北京天气
    A->>M: 消息 + 工具定义
    M-->>A: tool call: get_weather(city=北京)
    A->>T: 执行本地或远程函数
    T-->>A: 返回温度、天气和更新时间
    A->>M: 原 assistant 调用 + tool result
    M-->>A: 最终文字回答
    A-->>U: 今天北京……

OpenAI Responses 的回传形式

Responses 不把工具结果包装成 role: "tool" 消息,而是把它作为下一次输入中的 function_call_output item,并通过 call_id 关联原来的函数调用:

[
  {
    "type": "function_call_output",
    "call_id": "call_123",
    "output": "{\"temperature\":18,\"condition\":\"晴\"}"
  }
]

如果使用 previous_response_id 维持响应链,还需要按照 Responses API 的规则传递前一响应关系;如果自行保存完整历史,则应保留函数调用 item 和对应结果。

OpenAI Chat Completions 的回传形式(兼容格式)

程序拿到 tool_calls 后,需要保留 assistant 的工具调用消息,再追加工具结果:

[
  {
    "role": "assistant",
    "content": null,
    "tool_calls": [
      {
        "id": "call_123",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\":\"北京\"}"
        }
      }
    ]
  },
  {
    "role": "tool",
    "tool_call_id": "call_123",
    "content": "{\"temperature\":18,\"condition\":\"晴\"}"
  }
]

Anthropic 的回传形式

Anthropic 要把模型原样返回的 tool_use 内容放进 assistant 消息,然后用 user 消息携带 tool_result:

[
  {
    "role": "assistant",
    "content": [
      {
        "type": "tool_use",
        "id": "toolu_123",
        "name": "get_weather",
        "input": { "city": "北京" }
      }
    ]
  },
  {
    "role": "user",
    "content": [
      {
        "type": "tool_result",
        "tool_use_id": "toolu_123",
        "content": "{\"temperature\":18,\"condition\":\"晴\"}"
      }
    ]
  }
]

这里最容易出错的不是字段名,而是消息历史的完整性:

  • 不要只保存工具结果,丢掉模型原来的工具调用。
  • 不要用数组下标匹配工具调用,要使用调用 ID。
  • 一次 assistant 消息可能包含多个工具调用,程序应决定串行还是并行执行。
  • 工具失败也要回传结构化错误,而不是让 Agent Loop 永久等待。
  • 工具结果进入上下文后,模型才有机会生成最终回答;工具本身不会替模型完成总结。

流式响应:同一个 SSE 外壳,里面的事件不同

流式输出一般通过 Server-Sent Events,也就是 SSE 返回一系列事件。很多 SDK 把它包装成迭代器,但底层事件仍然不是同一套。

OpenAI Responses 的典型思路

Responses 流式返回 response 和 output item 生命周期事件。文本、函数调用开始、函数参数增量和响应结束,分别由不同类型的 response event 表达。不要把它当成 Chat Completions 的 choices[].delta:

response.created
response.output_item.added
response.output_text.delta
response.function_call_arguments.delta
response.function_call_arguments.done
response.output_item.done
response.completed

函数参数同样可能分多次到达。必须按 call_id 累积参数,等参数完成后再解析和校验,不能收到第一段 JSON 就执行工具。

OpenAI Chat Completions 的典型思路(兼容格式)

OpenAI Chat Completions 流式返回的是一系列 chat.completion.chunk。文本增量通常在:

{
  "choices": [
    {
      "delta": {
        "content": "北京今天"
      }
    }
  ]
}

工具调用也会被拆成增量片段。尤其是 function.arguments 可能分多次到达:

"{\"city\":"
"\"北京\"}"

所以不能收到第一段就执行工具。正确做法是按 tool_call 的索引或 ID 累积名称和参数,等确认调用结束后再解析 JSON。

Anthropic 的典型思路

Anthropic 流式事件更明确地区分消息和内容块生命周期,常见事件包括:

message_start
content_block_start
content_block_delta
content_block_stop
message_delta
message_stop

文本增量和工具输入增量分别出现在内容块事件中。工具输入通常需要将多个 input_json_delta 片段拼起来,等内容块结束后再解析。

两套流式事件的设计差异可以概括成:

需求OpenAI ResponsesOpenAI Chat CompletionsAnthropic Messages
文本增量response output text 事件choices[].delta.contentcontent_block_delta 中的文本增量
工具增量function call arguments 事件delta.tool_callsinput_json_delta 等内容块事件
结束判断response.completed 等 response 事件finish_reason 或 [DONE]message_stop、内容块结束等事件
解析策略依赖 response event、item 和 call ID依赖 choice、index 和字段增量依赖消息、内容块和事件类型

适配层最好不要向上层暴露原始 SSE 事件,而是统一成类似下面的事件:

TextDelta("北京今天")
ToolCallStarted(id, name)
ToolArgumentsDelta(id, "{\"city\":")
ToolCallFinished(id)
Usage(input_tokens, output_tokens)
TurnFinished(reason)

这样 UI 只负责显示文本,Agent Loop 只负责等待 ToolCallFinished 后执行工具,不需要知道某个供应商叫它 content_block_stop 还是 [DONE]。

系统提示和上下文管理,不能只做字段搬运

两套 API 都支持系统级指令,但位置不同只是表面问题,更深的问题是:你的内部上下文模型必须先把“系统指令”和“对话消息”分开。

推荐的内部结构是:

RequestContext
├── system_instructions: Vec<TextOrBlock>
├── messages: Vec<UserOrAssistantMessage>
├── tools: Vec<ToolDefinition>
├── generation: GenerationOptions
└── metadata: RequestMetadata

发送到 OpenAI Chat Completions 时,可以把系统指令转换成 messages 中的 system 消息;发送到 OpenAI Responses 时通常对应顶层 instructions;发送到 Anthropic 时则对应顶层 system。如果内部结构一开始就把 system 当作普通 user/assistant 消息,后面会遇到这些问题:

  • 不同供应商对 system 消息位置的要求不同。
  • 压缩上下文时,不应把系统指令和普通历史一起随意删掉。
  • 多段系统指令可能需要合并、分块或加入缓存控制信息。
  • 某些供应商专属参数不能无损迁移,应该在 provider options 中单独保存。

同理,temperature、最大输出 token、停止原因、缓存、思维过程、引用和供应商扩展,也不能假设两边具有完全相同的含义。适配器的目标应是保留业务真正需要的语义,而不是制造一个看起来字段完全对齐的假统一接口。

多模态消息:字符串只是最小公分母

OpenAI 和 Anthropic 都可以在消息中表达图片等内容,但内容块的类型和字段并不相同。一个跨供应商的内部消息至少应区分:

Text("请描述这张图片")
ImageUrl("https://...")
ImageBytes(mime="image/png", data=...)
ToolResult(...)

然后分别转换:

  • OpenAI 侧转换为它支持的文本和图片内容对象。
  • Anthropic 侧转换为对应的 text、image 等内容块。

不要把图片 URL 当成普通文本拼进 prompt。这样模型可能只看到一串地址,而不是图片内容;也不要默认所有供应商、模型和账号配置都支持相同的图片格式、尺寸和来源方式。多模态能力必须以具体模型和官方文档为准。

OpenAI 兼容接口到底是什么

很多本地模型服务、代理网关和第三方平台会说“兼容 OpenAI API”。通常它们指的是:

客户端使用 OpenAI SDK
        ↓ 修改 base_url、api_key 或 model
第三方服务接收 /v1/chat/completions 风格请求

这提供了很好的迁移入口,但“兼容”至少有三个等级:

  1. 请求格式兼容:能接受 messages、model、tools。
  2. 响应格式兼容:返回 choices、message、usage,并支持流式 chunk。
  3. 语义兼容:工具调用、并行调用、停止原因、JSON Schema、视觉输入和错误行为都足够接近。

很多服务只做到前两级。普通文本聊天可能正常,遇到工具调用就暴露差异。例如:

  • 工具参数被返回成文本,而不是 tool_calls。
  • 流式响应缺少完整的 tool call index。
  • finish_reason 的取值和 OpenAI 不一致。
  • response_format 被忽略或只支持部分 JSON Schema。
  • usage 只在流结束时返回,甚至不返回。

因此,把 base_url 换掉只能证明 HTTP 请求发出去了,不能证明 Agent 能可靠运行。至少要用文本、图片、单工具、多工具、工具失败和流式工具参数做一组兼容性测试。

Anthropic 也提供了面向 OpenAI SDK 的兼容路径,但官方兼容说明明确提醒:兼容层适合迁移和测试,并不意味着 OpenAI 与 Anthropic 的所有能力和语义完全等价。需要 Anthropic 特有能力时,应直接使用 Anthropic API 或官方 SDK。参考:Anthropic OpenAI SDK compatibility:https://docs.anthropic.com/en/api/openai-sdk。

设计适配器时,推荐三层而不是两个大分支

一个可维护的实现可以拆成三层:

第一层:业务消息模型
  UserMessage / AssistantTurn / ToolResult / Image

第二层:规范化 Agent 事件
  TextDelta / ToolCall / Usage / Error / TurnFinished

第三层:供应商适配器
  OpenAIChatAdapter / AnthropicMessagesAdapter

业务层只表达意图

业务层说“这里有一个工具,名字是 get_weather,参数是这个 JSON Schema”,不关心它应该放在 function.parameters 还是 input_schema。

适配器负责有损转换

适配器要明确记录哪些字段可以转换,哪些字段只能降级或拒绝:

内部 system 指令        → OpenAI Chat 的 `messages[].role=system`
                         → OpenAI Responses 的 `instructions`
                         → Anthropic top-level `system`

内部 ToolCall           → tool_calls + function.arguments
                         → tool_use + input

内部 ToolResult         → role=tool
                         → role=user + tool_result

内部 stop 条件          → provider-specific 参数

如果某个功能没有可靠映射,不要静默丢弃。可以采用三种策略:

  • 能安全降级就降级,并记录 warning。
  • 不能保证语义时返回“不支持该能力”。
  • 业务强依赖时,在能力协商阶段直接拒绝请求。

能力矩阵比一个布尔值更有用

不要只有一个 supports_tools: true。更实用的是:

能力OpenAI ChatAnthropic Messages适配器需要关注
文本输入输出支持支持基本路径
工具调用支持支持调用 ID、参数格式、回传角色
流式文本支持支持事件归一化
流式工具参数支持支持增量拼接和结束判断
图片输入依模型而定依模型而定MIME、来源、大小限制
JSON 结构化输出依模型和接口而定依模型和接口而定不要只看字段名
推理/思维相关字段依接口和模型而定依接口和模型而定不要把内部推理当普通文本

最常见的六个错误

1. 只替换 URL 和模型名

这只适用于最简单的纯文本请求。涉及工具和流式响应时,还要改请求序列化、响应解析和工具结果回传。

2. 把 content 永远当字符串

Anthropic 的 content 可以是内容块数组;OpenAI 的 assistant 消息也可能同时包含文本和工具调用。正确做法是先解析成内部内容列表。

3. 把模型返回的工具调用当成工具结果

tool_use 或 tool_calls 只是模型提出的调用。你的程序必须验证工具名、校验参数、执行函数、限制权限,再把结果回传。

4. 忘记保存原始 assistant 工具消息

下一次请求不仅需要工具结果,还需要让模型看到“自己刚刚调用了什么”。缺少这条历史,供应商可能拒绝请求,也可能让模型失去上下文。

5. 流式收到半截 JSON 就执行工具

工具参数可能跨多个事件到达。必须按调用 ID 累积,确认结束,解析并校验后再执行。

6. 把供应商专属字段伪装成通用字段

例如缓存、引用、推理摘要、服务等级、预测输出或特定响应事件,都可能没有一一对应关系。通用层只应暴露已经定义清楚、跨供应商确实成立的语义。

一套最低限度的适配器测试

如果你正在实现网关、模型路由器或 Agent 框架,建议至少覆盖这些用例:

T1  纯文本:system + user → 最终文本
T2  空文本工具调用:assistant 只返回工具调用
T3  文本 + 工具调用:同一 assistant 消息同时包含两者
T4  单工具结果:成功回传后生成最终答案
T5  工具异常:超时、参数错误、业务错误能回传
T6  并行工具:一次返回两个调用,ID 不串线
T7  流式文本:增量拼接后的文本与非流式一致
T8  流式工具:参数分片后得到合法 JSON
T9  多模态:图片内容没有被错误降级成 URL 文本
T10 向量化:输入顺序、向量维度和 index 能正确对应
T11 取消与重试:取消后不重复执行不可幂等工具

测试断言不要只看 HTTP 状态码。至少还要检查:

  • 规范化后的文本是否正确。
  • 工具名和参数是否保持不变。
  • 工具调用 ID 是否能够闭环。
  • stop reason 是否被正确转换。
  • token usage 是否在支持时被记录。
  • 流式和非流式是否最终得到等价的 Agent 状态。

最后:该选哪套协议

如果你只做 OpenAI 新项目,本文建议优先从 Responses API 开始;如果是在维护已有 Chat Completions 代码、对接固定的 OpenAI-compatible 网关,继续使用 Chat Completions 也完全合理。如果你只做 Anthropic 模型,也不需要为了“统一”先套一层 OpenAI 格式。这里的“优先”是针对本文示例和新项目起点的工程建议,不是要求所有存量项目迁移。

如果你要同时接入多个供应商,建议采用下面的判断:

  • OpenAI 新项目优先使用 Responses API,尤其是 Agent、工具调用、内置工具和多模态场景。
  • 已有 OpenAI 项目可以继续使用 Chat Completions,不要为了迁移而忽视现有兼容成本。
  • 业务代码使用自己的消息、工具和事件模型。
  • 供应商边界使用官方 SDK 或清晰的 HTTP adapter。
  • 纯文本迁移可以依赖兼容接口快速迁移。
  • Agent、工具调用、多模态和流式场景优先验证原生协议语义。
  • 不兼容的能力显式暴露或拒绝,不要静默丢失。

OpenAI Chat Completions、Responses 和 Anthropic Messages 的共同点,是都把生成式模型接入封装成“输入进、结构化响应出”;Embeddings 则是另一条“文本进、向量出”的接口链路。真正的差异在于它们如何建模消息、内容块、响应 item、工具生命周期、流式事件和向量结果。

理解这层差异之后,“换一个模型供应商”就不再是改 URL 的运气题,而会变成一个可以测试、可以降级、可以维护的适配问题。

官方文档


云服务器推荐

如果你需要部署博客、Docker 服务或个人项目,可以参考:

本文包含推广链接。通过链接购买不会增加你的费用,但我可能获得少量佣金。

评论