大模型服务集成:Spring AI 框架下的多模型编排与容错实践
一、多模型集成的工程困境:从单点调用到多供应商编排
企业引入大模型能力时,往往不会只依赖单一供应商。某电商平台同时使用 OpenAI 处理商品描述生成、Claude 处理客服对话、本地部署的 Qwen 处理隐私数据分类。三个模型来自不同供应商,API 协议各异,认证方式不同,错误码体系也不统一。后端团队为每个模型写了一套独立的调用封装,导致代码重复、监控分散、容错策略不一致。
更棘手的是模型切换场景。当 OpenAI API 出现区域性故障时,需要将流量切换到 Claude,但两家的 Prompt 格式和参数命名不同,切换不是简单的 URL 替换,而是需要重新适配请求结构。
Spring AI 框架的出现,为 Java 生态提供了一套统一的大模型集成抽象。它屏蔽了不同供应商的 API 差异,提供统一的ChatModel接口,并内置了 Prompt 模板、对话记忆、函数调用等能力。本文将围绕 Spring AI 的多模型编排与容错机制展开实践。
二、Spring AI 的核心抽象与多模型编排架构
Spring AI 的设计哲学与 Spring Data 类似:提供统一接口,通过不同实现适配多种数据源(此处是模型供应商)。
graph TB subgraph "应用层" App["业务服务<br/>ChatService / RAGService"] end subgraph "Spring AI 抽象层" ChatModel["ChatModel 接口<br/>统一调用协议"] Prompt["Prompt 模板<br/>参数化 Prompt 管理"] Memory["ChatMemory<br/>对话上下文管理"] Function["FunctionCallback<br/>工具函数注册"] end subgraph "模型适配层" OpenAI["OpenAiChatModel<br/>GPT-4 / GPT-4o"] Claude["ClaudeChatModel<br/>Claude 3.5"] Qwen["QwenChatModel<br/>通义千问"] Ollama["OllamaChatModel<br/>本地模型"] end subgraph "容错与治理层" Fallback["模型降级链<br/>OpenAI → Claude → Qwen"] Retry["重试策略<br/>指数退避"] CircuitBreaker["熔断器<br/>Resilience4j"] end App --> ChatModel App --> Prompt App --> Memory ChatModel --> OpenAI ChatModel --> Claude ChatModel --> Qwen ChatModel --> Ollama OpenAI -->|"故障"| Fallback Fallback -->|"降级"| Claude Claude -->|"降级"| Qwen Retry --> CircuitBreakerChatModel:统一的调用抽象
Spring AI 的ChatModel接口定义了三个核心方法:
call(Prompt prompt):同步调用,返回ChatResponsestream(Prompt prompt):流式调用,返回Flux<ChatResponse>call(String message):简化调用,直接传入文本
不同供应商的 ChatModel 实现类负责将统一请求转换为供应商特定的 API 调用。应用代码只依赖ChatModel接口,不感知底层供应商差异。
Prompt 模板:参数化管理
Prompt 是大模型调用的核心输入。Spring AI 的PromptTemplate支持{variable}占位符,将 Prompt 结构与业务数据分离:
PromptTemplate template = new PromptTemplate( "你是一个{role},请用{style}的风格回答以下问题:{question}" ); Prompt prompt = template.create( Map.of("role", "Java架构师", "style", "严谨务实", "question", userQuestion) );这种参数化管理使得 Prompt 的迭代优化与业务代码解耦,修改 Prompt 不需要重新部署应用。
三、多模型编排与容错的代码实现
以下是基于 Spring AI 和 Resilience4j 的多模型编排服务实现:
@Service public class MultiModelChatService { private final Map<String, ChatModel> modelMap; private final CircuitBreakerRegistry circuitBreakerRegistry; private final MeterRegistry meterRegistry; // 模型降级链:按优先级排列 private static final List<String> MODEL_FALLBACK_CHAIN = List.of("openai", "claude", "qwen"); public MultiModelChatService( Map<String, ChatModel> modelMap, CircuitBreakerRegistry circuitBreakerRegistry, MeterRegistry meterRegistry) { this.modelMap = modelMap; this.circuitBreakerRegistry = circuitBreakerRegistry; this.meterRegistry = meterRegistry; } /** * 带降级链的模型调用 * 按优先级尝试,主模型熔断后自动切换到备用模型 */ public ChatResponse chatWithFallback(String model, Prompt prompt) { List<String> chain = buildFallbackChain(model); for (String modelName : chain) { ChatModel chatModel = modelMap.get(modelName); if (chatModel == null) { continue; } CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker( "llm-" + modelName, CircuitBreakerConfig.custom() .failureRateThreshold(50.0f) .waitDurationInOpenState(Duration.ofSeconds(30)) .slidingWindowSize(10) .build() ); try { ChatResponse response = Decorators.ofSupplier(() -> chatModel.call(prompt)) .withCircuitBreaker(cb) .withRetry(RetryConfig.custom() .maxAttempts(2) .waitDuration(Duration.ofMillis(500)) .retryOnException(this::isRetryable) .build()) .get(); meterRegistry.counter("llm.call.success", "model", modelName).increment(); return response; } catch (CallNotPermittedException e) { // 熔断器打开,跳到下一个模型 meterRegistry.counter("llm.circuitbreaker.open", "model", modelName).increment(); continue; } catch (Exception e) { meterRegistry.counter("llm.call.failure", "model", modelName).increment(); continue; } } // 所有模型均不可用,返回兜底响应 return ChatResponse.builder() .content("当前服务繁忙,请稍后重试") .build(); } /** * 构建降级链:将指定模型放在首位,其余按默认顺序排列 */ private List<String> buildFallbackChain(String preferredModel) { List<String> chain = new ArrayList<>(); chain.add(preferredModel); for (String model : MODEL_FALLBACK_CHAIN) { if (!model.equals(preferredModel)) { chain.add(model); } } return chain; } /** * 判断异常是否可重试 * 限流错误和超时错误可重试,认证错误不可重试 */ private boolean isRetryable(Throwable t) { if (t instanceof HttpStatusCodeException e) { int status = e.getStatusCode().value(); return status == 429 || status == 503 || status == 504; } return t instanceof TimeoutException || t instanceof SocketTimeoutException; } }对话记忆的集成
@Service public class ContextualChatService { private final ChatModel chatModel; private final ChatMemory chatMemory; private static final int MAX_HISTORY = 10; /** * 带上下文记忆的对话 * 自动维护对话历史,避免上下文溢出 */ public String chat(String sessionId, String userMessage) { // 加载历史对话 List<Message> history = chatMemory.get(sessionId, MAX_HISTORY); // 构建完整 Prompt:系统指令 + 历史 + 当前问题 List<Message> messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个专业的 Java 架构顾问")); messages.addAll(history); messages.add(new UserMessage(userMessage)); Prompt prompt = new Prompt(messages); ChatResponse response = chatModel.call(prompt); // 保存本轮对话到记忆 chatMemory.add(sessionId, new UserMessage(userMessage)); chatMemory.add(sessionId, new AssistantMessage(response.getContent())); return response.getContent(); } }生产环境注意点:
- 对话历史截断:大模型有 Token 上限,历史消息过长会导致超限错误。必须设置
MAX_HISTORY,并在 Prompt 组装时计算 Token 数量,超限时截断最早的消息。 - 记忆存储选择:开发环境可用
InMemoryChatMemory,生产环境必须用RedisChatMemory或数据库持久化,避免服务重启后丢失上下文。 - 函数调用安全:Spring AI 的
FunctionCallback允许大模型调用 Java 方法。必须对可调用的函数做白名单控制,禁止大模型执行任意代码。
四、多模型集成的架构权衡
统一抽象的代价
Spring AI 的ChatModel接口提供了统一抽象,但也意味着无法使用供应商特有的能力。例如,OpenAI 的 Function Calling 和 Claude 的 Tool Use 在协议层面有差异,Spring AI 的抽象层需要做适配,某些高级特性可能无法完整暴露。
降级链的延迟叠加
降级链在提高可用性的同时,也引入了延迟叠加风险。当主模型熔断后,请求需要依次尝试备用模型。如果主模型响应超时 5 秒后才触发降级,用户感知的延迟可能达到 10 秒以上。解决方案是为主模型设置合理的超时时间(建议 3-5 秒),超时即降级,而非等待熔断器打开。
Token 成本的跨模型核算
不同模型的 Token 单价差异巨大。降级到备用模型时,虽然保证了可用性,但成本可能大幅增加。需要在监控层面按模型维度统计 Token 消耗,设置成本告警阈值。
对话记忆的一致性
降级切换模型后,对话历史中的 Assistant 回复来自不同模型,可能导致上下文理解不一致。例如 OpenAI 和 Claude 对同一问题的回答风格和格式不同,后续模型可能对历史上下文产生误解。在高一致性要求的场景下,切换模型时应考虑清空对话历史或添加过渡提示。
五、总结
Spring AI 为 Java 生态的大模型集成提供了统一抽象层,屏蔽了多供应商的 API 差异。通过ChatModel接口、Prompt 模板和对话记忆的组合,开发者可以用一致的编程模型调用不同的大模型。结合 Resilience4j 的熔断与重试机制,可以实现多模型降级链,保障服务可用性。
但统一抽象必然意味着特性折衷,降级链必然引入延迟叠加。架构设计需要在"统一性"与"灵活性"、"可用性"与"延迟"之间做出取舍。对于大多数企业场景,Spring AI 的抽象层已经足够,但在需要深度利用供应商特有能力的场景下,可能需要绕过抽象层直接调用原生 API。
落地路线建议:先用 Spring AI 接入单一模型,验证 Prompt 模板和对话记忆的集成效果;然后引入第二个模型,实现降级链和熔断机制;最后建立按模型维度的 Token 成本监控和延迟监控,形成完整的可观测闭环。