AI 多供应商适配层设计:OpenAI/Claude/DeepSeek/Ollama 统一接口与动态路由
AI 多供应商适配层设计思路
引言
随着 AI 大模型生态的蓬勃发展,企业级应用往往需要对接多家 AI 供应商:OpenAI、Claude、智谱 GLM、DeepSeek、MiniMax、豆包、Moonshot、Ollama 等。不同供应商的 API 接口、请求格式、响应结构各不相同,如果业务代码直接调用各供应商 SDK,将面临以下问题:
- 切换供应商需改代码:从 OpenAI 切换到 DeepSeek,需修改所有调用点
- 新增供应商成本高:每新增一家供应商,需改动业务逻辑
- 无法混合调用:不同功能使用不同供应商的需求难以满足
本文将分享 AI 多供应商适配层的设计思路,通过统一接口抽象 + 工厂模式 + 策略模式,实现供应商切换无需改代码。
一、供应商差异分析
1.1 接口差异对比
| 供应商 | Chat 接口 | 流式响应 | Embedding | 模型列表 |
|---|---|---|---|---|
| OpenAI | /v1/chat/completions | SSE | /v1/embeddings | /v1/models |
| Claude | /v1/messages | SSE | 不支持 | 不支持 |
| 智谱 GLM | /api/paas/v4/chat/completions | SSE | /api/paas/v4/embeddings | /api/paas/v4/models |
| DeepSeek | /v1/chat/completions | SSE | /v1/embeddings | /v1/models |
| Ollama | /api/chat | NDJSON | /api/embeddings | /api/tags |
| 豆包 | /api/v3/chat/completions | SSE | /api/v3/embeddings | /api/v3/models |
1.2 核心差异点
graph LR
A[供应商差异] --> B[API地址不同]
A --> C[请求格式不同]
A --> D[响应格式不同]
A --> E[认证方式不同]
A --> F[流式协议不同]
A --> G[模型命名不同]
style A fill:#ffcdd2,stroke:#c62828
二、适配层架构设计
2.1 分层架构
graph TB
subgraph 业务层
A[ChatService<br/>对话服务]
B[EmbeddingService<br/>向量服务]
C[ImageService<br/>图像服务]
end
subgraph 适配层
D[AIAdapter<br/>统一接口]
E[AdapterFactory<br/>供应商工厂]
F[RequestConverter<br/>请求转换器]
G[ResponseConverter<br/>响应转换器]
end
subgraph 供应商实现
H[OpenAIAdapter]
I[ClaudeAdapter]
J[DeepSeekAdapter]
K[OllamaAdapter]
L[ZhipuAdapter]
M[MiniMaxAdapter]
end
A --> D
B --> D
C --> D
D --> E
E --> H
E --> I
E --> J
E --> K
E --> L
E --> M
style D fill:#e1f5fe,stroke:#0288d1
style E fill:#f3e5f5,stroke:#7b1fa2
2.2 核心接口定义
/**
* AI供应商适配器统一接口
* 所有供应商实现此接口
*/
public interface AIAdapter {
/**
* 获取供应商类型标识
*/
String getProvider();
/**
* 同步对话
* @param request 统一请求
* @return 统一响应
*/
ChatResponse chat(ChatRequest request);
/**
* 流式对话
* @param request 统一请求
* @return 流式响应
*/
Flux<ChatResponse> chatStream(ChatRequest request);
/**
* 获取Embedding向量
* @param request 统一请求
* @return 向量响应
*/
EmbeddingResponse embedding(EmbeddingRequest request);
/**
* 获取可用模型列表
* @return 模型列表
*/
List<ModelInfo> listModels();
}
2.3 统一请求/响应对象
/**
* 统一对话请求
*/
public record ChatRequest(
String model, // 模型名称
List<ChatMessage> messages, // 消息列表
Double temperature, // 温度参数
Double topP, // Top-P参数
Integer maxTokens, // 最大Token数
Boolean stream // 是否流式
) {}
/**
* 统一消息
*/
public record ChatMessage(
String role, // system/user/assistant
String content // 消息内容
) {}
/**
* 统一对话响应
*/
public record ChatResponse(
String id, // 响应ID
String model, // 使用的模型
List<ChatChoice> choices, // 回复选项
UsageInfo usage // Token用量
) {}
/**
* 回复选项
*/
public record ChatChoice(
Integer index,
ChatMessage message,
String finishReason
) {}
/**
* Token用量信息
*/
public record UsageInfo(
Integer promptTokens,
Integer completionTokens,
Integer totalTokens
) {}
三、供应商适配器实现
3.1 OpenAI 适配器
/**
* OpenAI供应商适配器
*/
@Component
public class OpenAIAdapter implements AIAdapter {
private final RestTemplate restTemplate;
@Value("${ai.openai.api-key}")
private String apiKey;
@Value("${ai.openai.base-url:https://api.openai.com}")
private String baseUrl;
@Override
public String getProvider() {
return "openai";
}
@Override
public ChatResponse chat(ChatRequest request) {
// 转换为OpenAI请求格式
Map<String, Object> openaiRequest = convertRequest(request);
openaiRequest.put("stream", false);
// 发送请求
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(apiKey);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity =
new HttpEntity<>(openaiRequest, headers);
ResponseEntity<Map> response = restTemplate.exchange(
baseUrl + "/v1/chat/completions",
HttpMethod.POST, entity, Map.class);
// 转换为统一响应格式
return convertResponse(response.getBody());
}
@Override
public Flux<ChatResponse> chatStream(ChatRequest request) {
Map<String, Object> openaiRequest = convertRequest(request);
openaiRequest.put("stream", true);
// 使用WebClient处理SSE流
return webClient.post()
.uri(baseUrl + "/v1/chat/completions")
.header("Authorization", "Bearer " + apiKey)
.bodyValue(openaiRequest)
.retrieve()
.bodyToFlux(String.class)
.filter(line -> !line.equals("[DONE]"))
.map(this::parseStreamChunk);
}
/**
* 将统一请求转换为OpenAI格式
*/
private Map<String, Object> convertRequest(ChatRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("model", request.model());
map.put("messages", request.messages().stream()
.map(m -> Map.of("role", m.role(), "content", m.content()))
.collect(Collectors.toList()));
if (request.temperature() != null) {
map.put("temperature", request.temperature());
}
if (request.maxTokens() != null) {
map.put("max_tokens", request.maxTokens());
}
return map;
}
}
3.2 Ollama 适配器
/**
* Ollama本地模型适配器
* Ollama的API格式与OpenAI不同,需要特殊处理
*/
@Component
public class OllamaAdapter implements AIAdapter {
@Value("${ai.ollama.base-url:http://localhost:11434}")
private String baseUrl;
@Override
public String getProvider() {
return "ollama";
}
@Override
public ChatResponse chat(ChatRequest request) {
// Ollama使用不同的请求格式
Map<String, Object> ollamaRequest = Map.of(
"model", request.model(),
"messages", request.messages().stream()
.map(m -> Map.of("role", m.role(), "content", m.content()))
.collect(Collectors.toList()),
"stream", false
);
ResponseEntity<Map> response = restTemplate.exchange(
baseUrl + "/api/chat",
HttpMethod.POST,
new HttpEntity<>(ollamaRequest),
Map.class
);
return convertOllamaResponse(response.getBody());
}
/**
* Ollama响应格式转换
* Ollama返回的格式与OpenAI不同,需要映射
*/
private ChatResponse convertOllamaResponse(Map body) {
Map message = (Map) body.get("message");
ChatMessage chatMessage = new ChatMessage(
(String) message.get("role"),
(String) message.get("content")
);
return new ChatResponse(
UUID.randomUUID().toString(),
(String) body.get("model"),
List.of(new ChatChoice(0, chatMessage, "stop")),
new UsageInfo(null, null, null) // Ollama不返回token用量
);
}
}
四、供应商工厂与动态路由
4.1 供应商工厂
/**
* AI供应商工厂
* 管理所有供应商适配器,支持动态路由
*/
@Component
public class AIAdapterFactory {
private final Map<String, AIAdapter> adapterMap = new ConcurrentHashMap<>();
/** 默认供应商 */
@Value("${ai.default-provider:openai}")
private String defaultProvider;
/**
* 自动注入所有AIAdapter实现
*/
public AIAdapterFactory(List<AIAdapter> adapters) {
adapters.forEach(a -> adapterMap.put(a.getProvider(), a));
}
/**
* 获取默认适配器
*/
public AIAdapter getDefault() {
return adapterMap.get(defaultProvider);
}
/**
* 根据供应商类型获取适配器
*/
public AIAdapter getAdapter(String provider) {
AIAdapter adapter = adapterMap.get(provider);
if (adapter == null) {
throw new BusinessException("不支持的AI供应商: " + provider);
}
return adapter;
}
/**
* 获取所有支持的供应商
*/
public List<String> listProviders() {
return new ArrayList<>(adapterMap.keySet());
}
}
4.2 模型路由策略
某些场景需要根据模型名称自动路由到对应供应商:
/**
* 模型路由器
* 根据模型名称自动选择供应商
*/
@Component
public class ModelRouter {
/** 模型 -> 供应商映射 */
private final Map<String, String> modelProviderMap = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
// OpenAI模型
modelProviderMap.put("gpt-4o", "openai");
modelProviderMap.put("gpt-4o-mini", "openai");
// DeepSeek模型
modelProviderMap.put("deepseek-chat", "deepseek");
modelProviderMap.put("deepseek-coder", "deepseek");
// 智谱模型
modelProviderMap.put("glm-4", "zhipu");
// Claude模型
modelProviderMap.put("claude-3-5-sonnet", "claude");
// Ollama模型
modelProviderMap.put("qwen2.5:7b", "ollama");
modelProviderMap.put("llama3:8b", "ollama");
}
/**
* 根据模型名称获取供应商
*/
public String route(String model) {
String provider = modelProviderMap.get(model);
if (provider == null) {
throw new BusinessException("未知的模型: " + model);
}
return provider;
}
}
五、业务调用示例
5.1 对话服务
/**
* AI对话服务
*/
@Service
public class AIChatService {
private final AIAdapterFactory adapterFactory;
private final ModelRouter modelRouter;
/**
* 对话(指定供应商)
*/
public ChatResponse chat(String provider, ChatRequest request) {
AIAdapter adapter = adapterFactory.getAdapter(provider);
return adapter.chat(request);
}
/**
* 对话(根据模型自动路由)
*/
public ChatResponse chat(ChatRequest request) {
String provider = modelRouter.route(request.model());
AIAdapter adapter = adapterFactory.getAdapter(provider);
return adapter.chat(request);
}
/**
* 流式对话
*/
public Flux<ChatResponse> chatStream(String provider, ChatRequest request) {
AIAdapter adapter = adapterFactory.getAdapter(provider);
return adapter.chatStream(request);
}
}
5.2 Controller 层
@RestController
@RequestMapping("/api/ai")
public class AIController {
private final AIChatService chatService;
/**
* 同步对话
*/
@PostMapping("/chat")
public R<ChatResponse> chat(@RequestBody ChatRequest request,
@RequestParam(required = false) String provider) {
ChatResponse response;
if (provider != null) {
response = chatService.chat(provider, request);
} else {
response = chatService.chat(request);
}
return R.ok(response);
}
/**
* 流式对话(SSE)
*/
@PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ChatResponse> chatStream(@RequestBody ChatRequest request,
@RequestParam String provider) {
return chatService.chatStream(provider, request);
}
}
六、供应商切换流程
flowchart TD
A[业务发起AI请求] --> B{指定供应商?}
B -->|是| C[从工厂获取指定适配器]
B -->|否| D{指定模型?}
D -->|是| E[模型路由器自动匹配供应商]
D -->|否| F[使用默认供应商]
E --> C
F --> C
C --> G[适配器转换请求格式]
G --> H[调用供应商API]
H --> I[适配器转换响应格式]
I --> J[返回统一ChatResponse]
style C fill:#e1f5fe,stroke:#0288d1
style G fill:#fff9c4,stroke:#f9a825
style I fill:#fff9c4,stroke:#f9a825
结论与建议
核心设计原则
- 统一接口抽象:定义与供应商无关的请求/响应对象,业务层只依赖抽象
- 适配器模式:每个供应商一个适配器,负责请求转换和响应转换
- 工厂 + 路由:工厂管理适配器实例,路由器根据模型自动选择供应商
- 配置驱动:供应商配置外部化,切换只需改配置
扩展建议
- Token 用量统计:在适配层统一收集各供应商的 Token 消耗,便于成本分析
- 降级策略:主供应商不可用时自动切换到备用供应商
- 负载均衡:同一供应商多 Key 轮询,避免单 Key 限流
- 流式响应统一:不同供应商的流式协议(SSE/NDJSON)在适配层统一为 Flux