Java 后端 AI 应用网关实战:多模型路由、Fallback、超时和可观测性设计

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; }
}
`

注意:真实生产环境还需要处理 temperaturemax_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"));
    }
}
`

这里有两个关键点:

  1. timeout(Duration.ofMillis(provider.getTimeoutMs())) 必须明确配置;
  2. 响应解析失败也要抛异常,让上层 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 专栏。

相关推荐
龙侠九重天1 小时前
C# 构建 AI Agent 系统 — 我的实践笔记
开发语言·人工智能·语言模型·自然语言处理·大模型·agent·智能体
小锋java12342 小时前
【技术专题】LangChain4j 开发Java Agent智能体 - 嵌入模型与向量数据库
java·人工智能
程序员皮皮林2 小时前
Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?
java·开发语言·dubbo
小锋java12342 小时前
10分钟学会Java16新特性record
java
是多巴胺不是尼古丁2 小时前
java‘期末复习--多态
java·开发语言
瑞雪兆丰年兮2 小时前
[从0开始学Java|第二十五天]项目阶段(综合练习&斗地主小游戏)
java·windows
Demon1_Coder2 小时前
Day4-微服务-Seata默认事务
java·数据库·微服务
Sunia2 小时前
《AgentX 专栏》08-工作流引擎:AgentWorkflow怎么把工具记忆流程串成一条流水线
java·架构
huipeng9262 小时前
企业级微服务开发实战(二):微服务基础设施搭建与中间件部署
java·redis·mysql·spring cloud·微服务·nacos·rabbitmq