尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

AI协议转换实战:Chat、Responses、Messages互转与流式处理

发布时间:2026/9/26 9:02:19

资讯中心
01
ARTICLE

AI协议转换实战:Chat、Responses、Messages互转与流式处理

AI协议转换实战:Chat、Responses、Messages互转与流式处理
1. 协议转换到底在解决什么问题1.1 从一个真实的502报错说起前段时间在调试一个本地服务时日志里反复出现这么一行unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:15721/v1/responses请求地址是本地回环端口也是自己起的按理说不该出现网关错误。排查了一圈才发现问题根本不在网络层而是协议不匹配客户端按Responses接口的格式发请求服务端却只认Chat Completions的报文结构中间那层转发组件拿到一个它看不懂的 body直接吐了个 502 出来。这个场景其实非常典型。现在做 AI 应用开发的人手里往往同时握着好几套接口规范有的 SDK 只支持Chat格式有的新工具默认走Responses还有的比如 Anthropic 系用的是Messages结构。三套协议字段名不一样、消息角色定义不一样、工具调用的嵌套层级也不一样。你要么改客户端要么改服务端要么在中间加一层翻译。micro-one-api这个项目干的就是第三件事——在中间做协议转换把Chat、Responses、Messages三种格式互相翻译让上游客户端和下游模型服务不用互相迁就。这篇内容就把这套转换逻辑拆开讲清楚包括字段映射、工具调用的坑、流式响应的处理以及我自己踩过的几个典型问题。适合正在做多模型接入、网关层开发或者被协议不兼容折腾过的朋友参考。1.2 三种协议的核心差异在哪先把三套协议摆到一起看。它们要表达的东西其实是一样的一段对话历史、一个待补全的请求、可能还有工具定义。但表达方式差别不小。维度Chat CompletionsResponsesMessages消息容器messages数组input数组messages数组角色命名system/user/assistant/tool类似但支持更多类型user/assistantsystem 独立字段系统提示作为一条 message作为 instructions 字段顶层system参数工具定义toolsfunction嵌套tools扁平化toolsinput_schema工具调用结果roletool 的消息function_call_output 类型user 消息里的 tool_result 块流式事件data: {choices:[...]}事件类型化response.output_text.delta 等事件类型化content_block_delta 等看这张表就能明白为什么直接转发会出问题。Chat里工具调用结果是一条独立的role: tool消息而Messages里它是塞在user消息的content数组里、类型为tool_result的一个块。你要是原样转发下游解析器根本找不到它期待的结构。提示协议转换的本质不是改字段名而是重建语义结构。字段名映射只是表层真正难的是把一种协议的嵌套关系翻译成另一种协议的等价表达。1.3 为什么要在网关层做转换有人会问为什么不直接让客户端适配服务端原因很现实客户端可能是第三方 SDK、可能是已经上线的产品、可能是用户自己写的脚本你改不动。服务端同理模型服务商给你什么接口你就得用什么。在网关层做转换的好处是一次开发两边都不用动。客户端继续用它熟悉的Chat格式发请求网关翻译成Responses发给上游拿到结果再翻译回Chat返回。整个过程对两端透明。代价是网关层要维护一套完整的映射规则而且要考虑流式、错误、工具调用这些边界情况。这也是为什么很多团队一开始觉得不就是改个字段名吗做起来才发现坑一个接一个。2. 核心字段映射与结构重建2.1 消息数组的翻译逻辑先看最基础的消息翻译。假设客户端发来一段Chat格式的请求{ model: some-model, messages: [ {role: system, content: 你是一个助手}, {role: user, content: 帮我查下天气} ] }翻译成Responses格式时system消息要抽出来放到顶层instructions{ model: some-model, instructions: 你是一个助手, input: [ {role: user, content: 帮我查下天气} ] }翻译成Messages格式时system同样抽到顶层{ model: some-model, system: 你是一个助手, messages: [ {role: user, content: 帮我查下天气} ] }这里有个细节要注意多条 system 消息怎么处理。Chat允许你放多条role: system但Responses的instructions和Messages的system都是单个字符串。我的做法是用换行符拼接保留顺序。虽然语义上略有损失原本是分开的两段指令但实际使用中影响很小。反过来翻译时如果Messages的system字段有值就把它转成一条role: system的消息插到messages数组最前面。这个方向是无损的。2.2 工具定义的三种写法工具定义是差异最大的地方。同一个查天气工具三种协议写法完全不同。Chat格式{ type: function, function: { name: get_weather, description: 查询指定城市天气, parameters: { type: object, properties: { city: {type: string} }, required: [city] } } }Responses格式把function这层嵌套去掉了{ type: function, name: get_weather, description: 查询指定城市天气, parameters: { type: object, properties: { city: {type: string} }, required: [city] } }Messages格式则把parameters改名叫input_schema{ name: get_weather, description: 查询指定城市天气, input_schema: { type: object, properties: { city: {type: string} }, required: [city] } }转换时要做的是Chat→Responses把function层拍平Chat→Messages把parameters改名成input_schema并去掉function层。反向转换就是逆操作。注意Messages的工具定义没有type字段而Chat和Responses都有type: function。转换到Messages时要把这个字段删掉否则下游可能报未知字段错误。2.3 工具调用结果的嵌套差异这是最容易出问题的地方。看一个完整的工具调用往返。Chat格式里模型返回工具调用请求{ role: assistant, content: null, tool_calls: [ { id: call_abc, type: function, function: { name: get_weather, arguments: {\city\:\北京\} } } ] }客户端执行完工具后回传结果{ role: tool, tool_call_id: call_abc, content: 北京今天晴25度 }同样的往返在Messages格式里长这样。模型返回{ role: assistant, content: [ { type: tool_use, id: call_abc, name: get_weather, input: {city: 北京} } ] }客户端回传{ role: user, content: [ { type: tool_result, tool_use_id: call_abc, content: 北京今天晴25度 } ] }看出区别了吗Chat里工具结果是一条独立的role: tool消息Messages里它是user消息 content 数组里的一个块。转换时要把独立的 tool 消息折叠进 user 消息的 content 数组或者反过来把 content 数组里的 tool_result 块展开成独立消息。这里有个坑如果连续多个工具调用Chat会返回多条role: tool消息而Messages要求它们都在同一条user消息的 content 数组里。转换时要判断遇到连续的 tool 消息合并成一条 user 消息遇到非 tool 消息才新起一条。2.4 参数序列化的双向处理Chat的function.arguments是字符串里面是 JSON 序列化后的内容。Messages的input是对象直接就是解析好的结构。Responses跟Chat类似arguments 也是字符串。转换时要做序列化和反序列化Chat→MessagesJSON.parse(arguments)得到对象赋给inputMessages→ChatJSON.stringify(input)得到字符串赋给arguments看起来简单但实际会遇到模型返回的 arguments 不是合法 JSON 的情况比如多了个逗号、少了引号。我的处理是加一层容错解析失败时先尝试修复常见错误去尾逗号、补引号再失败就原样透传并记录日志不要直接抛异常中断整个请求。3. 流式响应的转换实现3.1 三种协议的流式事件模型非流式请求好办拿到完整响应再转换就行。流式请求麻烦得多因为三种协议的事件模型完全不同。Chat的流式是增量式的每个 chunk 里带deltadata: {choices:[{delta:{content:你}}]} data: {choices:[{delta:{content:好}}]} data: {choices:[{delta:{},finish_reason:stop}]} data: [DONE]Responses是事件类型化的每个事件有明确的typeevent: response.output_text.delta data: {type:response.output_text.delta,delta:你} event: response.output_text.delta data: {type:response.output_text.delta,delta:好} event: response.completed data: {type:response.completed,response:{...}}Messages也是事件类型化的但事件名和结构不同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}3.2 流式转换的状态机设计流式转换不能简单地一个事件进、一个事件出因为三种协议的事件粒度不一样。Chat一个 chunk 可能同时包含文本增量和工具调用增量而Messages要求文本和工具调用分开成不同的事件。我的做法是维护一个转换状态机记录当前处于哪个阶段class StreamState: def __init__(self): self.text_started False self.tool_started False self.current_tool_index None self.buffer 处理Chat→Messages时逻辑大致是收到第一个带content的 delta如果text_started为 False先发一个content_block_start事件再发content_block_delta收到带tool_calls的 delta如果tool_started为 False先发工具块的content_block_start再发input_json_delta收到finish_reason发content_block_stop和message_stop关键点是事件顺序。Messages要求先content_block_start再content_block_delta再content_block_stop顺序错了客户端会解析失败。而Chat没有这些边界事件所以转换时要自己补上。3.3 工具调用增量的拼接流式场景下工具调用的参数是分片传输的。Chat的流式响应里tool_calls[0].function.arguments可能被拆成好几段data: {choices:[{delta:{tool_calls:[{index:0,function:{name:get_weather,arguments:{\ci}}]}}]} data: {choices:[{delta:{tool_calls:[{index:0,function:{arguments:ty\:\北}}]}}]} data: {choices:[{delta:{tool_calls:[{index:0,function:{arguments:京\}}}]}}]}转换到Messages时这些分片要拼成完整的 JSON 再发input_json_delta。但问题是你不能等全部拼完再发那样就失去流式意义了。Messages的input_json_delta本身就是设计成可以分片发的所以直接把arguments的增量透传过去就行客户端会自己拼接。不过有个细节Messages的input_json_delta事件里字段叫partial_json不是arguments。转换时要改名。# Chat delta - Messages event { type: content_block_delta, index: tool_index, delta: { type: input_json_delta, partial_json: chat_delta[function][arguments] } }3.4 流式转换的缓冲与刷新实际写代码时流式数据往往不是一条一条来的而是一批一批来的。你可能一次读到 3 个 chunk 的数据也可能一个 chunk 被拆成两次读到。所以需要一个行缓冲机制class LineBuffer: def __init__(self): self.buffer def feed(self, chunk): self.buffer chunk lines [] while \n in self.buffer: line, self.buffer self.buffer.split(\n, 1) lines.append(line) return lines def flush(self): if self.buffer: line, self.buffer self.buffer, return [line] return []每次读到数据就喂给 buffer取出完整的行处理。请求结束时调用 flush 处理残留数据。这个机制看起来简单但不做的话会遇到最后一条事件丢失的问题因为最后一行可能没有换行符结尾。提示流式转换里最常见的 bug 就是最后一条消息丢了。九成是因为没处理缓冲区残留。养成习惯流结束前一定要 flush。4. 常见报错与排查实录4.1 502 与协议不匹配的关联回到开头那个 502。unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:15721/v1/responses这个报错表面看是网关错误实际原因通常是请求发到了/v1/responses但服务端只实现了/v1/chat/completions服务端收到了请求但 body 结构不符合它期待的格式解析时抛异常被上层包装成 502中间转发组件配置了错误的 upstream 地址排查顺序应该是先确认服务端实际监听的路径和它支持的协议再确认请求 body 是否符合该协议最后才看网络层。我遇到的那次是客户端 SDK 默认走Responses但本地服务只实现了Chat。解决办法是在网关层加一个路由规则/v1/responses的请求先转换成Chat格式再转发到/v1/chat/completions。4.2 tool_calls 必须跟 tool 消息另一个高频报错an assistant message with tool_calls must be followed by tool messages responding to each tool_call_id这个错误的意思是你发了一条带tool_calls的 assistant 消息但后面没有对应的 tool 消息来回应每个tool_call_id。出现这个错误通常有两个原因原因一转换时丢了 tool 消息。比如从Messages转Chat时user消息里的tool_result块没有被正确展开成独立的role: tool消息。原因二tool_call_id 对不上。转换过程中 ID 被改写或丢失导致 assistant 消息里的call_abc和 tool 消息里的tool_call_id不一致。排查方法把转换前后的完整消息数组打印出来逐个核对tool_calls[].id和tool消息的tool_call_id是否一一对应。报错信息常见原因排查方向502 bad gateway协议不匹配或路径错误确认服务端支持的协议和路径tool_calls must be followed by tool messagestool 消息丢失或 ID 不匹配核对消息数组和 ID 对应关系unknown field input_schema目标协议不支持该字段检查字段名是否符合目标协议invalid type for contentcontent 类型不匹配确认是字符串还是数组missing required field messages字段名翻译错误检查 input/messages 是否混淆4.3 内容类型不匹配的处理Chat的content可以是字符串也可以是数组多模态场景。Messages的content必须是数组。Responses的content又不太一样。转换时要统一处理def normalize_content(content): if isinstance(content, str): return [{type: text, text: content}] if isinstance(content, list): return content return []反向转换时如果数组里只有一个 text 块可以简化成字符串Chat支持这种简写但多个块就必须保持数组形式。注意不要想当然地认为只有一个 text 块就能简化成字符串。有些客户端对content类型很敏感字符串和数组会走不同的解析分支。稳妥做法是保持数组形式除非你确定下游能处理字符串。4.4 排查工具与日志策略协议转换的调试光看报错信息往往不够得把中间态打出来。我的做法是在转换函数的入口和出口各加一个日志点def convert_chat_to_messages(chat_request): logger.debug(CHAT_IN: %s, json.dumps(chat_request, ensure_asciiFalse)) messages_request do_convert(chat_request) logger.debug(MESSAGES_OUT: %s, json.dumps(messages_request, ensure_asciiFalse)) return messages_request日志级别用 DEBUG生产环境默认关闭需要时动态打开。这样既不影响性能又能在出问题时快速定位。另外建议做一个协议转换的单元测试集把常见的请求形态纯文本、带工具、多轮对话、流式都覆盖到。每次改转换逻辑跑一遍能挡住大部分回归问题。5. 实操中的经验与避坑5.1 ID 生成与保持策略工具调用的 ID 在转换过程中必须保持稳定。有些实现图省事转换时重新生成 ID结果 assistant 消息里的 ID 和 tool 消息里的 ID 对不上直接触发前面说的那个报错。我的原则是能保持就保持必须生成时用确定性规则。比如Messages的tool_use_id转Chat的tool_call_id时直接透传原值。如果原值为空有些客户端不填就用tool_name index拼一个保证同一次请求内唯一且可预测。5.2 空值与缺省字段的处理三种协议对可选字段的处理不一样。Chat的content在工具调用时可以是nullMessages则要求content必须是数组可以是空数组。转换时要把null转成[]否则下游可能报类型错误。反过来Messages的空数组转Chat时如果同时有tool_callscontent应该设为null而不是空字符串。这个细节不注意的话模型可能把空字符串当成有效内容处理。5.3 流式转换的性能考量流式转换是逐 chunk 处理的每个 chunk 都要做一次转换。如果转换逻辑里有复杂的 JSON 解析或字符串操作高并发下会成为瓶颈。优化思路避免在每个 chunk 里做完整的 JSON 序列化/反序列化能直接改字段的就直接改状态机用简单的布尔标志和整数索引不要用复杂对象缓冲区用列表拼接而不是字符串累加Python 里字符串是不可变的累加会产生大量临时对象实测下来优化前后在 1000 并发流式请求下CPU 占用能差 30% 左右。5.4 兼容性测试的覆盖要点协议转换最容易出问题的地方是边界情况。我的测试集里必测的场景纯文本单轮对话多轮对话带 system单工具调用多工具并行调用工具调用后继续对话流式纯文本流式带工具调用空消息、超长消息特殊字符换行、引号、Unicode每个场景都要测三个方向的转换Chat↔Responses、Chat↔Messages、Responses↔Messages确保往返转换后语义一致。5.5 一个容易被忽略的细节finish_reason 映射Chat的finish_reason有stop、length、tool_calls、content_filter几种值。Messages的stop_reason对应的是end_turn、max_tokens、tool_use、stop_sequence。Responses又有一套自己的状态值。转换时要建一张映射表ChatMessagesResponsesstopend_turncompletedlengthmax_tokensincompletetool_callstool_userequires_actioncontent_filter-content_filter映射不完整的话客户端可能无法正确判断响应是否结束导致流式连接挂起或提前关闭。这个坑我在早期版本踩过表现是流式响应偶尔卡住不结束查了半天才发现是finish_reason没映射对。6. 写在最后的一点个人体会协议转换这活儿技术难度不算高但琐碎程度超出预期。字段名映射只是冰山一角真正花时间的是工具调用的嵌套重建、流式事件的状态管理、以及各种边界情况的兼容。我自己的经验是先把三种协议的完整请求-响应往返各写一遍手动构造数据跑通再抽象成转换函数。不要一上来就写通用转换器那样很容易在细节里迷失。等三个方向都跑通了再回头看哪些逻辑可以复用哪些必须分开处理。另外日志一定要打全。协议转换的问题十有八九是中间态和你想的不一样。把转换前后的数据都记下来排查效率能提升好几倍。这个习惯我从做网关开始就养成了到现在依然受用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。