同一条消息,两种写法: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 项目仓库。
请先记住课程对它的定位:它是一个实现案例,不是模型、不是行业标准,也不代表任何官方服务。我们用它来练习阅读转换代码,而不是推荐部署,也不推荐任何同名托管服务。
带着上文的清单去读它的转换代码,重点看四个问题:
- 系统提示往哪个方向合并或拆分,出现多条时如何取舍。
content块与字符串之间如何来回改写,图片和工具结果怎么处理。- 工具调用参数是否被解析成对象,工具结果是否对上了正确的关联 ID。
- 流式事件如何分发,结束事件与
[DONE]是否都被正确终止。
读完后把结论分成三列:“已确认”“推断”“未知”。找不到资料的部分就写“未知”,不要因为相邻功能可用就默认转换完整。公开源码让审查成为可能,但它并不自动担保任何具体部署实例的日志、配置或安全性;具体部署是否记录请求、是否开启保护,需要单独核验。
动手练习
- 打开两家的官方接口文档,各找一个“工具调用”的请求或响应示例,把工具定义、调用参数和工具结果三处字段抄下来并一一对应。
- 写一份字段对照表,至少包含:系统提示位置、
content形态、工具定义嵌套、工具参数类型、工具结果位置、max_tokens是否必填、停止参数、停止原因、响应体顶层字段、流式结束标记。 - 从上一步挑两项你判断无法无损转换的字段,写出应用应该报错、降级还是转为人工确认,并说明理由。
参考答案与推演
对照表的关键是两类都要标出来:“同物不同名”和“同名不同物”。“停止原因”是典型的同物不同名:stop_reason 的 end_turn 大致对应 finish_reason 的 stop,max_tokens 对应 length,tool_use 对应 tool_calls。“content”是典型的同名不同物:一边是带 type 的块数组,一边可以是普通字符串;如果只比较字段名就会以为它们等价。
无法无损转换的例子可以这样写:“图片块以 URL 形式提供,上游只接受 base64;当前实现没有下载与转码,因此遇到图片输入直接报错,不静默丢弃图片。”这比“已兼容多模态”更能指导维护者。另一种合格写法是把结构化输出降级为“提示模型返回 JSON,再由应用校验”,并明确说明校验失败时不能自动执行后续动作。
完成检查
- 能说出
system与role:"system"各自出现在哪一层。 - 能指出
input_schema与function.parameters的嵌套差异。 - 知道工具参数在一边是对象、另一边是需要解析的字符串。
- 能区分
stop_reason与finish_reason的取值。 - 能说明流式两边的结束标记不同,不能只做逐行转发。
- 能至少列举两项兼容转换会丢失的语义,并给出处理方式。
参考与来源
- Anthropic:Messages API 参考:查看顶层
system、max_tokens、stop_reason与content块定义。 - Anthropic:工具调用概览:查看
input_schema与tool_result块的写法。 - OpenAI:Chat Completions 参考:查看
role:"system"、function.parameters、finish_reason与delta。