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 字段 | 备注 |
|---|---|---|---|
| 模型名 | model | model | 命名规则完全不同,不能混用 |
| 对话消息 | messages | messages | 结构有差异,见下节 |
| 系统提示 | messages里 role 为 system | 顶层system | 这是最大的结构差异 |
| 最大输出长度 | max_tokens(可选) | max_tokens(必填) | Anthropic 不填直接报错 |
| 采样温度 | temperature | temperature | 取值范围都是 0 到 1 |
| 核采样 | top_p | top_p | 语义一致 |
| 流式开关 | stream | stream | 语义一致,但事件格式不同 |
| 停止词 | stop | stop_sequences | 字段名不同 |
| 工具定义 | tools | tools | 结构差异较大 |
| 工具选择策略 | tool_choice | tool_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_tokens | Anthropic 必填项没传 | 补上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 或者改适配层就跑一遍,能快速发现协议层面的破坏性变更。这个方法帮我提前发现过好几次字段改名的问题,比等线上报错再排查省事多了。