Spring AI 2.0 多模型路由网关:Astra/Sol 到国产模型的智能调度与降级

当 GPT-6 Astra 以 2.5 倍价格横扫 Agentic 任务,DeepSeek 与通义千问以成本优势守住日常问答,Java 后端团队如果还在业务代码里直接 new OpenAiApi(),就相当于把模型选型的开关交到了每个业务开发者手里。本文围绕一个真实的企业级 AI 网关项目,深入 Spring AI 2.0 的模型抽象与路由源码,给出可落地的多模型调度、降级、限流与成本治理方案。

一、为什么企业级 AI 应用必须有一层模型路由网关

2026 年 9 月 3 日,OpenAI 正式发布 GPT-6 Astra。它在 OSWorld 2.0 的 Computer Use 任务准确率从 GPT-5.6 Sol 的 65.7% 提升到 72.6%,单次任务耗时从约 75 分钟压缩到 40 分钟;但代价是 API 价格涨了 2.5 倍------输入 10/1M tokens、输出 50/1M tokens,且 Fast 模式再翻倍。几乎同时,DeepSeek、通义千问、文心一言等国产模型仍在快速迭代,价格只有 Astra 的几分之一。

这对 Java 后端团队意味着一个直接问题:不是"用哪个模型",而是"同一条业务链路如何根据任务特征动态选择模型,并在模型异常时自动降级"

常见反模式包括:

  • 业务代码直接引入某一家 SDK,OrderServiceKnowledgeService 各调各的;

  • 简单 FAQ 也走强模型,月度账单失控;

  • 主模型 503 时整个功能入口崩溃,没有备用链路;

  • 切换模型需要改业务代码,回归测试周期长。

解决这些问题的关键,是在 LLM 调用前增加一层模型路由网关(Model Routing Gateway):对业务侧暴露统一协议,由网关负责模型选择、故障转移、Token 限流、成本审计和响应标准化。

二、Spring AI 2.0 的模型抽象:路由的地基

Spring AI 2.0(2026-06-12 GA)把模型调用抽象成三层:ChatModel / StreamingChatModel 是底层引擎,ChatClient 是面向业务的流式 DSL,而 Advisor 链则负责在请求前后插入治理逻辑。

2.1 ChatModel 统一接口

org.springframework.ai.chat.model.ChatModel 的核心签名非常克制:

java 复制代码
public interface ChatModel extends Model<Prompt, ChatResponse> {
    default String call(String message) { ... }
    @Override
    ChatResponse call(Prompt prompt);
}

OpenAiChatModelDashScopeChatModelAnthropicChatModelOllamaChatModelDeepSeekChatModel 都实现了这一接口。业务代码只要面向 ChatModel 编程,切换模型就不需要改调用处,只需换注入的 Bean。

2.2 ChatClient:面向业务的高阶 API

ChatClientChatClient.Builder 构建,内部持有 ChatModelObservationRegistry、默认 Advisor 列表等。Spring AI 2.0 强烈推荐用 ChatClient 作为业务入口,因为:

  • 它内置 ToolCallingAdvisorMessageChatMemoryAdvisor 等治理组件;

  • .stream() 一行开启 SSE 流式;

  • .options() 按请求覆盖模型参数,且 2.0 中 ChatOptions 改为不可变 Builder 模式。

java 复制代码
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultSystem("你是企业知识库助手,只基于检索结果回答")
    .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
    .build();

2.3 Options 不可变与 mutate()

Spring AI 2.0 对 Options 做了破坏性重构:移除了 copy()fromOptions(),统一用 mutate() 派生新实例。以 OpenAiChatOptions 为例:

java 复制代码
// 1.x 写法(已废弃)
OpenAiChatOptions opts = originalOptions.copy();
opts.setModel("gpt-6-astra");

// 2.0 写法
OpenAiChatOptions opts = originalOptions.mutate()
    .model("gpt-6-astra")
    .temperature(0.2)
    .maxTokens(4096)
    .build();

这个设计在多模型路由场景下尤其重要:网关可以为每个模型维护一份默认 ChatOptions,然后在请求到达时 mutate() 出新的运行时选项,既保证并发安全,又避免深拷贝带来的配置漂移。

2.4 ModelRegistry / ClientRegistry 运行时路由

Spring AI 2.0-rc2 引入了 ModelRegistryClientRegistry,允许在运行时按名称注册和获取模型实例:

java 复制代码
@Bean
public ModelRegistry modelRegistry(
        OpenAiChatModel openAiChatModel,
        DashScopeChatModel dashScopeChatModel,
        OllamaChatModel ollamaChatModel) {
    ModelRegistry registry = new ModelRegistry();
    registry.register("openai-gpt-4o", openAiChatModel);
    registry.register("qwen-plus", dashScopeChatModel);
    registry.register("qwen2.5-7b-local", ollamaChatModel);
    return registry;
}

