☰
OpenAI与Anthropic API迁移必备指南:请求体、工具调用与流式输出对照
2026/10/8 9:40:55 网站建设 项目流程

最近接了不下三个项目,都是从 OpenAI 体系往 Anthropic 迁移,或者反过来。每次开会第一句话都是“就是换个 endpoint 和 key,应该很简单吧”,我听完就头大。OpenAI 的 Chat Completions 和 Anthropic 的 Messages API,表面上都叫“大模型接口”,实际请求体、认证方式、返回结构、流式事件、工具调用全都不一样。直接硬切代码,轻则报 400,重则工具调用链路整个断掉,排查半天发现是消息历史格式错了。

这篇文章我就把两套协议的差异一次讲清楚,不绕弯子,直接做字段级对照,给可直接复制的请求示例,再把我在实操里踩过的坑一并列出来。如果你正要接 Claude 或者打算把现有应用从 OpenAI 换到 Anthropic,这篇就是给你准备的。看完你会知道改哪里、怎么改、哪些地方最容易翻车。

1. 为什么两套协议长得这么不一样

1.1 OpenAI 的消息数组:从聊天补全演进出来的统一模型

OpenAI 的 Chat Completions 接口脱胎于早期文本补全(completion)能力,后来为了支持多轮对话,把对话历史统一成一个messages数组,里面分system、user、assistant三类角色。这个设计的核心是“所有上下文都塞进同一个数组”,包括系统提示词、历史对话、工具调用结果,全部按时间顺序排布在消息流里。

这种设计的好处是概念少、理解门槛低。你不需要单独理解什么叫“系统提示词”,它不过是 messages 数组里 role 为 system 的一条消息。后来加入 function calling 和 tool calling,也继续沿用同一套思路:工具调用结果作为一条 role 为tool的消息追加进数组。整条链路就是“消息进、消息出”,状态全部由调用方维护。

但这也带来一个隐含问题:模型对消息数组里各条消息的角色切换有严格要求。比如用户消息之后可以直接跟 assistant 回复,但 assistant 带了tool_calls之后,下一条必须是对应tool角色消息,否则接口直接拒绝。这个我就踩过一次,后面专门讲。

1.2 Anthropic 的顶层 system:把提示词优先级摆到明面上

Anthropic 的 Messages API 走了另一条路。它把system单独提升为请求体的顶层字段,messages数组里只有user和assistant两种角色,不会再出现system角色。这在语义上更清晰:系统提示词是“全局指令”,不该和对话历史混在一起;对话历史则是“一来一回的记录”,两种东西本来就该分开放。

另外一个显著差异是返回结构。Anthropic 的每条 assistant 回复不是一个简单的字符串,而是一个content数组,里面的每个元素是一个“内容块”(content block),可能有text类型,也可能有tool_use类型。这个设计明显是为多模态和工具调用提前铺路:文本、图片、工具调用请求都是不同类型的块,可以按顺序组合在一条回复里。

设计哲学决定了协议差异的根源,迁移的时候不要想着“把字段名替换一下就完事”,你面对的是两种不同的消息模型。理解了这一点,后面的对照表就顺理成章了。

2. 协议差异拆解:请求体、认证、参数与返回结构

2.1 端点和认证头:先改这两个再谈别的

调 API 的第一步永远是地址和鉴权,这两家在这块就完全不同。OpenAI 走的是标准的Authorization: Bearer头,API key 以sk-开头。Anthropic 不走 Bearer,它要求x-api-key头,并且强制带一个anthropic-version版本头,否则接口报错。

Anthropic SDK 会自动帮你带上这两个头,但如果你直接用 HTTP 客户端调,忘记anthropic-version会收到 400 或认证异常,这个细节非常容易被忽略。

维度OpenAIAnthropic
端点POST https://api.openai.com/v1/chat/completionsPOST https://api.anthropic.com/v1/messages
认证头Authorization: Bearer sk-...x-api-key: sk-ant-...
版本头无anthropic-version: 2023-06-01
默认 Content-Typeapplication/jsonapplication/json

我做迁移的时候习惯先把两个端点的连通性单独测一遍,用 curl 打一个最简单的请求,确认网络和鉴权都没问题,再动业务代码。不要一上来就改项目,否则出了问题你分不清是网络、鉴权还是协议的问题。

2.2 消息结构差异:system 的位置和 role 命名

这是协议差异里最核心、最容易踩坑的地方。OpenAI 的 system 提示词是 messages 数组里的一条消息:

{ "model": "gpt-5-mini", "messages": [ {"role": "system", "content": "你是资深运维专家,回答要简洁。"}, {"role": "user", "content": "解释一下什么是死锁。"} ] }

