☰
OpenAI与Anthropic API协议迁移实战:请求结构、工具调用与流式响应差异全解析
2026/10/7 5:36:55 网站建设 项目流程

1. 两套协议到底差在哪:从一次真实迁移说起

去年底我把一个内部知识库问答工具从 OpenAI 的接口切到 Anthropic,原本以为只是改个 URL 和 key 的事,结果整整折腾了一个下午。请求发出去要么 400,要么返回的内容结构对不上,最坑的是流式输出那块,事件格式完全不是一回事。那次之后我把两套协议的差异从头到尾梳理了一遍,今天就把这些踩过的坑和对照关系一次讲清楚。

这篇文章面向的是已经在用大模型 API 做开发的工程师,或者正准备从一套协议迁移到另一套的团队。我会把两套协议在请求结构、消息角色、系统提示、工具调用、流式响应、错误处理这几个维度的差异全部拆开讲,每个差异都配上可直接复制的请求对照,最后附上我实际踩过的坑和排查方法。读完你应该能做到:拿到一份 OpenAI 风格的请求,心里清楚要改哪几个字段才能跑在 Anthropic 上,反过来也一样。

先说结论层面的东西,方便你建立整体印象。OpenAI 的接口设计偏向"对话补全"这个原始定位,核心是messages数组加上role区分身份;Anthropic 从设计之初就是"给模型喂上下文"的思路,所以它把系统提示单独拎出来做顶层参数,消息角色只有 user 和 assistant 两种。这个根本差异会像涟漪一样扩散到后面所有的细节里。理解了这一点,后面那些字段名对不上的问题就都好解释了。

我下面所有的对照都基于两家当前主流的对话接口,OpenAI 这边是 chat completions 风格,Anthropic 这边是 messages 接口。参数名和结构我会尽量给准确,但两家迭代都很快,具体字段以你接入时的官方文档为准,我这里讲的是稳定了很长时间、短期内不会变的核心结构。

2. 请求体结构逐字段对照

2.1 顶层参数的一一映射

先看最外层。OpenAI 的请求体里,模型、消息、温度这些都在同一层;Anthropic 也类似,但多了几个 OpenAI 没有的顶层字段,同时少了几个。我把最常用的字段列成表,你迁移的时候直接照着改。

含义OpenAI 字段Anthropic 字段备注
模型名modelmodel命名规则完全不同,不能混用
对话消息messagesmessages结构有差异,见下节
系统提示messages里 role 为 system顶层system这是最大的结构差异
最大输出长度max_tokens(可选)max_tokens(必填)Anthropic 不填直接报错
采样温度temperaturetemperature取值范围都是 0 到 1
核采样top_ptop_p语义一致
流式开关streamstream语义一致,但事件格式不同
停止词stopstop_sequences字段名不同
工具定义toolstools结构差异较大
工具选择策略tool_choicetool_choice取值枚举不同

这张表里最容易被忽略的是max_tokens。OpenAI 这边你不传它会用模型默认值,请求照样成功;Anthropic 这边max_tokens是必填项,漏了直接返回 400,报错信息大意是缺少必填参数。我第一次迁移就是栽在这,因为原来的代码里根本没写这个字段。

另一个高频坑是stop和stop_sequences。字段名不一样就算了,值的类型也有讲究,OpenAI 接受字符串或字符串数组,Anthropic 只接受字符串数组。如果你原来传的是单个字符串,迁移时记得包成数组。

2.2 消息数组的结构差异

消息数组是两套协议差异最集中的地方。OpenAI 的messages里每条消息有role和content,role可以是system、user、assistant、tool四种。Anthropic 的messages里role只有user和assistant两种,系统提示被提到了顶层。

先看 OpenAI 的典型结构:

{ "model": "gpt-4o", "messages": [ {"role": "system", "content": "你是一个严谨的技术助手"}, {"role": "user", "content": "解释一下什么是幂等性"} ], "temperature": 0.7 }

同样的语义,Anthropic 要写成这样:

{ "model": "claude-sonnet-4-20250514", "system": "你是一个严谨的技术助手", "messages": [ {"role": "user", "content": "解释一下什么是幂等性"} ], "max_tokens": 1024, "temperature": 0.7 }

注意system从数组里的一条消息,变成了顶层的独立字符串。这个改动看起来小,但它影响的是你整个消息拼装逻辑。如果你原来的代码是动态往messages里插 system 消息,迁移时得把这段逻辑单独抽出来。

