同一模型两套接口格式:LongCat-2.5-Preview网关层适配实践

发布时间:2026/10/13 18:43:29
同一模型两套接口格式:LongCat-2.5-Preview网关层适配实践
LongCat-2.5-Preview 上架那天DMXAPI 的接入群消息直接炸了。大家问得最多的不是这个模型的榜单分数又涨了几个点而是“新接口到底怎么接”。我盯着发布单看了很久真正让我觉得值得单独写一篇的不是 LongCat-2.5-Preview 的参数而是同一个模型给了两套接口格式这件事。参数当然重要但两套接口格式意味着完全不同的工程信号存量代码可以带着老协议直接切过来新用户又可以通过新协议去尝鲜原生特性。这比参数更能决定一个模型在生态里的接入体验。我在 DMXAPI 负责模型接入和网关层这部分LongCat-2.5-Preview 是我完整跟下来的一个协议适配项目中间踩了不少坑也把方案收敛成了一套可以复用到其他模型上的接入模板。今天把这些经验完整写一遍。1. 两套接口格式整体设计与核心思路拆解1.1 LongCat-2.5-Preview 为什么需要两套格式先说背景。LongCat-2.5-Preview 这个模型本身带了不少原生特性比如更灵活的 system 指令隔离、thinking_level 采样控制、动态上下文长度调整。如果只做一套接口这些能力只能通过扩展字段透传调用方理解成本很高。但如果完全按 LongCat 原生协议出接口又会把大量已经熟悉 Chat 风格的老用户挡在门外。这里面的核心矛盾是模型原生能力越来越强但生态惯性还牢牢绑在“messages 数组 role/content”这套通用 Chat 风格上。绝大多数第三方应用、开源框架、内部工具链都是按 Chat 风格封装好的。让开发者为了一个新模型去改整套调用链基本等于劝退。两套接口格式的价值就在这里LongCat 原生格式负责把这些新能力完整暴露出来Chat 风格接口负责兼容存量用户。同一个模型权重同一个推理服务对外呈现两种协议谁都不用迁就谁。这和我以前做视频采集卡时的经验很像——同一台设备既给 HDMI 又给 Type-C用户手里是哪种线都能直接插上而不是逼着所有人统一换线。1.2 方案选型在模型里写双 handler还是在网关层做协议适配一开始内部讨论过三个方案。方案 A 是在模型服务内部直接写两套 handler每个接口路径各接各的参数各自拼 prompt各自解析返回。这个方案最简单但问题也最明显每接一个新模型就要把同样的逻辑复制一遍模型升级时两套代码要同步改prompt 拼接逻辑稍微走样一点两边返回就不一致了。做两三个模型之后基本就是维护灾难。方案 B 是独立一个 API 网关层负责路由、鉴权、限流、协议转换、计量统计后端模型服务只暴露一套内部原生协议。DMXAPI 最终选了这条路。原因是平台上的模型不止 LongCat 一个网关层一旦把协议转换做成通用能力后面每个新模型都能复用而不是每加一个模型就重写一遍接口层。方案 C 是给每个模型配一个独立的转换 agent不在请求路径上做字段映射而是用模型自己来“理解”请求再转发给目标模型。这个方案当时被我直接否了转换 agent 本质上是把确定性的事情交给不确定性去做延迟高、成本贵、还容易在格式理解上翻车。最终选方案 B核心考虑有几点协议转换是纯确定性的字段映射完全不需要模型参与也不应该让模型参与。网关层可以把鉴权、限流、计量、链路追踪统一掉屏蔽掉后端各种模型服务的差异。后续协议升级只需要改网关的映射表SDK 和下游代码可以保持稳定。1.3 这套设计避开了什么问题最直接的是避开了“每出一个模型就多一套 SDK”的老路。早期大模型平台喜欢给每个模型单独出 SDK用户接一个模型就要装一个新包包和包之间的参数风格还不一样搞得开发者非常痛苦。两套格式配合网关层之后用户要么用市面上已经通用的 Chat 风格 SDK要么用 LongCat 原生 SDK两条路都有明确文档和稳定契约。其次避开了供应商锁定。开发者最担心的事是今天接得爽、明天想换模型发现迁移成本高到离谱。两套接口格式意味着调用方的业务代码和具体模型之间的耦合被压到了最低。真到要换模型那天只需要改网关层配置下游代码基本不用动。还有一个容易被忽略的点错误码。后端不同模型返回的错误结构五花八门有些是字符串错误码有些是数字 HTTP status有些错误信息里还带了一坨内部堆栈。网关层做统一协议适配时顺手把错误码也标准化了。调用方拿到手的是一个统一的错误模型排障效率高很多。2. 两套接口格式的核心差异与字段对照2.1 请求参数差异Chat 风格与 LongCat 原生风格差在哪LongCat-2.5-Preview 的两套格式一套是 Chat 风格另一套是 LongCat 原生格式。两边的差异远不止路径不同参数结构差别很大。含义Chat 风格接口LongCat 原生格式接口模型标识modelmodel_id对话历史messages数组元素含role、contentprompt字符串 conversation_id系统提示messages里塞一条rolesystem独立字段system_profile采样温度temperaturetemp随机采样top_ptop_p命名一致但取值边界不同最大输出长度max_tokensmax_output_tokens停止条件stopstop_sequences流式开关streamstream工具调用toolstool_choicefunctionsfunction_call扩展能力无只能透传thinking_level、dynamic_context等一开始我以为这就是改个字段名的事实际做下来发现完全不是。最典型的坑是 Chat 风格里系统提示和用户消息混在同一个messages数组里模型需要自己区分而 LongCat 原生格式把system_profile独立出来等于在协议层就把指令上下文和用户内容隔开了。这种差异不是改个字段名能覆盖的转换层必须把 Chat 风格里的rolesystem消息抽出来经过合法性校验后再注入到system_profile里。2.2 返回结构与流式事件的差异非流式返回的差异同样不小。Chat 风格的标准返回是三层结构choices - message - content附带一个finish_reason。LongCat 原生格式则是output.text直接返回正文文本下面再挂采样参数和 token 统计。转换层要做的不是简单把choices[0].message.content改成output.text而是要把整个返回体结构重排保证两端拿到的字段语义完全一致。流式接口差异更大。Chat 风格的 SSE 事件里通常是choices[0].delta.content每个 chunk 里只带增量文本LongCat 原生格式流式事件是event: deltadata 里放结构不同的 JSON。网关层做流式转换时不能把整个响应体缓存住等结束再吐那样首字延迟会很高必须做成流式转发后端每推一个 chunk转换层立刻改写成目标格式再推给客户端。token 统计也是重点。Chat 风格习惯用prompt_tokens、completion_tokens、total_tokensLongCat 原生格式用input_tokens、output_tokens。字段名不统一会导致计费系统崩溃。我在网关层写了一个标准化的 usage 结构所有后端返回都先转成统一口径再根据调用方使用的协议格式重新渲染字段。2.3 工具调用与停止条件的映射细节工具调用是字段映射里最复杂的一块没有之一。Chat 风格的tools是一个数组每个工具元素包含type、functionfunction里有name、description、parameters其中parameters是 JSON Schema。LongCat 原生格式的functions结构更扁平有的版本用input_schema而不是parameters。转换时要把 JSON Schema 里的required、properties、type完整搬运过去缺一个字段底层就可能拒绝执行。停止条件也容易出错。Chat 风格的stop是字符串或字符串数组LongCat 原生格式叫stop_sequences语义上还多一个“停止后是否保留终止序列”的选项。我一开始转换时直接把stop内容塞进stop_sequences结果发现模型输出结尾会多出半个词。后来查了原生协议文档才发现stop_sequences在内部实现里会对序列做 tokenize转换时必须原样透传不能自己先用字符串截断一遍。3. 网关层接入与配置落地3.1 网关路由与整体请求链路DMXAPI 网关层从拿到请求到最终返回完整链路是这样的客户端发起请求URL 路径区分协议格式。Chat 风格走/v1/chat/completionsLongCat 原生格式走/v1/longcat/generate。网关先统一鉴权。不管调用方用的是 Bearer Token 还是X-API-Key进入网关后一律转成内部身份标识。限流和配额检查。请求适配层根据协议类型做字段映射转成后端模型服务内部统一格式。调用 LongCat-2.5-Preview 推理服务拿到响应。响应适配层处理流式或非流式返回再把 usage、错误码、finish_reason 按调用方协议格式渲染回去。网关层本身是无状态的协议转换不依赖任何会话数据所以可以水平扩展。路由规则不用写在代码里我用 YAML 维护了一张映射表不同协议格式对应不同的适配器。3.2 请求转换实现的几个关键步骤请求转换的核心代码如下我以网关内部的适配器为例。这个函数做几件事把 Chat 风格的messages转成原生格式的prompt抽取 system 消息放入system_profile把参数名统一替换。def normalize_chat_to_longcat(req: dict) - dict: messages req.get(messages, []) system_profile [] dialogue [] for msg in messages: role msg.get(role) content msg.get(content, ) if role system: system_profile.append(content) else: dialogue.append(f{role}: {content}) normalized { model_id: req.get(model), prompt: \n.join(dialogue), system_profile: \n.join(system_profile) if system_profile else None, temp: req.get(temperature, 0.7), max_output_tokens: req.get(max_tokens, 1024), } if req.get(stop) is not None: normalized[stop_sequences] req[stop] if req.get(tools) is not None: normalized[functions] convert_tools_to_functions(req[tools]) if req.get(stream): normalized[stream] True return normalized这段代码看着简单但有两个细节很关键。第一system_profile不能直接拼接字符串因为原生接口有可能对这个字段做长度限制超过上限会返回鉴权错误或者参数错误。转换层必须对 system 内容做长度保护超长时优先丢弃中间的重复指令保留首尾的关键约束。第二temperature的取值边界两边不一致Chat 风格允许 0 到 2LongCat 原生格式只允许 0 到 1.5转换时不能直接透传必须做 clamp。3.3 流式响应转换的实现要点流式转换是这次接入里最磨人的部分。LongCat 原生格式的流式事件类型比 Chat 风格多除了增量文本外还有thinking事件和done事件。Chat 风格只关心choices里的delta所以转换层要把多余事件过滤掉同时保证finish_reason只出现在最后一个 chunk 里。async def convert_longcat_stream_to_chat(src_stream): async for event in src_stream: if event.get(type) thinking: continue if event.get(type) delta: text event.get(text, ) chunk { choices: [ { delta: {content: text}, index: 0, } ] } yield chunk if event.get(type) done: finish event.get(finish_reason, stop) chunk { choices: [ { delta: {}, index: 0, finish_reason: finish, } ] } yield chunk return流式转换有两个坑需要特别注意。一个是 SSE 格式的续行处理流式输出里如果文本内容包含换行必须按 SSE 规范转义否则客户端解析会断开。另一个是流结束时不能漏掉usage很多场景下调用方依赖流式响应末尾的 token 统计做计量但流式的 usage 往往是在done事件里单独携带的转换层必须把它缓存住并拼到最终 chunk 里。3.4 性能开销与配置细节协议转换层最让人担心的是性能。我实测下来纯字段映射的开销非常小一个 10k tokens 的请求请求体约 40KB 到 60KB转换耗时在 1ms 到 3ms 之间。网关主要开销还是在网络转发和鉴权上。内存方面转换层会创建一份新的请求体结构10k tokens 场景下额外占用内存不到 10MB128k 长上下文场景会多出 0.5MB 到 1MB 的临时对象这在常规网关实例上完全不是瓶颈。真正的性能隐患在流式场景。如果你在响应适配层用了“攒一批再推一把”的缓冲策略首字延迟会直线上升。LongCat-2.5-Preview 首字延迟本身在百毫秒级如果网关再缓冲 2 秒用户体验直接崩掉。我的经验是流式链路全程用异步生成器每收到一个事件就立刻转换立刻下发不做任何聚合缓冲。另一个配置细节是后端调用的 read timeout 不能设太短。Chat 风格的请求里有些复杂推理请求的输出很长首字产生时间可能超过普通 HTTP 超时阈值。网关对后端模型服务的超时设置要比对外客户端超时更宽裕否则对外还没超时对内已经先掐断连接了。4. 迁移与联调中的常见问题与排查技巧4.1 最容易踩的五个坑第一是鉴权头不一致。不少老客户端习惯用Authorization: Bearer xxx但也有 SDK 用X-API-Key。如果网关层只认一种另一类调用方上来就是 401。处理方式是网关统一从两个位置读取身份然后归一化成内部identity字段后端模型服务完全不感知原始协议差异。第二是max_tokens语义不一致。有些 Chat 风格客户端会在max_tokens里传-1表示“由服务端决定”但 LongCat 原生接口不接受负数。转换时必须把-1映射成模型默认最大输出长度否则后端直接报参数校验错误。第三是usage缺失导致计费异常。LongCat-2.5-Preview 在非流式返回里一定会带用量但流式模式下部分推理服务在异常中断时不会推 final usage。网关必须把已转发的 token 数在内存里累计一份流被客户端断开时也要能输出一个兜底用量否则计费就漏了。第四是 system prompt 被覆盖。Chat 风格里用户可以在多轮对话中多次插入rolesystem消息每次都转成system_profile就会出现多个 system 指令互相打架。我后来在转换层做了一次合并不是简单拼接而是把“指令型 system”和“内容型 system”分开指令型提取进system_profile内容型保留在对话上下文里。第五是工具调用的 JSON Schema 差异。Chat 风格的required数组可能为空原生格式某些版本要求required必须显式存在。转换时如果不补一个空数组模型侧工具注册会失败。这类问题往往不在联调初期暴露而是在并发压测时随机出现非常隐蔽。4.2 排查思路与最小复现方法协议转换类问题最怕的是两边日志各查各的。我在网关里强制要求所有内部请求携带同一个trace_id并且从客户端请求头透传到后端模型服务日志。排查问题时先拿trace_id把“客户端请求 - 网关转换后请求 - 后端返回 - 网关还原后响应”四段日志串起来基本一眼就能看出是哪个环节出了问题。定位到转换环节后再做最小复现单独写一个测试脚本把客户端原始请求 JSON 固定下来直接调网关内部的转换函数比较转换前后的 JSON 结构。这样可以把网络问题、鉴权问题、后端推理问题全部隔离掉只看字段映射本身对不对。这个思路我用了很多次每次都管用。4.3 常见问题速查表问题现象可能原因处理方式两边接口返回的文本不一致system 消息合并策略不同统一 system 抽取规则确认合并顺序流式响应少最后一段done事件被过滤但未携带 finish保留 done 事件并映射 finish_reason工具调用报错required缺失或 JSON Schema 格式不符转换层补齐空数组并校验类型计费 token 数量对不上usage 字段名未映射在网关统一 usage 结构长对话后出现截断上下文超过后端限制转换层增加上下文长度保护POST 请求返回 413请求体过大但网关未分片网关层开启请求体流式转储5. 运维视角计费口径、灰度发布与监控5.1 计费口径统一是协议适配的隐藏任务两套接口格式最容易被忽略的运维问题就是计费口径。Chat 风格返回的total_tokens和 LongCat 原生格式返回的output_tokens都是“真实”的但统计口径可能有细微差异。有的会包含特殊 token有的不包含有的在思考链模式下会把思考 token 单独列出来。如果直接拿两个字段各自计费同一个会话分账就会出现偏差。DMXAPI 的处理方式是在网关层定义一个内部标准化 usage 结构字段固定为input_tokens、output_tokens、total_tokens。后端返回先转成这个结构然后计费系统只认这个结构。对外暴露时再根据调用方协议格式渲染。这样多套接口格式虽然协议不同但计费源始终是同一个不会出现两套账单。5.2 灰度发布与双协议一致性校验LongCat-2.5-Preview 上架不是直接把流量全部切过去而是先做了灰度。网关层的路由规则里同时支持model_version和format两个维度比如先让 5% 的新用户走 LongCat 原生格式剩下的继续走 Chat 风格。两个协议都打到同一个后端推理服务上不存在“同版本不同权重”的差异这给灰度带来了很大便利。灰度期间我做了一轮双协议一致性校验用同一批 prompt 分别通过两套接口调用比较输出文本的语义相似度和finish_reason一致性。因为采样参数存在随机性文本完全一致是不可能的但语义相似度要超过阈值finish_reason必须完全一致。如果发现 Chat 风格的 stop 行为和原生格式不一致就说明映射表有 bug。这套校验跑了一天还真抓到了两个 stop 相关的问题。5.3 监控指标与告警阈值双协议上线后原先只看模型服务本身的监控已经不够了我加了下面这些网关层指标指标含义告警阈值gw_convert_error_rate协议转换失败率连续 5 分钟大于 1%gw_format_ratio两套格式的流量占比无固定阈值用于观察迁移趋势gw_convert_latency_p99转换层耗时持续大于 50msgw_stream_interrupt_rate流式连接异常中断率大于 2%gw_usage_missing_rateusage 缺失率大于 0.5%这里想说一个实用细节新增协议适配器的上线不要等到出问题才看监控。我习惯在灰度发布的第一时间就盯着gw_convert_error_rate和gw_stream_interrupt_rate两个指标它们对转换 bug 非常敏感。第一轮灰度我就是在监控面板上看到一个interrupt_rate突然跳到 4%才定位到流式转换里有个空 chunk 没有正确过滤的问题。6. 最后说点实际经验LongCat-2.5-Preview 这次双协议接入我最大的感受是同一个模型给两套接口格式绝不是多写几个 if 那么简单。它要求你把协议当成一个独立产品去维护字段映射、错误码、流式事件、usage 口径每一层都要有明确契约。最初我也想过在模型服务内部偷懒写两套 handler现在回头看如果没有网关层这套统一适配后面接更多模型时迟早会被维护成本压垮。还有一点想分享给接模型服务的同行无论做几套协议trace_id和 usage 口径一定要在第一天就统一不要等联调出问题了再补system 消息的合并策略也一定要在映射表里写清楚不然两边接口在复杂对话场景下很容易出现行为分叉。这套思路现在已经被 DMXAPI 复用到了后续其他模型的接入流程里实测下来从立项到灰度上线比之前只做单协议的模型节省了大概三分之一的时间。