当 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,OrderService和KnowledgeService各调各的;
- 简单 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的核心签名非常克制:
public interface ChatModel extends Model<Prompt, ChatResponse> { default String call(String message) { ... } @Override ChatResponse call(Prompt prompt); }OpenAiChatModel、DashScopeChatModel、AnthropicChatModel、OllamaChatModel、DeepSeekChatModel都实现了这一接口。业务代码只要面向ChatModel编程,切换模型就不需要改调用处,只需换注入的 Bean。
2.2 ChatClient:面向业务的高阶 API
ChatClient由ChatClient.Builder构建,内部持有ChatModel、ObservationRegistry、默认Advisor列表等。Spring AI 2.0 强烈推荐用ChatClient作为业务入口,因为:
- 它内置ToolCallingAdvisor、MessageChatMemoryAdvisor等治理组件;
-.stream()一行开启 SSE 流式;
-.options()按请求覆盖模型参数,且 2.0 中ChatOptions改为不可变 Builder 模式。
ChatClient chatClient = ChatClient.builder(chatModel) .defaultSystem("你是企业知识库助手,只基于检索结果回答") .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build();2.3 Options 不可变与 mutate()
Spring AI 2.0 对 Options 做了破坏性重构:移除了copy()和fromOptions(),统一用mutate()派生新实例。以OpenAiChatOptions为例:
// 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 引入了ModelRegistry与ClientRegistry,允许在运行时按名称注册和获取模型实例:
@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 项目依赖
<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 模型角色与路由策略配置
把模型抽象成角色,而不是把模型名硬编码到代码里:
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 分类器,用规则 + 正则即可落地:
@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 链:
@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 的滑动窗口:
@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预扣,响应返回后再通过 Redisdecrement修正差额。
3.6 统一 Controller 入口
@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中处理如下:
ChatClient.Builder构建时保存defaultOptions;- 每次请求通过
.options(ChatOptions.Builder)传入运行时 Builder; - 运行时用
ChatOptions.Builder.build()生成最终选项; ChatModel实现内部把最终选项与模型默认选项合并。
关键源码片段(来自org.springframework.ai.chat.client.DefaultChatClient):
public ChatClientRequestSpec options(ChatOptions.Builder options) { this.chatOptions = options.build(); return this; }而在OpenAiChatModel内部:
protected OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, OpenAiChatOptions options) { OpenAiChatOptions mergedOptions = this.defaultOptions.mutate() .model(options.getModel()) .temperature(options.getTemperature()) // ... 其他字段 .build(); // 构造 HTTP 请求体 }这意味着:网关只需维护一份ModelConfig,在请求时把model、temperature、maxTokens等字段注入 Builder,Spring AI 会自动处理厂商差异。业务代码里不会出现if (openai) ... else if (dashscope) ...的分支地狱。
五、生产踩坑清单:从 Demo 到高可用
5.1 把 AI 网关做成 SDK 工具类
反模式:
public class AiUtil { public static String ask(String question) { ... } }问题:限流、熔断、审计散落在业务代码,无法统一治理。正确做法是把网关作为独立服务或 Sidecar,业务通过 HTTP/gRPC 调用。
5.2 只看平均耗时,不看 P99 与成本
大模型响应时长波动极大。Astra 的复杂 Agentic 任务可能 60 秒以上,Turbo 的 FAQ 只要 2 秒。如果只配全局超时,要么 Turbo 被拖慢,要么 Astra 被截断。建议为每个模型角色单独配置超时和重试:
frontier: timeout-ms: 120000 max-retry: 1 # 贵模型,失败直接降级更划算 primary: timeout-ms: 30000 max-retry: 25.3 降级后输出格式不一致导致下游解析失败
GPT-6 Astra、Claude、通义千问的 JSON 模式参数不完全一致。降级到备用模型时,如果下游依赖固定 JSON Schema,必须在网关层加一层响应格式标准化:
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>,网关应直接透传:
@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,可重试)和非瞬态异常(认证失败、参数非法)。网关层应只捕获瞬态异常进行重试,并配合指数退避:
@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:
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”的最佳注脚:模型可以换,但工程化、稳定性、可治理性,才是企业真正的护城河。
参考链接
- Spring AI 2.0.0 GA Release Notes: https://spring.io/blog/2026/06/12/spring-ai-2-0-0-GA-available-now
- OpenAI GPT-6 Astra API Reference(2026-09-03)
- Resilience4j Spring Boot 3 文档
- Spring AI GitHub: https://github.com/spring-projects/spring-ai