Spring AI 生产环境配置清单与容错最佳实践
把大模型接口接入业务系统并不难,写个 Controller 调一下 SDK 半天就能上线。但一旦业务流量上来,模型端偶尔抖动、网络偶发丢包、上下文超长触发限流或者下游供应商突发宕机,原先写得轻巧的代码就会迅速引发连接池耗尽、线程打满,甚至直接拖垮核心业务。
在生产环境中落地 Spring AI,不能当作普通 HTTP 客户端来对待。大模型调用兼具高延迟、高消耗、响应体积大以及流式长连接等特征。这里把我们在生产环境摸爬滚打总结出来的核心配置清单与容错方案做一次完整梳理。
一、HTTP 传输层与客户端选型
Spring AI 默认支持通过 RestClient 或 WebClient 与大模型服务交互。很多同学在本地测试使用默认配置,进入生产后立刻遇到了连接泄漏与超时失控的问题。
1. 放弃默认 SimpleClientHttpRequestFactory
默认的 JDK 原生 HttpURLConnection 缺少连接池复用与细粒度的超时控制,在并发场景下会频繁创建 TCP 连接导致 TIME_WAIT 堆积。推荐切换到底层由 Apache HttpClient 5 或 OkHttp 支持的 ClientHttpRequestFactory。
2. 超时参数必须分级配置
大模型的首字延迟(TTFT)通常在几百毫秒到数秒不等,完整内容生成可能长达几十秒。必须将“建连超时”、“读取超时”以及“连接池借出超时”明确拆分:
spring: ai: openai: api-key: ${AI_OPENAI_API_KEY} base-url: ${AI_GATEWAY_URL:https://api.openai.com} chat: options: model: gpt-4o-mini temperature: 0.3 max-tokens: 2048 # 底层 HTTP 连接池优化 http: client: max-total-connections: 200 max-per-route: 50 connect-timeout-ms: 3000 socket-timeout-ms: 60000 connection-request-timeout-ms: 2000在配置类中定制 RestClient.Builder,避免全局共享线程阻塞:
package com.yali.ai.config; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.core5.util.Timeout; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestClient; import java.util.concurrent.TimeUnit; @Configuration public class AiHttpClientConfig { @Bean public RestClientCustomizer restClientCustomizer() { PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(200); connectionManager.setDefaultMaxPerRoute(50); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(Timeout.of(3, TimeUnit.SECONDS)) .setResponseTimeout(Timeout.of(60, TimeUnit.SECONDS)) .setConnectionRequestTimeout(Timeout.of(2, TimeUnit.SECONDS)) .build(); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .evictExpiredConnections() .evictIdleConnections(Timeout.of(30, TimeUnit.SECONDS)) .build(); return restClientBuilder -> restClientBuilder .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)); } }二、容错体系:重试风暴拦截与断路降级
大模型服务遇到 429(Rate Limit)或 503(Service Unavailable)属于家常便饭。盲目重试非但救不回请求,反而会成倍放大上游压力。
1. 智能退避重试(Exponential Backoff)
重试逻辑必须满足三个条件:
- 只对特定状态码(429、500、502、503、504)和网络 I/O 超时进行重试;
- 引入指数退避加抖动(Jitter),防止集群多节点同时重试形成共振;
- 单个请求的最大重试次数严控在 2 次以内。
结合 Resilience4j 实现声明式保护:
resilience4j: retry: instances: aiChatService: max-attempts: 3 wait-duration: 1000ms exponential-backoff-multiplier: 2.0 randomize-wait: true retry-exceptions: - org.springframework.web.client.ResourceAccessException - org.springframework.web.client.HttpServerErrorException ignore-exceptions: - org.springframework.web.client.HttpClientErrorException.BadRequest - org.springframework.web.client.HttpClientErrorException.Unauthorized circuitbreaker: instances: aiChatService: sliding-window-type: COUNT_BASED sliding-window-size: 20 minimum-number-of-calls: 10 failure-rate-threshold: 50 wait-duration-in-open-state: 15s permitted-number-of-calls-in-half-open-state: 3 slow-call-duration-threshold: 8000ms slow-call-rate-threshold: 702. 双模型主备无缝降级
当主模型服务商响应超时或进入熔断状态时,业务层不能直接对用户抛出错误弹窗,应当优雅切换到备用模型或离线规则缓存。
package com.yali.ai.service; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.retry.annotation.Retry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; @Service public class RobustAiChatService { private static final Logger log = LoggerFactory.getLogger(RobustAiChatService.class); private final ChatClient primaryChatClient; private final ChatClient backupChatClient; public RobustAiChatService( @Qualifier("primaryChatClient") ChatClient primaryChatClient, @Qualifier("backupChatClient") ChatClient backupChatClient) { this.primaryChatClient = primaryChatClient; this.backupChatClient = backupChatClient; } @Retry(name = "aiChatService") @CircuitBreaker(name = "aiChatService", fallbackMethod = "fallbackToBackupModel") public String generateContent(String systemPrompt, String userMessage) { return primaryChatClient.prompt() .system(systemPrompt) .user(userMessage) .call() .content(); } public String fallbackToBackupModel(String systemPrompt, String userMessage, Throwable throwable) { log.warn("主模型调用失败,触发备用模型降级。原因: {}", throwable.getMessage()); try { return backupChatClient.prompt() .system(systemPrompt) .user(userMessage) .call() .content(); } catch (Exception ex) { log.error("备用模型亦调用失败,返回兜底话术", ex); return "当前智能助手正处于用量高峰,请稍后重试或联系人工客服。"; } } }三、流式输出与背压控制(SSE / Flux)
很多业务场景使用 SSE(Server-Sent Events)向前端推流。这里存在一个经典的生产隐患:如果客户端网络变慢或主动断开连接,后端的模型请求是否会继续空跑并白白消耗 Token?
答案是:如果不做处理,Spring AI 仍会接收完整个 HTTP 响应。
必须在流式管道中显式处理生命周期取消信号:
package com.yali.ai.controller; import com.yali.ai.service.StreamAiService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController public class StreamChatController { private static final Logger log = LoggerFactory.getLogger(StreamChatController.class); private final StreamAiService streamAiService; public StreamChatController(StreamAiService streamAiService) { this.streamAiService = streamAiService; } @GetMapping(value = "/api/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamChat(@RequestParam String prompt) { return streamAiService.streamPrompt(prompt) .doOnCancel(() -> log.info("客户端主动断开连接,已停止下游模型消费")) .doOnError(e -> log.error("流式推送发生异常: {}", e.getMessage())) .onErrorResume(e -> Flux.just("[服务异常,生成中断]")); } }四、生产可观测性与 Token 消耗审计
调用外部 AI 接口不仅关系到可用性,更关系到真金白银的账单。在 Spring AI 中,每次交互都必须精确记录 Prompt Tokens、Completion Tokens 以及耗时指标。
推荐通过ChatModelObservationConvention或自定义拦截器将关键指标上报至 Prometheus / Micrometer:
package com.yali.ai.advisor; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.ai.chat.client.advisor.api.AdvisedRequest; import org.springframework.ai.chat.client.advisor.api.AdvisedResponse; import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor; import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain; import org.springframework.ai.chat.metadata.Usage; import org.springframework.stereotype.Component; @Component public class TokenMetricsAdvisor implements CallAroundAdvisor { private final Counter promptTokenCounter; private final Counter completionTokenCounter; public TokenMetricsAdvisor(MeterRegistry registry) { this.promptTokenCounter = registry.counter("ai.tokens.prompt.total"); this.completionTokenCounter = registry.counter("ai.tokens.completion.total"); } @Override public String getName() { return "TokenMetricsAdvisor"; } @Override public int getOrder() { return 0; } @Override public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) { AdvisedResponse response = chain.nextAroundCall(advisedRequest); if (response.response() != null && response.response().getMetadata() != null) { Usage usage = response.response().getMetadata().getUsage(); if (usage != null) { promptTokenCounter.increment(usage.getPromptTokens()); completionTokenCounter.increment(usage.getGenerationTokens()); } } return response; } }五、落地总结与上线检查清单
在正式将 Spring AI 业务推向生产环境前,建议按照以下五项核心检查点逐一对账:
- 连接池与生命周期:是否剔除了默认简单 HTTP 工厂,并配置了空闲连接定期回收策略;
- 超时梯度隔离:建连超时是否控制在 3 秒以内,单次模型交互是否设置了合理的上限,避免慢调用占满 Worker 线程;
- 熔断与主备切换:是否配置了 Resilience4j 状态熔断,且定义了兜底模型或离线文案;
- 前端断连保护:流式推送在用户关掉页面时是否能及时传播 Cancel 信号,终止云端 Token 计费;
- 精细化审计打点:是否记录了租户维度的 Token 用量分布与延迟直方图,便于做容量规划与异常排查。
把这些基础设施层面的护栏打扎实,业务层的 Prompt 编排与 Agent 流程才能真正稳当运行。