业务侧通过 registry.get("qwen-plus", ChatModel.class) 动态选择模型。这个机制是我们构建路由网关的核心依赖。

三、企业级多模型路由网关实战:SmartModelGateway

下面是一个生产可用的 Java 项目骨架,命名为 SmartModelGateway,基于 Spring Boot 4 + Spring AI 2.0 + Resilience4j + Micrometer。

3.1 项目依赖

xml 复制代码
<properties>
    <java.version>21</java.version>
    <spring-ai.version>2.0.0</spring-ai.version>
    <resilience4j.version>2.2.0</resilience4j.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-dashscope</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-ollama</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-spring-boot3</artifactId>
        <version>${resilience4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
</dependencies>

3.2 模型角色与路由策略配置

把模型抽象成角色,而不是把模型名硬编码到代码里:

yaml 复制代码
smart:
  gateway:
    models:
      frontier:
        provider: openai
        model: gpt-6-astra
        cost-input-per-1k: 0.010      # $10 / 1M
        cost-output-per-1k: 0.050     # $50 / 1M
        timeout-ms: 120000
        max-retry: 1
      primary:
        provider: dashscope
        model: qwen-plus
        cost-input-per-1k: 0.0005
        cost-output-per-1k: 0.0015
        timeout-ms: 30000
        max-retry: 2
      economic:
        provider: dashscope
        model: qwen-turbo
        cost-input-per-1k: 0.0001
        cost-output-per-1k: 0.0003
        timeout-ms: 20000
        max-retry: 2
      local:
        provider: ollama
        model: qwen2.5:7b
        cost-input-per-1k: 0
        cost-output-per-1k: 0
        timeout-ms: 60000
        max-retry: 1
    routes:
      - name: code-review
        pattern: "代码审查|code review|review"
        target: frontier
      - name: daily-qa
        pattern: ".*"
        target: primary
        fallback-chain: [economic, local]

3.3 路由决策器:把自然语言映射到模型角色

第一版不要上 LLM 分类器,用规则 + 正则即可落地:

java 复制代码
@Component
public class ModelRouter {
    private final GatewayProperties props;
    private final ModelRegistry registry;

    public ModelRoute decide(RoutingRequest req) {
        String text = (req.systemPrompt() + " " + req.userMessage()).toLowerCase();

        for (RouteRule rule : props.getRoutes()) {
            if (Pattern.compile(rule.getPattern(), Pattern.CASE_INSENSITIVE)
                       .matcher(text).find()) {
                return new ModelRoute(rule.getTarget(), rule.getFallbackChain());
            }
        }
        return new ModelRoute("primary", List.of("economic", "local"));
    }

    public ChatModel resolveModel(String role) {
        ModelConfig cfg = props.getModels().get(role);
        return registry.get(cfg.provider() + "-" + cfg.model(), ChatModel.class);
    }
}

进阶做法:用一个小型本地分类模型在网关层先做复杂度判断,但会增加一次调用延迟,建议第二版再引入。

3.4 带熔断与降级的模型执行器

这里是网关的核心。用 Resilience4j 的 CircuitBreaker 包装每个模型角色,失败时切换到 fallback 链:

java 复制代码
@Service
public class ResilientModelExecutor {
    private final ModelRouter router;
    private final GatewayProperties props;
    private final Map<String, CircuitBreaker> breakers;
    private final MeterRegistry meterRegistry;

    public ChatResponse execute(RoutingRequest req) {
        ModelRoute route = router.decide(req);
        List<String> chain = new ArrayList<>();
        chain.add(route.primary());
        chain.addAll(route.fallbacks());

        String lastError = null;
        for (String role : chain) {
            CircuitBreaker cb = breakers.get(role);
            if (cb.getState() == CircuitBreaker.State.OPEN) {
                meterRegistry.counter("model.gateway.skipped.open",
                    "role", role).increment();
                continue;
            }

            try {
                ChatModel model = router.resolveModel(role);
                ModelConfig cfg = props.getModels().get(role);
                Prompt prompt = buildPrompt(req, cfg);

                ChatResponse resp = cb.executeCallable(() ->
                    model.call(prompt)
                );

                recordCost(role, resp.getMetadata().getUsage(),
                    cfg.costInputPer1k(), cfg.costOutputPer1k());
                return resp;
            } catch (CallNotPermittedException e) {
                lastError = "circuit-open:" + role;
            } catch (Exception e) {
                lastError = e.getMessage();
                meterRegistry.counter("model.gateway.failure",
                    "role", role, "reason", e.getClass().getSimpleName()).increment();
            }
        }
        throw new ModelUnavailableException("所有模型均不可用,最后失败原因:" + lastError);
    }

    private Prompt buildPrompt(RoutingRequest req, ModelConfig cfg) {
        ChatOptions options = cfg.options().mutate()
            .model(cfg.model())
            .temperature(cfg.temperature())
            .maxTokens(cfg.maxTokens())
            .build();

        return new Prompt(
            List.of(
                new SystemMessage(req.systemPrompt()),
                new UserMessage(req.userMessage())
            ),
            options
        );
    }

    private void recordCost(String role, Usage usage,
                            double inputRate, double outputRate) {
        if (usage == null) return;
        double cost = usage.getPromptTokens() * inputRate / 1000.0
                    + usage.getGenerationTokens() * outputRate / 1000.0;
        meterRegistry.counter("model.gateway.cost.usd",
            "role", role).increment(cost);
    }
}

3.5 Token 级别限流:保护成本和稳定性

大模型调用不能按请求数限流,必须按 Token 数限流。网关维护一个基于 Redis 的滑动窗口:

java 复制代码
@Component
public class TokenRateLimiter {
    private final StringRedisTemplate redis;
    private final GatewayProperties props;

    public boolean allow(String tenantId, int estimatedTokens) {
        String key = "gw:token:%s:%s".formatted(
            LocalDate.now(), tenantId);
        Long current = redis.opsForValue().increment(key, estimatedTokens);
        if (current == 1) {
            redis.expire(key, Duration.ofDays(1));
        }
        return current <= props.getTenantDailyLimit();
    }
}

估算输入 Token 可用 JTokkit 或近似按 1 token ≈ 0.75 个汉字;输出 Token 可在请求前按 maxTokens 预扣,响应返回后再通过 Redis decrement 修正差额。

3.6 统一 Controller 入口

java 复制代码
@RestController
@RequestMapping("/api/v1/ai")
public class AiGatewayController {
    private final ResilientModelExecutor executor;
    private final TokenRateLimiter limiter;

    @PostMapping("/chat")
    public ResponseEntity<?> chat(@RequestBody @Valid ChatRequest req,
                                  @RequestHeader("X-Tenant-Id") String tenantId) {
        int estimated = TokenEstimator.estimate(req.userMessage())
                       + req.systemPrompt().length() / 2
                       + req.maxTokens();
        if (!limiter.allow(tenantId, estimated)) {
            return ResponseEntity.status(429)
                .body(Map.of("error", "TOKEN_LIMIT_EXCEEDED"));
        }

        RoutingRequest rr = new RoutingRequest(
            req.systemPrompt(), req.userMessage(), req.maxTokens());
        ChatResponse resp = executor.execute(rr);
        return ResponseEntity.ok(Map.of(
            "content", resp.getResult().getOutput().getContent(),
            "model", resp.getMetadata().getModel(),
            "usage", resp.getMetadata().getUsage()
        ));
    }
}

四、源码级深度:Spring AI 2.0 如何把模型切换做到"一行不改"

多模型路由能工作的前提是运行时选项与默认选项的合并策略 。Spring AI 2.0 在 DefaultChatClientRequestSpec 中处理如下:

  1. ChatClient.Builder 构建时保存 defaultOptions
  2. 每次请求通过 .options(ChatOptions.Builder) 传入运行时 Builder;
  3. 运行时用 ChatOptions.Builder.build() 生成最终选项;
  4. ChatModel 实现内部把最终选项与模型默认选项合并。

关键源码片段(来自 org.springframework.ai.chat.client.DefaultChatClient):

java 复制代码
public ChatClientRequestSpec options(ChatOptions.Builder options) {
    this.chatOptions = options.build();
    return this;
}

而在 OpenAiChatModel 内部:

java 复制代码
protected OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt,
                                                          OpenAiChatOptions options) {
    OpenAiChatOptions mergedOptions = this.defaultOptions.mutate()
        .model(options.getModel())
        .temperature(options.getTemperature())
        // ... 其他字段
        .build();
    // 构造 HTTP 请求体
}