还有一个细节:Anthropic 要求messages里的角色必须交替出现,也就是 user 和 assistant 轮流来,不能连续两条都是 user。OpenAI 没这个限制,你连着塞两条 user 消息它也能处理。这个约束在拼接多轮对话历史的时候特别容易触发,比如你把用户连续两次追问合并处理,就可能出现两条相邻的 user 消息,Anthropic 会直接报错。

2.3 content 字段的两种形态

content这个字段两套协议都支持字符串和数组两种形态,但数组里元素的写法不一样。字符串形态就是纯文本,这个两边通用。数组形态用于多模态场景,比如图文混合输入。

OpenAI 的数组元素长这样:

{ "role": "user", "content": [ {"type": "text", "text": "这张图里有什么"}, {"type": "image_url", "image_url": {"url": "https://example.com/a.png"}} ] }

Anthropic 的数组元素长这样:

{ "role": "user", "content": [ {"type": "text", "text": "这张图里有什么"}, {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}} ] }

差异点有两个。第一,图片的类型标识,OpenAI 用image_url,Anthropic 用image。第二,图片来源的传法,OpenAI 支持直接给 URL,Anthropic 这边主流做法是传 base64 数据,需要你自己把图片编码后塞进data字段。如果你原来依赖 OpenAI 的 URL 传图,迁移到 Anthropic 时得先下载图片再编码,这一步经常被漏掉。

提示:Anthropic 的图片 base64 数据不要带data:image/png;base64,这个前缀,只放纯 base64 字符串,前缀信息通过media_type字段单独表达。带前缀会报解析错误。

3. 工具调用与函数调用的协议分歧

3.1 工具定义的结构对比

工具调用这块,两套协议的差异比消息结构还大。OpenAI 的工具定义是type加function两层嵌套,Anthropic 是扁平的name、description、input_schema三个字段。

OpenAI 的写法:

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

Anthropic 的写法:

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

关键差异是parameters变成了input_schema,而且去掉了type: function和function这层包裹。如果你有工具定义的 JSON Schema 存在数据库里,迁移时要做一次结构转换,把外层剥掉、字段改名。

3.2 模型返回工具调用的格式

模型决定调用工具时,两边的返回结构也不一样。OpenAI 在choices[0].message里放一个tool_calls数组,每个元素有id、type、function三部分,其中function.arguments是一个 JSON 字符串。

Anthropic 的返回里,content是一个数组,工具调用是其中一个type为tool_use的元素,带id、name、input三个字段,注意input已经是解析好的对象,不是字符串。

这个差异直接影响你的解析代码。OpenAI 那边你需要对arguments做一次JSON.parse,Anthropic 这边直接就是对象,不用再解析。反过来,如果你写了一套通用解析逻辑,得判断当前是哪套协议,走不同的分支。

3.3 工具结果的回传方式

工具执行完,结果要回传给模型继续对话,这一步两边的消息结构差异也很大。

OpenAI 是新增一条role为tool的消息,带上tool_call_id:

{ "role": "tool", "tool_call_id": "call_abc123", "content": "北京今天晴,25度" }

Anthropic 是把工具结果作为一条role为user的消息,content数组里放type为tool_result的元素:

{ "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_abc123", "content": "北京今天晴,25度" } ] }

这里有个容易搞混的点:Anthropic 用user角色来承载工具结果,而不是单独搞一个tool角色。原因是它的设计哲学里,工具结果本质上是"用户侧提供给模型的信息",所以归到 user 这边。理解了这个逻辑,你就不会觉得别扭了。

注意:Anthropic 回传工具结果时,tool_use_id必须和模型返回的tool_use里的id严格对应,写错了模型会认为工具没被调用,可能重复发起调用。这个 id 是模型生成的,你原样带回去就行,不要自己造。

4. 流式响应的解析差异

4.1 事件格式的根本不同

流式输出是迁移时最费劲的部分,因为两套协议的 SSE 事件格式完全不一样。OpenAI 的流式响应里,每个 chunk 是一个 JSON,结构类似非流式的choices,只是delta字段里放增量内容。

OpenAI 的 chunk 长这样:

data: {"choices":[{"delta":{"content":"你"},"index":0}]} data: {"choices":[{"delta":{"content":"好"},"index":0}]} data: [DONE]

Anthropic 的流式响应是带事件类型的,每个 SSE 消息有event和data两部分,事件类型包括message_start、content_block_start、content_block_delta、content_block_stop、message_delta、message_stop等。

Anthropic 的流大概长这样:

event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"你"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"好"}} event: message_stop data: {"type":"message_stop"}

差异一目了然。OpenAI 你只需要取delta.content拼接就行,Anthropic 你得先判断事件类型,只在content_block_delta且delta.type为text_delta的时候取delta.text。如果你直接把 Anthropic 的流按 OpenAI 的方式解析,会拿到一堆空内容,因为文本藏在更深的一层。

