当 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 的核心签名非常克制:
java
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 模式。
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 引入了 ModelRegistry 与 ClientRegistry,允许在运行时按名称注册和获取模型实例:
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预扣,响应返回后再通过 Redisdecrement修正差额。
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 中处理如下:
ChatClient.Builder构建时保存defaultOptions;- 每次请求通过
.options(ChatOptions.Builder)传入运行时 Builder; - 运行时用
ChatOptions.Builder.build()生成最终选项; 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,在请求时把 model、temperature、maxTokens 等字段注入 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"的最佳注脚:模型可以换,但工程化、稳定性、可治理性,才是企业真正的护城河。
参考链接
-
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