适配器模式把一个类的接口转换成客户端期望的另一个接口,让原本不兼容的类能协作,它不改变现有实现,只在中间加一层”转接头”。在我们自研的企业级 AI 网关里,OpenAI、通义千问、DeepSeek、智谱的 API 在鉴权方式、参数命名、流式返回格式上差异很大,我们靠一套统一的 ModelAdapter 接口加每厂商一个适配器实现来屏蔽差异。业务侧只面向统一接口编程,新增一家模型厂商只需新增一个适配器类,路由、限流、日志等横切逻辑全部复用,业务代码一行不用改。该网关同时服务着内部 AI 爆款文章创作器的多阶段生成。
一、适配器模式在网关里解决什么问题
多模型接入的第一痛点不是”能不能调通”,而是”切换成本”。OpenAI 用 temperature 控制随机性,Google Gemini 用 candidateCount,通义千问的流式返回结构又和 Anthropic 不同。如果业务代码直接调用各家原生 SDK,每接一个厂商就要写一套调用逻辑、一套超时重试、一套错误码映射,改模型等于改业务代码。
适配器模式的核心价值是建立契约。我们抽出四个方法作为统一契约:
| 契约方法 | 职责 | 说明 |
|---|---|---|
chat(request) |
同步对话 | 返回完整文本结果 |
chatStream(request) |
流式对话 | 返回 Flux<String>,逐 token 推送 |
embed(text) |
向量化 | 供 RAG 检索链路使用 |
providerName() |
厂商标识 | 用于路由与成本统计 |
四个方法覆盖了网关 90% 的调用场景。同步调用给管理后台做测试,流式调用给文章创作器和对话网关用,向量化给知识库检索用。每家厂商只需把自家原生 SDK 塞进这四根管子里,对外表现一致。
二、适配层怎么落地:接口 + 适配器 + 工厂装配
网关的适配层分三层:顶层是 ModelAdapter 接口,中间是各厂商适配器,底层是 Spring 容器按配置装配。业务代码从不用关心具体实现类。
public interface ModelAdapter {
String providerName();
String chat(ChatRequest request);
Flux<String> chatStream(ChatRequest request);
float[] embed(String text);
}
DeepSeek 的适配器把 Spring AI 的 ChatModel 包一层即可:
@Service
public class DeepSeekAdapter implements ModelAdapter {
private final ChatModel chatModel;
public DeepSeekAdapter(ChatModel chatModel) {
this.chatModel = chatModel;
}
@Override
public String providerName() {
return "deepseek";
}
@Override
public String chat(ChatRequest request) {
Prompt prompt = new Prompt(request.messages());
ChatResponse resp = chatModel.call(prompt);
return resp.getResult().getOutput().getText();
}
@Override
public Flux<String> chatStream(ChatRequest request) {
return chatModel.stream(new Prompt(request.messages()))
.map(r -> r.getResult().getOutput().getText());
}
}
接入新厂商时照这个模板复制一份,改改参数和内部实现,适配器就完成了。网关侧再维护一个 Map<String, ModelAdapter>,启动时把容器里所有适配器按 providerName() 注册进去,路由模块按模型名取出对应适配器调用。整个过程对业务透明。
2.1 流式输出的格式差异怎么收敛
不同厂商的 SSE 流式格式不同,OpenAI 返回的是 data: {...} 逐段 JSON,Anthropic 用事件前缀标记消息边界。适配器层把这种差异消化在内部:适配器只向网关层返回统一格式的文本块,网关再把文本块按统一 SSE 协议推给前端。创作器的 Vue 前端订阅固定地址,底层是哪个模型对它不可见,多阶段生成(选题 → 大纲 → 成文 → 润色)可以分别路由到不同模型。
2.2 错误码映射也属于适配工作
各家厂商的异常体系完全不一致,OpenAI 用带 status 的 HTTP 错误,通义千问返回业务码。适配器统一转换,把厂商错误映射成网关自己的错误枚举(限流、超时、内容审核、服务不可用)。路由与降级模块只认这套枚举,判断逻辑保持简单干净。
三、接入一家新模型的五步标准动作
新模型接入被固定成可复制的流程,新同学照着走一遍就能上线:
- 在
application.yml里配置base-url、api-key、默认模型名,走 Spring 配置绑定; - 新建适配器类实现
ModelAdapter,把厂商原生调用翻译成四个契约方法; - 在
@Bean工厂方法里把适配器注册进适配器注册表,启动时自动装配; - 用网关内置的联调接口跑一遍同步和流式两条链路,核对返回文本;
- 配置一条测试路由,压测流式首 token 耗时与超时重试表现,通过后开放给生产。
这套流程跑熟后,单模型接入耗时从数天压缩到几小时,且不碰任何业务代码。
四、多模型接入三种方案的对比
| 方案 | 维护成本 | 扩展成本 | 统一能力(限流/监控) | 适用阶段 |
|---|---|---|---|---|
| 业务代码直连各家 SDK | 高,散落各处 | 高,每处都改 | 无,各写各的 | 原型验证 |
| 统一接口 + 适配器(本项目) | 中,集中在适配层 | 低,新增适配器即可 | 有,网关层统一收口 | 生产多模型接入 |
| 引入成熟 AI 网关中间件 | 低 | 低 | 有 | 团队人手不足时 |
直连方案只接一家模型时很省事,接两家以上就失控。自研适配器适合已用 Spring AI、需要定制路由与合规逻辑的团队。引入成熟中间件省人力,但定制能力受限。
五、落地时容易踩的坑
最容易被忽视的是消息格式转换,尤其是工具调用(Function Calling)的格式,各家厂商差异最大,占了适配工作量的绝大部分。其次是超时与重试要放在适配器内部统一处理,用 RestClient 拦截器收口,而不是每个适配器各自写一遍。流式链路的背压也要控制好,Flux 慢消费会把前端 SSE 连接拖垮。
常见问题(FAQ)
Q1:适配器模式和策略模式怎么区分?
适配器解决接口不兼容,策略解决算法可替换。网关里二者叠加用:适配器翻译接口,策略决定路由选谁。
Q2:必须为每个模型单独写适配器吗?
同一厂商同协议可以共用一个适配器,用配置区分模型名。跨厂商、跨协议则必须单独实现。
Q3:适配器写好后业务代码还感知模型差异吗?
不感知。业务只面向统一接口编程,换模型只改配置与路由,不碰业务代码。