这意味着:网关只需维护一份 ModelConfig,在请求时把 modeltemperaturemaxTokens 等字段注入 Builder,Spring AI 会自动处理厂商差异。业务代码里不会出现 if (openai) ... else if (dashscope) ... 的分支地狱。

五、生产踩坑清单:从 Demo 到高可用

5.1 把 AI 网关做成 SDK 工具类

反模式:

java 复制代码
public class AiUtil {
    public static String ask(String question) { ... }
}

问题:限流、熔断、审计散落在业务代码,无法统一治理。正确做法是把网关作为独立服务或 Sidecar,业务通过 HTTP/gRPC 调用。

5.2 只看平均耗时,不看 P99 与成本

大模型响应时长波动极大。Astra 的复杂 Agentic 任务可能 60 秒以上,Turbo 的 FAQ 只要 2 秒。如果只配全局超时,要么 Turbo 被拖慢,要么 Astra 被截断。建议为每个模型角色单独配置超时和重试:

yaml 复制代码
frontier:
  timeout-ms: 120000
  max-retry: 1          # 贵模型,失败直接降级更划算
primary:
  timeout-ms: 30000
  max-retry: 2

5.3 降级后输出格式不一致导致下游解析失败

GPT-6 Astra、Claude、通义千问的 JSON 模式参数不完全一致。降级到备用模型时,如果下游依赖固定 JSON Schema,必须在网关层加一层响应格式标准化

