/同一条消息,两种写法:Messages API 与 Chat Completions 的字段对照登录后记录进度

同一条消息,两种写法:Messages API 与 Chat Completions 的字段对照

建议用时:25 分钟(含练习)

学习目标

能够逐项对照 Anthropic Messages API 与 OpenAI Chat Completions 的请求体、响应体、流式事件和工具调用字段,并说明兼容为什么不等于等价。

本节产出

一份两种协议的字段对照与转换降级清单

核心知识:同一条消息,两套字段

Anthropic 的 Messages API 和 OpenAI 的 Chat Completions 都叫“聊天接口”,都能接收一段对话并返回模型的下一段回复。很多客户端因此把两者当成可以互相替换的地址。真实情况是:它们描述同一件事的字段不同,转换时必须逐项决定“对应什么”,而不是把请求体原样改个名字。

这一节先只做一件事:把同一条“用户问一句话”的消息,分别写成两种请求体,再逐行对照。读完之后,你应该能拿到任何一份兼容文档,自己判断它到底转换了哪些字段、又悄悄丢掉了哪些。

系统提示:顶层独立参数,还是消息里的一条

Messages API

Messages API 把系统提示放在顶层的 system 参数里,与 messages 平级:

  • system 是顶层字段,通常只接受一条(也可以是带 type:"text" 的块数组)。
  • 它不属于对话轮次,因此不会出现在 messages 里。

Chat Completions

Chat Completions 没有顶层 system 参数,而是把它当作 messages 数组里的普通一条,用 role:"system" 表示:

  • 系统提示与用户、助手消息在同一个数组里,靠 role 区分。
  • 数组里可以出现多个 role:"system" 条目(不同服务可能只取第一条或最后一条)。

转换时要注意方向:从 Messages API 转到 Chat Completions,需要把顶层 system 移进 messages;反过来,如果原来是多条 role:"system",只能保留一条,其余要么合并,要么明确报告“无法无损转换”。

content:带类型的块数组,还是普通字符串

Messages API 的 content 是块数组,每个块带 type。最简单的文本也要写成一个块:

{ "role": "user", "content": [{ "type": "text", "text": "你好" }] }

Chat Completions 允许把纯文本 content 直接写成字符串:

{ "role": "user", "content": "你好" }

数组形式并不是“更啰嗦”,它承载了字符串装不下的东西:图片、工具结果、缓存断点等都以不同 type 的块出现。转换到字符串形式时,必须先决定这些块怎么降级——比如图片块换成什么、工具结果放到哪里。

工具定义与工具调用

工具定义的差异集中在嵌套层级:

Messages API

  • Messages API 的工具是扁平结构:name、description、input_schema 直接写在同一层。

Chat Completions

  • Chat Completions 的工具要包一层 type:"function",参数 schema 放在 function.parameters 里。

也就是说,同一个 JSON Schema,在一边是 input_schema,在另一边是 function.parameters,外面还多一层 function。

工具调用参数的形态也不同:

  • Messages API 返回的是已经解析好的对象,可以直接按字段取值。
  • Chat Completions 把参数放在 function.arguments 里,是一段字符串,必须自己 JSON.parse,而且这段字符串可能并不合法。

工具结果的回传方式同样不同:

  • Messages API 把结果放进一条 role:"user" 消息的 content 里,作为一个 type:"tool_result" 块,用 tool_use_id 关联调用。
  • Chat Completions 使用一条独立的 role:"tool" 消息,用 tool_call_id 关联,内容放在 content 里。

写转换代码的人最容易在这里出错:把 tool_result 块直接搬到 role:"tool" 消息上,或忘记把字符串参数解析成对象,于是后续代码拿到的是文本而不是字段。

长度、停止参数与响应体

  • max_tokens:Messages API 必填;Chat Completions 可选(省略时由服务端默认值决定)。转换时如果对方必填而你没有,就要显式补一个上限。
  • 停止参数:Messages API 用 stop_sequences(字符串数组);Chat Completions 用 stop。
  • 停止原因:Messages API 在响应里给 stop_reason,常见取值是 end_turn、max_tokens、tool_use、stop_sequence;Chat Completions 的对应字段叫 finish_reason,常见取值是 stop、length、tool_calls。

响应体本身的结构也不一样:

  • Messages API 返回 content 块数组(文本、工具调用各是一个块),文本要遍历块取出 text。
  • Chat Completions 返回 choices 数组,默认取 choices[0].message:文本在 message.content,工具调用在 message.tool_calls。

