1. 项目概述:为什么我们需要一个“进化式”的LLM多模型接入框架?
如果你正在用Spring Boot开发应用,并且最近被各种大语言模型(LLM)的API搞得焦头烂额,那么你肯定遇到过这些问题:今天老板说要接ChatGPT,明天产品经理说DeepSeek效果更好,后天运维反馈说Azure OpenAI服务更稳定。每次换模型,你都得吭哧吭哧改代码、调配置、测接口,一个简单的/chat接口背后,可能藏着四五套不同厂商的SDK调用逻辑,代码里到处都是if-else,维护成本直线上升。
这就是“Spring AI + Spring Boot:从 0 到 1 进化式搭建 LLM 多模型接入”这个项目要解决的核心痛点。它不是一个简单的“Hello World”式集成教程,而是一套可演进、可插拔、面向生产的架构设计方案。所谓“进化式”,指的是你的系统从一开始就能容纳未来可能接入的任何模型,就像搭乐高一样,新的模型能力可以作为一个模块轻松“插入”现有系统,而不会引起架构地震。
Spring AI项目正是为此而生。它本质上是一个抽象层,将不同LLM提供商(如OpenAI、Anthropic、Azure OpenAI、本地部署的Ollama等)的API差异封装起来,提供了一套统一的编程模型。但仅仅引入Spring AI的starter依赖,离“多模型接入”还有很大距离。真正的挑战在于:如何根据用户请求、内容类型、成本预算或模型性能,动态地选择最合适的模型?如何优雅地处理模型切换、失败重试和监控?这就是“模型路由”要解决的问题,也是本次搭建的核心。
这套方案适合谁?如果你是后端开发者,正在或计划将LLM能力集成到你的Spring Boot应用中,无论是构建智能客服、内容生成、代码辅助还是复杂的AI Agent系统,这篇文章都能为你提供一个坚实的起点。我们将从最基础的单一模型接入开始,逐步构建出一个支持动态路由、具备良好扩展性的生产级框架。
2. 核心架构设计与思路拆解
2.1 统一抽象:Spring AI的核心价值
在深入搭建之前,必须理解Spring AI的设计哲学。它没有重新发明轮子去调用每个模型的HTTP API,而是定义了两个最核心的接口:ChatClient和ChatModel。ChatClient侧重于对话的交互式调用,而ChatModel则是对模型能力的更底层抽象。无论背后是GPT-4还是Claude 3,你都可以通过chatModel.call(prompt)这样的统一方式来发起请求。
这个抽象带来的最大好处是解耦。你的业务逻辑(如处理用户问题、组装提示词)不再关心具体调用的是哪个厂商的API。当需要切换模型时,你只需要在配置层面更换一个Bean,或者通过我们后面要实现的“路由”逻辑来动态选择,业务代码几乎无需改动。这为多模型接入奠定了理论基础。
2.2 进化式架构蓝图
我们的目标架构不是一蹴而就的,而是分阶段“进化”的。我将其分为三个核心阶段,你可以根据项目当前需求,选择停留在某一阶段,或平滑演进到下一阶段。
阶段一:基础统一接入层这是起点。我们引入Spring AI,并配置多个不同模型的ChatModelBean(例如,一个叫openAiChatModel,一个叫azureOpenAiChatModel)。此时,虽然我们有多个Bean,但业务代码可能还是通过@Qualifier硬编码指定使用某一个。这一步的价值在于,我们统一了所有模型的调用方式,将厂商SDK的差异隔离在了Spring的配置类里。
阶段二:静态策略路由当我们需要根据简单规则(比如,A功能用模型A,B功能用模型B)切换模型时,就进入这个阶段。我们会创建一个ModelRouterService,其内部维护一个从“路由键”到具体ChatModelBean的映射。这个“路由键”可以来自注解、请求头,或简单的业务逻辑判断。例如,所有标注了@UseModel(name=“fast”)的控制器方法,都自动使用低延迟的模型。此时,模型选择逻辑开始集中化管理。
阶段三:动态智能路由这是完全体。路由决策不再是静态配置,而是由一系列“路由器”组成的责任链动态计算得出。决策因素可以非常复杂:考虑当前用户的套餐等级(免费用户用低成本模型)、查询内容的领域(代码问题用CodeLlama)、目标输出格式(需要JSON格式的用GPT-4 Turbo)、各个模型的实时负载和延迟,甚至结合上次调用的效果反馈(A模型回答不好,下次类似问题换B模型)。这个阶段的核心是一个可扩展的RoutingStrategy接口和一系列实现它的策略类。
为什么选择分阶段演进?因为技术债往往源于过度设计。如果你的应用只需要对接1-2个模型,且切换频率极低,那么阶段一完全够用,强行引入复杂的路由框架只会增加维护成本。分阶段演进让你能够根据实际业务压力的增长,适时地投入架构复杂度,确保技术始终服务于业务,而不是相反。
2.3 技术栈选型与考量
除了Spring Boot和Spring AI这两个基石,我们还需要一些辅助组件来构建健壮的系统:
- Spring Boot 3.x+:这是必须的。Spring AI 2.0+ 基于Spring Framework 6,它要求JDK 17+和Spring Boot 3.x。这带来了更好的性能、更现代的API(如HTTP接口Client)和更清晰的模块化。
- 配置管理:Spring Cloud Config 或 Apollo:多模型意味着大量的API Key、Endpoint URL、模型名称等配置。硬编码在
application.yml里会是一场噩梦。必须使用外部化配置中心,实现不同环境(开发、测试、生产)配置的隔离和动态刷新。 - 监控与观测:Micrometer + Prometheus/Grafana:你需要知道每个模型的调用耗时、成功率、Token消耗情况。Spring AI天然支持Micrometer,可以轻松地将这些指标暴露出来,结合Grafana面板,你能一目了然地看到哪个模型现在是瓶颈,成本消耗如何。
- ** resilience4j**:网络调用必然失败。你需要为每个模型的客户端配置熔断、限流、重试和超时控制。Resilience4j与Spring Boot集成良好,可以防止一个模型服务的不稳定拖垮整个应用。
- 缓存:Caffeine 或 Redis:对于一些常见的、结果确定的提示词(例如,“将以下文本翻译成英文”),可以将结果缓存起来,避免重复调用LLM,显著降低成本和延迟。根据缓存是否需要跨实例共享,选择本地缓存或分布式缓存。
注意:版本兼容性是第一道坎。Spring AI发展迅速,务必在 start.spring.io 创建项目时,仔细核对Spring Boot、Spring AI以及云组件(如Spring Cloud)的版本兼容性。一个常见的坑是,直接引入最新的
spring-ai-openai-spring-boot-starter,却发现与项目中某个老版本的Spring Cloud组件冲突。我的建议是,在项目初期就锁定一个经过验证的版本组合,例如 Spring Boot 3.2.x + Spring AI 1.0.0 M2(稳定版),并做好BOM管理。
3. 从0到1:基础多模型环境搭建
3.1 项目初始化与依赖引入
首先,我们创建一个干净的Spring Boot 3.2项目。在pom.xml中,我们不是直接引入各个模型的starter,而是先引入Spring AI的BOM(物料清单)来统一管理版本,这是避免依赖地狱的最佳实践。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M2</version> <!-- 使用当前稳定里程碑版本 --> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后,引入我们需要的具体模型starter和必要的功能依赖。假设我们计划接入OpenAI和Ollama(用于本地模型)。
<dependencies> <!-- Spring Boot 基础 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <!-- 用于健康检查和监控 --> </dependency> <!-- Spring AI 模型接入 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <!-- 版本由上面的BOM控制 --> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> </dependency> <!-- 熔断限流 --> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <!-- Resilience4j 需要 --> </dependency> <!-- 监控 --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <scope>runtime</scope> </dependency> <!-- 工具类 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>3.2 配置多个模型客户端
接下来是核心步骤:在application.yml中配置多个模型的连接信息。这里的关键是,Spring AI默认会为每个starter自动配置一个主ChatModelBean,名字通常是chatModel。但当我们有多个同类starter时,会产生Bean冲突。因此,我们需要禁用默认自动配置,并手动定义多个具有不同名称的Bean。
首先,配置application.yml:
spring: application: name: llm-gateway # OpenAI 配置 spring.ai.openai: api-key: ${OPENAI_API_KEY:sk-your-key-here} base-url: https://api.openai.com/v1 # 如果是Azure,这里要改 chat: options: model: gpt-3.5-turbo # 默认模型 temperature: 0.7 # Ollama 配置 (假设本地运行了Ollama服务) spring.ai.ollama: base-url: http://localhost:11434 chat: options: model: llama3:8b # 本地模型名称 temperature: 0.8 # 禁用默认的ChatModel Bean自动配置,避免冲突 spring.autoconfigure.exclude: > org.springframework.ai.openai.autoconfigure.OpenAiAutoConfiguration, org.springframework.ai.ollama.autoconfigure.OllamaAutoConfiguration然后,创建一个配置类ModelConfiguration.java,手动定义我们需要的Bean:
@Configuration public class ModelConfiguration { @Bean @Primary // 可以指定一个为主Bean,在某些简单场景下直接注入ChatModel会用到这个 public ChatModel openAiChatModel(OpenAiChatOptions openAiOptions) { // 使用OpenAiChatClient,它实现了ChatModel接口 OpenAiChatClient client = new OpenAiChatClient(openAiOptions); // 这里可以进一步配置client,如设置自定义的RestTemplate(用于代理等) return client; } @Bean public ChatModel ollamaChatModel(OllamaChatOptions ollamaOptions) { OllamaChatClient client = new OllamaChatClient(ollamaOptions); return client; } // 为不同的ChatOptions提供Bean,方便从配置文件注入 @Bean @ConfigurationProperties(prefix = "spring.ai.openai.chat.options") public OpenAiChatOptions openAiChatOptions() { return new OpenAiChatOptions(); } @Bean @ConfigurationProperties(prefix = "spring.ai.ollama.chat.options") public OllamaChatOptions ollamaChatOptions() { return new OllamaChatOptions(); } }现在,你的应用里就有了两个ChatModelBean:openAiChatModel和ollamaChatModel。你可以通过@Qualifier注解在Service中注入特定的一个。但这只是“多模型并存”,还不是“智能接入”。
3.3 构建统一门面服务
为了让业务方更方便地调用,我们创建一个门面服务LlmGatewayService。在初级阶段,它可以提供一个简单的方法,通过参数选择模型。
@Service @Slf4j public class LlmGatewayService { private final ChatModel openAiChatModel; private final ChatModel ollamaChatModel; private final MeterRegistry meterRegistry; // 用于监控 public LlmGatewayService(@Qualifier("openAiChatModel") ChatModel openAiChatModel, @Qualifier("ollamaChatModel") ChatModel ollamaChatModel, MeterRegistry meterRegistry) { this.openAiChatModel = openAiChatModel; this.ollamaChatModel = ollamaChatModel; this.meterRegistry = meterRegistry; } public String chat(String prompt, String modelType) { ChatModel targetModel = switch (modelType.toLowerCase()) { case "openai" -> openAiChatModel; case "ollama" -> ollamaChatModel; default -> throw new IllegalArgumentException("Unsupported model type: " + modelType); }; // 使用Timer监控每次调用耗时 Timer.Sample sample = Timer.start(meterRegistry); try { String response = targetModel.call(prompt); sample.stop(Timer.builder("llm.call.duration") .tag("model", modelType) .tag("status", "success") .register(meterRegistry)); return response; } catch (Exception e) { sample.stop(Timer.builder("llm.call.duration") .tag("model", modelType) .tag("status", "error") .register(meterRegistry)); log.error("LLM call failed for model: {}", modelType, e); throw new RuntimeException("LLM service call failed", e); } } }这个服务已经具备了基础的多模型调用和监控能力。控制器可以这样调用它:llmGatewayService.chat("你好", "openai")。但这还很原始,模型选择逻辑硬编码在Service里,且缺乏弹性。
4. 实现模型路由:从静态到动态的进化
4.1 定义路由策略接口
路由的核心是“根据上下文,选择最合适的模型”。我们首先定义一个策略接口RoutingStrategy:
public interface RoutingStrategy { /** * 决定使用哪个模型 * @param context 路由上下文,包含所有决策所需信息 * @return 模型标识符(对应我们定义的Bean名称,如 "openAiChatModel") */ String determineModel(RoutingContext context); /** * 策略优先级,数值越小优先级越高 */ int getOrder(); }RoutingContext是一个承载上下文信息的容器,你可以根据需要扩展它:
@Data @Builder public class RoutingContext { // 用户身份与权限 private String userId; private String userTier; // 如 "free", "premium" // 请求内容 private String prompt; private PromptType promptType; // 枚举:CODE_GENERATION, TRANSLATION, SUMMARIZATION, GENERAL_CHAT等 private Map<String, Object> extraParams; // 扩展参数,如要求JSON格式 // 系统状态(可从监控系统实时获取) private Map<String, ModelHealth> modelHealthStatus; // 各模型健康状态 // 历史信息(可选,用于学习型路由) private List<InteractionHistory> previousInteractions; }4.2 实现具体的路由策略
现在,我们可以实现多种策略。策略之间可以形成责任链,高优先级的策略先执行,如果它能做出决定,则返回;否则传递给下一个策略。
策略1:基于用户等级的路由(静态策略)
@Component @Order(10) // 高优先级 public class UserTierRoutingStrategy implements RoutingStrategy { @Override public String determineModel(RoutingContext context) { if ("premium".equals(context.getUserTier())) { return "openAiChatModel"; // 付费用户用更好的模型 } else if ("free".equals(context.getUserTier())) { return "ollamaChatModel"; // 免费用户用成本更低的本地模型 } return null; // 无法决定,交给下一个策略 } @Override public int getOrder() { return 10; } }策略2:基于内容类型的路由(静态策略)
@Component @Order(20) public class ContentTypeRoutingStrategy implements RoutingStrategy { @Override public String determineModel(RoutingContext context) { PromptType type = context.getPromptType(); if (type == PromptType.CODE_GENERATION || type == PromptType.CODE_EXPLANATION) { // 假设我们为代码任务专门微调了一个模型 return "codeLlamaChatModel"; } else if (type == PromptType.TRANSLATION) { // 翻译任务可能对某个模型有优化 return "openAiChatModel"; } return null; } // ... getOrder }策略3:基于负载和成本的动态路由(动态策略)这个策略更复杂,它需要实时数据。
@Component @Order(30) // 较低优先级,作为兜底 public class DynamicLoadBalancingRoutingStrategy implements RoutingStrategy { private final MeterRegistry meterRegistry; private final CostCalculator costCalculator; // 假设的成本计算器 // 模拟一个简单的模型健康度与延迟内存 private final Map<String, ModelMetrics> modelMetrics = new ConcurrentHashMap<>(); @Scheduled(fixedDelay = 30000) // 每30秒更新一次指标 public void updateMetrics() { // 从Micrometer或外部监控系统拉取各模型的平均延迟、错误率 // 更新到 modelMetrics Map中 modelMetrics.put("openAiChatModel", fetchMetrics("openai")); modelMetrics.put("ollamaChatModel", fetchMetrics("ollama")); } @Override public String determineModel(RoutingContext context) { // 1. 过滤掉不健康的模型(错误率过高) List<String> healthyModels = modelMetrics.entrySet().stream() .filter(e -> e.getValue().getErrorRate() < 0.1) // 错误率低于10% .map(Map.Entry::getKey) .collect(Collectors.toList()); if (healthyModels.isEmpty()) { throw new RuntimeException("No healthy model available"); } // 2. 根据成本和延迟加权打分 String selectedModel = healthyModels.stream() .min(Comparator.comparingDouble(model -> { ModelMetrics metrics = modelMetrics.get(model); double cost = costCalculator.estimateCost(model, context.getPrompt()); double latency = metrics.getAvgLatency(); // 一个简单的加权公式:总分 = 成本权重 * 成本 + 延迟权重 * 延迟 // 成本需要归一化处理,这里仅为示例 return 0.7 * cost + 0.3 * latency; })) .orElse(healthyModels.get(0)); // 兜底选择第一个 return selectedModel; } // ... getOrder }4.3 构建路由链与路由服务
有了策略,我们需要一个路由器来协调它们:
@Component public class ModelRouter { private final List<RoutingStrategy> strategies; private final Map<String, ChatModel> modelRegistry; // 模型名称 -> ChatModel Bean的映射 public ModelRouter(List<RoutingStrategy> strategies, @Qualifier("openAiChatModel") ChatModel openAi, @Qualifier("ollamaChatModel") ChatModel ollama) { this.strategies = strategies.stream() .sorted(Comparator.comparingInt(RoutingStrategy::getOrder)) .collect(Collectors.toList()); this.modelRegistry = Map.of( "openAiChatModel", openAi, "ollamaChatModel", ollama // 注册更多模型... ); } public ChatModel route(RoutingContext context) { for (RoutingStrategy strategy : strategies) { String modelName = strategy.determineModel(context); if (modelName != null && modelRegistry.containsKey(modelName)) { log.debug("Strategy {} selected model: {}", strategy.getClass().getSimpleName(), modelName); return modelRegistry.get(modelName); } } // 如果所有策略都无法决定,使用默认模型 log.warn("No routing strategy made a decision, using default model."); return modelRegistry.get("openAiChatModel"); } public Map<String, ChatModel> getModelRegistry() { return Collections.unmodifiableMap(modelRegistry); } }最后,升级我们的门面服务,使用路由器:
@Service @Slf4j public class SmartLlmGatewayService { private final ModelRouter modelRouter; public SmartLlmGatewayService(ModelRouter modelRouter) { this.modelRouter = modelRouter; } public String chat(String prompt, String userId, String userTier) { // 构建路由上下文 RoutingContext context = RoutingContext.builder() .userId(userId) .userTier(userTier) .prompt(prompt) .promptType(detectPromptType(prompt)) // 实现一个简单的类型检测 .build(); // 路由到合适的模型 ChatModel targetModel = modelRouter.route(context); // 执行调用(这里可以添加熔断器、重试等装饰) return targetModel.call(prompt); } private PromptType detectPromptType(String prompt) { // 简单的关键词检测,实际应用可以用更复杂的ML模型 String lowerPrompt = prompt.toLowerCase(); if (lowerPrompt.contains("代码") || lowerPrompt.contains("function") || lowerPrompt.contains("def ")) { return PromptType.CODE_GENERATION; } else if (lowerPrompt.contains("翻译") || lowerPrompt.contains("translate")) { return PromptType.TRANSLATION; } return PromptType.GENERAL_CHAT; } }至此,一个具备动态路由能力的多模型接入框架就初具雏形了。业务方只需要调用smartLlmGatewayService.chat(prompt, userId, userTier),背后的复杂路由逻辑完全被屏蔽。
5. 生产级增强:熔断、监控与缓存
5.1 为每个模型添加Resilience4j熔断器
直接调用外部API是不稳定的。我们需要为每个ChatModelBean包装一个熔断器。我们可以利用Spring AOP和Resilience4j的注解。
首先,定义一个自定义注解,用于标记需要熔断的方法:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ModelCircuitBreaker { String modelName(); // 指定模型名称,对应不同的熔断器配置 }然后,创建一个AOP切面,将注解与Resilience4j的@CircuitBreaker注解联动。但更直接的方式是在ModelRouter的route方法返回前,动态获取或创建一个被熔断器包装的ChatModel代理。这里展示一个更清晰的方案:为每个模型Bean手动配置熔断器。
修改ModelConfiguration.java,在创建Bean时进行包装:
@Configuration public class ModelConfiguration { @Bean public ChatModel openAiChatModel(OpenAiChatOptions openAiOptions, CircuitBreakerRegistry circuitBreakerRegistry) { OpenAiChatClient rawClient = new OpenAiChatClient(openAiOptions); // 为OpenAI客户端创建一个熔断器 CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("openAiChatModel", "openAiConfig"); // 使用配置文件中的openAiConfig // 使用Resilience4j的装饰器模式包装客户端 ChatModel decoratedClient = CircuitBreaker.decorateFunction(circuitBreaker, (String prompt) -> { // 注意:这里需要将ChatModel的call方法适配为Function // 实际中,你可能需要创建一个实现了ChatModel接口的装饰器类,这里为简化示例 return rawClient.call(prompt); })::apply; // 简化示意,实际实现需要更严谨的类型适配 // 更推荐的方式是创建一个实现了ChatModel的装饰器类,在call方法内使用CircuitBreaker。 // 为了代码清晰,我们假设有一个这样的装饰器类 ModelCircuitBreakerDecorator return new ModelCircuitBreakerDecorator(rawClient, circuitBreaker); } // ... 其他模型Bean配置类似 }ModelCircuitBreakerDecorator的实现:
public class ModelCircuitBreakerDecorator implements ChatModel { private final ChatModel delegate; private final CircuitBreaker circuitBreaker; public ModelCircuitBreakerDecorator(ChatModel delegate, CircuitBreaker circuitBreaker) { this.delegate = delegate; this.circuitBreaker = circuitBreaker; } @Override public String call(String prompt) { // 使用熔断器保护调用 Supplier<String> decoratedSupplier = CircuitBreaker.decorateSupplier(circuitBreaker, () -> delegate.call(prompt)); try { return decoratedSupplier.get(); } catch (Exception e) { throw new RuntimeException("Call failed with circuit breaker", e); } } // 同样需要实现其他重载的call方法... }在application.yml中配置熔断器参数:
resilience4j.circuitbreaker: configs: default: slidingWindowSize: 10 # 基于最近10次调用计算失败率 minimumNumberOfCalls: 5 # 至少5次调用后才开始计算 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 10s # 熔断后10秒进入半开状态 failureRateThreshold: 50 # 失败率超过50%熔断 eventConsumerBufferSize: 10 instances: openAiChatModel: baseConfig: default ollamaChatModel: baseConfig: default failureRateThreshold: 70 # 本地模型可以容忍更高的失败率5.2 全面的监控与可观测性
监控是生产系统的眼睛。我们已经通过Micrometer集成了基础耗时统计。现在需要更全面的指标:
- Token消耗监控:Spring AI的
ChatClient调用会返回ChatResponse对象,其中包含Usage信息(如prompt tokens, completion tokens)。我们需要在门面服务中捕获并记录这些数据,可以发布到自定义的Meter或发送到专门的日志/分析系统。
// 在SmartLlmGatewayService的chat方法中,如果使用返回ChatResponse的方法 ChatResponse response = targetModel.call(new UserMessage(prompt)); Usage usage = response.getMetadata().getUsage(); meterRegistry.counter("llm.tokens.prompt", "model", modelName).increment(usage.getPromptTokens()); meterRegistry.counter("llm.tokens.completion", "model", modelName).increment(usage.getCompletionTokens());- 模型健康检查端点:为每个模型创建一个健康检查,定期发送一个简单的测试提示(如“Hello”),根据响应时间和成功率判断模型是否健康。这可以集成到Spring Boot Actuator的Health Indicator中。
@Component public class ModelHealthIndicator implements HealthIndicator { private final ModelRouter modelRouter; @Override public Health health() { Map<String, ChatModel> models = modelRouter.getModelRegistry(); Map<String, Object> details = new HashMap<>(); boolean allHealthy = true; for (Map.Entry<String, ChatModel> entry : models.entrySet()) { String modelName = entry.getKey(); try { long start = System.currentTimeMillis(); // 发送一个轻量级测试请求 String result = entry.getValue().call("Hello, are you alive?"); long duration = System.currentTimeMillis() - start; details.put(modelName, Map.of("status", "UP", "responseTime", duration + "ms")); } catch (Exception e) { details.put(modelName, Map.of("status", "DOWN", "error", e.getMessage())); allHealthy = false; } } Health.Builder builder = allHealthy ? Health.up() : Health.down(); return builder.withDetails(details).build(); } }- Grafana仪表盘:利用Prometheus收集的指标,构建仪表盘,关键面板包括:
- 各模型QPS(每秒查询率)与延迟(P50, P95, P99)
- 各模型错误率与熔断器状态(开、关、半开)
- 各模型Token消耗趋势与预估成本
- 路由策略命中次数饼图
5.3 实现结果缓存
对于某些确定性高的请求(如标准翻译、固定格式转换),缓存结果能极大提升响应速度并节省成本。我们可以实现一个CachingChatModelDecorator。
public class CachingChatModelDecorator implements ChatModel { private final ChatModel delegate; private final Cache<String, String> cache; // 使用Caffeine或Redis缓存 public CachingChatModelDecorator(ChatModel delegate) { this.delegate = delegate; this.cache = Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(1, TimeUnit.HOURS) // 缓存1小时 .build(); } @Override public String call(String prompt) { // 生成缓存键:可以简单用prompt,但更好的是结合模型名称和参数生成哈希键 String cacheKey = "model:" + delegate.getClass().getSimpleName() + ":prompt:" + prompt.hashCode(); return cache.get(cacheKey, key -> { // 缓存未命中,调用真实模型 return delegate.call(prompt); }); } }然后,在ModelConfiguration中,将原始的ChatModelBean用这个装饰器包装。注意,缓存策略需要谨慎设计,不是所有提示词都适合缓存。可以通过在RoutingContext中添加一个cacheable标志,让路由策略或业务逻辑来决定是否使用缓存装饰的模型。
6. 常见问题、排查技巧与演进思考
6.1 实战中踩过的坑
连接池耗尽:高频调用LLM API时,如果使用默认的RestTemplate,可能会遇到连接池耗尽的问题。解决方案:为每个模型的客户端配置独立的、带有连接池的RestTemplate或WebClient,并合理设置
max-connections和timeout。Token计算差异:不同模型对Token的计算方式有细微差别,特别是对于中文。Spring AI的
TokenCountEstimator可能不准确。解决方案:对于计费敏感的场景,最好以模型API返回的实际Usage为准,并建立自己的Token审计日志,定期核对。上下文长度限制:模型都有最大Token限制。当提示词过长时,直接调用会失败。解决方案:在门面服务中添加一个预处理步骤,使用
TokenCountEstimator估算长度,如果超限,则触发自动的文本总结、分割或丢弃最旧消息(在对话场景中)的策略。速率限制(429错误):这是调用云厂商API最常见的问题。解决方案:除了熔断器,还需要实现一个带指数退避的重试机制。Resilience4j的
Retry模块可以很容易地与CircuitBreaker组合使用。更精细化的控制可以为每个API Key设置速率限制器。Bean命名冲突:这是多模型接入初期最容易遇到的问题。牢记:一定要在配置文件中
spring.autoconfigure.exclude排除默认的自动配置类,并手动定义具有明确名称的Bean。
6.2 如何进一步演进这个框架?
向量数据库与RAG集成:当前的框架主要处理“生成”。下一步可以引入向量数据库(如Milvus, Pinecone),将路由策略扩展为:先根据问题检索相关知识片段,再选择最擅长处理该知识领域的模型进行生成。Spring AI也提供了
VectorStore和RetrievalAugmentor等抽象,可以无缝集成。Agent技能编排:模型可以看作是一个“思考者”,而Agent是“执行者”。可以在路由层之上,构建一个Agent层。路由策略不仅选择模型,还可能选择一组预定义的Agent技能(Skill)。例如,对于“分析这张图表并总结”的请求,路由结果可能是:先调用
ChartOCRModel提取数据,再调用DataAnalysisModel分析,最后调用SummaryModel生成报告。这需要更复杂的Workflow引擎,如Spring AI实验性的Spring AI Graph支持。反馈学习回路:当前的路由策略是预定义的。可以引入一个反馈系统,记录每次调用的(用户问题,所选模型,用户满意度/评分)。利用这些数据,可以训练一个简单的分类模型,来预测未来问题的最佳模型,实现路由策略的自我优化。
配置动态化:将路由策略的规则(如用户等级对应关系、成本权重)从代码移到数据库或配置中心,实现不停机动态调整。可以开发一个简单的管理界面,让运营人员能够根据实时成本和效果数据,调整路由策略。
搭建这样一个进化式的多模型接入框架,初期投入看似比直接写死调用某个API要大,但随着业务需求快速变化和模型技术的迭代,其灵活性和可维护性优势会越来越明显。它让你的应用真正具备了“拥抱变化”的能力,而不是在每次变化来临时推倒重来。