Anthropic 的 system 是独立顶层字段:

{ "model": "claude-sonnet-4-5", "system": "你是资深运维专家,回答要简洁。", "messages": [ {"role": "user", "content": "解释一下什么是死锁。"} ] }

如果你把 OpenAI 的请求体原封不动发给 Anthropic,会拿到一个 400 invalid_request_error,提示role不合法或者system位置不对。反过来也一样,Anthropic 请求里少了max_tokens,OpenAI 报的参数错误又会变花样。

实现统一适配层的时候,最简单的映射规则是:把 OpenAI 请求体里 role 为 system 的首条消息提取出来,作为 Anthropic 的顶层 system 字段,其余消息按顺序映射为 user/assistant。反向迁移则把 Anthropic 的 system 作为 messages 数组的第一条 system 消息插入。

2.3 参数语义差异:max_tokens 是否必填与上下文窗口

参数层面最大的差异是max_tokens。OpenAI 的 Chat Completions 里max_tokens是选填的,不传会按模型默认输出长度生成(虽然新模型推荐用max_completion_tokens替代,老参数也兼容)。Anthropic 的 Messages API 里max_tokens必填,不传直接报missing required field: max_tokens。

这个差异背后有产品逻辑:Claude 希望调用方明确声明输出上限,防止长文本场景下生成失控、成本超预期。我个人挺认可这种设计,OpenAI 默认值在长文档生成场景容易超出预期,账单出来才发现 output token 走得飞快。

两个模型的上下文窗口都很夸张。OpenAI 的 gpt-5 系列和 Anthropic 的 Claude Sonnet 4.5 都支持百万 token 级别上下文。但你千万记住,上下文窗口大不等于输出可以无限长,输出上限通常要远小于输入上限。

还有一组参数名差异需要注意:OpenAI 用top_p,Anthropic 也有top_p,但 Anthropic 多了一个top_k。如果你要完全对齐采样行为,Anthropic 侧建议同时设置这三个参数;OpenAI 侧设置temperature和top_p就差不多了。

2.4 响应结构差异:choices 数组与 content blocks

响应结构是我认为迁移时最“反直觉”的地方。OpenAI 的返回体里生成内容在choices[0].message.content,一个数组包着一堆字段,取内容要先穿两层。

{ "id": "chatcmpl-xxx", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "死锁是……" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 30, "total_tokens": 42 } }

Anthropic 的返回体把顶层 role 直接暴露,生成内容放在content数组的块里:

{ "id": "msg_01XxYy", "type": "message", "role": "assistant", "content": [ {"type": "text", "text": "死锁是……"} ], "stop_reason": "end_turn", "usage": { "input_tokens": 12, "output_tokens": 30 } }

注意 usage 字段的命名也不一样,OpenAI 叫 prompt/completion/total tokens,Anthropic 叫 input/output tokens。做成本统计的脚本必须两套字段都兼容。

3. 请求对照实操:非流式、流式与工具调用

3.1 最简单的请求对照:JSON 和 Python SDK 各来一发

代码层面的对照,我直接给两份能跑的 Python SDK 示例。环境里先装好依赖,pip install openai anthropic。

OpenAI 侧:

from openai import OpenAI client = OpenAI(api_key="sk-xxxx") resp = client.chat.completions.create( model="gpt-5-mini", messages=[ {"role": "system", "content": "你是网络工程师。"}, {"role": "user", "content": "TCP 三次握手的意义是什么?"} ], max_completion_tokens=1024, temperature=0.7, ) print(resp.choices[0].message.content)

Anthropic 侧:

from anthropic import Anthropic client = Anthropic(api_key="sk-ant-xxxx") resp = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, system="你是网络工程师。", messages=[ {"role": "user", "content": "TCP 三次握手的意义是什么?"} ], temperature=0.7, ) print(resp.content[0].text)

对照看下来,最大的差异集中在三处:system 提示词的位置、max_tokens 是否必填、取结果的下标路径。迁移时不需要改业务逻辑,只需要写一层 response parser,把 Anthropic 的 content blocks 统一成 OpenAI 风格的字符串。

3.2 流式输出:两个事件模型怎么对齐

流式输出是迁移里的重灾区,因为两家的 SSE 事件结构完全不一样。OpenAI 流式返回一个事件序列,每个 chunk 里有choices[0].delta,你从delta.content里拼文本;如果出现工具调用,这里会出现delta.tool_calls。

stream = client.chat.completions.create( model="gpt-5-mini", messages=[{"role": "user", "content": "写一段 ping 命令的说明"}], stream=True, ) for chunk in stream: if chunk.choices and chunk.choices[0].delta: delta = chunk.choices[0].delta if delta.content: print(delta.content, end="")

