前言
Spring AI 是 Spring 官方打造的 AI 应用开发框架,旨在将 AI 能力无缝集成到 Spring 生态系统中。它侧重于提供构建 AI 应用所需的底层原子能力抽象,让 Java 开发者可以像使用其他 Spring 组件一样,轻松地与大语言模型进行交互。
一、Spring AI 核心能力
| 能力 |
说明 |
应用场景 |
| 模型通信 (ChatClient) |
统一接口与不同 LLM 对话 |
简历评分、问答生成 |
| 提示词 (Prompt) |
结构化管理发送给模型的提示词 |
.st 模板文件管理 |
| RAG (检索增强生成) |
通过 VectorStore 实现 RAG 模式 |
知识库问答 |
| 工具调用 (Function Calling) |
模型调用 Java 应用中定义的方法 |
Agent |
| 记忆 (ChatMemory) |
管理多轮对话的上下文历史 |
会话管理 |
二、框架对比与选型
2.1 Java AI 框架对比
| 框架 |
特点 |
JDK 基线 |
适用场景 |
| Spring AI |
Spring 官方,原生集成 |
JDK 17+(建议 JDK 21) |
Spring 项目首选 |
| LangChain4j |
功能全面,更新快 |
JDK 8+ |
老项目,兼容性需求 |
| Solon-AI |
轻量化,性能优秀 |
JDK 8+ |
Serverless、GraalVM |
| Agent-Flex |
专注于 Agent 编排 |
JDK 11/17+ |
Agent 编排需求 |
2.2 为什么选择 Spring AI
- 无缝集成:与现有 Spring Boot 技术栈完美融合,学习成本低
- 高度抽象:统一的、与具体模型无关的 API,轻松切换模型
- 生态整合:整合向量数据库、ETL 框架等 AI 应用开发工具链
- 虚拟线程支持:全面拥抱 Java 21 虚拟线程,高并发处理能力强
三、项目依赖配置
3.1 添加依赖
1
2
3
4
5
6
7
|
// build.gradle
dependencies {
// Spring AI 2.0 - OpenAI兼容模式 (阿里云DashScope)
implementation "org.springframework.ai:spring-ai-starter-model-openai:${spring-ai.version}"
// Spring AI 2.0 - PostgreSQL Vector Store (pgvector)
implementation "org.springframework.ai:spring-ai-starter-vector-store-pgvector:${spring-ai.version}"
}
|
3.2 配置属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
# application.yml
spring:
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${AI_BAILIAN_API_KEY}
chat:
options:
model: ${AI_MODEL:qwen-plus}
temperature: 0.2
# Embedding模型配置
embedding:
options:
model: text-embedding-v3
# 禁用自动重试机制,让异常立即返回
retry:
max-attempts: 1
on-client-errors: false
# PostgreSQL Vector Store配置
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1024
initialize-schema: true
remove-existing-vector-store-table: false
|
3.3 关键配置说明
| 配置项 |
说明 |
base-url |
阿里云 DashScope 的 OpenAI 兼容端点 |
api-key |
通过环境变量注入,严禁硬编码 |
temperature |
控制输出随机性(0-1),结构化输出使用 0.2 更稳定 |
dimensions |
text-embedding-v3 的向量维度为 1024 |
initialize-schema |
开发环境设为 true 自动创建表 |
四、ChatClient 大模型调用
4.1 服务类设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Service
public class ResumeGradingService {
private final ChatClient chatClient;
private final PromptTemplate systemPromptTemplate;
private final PromptTemplate userPromptTemplate;
private final BeanOutputConverter<ResumeAnalysisResponseDTO> outputConverter;
public ResumeGradingService(
ChatClient.Builder chatClientBuilder,
@Value("classpath:prompts/resume-analysis-system.st") Resource systemPromptResource,
@Value("classpath:prompts/resume-analysis-user.st") Resource userPromptResource) {
this.chatClient = chatClientBuilder.build();
this.systemPromptTemplate = new PromptTemplate(systemPromptResource.getContentAsString(StandardCharsets.UTF_8));
this.userPromptTemplate = new PromptTemplate(userPromptResource.getContentAsString(StandardCharsets.UTF_8));
this.outputConverter = new BeanOutputConverter<>(ResumeAnalysisResponseDTO.class);
}
}
|
4.2 调用大模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public ResumeAnalysisResponse analyzeResume(String resumeText) {
// 1. 加载系统提示词
String systemPrompt = systemPromptTemplate.render();
// 2. 加载用户提示词并填充变量
Map<String, Object> variables = new HashMap<>();
variables.put("resumeText", resumeText);
String userPrompt = userPromptTemplate.render(variables);
// 3. 添加格式指令到系统提示词
String systemPromptWithFormat = systemPrompt + "\n\n" + outputConverter.getFormat();
// 4. 调用AI
ResumeAnalysisResponseDTO dto = chatClient.prompt()
.system(systemPromptWithFormat)
.user(userPrompt)
.call()
.entity(outputConverter);
// 5. 转换为业务对象
return convertToResponse(dto, resumeText);
}
|
4.3 调用流程图
1
2
3
4
5
6
7
8
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 加载提示词 │────▶│ 填充变量 │────▶│ 调用 ChatClient│
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 转换业务对象 │◀────│ 解析响应 │◀────│ 获取 AI 响应 │
└─────────────┘ └─────────────┘ └─────────────┘
|
五、结构化输出
5.1 Java Record 定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 中间DTO用于接收AI响应
private record ResumeAnalysisResponseDTO(
int overallScore,
ScoreDetailDTO scoreDetail,
String summary,
List<String> strengths,
List<SuggestionDTO> suggestions
) {}
private record ScoreDetailDTO(
int contentScore,
int structureScore,
int skillMatchScore,
int expressionScore,
int projectScore
) {}
private record SuggestionDTO(
String category,
String priority,
String issue,
String recommendation
) {}
|
5.2 结构化输出重试策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public <T> T invokeWithRetry(ChatClient chatClient, String systemPrompt,
String userPrompt, BeanOutputConverter<T> converter) {
int maxAttempts = 2;
Exception lastError = null;
for (int i = 0; i < maxAttempts; i++) {
try {
return chatClient.prompt()
.system(systemPrompt + "\n\n" + converter.getFormat())
.user(userPrompt)
.call()
.entity(converter);
} catch (Exception e) {
lastError = e;
// 重试时注入上次错误信息
userPrompt += "\n\n注意:上次输出解析失败,错误原因:" + e.getMessage();
}
}
throw new BusinessException(ErrorCode.AI_RESPONSE_PARSING_FAILED, lastError);
}
|
5.3 处理流水线
| 阶段 |
说明 |
失败处理 |
| 生成 |
LLM 输出 JSON |
- |
| 解析 |
Jackson 反序列化 |
进入修复阶段 |
| 修复 |
将错误信息发回 LLM 要求修正 |
达最大次数则返回错误 |
| 校验 |
JSR-380 Bean Validation |
进入修复阶段 |
六、流式输出 SSE
6.1 SSE 简介
SSE(Server-Sent Events) 是一种基于 HTTP 的服务器推送技术,允许服务器向客户端单向发送事件流。
6.2 SSE vs WebSocket
| 维度 |
SSE |
WebSocket |
| 通信方式 |
单向(服务端→客户端) |
全双工 |
| 协议 |
基于 HTTP |
独立协议 |
| 实现复杂度 |
低 |
高 |
| 自动重连 |
支持 |
需自己实现 |
| 二进制数据 |
需编码 |
原生支持 |
| 适用场景 |
AI 流式生成、实时通知 |
聊天室、协作编辑 |
6.3 SSE 协议格式
1
2
3
4
5
6
7
|
data: 第一行内容
data: 第二行内容
data: 第三行内容
id: 事件ID
event: 自定义事件类型
retry: 3000
|
| 字段 |
说明 |
data |
具体的业务数据 |
event |
自定义事件类型 |
id |
事件 ID,断线重连时使用 |
retry |
重连间隔(毫秒) |
6.4 Spring Boot SSE 实现
1
2
3
4
5
6
7
8
9
10
|
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamData() {
return Flux.interval(Duration.ofMillis(500))
.map(seq -> ServerSentEvent.<String>builder()
.id(String.valueOf(seq))
.event("message")
.data("消息 " + seq)
.retry(3000L)
.build());
}
|
6.5 Spring AI 流式调用
1
2
3
4
5
6
7
8
9
10
11
|
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamChat(@RequestParam String question) {
return chatClient.prompt()
.system(systemPrompt)
.user(question)
.stream()
.content()
.map(chunk -> ServerSentEvent.<String>builder()
.data(chunk.replace("\n", "\\n").replace("\r", "\\r"))
.build());
}
|
6.6 SSE 格式转义处理
1
2
3
4
5
6
7
|
// 后端转义
.map(chunk -> ServerSentEvent.<String>builder()
.data(chunk.replace("\n", "\\n").replace("\r", "\\r"))
.build())
// 前端反向转义
const text = chunk.replace(/\\n/g, '\n').replace(/\\r/g, '\r');
|
6.7 错误处理与超时控制
1
2
3
4
5
6
|
return responseFlux
.timeout(Duration.ofSeconds(30))
.onErrorResume(TimeoutException.class, e -> {
log.error("流式输出超时", e);
return Flux.just("【错误】回答生成超时,请缩短问题或稍后重试。");
});
|
6.8 虚拟线程配置
1
2
3
4
|
spring:
threads:
virtual:
enabled: true # 启用虚拟线程
|
虚拟线程 vs 平台线程:
| 场景 |
平台线程 |
虚拟线程 |
| 200 并发 SSE |
线程池满,排队 |
轻松处理 |
| AI 调用等待 3 秒 |
线程阻塞,占用资源 |
自动挂起,让出资源 |
| 10000 并发请求 |
拒绝服务 |
正常处理 |
七、Function Calling
7.1 Function Calling 原理
Function Calling 允许 LLM 调用我们定义的 Java 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultFunction("getWeather", new GetWeatherFunction())
.build();
}
public class GetWeatherFunction implements Function<WeatherRequest, WeatherResponse> {
@Override
public WeatherResponse apply(WeatherRequest request) {
// 调用天气 API
return weatherService.getWeather(request.getCity());
}
}
|
7.2 Prompt 模板定义
1
2
3
4
5
6
|
# System Prompt
你是一个专业的助手。当用户询问天气时,
请调用 getWeather 函数获取实时天气信息。
# User Prompt
{userQuestion}
|
7.3 处理流程
1
|
用户输入 → LLM 判断需要调用函数 → 执行函数 → 返回结果 → LLM 生成最终回答
|
八、提示词模板管理
8.1 .st 模板文件
1
2
3
4
5
6
7
8
|
# interview-question-system.st
你是一个专业的面试官。请根据候选人的简历内容,
生成针对性的面试问题。
## 要求
1. 问题要基于简历中的具体经历
2. 每个主问题需要生成追问
3. 问题难度要适中,考察真实能力
|
8.2 变量注入
1
2
3
4
5
6
|
Map<String, Object> variables = new HashMap<>();
variables.put("resumeText", resumeText);
variables.put("questionCount", 10);
variables.put("followUpCount", 2);
String userPrompt = userPromptTemplate.render(variables);
|
8.3 安全注入
用户输入注入模板前需清洗特殊符号,防止 Prompt Injection:
1
2
3
4
5
6
|
public String sanitizeUserInput(String input) {
// 移除可能破坏 Prompt 结构的符号
return input.replace("<", "<")
.replace(">", ">")
.replace("\"", """);
}
|
九、常见问题与解决方案
| 问题 |
原因 |
解决方案 |
| JSON 解析失败 |
低温导致格式不稳定 |
升温至 0.3 + Retry 闭环 |
| 流式输出中断 |
网络波动 / 超时 |
添加超时控制 + 前端重连 |
| 响应太慢 |
上下文太长 |
减少输入 Token 或分批处理 |
| 429 限流 |
请求频率过高 |
添加限流器 + 指数退避 |
| 线程池耗尽 |
同步调用阻塞 |
启用虚拟线程 + 异步处理 |
十、总结
Spring AI 提供了企业级 AI 应用开发所需的核心能力:
- 统一抽象:一套代码支持多种 LLM,切换模型只需改配置
- 结构化输出:BeanOutputConverter 实现 LLM 输出到 Java 对象的自动映射
- 流式响应:SSE 实现"边生成边展示"的打字机效果
- 提示词管理:.st 模板文件实现提示词的外置化管理
- 高并发支持:Java 21 虚拟线程轻松应对高并发场景