java 复制代码
public String normalizeJson(String raw, Class<?> expectedType) {
    // 先尝试直接解析;失败则让经济型模型做一次 JSON 修复
    try {
        objectMapper.readTree(raw);
        return raw;
    } catch (JsonProcessingException e) {
        return jsonRepairModel.call("修复以下 JSON,使其符合:" + expectedType + "\n" + raw);
    }
}

5.4 Token 估算偏差导致限流失效

用字符数估算中文 Token 时,1 个汉字通常 1~1.5 token,但不同 tokenizer 差异很大。生产建议:

  • 输入用 JTokkit 精确计算;

  • 输出先按 maxTokens 预占额度,响应回来再修正;

  • 给每个租户设置日预算和告警,不要只设 QPS。

5.5 流式响应在网关层"憋大招"

如果前端走 SSE,网关层不要用同步 model.call() 拿到完整结果再返回。Spring AI 的 StreamingChatModel 支持 Flux<ChatResponse>,网关应直接透传:

java 复制代码
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(ChatRequest req) {
    ChatModel model = router.resolveModel(router.decide(req).primary());
    return model.stream(new Prompt(req.userMessage()))
                .map(chunk -> chunk.getResult().getOutput().getContent());
}

5.6 模型异常分类错误:该重试的没重试,不该重试的死循环

Spring AI 2.0 把异常分为 TransientAiException(限流、超时、5xx,可重试)和非瞬态异常(认证失败、参数非法)。网关层应只捕获瞬态异常进行重试,并配合指数退避:

java 复制代码
@Retryable(
    retryFor = TransientAiException.class,
    backoff = @Backoff(delay = 500, multiplier = 2, maxDelay = 8000),
    maxAttempts = 3
)
public ChatResponse callWithRetry(Prompt prompt, ChatModel model) {
    return model.call(prompt);
}

5.7 没有 Prompt 缓存导致重复计费

Spring AI 2.0 支持 OpenAI 的 promptCacheKey 机制(RC 后修复字段名)。对于系统提示固定、用户问题变化的知识库场景,务必开启 Prompt Caching,可让缓存输入价格降到 $1/1M tokens:

java 复制代码
OpenAiChatOptions options = OpenAiChatOptions.builder()
    .model("gpt-6-astra")
    .promptCacheKey("kb-v1.2.0-system")
    .build();

六、总结

GPT-6 Astra 的发布不是让 Java 后端团队"换模型",而是让多模型共存成为常态。Spring AI 2.0 通过 ChatModel/ChatClient 统一抽象、ModelRegistry 运行时路由、Advisor 链式编排,已经把"模型无关"这件事做通了。

真正难的不是接入模型,而是用 Java 的工程化能力把多模型调度做成可观测、可降级、可审计的基础设施 。SmartModelGateway 的关键设计可以概括为四句话:

  • 按角色抽象模型 ,而不是按厂商名硬编码;

  • 规则 + 成本驱动路由 ,第一版避免黑盒分类器;

  • 熔断 + 降级链兜底 ,先保可用,再保质量;

  • Token 级限流 + 成本指标,让每一分钱都看得见。

对于坚持在 Java 生态里做 AI 的工程师来说,这层网关就是回答"为什么不用转 Python"的最佳注脚:模型可以换,但工程化、稳定性、可治理性,才是企业真正的护城河。


参考链接

相关推荐
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之密码存储安全进化与实践
java·安全·spring
小七在进步1 小时前
C++入门(3)
android·java·c++
前端 贾公子1 小时前
Milvus使用指南 (下)
java·服务器·前端
步行cgn1 小时前
OCP开闭原则:面向对象设计的核心原则
java·开发语言·开闭原则
DolphinDB2 小时前
Text-to-SQL 已过时?DolphinX 正在重新定义 AI 问数
ai·时序数据库·dolphindb
落木萧萧8252 小时前
MyBatis 启动的时候都在干什么:从 MappedStatement 说起
java·数据库·后端
en.en..2 小时前
Linux wait()函数(预防僵尸进程)
java·大数据·开发语言
扬大平仔2 小时前
小深:用 AgentScope Java 2.0 Harness 做私人助手(上)
java·开发语言
captain3762 小时前
网络编程(1)
java·网络·ide·java-ee