Anthropic 的流式事件类型更多,有message_start、content_block_start、content_block_delta、content_block_stop、message_delta。文本片段在content_block_delta事件的delta.text里。直接用 SDK 的话,官方封装了text_stream迭代器,省心很多:

with client.messages.stream( model="claude-sonnet-4-5", max_tokens=1024, messages=[{"role": "user", "content": "写一段 ping 命令的说明"}], ) as stream: for text in stream.text_stream: print(text, end="")

如果你自己写适配层解析 SSE,需要把 OpenAI 的 chunk 流转换为 Anthropic 风格的事件序列,或者反向映射。这里最容易被忽略的是“事件结束”的语义:OpenAI 用finish_reason表示结束,Anthropic 用message_stop事件,适配层记得把两种终止信号都转发给上游。

3.3 工具调用:从 function calling 到 tool use

工具调用是差异最大的功能点,也是每个迁移项目里最耗时的部分。OpenAI 的 tools 定义结构如下:

{ "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "查询城市天气", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"] } } } ], "tool_choice": "auto" }

Anthropic 的工具定义把最外层type字段去掉了,参数结构叫input_schema而不是parameters:

{ "tools": [ { "name": "get_weather", "description": "查询城市天气", "input_schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"] } } ], "tool_choice": {"type": "auto"} }

工具调用结果的回传格式差异更大。OpenAI 的 assistant 回复里带tool_calls,里面每一项的function.arguments是 JSON 字符串,需要先反序列化才能拿到参数对象。执行完工具后,再追加一条role: "tool"的消息:

messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": "晴天,25°C" })

Anthropic 的 assistant 回复里工具调用是 content block,类型为tool_use,参数直接放在input对象里,不需要反序列化。执行完工具后,你要追加一条 user 消息,里面放一个tool_result块:

