统一大模型API:多模型接入的协议差异与工程实践

发布时间:2026/9/5 22:32:49
统一大模型API:多模型接入的协议差异与工程实践 1. 多模型接入的第一性问题不是模型不够强而是差异太碎了先交代一下我为什么会去折腾这个东西。团队里模型落在三家不同的服务上主力对话走云端闭源模型部分私有化场景接的是本地部署的开源模型还有一些实验性任务会用另一个厂商的小模型。一开始大家各写各的比如对话功能直接调 A 厂商SDK摘要功能直接调 B 厂商SDK每个业务都保存一份自己的 key、日志格式、超时策略。这个结构撑到三四个模型、两三个供应商的时候就已经很难受了。最大的问题不是“接口不一样”而是每个业务都在用各自的方式理解模型能力。A 模型支持把 system prompt 单独传B 模型要求把同样的内容拼进第一条 user 消息A 模型不接受max_tokens缺失B 模型不填就默认给你一个很小的值A 模型返回的finish_reason是lengthB 模型在同样情况下告诉你max_tokens。这些细节单独挑出来都很小但当 API 数量上来了每一个小差异都会变成业务代码里的一个 if-else。后来做多模型评估需要把同一个 prompt 同时发给 5 个模型对比输出质量问题就彻底暴露了。评估脚本为了兼容各家 SDK被迫写成了一百多行的 switch-case。我当时就意识到真正应该做的是造一个中间层把模型供应商的差异全部挡在这层外面让上层业务只面对一个稳定的、内部定义的协议。所以这篇文章要聊的就是做这样一套统一 API SDK 时真正值得关注的工程问题。不是“调 OpenAI 用什么代码”这种入门话题而是当你试图把 Anthropic、Gemini、OpenAI、本地 vLLM/Ollama 等一堆大模型装进同一套接口时你会踩到的那些协议差异、流式陷阱、错误码混乱和对齐测试问题。有这需求的读者大概和我当初的状态差不多多模型只是手段真正要做的是能力选型、容灾切换、成本对比和灰度发布。我不是建议所有人都写一套自己的 SDK后文也会对比成熟开源方案但无论你用现成的还是自己写这篇文章里讲的差异和坑都是绕不开的。2. 各家模型服务差异到底藏在哪把请求、响应、流式拆开看统一 API 容易让人误以为“反正都是 chat 接口能差到哪里去”。真把协议铺开看差别至少能分成四层消息结构、生成参数、流式协议、错误语义。每一层单独看不复杂叠起来就很要命。2.1 消息结构差异system 角色没有你想象的那么通用OpenAI 系的接口基本已经成了行业默认格式messages是一个数组角色有system、user、assistant后来加了developer和tool。内容content可以是字符串也可以是content part数组图片就嵌在image_url里。Anthropic 的接口也带system但它是独立的顶层参数不在messages数组里。另外消息角色里没有通用的developer如果要给对话设定长期行为只能继续用system。Gemini 同样把系统指令放在systemInstruction顶层字段里消息角色叫user和model而不是assistant。这意味着如果你用“OpenAI 格式 一个转换函数”去接所有服务最核心的不是翻译model字段而是重新组织消息结构。一个system 多轮user/assistant的对话在 Anthropic 里代码可能没问题但如果系统指令长度超了、中间又插过 tool 结果转换就会出错。比较稳的做法是内部协议里始终存一个结构化的消息对象而不是直接存各家原始 JSON这样 adapter 每次只处理内部对象到外部协议的转换。2.2 生成参数差异默认值和取值范围都不一样这里我整理了一张常用参数对照表大概可以感受到统一层要处理的信息量。参数OpenAIAnthropicGemini说明最大生成长度max_tokensmax_tokensmax_output_tokensGemini 新旧版本字段名不同温度默认值1.01.0无默认需显式设置不传可能得到不稳定结果top_p支持支持但不建议与温度同用支持部分模型文档明确提示二者互斥停止符stopstop_sequencesstop_sequences字段名不同结构化输出response_formattool_choice结合工具responseSchema/responseMimeType三者原理完全不同工具调用toolstoolstoolsGemini 还有 Google Search 等特殊工具另一个容易踩坑的点是max_tokens在 Anthropic 里是必填参数不传直接报错。而 OpenAI 的接口如果不填会根据模型有不同默认值。国内很多兼容 OpenAI 的服务也继承了这个问题但默认值实现得各不相同。统一 SDK 必须在内部维护一张模型参数规范表例如“缺省 max_tokens 时按 4096 补齐”。否则同一个调用在不同后端会产生完全不同的行为。2.3 流式输出差异同样的“打字机”效果底层结构差很远流式是统一接入里最容易被低估的环节。业务侧看到的打字机效果很一致但协议层返回的数据完全是异构的。OpenAI 的流式是标准 SSE每个 chunk 都有choices[0].deltadelta里可能带着content也可能带着tool_calls最后一个 chunk 才有finish_reason。Anthropic 的流式不是简单的 delta 拼接它的事件类型分content_block_delta、message_start、message_delta等内容和 usage 统计是散布在不同事件里的。Gemini 的流式也不是标准 chat 格式而是在candidates里带分片。如果只是把各家的 chunk 原样抛给前端那还不需要统一层。真实场景是后端自己要先聚合结果用来判断是否触发工具调用、是否截断、是否违规然后再决定向下游返回什么。所以统一协议里至少要定义两类流式事件一类是内容增量一类是元数据变更比如最终finish_reason和最终用量统计。2.4 错误语义HTTP 状态码相同含义不一定相同各家 API 错误设计不统一导致重试策略很难写。比如限流OpenAI 会明确返回 429 并带Retry-AfterAnthropic 同样返回 429但部分错误也包装成了 529。Gemini 的限流通常返回 429 或 503而且错误体的结构完全不一样。更麻烦的是超时。同一 HTTP 状态码可能是连接超时、读超时、模型排队超时或内容审核后的“假超时”。统一 SDK 需要把这些错误翻译成一套内部错误码例如rate_limit_exceeded、context_length_exceeded、content_policy_violation、model_not_found、upstream_connection_error并针对每个错误码给默认重试策略。直接向调用方抛原始 HTTP 错误是省事但业务方被迫了解所有供应商错误的写法就完全背离了统一层的目的。3. 第一版统一 API 设计先定好四类抽象别急着写适配器不少人的做法是“先把三个 SDK 都调通然后把函数签名改成一样的”。这样做的结果是 adapter 和协议耦合得太深今天想加一个供应商就要重写一遍公共逻辑。我自己的经验是反过来先定义一套稳定的内部协议再让各家 provider 往里靠。第一个版本不用做得很复杂但四类抽象必须定下来。3.1 内部协议用“行为字段”而不是“厂商字段”内部协议不建议直接复制 OpenAI 格式理由前面说了很多模型并不遵循同样的角色体系。我在实际改造里用的是类似下面这样的统一结构dataclass class UnifiedMessage: role: str # system / user / assistant / tool content: str | list[ContentPart] name: str | None None tool_calls: list[ToolCall] | None None tool_call_id: str | None Nonecontent可以是纯文本也可以是一个数组数组里的元素可以是text_part、image_part或audio_part。好处是面对多模态请求时消息结构不用另起炉灶。生成参数也单独抽一层dataclass class GenerationConfig: max_tokens: int | None None temperature: float | None None top_p: float | None None stop: list[str] | None None response_format: str | None None # text / json / json_schema json_schema: dict | None None tools: list[dict] | None None tool_choice: str | dict | None Noneresponse_format我建议统一成枚举值json表示需要标准 JSON 输出而不是直接透传 OpenAPI 的response_format对象。为什么因为各家实现 JSON mode 的方式完全不一样OpenAI 用response_format字段Anthropic 要你写一个强制调用的工具Gemini 则设置响应 MIME 类型。SDK 这边只要关心“业务要 JSON”具体怎么让模型输出 JSON 是 provider adapter 需要考虑的事。3.2 四个能力入口大多数业务只需要 chat / embed / rerank / image面向业务方暴露的 SDK 入口我建议固定为四类chat.completions统一对话补全支持流式与非流式embeddings向量化用于检索类场景rerank重排如果只用云上模型这部分可以后续再加images.generations文生图如果以文本模型为主也可以推迟不要为了“完备”一开始就暴露models.list、fine_tuning、audio这些能力。虽然很多后端支持但这些接口差异更大统一成本高早期上线容易拖慢节奏。先覆盖高频需求剩下的用原生透传入口去兜底。3.3 模型资源命名别只存字符串统一 API 要管理的不只是 provider还有模型版本和部署地址。最省事的办法是统一用类似provider/model:deployment的格式来标识模型。比如这样一个结构openai/gpt-4o-mini:default anthropic/claude-3-5-sonnet:default vllm/qwen2.5-72b:prod # 本地部署 gemini/gemini-1.5-flash:testSDK 内部维护一个路由表把上面的资源名映射到真实的基础 URL、API Key、超时配置和模型名。业务侧永远只使用资源名这样以后把openai/gpt-4o-mini的流量全部切到vllm/qwen2.5-72b或另一个厂商同级别模型时只需要改配置不需要动业务代码。3.4 统一响应结构保留原始 metadata 的逃生舱响应的内部结构也要统一通常建议至少包含dataclass class UnifiedResponse: id: str model: str # 实际完成调用的模型名 finish_reason: str # stop / length / tool_calls / content_filter message: UnifiedMessage usage: Usage # input_tokens / output_tokens / total_tokens raw: dict # 各provider原始返回方便排查里面最容易忽略的是raw字段。我在实践中发现不管内部协议定义得多全面总会有排查时需要看原始返回的瞬间。如果 SDK 把原始 JSON 丢了很多线上问题只能靠翻网关日志定位效率低很多。保留原始返回会给排查留一条后路代价只是内存里多存一个字典完全值得。4. 每个 Provider 适配器的高价值实现细节翻译请求比翻译响应更费心完成了内部协议定义接下来就是按 provider 写适配器。这一层很多人误以为是“HTTP 调用封装”写起来很简单真的深入后会发现请求翻译阶段决定了你的兼容性能做到什么程度。4.1 适配器不该直接透传配置要按 provider 规范映射以从内部协议翻译到 Anthropic 请求为例。Anthropic 的max_tokens必填且不接受某些 OpenAI 风格参数按原样传递系统提示要放到system顶层字段历史消息中不能把system角色混在messages里。一个关键的转换片段看起来像这样def build_anthropic_request(req: UnifiedRequest) - dict: system_text None messages [] for m in req.messages: if m.role system: system_text m.content elif m.role user: messages.append({ role: user, content: [{type: text, text: m.content}] }) elif m.role in (assistant, tool): # assistant 消息里的 content 可能为空但 tool_calls 要单独走 tools 字段 messages.append(format_assistant_with_tools(m)) else: # 不认识的 role 丢弃或记为 user取决于业务约定 messages.append({role: user, content: m.content}) payload { model: req.model_real_name, max_tokens: req.generation.max_tokens or 4096, messages: messages, } if system_text: payload[system] system_text # 其他参数如 temperature 按需携带 return payload这段代码最容易出错的就是消息顺序问题。内部协议可能记录了“第 0 条消息是 system”翻译时把它抽出去放到system字段剩下消息的 role 分布就比较干净。但遇到中间穿插了工具调用的复杂历史时Anthropic 要求assistant消息里如果包含tool_calls后面的用户消息必须带上对应的tool_result否则会报错。同一个逻辑在 OpenAI 风格里叫tool消息在 Anthropic 里是user消息里挂一个tool_result块。这里的兼容逻辑建议单独封装不要散落在消息遍历循环里。Gemini 适配器会简单一些它对普通文本对话的结构化要求没那么细。但 Gemini 的systemInstruction同样需要从消息列表里摘出去。而且 Gemini 很多代模型对temperature的默认值处理不同如果统一层里温度是 None最好直接省略而不是填 0。4.2 流式翻译统一成“内容增量 终止事件”两种形态流式适配如果只转发文本内容做到 80% 场景没问题一旦业务用到工具调用就麻烦多了。以 OpenAI 会话补全流式为例工具调用是分片出现的{choices:[{delta:{tool_calls:[{index:0,function:{arguments:{\city\:}}]}}]} {choices:[{delta:{tool_calls:[{index:0,function:{arguments:\北京\}}}]}}]}如果你只是把delta.content拼接工具调用的arguments就丢了。Anthropic 的流式输出里工具调用属于content_block_delta并且一个工具块会有独立的index。Gemini 则是用functionCall在候选内容里逐步返回。统一 SDK 的实现里建议维护一个“工具调用聚合器”class ToolCallAggregator: def __init__(self): self.parts {} # index - arguments 字符串 def add_openai_delta(self, delta): for tc in delta.tool_calls or []: idx tc.index self.parts.setdefault(idx, ) self.parts[idx] tc.function.arguments def to_tool_calls(self): return [ ToolCall(idstr(i), argumentsargs) for i, args in self.parts.items() ]这个类把不同 provider 的流式片段统一累加成完整的工具调用 JSON。这么做的意义在于业务层不用关心工具调用的参数是分几次吐出来的直接拿到聚合后的完整结果再执行下一步。4.3 错误翻译矩阵别让业务层知道 529 和 503 的区别错误码的归一化是要单独写一层而不是在 adapter 里顺便处理。原因在于不同供应商的错误形态可能出现在 HTTP 状态、响应体或代理网关层处理逻辑比较脏散落到处会导致维护困难。一个比较实用的错误翻译函数骨架def normalize_error(provider: str, status_code: int, body: str) - UnifiedError: if status_code 429: return UnifiedError(coderate_limit_exceeded, retryableTrue) if provider anthropic and status_code 529: return UnifiedError(codeupstream_overloaded, retryableTrue) if status_code 400 and context_length in body: return UnifiedError(codecontext_length_exceeded, retryableFalse) return UnifiedError(codeunknown, retryableFalse, raw_bodybody)实现时还必须考虑Retry-After头。有的服务返回 429 但不带重试时间有的是整数秒有的是 HTTP 时间戳格式。SDK 里要统一转成“下次重试时间戳”让调度层能够按优先级处理。4.4 本地模型的接入方式别指望它们完全等价很多团队最后都会把本地部署的模型作为统一 API 的后端之一。常见路径是用 vLLM 或 Ollama 起一个 OpenAI 兼容的服务这类服务通常实现了/v1/chat/completions所以看起来“接入成本极低”。我的建议是能用 OpenAI 兼容协议直接接当然好但本地模型要单独处理几个问题。第一模型名映射不同vLLM 启动时的模型名和内部资源名通常不一致要配置别名映射。第二本地的并发控制很重要GPU 显存有限多个并发请求可能直接 OOMSDK 要给本地 provider 单独设置一个并发信号量和排队队列。第三超时设置要更宽云端模型通常 60 秒内能返回本地模型加载权重、排队推理、甚至首次请求触发 cold start 都可能超过 120 秒如果沿用统一超时大概率会看到“模型没报错但请求超时”的情况。Ollama 自己的原生接口和 OpenAI 兼容接口在细节上也有差别比如/api/chat消息格式是扁平化的和/v1/chat/completions并不完全一样。如果团队已经在用 Ollama 命令行调试建议默认都走它的 OpenAI 兼容端点而不是在 SDK 里单独写一个 Ollama 协议适配器少维护一个分支。5. 容易返工的几个兼容性隐蔽点多模态、时序和参数裁剪我在第二个月才踩完第一批适配器上线后业务侧确实跑通了基础对话。但真正的兼容性问题往往在加了图片、流式审计、返回体校验等功能后大面积冒出来。这里挑几个我印象最深的这几个点如果一开始就规划好后续会省很多返工。5.1 多模态消息不是“加一个 image_url 字段”那么简单统一协议里图片应该是一种 content part比如image_part ImagePart( media_typeimage/jpeg, database64编码后的内容 # 或 url )但各家对图片的处理方式不同。OpenAI 的image_url可以传公网 URL 或 base64Anthropic 需要的是source对象里面区分base64和url类型Gemini 走的是inline_data或file_data而且对图片大小有限制。同样一张图片在 OpenAI 里可以原样传在 Gemini 里可能因为 base64 体积超过限制被拒绝。所以 adapter 翻译的时候不能只做字段名替换。更好的做法是 SDK 在发送前检查内容 part 的总大小并对不适用的 provider 做裁剪或报错而不是等上游返回 400 再排查。5.2 返回体的 finish_reason 分类不要无条件相信各家命名内部协议的finish_reason我建议用统一枚举stop模型自然结束length达到长度限制被截断content_filter内容被服务端过滤策略截断tool_calls模型请求调用工具但不同厂商返回的字段五花八门比如 OpenAI 是content_filterAnthropic 的流式里截断结束事件可能体现在stop_reason为max_tokensGemini 的finishReason可能是MAX_TOKENS或SAFETY。适配器必须把这些都映射成统一的枚举值。这里最容易被忽略的是业务侧通常会把length当作异常重试把content_filter当作恶意内容告警。如果错误映射不准确很有可能把正常的版权保护拦截误判为用户主动停止甚至重试多次产生额外费用。5.3 JSON 输出模式的兼容与其依赖各家实现不如自己做一层兜底结构化输出是现在业务非常依赖的能力但各家实现差异巨大直接硬刚很容易踩坑。OpenAI 的response_format只能保证“返回合法 JSON”不保证 schema 一定匹配它的json_schema功能目前也仅限部分模型。Anthropic 的标准做法是通过工具调用强制 JSON如果你不熟悉可能会困惑为什么直接传一个 JSON 指令并没有得到干净的结果。Gemini 也提供响应 MIME 类型和 schema 声明但字段细节又不同。统一 SDK 在早期最稳妥的方案是优先把各家原生 JSON 能力都封装好如果某个 provider 不支持就退化为“普通文本生成 客户端 JSON 提取和修复”。举个例子本地一个小模型不支持响应 schema但你加了一句话“只输出 JSON”模型可能还是会带多余解释。SDK 可以再做一个后处理器从文本里截取第一个{到最后一个}之间的内容再尝试解析。这样业务代码不需要知道这个模型到底支不支持原生 JSON mode只要下游返回了合法结果就算成功。5.4 流式审计与用量统计非流式拿 usage 容易流式要自己攒云端非流式响应通常都带usage流式响应则不一定。OpenAI 的流式最后会攒出一个 usageAnthropic 需要你在message_delta事件里读取输出 token 数输入 token 只在message_start里有。Gemini 也可能返回空的部分 usage。如果你要做费用核算不能只依赖非流式统计。我的做法是 provider adapter 每次流式结束后内部从各事件里拼出统计对象如果服务端没有给 usage就按当前模型的 token 估算函数补一个近似值并在指标上标记为“估算”。成本统计宁可带个估算标记也不能完全没有数据。5.5 参数裁剪一个 provider 的非标准参数别带到另一个 provider内部协议里会有一些通用配置比如logprobs、seed、user字段。真实情况是某家服务支持seed另一家的 OpenAI 兼容接口虽然不报错但静默忽略还有一家的兼容接口遇到未知参数直接报 400。这要求 adapter 在转换成上游请求时永远不要用 dict 整体透传内部配置而是要显式挑选上游支持的字段。我看到不少项目为了省事把整个 GenerationConfig 转成 dict 后丢给上游 SDK结果某个字段传到不支持的服务上线上就开始大量报错。解决的办法是每家的 adapter 写一个白名单函数明确列出“能翻译/能透传/必须裁剪”的参数宁可多写几行也不能留下隐式传递的黑洞。6. 多模型回归测试的设计这是统一 SDK 最省不了的工程投入统一 API 难不在写代码难在保证不同版本的模型、不同供应商的行为一致性。每次上游 SDK 更新、模型版本切换、新参数上线都可能悄悄破坏原本没问题的功能。没有一套自动化的回归测试矩阵后面的维护就是无底洞。6.1 用例按能力轴设计而不是按厂商设计测试用例建议分成几个能力域Text generation普通文本、多轮对话、超长上下文Streaming文本流分段、工具调用流分段、中断恢复Tool call单工具、多工具、并行工具调用、无工具可调用JSON output标准 JSON、JSON schema、非法 JSON 的后处理Multimodal单图、多图、base64 图片、图片附带文本Error handling限流、模型无权限、上下文超长、内容过滤每个能力域下再按供应商分组。跑测试时不能只验证 HTTP 200还要断言规范化后的字段比如finish_reason length时是否真的返回了截断结果、usage 是否有值、流式能否聚合成一条完整消息。我给一个简单的测试矩阵示意能力OpenAIAnthropicGemini本地 vLLM多轮文本对话必测必测必测必测流式文本必测必测必测必测并行工具调用必测重点可选可选JSON 模式必测重点重点后处理兜底图片输入必测必测必测可选6.2 关键技巧用本地 mock 服务做协议回归不烧真实 token同一套测试用例全部打真实 API成本太高而且上游不稳定会导致测试随机失败。推荐的方法是做一层 mock provider录制各家 API 的响应样本放到 fixtures 目录跑测时 adapter 直接返回录制内容不发起真实请求。mock 测试的核心收益有两个一是验证统一层的转换逻辑是否正确二是上游协议变更后回放旧样本能快速知道哪些字段因为上游改动而无法匹配。比如 Anthropic 如果加了新的事件类型旧录制样本里没有对应 handler测试就会立刻报警而不是到线上才暴露。我会建议用类似录制回放的方式保留每个 provider 的原始响应线上真实流量也可以按需采样存到存储里用于“能否解析最新返回”的离线校验。6.3 灰度切换的隐藏成本模型差异要在发布前暴露统一 API 的另一个价值是支持线上模型无感切换。但无感不等于“完全等价”。两个能力相近的模型在同样 prompt 下可能因为训练数据差异、特殊 token 处理和默认参数不同产生肉眼可见的结果差异。我经历过一次灰度切换事故内部把一个入口从模型 A 切到模型 B只是改了资源路由配置没有注意到 A 的接口默认返回 JSON、B 需要显式开 JSON mode。因为 B 提供商配置里没映射response_format线上服务直接返回了一段带解释性文字的 markdown导致下游解析失败。自那以后我在切换任何模型前都会要求先跑一遍本次的回归集重点看 JSON 输出、工具调用截断、首个 token 延迟这几个指标并把新老模型放到业务评测集上做一次对比。6.4 可观测性统一日志的字段比协议本身更能救命统一 SDK 上线后日志格式最好也统一成 JSON 结构每个日志里包含这些上下文trace_id贯穿业务请求和上游调用的唯一 IDprovider真实使用的供应商model供应商侧模型名request_id上游返回的 request id排障时直接拿这个找对方支持latency_ms总耗时usage_input_tokens/usage_output_tokens/estimated_costfinish_reason统一后的结束原因error_code统一错误码这个模型是我们在实际排查中发现最有用的。有一次线上报错说“偶尔超时”通过统一日志一查发现超时全部集中在某个模型在流式模式下首次 token 延迟过高而普通模式下却正常。后来确认是该模型服务端冷启动导致。如果没有 trace 贯穿这类问题很难从业务日志中定位。7. 我最后想多说一句关于选型的话做统一 API 不一定要从零开始写一个自己的 SDK。现在开源世界里已经有不少不错的选择比如偏向 Python 接入层的 LiteLLM以及偏向 API 网关形态的 one-api 风格项目。它们解决的问题不一样LiteLLM类库适合团队自己内部做统一调用想要在代码层控制消息转换和重试逻辑且主要语言是 Python 的场景。它的优点是保持各家能力的同时提供统一入口缺点是部分高级能力还是要在 provider 层单独写逻辑。one-api 风格网关适合想给团队发 key、做额度管理和调用审计的场景。它自带 Web 管理界面方便把多个模型上架成统一 API 服务适合作为企业内部 LLM 网关。自研 SDK适合有强定制需求的情况比如内部协议需要贴合自己的知识库格式、工具调用需要统一成公司内部协议、或者要深度对接本地 vLLM 部署。自研需要付出的核心成本不是初始代码量而是后续维护协议差异和回归测试的长期投入。以我个人的经验而言如果团队已经有三条业务线在直接调模型厂商接口不要等出事故再动。先用一个网关层解决“key 分散、模型散落”的问题然后再逐步用统一 SDK 替换业务代码里的厂商 SDK 调用。相比一口气把所有业务改完这个渐进式迁移的风险要小很多。再给一个建议统一层第一版上线后保留“透传原生参数”的逃生舱入口。有些新模型功能刚发布时官方 SDK 还没有完整支持如果你的统一层把所有参数都白名单化了业务就会在模型最新能力上落后半拍。给每个 provider 的 chat 方法加一个extra_body或者provider_kwargs参数让高级用户直接传原始参数。等这个能力用稳定了再升级为正式的内部协议字段。一套 SDK 调用所有大模型这件事听起来像是在做“万能适配器”做了以后才明白真正的难点在于让你的业务团队完全不需要关心模型差异背后的故事。能把上游几百种不同风格的返回碾成一个稳定的协议本身就已经是在给团队降低认知负担了。后续加模型只会越来越顺因为协议已经被磨过一遍剩下的只是新 adapter 的翻译工作。