【AI 工程化第八篇】Spring AI 多模型路由实战:DeepSeek、通义、Kimi、OpenAI 如何统一接入和自动切换
系列定位:
第一篇:Agent 服务骨架。
第二篇:RAG 知识库。
第三篇:MCP / Tool Calling。
第四篇:线上可观测。
第五篇:生产治理。
第六篇:多租户知识库与数据安全。
第七篇:RAG 评测闭环。
第八篇:多模型统一接入、动态路由、自动降级和成本控制。
技术栈:Java 17 · Spring Boot 4.1.x / 3.5.x · Spring AI 2.0.x · PostgreSQL · Redis · Resilience4j
适用场景:企业 AI 网关、多模型平台、智能客服、RAG 问答、Agent 编排、多租户 SaaS
阅读前置:建议看过第五篇"生产治理"和第七篇"RAG 评测"
为什么企业 AI 应用不能只接一个模型
很多团队第一版 AI 应用会这样写:
yaml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4.1-mini
这没问题,能快速跑通。
但上线后很快会遇到这些问题:
text
主模型限流了,业务接口全挂
客服高频问答用贵模型,成本太高
复杂工单用便宜模型,答案质量不稳定
国内外模型延迟差异大
某个模型工具调用能力强,另一个模型结构化输出更稳
不同租户买的套餐不同,应该路由到不同模型
所以生产级 AI 应用一定要有"模型路由层"。
它要解决的是:
text
同一套业务代码,如何统一接 DeepSeek、通义、Kimi、OpenAI?
不同场景如何自动选择模型?
主模型失败后如何降级?
如何按租户、成本、延迟、质量做路由?
如何记录每次路由原因,方便排查和计费?
一、先看最终效果
业务代码只关心"我要一次聊天调用":
java
ChatResponse response = modelGateway.chat(ModelGatewayRequest.builder()
.tenantId("tenant_a")
.userId("10001")
.scene("customer-service")
.riskLevel("LOW")
.message("超过7天还能退货吗?")
.build());
真正调用哪个模型,由路由层决定:
json
{
"selectedProvider": "deepseek",
"selectedModel": "deepseek-chat",
"routeReason": "低风险客服问答,优先使用性价比模型",
"fallback": false,
"promptTokens": 1280,
"completionTokens": 210,
"costMillis": 1830
}
如果主模型失败,路由层自动降级:
json
{
"selectedProvider": "openai",
"selectedModel": "gpt-4.1-mini",
"routeReason": "deepseek 连续失败触发熔断,切换 fallback 模型",
"fallback": true
}
业务层不需要知道背后细节。
这就是模型网关的价值。
二、多模型路由的核心原则
不要把"选模型"写死在 Controller 或 Service 里。
应该抽象成一个独立的模型治理层:
text
业务场景 -> 路由策略 -> 模型提供商 -> ChatModel -> ChatClient -> 观测审计
路由决策至少考虑 6 个维度:
| 维度 | 示例 |
|---|---|
| 场景 | 客服、RAG、代码生成、工单总结、工具调用 |
| 风险 | 低风险咨询、高风险金融/合同/退款 |
| 租户套餐 | 免费版、专业版、企业版 |
| 成本预算 | 今日 Token 是否超额 |
| 实时状态 | 模型是否熔断、是否限流 |
| 能力差异 | 工具调用、长上下文、结构化输出、多模态 |
推荐第一版规则:
text
低风险高频:便宜模型
高风险决策:强模型
结构化输出:稳定模型
工具调用:工具能力强的模型
主模型失败:fallback 模型
预算不足:降级模型
三、整体架构设计
3.1 功能模块图
text
┌────────────────────────────────────────────────────────────────────┐
│ 业务应用层 │
│ RAG 问答 · 智能客服 · 工单总结 · Agent 工具调用 · 代码助手 │
└─────────────────────────────┬──────────────────────────────────────┘
│
┌─────────────────────────────▼──────────────────────────────────────┐
│ Model Gateway │
│ │
│ ModelRouter │
│ - 按场景 / 风险 / 租户 / 预算选择模型 │
│ │
│ ProviderRegistry │
│ - OpenAI │
│ - DeepSeek │
│ - Tongyi │
│ - Kimi │
│ │
│ Resilience Layer │
│ - Timeout │
│ - Retry │
│ - CircuitBreaker │
│ - Fallback │
│ │
│ Observability │
│ - Token │
│ - Latency │
│ - Error Rate │
│ - Route Reason │
└─────────────┬──────────────┬──────────────┬───────────────────────┘
│ │ │
┌─────────────▼────┐ ┌───────▼───────┐ ┌────▼────────┐
│ OpenAI Compatible │ │ DeepSeek API │ │ Tongyi API │ ...
│ ChatModel │ │ ChatModel │ │ ChatModel │
└──────────────────┘ └───────────────┘ └─────────────┘
3.2 路由时序图
text
业务服务 ModelGateway ModelRouter ProviderClient LLM Provider
│ │ │ │ │
│ chat(request) ──▶│ │ │ │
│ │ 预估 Token │ │ │
│ │ 检查租户预算 │ │ │
│ │─────────────────▶│ │ │
│ │◀─────────────────│ route result │ │
│ │──────────────────────────────────────▶│ │
│ │ │ │ call model ─────▶│
│ │ │ │◀────────────────│
│ │ 记录路由日志 │ │ │
│ │ 记录 Token/耗时 │ │ │
│◀─────────────────│ │ │ │
四、核心表结构
4.1 模型提供商表
sql
CREATE TABLE t_ai_model_provider (
id BIGSERIAL PRIMARY KEY,
provider_code VARCHAR(64) NOT NULL UNIQUE,
provider_name VARCHAR(128) NOT NULL,
api_base_url VARCHAR(255) NOT NULL,
api_key_ref VARCHAR(128) NOT NULL,
protocol_type VARCHAR(32) NOT NULL DEFAULT 'OPENAI_COMPATIBLE',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
priority INT NOT NULL DEFAULT 100,
create_time TIMESTAMP NOT NULL DEFAULT now(),
update_time TIMESTAMP NOT NULL DEFAULT now()
);
4.2 模型能力表
sql
CREATE TABLE t_ai_model_catalog (
id BIGSERIAL PRIMARY KEY,
provider_code VARCHAR(64) NOT NULL,
model_code VARCHAR(128) NOT NULL,
model_name VARCHAR(128) NOT NULL,
context_window INT,
support_tool_calling BOOLEAN DEFAULT FALSE,
support_json_output BOOLEAN DEFAULT FALSE,
support_vision BOOLEAN DEFAULT FALSE,
input_price_per_1k NUMERIC(12, 6),
output_price_per_1k NUMERIC(12, 6),
avg_latency_ms BIGINT,
enabled BOOLEAN DEFAULT TRUE,
create_time TIMESTAMP DEFAULT now(),
update_time TIMESTAMP DEFAULT now(),
UNIQUE(provider_code, model_code)
);
4.3 路由规则表
sql
CREATE TABLE t_ai_model_route_rule (
id BIGSERIAL PRIMARY KEY,
route_key VARCHAR(64) NOT NULL UNIQUE,
scene VARCHAR(64) NOT NULL,
tenant_plan VARCHAR(64) NOT NULL DEFAULT 'DEFAULT',
risk_level VARCHAR(32) NOT NULL DEFAULT 'LOW',
primary_provider VARCHAR(64) NOT NULL,
primary_model VARCHAR(128) NOT NULL,
fallback_provider VARCHAR(64),
fallback_model VARCHAR(128),
cheap_provider VARCHAR(64),
cheap_model VARCHAR(128),
gray_percent INT NOT NULL DEFAULT 0,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
create_time TIMESTAMP DEFAULT now(),
update_time TIMESTAMP DEFAULT now()
);
4.4 路由审计表
sql
CREATE TABLE t_ai_model_route_log (
id BIGSERIAL PRIMARY KEY,
trace_id VARCHAR(64) NOT NULL,
tenant_id VARCHAR(64) NOT NULL,
user_id VARCHAR(64),
scene VARCHAR(64) NOT NULL,
selected_provider VARCHAR(64) NOT NULL,
selected_model VARCHAR(128) NOT NULL,
route_reason VARCHAR(500),
fallback BOOLEAN DEFAULT FALSE,
prompt_tokens INT DEFAULT 0,
completion_tokens INT DEFAULT 0,
success BOOLEAN DEFAULT TRUE,
error_msg VARCHAR(500),
cost_millis BIGINT DEFAULT 0,
create_time TIMESTAMP DEFAULT now()
);
CREATE INDEX idx_model_route_trace ON t_ai_model_route_log(trace_id);
CREATE INDEX idx_model_route_scene_time ON t_ai_model_route_log(scene, create_time DESC);
五、配置多个模型提供商
Spring AI 的 OpenAI Chat Model 支持配置 base-url、api-key、model 等参数。很多厂商提供 OpenAI 兼容协议时,都可以用这套方式统一接入。
注意:不同厂商的 base-url、模型名、鉴权方式会变,请以厂商官方文档为准。本文用环境变量占位,避免把密钥写进配置。
yaml
ai:
providers:
openai:
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
api-key: ${OPENAI_API_KEY}
default-model: gpt-4.1-mini
deepseek:
base-url: ${DEEPSEEK_BASE_URL}
api-key: ${DEEPSEEK_API_KEY}
default-model: deepseek-chat
tongyi:
base-url: ${TONGYI_BASE_URL}
api-key: ${TONGYI_API_KEY}
default-model: qwen-plus
kimi:
base-url: ${KIMI_BASE_URL}
api-key: ${KIMI_API_KEY}
default-model: kimi-k2
业务代码不要直接读取这些配置。
统一通过 ModelProviderProperties 管理:
java
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Map;
@ConfigurationProperties(prefix = "ai")
public record ModelProviderProperties(
Map<String, ProviderConfig> providers
) {
public record ProviderConfig(
String baseUrl,
String apiKey,
String defaultModel
) {
}
}
六、请求和路由对象
java
import lombok.Builder;
@Builder
public record ModelGatewayRequest(
String traceId,
String tenantId,
String userId,
String scene,
String riskLevel,
String systemPrompt,
String message,
int estimatedPromptTokens,
boolean requireToolCalling,
boolean requireJsonOutput
) {
}
@Builder
public record ModelRouteResult(
String providerCode,
String modelCode,
String reason,
boolean fallback,
boolean cheapMode
) {
}
public record ModelGatewayResponse(
String content,
String providerCode,
String modelCode,
String routeReason,
boolean fallback,
int promptTokens,
int completionTokens,
long costMillis
) {
}
七、ModelRouter:模型路由策略
java
import org.springframework.stereotype.Service;
@Service
public class ModelRouter {
private final TenantBudgetService tenantBudgetService;
private final ModelHealthService modelHealthService;
private final ModelRouteRuleRepository routeRuleRepository;
public ModelRouter(TenantBudgetService tenantBudgetService,
ModelHealthService modelHealthService,
ModelRouteRuleRepository routeRuleRepository) {
this.tenantBudgetService = tenantBudgetService;
this.modelHealthService = modelHealthService;
this.routeRuleRepository = routeRuleRepository;
}
/**
* 路由顺序:
* 1. 高风险场景优先强模型
* 2. 工具调用 / JSON 输出选择能力匹配模型
* 3. 租户预算不足时降级便宜模型
* 4. 主模型熔断时切 fallback
*/
public ModelRouteResult route(ModelGatewayRequest request) {
ModelRouteRule rule = routeRuleRepository.findMatched(
request.scene(),
request.riskLevel(),
request.tenantId()
);
if (!tenantBudgetService.hasEnoughBudget(request.tenantId(), request.estimatedPromptTokens())) {
return ModelRouteResult.builder()
.providerCode(rule.cheapProvider())
.modelCode(rule.cheapModel())
.cheapMode(true)
.reason("租户预算不足,降级到低成本模型")
.build();
}
if (!modelHealthService.isHealthy(rule.primaryProvider(), rule.primaryModel())) {
return ModelRouteResult.builder()
.providerCode(rule.fallbackProvider())
.modelCode(rule.fallbackModel())
.fallback(true)
.reason("主模型不可用或已熔断,切换 fallback 模型")
.build();
}
if ("HIGH".equalsIgnoreCase(request.riskLevel())) {
return ModelRouteResult.builder()
.providerCode(rule.primaryProvider())
.modelCode(rule.primaryModel())
.reason("高风险场景使用主力模型")
.build();
}
if (request.requireToolCalling()) {
ModelCapability capability = routeRuleRepository.findBestToolCallingModel(request.scene());
return ModelRouteResult.builder()
.providerCode(capability.providerCode())
.modelCode(capability.modelCode())
.reason("当前请求需要工具调用,选择工具调用能力更稳定的模型")
.build();
}
return ModelRouteResult.builder()
.providerCode(rule.cheapProvider())
.modelCode(rule.cheapModel())
.cheapMode(true)
.reason("低风险高频场景使用性价比模型")
.build();
}
}
这个路由策略很朴素,但足够好用。
等你有了第七篇里的评测体系,就可以把路由从"规则驱动"升级成"数据驱动":
text
不同模型在不同场景的 passRate
不同模型的平均成本
不同模型的 P95 延迟
不同模型的工具调用失败率
八、ProviderRegistry:统一创建 ChatClient
Spring AI 的 ChatClient 是对 ChatModel 的高级封装,支持同步和流式调用,也支持 system/user prompt、options、advisors、tools 等能力。
这里我们用 providerCode + modelCode 动态拿到对应客户端。
java
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ChatClientRegistry {
private final ModelProviderProperties properties;
private final Map<String, ChatClient> cache = new ConcurrentHashMap<>();
public ChatClientRegistry(ModelProviderProperties properties) {
this.properties = properties;
}
public ChatClient get(String providerCode) {
return cache.computeIfAbsent(providerCode, this::createClient);
}
private ChatClient createClient(String providerCode) {
ModelProviderProperties.ProviderConfig config = properties.providers().get(providerCode);
if (config == null) {
throw new BizException("模型提供商不存在:" + providerCode);
}
OpenAiApi openAiApi = OpenAiApi.builder()
.baseUrl(config.baseUrl())
.apiKey(config.apiKey())
.build();
ChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.build();
return ChatClient.builder(chatModel).build();
}
}
如果你的项目不想运行时创建,也可以在配置类里显式注册多个 ChatClient Bean。
但当模型提供商很多、配置要热更新时,Registry 更灵活。
九、ModelGateway:统一调用入口
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
@Service
public class ModelGateway {
private static final Logger log = LoggerFactory.getLogger(ModelGateway.class);
private final ModelRouter modelRouter;
private final ChatClientRegistry chatClientRegistry;
private final ModelRouteAuditService auditService;
private final TenantBudgetService tenantBudgetService;
public ModelGateway(ModelRouter modelRouter,
ChatClientRegistry chatClientRegistry,
ModelRouteAuditService auditService,
TenantBudgetService tenantBudgetService) {
this.modelRouter = modelRouter;
this.chatClientRegistry = chatClientRegistry;
this.auditService = auditService;
this.tenantBudgetService = tenantBudgetService;
}
public ModelGatewayResponse chat(ModelGatewayRequest request) {
Instant start = Instant.now();
ModelRouteResult route = modelRouter.route(request);
Usage usage = null;
boolean success = false;
String errorMsg = null;
try {
ChatClient chatClient = chatClientRegistry.get(route.providerCode());
ChatResponse response = chatClient.prompt()
.system(request.systemPrompt())
.user(request.message())
.options(OpenAiChatOptions.builder()
.model(route.modelCode())
.temperature(0.2)
.maxTokens(1200)
.build())
.call()
.chatResponse();
usage = response.getMetadata().getUsage();
success = true;
int totalTokens = usage == null ? 0 : usage.getTotalTokens();
tenantBudgetService.consume(request.tenantId(), totalTokens);
return new ModelGatewayResponse(
response.getResult().getOutput().getText(),
route.providerCode(),
route.modelCode(),
route.reason(),
route.fallback(),
usage == null ? 0 : usage.getPromptTokens(),
usage == null ? 0 : usage.getCompletionTokens(),
Duration.between(start, Instant.now()).toMillis()
);
} catch (Exception ex) {
errorMsg = ex.getMessage();
log.error("模型网关调用失败 traceId={}, provider={}, model={}",
request.traceId(), route.providerCode(), route.modelCode(), ex);
throw new BizException("AI 模型调用失败,请稍后重试");
} finally {
auditService.save(ModelRouteAuditCommand.builder()
.traceId(request.traceId())
.tenantId(request.tenantId())
.userId(request.userId())
.scene(request.scene())
.providerCode(route.providerCode())
.modelCode(route.modelCode())
.routeReason(route.reason())
.fallback(route.fallback())
.promptTokens(usage == null ? 0 : usage.getPromptTokens())
.completionTokens(usage == null ? 0 : usage.getCompletionTokens())
.success(success)
.errorMsg(errorMsg)
.costMillis(Duration.between(start, Instant.now()).toMillis())
.build());
}
}
}
到这里,业务层已经不需要知道具体厂商。
text
RAG 服务调用 ModelGateway
MCP Agent 调用 ModelGateway
客服助手调用 ModelGateway
评测 Runner 调用 ModelGateway
十、自动 fallback:不要只靠 catch
上面的代码失败后直接抛异常,真实生产还要做 fallback。
推荐流程:
text
主模型失败
-> 标记一次失败
-> 如果是可重试错误,有限重试
-> 如果仍失败,切 fallback 模型
-> fallback 也失败,返回降级话术
示例:
java
public ModelGatewayResponse chatWithFallback(ModelGatewayRequest request) {
ModelRouteResult primary = modelRouter.route(request);
try {
return doChat(request, primary);
} catch (Exception primaryEx) {
modelHealthService.recordFailure(primary.providerCode(), primary.modelCode(), primaryEx);
ModelRouteResult fallback = modelRouter.fallback(request, primary);
try {
return doChat(request, fallback);
} catch (Exception fallbackEx) {
modelHealthService.recordFailure(fallback.providerCode(), fallback.modelCode(), fallbackEx);
return new ModelGatewayResponse(
"当前 AI 服务繁忙,请稍后再试。你也可以换一种问法,或联系人工客服。",
fallback.providerCode(),
fallback.modelCode(),
"主模型和备用模型均失败,返回固定降级话术",
true,
0,
0,
0
);
}
}
}
不要无限 fallback。
否则会出现:
text
主模型失败 -> 备用模型失败 -> 第三个模型失败 -> 第四个模型失败
用户等了很久,成本也烧了很多。
建议最多 1 次 fallback。
十一、前端如何透出模型信息
普通用户不需要看到模型细节。
但管理后台要能看到:
ts
export interface ModelRouteInfo {
providerCode: string
modelCode: string
routeReason: string
fallback: boolean
promptTokens: number
completionTokens: number
costMillis: number
}
后台排查时可以展示:
text
模型:deepseek / deepseek-chat
路由原因:低风险客服问答,优先使用性价比模型
Fallback:否
Token:1490
耗时:1830ms
这样当用户反馈"今天回答变差了",你能快速判断是不是路由到了便宜模型。
十二、生产环境最容易踩的坑
12.1 所有场景都用一个模型
这是最贵也最不稳定的做法。
建议按场景拆:
text
客服 FAQ:低成本模型
合同审阅:强模型
工具调用:Tool Calling 稳定模型
结构化抽取:JSON 输出稳定模型
摘要生成:低温度、低成本模型
12.2 fallback 模型能力不兼容
主模型支持工具调用,fallback 模型不支持。
结果:
text
主模型失败后 fallback 成功了
但工具调用能力没了
Agent 行为变了
路由时必须检查能力:
text
support_tool_calling
support_json_output
context_window
12.3 不记录路由原因
没有路由审计,就无法解释:
text
为什么这个用户走了贵模型?
为什么这个租户被降级?
为什么今天错误率升高?
12.4 模型名写死在 Prompt 或业务代码里
模型名应该属于配置和路由层。
Prompt 里不要写:
text
你正在使用 gpt-4.1-mini
否则切模型时会出现不一致。
12.5 只看成本,不看评测指标
降成本很容易:
text
全部路由到便宜模型
但答案质量可能直接崩。
正确做法:
text
成本下降
RAG Eval passRate 不下降
用户点踩率不升高
P95 延迟可接受
十三、总结
这篇文章把模型接入从"单模型配置"升级到了"多模型网关":
- 生产级 AI 应用不应该只依赖一个模型。
- 多模型路由要按场景、风险、租户、预算、能力和实时健康状态决策。
- 业务层只调用
ModelGateway,不直接依赖具体厂商。 - 路由原因、Token、耗时、成功失败必须落审计表。
- fallback 不能无限重试,最多 1 次备用模型更稳。
- 多模型路由必须结合第七篇的 RAG Eval,用数据证明质量没有下降。
下一篇我准备写:
Spring AI Prompt 模板与版本治理实战:System Prompt、Few-shot、变量注入、灰度发布和安全防注入
如果你想要完整工程骨架,可以在评论区留一句:
text
多模型
我会继续把这个系列写完整。
可选标题
- Spring AI 多模型路由实战:DeepSeek、通义、Kimi、OpenAI 统一接入
- Java AI Agent 第八篇:从单模型调用升级到多模型网关
- 企业 AI 应用如何自动切模型:Spring AI 模型路由、降级和成本控制
- Spring Boot 多模型网关:让 RAG 和 Agent 自动选择最合适的大模型
推荐标签
Spring AI 多模型路由 AI Gateway DeepSeek 通义千问 Kimi OpenAI Java
参考资料
- Spring AI OpenAI Chat:https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html
- Spring AI ChatClient:https://docs.spring.io/spring-ai/reference/api/chatclient.html
- Spring AI Prompts:https://docs.spring.io/spring-ai/reference/api/prompt.html
- Spring AI Observability:https://docs.spring.io/spring-ai/reference/observability/
- Spring Cloud CircuitBreaker Resilience4j:https://docs.spring.io/spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html