messages.append({ "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block.id, "content": "晴天,25°C" } ] })

这里有两个坑。第一,Anthropic 的 tool_result 必须放在 user 消息的 content 块里,不能单独成为一条消息。第二,OpenAI 的 arguments 是字符串,解析时一旦 JSON 里带转义字符,很容易 parse 失败;Anthropic 直接给对象,省掉这层折腾。如果你要做统一协议,工具调用这块建议封装成抽象函数,内部根据 provider 分别处理。

4. 统一封装和网关选型建议

4.1 为什么建议走一层适配层

如果你只是接一个大模型,直接写两套调用代码问题不大。但现实情况往往是业务代码同时接 OpenAI、Anthropic,还可能接 DeepSeek、智谱这类国内模型。每个模型的参数格式、错误类型都不同,如果业务代码里到处散落着if provider == "openai"的判断,后面会很难维护。

所以我的建议是:哪怕只接两家,也要在应用和模型之间加一层“模型适配层”。适配层的职责就是把请求统一成自己业务的“内部格式”,例如业务侧定义messages、tools、max_tokens,适配层负责转换为具体厂商的请求体;响应侧统一返回text和tool_calls两类结果。这样一来,换模型时只改适配层,不改业务逻辑。

4.2 网关方案取舍:LiteLLM、OneAPI 与自研适配层

市面上现成的网关方案有不少。LiteLLM 是开源社区比较活跃的方案,支持 OpenAI、Anthropic、Bedrock 等多家模型,还提供统一的 OpenAI 风格接口,也就是说你可以用 OpenAI SDK 的格式去请求一个 Anthropic 模型,网关帮你做转换。OneAPI 这类项目在国内团队里也用得多,支持渠道管理、令牌管理和多种模型接入,适合做团队内的“模型统一入口”。

但网关不是银弹。用网关的时候,模型名映射就得严格按网关的配置走,否则会碰到路由错误。我自己在 LiteLLM 上就碰到过expected a gateway model route reference的报错,这个后面专门说。

如果你的场景对数据管控要求高,或者要做的转换逻辑比较特殊,自研适配层更稳。不要一上来就上大而全的网关,先评估你的模型种类和调用量。只是内部实验,LiteLLM 快速拉起一条路由很合适;生产环境对接多个供应商、要统一记账和限流,再考虑 OneAPI 这类完整方案。

5. 高频问题排查与避坑实录

5.1 上下文溢出:1048576 token 错误怎么理解

OpenAI 新模型上下文到了百万 token 之后,报错也变成了一个容易误导人的格式。我实测见过这样一条:

API error: 400 this model's maximum context length is 1048576 tokens. However, you requested ...

字面意思是“模型的上下文长度上限是 1048576 token,但你请求的量超过了”,线程模型支持,报错的常见原因其实是同时输入了超长文档,又把max_tokens配得很大。模型上下文窗口是“输入 token + 输出 token”共享的,一旦 prompt 本身就接近百万 token,输出上限就得相应下调。

排查思路很简单:先看请求里 prompt 的 token 数,再算一下输入加输出是否溢出。若有,要么缩减输入,要么调低输出上限,要么走摘要/分片策略。之前有同事以为 1M 窗口就是“随便塞”,把一部小说全文加若干指令一次性扔进去,结果 AB 测试时多轮对话把历史越积越满,最后触发 400。长文本场景务必对历史做截断或摘要。

5.2 认证与连接类报错排查

认证类问题相对好排查,但有几个坑值得提。OpenAI 用sk-开头的 key,如果你误把 Anthropic 的sk-ant-key 填进去,会收到 401。反过来,Anthropic 不仅查 key 有效性,还要求anthropic-version头,版本头缺失或格式不对,SDK 之外自己拼请求就会踩到。

还有一个常见的连接层报错:

unable to connect to anthropic services: failed to connect to api.anthropic.com

这种一般是网络层面的原因,DNS 解析失败、出口防火墙拦截、TLS 握手失败都可能触发。排查顺序建议是从“最简单的连通性测试”开始,先用curl -I https://api.anthropic.com看能不能通,再检查 company 网络策略允许的出口范围,最后看 TLS 版本是否满足服务端要求。不要一上来就怀疑代码。

5.3 网关模型路由错误:expected a gateway model route reference

这条错误我在用 LiteLLM 做网关时踩过。请求发到网关后,返回:

doesn't look like an anthropic model: expected a gateway model route reference

原因是我在请求里直接写了模型名claude-sonnet-4-5,而网关注册路由时用的是带 provider 前缀的名字,比如anthropic/claude-sonnet-4-5。网关拿到请求里的模型名,去和gateway model route列表做匹配,匹配不上就直接报错。这不是协议问题,是网关的模型名映射问题。解决方法是:要么把请求里的 model 改成网关注册的路由名,要么在网关配置里加一个不带前缀的 alias。用网关之前先翻一遍路由配置,别凭记忆填模型名。

5.4 工具调用链路里的经典 Bug

工具调用相关的 Debug 大多数发生在消息历史拼接环节。OpenAI 侧,assistant 回复里出现tool_calls后,如果下一条消息不是role: "tool",接口会返回“对话历史无法继续”之类的错误;Anthropic 侧,assistant 回复里出现tool_use块之后,必须追加一条 user 消息包含对应tool_result块,而且tool_use_id必须对得上。

实操中常见问题就是tool_use_id或tool_call_id没对上。你在把工具结果回传时,如果复制错了 id,模型端会认为历史不连续,轻则生成结果错乱,重则直接抛 400。建议在适配层里做 id 校验,回传前比对一下 id 是否在最近一轮工具调用中出现过。还有一个细节:Anthropic 官方建议 tool_result 最好单独放在一条 user 消息里,不要和普通文本混在同一个 content 数组,避免模型理解混乱。

对于两套协议的高频问题,我做了一个速查表,方便日常对照:

症状可能原因处理建议
400 invalid roleOpenAI 请求直接发给了 Anthropic删除 assistant 以外的多余 role,system 提取到顶层字段
400 missing max_tokens调用 Anthropic 没传 max_tokens请求体补上 max_tokens
400 上下文超限prompt + 输出超过模型窗口缩短输入或调低输出上限
401/403key 填错或没有权限核对 key 前缀,确认账号权限
认证异常但 key 正确Anthropic 调用缺版本头补anthropic-version: 2023-06-01
gateway model route 错误网关模型名与路由配置不符检查网关路由,改用注册名或加 alias
工具调用后 400消息历史缺少 tool/tool_result检查 role 和 tool_use_id 的完整性
流式输出乱码拼接 delta 的顺序或事件类型搞混确认用的是 delta.content 还是 content_block_delta

最后再分享一个小经验。不管你是从 OpenAI 迁到 Anthropic,还是反向迁移,先做“字段级对照表”而不是“接口级对接”。把 system、messages、max_tokens、tools、tool_choice、usage 这些字段一个个列出来,标注两家的名字和位置,再开始写适配代码。这样做的好处是,工具调用、流式输出这些复杂链路不会在迁移中途被你遗漏,排查问题也清晰得多。我负责的几个迁移项目,凡是老老实实做了对照表的,上线时间比预期快一倍以上。协议差异本身并不复杂,复杂的永远是“以为差不多,实际差很多”的细节。

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

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

立即咨询