共计 8420 个字符,预计需要花费 22 分钟才能阅读完成。
最近写一个内部用的 agent 调度器,需要同时对接 [[OpenAI]]、[[Anthropic]] 和 [[Google Gemini]] 三家的接口,原本以为大同小异,无非是 HTTP 请求加 JSON 数据,结果真正动手才发现,三家在请求体结构、消息组织方式、工具调用约定、流式响应分块等细节上的差异,远远超出了我的想象。最让我意外的是,即便是同一个供应商,比如 OpenAI,自己内部也存在两套并行的接口规范,新旧之间还要应付不同的迁移路径。
写这篇文章不是为了给出一套统一的最佳实践,而是把我这些天踩坑过程中的观察记录下来,方便自己后续翻阅,也希望能给同样在做多模型对接的朋友一点参考。

为什么会有这么多套接口
时间倒回到 2022 年底,[[ChatGPT]] 引爆全民关注之前,大模型 API 的设计可以说是百家争鸣。OpenAI 在 2023 年 3 月发布了 Chat Completions 接口,把对话场景抽象成 messages 数组的结构,配合 system、user、assistant 三种角色,定义了第一个被广泛接受的范式。当时几乎所有后续入场的玩家都默认参考这套设计,包括开源社区的 vLLM、Ollama,以及云厂商如 Groq、Together、Mistral 等等,都提供了 OpenAI 兼容端点。
然而,事情并没有就此停下。Anthropic 在做 [[Claude]] 的时候,从一开始就走了一条不同的路,他们把 system 提取成独立参数而不是消息中的一种角色,把 max_tokens 设为必填,把响应内容拆成 content blocks 数组。这些设计上的不同看似细节,背后其实反映的是对话语义的差异理解。Google 则更进一步,在 Gemini 上用 contents 替代 messages,用 parts 表达多模态分块,整体接口风格更接近他们自家的 Vertex AI 生态。
到 2024 年下半年,OpenAI 又推出了 Responses API,试图把工具调用、状态保持、内置工具(如 web search、code interpreter)整合成一个更高层级的抽象,这又让接口的复杂度上升了一个台阶。所以当下要做多模型对接,面对的不是三家,而是三家加上各种历史版本的混合战场。
OpenAI 的接口家族
OpenAI 目前有两套并行使用的对话接口,分别是经典的 Chat Completions 和较新的 Responses API,前者是事实上的行业标准,后者代表了官方推荐的演进方向。理解这两套之间的关系,是用好 OpenAI API 的前提。
Chat Completions 接口的请求路径是 /v1/chat/completions,请求体的核心字段是 model 和 messages。messages 是一个数组,每个元素包含 role 和 content,role 可以是 system、user、assistant、tool 四种之一。文本类的 content 直接是字符串,多模态的 content 则是一个包含 type 为 text 或 image_url 的对象数组。工具调用通过顶层的 tools 字段声明,模型决定调用时会在响应里返回 tool_calls 数组,调用方执行完工具后,再以一条 role 为 tool 的消息把结果传回。
{
"model": "gpt-4o",
"messages": [
{"role": "system", "content": "你是一个简洁的助手"},
{"role": "user", "content": "今天上海天气如何"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询城市天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}
],
"tool_choice": "auto",
"stream": false
}
响应结构上,Chat Completions 返回一个包含 choices 数组的对象,每个 choice 里有 message、finish_reason 和 index 字段,usage 字段在响应根部,记录了 prompt_tokens、completion_tokens 和 total_tokens。流式响应通过 SSE 推送多个 chunk,每个 chunk 的 delta 字段是增量内容,最后一个 chunk 的 finish_reason 不为空表示生成结束。这套模式简单直接,绝大多数客户端 SDK 都能轻松对接。
Responses API 则是 OpenAI 在 2024 年末推出的下一代接口,请求路径是 /v1/responses。它把 messages 改名为 input,可以传入字符串也可以传入数组,把 system 改名为 instructions 放到顶层。最大的差别在于内置工具的支持,比如 web_search、file_search、code_interpreter 可以直接在 tools 里声明类型而无需自己实现,模型调用时由 OpenAI 平台代为执行,结果直接回填到响应里。另一个亮点是状态保持,通过 previous_response_id 可以延续上一轮对话而不必每次重传完整历史,这对长对话场景的成本控制非常有意义。
不过 Responses API 也带来了新的复杂度,响应里的 output 字段是一个数组,包含多个不同类型的 item,比如 reasoning、message、function_call 等等,需要遍历处理。对于只用最终文本的简单场景,SDK 提供了 response.output_text 这样的便捷属性,但是一旦涉及到流式或者复杂的工具链,开发者就必须理解整套事件协议。
Anthropic Messages 接口
Anthropic 的 [[Claude]] 系列从 Claude 2 时代就采用了独立设计的 Messages API,请求路径是 /v1/messages,需要在请求头里带上 anthropic-version 这个版本声明。整体结构和 OpenAI 看起来相似,但是几个关键差异会让初次对接的人很容易踩坑。
第一个差异是 system prompt 的位置。Anthropic 把 system 作为顶层参数而不是 messages 中的一项,这意味着你不能像 OpenAI 那样在对话中途插入 system 消息。这个设计源于 Anthropic 对系统指令角色的严格定义,他们认为 system 应该是整个会话的稳定背景而不是可变的消息体。
第二个差异是 max_tokens 必填。OpenAI 的 max_tokens 可以省略,模型会按照默认上限生成;Anthropic 则强制要求开发者明确指定,这是为了避免开发者意外消耗大量 token,体现了他们对成本控制的偏好。
第三个差异是 content 的内容块结构。Anthropic 的 message content 既可以是字符串,也可以是一个包含 type 字段的对象数组,对象类型包括 text、image、tool_use、tool_result 等。响应里的 content 也是这种数组结构,模型如果调用了工具,就会返回一个 type 为 tool_use 的块,调用方执行后以一条 user 消息回传,其中 content 包含一个 type 为 tool_result 的块。这种统一的块结构在表达多模态和复杂工具流时非常清晰,但是和 OpenAI 那种纯文本 content 的简单场景对比,处理起来要多写一些代码。
{
"model": "claude-opus-4-7",
"system": "你是一个简洁的助手",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "今天上海天气如何"}
],
"tools": [
{
"name": "get_weather",
"description": "查询城市天气",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
],
"tool_choice": {"type": "auto"}
}
响应的字段命名也不一样,OpenAI 用 finish_reason,Anthropic 用 stop_reason,取值集合也有差异,比如 Anthropic 有 end_turn、tool_use、max_tokens、stop_sequence 这几种。token 使用统计字段叫 usage,里面的字段是 input_tokens 和 output_tokens 而不是 prompt 和 completion,此外还有 cache_creation_input_tokens 和 cache_read_input_tokens 用来表达 prompt caching 命中情况,这是 Anthropic 比较早就提供的成本优化能力,对长 system prompt 场景非常有用。
流式响应方面,Anthropic 的事件类型比 OpenAI 更丰富,包括 message_start、content_block_start、content_block_delta、content_block_stop、message_delta、message_stop 等多种事件。这个设计的好处是可以精确知道当前流到了哪种 content block 的什么位置,对前端实时渲染特别友好,但是对应的事件处理逻辑也更复杂。
Google Gemini 接口
Google 的 Gemini API 走的是又一种风格,整体接口形态明显带有 Google 自家 protobuf 生态的痕迹。请求路径是 /v1beta/models/{model}:generateContent 或者 /v1beta/models/{model}:streamGenerateContent,模型名直接拼接在 URL 路径里,这一点和前两家把 model 放在请求体里的做法不同。
认证方式上 Google 也比较特立独行。OpenAI 和 Anthropic 都是用 Bearer token 放在 Authorization 头里,Google 则支持 x-goog-api-key 头或者 URL 查询参数 key=YOUR_KEY 两种方式。对于云上的 Vertex AI 端点,又会走完整的 IAM 身份认证流程,需要 OAuth token,这对纯调用 API 的场景来说显得稍微繁琐。
请求体里 Google 把对话内容字段命名为 contents 而不是 messages,每个元素包含 role 和 parts,role 只有 user 和 model 两种,没有 system 角色。parts 是一个数组,可以包含文本、图片、视频、文件等多种类型的内容块,这种 parts 结构对多模态原生支持非常好。system 指令通过顶层的 systemInstruction 字段传入,结构同样是 parts。生成参数都放在 generationConfig 这个嵌套对象里,包括 temperature、maxOutputTokens、topP、topK 以及 responseMimeType 和 responseSchema 等结构化输出控制。
{
"contents": [
{
"role": "user",
"parts": [{"text": "今天上海天气如何"}]
}
],
"systemInstruction": {
"parts": [{"text": "你是一个简洁的助手"}]
},
"tools": [
{
"functionDeclarations": [
{
"name": "get_weather",
"description": "查询城市天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
]
}
],
"generationConfig": {
"temperature": 0.7,
"maxOutputTokens": 1024
}
}
响应结构上,Gemini 用 candidates 数组承载多个候选输出,每个 candidate 包含 content 和 finishReason,content 里又是 parts 数组。token 使用信息放在顶层的 usageMetadata 字段,命名为 promptTokenCount、candidatesTokenCount 和 totalTokenCount。如果模型决定调用函数,会在 parts 里返回一个包含 functionCall 字段的块,调用方需要把执行结果以 functionResponse 块的形式构造成一条 user 消息回传。
值得一提的是,Google 在 2024 年底推出了 OpenAI 兼容端点,可以让原本用 openai SDK 的代码不修改业务逻辑就调用 Gemini 模型,这降低了切换成本但是丧失了 Gemini 自有的一些独特能力,比如长上下文、特定的安全分类和文件 API 等等。
三家接口的横向对比
把三家放到一张表里对比,几个核心维度的差异会更清晰。从字段命名来看,OpenAI 用 messages 和 finish_reason,Anthropic 用 messages 和 stop_reason,Google 用 contents 和 finishReason。表达角色时,OpenAI 用 system/user/assistant/tool 四种,Anthropic 用 system(独立参数)/user/assistant 三种,Google 只用 user/model 两种并配合 systemInstruction。这些命名差异看似无关紧要,但当你需要写一层适配代码的时候,每一个字段都得逐个映射。
工具调用的语义虽然三家都支持,但是回传格式很不一样。OpenAI 用 tool 角色的消息承载工具结果,并通过 tool_call_id 关联请求;Anthropic 用 user 消息里的 tool_result 内容块承载,并通过 tool_use_id 关联;Google 用 user 消息里的 functionResponse 块承载,通过 name 关联。如果工具调用链路很长,三家的状态管理方式差异会进一步放大代码复杂度。
多模态支持上,三家都支持图片输入,但是输入方式有别。OpenAI 在 content 数组里用 image_url 类型,传入 URL 或 base64;Anthropic 在 content 里用 image 类型,必须以 base64 或者 file_id 形式提供;Google 在 parts 里用 inlineData 或 fileData 字段,可以引用 Files API 上传的文件。对视频和音频的支持,Google 的覆盖面更广,Anthropic 在 2025 年开始支持 PDF 直接输入,OpenAI 则更多依赖 Responses API 的文件相关工具来处理。
流式响应方面,三家都使用 SSE,但事件结构差别很大。OpenAI 的 chunk 是 delta 增量,事件类型单一;Anthropic 的事件类型多达六七种,可以精确表达每个 content block 的边界;Google 的 streamGenerateContent 端点返回的是 JSON 数组的流式追加,每个元素是一个完整的 partial 响应。对前端来说,OpenAI 最简单,Anthropic 最精细,Google 最特殊。
成本控制和性能优化能力上,Anthropic 的 prompt caching 在长 system prompt 场景下非常实用,OpenAI 的 cached prompt 和 batch API 各有优势,Google 的 context caching 也提供了类似能力。三家都支持结构化输出,但是约束机制不同,OpenAI 通过 response_format 和 json_schema 实现,Anthropic 主要依靠 tool_use 模拟,Google 通过 responseSchema 直接接受 JSON Schema。
OpenAI 兼容标准的形成
虽然三家接口在底层差异明显,但是 OpenAI Chat Completions 的格式已经事实上成为行业默认。这个现象有点像早年 SQL 标准化之前的数据库市场,大家都各自实现,但是渐渐都会向占据领先位置的那一家靠拢,至少在表层兼容。
目前提供 OpenAI 兼容端点的服务商包括但不限于 Anthropic 自己(通过 anthropic-openai-compat 路径)、Google Gemini(/v1beta/openai 端点)、xAI Grok、Mistral、Together AI、Groq、Fireworks、DeepSeek、Moonshot 等。本地部署的 [[Ollama]]、[[vLLM]]、[[LM Studio]] 也都默认暴露兼容端点。这意味着你的代码可以只写一份,通过切换 base_url 和 api_key 就能在十几个供应商之间切换,对小型项目或者快速验证非常有价值。
不过兼容并不等于完整。OpenAI 兼容端点通常只覆盖了 Chat Completions 的基础功能,对于供应商自家独有的高级能力,比如 Anthropic 的 prompt caching、Google 的 grounding、长上下文优化等等,兼容端点要么不支持要么需要通过特殊字段开启。所以在真正的生产系统里,如果想用尽某家模型的潜力,最终还是得用原生接口。
我自己最近的做法是写一个统一的内部接口层,对外暴露一套简化的对话原语(如 send、stream、tool_call),对内根据当前选用的 provider 适配到原生 API。这样既保留了用尽各家原生能力的可能性,又不至于让业务代码被供应商细节侵入。这个抽象层的成本是真实存在的,但只要项目规模超过单一脚本,长期看是划算的投入。
一些选型与对接建议
如果项目还在原型阶段,又只用一家模型,那么直接用官方 SDK 是最快的选择,OpenAI 的 openai 包、Anthropic 的 anthropic 包、Google 的 google-genai 包都已经相当成熟,文档和示例丰富,社区问题也很容易找到答案。原型期不要急于做抽象,等业务跑通之后再回头看模型选择和接口形态是否值得复杂化。
如果项目需要多模型切换,但是只用基础对话能力,那么 OpenAI 兼容端点是性价比最高的方案。配合 [[LiteLLM]]、[[OpenRouter]] 这样的代理层,可以做到一行代码切换供应商,对成本敏感或者要做容灾的场景非常友好。
如果业务深度依赖某家模型的独有能力,比如想用 Anthropic 的长上下文配合 prompt caching 做检索增强,或者想用 Gemini 的多模态原生处理视频流,那就必须用原生接口,并接受相应的代码复杂度上升。这种情况下,把适配层做得轻薄一点,明确标记哪些功能是 provider-specific 的,避免误导未来的维护者。
如果是长期演进的产品级系统,建议从一开始就规划好 provider 抽象的边界,把 prompt 模板、参数配置、错误处理、重试逻辑、token 计量、日志埋点这些横切关注点统一收口,避免到处都是 if provider … 的分支。这一层架构上的投入,远比追新一两个模型版本来得有回报。
工具调用是另一个值得提前规划的点。我建议把工具的 schema 定义抽出来用 [[Pydantic]] 或者 [[Zod]] 维护,然后通过适配代码生成各家所需的 schema 格式,这样修改工具定义只需要改一个地方。错误处理上,三家对工具执行失败的语义略有不同,最好统一封装成 ToolError 这种自定义异常,由适配层负责映射。
最后
写了这么多接口对比,我自己最大的感受是,所谓的 API 标准化在 AI 大模型时代其实远远没有到来。OpenAI 凭借先发优势让自家格式成为事实标准,但是其他玩家不愿意完全跟随,因为各自的产品定位和能力侧重确实有别。这种局面在短期内不会改变,作为开发者,我们能做的是接受这种异质性,并通过良好的抽象层把这种异质性隔离在业务代码之外。
更深一层的思考是,接口形态最终反映的是供应商对对话本质的理解。OpenAI 把对话看作消息列表,简洁直接;Anthropic 把对话看作内容块的有序组合,强调结构化表达;Google 把对话看作多模态部分的流式合成,强调原生融合。哪一种更接近"对话"的本质很难说,但是当我们在写 prompt、设计 agent、构建多轮交互时,这些底层假设会以微妙的方式影响我们的思维方式。
对于即将开始多模型对接的朋友,我的建议是先把官方文档读两遍,再写个最小可运行 demo 跑通三家,最后再考虑抽象层。这个过程虽然慢,但是能让你对各家接口形成第一手的感性认识,比直接看对比文章要扎实得多。希望这篇文章能给你少踩几个坑,剩下的就靠自己亲手去试了。
