Spring Boot 3 + Spring AI 实战:十分钟集成 OpenAI API 构建智能应用
-
- 🚀 前言:为什么选择 Spring AI?
- 🧠 一、Spring AI 核心架构与原理
-
- 1.1 整体架构原理图
- 1.2 AI 交互流程图
- ⚙️ 二、环境准备与项目初始化
-
- 2.1 技术栈要求
- 2.2 Maven 依赖配置
- 2.3 配置文件设置
- 💬 三、快速入门:你的第一个 AI 对话机器人
-
- 3.1 创建 Service 层
- 3.2 创建 Controller 层
- 🚀 四、进阶实战:提示词工程与流式响应
-
- 4.1 提示词模板
- 4.2 流式响应
- 🔍 五、核心应用:RAG(检索增强生成)实战
-
- 5.2 Spring AI 实现 RAG
- 🛠️ 六、Function Calling:赋予 AI 调用 Java 方法的能力
-
- 6.1 Function Calling 流程图
- 6.2 实战:查询天气机器人
- 🛡️ 七、生产环境最佳实践
-
- 7.1 安全性
- 7.2 Token 计费控制
- 7.3 异常处理
- 7.4 性能优化
- 🎯 八、总结
摘要:随着生成式 AI 的爆发,Java 开发者不再需要羡慕 Python 的丰富生态。Spring AI 作为 Spring 家族的新成员,为 Java 生态带来了类似 LangChain 的标准化抽象。本文将基于 Spring Boot 3,深入剖析 Spring AI 的核心架构,通过实战案例带你十分钟集成 OpenAI API,构建具备流式输出、提示词模板甚至 RAG(检索增强生成)能力的智能应用。
🚀 前言:为什么选择 Spring AI?
在过去的一年里,如果你想用 Java 开发 AI 应用,可能需要直接调用 OpenAI 的 HTTP 接口,或者移植 Python 的 LangChain 库。这不仅繁琐,而且缺乏 Java 生态的“味道”。 Spring AI 的诞生改变了这一切。它并非 LangChain 的简单移植,而是遵循 Spring 设计哲学(可移植性、模块化、面向接口)构建的 AI 应用开发框架。它提供了以下核心价值:
🧠 一、Spring AI 核心架构与原理
在写代码之前,理解 Spring AI 的架构设计至关重要,这有助于我们理解它的扩展性。
1.1 整体架构原理图
Spring AI 将 AI 模型交互抽象为多个层次,从底层的 HTTP 通信到上层的 POJO 转换。
#mermaid-svg-AOAml1yD54pv6wcZ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AOAml1yD54pv6wcZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AOAml1yD54pv6wcZ .error-icon{fill:#552222;}#mermaid-svg-AOAml1yD54pv6wcZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AOAml1yD54pv6wcZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AOAml1yD54pv6wcZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AOAml1yD54pv6wcZ .marker.cross{stroke:#333333;}#mermaid-svg-AOAml1yD54pv6wcZ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AOAml1yD54pv6wcZ p{margin:0;}#mermaid-svg-AOAml1yD54pv6wcZ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster-label text{fill:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster-label span{color:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster-label span p{background-color:transparent;}#mermaid-svg-AOAml1yD54pv6wcZ .label text,#mermaid-svg-AOAml1yD54pv6wcZ span{fill:#333;color:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .node rect,#mermaid-svg-AOAml1yD54pv6wcZ .node circle,#mermaid-svg-AOAml1yD54pv6wcZ .node ellipse,#mermaid-svg-AOAml1yD54pv6wcZ .node polygon,#mermaid-svg-AOAml1yD54pv6wcZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AOAml1yD54pv6wcZ .rough-node .label text,#mermaid-svg-AOAml1yD54pv6wcZ .node .label text,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape .label,#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-AOAml1yD54pv6wcZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AOAml1yD54pv6wcZ .rough-node .label,#mermaid-svg-AOAml1yD54pv6wcZ .node .label,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape .label,#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape .label{text-align:center;}#mermaid-svg-AOAml1yD54pv6wcZ .node.clickable{cursor:pointer;}#mermaid-svg-AOAml1yD54pv6wcZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AOAml1yD54pv6wcZ .arrowheadPath{fill:#333333;}#mermaid-svg-AOAml1yD54pv6wcZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AOAml1yD54pv6wcZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AOAml1yD54pv6wcZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AOAml1yD54pv6wcZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AOAml1yD54pv6wcZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AOAml1yD54pv6wcZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AOAml1yD54pv6wcZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster text{fill:#333;}#mermaid-svg-AOAml1yD54pv6wcZ .cluster span{color:#333;}#mermaid-svg-AOAml1yD54pv6wcZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AOAml1yD54pv6wcZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AOAml1yD54pv6wcZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape p,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AOAml1yD54pv6wcZ .icon-shape rect,#mermaid-svg-AOAml1yD54pv6wcZ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AOAml1yD54pv6wcZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AOAml1yD54pv6wcZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AOAml1yD54pv6wcZ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
基础设施与外部服务
模型提供商适配层
Spring AI 抽象层
应用层
Spring Boot Application
Chat Client / Image Client
Prompt Templates
Output Parser
Embedding Model
OpenAI Adapter
Azure Adapter
HuggingFace Adapter
OpenAI API
Vector Databasee.g. Redis/PGVector
核心组件解析:
- Client 层:最顶层的入口,如 ChatClient,开发者通过它与 AI 交互。
- Prompt 层:负责将用户输入、系统指令和变量组装成 AI 能理解的格式。
- Model 层:具体的模型实现,负责将 Prompt 发送给 API 并解析响应。
1.2 AI 交互流程图
当我们调用 chatClient.call() 时,Spring AI 内部发生了什么?
Converter
OpenAI HTTP API
Converter Jackson/JSON
Prompt Template
ChatClient
Spring Boot App
Converter
OpenAI HTTP API
Converter Jackson/JSON
Prompt Template
ChatClient
Spring Boot App
#mermaid-svg-7MFetsnPb1s8BG24{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7MFetsnPb1s8BG24 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7MFetsnPb1s8BG24 .error-icon{fill:#552222;}#mermaid-svg-7MFetsnPb1s8BG24 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7MFetsnPb1s8BG24 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7MFetsnPb1s8BG24 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7MFetsnPb1s8BG24 .marker.cross{stroke:#333333;}#mermaid-svg-7MFetsnPb1s8BG24 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7MFetsnPb1s8BG24 p{margin:0;}#mermaid-svg-7MFetsnPb1s8BG24 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7MFetsnPb1s8BG24 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7MFetsnPb1s8BG24 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-7MFetsnPb1s8BG24 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-7MFetsnPb1s8BG24 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-7MFetsnPb1s8BG24 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-7MFetsnPb1s8BG24 .sequenceNumber{fill:white;}#mermaid-svg-7MFetsnPb1s8BG24 #sequencenumber{fill:#333;}#mermaid-svg-7MFetsnPb1s8BG24 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-7MFetsnPb1s8BG24 .messageText{fill:#333;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7MFetsnPb1s8BG24 .labelText,#mermaid-svg-7MFetsnPb1s8BG24 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .loopText,#mermaid-svg-7MFetsnPb1s8BG24 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7MFetsnPb1s8BG24 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7MFetsnPb1s8BG24 .noteText,#mermaid-svg-7MFetsnPb1s8BG24 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-7MFetsnPb1s8BG24 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7MFetsnPb1s8BG24 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7MFetsnPb1s8BG24 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7MFetsnPb1s8BG24 .actorPopupMenu{position:absolute;}#mermaid-svg-7MFetsnPb1s8BG24 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-7MFetsnPb1s8BG24 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7MFetsnPb1s8BG24 .actor-man circle,#mermaid-svg-7MFetsnPb1s8BG24 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-7MFetsnPb1s8BG24 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
.prompt().user("…").call()
组装 Prompt (System/User)
String / ChatRequest
序列化为 JSON
JSON Body
HTTP POST (/v1/chat/completions)
JSON Response
反序列化为 ChatResponse
ChatResponse 对象
返回 Entity / String
⚙️ 二、环境准备与项目初始化
2.1 技术栈要求
硬性要求:
- JDK 17+:Spring Boot 3 强制要求。
- Spring Boot 3.2.x+:确保兼容性。
- OpenAI API Key:你需要一个密钥。
2.2 Maven 依赖配置
Spring AI 的版本更新较快,建议使用 BOM(Bill of Materials)管理版本。
<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>
<dependencies>
<!– Spring AI OpenAI Starter –>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!– Web Starter –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
注意:由于 Spring AI 尚未放入 Maven 中央仓库的 release 版本,你需要确保添加了 Spring Milestone 或 Snapshot 仓库。
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
2.3 配置文件设置
在 application.yml 中配置 API Key 和基础选项:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY} # 建议使用环境变量
chat:
options:
model: gpt–4–turbo # 或 gpt-3.5-turbo
temperature: 0.7 # 控制随机性,0.0-2.0
💬 三、快速入门:你的第一个 AI 对话机器人
我们将构建一个简单的 REST API,接收用户问题并返回 AI 回答。
3.1 创建 Service 层
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public String chat(String message) {
// 简单调用
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
代码解析:
- ChatClient.Builder:Spring Boot 自动配置注入的构建器。
- .prompt():开始构建对话请求。
- .user():添加用户消息。
- .call():同步阻塞调用(等待完整响应)。
- .content():提取返回文本内容。
3.2 创建 Controller 层
@RestController
@RequestMapping("/api/ai")
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
@GetMapping("/chat")
public Map<String, String> chat(@RequestParam String message) {
String response = chatService.chat(message);
return Map.of("answer", response);
}
}
运行项目,访问 http://localhost:8080/api/ai/chat?message=Hello%20Spring%20AI,你将看到 AI 的回复。
🚀 四、进阶实战:提示词工程与流式响应
简单的字符串拼接无法胜任复杂业务。我们需要提示词模板来管理上下文,并使用流式传输来提升用户体验。
4.1 提示词模板
假设我们要做一个“写诗助手”,需要固定的角色设定。 创建 prompts/poet-template.st 文件(放在 src/main/resources 下):
你是一位古代诗人,擅长唐诗宋词。
请根据以下主题:{topic},创作一首七言绝句。
要求:
1. 韵律工整
2. 意境优美
3. 字数严格
Service 代码升级:
@Service
public class PoetService {
private final ChatClient chatClient;
// Spring AI 自动加载 resources 目录下的 .st 文件
public PoetService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultSystem("你是一位专业的文学创作助手") // 默认系统提示
.build();
}
public String writePoem(String topic) {
return chatClient.prompt()
.user(userSpec -> userSpec
.text("请根据主题:{topic} 写一首诗") // 内联模板
.param("topic", topic)
)
.call()
.content();
}
}
4.2 流式响应
流式响应让用户感觉像是在实时打字,而不是等待几秒钟后一次性蹦出一段文字。这在 Web 界面中尤为重要。 流式接口原理图:
#mermaid-svg-YMN60JdmtVtlwVjk{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YMN60JdmtVtlwVjk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YMN60JdmtVtlwVjk .error-icon{fill:#552222;}#mermaid-svg-YMN60JdmtVtlwVjk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YMN60JdmtVtlwVjk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YMN60JdmtVtlwVjk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YMN60JdmtVtlwVjk .marker.cross{stroke:#333333;}#mermaid-svg-YMN60JdmtVtlwVjk svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YMN60JdmtVtlwVjk p{margin:0;}#mermaid-svg-YMN60JdmtVtlwVjk .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster-label text{fill:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster-label span{color:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster-label span p{background-color:transparent;}#mermaid-svg-YMN60JdmtVtlwVjk .label text,#mermaid-svg-YMN60JdmtVtlwVjk span{fill:#333;color:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .node rect,#mermaid-svg-YMN60JdmtVtlwVjk .node circle,#mermaid-svg-YMN60JdmtVtlwVjk .node ellipse,#mermaid-svg-YMN60JdmtVtlwVjk .node polygon,#mermaid-svg-YMN60JdmtVtlwVjk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YMN60JdmtVtlwVjk .rough-node .label text,#mermaid-svg-YMN60JdmtVtlwVjk .node .label text,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape .label,#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape .label{text-anchor:middle;}#mermaid-svg-YMN60JdmtVtlwVjk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YMN60JdmtVtlwVjk .rough-node .label,#mermaid-svg-YMN60JdmtVtlwVjk .node .label,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape .label,#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape .label{text-align:center;}#mermaid-svg-YMN60JdmtVtlwVjk .node.clickable{cursor:pointer;}#mermaid-svg-YMN60JdmtVtlwVjk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YMN60JdmtVtlwVjk .arrowheadPath{fill:#333333;}#mermaid-svg-YMN60JdmtVtlwVjk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YMN60JdmtVtlwVjk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YMN60JdmtVtlwVjk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YMN60JdmtVtlwVjk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YMN60JdmtVtlwVjk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YMN60JdmtVtlwVjk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YMN60JdmtVtlwVjk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster text{fill:#333;}#mermaid-svg-YMN60JdmtVtlwVjk .cluster span{color:#333;}#mermaid-svg-YMN60JdmtVtlwVjk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YMN60JdmtVtlwVjk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YMN60JdmtVtlwVjk rect.text{fill:none;stroke-width:0;}#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape p,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YMN60JdmtVtlwVjk .icon-shape rect,#mermaid-svg-YMN60JdmtVtlwVjk .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YMN60JdmtVtlwVjk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YMN60JdmtVtlwVjk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YMN60JdmtVtlwVjk :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
SSE Request
Flux
Async HTTP
Chunks
Chunks
SSE Events
Client
Spring Controller
ChatClient
OpenAI API
代码实现: Spring AI 的 stream() 方法返回的是 Reactor 的 Flux<String>,这可以完美适配 Spring WebFlux 或者 Spring MVC 的流式响应。
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream() // 关键:开启流式
.content(); // 返回 Flux<String>
}
前端测试体验: 使用 curl 测试可以看到数据一行行吐出:
curl -N http://localhost:8080/api/ai/stream?message=讲个笑话
🔍 五、核心应用:RAG(检索增强生成)实战
大模型有两个致命弱点:知识幻觉(瞎编乱造)和知识截止日期(不知道今天发生的事)。 RAG (Retrieval-Augmented Generation) 是解决这些问题的银弹:先去外部知识库检索相关信息,然后扔给 LLM 让它基于这些信息回答。
5.2 Spring AI 实现 RAG
我们需要 EmbeddingModel(将文本转向量)和 VectorStore(存向量)。为了简化演示,我们使用基于内存的向量存储(生产环境建议用 PgVector 或 Milvus)。 添加依赖:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
</dependency>
<!– 注意:Transformers 比较重,适合本地演示。生产环境建议用 OpenAI 的 Embedding API –>
RAG Service 代码:
@Service
public class RagService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public RagService(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
this.chatClient = chatClientBuilder.build();
this.vectorStore = vectorStore;
// 初始化时加载一些文档
loadKnowledgeBase();
}
private void loadKnowledgeBase() {
List<Document> documents = List.of(
new Document("Spring Boot 3 最低要求 JDK 17。"),
new Document("Spring AI 支持函数调用,可以连接外部 API。"),
new Document("GraalVM 可以将 Java 应用编译为本地镜像,实现毫秒级启动。")
);
vectorStore.add(documents);
}
public String ask(String question) {
// 1. 检索相关文档
List<Document> similarDocs = vectorStore.similaritySearch(SearchRequest.query(question).withTopK(2));
// 2. 构建包含上下文的 Prompt
String context = similarDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\\n"));
String systemPrompt = """
你是一个助手。请仅使用以下提供的上下文信息来回答用户的问题。
如果上下文中不包含答案,请说"我不知道"。
上下文:
{context}
""";
// 3. 发送给 LLM
return chatClient.prompt()
.system(systemPrompt)
.user(question)
.call()
.content();
}
}
关键点解析:
🛠️ 六、Function Calling:赋予 AI 调用 Java 方法的能力
这是 AI 应用最强大的功能之一。AI 不再只是“聊天机器人”,它可以理解意图并调用后端业务逻辑,比如“查询天气”、“下单”。
6.1 Function Calling 流程图
Java Function
Spring AI
LLM
用户
Java Function
Spring AI
LLM
用户
#mermaid-svg-aNDcoXXGcYgRWlaD{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aNDcoXXGcYgRWlaD .error-icon{fill:#552222;}#mermaid-svg-aNDcoXXGcYgRWlaD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aNDcoXXGcYgRWlaD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aNDcoXXGcYgRWlaD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aNDcoXXGcYgRWlaD .marker.cross{stroke:#333333;}#mermaid-svg-aNDcoXXGcYgRWlaD svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aNDcoXXGcYgRWlaD p{margin:0;}#mermaid-svg-aNDcoXXGcYgRWlaD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aNDcoXXGcYgRWlaD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aNDcoXXGcYgRWlaD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD .sequenceNumber{fill:white;}#mermaid-svg-aNDcoXXGcYgRWlaD #sequencenumber{fill:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-aNDcoXXGcYgRWlaD .messageText{fill:#333;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aNDcoXXGcYgRWlaD .labelText,#mermaid-svg-aNDcoXXGcYgRWlaD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .loopText,#mermaid-svg-aNDcoXXGcYgRWlaD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aNDcoXXGcYgRWlaD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-aNDcoXXGcYgRWlaD .noteText,#mermaid-svg-aNDcoXXGcYgRWlaD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-aNDcoXXGcYgRWlaD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aNDcoXXGcYgRWlaD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aNDcoXXGcYgRWlaD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aNDcoXXGcYgRWlaD .actorPopupMenu{position:absolute;}#mermaid-svg-aNDcoXXGcYgRWlaD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-aNDcoXXGcYgRWlaD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aNDcoXXGcYgRWlaD .actor-man circle,#mermaid-svg-aNDcoXXGcYgRWlaD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-aNDcoXXGcYgRWlaD :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
帮我查询北京现在的天气
识别意图 ->> 需要调用 getWeather(city)
返回 Function Call 指令
执行 getWeather("Beijing")
返回 "晴, 25度"
将结果作为上下文再次请求
北京现在天气晴朗,气温25度
6.2 实战:查询天气机器人
步骤 1:定义 Java Function
public record WeatherRequest(String city) {}
public record WeatherResponse(String city, double temperature, String condition) {}
@Component
public class WeatherService {
@Bean
public Function<WeatherRequest, WeatherResponse> weatherFunction() {
return request -> {
// 模拟调用第三方天气 API
if ("Beijing".equalsIgnoreCase(request.city())) {
return new WeatherResponse("Beijing", 25.5, "Sunny");
}
return new WeatherResponse(request.city(), 0, "Unknown");
};
}
}
步骤 2:启用 Function Calling
String response = chatClient.prompt()
.user("北京现在天气怎么样?")
.functions("weatherFunction") // 指定 Bean 名称
.call()
.content();
原理: Spring AI 会自动扫描容器中注册的 Function Bean,并在调用 OpenAI 时将函数的 JSON Schema 发送给模型。如果模型判断需要调用该函数,它会暂停生成,返回一个工具调用指令,Spring AI 拦截该指令,执行 Java 方法,将结果再次喂给模型,模型最后组织语言回复用户。
🛡️ 七、生产环境最佳实践
将 Demo 级应用投入生产,需要注意以下几点:
7.1 安全性
绝对不要将 API Key 提交到 Git。
- 方案 A:使用环境变量(推荐)。export OPENAI_API_KEY=sk-xxxx...
- 方案 B:使用 Spring Cloud Config 或 Vault。
7.2 Token 计费控制
LLM 按输入和输出的 Token 计费。为了防止用户滥用导致账单爆炸:
.options(OpenAiChatOptions.builder().withMaxTokens(500).build())
...
7.3 异常处理
网络波动或 OpenAI 服务不可用是常态。
try {
return chatClient.prompt().user(msg).call().content();
} catch (OpenAiApiException e) {
if (e.getStatusCode().is5xxServerError()) {
// 服务端错误,重试
return retryLogic(msg);
}
// 其他错误
return "抱歉,AI 服务暂时不可用。";
}
7.4 性能优化
- 连接池配置:Spring AI 底层使用 WebClient 或 RestTemplate,确保配置了适当的连接池大小。
- 缓存:对于常见问题(如 FAQ),可以使用 Redis 缓存 AI 的回答,直接返回,避免重复计费。
🎯 八、总结
通过 Spring Boot 3 和 Spring AI,Java 开发者现在拥有了构建企业级 AI 应用的强力武器。本文涵盖了从基础架构、简单对话、流式交互到复杂的 RAG 和 Function Calling 的完整链路。 技术演进路径: 手工 HTTP 调用 ➡️ Spring AI 标准化 Client ➡️ RAG + Function Calling 的智能 Agent Spring AI 的优势在于它完美融合了 Spring 生态的非侵入式设计理念,让 Java 开发者能专注于业务逻辑,而不是繁琐的 HTTP 编解码。未来的 AI 应用开发,将不仅仅是算法模型的比拼,更是工程化落地能力的较量。 现在,打开你的 IDE,开始构建你的第一个 Spring AI 应用吧!