4.2 流式解析的代码对照

我把两边的解析逻辑写成伪代码,你对照着看就清楚了。

OpenAI 的解析:

for line in response.iter_lines(): if not line.startswith(b"data: "): continue payload = line[6:] if payload == b"[DONE]": break chunk = json.loads(payload) delta = chunk["choices"][0]["delta"] if "content" in delta: yield delta["content"]

Anthropic 的解析:

for line in response.iter_lines(): if not line.startswith(b"data: "): continue chunk = json.loads(line[6:]) if chunk["type"] == "content_block_delta": delta = chunk["delta"] if delta["type"] == "text_delta": yield delta["text"]

注意 Anthropic 这边没有[DONE]这个结束标记,你得靠message_stop事件或者直接等连接关闭来判断结束。这个差异在写循环终止条件的时候特别容易出错,我见过有人一直等[DONE]结果死循环的。

4.3 工具调用的流式处理

流式场景下工具调用的处理更麻烦。OpenAI 的tool_calls在流里是分片到达的,function.arguments会一段一段拼起来,你得自己维护一个缓冲区,等finish_reason变成tool_calls再整体解析。

Anthropic 这边工具调用的流式事件是content_block_start里带tool_use的初始信息,然后input_json_delta事件里分片传partial_json,最后content_block_stop表示这个块结束。你需要按index把分片归到对应的工具调用上。

这块两边的复杂度都不低,我的建议是如果你的场景对首字延迟不敏感,工具调用干脆别用流式,等完整响应回来再处理,能省掉一大堆拼接逻辑。等业务真的需要了再优化。

5. 错误处理与状态码的坑

5.1 错误响应的结构差异

请求出错时,两套协议返回的错误结构也不一样。OpenAI 的错误在顶层error对象里,有message、type、code、param几个字段。Anthropic 的错误也是顶层error,但字段是type和message,类型枚举值不同。

OpenAI 的错误示例:

{ "error": { "message": "Invalid API key", "type": "invalid_request_error", "code": "invalid_api_key" } }

Anthropic 的错误示例:

{ "type": "error", "error": { "type": "authentication_error", "message": "invalid x-api-key" } }

注意 Anthropic 的错误类型是authentication_error这种更粗粒度的分类,OpenAI 会细分到invalid_api_key这种具体 code。如果你原来依赖 OpenAI 的code字段做精细化错误处理,迁移到 Anthropic 后得改成按type判断,粒度会变粗。

5.2 认证方式的差异

认证这块两套协议用的是不同的请求头。OpenAI 用Authorization: Bearer <key>,Anthropic 用x-api-key: <key>,而且 Anthropic 还要求带一个anthropic-version头来指定 API 版本。

# OpenAI curl https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_KEY" \ -H "Content-Type: application/json" \ -d '{...}' # Anthropic curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "Content-Type: application/json" \ -d '{...}'

anthropic-version这个头是必填的,漏了会报错。它的作用是让 Anthropic 能在不破坏老用户的前提下演进 API,你指定了版本,行为就锁定在那个版本。这个设计挺聪明的,但第一次接入的人经常忘。

5.3 常见错误速查表

我把迁移过程中最常撞上的错误整理成表,方便你对照排查。

现象可能原因解决方向
400 缺少 max_tokensAnthropic 必填项没传补上max_tokens
400 角色不交替messages 里连续同角色合并或插入占位消息
401 认证失败请求头用错换成x-api-key并加版本头
400 模型不存在模型名混用用对应平台的模型名
流式无内容事件解析逻辑不对按事件类型分支处理
工具调用重复tool_use_id 对不上原样回传模型给的 id
图片解析失败base64 带了前缀去掉data:前缀

这张表里的每一条我基本都亲自撞过,尤其是"流式无内容"和"工具调用重复"这两个,排查起来最费时间,因为报错信息不会直接告诉你原因,得靠日志一点点看。

6. 迁移实操:一份请求的双向改写

6.1 从 OpenAI 改到 Anthropic 的完整步骤

假设你手上有一份能跑的 OpenAI 请求,要改成 Anthropic 版本,按这个顺序改最不容易漏。

第一步,换 URL 和认证头。URL 从/v1/chat/completions换成/v1/messages,认证头从Authorization: Bearer换成x-api-key,加上anthropic-version。

第二步,把messages里的 system 消息抽出来,放到顶层system字段。如果有多条 system 消息,用换行拼成一个字符串。

第三步,补上max_tokens。这个值根据你的业务定,一般对话场景 1024 到 4096 够用,长文本生成再往上加。

