一.SpringAI
Spring AI 是 Spring 官方推出的 Java 生成式 AI 开发框架,把大模型、向量库、RAG、函数调用封装进 Spring 生态,让 Java/SpringBoot 项目快速接入 AI 能力,对标 Python的LangChain,不是大模型,是接入大模型的中间件框架Spring。
✨核心特点
- 统一抽象 API,消除厂商锁定一套
ChatClient接口,切换 OpenAI/Ollama/DeepSeek/ 通义千问,只改配置,业务代码不动。 - 深度 Spring 生态融合Starter 自动配置、DI 依赖注入、AOP、可观测性、SpringSecurity,和普通 Spring 业务开发完全一致。
- 完整 AI 能力栈对话 Chat、Embedding 向量、文生图、语音转文字、RAG 检索增强生成、Function Calling 函数调用、结构化输出映射 POJO、Advisor 拦截器(提示词修改、日志、过滤)Spring。
- 支持主流向量数据库Chroma、PGVector、Milvus、RedisVector、Cassandra 等,开箱即用做知识库问
二、本课程环境
2.1 SpringBoot4和Spring AI 2.0
环境基于JDK21、SpringBoot 4.10,该版本集成的Spring AI 2.0是目前最新的生成环境选择。
2.2 利用外部公共平台AI模型
外部公共平台有很多,诸如阿里云百炼,火山方舟,硅基流动
注册登录后,开通后要获取三个重要信息:
第一是访问平台的baseurl:api.siliconflow.cn/v1
第二是申请一个API 密钥,即sk开头的一个字符串,这个API-KEY需要保存为本机环境变量SILICONFLOW_API_KEY,以便后续搭建AI程序使用,如下图所示:
第三是要使用的模型名称,可以在模型广场复制得到
三、起步案例
Spring AI 目前支持以语言、图像和音频形式处理输入和输出的模型。
上表中的最后一行接受文本作为输入并输出数字,通常称为嵌入文本(Embedding Text),用来表示 AI 模型中使用的内部数据结构。Spring AI 提供了对 Embedding 的支持以支持开发更高级的应用场景。
3.1 新建Maven工程
修改pom.xml,增加以下配置
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<!-- Spring AI OpenAI Starter(硅基流动兼容OpenAI格式) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- 可选:Reactor 用于流式输出 -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- SpringBoot通用测试依赖,内置JUnit、AssertJ、MockMvc基础等测试组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- WebMvc专项测试包,针对Controller层测试增强,支持@WebMvcTest轻量化测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
3.2 配置AI模型
在resources中新建application.yml
${SILICONFLOW_API_KEY}是环境变量中配置的硅基流动的API-KEY
以下将以智谱大模型为例
# 服务端口号
server:
port: 8999
# Spring AI 配置
spring:
ai:
openai:
api-key: ${SILICONFLOW_API_KEY}
base-url: https://api.siliconflow.cn/v1
chat:
model: THUDM/GLM-Z1-9B-0414
temperature: 0.7
max-tokens: 1024
# 日志
logging:
level:
org.springframework.ai: debug
org.springframework.web.client: debug
3.2.1 关于配置的特别说明
spring.ai.openai.chat提供了配置多项参数,合理调整这些参数,对提升回答结果的准确性与可用性至关重要,往往需要经过反复调试,才能匹配自身业务场景的最优参数。下面是各大模型服务商通用的核心配置项:
1.温度(Temperature)
简单来说:温度值越低,模型输出结果确定性越强,会优先选择概率最高的下一个字词单元(token) ;调高温度值会提升随机性,生成内容更多样、更富有创造性,本质是拉高了低概率候选字词的选中权重。落地使用上:客观问答等需要严谨事实的场景建议调低温度,保障回答贴合事实、简洁精炼;写诗、文案创作等创意类任务,适合调高温度。
2.核采样阈值(Top P)
该参数搭配温度使用,也叫核采样,用于调控模型输出的确定性。需要精准、客观的标准答案时,调低该数值;想要内容丰富多元,则调高数值。Top P 的规则:模型仅选取累计概率达到top_p阈值的字词集合参与生成。数值越小,模型只选用高置信度词汇;数值越大,模型会纳入更多低概率备选字词,输出内容多样性更强。
通用使用建议:温度、Top P 二选一调整,不建议同时修改两项参数。
3.最大生成长度(Max Length)
通过设置max length限制模型生成的 token 总数,既能避免生成冗长、无关的多余内容,也能有效控制调用成本。
4.停止符(Stop Sequences)
停止符是一段指定字符,当模型生成到该字符时会立刻终止输出,是管控回答篇幅与格式的常用方式。举例:如需限定列表最多 10 条内容,可设置11作为停止标识符。
5.频率惩罚(Frequency Penalty) 该参数会依据字词在提示词和已生成内容里的出现频次施加扣分,出现次数越多,后续再次被选用的惩罚越高。数值越大,词语重复出现的概率越低,用来减少回答里的用词重复问题。
6.存在惩罚(Presence Penalty) 同样用于抑制重复用词,但和频率惩罚逻辑不同:只要字词出现过,无论重复 2 次还是 10 次,统一施加同等惩罚,避免整句、整段内容反复复述。想要创意丰富的文本,可拉高存在惩罚;需要内容聚焦、表述精简时,选用偏低的参数值。 和温度、Top P 搭配原则一致:频率惩罚、存在惩罚建议只调整其中一项,不要同时改动。
3.3 编写控制器
新建类:ChatModelDemo
ChatModel 是 Spring‑AI 底层核心接口(低级 API) ,统一抽象各大厂商对话大模型,屏蔽 OpenAI、通义千问、Ollama、DeepSeek 等底层接口差异。
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatModelDemo {
private final ChatModel chatModel;
public ChatModelDemo(ChatModel chatModel) {
super();
// TODO Auto-generated constructor stub
this.chatModel=chatModel;
}
@GetMapping("/chatmodel")
public String simpleChat() {
// 声明提示词
Prompt prompt=new Prompt("你好呀");
// 获得聊天响应
ChatResponse chatResponse=chatModel.call(prompt);
// 获得生成内容
Generation generation=chatResponse.getResult();
// 提取消息
AssistantMessage assistantMessage=generation.getOutput();
// 从消息中提取文本
String text=assistantMessage.getText();
return text;
}
}
ChatModel是Spring AI框架定义的核心API之一,它是一个接口,提供文本聊天交互模型,支持纯文本格式作为输入,并将模型的输出以格式化文本形式返回。 Prompt类则是用于封装提示词,以便AI能够做出更加精确的回应。
3.4 编写主启动类
新建类:Application
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.5 编写测试类
新建类:AITest
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
public class AITest {
@Autowired
private MockMvc mockMvc;
@Test
void testModel() throws Exception {
mockMvc.perform( get("/chatmodel") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
}
四、基础聊天模型
4.1 ChatModel
ChatModel API 让应用开发者 AI 模型进行文本交互,它抽象了应用与模型交互的过程,包括使用 Prompt 作为输入,使用 ChatResponse 作为输出等。
ChatModel 是底层原始接口,追求可控灵活,但几乎所有上层能力都要自己手写,业务直接用会踩很多生产坑。
开发体验层面
- 样板代码极多,重复劳动大
- 没有内置结构化输出
- 工具调用体验笨重
- 无任何 Memory 能力
4.2 ChatClient
ChatClient 类似于应用程序开发中的服务层,它为应用程序直接提供 AI 服务,开发者可以快速完成一整套 AI 交互流程的组装。
ChatClient = 上层门面,内部持有 ChatModel,业务开发首选,链式 Fluent API,把消息组装、Prompt、工具调用、记忆、拦截器全部封装好。
ChatClient 优点
- 样板代码极少,不用手动组装
List<Message>、Prompt、ChatOptions;链式 API 可读性高。 - 提示词模板原生支持
.param(),变量替换不用自己拼接字符串。 - Advisor 切面扩展机制:日志、RAG、记忆、输出校验统一拦截处理,不需要装饰器包装 ChatModel。
- 工具调用全自动循环:
@Tool注解,自动调用本地 Java 方法、把结果塞回消息,不用手写 while 循环。 - 结构化输出
.entity(),自动处理 markdown ```json,直接映射 POJO,支持校验失败自动重试。 - 全局默认配置:builder 设置
defaultSystem、defaultTemperature,单次调用可覆盖。 - 内置 Memory 支持,一行接入会话记忆。
4.2.1 基础ChatClient实例
- 新建类:ChatClientDemo
import org.springframework.ai.chat.client.ChatClient;
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 ChatClientDemo {
private final ChatClient chatClient;
// 构造器注入 Builder
public ChatClientDemo(ChatClient.Builder builder) {
//创建 ChatClient 实例
this.chatClient = builder.build();
}
@GetMapping("/chatclient")
public String generateResponse() {
// 链式调用:构建提示 -> 发送请求 -> 获取文本响应
return chatClient.prompt()
.user("AI") // 用户消息
.call() // 同步调用模型
.content(); // 提取响应内容
}
}
核心方法解析:
prompt():启动聊天提示词构建流程,支持链式调用,返回一个 ChatClientRequestSpec接口类型对象,ChatClientRequestSpec用于设置聊天请求的规范,如提示信息等。user(String content):本方法来自 ChatClientRequestSpec接口,用于添加用户提问消息,返回值类型为ChatClientRequestSpeccall():本方法来自ChatClientRequestSpec接口,用于触发同步请求,返回CallResponseSpec接口类型对象,通过CallResponseSpec可以获取聊天响应的规范,获取响应内容。content():本方法来自CallResponseSpec接口,返回模型生成的纯文本聊天内容。
- 增加测试方法
@Test
void testClient() throws Exception {
mockMvc.perform( get("/chatclient") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
4.2.2 流式响应Server-Sent Events (SSE)实例
流式响应:模型生成一点,就立刻下发一点给前端,实现网页上那种打字机、逐字输出效果。
大模型生成是一个字一个字往外吐(token),流式就是把每一小块 token 实时传给客户端,不用等全部生成完毕。
在 Spring AI 中,支持流式响应的 ChatClient 实例,核心在于调用其 .stream() 方法,并配合响应式编程模型使用。
在 Controller 中返回 Flux:为了使前端能实时接收数据,Controller 的接口需要:
- 返回类型为
Flux<String>。 - 使用
@GetMapping或@PostMapping,并通过produces = MediaType.TEXT_EVENT_STREAM_VALUE。
- 在ChatClientDemo类中,增加方法
streamChat
// 流式接口的核心:返回Flux,并设置produces为SSE格式
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String msg) {
return chatClient.prompt()
.user(msg)
.stream() // 关键步骤:开启流式输出
.content(); // 直接获取文本内容的Flux流
}
2. 流式聊天的前端Vue组件
<template>
<div class="container">
<h1>本地部署聊天界面</h1>
<div class="input-group">
<input type="text" id="messageInput" placeholder="请输入问题..." v-model.trim="message" :disabled="loading">
<button id="submitBtn" @click="mes" :disabled="loading">{{ loading ? '等待中...' : '提交' }}</button>
</div>
<div id="response" ref="responseRef">
<div v-if="loading && !content" class="loading">
<span></span><span></span><span></span>
</div>
{{content}}
</div>
</div>
</template>
<script setup>
import { ref, watch, onBeforeUnmount, nextTick } from 'vue';
const API_URL = 'http://127.0.0.1:8999/stream';
const TYPING_INTERVAL = 50;
const message = ref('');
const content = ref('');
const loading = ref(false);
const responseRef = ref(null);
let eventSource = null;
let typingTimer = null;
let prevMessage = '';
const cleanup = () => {
typingTimer && clearInterval(typingTimer);
typingTimer = null;
if (eventSource) {
eventSource.close();
eventSource = null;
}
};
const typeStream = (data, onDone) => {
let i = 0;
const step = () => {
if (i >= data.length) {
typingTimer && clearInterval(typingTimer);
typingTimer = null;
onDone && onDone();
return;
}
content.value += data.charAt(i++);
};
typingTimer = setInterval(step, TYPING_INTERVAL);
};
const mes = () => {
cleanup();
content.value = '';
loading.value = true;
const msg = message.value;
if (!msg) {
loading.value = false;
return;
}
prevMessage = msg;
let queuedData = '';
let streamDone = false;
const drainQueue = () => {
if (queuedData.length === 0) {
if (streamDone) cleanup();
return;
}
const chunk = queuedData;
queuedData = '';
typeStream(chunk, () => {
if (streamDone && queuedData.length === 0) cleanup();
});
};
eventSource = new EventSource(`${API_URL}?msg=${encodeURIComponent(msg)}`);
eventSource.onmessage = (event) => {
loading.value = false;
queuedData += event.data;
if (!typingTimer) drainQueue();
};
eventSource.onerror = () => {
loading.value = false;
streamDone = true;
if (queuedData.length === 0 && !typingTimer) cleanup();
eventSource && eventSource.close();
};
eventSource.onclose = () => {
streamDone = true;
};
};
watch(message, (val) => {
if (loading.value && val !== prevMessage) {
cleanup();
loading.value = false;
}
});
watch(content, async () => {
await nextTick();
if (responseRef.value) {
responseRef.value.scrollTop = responseRef.value.scrollHeight;
}
});
onBeforeUnmount(cleanup);
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
gap: 24px;
}
h1 {
color: #fff;
font-size: 28px;
font-weight: 600;
text-align: center;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
letter-spacing: 1px;
}
.input-group {
display: flex;
gap: 12px;
background: #fff;
padding: 16px;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
}
#messageInput {
flex: 1;
padding: 14px 18px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 15px;
outline: none;
transition: all 0.3s ease;
background: #fafafa;
}
#messageInput:focus {
border-color: #667eea;
background: #fff;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
}
#messageInput::placeholder {
color: #aaa;
}
#submitBtn {
padding: 14px 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
#submitBtn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
#submitBtn:active {
transform: translateY(0);
}
#response {
background: #fff;
border-radius: 16px;
padding: 24px;
height: 300px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
font-size: 15px;
line-height: 1.8;
color: #333;
white-space: pre-wrap;
word-break: break-word;
overflow-y: auto;
}
#response::-webkit-scrollbar {
width: 8px;
}
#response::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
#response::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 4px;
}
#response::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #5a67d8, #6b46c1);
}
#response:empty::before {
content: 'AI 回复将在这里显示...';
color: #bbb;
font-style: italic;
}
.loading {
display: flex;
gap: 8px;
padding: 20px 0;
}
.loading span {
width: 10px;
height: 10px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.loading span:nth-child(1) {
animation-delay: -0.32s;
}
.loading span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
#submitBtn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
#messageInput:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
五、AI人设与提示词Prompt
5.1 简单全局AI人设
为了使AI能够专业回答问题,通常可以给AI进行人物角色设定(简称人设),例如让AI以领域专家、老师、历史人物的身份回答问题。AI人设无法在Spring AI全局配置文件中完成,需要用Java代码编程完成,ChatClient.Builder.defaultSystem方法可以实现全局人设设定。
- 新建类: SystemDemo
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/system")
public class SystemDemo {
private final ChatClient chatClient;
public SystemDemo(ChatClient.Builder builder) {
//利用defaultSystem方法实现AI全局人设设定
chatClient=builder.defaultSystem("你是一位你是天文专家").build();
}
@GetMapping("/teacher")
public String chat() {
return chatClient
.prompt()
.user("请用30字介绍月球")
.call()
.content();
}
}
2. 增加测试方法:testSystem()
@Test
void testSystem() throws Exception {
mockMvc.perform( get("/system/teacher") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
5.2 复杂全局AI人设
如果人设的文字内容很多,则不适合在类中硬编码,推荐使用人设配置文件
- 在resources目录下新建文件:system/user.sys,内容如下:
你是一位幼儿园老师,面对小朋友的提问,你要友善、风趣、幽默地回答问题
2. 增加配置类:ChatClientConfig
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
@Configuration
public class ChatClientConfig {
@Value("classpath:system/user.sys")
private Resource systemResource;
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder.defaultSystem(systemResource).build();
}
}
3. 修改类:SystemDemo中的构造方法
public SystemDemo(ChatClient chatClient) {
this.chatClient = chatClient;
}
4. 再次运行测试方法:testSystem
5.3 局部AI人设
在方法中调用system方法进行局部人设设定,此时会覆盖全局人设。
- 修改类SystemDemo中的chat方法
@GetMapping("/teacher")
public String chat() {
return chatClient
.prompt()
.system("你是佛祖,请用佛祖的语气回答问题")
.user("请用30字介绍月球")
.call()
.content();
}
2. 再次运行测试方法:testSystem
5.4 提示词工程
提示是引导模型输出的关键,ChatClient支持多种构建方式,满足不同复杂度需求:
提示词本身是一种专业工程,详细可以参考:www.promptingguide.ai/
在 Spring AI 中做提示词工程,核心思路是把提示词当作结构化的输入来设计,而不是一大段拼凑的文本它通过不同角色的Message来组合,让指令、任务和上下文分离,逻辑更清晰,效果也更好。
理解 Spring AI 中的 Prompt
在 Spring AI 里,Prompt不再是一个简单的字符串,而是一个由多条Message组成的容器,每条消息都有明确的角色。主要角色有以下几种:
- SystemMessage (系统消息):为整个对话设定“游戏规则”。用于定义 AI 的角色、行为边界、回复风格和全局约束。它就像一个幕后导演,贯穿全程。
- UserMessage (用户消息):代表用户当前的具体提问或任务指令。
- AssistantMessage (助手消息):代表 AI 的回复。在多轮对话中,用于存储历史记录,保证上下文的连贯性。
- ToolResponseMessage (工具响应消息):当 AI 调用外部工具(如查询天气、数据库)后,返回的结果消息。
Spring AI 提示词工程四步法
5.4.1 角色扮演:设定“人格”与“语境”
通过system()方法为 AI 分配一个专家身份,可以激活其特定领域的知识储备,让回答更专业、更对口。
- 新建类:PromptDemo
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/prompt")
public class PromptDemo {
private final ChatClient chatClient;
public PromptDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/persona")
public String persona () throws Exception {
return chatClient.prompt()
.system("你是一位拥有10年经验的资深Java架构师,请用专业且易于理解的方式回答问题。")
.user("什么是反应式编程?")
.call()
.content();
}
}
2.增加测试方法:testPersona
@Test
void testPersona() throws Exception {
mockMvc.perform( get("/prompt/persona") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
5.4.2 模板化:参数驱动,告别硬编码
PromptTemplate是实现提示词复用和动态管理的核心。它允许你定义模板,然后在运行时填入具体参数,使代码更简洁、更易维。
- 修改类:PromptDemo增加方法template
@GetMapping("/template")
public String template() throws Exception {
// 1. 定义模板
PromptTemplate template = new PromptTemplate("""
请用{style}的风格,介绍{topic}。
回复需要包含至少{points}个要点。
""");
// 2. 创建Prompt对象并传入参数
Prompt prompt = template.create(Map.of(
"style", "生动有趣",
"topic", "Spring Cloud",
"points", "3"
));
return chatClient.prompt(prompt)
.call()
.content();
}
2.修改测试方法:testTemplate
@Test
void testTemplate() throws Exception {
mockMvc.perform( get("/prompt/template") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
5.4.3 少样本学习:用“例子”框定“格式”
给模型提供一两个“输入-输出”的示例,是约束输出格式、让它快速理解你意图的高效方法。
- 修改类:PromptDemo增加方法sample
@GetMapping("/sample")
public String sample() throws Exception {
return chatClient.prompt()
.system("你是一个订单信息提取助手,请将用户的自然语言指令转换为JSON格式。")
.user("""
示例:
用户输入:我要一份大杯的拿铁咖啡。
输出:{"size":"大杯","type":"拿铁","quantity":1}
现在请转换:我要两杯中杯的美式咖啡。
""")
.call()
.content();
// 期望输出:{"size":"中杯","type":"美式","quantity":2}
}
2.修改测试方法:testSample
@Test
void testSample() throws Exception {
mockMvc.perform( get("/prompt/sample") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
5.4.4 上下文注入:让 AI 更“懂你”
当模型需要了解特定背景才能给出更好回答时,可以使用.param()方法将上下文信息动态注入到提示词中。
- 修改类:PromptDemo增加方法background
@GetMapping("/background")
public String background() throws Exception {
return chatClient.prompt()
.user(u -> u
.text("请根据以下背景知识,制定一份学习计划:{context}")
.param("context", "用户是Java新手,已学习Java SE基础,希望在一个月内掌握Spring Boot核心开发。")
)
.call()
.content();
}
2.修改测试方法:testBackground
@Test
void testBackground() throws Exception {
mockMvc.perform( get("/prompt/background") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
5.4.5 调优:将“生成参数”交给 Options
还需要特别注意:像temperature(温度,控制随机性)、maxTokens(最大输出长度)这类控制模型生成行为的参数,应通过.options()方法设置,而不是用自然语言写在 Prompt 里要求模型。这能让控制更直接、更稳定。
- 修改类:PromptDemo增加方法options
@GetMapping("/options")
public String options() throws Exception {
return chatClient.prompt()
.system("你是一位文案专家,为产品写一句宣传语。")
.user("产品是一款AI编程助手,面向资深开发者。")
.options( ChatOptions.builder()
.temperature(0.8)
.maxTokens(100)
)
.call()
.content();
}
2.修改测试方法:testOptions
@Test
void testOptions() throws Exception {
mockMvc.perform( get("/prompt/options") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
核心设计思路总结
从实践角度,我把 Spring AI 的提示词工程思路浓缩为以下三点:
- 结构化设计:放弃“拼字符串”的旧习惯。将长期规则、当前任务、历史记录和外部背景分层管理,让提示词的职责清晰,易于维护和扩展。
- 配置化管理:将提示词模板、模型参数(
temperature等)和系统角色从代码中抽离,作为外部配置(如文件、数据库)进行管理。这让你无需修改代码和重新部署,就能迭代和优化提示词。 - 动态化调用:通过
ChatClient的构建器与调用 API,实现“全局默认”与“局部覆盖”的灵活组合。大部分场景使用默认配置,特殊需求通过.options()和.param()进行精准调整
六、结构化输出
Spring AI 的结构化输出能力让你可以把 LLM 的文本响应直接映射为 Java 对象(POJO/Record),而不用手动解析 JSON。
目前,Spring AI 主要提供了两种在 ChatClient 上实现结构化输出的方式,你可以根据对可靠性和模型支持的要求来选择。
6.1 简洁方式 - 核心 API:.entity()
使用方式是通过 ChatClient 的 .entity() 方法。你只需要定义一个 Java Record 来描述期望的数据结构,Spring AI 会自动完成 JSON Schema 的生成、提示词的组装,以及响应的反序列化。
6.1.1 结构化输出单个对象
- 定义一个 Java Record 来映射期望的 JSON 结构
Record是 Java 16 正式引入,Spring‑AI2.0 结构化输出
.entity()强烈推荐使用 Record,用来做数据载体,替代普通 POJO/DTO。Spring‑AI 在做 JSON 反序列化、结构化输出时,对Record 支持非常友好。Record = 数据载体类,专门用来保存不可变数据。
//核心目标是:用最少的代码,实现一个不可变(Immutable)的数据类,并自动生成构造器、访问器、equals()、hashCode() 和 toString()。
public record Computer(
String brand,
String type,
Double price
) {}
2. 新建类:RecordDemo
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class RecordDemo {
private final ChatClient chatClient;
public RecordDemo(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/computer")
public String chatComputer() {
//包含品牌、型号和价格 提示词来指导模型
Computer result=chatClient.prompt()
.user("请随机介绍一款笔记本,包含品牌、型号和价格")
.call()
.entity(Computer.class);
log.info("result={}", result);
return "success";
}
}
3.新增测试方法
@Test
void testComputer() throws Exception {
mockMvc.perform( get("/computer") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
可靠性增强:两种保障机制
默认的 .entity() 依赖于提示词来指导模型,是一种“尽力而为”的方式,模型仍有可能返回格式错误的 JSON。为此,Spring AI 2.0 提供了两种互补的机制来提升可靠性。
1. 响应端修正:validateSchema()
- 工作原理:开启后,Spring AI 会验证模型返回的 JSON 是否符合 Record 定义的 Schema。如果验证失败(如缺少字段),错误信息会被追加到提示词中,并自动重试(默认最多3次),引导模型自行修正。
代码示例:
Computer result=chatClient.prompt()
.user("请随机介绍一款笔记本,包含品牌、型号和价格")
.call()
.entity(Computer.class,spec -> spec.validateSchema());
2. 请求端约束:useProviderStructuredOutput()
- 工作原理:调用模型供应商(如 OpenAI、Anthropic)的原生结构化输出 API。此时,JSON Schema 会作为 API 层面的约束条件发送,由供应商的服务端强制保证输出格式的合规性。
代码示例:
//包含品牌、型号和价格 提示词来指导模型
Computer result=chatClient.prompt()
.user("请随机介绍一款笔记本,包含品牌、型号和价格")
.call()
.entity(Computer.class, spec -> spec.useProviderStructuredOutput());
6.1.2 结构化输出List
1.修改类:RecordDemo,增加方法:chatComputerList()
@GetMapping("/computerlist")
public String chatComputerList() {
//包含品牌、型号和价格 提示词来指导模型
ParameterizedTypeReference<List<Computer>> typeRef = new ParameterizedTypeReference<List<Computer>>() { };
List<Computer> result=chatClient.prompt()
.user("请随机介绍三款计算机,包含品牌、型号和价格")
.call()
.entity(typeRef);
log.info("result={}", result);
return "success";
}
注意此处使用了ParameterizedTypeReference,该类是 Spring 提供的一个工具类(抽象类),Java 的泛型在运行时会被类型擦除,导致程序无法在运行时获知一个 List 中的 Computer 类型信息。通过ParameterizedTypeReference,保留了完整的泛型类型信息,保证结构化输出的结果。
3.新增测试方法
@Test
void testComputerList() throws Exception {
mockMvc.perform( get("/computerlist") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
6.2 复杂方式-结构化输出转换器介绍
Spring AI 的结构化输出转换器(StructuredOutputConverter)是一套围绕大语言模型(LLM)调用前后两个阶段工作的工具。它最初设计用来解决从模型原始文本回复到 Java 对象之间的转换问题,在调用前为提示词注入格式指令,调用后将文本解析为目标类型。
StructuredOutputConverter核心实现类包括:
BeanOutputConverter<T>:将输出转换为指定的 Java 对象,是ChatClient背后默认使用的方式。MapOutputConverter:将输出转换为Map<String, Object>。ListOutputConverter:将输出转换为逗号分隔的List<String>。
核心概念:两阶段工作流
StructuredOutputConverter<T> 接口本身继承了两个接口,这也对应了它的两大核心职责:
FormatProvider(调用前) :通过getFormat()方法生成格式指令,这些指令会被追加到你的提示词末尾,指导模型按特定结构(如 JSON Schema)输出。Converter<String, T>(调用后) :通过convert(String text)方法,将模型返回的原始文本字符串,转换为你指定的 Java 对象(如Bean、Map或List)。
下面的流程图清晰地展示了数据在调用前后的流转过程:
完整代码示例
- 定义目标 Record
// 定义你想要转换成的数据结构
public record ActorsFilms(String actor, List<String> movies) {}
2. 创建转换器并组装提示词
// 1. 创建转换器,指定目标类型
BeanOutputConverter<ActorsFilms> converter = new BeanOutputConverter<>(ActorsFilms.class);
// 2. 获取格式指令,并注入到用户提示词模板中
String userPromptTemplate = """
请随机生成一位演员及其电影作品列表。
{format}
"""; // {format} 是一个占位符
PromptTemplate promptTemplate = new PromptTemplate(userPromptTemplate,
Map.of("format", converter.getFormat())); // 用转换器的格式指令替换占位符
Prompt prompt = new Prompt(promptTemplate.createMessage());
3. 调用模型并转换结果
// 使用 ChatModel API 发送请求
ChatResponse response = chatModel.call(prompt);
String rawOutput = response.getResult().getOutput().getText();
// 使用转换器将模型输出文本转换为 Java 对象
ActorsFilms result = converter.convert(rawOutput);
// 使用结果
System.out.println("演员: " + result.actor());
System.out.println("电影: " + result.movies());
在上层使用了 ChatClient,那么几乎不需要直接接触 StructuredOutputConverter。它是为了更底层的框架扩展而保留的。如果你遇到具体的报错场景,可以告诉我,我可以帮你分析是应该切换 API,还是通过自定义方式绕过缺陷。
七、工具调用
AI大模型无法访问实时信息。模型无法回答任何假设了解信息(如当前日期或天气预报)的问题。但是,我们可以提供一个可以检索此信息的工具,并让模型在需要访问实时信息时调用此工具。
准备工作:申请 API Key
首先,访问聚合数据官网注册账号,在“个人中心”申请天气预报API接口,并获取到你的 API Key
7.1 查询天气案例
- 定义Record,描述返回的数据结构
public record WeatherInfo(
String city, //城市
double temperature, //温度
String info, //天气状况
String wind //风
){}
2. 定义工具类
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
@Component
public class WeatherService {
//API_KEY
private final String apiKey = "你的key";
private final ObjectMapper objectMapper = new ObjectMapper();
private final WebClient webClient;
public WeatherService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.build();
}
// 1. 使用 @Tool 注解暴露方法
//description 是大模型决定是否调用该工具的核心依据,务必清晰
@Tool(description = "根据城市名称获取该地当前的实时天气信息")
public WeatherInfo getWeather(
// 2. 使用 @ToolParam 描述参数,默认所有参数都是 required
@ToolParam(description = "城市的中文名称,例如:北京、上海") String city) throws Exception {
// 调用真实的天气API
String url = String.format(
"http://apis.juhe.cn/simpleWeather/query?key=%s&city=%s",
apiKey,
city
);
//发送请求,获取响应数据
String response = webClient.get()
.uri(url)
.retrieve()
.bodyToMono(String.class)
.block();
// 解析聚合数据返回的 JSON,提取关键字段
JsonNode root = objectMapper.readTree(response);
JsonNode result = root.path("result");
JsonNode realtime = result.path("realtime");
// 构建 WeatherInfo 对象
return new WeatherInfo( result.path("city").asText(),Double.parseDouble(realtime.path("temperature").asText()),
realtime.path("info").asText(),( realtime.path("direct").asText()+" "+realtime.path("power").asText())) ;
}
}
2. 新建类:WeatherDemo
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WeatherDemo {
private final ChatClient chatClient;
private final WeatherService weatherService;
public WeatherDemo(ChatClient.Builder builder, WeatherService weatherService) {
this.chatClient = builder.build();
this.weatherService = weatherService;
}
@GetMapping("/weather")
public String askWeather(@RequestParam String city) {
// 用户提问,框架会自动完成一切:判断 -> 调用工具 -> 组织最终回答
return this.chatClient.prompt()
.tools(weatherService) //注册工具
.user("今天" + city + "的天气怎么样?适合出门运动吗?")
.call()
.content();
}
}
3. 新增测试方法:askWeather
@GetMapping("/weather")
public String askWeather(@RequestParam String city) {
// 用户提问,框架会自动完成一切:判断 -> 调用工具 -> 组织最终回答
return this.chatClient.prompt()
.tools(weatherService) //注册工具
.user("今天" + city + "的天气怎么样?适合出门运动吗?")
.call()
.content();
}
特别说明:如何判断AI模型是否支持工具调用,这个可以在类似硅基流动的平台上看到,只要AI模型信息下有tool标签即可。
八、MCP
MCP(Model Context Protocol,模型上下文协议)是由 Anthropic 推出的一种标准化开放协议,它定义了 AI 模型如何以统一、安全的方式调用外部工具、获取资源和交互。
在Spring AI 2.0 中,对 MCP 的支持已经非常成熟。它相当于为 AI 世界提供了一个“通用插头”,让大语言模型(LLM)不再是信息孤岛,而是能与现实世界的各种服务顺畅对话)。
MCP 的核心价值:从“定制开发”到“即插即用”
在 MCP 出现之前,每个AI应用要调用外部服务(如查询天气、操作数据库),都需要编写特定的适配代码,效率低且难维护。MCP 通过引入客户端-服务器(Client-Server)架构解决了这个问题。
- MCP 客户端 (Client):通常是 AI 应用本身,它负责发起请求。在 Spring AI 中,它通过 spring-ai-starter-mcp-client 启动器实现。
- MCP 服务器 (Server):负责提供具体的功能(如天气查询API、查询),并通过标准协议暴露这些能力。开发者可使用 spring-ai-starter-mcp-server-webmvc 或 webflux 快速搭建。
这种模式让开发者可以像搭积木一样,将社区已有的 MCP Server 直接集成到自己的应用中。
8.1 搭建MCP服务器端
MCP 服务器支持三种传输机制,每种机制都有其专用的Starters:
- 标准输入/输出 (STDIO) -
spring-ai-starter-mcp-server - Spring MVC(服务器发送的事件)-
spring-ai-starter-mcp-server-webmvc - Spring WebFlux(反应式 SSE)-
spring-ai-starter-mcp-server-webflux
本例采用的是Spring MVC模式
- 创建MCP服务器的Maven项目,修改pom.xml文件
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<!--MCP服务端依赖-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<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. 新建服务层接口及实现类
接口
public interface FoodService {
//美食推荐
String recommend();
}
实现类
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.stereotype.Service;
@Service
public class FoodServiceImpl implements FoodService {
//McpTool注解是 Spring AI 为 MCP 服务端(Server)设计的注解
//只需SpringBean方法上添加此注解,该方法就会自动注册为一个 MCP 工具,供 AI 模型或其他 MCP 客户端调用
//description属性,大模型会根据用户的提示词,匹配该方法,调用方法获得结果。
@McpTool(description = "美食推荐")
@Override
public String recommend() {
return "pizza";
}
}
3. 增加配置文件:application.yml
# 服务器端口号
server:
port: 8888
# MCP Server 配置
spring:
ai:
mcp:
server:
# Streamable‑HTTP,推荐,http传输,访问地址:http://127.0.0.1:8888/mcp
protocol: STREAMABLE
name: mcp-server
4. 编写主启动类,并启动应用
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
8.2 搭建MCP客户端
- 修改ai项目,在pom.xml中增加MCP客户端依赖坐标
ai
<!--MCP客户端依赖-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
2. 修改application.yml文件,增加MCP客户端配置
mcp:
client:
streamable‑http:
connections:
my‑demo‑server:
url: http://127.0.0.1:8888/mcp
3. 新增类 :McpClientDemo
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class McpClientDemo {
private final ChatClient chatClient;
// MCP客户端自动注入,获取远端MCP服务全部工具
private final SyncMcpToolCallbackProvider mcpToolProvider;
public McpClientDemo(ChatModel chatModel, SyncMcpToolCallbackProvider mcpToolProvider) {
this.chatClient = ChatClient.builder(chatModel).build();
this.mcpToolProvider = mcpToolProvider;
}
@GetMapping("/mcp")
public String mcp() {
String resp = chatClient.prompt()
.user("美食推荐")
// 直接加载MCP远端全部工具,大模型自动调用MCP‑Server上recommend
.tools(mcpToolProvider.getToolCallbacks())
.call()
.content();
return resp;
}
}
4. 增加测试方法:testMcp
@Test
void testMcp() throws Exception {
mockMvc.perform( get("/mcp"))
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
九、利用Token监控AI性能
在Spring AI中监控 Token 和 AI 性能,主要有编程式和可观测性指标两种方式,它们分别适用于实时逻辑处理和集中监控分析。
1. 编程式获取:实时获取单次调用的 Token 消耗
如果你需要在代码逻辑中实时获取某次 AI 调用的 Token 消耗,可以直接从 ChatResponse 中获取 Usage 对象。这种方式适用于实时计费、动态限流或即时告警等场景。
代码示例:
// 调用 ChatModel 或 ChatClient 得到 ChatResponse
ChatResponse response = chatModel.call(new Prompt("Srping AI 简介"));
// 从响应的元数据中获取 Usage 对象
Usage usage = response.getMetadata().getUsage();
// 获取详细的 Token 统计信息
Long promptTokens = usage.getPromptTokens(); // 输入 Token 数
Long completionTokens = usage.getCompletionTokens(); // 输出 Token 数
Long totalTokens = usage.getTotalTokens(); // 总 Token 数
System.out.println("本次调用消耗 Token:");
System.out.println("输入 Token: " + promptTokens);
System.out.println("输出 Token: " + completionTokens);
System.out.println("总计 Token: " + totalTokens);
2. 可观测性指标:集中监控与可视化
第一步:添加依赖。确保项目中包含了 spring-boot-starter-actuator 和 micrometer-registry-prometheus。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
第二步:配置属性。
在 application.yml 中控制可观测性行为。
spring:
ai:
openai:
# 启用从AI提供商API响应头中收集速率限制信息
metadata:
rate-limit-metrics-enabled: true
chat:
client:
observations:
# 启用记录提示词内容(用于调试,注意不要在生产环境暴露敏感信息)
log-prompt: true
# 启用记录模型响应内容
log-completion: true
第三步:访问端点。
启动应用后,访问 /actuator/prometheus 端点即可看到包括 gen_ai_client_token_usage_total 在内的所有指标。直观地查看 Token 消耗趋势和性能瓶颈。
十、检索增强生成RAG
RAG的英文全称为Retrieval Augmented Generation,RAG 可以说是 Spring AI 2.0 最核心、最成熟的应用模式之一。
它的精髓在于:把私域知识库(如公司文档、规章制度)切片、向量化后存储,当用户提问时,先从知识库中检索出最相关的片段,再把这些片段作为“参考资料”连同问题一起交给大模型,从而生成有理有据的回答。
这能有效解决大模型的“幻觉”问题。
10.1 RAG基本流程
- ETL:读取 PDF/TXT → Token 分块 → Embedding 生成向量 →存入向量库。
- 查询:用户提问 → 向量相似度检索召回文档片段 → 把参考上下文塞进 Prompt 交给大模型回答,减少幻觉。
10.2 向量
在 Spring AI 中,向量(Vector) 是实现语义搜索和 RAG(检索增强生成)的核心概念。简单来说,向量是一组浮点数,用来将文本、图像等内容转换成机器可理解的"语义坐标"。例如:
"苹果很好吃" → [0.23, -0.56, 0.89, 0.12, -0.34, ...] (1024维)
"香蕉很甜" → [0.18, -0.62, 0.91, 0.08, -0.28, ...] (1024维)
"汽车很快" → [-0.45, 0.73, -0.21, 0.56, 0.33, ...] (1024维)
核心概念
1. 嵌入(Embedding)→ 向量
- Embedding 是将人类可读的内容(如一句话)通过 AI 模型(如 OpenAI、Ollama、本地模型)转换为高维浮点数组的过程。
- 语义相近的内容,其向量在数学空间中的距离也很近。
- 例如:"猫" 和 " kitten" 的向量会比 "猫" 和 "汽车" 更接近。
2. 向量存储(Vector Store)
Spring AI 提供了统一的 VectorStore 接口,屏蔽了底层实现差异,支持:
- PgVector(PostgreSQL)
- Redis
- Milvus / Pinecone / Weaviate / Qdrant 等专用向量数据库
- 简单内存存储(
SimpleVectorStore,适合演示)
3. 相似度搜索
存入向量后,你可以用自然语言查询,Spring AI 会:
- 将查询文本也转为向量
- 在向量空间中计算余弦相似度或欧氏距离
- 返回最相近的 Top-K 条结果
10.3 RAG实现案例 -简单内存存储
10.3.1 添加RAG相关依赖
<!-- Spring AI 2.0: 向量存储核心 (含 SimpleVectorStore) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store</artifactId>
</dependency>
<!-- Spring AI 2.0: RAG Advisor (QuestionAnswerAdvisor) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store-advisor</artifactId>
</dependency>
<!-- PDF解析 (SpringAI内置的PDF文档读取) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
<version>${spring-ai.version}</version>
</dependency>
10.3.2 修改配置文件## (application.yml)
- openai
embedding: # embedding模型
model: BAAI/bge-m3
2. ai
vectorstore: #向量存储
simple: # 简单内存存储
dimensions: 1024 # 向量维度与 EmbeddingModel 匹配
distance-type: COSINE # 距离度量算法,用于计算向量之间的相似度 可选:COSINE, EUCLIDEAN, DOT
关于embedding简要解释:embedding表示嵌入模型,这种模型与大语言模型差异在于它用来把输入的问题转换为向量,然后再到向量知识库中匹配近似的答案,然后大语言模型再把答案提供给最终用户。
10.3.3 内存向量存储配置
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class VectorStoreConfig {
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
// 内存向量存储,无需外部依赖,重启后数据丢失
return SimpleVectorStore.builder(embeddingModel).build();
}
}
10.3.4 PDF加载与入库 Service
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Service
@Slf4j
public class PdfIngestionService {
private final VectorStore vectorStore;
private final TokenTextSplitter textSplitter;
public PdfIngestionService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
/**
* TokenTextSplitter 配置
* 针对中文技术文档优化的配置:
* - chunkSize: 800 → 每块 800 tokens
* - minChunkSize: 150 → 小于 150 字符的块会合并
* - keepSeparator: true → 保留标点符号
*/
this.textSplitter = TokenTextSplitter.builder()
.withChunkSize(800) // 块大小 (默认800)
.withMinChunkSizeChars(150) // 最小字符数
.withKeepSeparator(true) // 是否保留分隔符 (默认true)
.build();
}
/**
* 加载 PDF 资源,切分后存入向量存储
*/
public void ingestPdf(Resource pdfResource) {
// 1. 读取 PDF(按页)
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(pdfResource);
List<Document> documents = pdfReader.read();
// 2. 切分为小块(带重叠,防止上下文断裂)
List<Document> chunks = textSplitter.apply(documents);
// 3. 自动调用 EmbeddingModel 生成向量并存储
vectorStore.add(chunks);
log.info("PDF 入库完成,共 {} 个片段", chunks.size());
}
}
10.3.5 启动时预加载 PDF
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
@Configuration
public class PdfLoader {
@Bean
CommandLineRunner loadPdf(PdfIngestionService ingestionService, ResourceLoader resourceLoader) {
return args -> {
// 启动时自动加载 classpath:docs/manual.pdf
var resource = resourceLoader.getResource("classpath:docs/demo.pdf");
if (resource.exists()) {
ingestionService.ingestPdf(resource);
}
};
}
}
10.3.6 调用知识库的控制器
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/rag")
public class RagController {
private final ChatClient chatClient;
private final VectorStore vectorStore;
public RagController(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
this.chatClient = chatClientBuilder
.build();
this.vectorStore = vectorStore;
}
// RAG问答接口
@GetMapping("/chat")
public String ragChat(@RequestParam String question) {
//1.向量检索
List<Document> relatedDocs = vectorStore.similaritySearch(question);
String context = formatDocs(relatedDocs);
//2.构造RAG提示词
String promptStr = """
你是文档问答助手,只根据下面【参考上下文】回答用户问题。
如果上下文没有相关信息,直接回答不知道,不要编造内容。
【参考上下文】
{context}
用户问题:{question}
""";
PromptTemplate promptTemplate = new PromptTemplate(promptStr);
promptTemplate.add("context", context);
promptTemplate.add("question", question);
Prompt prompt = promptTemplate.create();
//3.调用大模型输出答案
return chatClient
.prompt(prompt)
.call()
.content();
}
private String formatDocs(List<Document> docs) {
StringBuilder sb = new StringBuilder();
for (Document doc : docs) {
sb.append(doc.getText()).append("\n\n");
}
return sb.toString();
}
}
10.3.7 增加测试方法
@Test
void testRag() throws Exception {
mockMvc.perform( get("/rag/chat")
.param("question","推理模型") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
10.4 RAG实现案例 - Redis存储
redis需要使用Linux版,且版本号达到4.0以上,windows系统可以通过docker安装redis-stack(即redis全家桶)。
建议安装DockerDesktop
安装环境要求
- 系统版本:建议 Windows 11,或 Windows 10 22H2 及以上版本(家庭版/专业版/企业版)。
- 硬件:需要 64 位处理器,支持并已在 BIOS 中启用硬件虚拟化功能,至少 4GB 内。
- 后端推荐:强烈建议使用 WSL 2(Windows Subsystem for Linux)作为后端。这能提供更好的性能和体验,你需要提前安装并配置好 WSL 2
- >启用或关闭 Windows 功能 : 勾选: Hyper-V,适用于Linux的Windows 子系统,虚拟机平台,点击确定。
Windows 系统安装步骤
- 以管理员身份打开 PowerShell ,运行以下命令安装并设置 WSL 2 为默认版本:
wsl --install
wsl --set-default-version 2
安装完成后,重启电脑。该命令通常会默认安装 Ubuntu 发行版。
- 下载与安装 Docker Desktop
- 前往 Docker 官网 下载 Windows 版安装程序。
- 双击运行安装程序,在安装选项中务必勾选 "Use WSL 2 instead of Hyper-V" (使用 WSL 2 代替 Hyper-V)。
- 按照提示完成。
- 启动与配置
- 从开始菜单启动 Docker Desktop。首次启动会弹出订阅服务协议,选择 Accept 接受后即可继续。
- 进入 设置 > Resources > Docker Engine,粘贴以下下载镜像
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker-0.unsee.tech",
"https://docker.m.daocloud.io"
]
}
4. 验证安装
- 打开你的 CMD 终端,运行以下命令验证:
docker --version
5. 拉取redis-stack镜像
执行以下命令:
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
这个命令解释如下:
-d: 让容器在后台运行。
--name redis-stack: 给容器起个名字
-p 6379:6379: 将容器的 Redis 服务端口映射到你的电脑,你的 Spring AI 应用就能连接 localhost:6379 了。
-p 8001:8001: 映射 Redis Insight 图形化管理工具的端口,成功后可在浏览器通过 http://localhost:8001 查看和管理数据。
redis/redis-stack:latest: 使用的镜像名,这个版本已经包含了向量检索功能。
10.4.1 添加Redis相关依赖
<!-- Spring Boot Redis 起步依赖:提供 Spring Data Redis 支持,默认集成 Lettuce 客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jedis 客户端 RedisVectorStore 直接基于 Jedis 实现 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- Spring AI Redis Vector Store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
10.4.2 修改配置文件## (application.yml)
- ai
vectorstore: #向量存储
redis:
dimensions: 1024 # 向量维度 与 EmbeddingModel 匹配
distance-type: COSINE # 距离度量算法,用于计算向量之间的相似度 可选:COSINE, EUCLIDEAN, DOT
index-name: vector-index # 用于在 Redis 中标识该向量索引
initialize-schema: true # 否在应用启动时自动创建索引和 Schema
prefix: ai:embedding # Redis Key 前缀,所有向量数据会存储在以该前缀开头的 Key 中
2. spring
data:
redis:
host: localhost
port: 6379
database: 0
client-type: jedis
10.4.3 移除内存向量存储配置
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//@Configuration
public class VectorStoreConfig {
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
// 内存向量存储,无需外部依赖,重启后数据丢失
return SimpleVectorStore.builder(embeddingModel).build();
}
}
10.4.4 重新运行测试方法
@Test
void testRag() throws Exception {
mockMvc.perform( get("/rag/chat")
.param("question","推理模型") )
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
十一、大模型的记忆能力
11.1 ChatMemory
ChatMemory(聊天记忆) 是 AI 应用开发中的核心组件,用于解决大语言模型(LLM)无状态(stateless) 的固有缺陷——每次请求独立处理,模型本身不会"记住"之前的对话内容。
核心概念
为什么需要 ChatMemory?
LLM 不会自动保留对话上下文。如果没有记忆机制:
用户:我叫小明。
AI:你好,小明。
用户:我叫什么名字?
AI:我不知道。
ChatMemory 负责在每次请求时,将相关的历史消息注入到当前提示(prompt)中,让模型获得足够的上下文来生成连贯的回复。
核心价值:为“金鱼大脑”装上记忆
大模型API本身是“无状态”的,每次调用都像初次见面,记不住之前的对话。ChatMemory的作用就是管理对话历史,让应用能记住上下文。它的核心设计理念是将逻辑管理与物理存储解耦,主要分为两层:
- 逻辑层 (
ChatMemory) :负责对话上下文的管理策略,比如“该保留最近多少条消息”,由MessageWindowChatMemory等类实现。 - 存储层 (
ChatMemoryRepository) :负责对话消息的物理存储,可以存在内存、数据库或向量数据库中,由InMemoryChatMemoryRepository等类实现。
目前ChatMemory接口有四种实现类:
- InMemoryChatMemory:内存型对话存储
- CassandraChatMemory:基于 Cassandra 实现(数据持久化并设置过期时间)
- Neo4jChatMemory:基于 Neo4j 实现(数据持久化无过期时间)
- JdbcChatMemory:基于 JDBC 实现(数据持久化无过期时间)
四类实现分别对应:内存存储、Cassandra 带过期持久化、Neo4j 持久化、JDBC 持久化。## ## 11.2 使用JdbcChatMemory
11.2.1 添加依赖
<!-- JDBC Chat Memory Repository Starter(自动配置) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
11.2.2 新建数据库和表
- 数据库名:ai_chat
- 建表脚本:
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
id BIGINT NOT NULL AUTO_INCREMENT,
conversation_id VARCHAR(36) NOT NULL,
sequence_id BIGINT NOT NULL,
content TEXT NOT NULL,
type VARCHAR(10) NOT NULL,
timestamp TIMESTAMP NOT NULL,
PRIMARY KEY (id),
INDEX SPRING_AI_CHAT_MEMORY_CONV_SEQ_IDX (conversation_id, sequence_id),
CONSTRAINT type_check CHECK (type IN ('USER','ASSISTANT','SYSTEM','TOOL'))
);
11.2.3 修改配置文件
# 记忆配置
memory:
repository:
jdbc:
platform: mysql
# 手动动建表
initialize-schema: never
重要提示:
initialize-schema: always会在应用启动时自动创建SPRING_AI_CHAT_MEMORY表 ,有一些版本会有bug
datasource:
url: jdbc:mysql://localhost:3306/ai_chat?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
11.2.4 核心配置 (ChatMemoryConfig)
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* SpringAI 聊天记忆持久化配置
* 基于JDBC将对话历史存储到数据库表 SPRING_AI_CHAT_MEMORY
* 使用滑动窗口策略,只保留最近N条会话消息,防止上下文无限膨胀
*/
@Configuration
public class ChatMemoryConfig {
/**
* 构建聊天记忆Bean
* @param repository JDBC持久化仓库,SpringAI自动装配,操作SPRING_AI_CHAT_MEMORY表
* @return ChatMemory 聊天记忆实例,用于保存/读取用户会话历史
*/
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
// 滑动窗口大小:保留最近20条消息(用户+助手成对算2条)
// 超出数量会自动淘汰最早的消息,控制LLM上下文token消耗
.maxMessages(20)
// 指定持久化实现,将对话消息存入数据库,而非内存
.chatMemoryRepository(repository)
.build();
}
}
11.2.5 编写控制器
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatMemoryController {
private final ChatClient chatClient;
public ChatMemoryController(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder
// 设置全局默认Advisor:开启聊天记忆拦截器
// MessageChatMemoryAdvisor会在请求前读取历史会话,请求后保存本次问答记录
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
/**
* 带持久化会话记忆的聊天接口
* @param input 用户输入提问内容
* @param conversationId 会话唯一标识,不同会话使用不同ID,从数据库读取/保存该会话的历史消息
* @return AI返回的回答文本
*/
@GetMapping("/memorychat")
public String chat(String input, String conversationId) {
// 设置本次请求的conversationId参数,Advisor会根据该ID操作SPRING_AI_CHAT_MEMORY表
// 1.请求前:读取该conversationId下历史消息拼入prompt上下文
// 2.请求结束:把本次用户提问、AI回答存入数据库
return chatClient.prompt()
.user(input)
.advisors(advisor -> advisor
.param(ChatMemory.CONVERSATION_ID, conversationId))
.call()
.content();
}
}
安全提示:
conversationId应来源于服务端会话(如 HttpSession ID),不要直接信任客户端传参,防止会话被冒用或污染。
11.2.6 会话记忆测试
- 第一次向大模型发送请求时,conversationId为c123,以马斯克的身份与大模型对话:
@Test
void testMemory() throws Exception {
mockMvc.perform( get("/memorychat")
.param("input","你好,我是马斯克")
.param("conversationId","c123"))
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
2. 第二次发送请求,conversationId为c123,问大模型“你记得我的名字吗” 从模型回答内容看显然是有记忆的。
@Test
void testMemory() throws Exception {
mockMvc.perform( get("/memorychat")
.param("input","你记得我的名字吗?")
.param("conversationId","c123"))
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
3. 第三次发送请求,换一个conversationId,如c456,由于不是与c123不是同一个ID,所以回答问题明显有出入。
@Test
void testMemory() throws Exception {
mockMvc.perform( get("/memorychat")
.param("input","你记得我的名字吗?")
.param("conversationId","c456"))
.andExpect(status().isOk() )
.andDo( MockMvcResultHandlers.print() );
}
11.3 清除记忆
可以调用ChatMemory接口的clear方法即可,示例代码如下:
@GetMapping("/clearmemory")
public String clearMemory(String conversationId) {
// 调用clear方法
chatMemory.clear(conversationId);
return "success";
}