同一个“生成完成”的语义,在两边用不同的字符串表示。任何一方遇到不认识的取值,都不应该当作成功继续往下走。

流式:事件类型不同构

两边的流式都建立在 Server-Sent Events 上,但组织方式不同。

Messages API 的每一帧是 event: 加 data: 两行:

event: content_block_delta
data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"你"}}

事件带明确类型(如 message_start、content_block_start、content_block_delta、message_delta、message_stop),客户端必须按事件类型分发,才能知道这次增量是普通文本、工具参数,还是结束信息。

Chat Completions 通常只有 data: 行,增量藏在 choices[0].delta 里,最后以一行 data: [DONE] 表示流结束:

data: {"choices":[{"delta":{"content":"你"}}]}
data: [DONE]

把 SSE 逐行转发并不能完成转换:一边的结束是事件,另一边的结束是哨兵字符串;一边的增量有类型,另一边的增量只有字段。任何一边漏掉结束或类型判断,客户端就可能一直等下去,或把工具参数的半截字符串当成正文显示。

兼容不等于等价

到这里可以回答“为什么改个地址就能用,却仍然出事”。协议转换是有损的,至少这几类语义在往返中容易丢失或降级:

语义转换中常见的变化后果
错误标记上游错误码与类型被压成统一的通用错误重试与告警策略失去判断依据
结构化输出一边用工具或输出结构约束 JSON,另一边只当文本解析失败时才在应用层暴露
缓存断点某一边特有的块级缓存标记被丢弃成本上升而请求仍然“成功”
多模态图片URL、base64、文件 ID 与媒体类型字段格式不同图片输入被拒绝或静默降级

因此,兼容层的正确做法不是“尽量都转”,而是逐项标注:哪些字段一一对应,哪些需要降级,哪些直接不支持。不支持的项要明确报错或走人工确认,而不是删掉字段后继续宣称可用。

两种协议字段对照:系统提示、内容块、工具定义与流式事件逐项映射,并标出转换会丢失的语义

图里画的是转换的检查顺序:先对齐请求结构(系统提示与内容形态),再对齐工具定义、响应与流式事件,最后把无法无损转换的项目单独记在清单里。

案例:一个第三方开源网关如何做协议转换

理解了字段差异,再去看一个真实实现会快很多。这里以一个第三方开源网关项目 Sub2API 为例——它提供 OpenAI 兼容入口,再把请求转发给上游账户,因此必须在两套协议之间来回转换。它的项目仓库是 Sub2API 项目仓库。

请先记住课程对它的定位:它是一个实现案例,不是模型、不是行业标准,也不代表任何官方服务。我们用它来练习阅读转换代码,而不是推荐部署,也不推荐任何同名托管服务。

带着上文的清单去读它的转换代码,重点看四个问题:

  1. 系统提示往哪个方向合并或拆分,出现多条时如何取舍。
  2. content 块与字符串之间如何来回改写,图片和工具结果怎么处理。
  3. 工具调用参数是否被解析成对象,工具结果是否对上了正确的关联 ID。
  4. 流式事件如何分发,结束事件与 [DONE] 是否都被正确终止。

读完后把结论分成三列:“已确认”“推断”“未知”。找不到资料的部分就写“未知”,不要因为相邻功能可用就默认转换完整。公开源码让审查成为可能,但它并不自动担保任何具体部署实例的日志、配置或安全性;具体部署是否记录请求、是否开启保护,需要单独核验。

动手练习

  1. 打开两家的官方接口文档,各找一个“工具调用”的请求或响应示例,把工具定义、调用参数和工具结果三处字段抄下来并一一对应。
  2. 写一份字段对照表,至少包含:系统提示位置、content 形态、工具定义嵌套、工具参数类型、工具结果位置、max_tokens 是否必填、停止参数、停止原因、响应体顶层字段、流式结束标记。
  3. 从上一步挑两项你判断无法无损转换的字段,写出应用应该报错、降级还是转为人工确认,并说明理由。

参考答案与推演

完成检查

  • 能说出 system 与 role:"system" 各自出现在哪一层。
  • 能指出 input_schema 与 function.parameters 的嵌套差异。
  • 知道工具参数在一边是对象、另一边是需要解析的字符串。
  • 能区分 stop_reason 与 finish_reason 的取值。
  • 能说明流式两边的结束标记不同,不能只做逐行转发。
  • 能至少列举两项兼容转换会丢失的语义,并给出处理方式。

参考与来源