第四步,改stop为stop_sequences,值包成数组。

第五步,改工具定义,parameters换成input_schema,去掉function外层。

第六步,改流式解析逻辑,按事件类型分支。

第七步,改工具结果的回传结构,从role: tool改成role: user加tool_result块。

这七步走完,基本就能跑通了。我建议每改一步就发一次请求验证,别攒着一起改,不然出错都不知道是哪步引入的。

6.2 反向迁移的注意点

从 Anthropic 改回 OpenAI 相对简单一些,因为 OpenAI 的约束更少。主要改这几处:system 从顶层塞回 messages 数组,max_tokens可以留着也可以删,stop_sequences改回stop,工具定义加回function外层,工具结果改成role: tool。

反向迁移有个坑要注意:Anthropic 的input是解析好的对象,OpenAI 的arguments是字符串。你从 Anthropic 迁到 OpenAI 时,得把工具调用的参数对象序列化成字符串再塞进arguments,忘了这步模型会收到格式错误。

6.3 用适配层屏蔽差异

如果你的项目要同时支持两套协议,别在每个业务代码里写 if-else,抽一个适配层出来。我的做法是定义一套内部统一的消息格式,然后写两个转换器,一个转 OpenAI,一个转 Anthropic,业务代码只跟内部格式打交道。

适配层要处理的核心转换点就三个:system 提示的位置、工具定义的结构、流式事件的解析。把这三个封装好,上层业务基本无感。这个投入在需要多平台兜底的场景下非常值,我现在的项目就是一套内部格式,切换平台只改一个配置项。

7. 我踩过的坑和排查心得

7.1 那些文档不会告诉你的细节

第一个坑是 Anthropic 的max_tokens上限。不同模型的上限不一样,你设太大也会报错,报错信息不会告诉你上限是多少,得去查文档。我的做法是设一个保守值,比如 4096,需要更长输出再针对性调。

第二个坑是流式响应里的ping事件。Anthropic 会定期发event: ping的心跳,你的解析逻辑如果没忽略它,可能会把它当成内容处理。我一开始就中招了,输出里混进了一堆空字符串。

第三个坑是消息历史的长度控制。两套协议对上下文长度的计算方式不一样,OpenAI 按 token 算,Anthropic 也是按 token 但分词方式不同,同样的文本两边算出来的 token 数会有差异。你做历史截断的时候,别用一套 token 估算逻辑套两边,容易一边超限一边浪费。

第四个坑是并发限流的表现形式。OpenAI 触发限流返回 429 带Retry-After头,Anthropic 也是 429 但重试建议的字段名不一样。写重试逻辑的时候要分别处理。

7.2 排查问题的通用思路

遇到请求失败,我的排查顺序是这样的:先看 HTTP 状态码,4xx 基本是请求本身的问题,5xx 是服务端问题可以重试;然后看错误响应体里的type和message,这两个字段通常能定位到具体原因;最后如果错误信息模糊,就把请求体完整打出来,逐字段对照文档检查。

流式问题排查稍微特殊一点,因为错误可能藏在流中间。我的做法是在解析循环里加详细日志,把每个事件的原始内容打出来,这样能清楚看到流是在哪一步断的、哪个事件格式不对。

提示:调试阶段把请求体和响应体完整落盘,出问题时能直接复现。生产环境注意脱敏,别把 key 和用户数据写进日志。

7.3 性能与成本的取舍

两套协议在计费维度上也有差异。OpenAI 按输入输出 token 分别计价,Anthropic 也是,但它的缓存机制设计得比较特别,支持显式标记可缓存的内容块,命中缓存的部分价格低很多。如果你的场景有大量重复的系统提示或知识库内容,用 Anthropic 的缓存能省不少钱。

延迟方面,两家的流式首字延迟都在几百毫秒级别,具体取决于模型和负载。我的实测是同一量级的模型,首字延迟差异不大,但完整响应时间跟输出长度强相关,这个两边都一样。

选哪套协议,我的建议是别只看协议本身,要看你整个技术栈。如果你的工具链、监控、日志都是围绕 OpenAI 生态建的,迁移成本要考虑进去。如果是从零开始,两套都试试,看哪套的模型输出更符合你的业务需求,协议差异其实是可以靠适配层抹平的。

最后分享一个我常用的验证方法:写一个最小的测试脚本,把同一个问题分别发给两套接口,把请求体和响应体都打出来对比。这个脚本我留在项目里当回归测试用,每次升级 SDK 或者改适配层就跑一遍,能快速发现协议层面的破坏性变更。这个方法帮我提前发现过好几次字段改名的问题,比等线上报错再排查省事多了。

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

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

立即咨询