Spring Boot 3 + AI 的最佳实践配置——上来先把这些设好
Spring Boot 3 + AI 的最佳实践配置——上来先把这些设好
适读人群:Spring Boot 开发者 / AI 应用工程师 | 阅读时长:约15分钟 | 核心价值:直接可用的 production-ready AI 配置,跳过踩坑阶段
去年我接了一个企业内部知识库的项目,甲方要求两周上线。我心想两周够了,不就是 RAG 嘛。结果第一周全部用来写业务逻辑,第二周上线前压测,直接炸了。
AI 调用超时没设,默认 30 秒的 HTTP 超时根本不够用,模型推理慢的时候直接给用户返回 500。线程池用的默认配置,并发一上来全部卡在等待队列。日志里全是 DEBUG 级别的 HTTP 请求体,每条 AI 响应都把几千字的 prompt 打出来,磁盘一天就满了。
那次经历让我明白:Spring Boot + AI 项目,有一套配置要在动第一行业务代码之前就设好。不是可选的,是必须的。
这两年我陆陆续续做了七八个 AI 项目,从最简单的问答机器人到复杂的多模型编排系统。踩的坑基本都在配置层面,功能本身反而没什么大问题。今天把这套配置整理出来,拿走直接用。
为什么 AI 应用的配置跟普通 Spring Boot 不一样
普通的 CRUD 应用,一个请求通常几十毫秒结束,线程快速释放,超时设个 5 秒绰绰有余。
AI 应用不一样:
- 延迟高:一次 LLM 调用,最快也要 1-2 秒,慢的时候 30 秒起步(特别是长文本生成)
- 响应不稳定:同样的请求,今天 2 秒,明天可能 15 秒,取决于模型负载
- 流量突发:用户一旦养成使用习惯,早晚高峰并发会很集中
- 日志体积大:prompt + completion 动辄几千字,打日志要格外小心
- 失败模式特殊:不只有超时,还有 rate limit、context length exceeded、content filter 等各种 AI 特有错误
这些特点决定了你不能照搬普通项目的配置套路。
完整的 application.yml 配置
我直接上配置,逐段解释为什么这么设。
spring:
application:
name: ai-service
# AI 客户端配置
ai:
openai:
api-key: ${OPENAI_API_KEY}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
chat:
options:
model: ${AI_CHAT_MODEL:gpt-4o-mini}
temperature: 0.7
max-tokens: 2048
# 重要:不要在这里设太大的 max-tokens
# 按业务需求设,设太大会浪费 token 且增加延迟
embedding:
options:
model: ${AI_EMBEDDING_MODEL:text-embedding-3-small}
# 向量数据库配置(以 PgVector 为例)
vectorstore:
pgvector:
initialize-schema: false # 生产环境关掉,用 Flyway 管理
index-type: hnsw
distance-type: cosine_distance
dimensions: 1536
# HTTP 客户端超时配置(Spring Boot 3 用这个)
spring:
ai:
openai:
# 关键:AI 调用需要长超时
timeout: 120s
# 自定义超时配置(更细粒度控制)
app:
ai:
timeout:
connect: 10s # 连接超时短一点,连不上就快速失败
read: 120s # 读取超时必须长,等模型生成
write: 30s # 写超时,发送 prompt 用
retry:
max-attempts: 3
initial-interval: 1s
multiplier: 2.0
max-interval: 10s
# 只重试这些错误码
retryable-status-codes: 429, 500, 502, 503, 529
rate-limit:
requests-per-minute: 60 # 根据你的 API 套餐设
tokens-per-minute: 90000
# 线程池配置——这个是重点
app:
thread-pool:
ai-tasks:
core-size: 10
max-size: 50
queue-capacity: 200
keep-alive: 60s
thread-name-prefix: "ai-task-"
embedding-tasks:
core-size: 5
max-size: 20
queue-capacity: 100
thread-name-prefix: "embed-task-"
# 日志配置——生产环境必须这样设
logging:
level:
root: INFO
com.yourcompany: INFO
# Spring AI 的 HTTP 客户端日志——默认 DEBUG 会打完整 prompt,绝对不能开
org.springframework.ai: INFO
# 如果要调试 AI 调用,临时改成 DEBUG,调完立刻改回来
# org.springframework.ai.openai: DEBUG
# HTTP 客户端日志——同上
org.apache.http: WARN
org.springframework.web.reactive.function.client: WARN
# 向量数据库操作日志
org.springframework.ai.vectorstore: INFO
# 日志格式,加上 traceId 方便追踪 AI 调用链
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"
file:
name: logs/ai-service.log
max-size: 100MB
max-history: 7 # 只保留 7 天,AI 应用日志体积大
# Actuator 指标暴露——监控 AI 调用是否健康
management:
endpoints:
web:
exposure:
include: health, metrics, prometheus, info, loggers
endpoint:
health:
show-details: when-authorized
show-components: when-authorized
loggers:
enabled: true # 支持运行时动态修改日志级别,排查问题用
metrics:
tags:
application: ${spring.application.name}
distribution:
percentiles-histogram:
# 这几个指标必须开直方图,方便看 P95/P99
http.server.requests: true
spring.ai.chat.client.operation: true
spring.ai.vectorstore.operation: true
percentiles:
http.server.requests: 0.5, 0.75, 0.90, 0.95, 0.99
spring.ai.chat.client.operation: 0.5, 0.75, 0.90, 0.95, 0.99
prometheus:
metrics:
export:
enabled: true线程池配置代码
光有 yml 不够,还需要把线程池注册成 Bean。
@Configuration
@EnableAsync
public class AsyncConfig {
@Value("${app.thread-pool.ai-tasks.core-size:10}")
private int aiCoreSize;
@Value("${app.thread-pool.ai-tasks.max-size:50}")
private int aiMaxSize;
@Value("${app.thread-pool.ai-tasks.queue-capacity:200}")
private int aiQueueCapacity;
/**
* AI 任务专用线程池
* 为什么要独立线程池?
* 1. 防止 AI 调用(高延迟)阻塞普通业务请求
* 2. 方便单独监控 AI 任务的队列积压情况
* 3. 可以针对 AI 任务设置不同的拒绝策略
*/
@Bean("aiTaskExecutor")
public ThreadPoolTaskExecutor aiTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(aiCoreSize);
executor.setMaxPoolSize(aiMaxSize);
executor.setQueueCapacity(aiQueueCapacity);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("ai-task-");
// 拒绝策略:调用者直接执行,不抛异常
// 这比默认的 AbortPolicy 更友好,不会让请求直接 500
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待任务完成再关闭,防止应用重启时 AI 请求被强制中断
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
@Bean("embeddingTaskExecutor")
public ThreadPoolTaskExecutor embeddingTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("embed-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}重试配置代码
AI API 的 rate limit 和临时故障是常态,重试逻辑必须有。但要注意:不是所有错误都该重试。
@Configuration
public class RetryConfig {
/**
* AI 调用的重试模板
* 关键点:只重试可恢复的错误,不重试客户端错误(4xx,除了429)
*/
@Bean("aiRetryTemplate")
public RetryTemplate aiRetryTemplate() {
return RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(
Duration.ofSeconds(1), // 初始等待 1 秒
2.0, // 每次乘以 2
Duration.ofSeconds(10) // 最长等待 10 秒
)
// 只在这些异常上重试
.retryOn(Arrays.asList(
ResourceAccessException.class, // 网络问题
HttpServerErrorException.class, // 5xx 服务器错误
HttpClientErrorException.TooManyRequests.class // 429 限流
))
// 这些异常不重试,直接抛出去
.notRetryOn(Arrays.asList(
HttpClientErrorException.BadRequest.class, // 400 请求有问题,重试没用
HttpClientErrorException.Unauthorized.class, // 401 认证失败,重试没用
HttpClientErrorException.Forbidden.class // 403 权限不足,重试没用
))
.withListener(new RetryListenerSupport() {
@Override
public <T, E extends Throwable> void onError(
RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.warn("AI 调用第 {} 次失败: {}",
context.getRetryCount(), throwable.getMessage());
}
})
.build();
}
}使用示例:
@Service
public class AiChatService {
@Autowired
private ChatClient chatClient;
@Autowired
@Qualifier("aiRetryTemplate")
private RetryTemplate retryTemplate;
public String chat(String userMessage) {
return retryTemplate.execute(context -> {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
});
}
}超时配置代码
Spring Boot 3 + Spring AI 的超时配置有点绕,直接给你能用的版本。
@Configuration
public class AiClientConfig {
/**
* 自定义 RestClient,覆盖 Spring AI 默认的超时设置
* Spring AI 默认超时太短,必须手动覆盖
*/
@Bean
public RestClient.Builder restClientBuilder() {
return RestClient.builder()
.requestFactory(clientHttpRequestFactory());
}
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
// 连接超时:等待建立连接的最长时间
// 10 秒够了,连不上就是网络问题,快速失败
factory.setConnectTimeout(Duration.ofSeconds(10));
// 读取超时:等待服务端响应的最长时间
// 这个必须设长,120 秒是我实际用下来比较稳的值
// 遇到超长文本生成可以调到 180 秒
factory.setReadTimeout(Duration.ofSeconds(120));
// 连接池设置(重要!不设的话每次都新建连接,性能差)
PoolingHttpClientConnectionManager connectionManager =
PoolingHttpClientConnectionManager.create();
connectionManager.setMaxTotal(100);
connectionManager.setDefaultMaxPerRoute(20);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
// 连接在连接池里最长空闲多久
.evictIdleConnections(TimeValue.ofSeconds(60))
.build();
factory.setHttpClient(httpClient);
return factory;
}
}Actuator 指标:监控 AI 调用必须看的几个
配置好之后,Prometheus + Grafana 里重点看这几个指标:
# AI 调用延迟(最重要)
spring_ai_chat_client_operation_seconds_bucket
# AI 调用成功/失败次数
spring_ai_chat_client_operation_seconds_count
# 向量检索延迟
spring_ai_vectorstore_operation_seconds_bucket
# 线程池状态(判断是否需要扩容)
executor_pool_size{name="aiTaskExecutor"}
executor_queue_size{name="aiTaskExecutor"}
executor_active_count{name="aiTaskExecutor"}给你一个 Grafana 里常用的报警规则参考:
# Prometheus 报警规则
groups:
- name: ai-service
rules:
- alert: AICallHighLatency
expr: histogram_quantile(0.95, spring_ai_chat_client_operation_seconds_bucket) > 30
for: 5m
annotations:
summary: "AI 调用 P95 延迟超过 30 秒"
- alert: AITaskQueueFull
expr: executor_queue_size{name="aiTaskExecutor"} > 150
for: 2m
annotations:
summary: "AI 任务队列积压超过 150,考虑扩容"几个容易忽略的细节
1. 不要用 @Async 默认线程池处理 AI 任务
Spring 默认的 @Async 用的是 SimpleAsyncTaskExecutor,每次创建新线程,不回收,不限制数量。并发一上来直接 OOM。必须指定自定义线程池:
@Async("aiTaskExecutor") // 必须指定,不能省略
public CompletableFuture<String> asyncChat(String message) {
// ...
}2. 环境变量和配置分离
API Key 绝对不能硬编码,也不能进代码库。用环境变量:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY} # 从环境变量读生产环境配合 K8s Secret 或 AWS Secrets Manager 用。
3. 不同环境用不同配置文件
application.yml # 公共配置
application-dev.yml # 开发环境:日志 DEBUG,超时可以短
application-staging.yml # 预发布:接近生产配置
application-prod.yml # 生产环境:严格按上面的配置4. 流式响应的超时不一样
如果用 streaming 模式,超时的计算方式不同。要配置的是"第一个 token 的等待时间",不是整个响应的时间:
// 流式调用时,超时设置要另外处理
chatClient.prompt()
.user(message)
.stream()
.content()
// 响应式超时:等待第一个元素不超过 15 秒
.timeout(Duration.ofSeconds(15));这套配置能解决什么问题
我给几个真实数字,用这套配置前后的对比:
| 指标 | 配置前 | 配置后 |
|---|---|---|
| 高并发下超时报错率 | 8% | <0.5% |
| 日志磁盘使用(每天) | 50GB | 2GB |
| 线程池满时拒绝请求数 | 大量 | 极少(CallerRunsPolicy 兜底) |
| P99 响应时间可观测性 | 无法监控 | Grafana 直接看 |
配置这件事不性感,没人愿意写文章讲。但它是 AI 应用能不能稳定跑的基础。业务代码写得再好,配置层烂了,上线就翻车。
先把这套配置设好,后面的事才能顺。
