Java 后端 AI 应用网关实战:多模型路由、Fallback、超时和可观测性设计
摘要:AI 应用真正进入业务系统后,后端服务不能只直连一个模型接口。本文用 Spring Boot 示例拆解一个 AI 应用网关的最小工程方案:多模型 Provider 配置、路由选择、Fallback 顺序、请求超时、熔断降级和可观测性日志。适合 Java 后端、架构师和正在把大模型能力接入业务系统的开发者参考。
验证状态:本文代码为 Spring Boot 3 + Java 17 的工程示例,HTTP 调用使用 Spring WebClient 演示;Provider 字段兼容常见 OpenAI-compatible /v1/chat/completions 中转接口。示例没有绑定某个具体商业中转站,真实生产环境需要按目标 Provider 的鉴权、速率限制和响应格式补充适配。
1. 为什么 AI 应用需要一个"后端网关层"
最近 AI 投资和 AI 应用公司的消息很多,说明 AI 能力正在从单点工具进入业务系统。对 Java 后端来说,更关键的问题不是"哪个模型最强",而是:
当业务开始依赖大模型接口时,后端服务应该如何稳定调用?
如果第一版只是这样写:
业务服务 -> 直接请求某个模型 API -> 返回结果
上线后很快会遇到几个问题:
| 问题 | 线上表现 | 后果 |
|---|---|---|
| 单 Provider 依赖 | 一个中转站超时,整个业务不可用 | 可用性差 |
| 没有超时控制 | 请求卡住几十秒甚至更久 | 线程/连接资源被占满 |
| 没有 Fallback | 主模型失败后无法切换备用模型 | 用户直接看到失败 |
| 没有追踪日志 | 不知道用了哪个模型、耗时多少、为什么失败 | 排查困难 |
| 没有降级策略 | 模型不可用时业务无兜底 | 影响核心链路 |
所以比较稳妥的做法是:在业务服务和模型 Provider 中间加一层轻量 AI 应用网关。
flowchart LR
A[业务服务] --> B[AI 应用网关]
B --> C[Provider A / 主模型]
B --> D[Provider B / 备用模型]
B --> E[Provider C / 低成本模型]
B --> F[降级结果 / 人工兜底]
这层网关不一定一开始就做成独立微服务,也可以先做成业务系统内的一个模块。但它至少应该负责:
- Provider 配置管理;
- 模型路由;
- 超时控制;
- Fallback;
- 重试边界;
- 请求日志和指标;
- 降级结果。
2. 本文的最小实现目标
本文不做复杂模型平台,只实现一个可落地的最小闭环。
目标能力:
示例技术栈:
| 组件 | 示例版本 | 说明 |
|---|---|---|
| JDK | 17 | Spring Boot 3 常用基线 |
| Spring Boot | 3.2.x / 3.3.x | 示例代码不依赖特殊版本 |
| WebClient | Spring WebFlux | 用于 HTTP 调用模型接口 |
| Resilience4j | 2.x | 可选,用于熔断/限流扩展 |
Maven 依赖示例:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
验证状态:依赖版本为工程示例,具体版本请以项目 Spring Boot BOM 和团队依赖基线为准。
3. Provider 配置设计
先定义配置文件。真实项目中 apiKey 应放到环境变量、配置中心或密钥管理系统,不建议明文写在 application.yml。
ai:
gateway:
providers:
- name: primary
base-url: https://api-primary.example.com/v1
api-key: ${AI_PRIMARY_API_KEY}
model: gpt-4o-mini
timeout-ms: 120000
priority: 1
- name: backup
base-url: https://api-backup.example.com/v1
api-key: ${AI_BACKUP_API_KEY}
model: glm-4-plus
timeout-ms: 120000
priority: 2
- name: cheap
base-url: https://api-cheap.example.com/v1
api-key: ${AI_CHEAP_API_KEY}
model: qwen-plus
timeout-ms: 60000
priority: 3
对应 Java 配置类:
java
`import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@Validated
@ConfigurationProperties(prefix = "ai.gateway")
public class AiGatewayProperties {
private List<ProviderConfig> providers = new ArrayList<>();
public List<ProviderConfig> getProviders() {
return providers.stream()
.sorted(Comparator.comparingInt(ProviderConfig::getPriority))
.toList();
}
public void setProviders(List<ProviderConfig> providers) {
this.providers = providers;
}
public static class ProviderConfig {
@NotBlank
private String name;
@NotBlank
private String baseUrl;
@NotBlank
private String apiKey;
@NotBlank
private String model;
@Positive
private long timeoutMs = 120_000;
private int priority = 100;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getBaseUrl() { return baseUrl; }
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getModel() { return model; }
public void setModel(String model) { this.model = model; }
public long getTimeoutMs() { return timeoutMs; }
public void setTimeoutMs(long timeoutMs) { this.timeoutMs = timeoutMs; }
public int getPriority() { return priority; }
public void setPriority(int priority) { this.priority = priority; }
}
}
`
启动类启用配置:
java
`import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(AiGatewayProperties.class)
public class AiGatewayConfig {
}
`
4. 请求和响应模型
为了示例简单,先定义一个业务侧请求对象:
java
`public record AiChatRequest(
String traceId,
String userMessage,
String scenario
) {}
`
返回对象需要带上实际使用的 Provider,方便业务和日志排查:
java
`public record AiChatResponse(
String traceId,
String provider,
String model,
String content,
boolean degraded
) {}
`
OpenAI-compatible 请求体可以先做最小字段:
java
`import java.util.List;
import java.util.Map;
public class OpenAiChatPayload {
private String model;
private List<Map<String, String>> messages;
public OpenAiChatPayload(String model, String userMessage) {
this.model = model;
this.messages = List.of(Map.of(
"role", "user",
"content", userMessage
));
}
public String getModel() { return model; }
public List<Map<String, String>> getMessages() { return messages; }
}
`
注意:真实生产环境还需要处理 temperature、max_tokens、streaming、tool calling、JSON schema、content filter、usage 统计等字段。本文只演示网关核心链路。
5. 单 Provider 调用封装
先写一个最小客户端,用 WebClient 调用 /chat/completions。
java
`import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;
@Component
public class OpenAiCompatibleClient {
public Mono<String> chat(AiGatewayProperties.ProviderConfig provider,
String userMessage) {
WebClient client = WebClient.builder()
.baseUrl(provider.getBaseUrl())
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + provider.getApiKey())
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
OpenAiChatPayload payload = new OpenAiChatPayload(provider.getModel(), userMessage);
return client.post()
.uri("/chat/completions")
.bodyValue(payload)
.retrieve()
.bodyToMono(Map.class)
.timeout(Duration.ofMillis(provider.getTimeoutMs()))
.map(this::extractContent);
}
@SuppressWarnings("unchecked")
private String extractContent(Map<String, Object> body) {
var choices = (java.util.List<Map<String, Object>>) body.get("choices");
if (choices == null || choices.isEmpty()) {
throw new IllegalStateException("LLM response choices is empty");
}
var message = (Map<String, Object>) choices.get(0).get("message");
if (message == null || message.get("content") == null) {
throw new IllegalStateException("LLM response message.content is empty");
}
return String.valueOf(message.get("content"));
}
}
`
这里有两个关键点:
timeout(Duration.ofMillis(provider.getTimeoutMs()))必须明确配置;- 响应解析失败也要抛异常,让上层 Fallback 处理。
如果不设置超时,模型接口一旦卡住,会把业务线程、连接池和用户体验都拖死。
6. Fallback 调用链实现
现在实现核心网关服务:按 Provider 优先级依次尝试。
java
`import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Service
public class AiGatewayService {
private static final Logger log = LoggerFactory.getLogger(AiGatewayService.class);
private final AiGatewayProperties properties;
private final OpenAiCompatibleClient client;
public AiGatewayService(AiGatewayProperties properties,
OpenAiCompatibleClient client) {
this.properties = properties;
this.client = client;
}
public AiChatResponse chat(AiChatRequest request) {
List<String> errors = new ArrayList<>();
for (AiGatewayProperties.ProviderConfig provider : properties.getProviders()) {
Instant start = Instant.now();
try {
log.info("ai_gateway_try traceId={} provider={} model={}",
request.traceId(), provider.getName(), provider.getModel());
String content = client.chat(provider, request.userMessage()).block();
long costMs = Duration.between(start, Instant.now()).toMillis();
log.info("ai_gateway_success traceId={} provider={} model={} costMs={}",
request.traceId(), provider.getName(), provider.getModel(), costMs);
return new AiChatResponse(
request.traceId(),
provider.getName(),
provider.getModel(),
content,
false
);
} catch (Exception e) {
long costMs = Duration.between(start, Instant.now()).toMillis();
String reason = e.getClass().getSimpleName() + ": " + e.getMessage();
errors.add(provider.getName() + " -> " + reason);
log.warn("ai_gateway_failed traceId={} provider={} model={} costMs={} reason={}",
request.traceId(), provider.getName(), provider.getModel(), costMs, reason);
}
}
log.error("ai_gateway_all_failed traceId={} errors={}", request.traceId(), errors);
return new AiChatResponse(
request.traceId(),
"degraded",
"none",
"当前 AI 服务暂时不可用,请稍后重试。",
true
);
}
}
`
这个版本很简单,但已经具备生产网关的基本形态:
请求 -> 主 Provider -> 失败 -> 备用 Provider -> 失败 -> 低成本 Provider -> 失败 -> 降级
需要注意:Fallback 不是无限重试。每个 Provider 只尝试一次,失败后切下一个。否则多个 Provider + 多次重试叠加,很容易把请求耗时放大到不可控。
7. Controller 示例
提供一个测试接口:
java
`import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@RequestMapping("/api/ai")
public class AiGatewayController {
private final AiGatewayService aiGatewayService;
public AiGatewayController(AiGatewayService aiGatewayService) {
this.aiGatewayService = aiGatewayService;
}
@PostMapping("/chat")
public AiChatResponse chat(@RequestBody ChatRequestBody body) {
String traceId = body.traceId() == null || body.traceId().isBlank()
? UUID.randomUUID().toString()
: body.traceId();
return aiGatewayService.chat(new AiChatRequest(
traceId,
body.message(),
body.scenario()
));
}
public record ChatRequestBody(
String traceId,
String message,
String scenario
) {}
}
`
本地请求示例:
curl -X POST 'http://localhost:8080/api/ai/chat' \
-H 'Content-Type: application/json' \
-d '{
"traceId": "demo-001",
"message": "请用 3 句话解释什么是 AI 应用网关",
"scenario": "demo"
}'
预期成功响应示例:
{
"traceId": "demo-001",
"provider": "primary",
"model": "gpt-4o-mini",
"content": "AI 应用网关是业务系统和大模型服务之间的一层统一调用入口...",
"degraded": false
}
主 Provider 超时后,日志可能类似:
ai_gateway_try traceId=demo-001 provider=primary model=gpt-4o-mini
ai_gateway_failed traceId=demo-001 provider=primary model=gpt-4o-mini costMs=120015 reason=TimeoutException: Did not observe any item or terminal signal
ai_gateway_try traceId=demo-001 provider=backup model=glm-4-plus
ai_gateway_success traceId=demo-001 provider=backup model=glm-4-plus costMs=3842
验证状态:日志为模拟格式,用于说明排查字段;真实日志格式请按项目 Logback / OpenTelemetry 规范调整。
8. 超时应该怎么配
AI 接口超时不能只配一个全局值。建议至少拆成三类:
| 超时类型 | 建议含义 | 示例 |
|---|---|---|
| connect timeout | 建立连接时间 | 3s - 10s |
| read timeout / request timeout | 单次模型调用最大等待 | 60s - 180s |
| business timeout | 用户请求整体最大等待 | 30s - 120s |
对普通 HTTP API 来说,120 秒已经很长;但对长上下文、工具调用、多轮推理模型来说,确实可能需要更长等待。生产上不建议简单把所有请求都放到 5 分钟,而是按场景分级:
| 场景 | 推荐策略 |
|---|---|
| 用户实时问答 | 短超时,失败快速降级 |
| 后台内容生成 | 可长超时,异步任务更合适 |
| 代码生成/长文生成 | 建议任务化,避免占住同步请求 |
| 工具调用 Agent | 单步设置 watchdog,整体设置任务上限 |
如果后端直接把长文生成放在同步 HTTP 请求里,很容易出现:
用户请求未完成
Nginx / 网关先超时
后端线程仍在等待模型
模型返回后用户已经断开
日志里看不到清晰失败原因
所以生产建议是:实时接口短超时,长任务走任务队列或异步状态轮询。
9. Fallback 的几个生产边界
9.1 不是所有失败都应该 Fallback
可以 Fallback 的失败:
- 连接失败;
- Provider 5xx;
- Provider 明确限流;
- 请求超时;
- 响应格式异常。
不建议直接 Fallback 的失败:
- prompt 本身非法;
- 用户输入违规;
- 业务权限不足;
- 请求参数错误;
- 明确的 4xx 配置错误。
否则可能把一个业务参数错误,误判成模型不可用,在多个 Provider 之间反复打。
9.2 Fallback 后要记录实际模型
很多系统只记录"AI 调用成功",但不记录用了哪个 Provider。后续排查时会很痛苦。
至少应该记录:
traceId
scenario
provider
model
costMs
inputTokens / outputTokens
finishReason
errorCode
fallbackIndex
如果用 OpenTelemetry,可以把这些字段写入 span attributes:
ai.provider = primary
ai.model = gpt-4o-mini
ai.fallback.index = 0
ai.request.timeout_ms = 120000
ai.response.finish_reason = stop
9.3 成本和质量也要进路由策略
第一版可以只按优先级路由。后续可以按场景区分:
| 场景 | 路由建议 |
|---|---|
| 简单分类/摘要 | 低成本模型优先 |
| 代码生成 | 高质量模型优先 |
| 长上下文分析 | 支持长上下文的 Provider 优先 |
| 高并发接口 | 成本和限流能力优先 |
| 后台批处理 | 可牺牲实时性,优先稳定和成本 |
也就是说,AI 网关不只是"失败切备用",还应该逐步演进成"按业务场景选择模型"。
10. 熔断和限流怎么加
如果某个 Provider 已经连续失败,就不要每个请求都先打它一遍。可以加 Resilience4j CircuitBreaker。
配置示例:
resilience4j:
circuitbreaker:
instances:
aiPrimary:
slidingWindowSize: 20
failureRateThreshold: 50
waitDurationInOpenState: 60s
permittedNumberOfCallsInHalfOpenState: 3
思路是:
Provider 连续失败
-> 熔断器打开
-> 一段时间内跳过该 Provider
-> 半开状态少量探测
-> 恢复后重新进入路由
也可以给不同 Provider 配独立限流,避免某个模型被瞬间打爆:
业务入口限流
Provider 维度限流
用户维度限流
场景维度限流
生产上更推荐把限流、熔断、降级和观测放在同一层,而不是散落在每个业务服务里。
11. 一个更完整的调用链
最终可以演进成下面这样的结构:
flowchart TD
A[业务请求] --> B[生成 traceId]
B --> C[识别业务场景]
C --> D[选择候选 Provider 列表]
D --> E{Provider 是否熔断}
E -->|是| F[跳过该 Provider]
E -->|否| G[发起模型请求]
G --> H{是否成功}
H -->|成功| I[记录 tokens / cost / model]
H -->|失败| J[记录失败原因]
J --> K{是否还有备用 Provider}
K -->|有| E
K -->|无| L[返回降级结果]
I --> M[返回业务结果]
这个链路里最重要的是:每一步都能被日志或指标还原,而不是只在最后看到一个"AI 调用失败"。
12. 常见踩坑
12.1 把 Fallback 当成质量提升手段
Fallback 的第一目标是可用性,不是让多个模型互相投票。如果要做多模型融合、评审或投票,那是另一套链路,成本和延迟都会明显增加。
12.2 所有业务共用同一个超时
实时客服、后台长文生成、代码分析和批量摘要的超时策略不应该一样。统一超时会导致两种问题:实时接口太慢,后台任务又太容易失败。
12.3 没有区分草稿任务和实时请求
AI 生成文章、报告、代码审查这类任务,建议走异步任务模型:
提交任务 -> 返回 taskId -> 后台执行 -> 查询状态 -> 获取结果
不要强行放进一个同步 HTTP 请求里。
12.4 没有记录失败 Provider
如果只记录最终成功结果,会掩盖前面几个 Provider 的失败。短期看业务没出问题,长期会让主 Provider 的稳定性问题一直没人发现。
12.5 在日志里打印完整 prompt 和 API Key
日志里可以记录 prompt 摘要、长度、token 数和 traceId,但不要打印完整用户输入、敏感业务数据和 API Key。尤其是多租户或内部系统,要把脱敏当成默认能力。
13. 生产落地建议
如果是第一版接入,我建议按这个顺序做:
| 阶段 | 目标 | 不建议一开始就做 |
|---|---|---|
| V1 | Provider 配置 + 超时 + Fallback + 日志 | 多模型投票 |
| V2 | 场景路由 + 成本统计 + token 统计 | 复杂模型平台 |
| V3 | 熔断限流 + OpenTelemetry + 告警 | 手写大量重复逻辑 |
| V4 | 异步任务队列 + 结果缓存 + 人工审核 | 所有请求同步等待 |
第一版最关键的是三件事:
这三件事做好,AI 应用接入业务系统时,至少不会因为某个模型或中转站短暂异常,把整个业务链路拖死。
14. 总结
AI 能力进入业务系统后,Java 后端需要关注的不只是模型效果,还包括稳定性、超时、Fallback、成本和可观测性。
一个最小可用的 AI 应用网关,可以先从下面几项开始:
- 多 Provider 配置;
- 按优先级路由;
- 单 Provider 明确超时;
- 失败后有边界地 Fallback;
- 全链路记录 traceId、provider、model、耗时和错误原因;
- 全部失败时返回可控降级结果。
后续再逐步补充熔断、限流、OpenTelemetry、场景路由、token 成本统计和异步任务队列。
如果你正在把大模型能力接入 Java 后端系统,不建议第一步就追求复杂平台。先把"稳定调用、失败可切、问题可查"这条链路跑通,往往比追最新模型更重要。
如果你关注 Java 后端、Spring Boot、AI 工程落地和线上问题排查,可以关注我的 CSDN 专栏。