SpringAI

0 阅读16分钟

一.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程序使用,如下图所示:

ScreenShot_2026-08-20_141032_826.png

第三是要使用的模型名称,可以在模型广场复制得到

三、起步案例

Spring AI 目前支持以语言、图像和音频形式处理输入和输出的模型。

image.png

上表中的最后一行接受文本作为输入并输出数字,通常称为嵌入文本(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
ChatModelSpring‑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 是底层原始接口,追求可控灵活,但几乎所有上层能力都要自己手写,业务直接用会踩很多生产坑。

开发体验层面

  1. 样板代码极多,重复劳动大
  2. 没有内置结构化输出
  3. 工具调用体验笨重
  4. 无任何 Memory 能力

4.2 ChatClient

ChatClient 类似于应用程序开发中的服务层,它为应用程序直接提供 AI 服务,开发者可以快速完成一整套 AI 交互流程的组装。
ChatClient = 上层门面,内部持有 ChatModel,业务开发首选,链式 Fluent API,把消息组装、Prompt、工具调用、记忆、拦截器全部封装好。

ChatClient 优点

  1. 样板代码极少,不用手动组装List<Message>PromptChatOptions;链式 API 可读性高。
  2. 提示词模板原生支持 .param() ,变量替换不用自己拼接字符串。
  3. Advisor 切面扩展机制:日志、RAG、记忆、输出校验统一拦截处理,不需要装饰器包装 ChatModel。
  4. 工具调用全自动循环@Tool注解,自动调用本地 Java 方法、把结果塞回消息,不用手写 while 循环。
  5. 结构化输出 .entity() ,自动处理 markdown ```json,直接映射 POJO,支持校验失败自动重试。
  6. 全局默认配置:builder 设置defaultSystem、defaultTemperature,单次调用可覆盖。
  7. 内置 Memory 支持,一行接入会话记忆。

4.2.1 基础ChatClient实例

  1. 新建类: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接口,用于添加用户提问消息,返回值类型为ChatClientRequestSpec
  • call():本方法来自ChatClientRequestSpec接口,用于触发同步请求,返回CallResponseSpec接口类型对象,通过CallResponseSpec可以获取聊天响应的规范,获取响应内容。
  • content():本方法来自CallResponseSpec接口,返回模型生成的纯文本聊天内容。
  1. 增加测试方法
@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
  1. 在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方法可以实现全局人设设定。

  1. 新建类: 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人设

如果人设的文字内容很多,则不适合在类中硬编码,推荐使用人设配置文件

  1. 在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方法进行局部人设设定,此时会覆盖全局人设。

  1. 修改类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 分配一个专家身份,可以激活其特定领域的知识储备,让回答更专业、更对口。

  1. 新建类: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是实现提示词复用和动态管理的核心。它允许你定义模板,然后在运行时填入具体参数,使代码更简洁、更易维。

  1. 修改类: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 少样本学习:用“例子”框定“格式”

给模型提供一两个“输入-输出”的示例,是约束输出格式、让它快速理解你意图的高效方法。

  1. 修改类: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()方法将上下文信息动态注入到提示词中。

  1. 修改类: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 里要求模型。这能让控制更直接、更稳定。

  1. 修改类: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 的提示词工程思路浓缩为以下三点:

  1. 结构化设计:放弃“拼字符串”的旧习惯。将长期规则当前任务历史记录外部背景分层管理,让提示词的职责清晰,易于维护和扩展。
  2. 配置化管理:将提示词模板、模型参数(temperature 等)和系统角色从代码中抽离,作为外部配置(如文件、数据库)进行管理。这让你无需修改代码和重新部署,就能迭代和优化提示词。
  3. 动态化调用:通过 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 结构化输出单个对象

  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> 接口本身继承了两个接口,这也对应了它的两大核心职责

  1. FormatProvider(调用前) :通过 getFormat() 方法生成格式指令,这些指令会被追加到你的提示词末尾,指导模型按特定结构(如 JSON Schema)输出。
  2. Converter<String, T>(调用后) :通过 convert(String text) 方法,将模型返回的原始文本字符串,转换为你指定的 Java 对象(如 BeanMap 或 List)。

下面的流程图清晰地展示了数据在调用前后的流转过程:
image.png

完整代码示例

  1. 定义目标 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 查询天气案例

  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标签即可。

image.png

八、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模式

  1. 创建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客户端

  1. 修改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基本流程

  1. ETL:读取 PDF/TXT → Token 分块 → Embedding 生成向量 →存入向量库。
  2. 查询:用户提问 → 向量相似度检索召回文档片段 → 把参考上下文塞进 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 会:

  1. 将查询文本也转为向量
  2. 在向量空间中计算余弦相似度欧氏距离
  3. 返回最相近的 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)

  1. 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 系统安装步骤

  1. 管理员身份打开 PowerShell ,运行以下命令安装并设置 WSL 2 为默认版本:
wsl --install
wsl --set-default-version 2

安装完成后,重启电脑。该命令通常会默认安装 Ubuntu 发行版。

  1. 下载与安装 Docker Desktop
  • 前往 Docker 官网 下载 Windows 版安装程序。
  • 双击运行安装程序,在安装选项中务必勾选 "Use WSL 2 instead of Hyper-V" (使用 WSL 2 代替 Hyper-V)。
  • 按照提示完成。
  1. 启动与配置
  • 从开始菜单启动 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)

  1. 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接口有四种实现类:
  1. InMemoryChatMemory:内存型对话存储
  2. CassandraChatMemory:基于 Cassandra 实现(数据持久化并设置过期时间
  3. Neo4jChatMemory:基于 Neo4j 实现(数据持久化无过期时间
  4. 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 新建数据库和表

  1. 数据库名:ai_chat
  2. 建表脚本:
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 会话记忆测试

  1. 第一次向大模型发送请求时,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";
}