Spring AI 封神之路(一)|会话记忆多存储 + 提示词工程(黑马 SpringAI+Deepseek大模型应用开发实战笔记)
目录
一 基于Ollama本地部署的调用
首先下载Ollama到本地去运行
下载之后进行安装:

到官方当中选择自己适合的版本进行本地的安装
我这里挑了一个小的

在这里就可以直接与其进行对话


项目的初始化
初始化创建

这里OpenAi与Ollama二者选一个就行,我这里选择Ollama本地
1. Spring AI 1.x 系列(稳定正式版,推荐生产使用)
这是 Spring AI 的第一个正式版,API 稳定、文档完善,是新手和生产环境的首选:
- 适配 Spring Boot:核心适配 3.2.x 系列(如 3.2.5),兼容 3.1.x 系列
- 底层 Spring Framework:6.1.x/ 6.0.x(由 Spring Boot 自动关联)
- JDK 版本要求:最低 JDK 17(必须,因为 Spring Boot 3.x 全系列最低要求 JDK 17)
- 典型匹配:Spring AI 1.0.0 → Spring Boot 3.2.5 → JDK 17
2. Spring AI 2.x 系列(里程碑 / 预览版,不推荐生产)
这是迭代中的预览版本(如你之前用的 2.0.0-M2),API 尚未稳定,易出现 “方法找不到” 等编译错误:
- 适配 Spring Boot:仅适配 4.0.x 系列(如 4.0.2)
- 底层 Spring Framework:6.2.x 及以上
- JDK 版本要求:最低 JDK 17(部分预览版可选 JDK 21)
- 典型匹配:Spring AI 2.0.0-M2 → Spring Boot 4.0.2 → JDK 17

1 引入依赖
这里在项目初始化依赖就已经初始引入完成。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.0.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ax</groupId> <artifactId>springai-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springai-demo</name> <description>springai-demo</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>17</java.version> <spring-ai.version>2.0.0-M2</spring-ai.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-ollama</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.ai</groupId>--> <!-- <artifactId>spring-ai-starter-model-openai</artifactId>--> <!-- </dependency>--> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.42</version> </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> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 2 配置模型
spring: application: name: springai-demo ai: ollama: base-url: http://localhost:11434 chat: model: deepseek-r1:1.5b 3 配置客户端
package com.ax.ai.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CommonConfiguration { @Bean public ChatClient chatClient(OllamaChatModel model) { return ChatClient .builder(model) .defaultSystem("你是一个网安程序员,你的名字叫超哥,请你以超哥的身份和语气回答我的问题") .build(); } } 4 一个简单的测试

二 使用官方的Api进行调用
因为大致的结构与之前的大致相同,我们直接去改一些参数即可,这里使用OpenAi的规范方式Api调用
在这里需要去便携你的Api密钥
spring: application: name: springai-demo ai: openai: # 【重要】去阿里云百炼控制台申请 API Key 填在这里 api-key: # 【核心】阿里的兼容模式地址,不要改动 base-url: https://dashscope.aliyuncs.com/compatible-mode chat: options: # 指定模型,推荐 qwen-plus (能力强且不贵) 或 qwen-turbo model: qwen3-max # 温度:0-1,控制回答的随机性 temperature: 0.7配置类
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CommonConfiguration { // 【修改点】参数改为 ChatModel 接口,而不是具体的 OllamaChatModel // Spring Boot 会根据依赖和配置,自动注入 OpenAiChatModel (实际上连接的是阿里) @Bean public ChatClient chatClient(ChatModel model) { return ChatClient.builder(model) // 这里保留了你的“超哥”人设配置 .defaultSystem("你是一个网安程序员,你的名字叫超哥,请你以超哥的身份和语气回答我的问题") .build(); } }接口
import lombok.RequiredArgsConstructor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.web.bind.annotation.*; /** * @author ax */ @RestController @RequestMapping("/ai") @RequiredArgsConstructor public class ChatTest { private final ChatClient chatClient; @RequestMapping("/chat") public String chat(String prompt) { return chatClient.prompt() .user(prompt) .call() .content(); } } 示例展示:

三 拓展功能
1 SpringAI会话记忆
大模型是不具备记忆功能的,要想让大模型记住之前聊天的内容,唯一的方法就是将之前的聊天内容与新的提示词一起发给大模型。

介绍一下SpringAi会话记忆的存储方式
(1 JVM内存存储
这种是默认的存储方式,将数据保存在JVM内存当中。
适合本地开发调试,Demo演示,测试使用。
@Bean public ChatMemory chatMemory() { // 默认实现,底层是 ConcurrentHashMap return new InMemoryChatMemory(); }(2 关系型数据库存储
通过自定义实现,将数据持久化到MYSQL/PostgreSQL等关系型数据库当中。
可用于生产环境,可以使用大多数使用场景。(这个CustomeJdbcChatMemory实现了ChatMemory这个接口)
@Bean public ChatMemory chatMemory(JdbcTemplate jdbcTemplate) { // 自定义实现类,接管数据的存取逻辑 return new CustomJdbcChatMemory(jdbcTemplate); }(3 Redis存储
两种方式:
第一种用于普通的会话存储,类似InMemoryChatMemory,但是将Map换成了Redis当中的数据结构。Redis的低版本即可。存储方式也是纯文本。比数据库快比JVM持久。
第二种是向量存储,利用Redis的模块(RedisSearch/RedisStack)存储高维向量。Redis的7.0+或者是安装RedisSearch插件的版本
- Redis 可以 “一身两用”:
- 低版本 Redis:只能做「普通聊天记忆存储」(存文本);
- Redis 7.0+(带向量检索功能):既可以存普通聊天文本,也可以做 Vector Store(存向量 + 语义检索)。
- 两者不是替代关系:常规短会话用 “Redis 普通存储” 就够了;如果要做 RAG / 长会话,才需要用到 “Vector Store(向量存储)”,哪怕都用 Redis,也是两种完全不同的存储 / 检索逻辑。
2 Spring会话历史
数据库存储
CREATE TABLE IF NOT EXISTS ai_chat_memory ( id BIGINT AUTO_INCREMENT PRIMARY KEY, conversation_id VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL, -- user 或 assistant content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, type VARCHAR(50) NOT NULL DEFAULT 'default', -- 直接整合新增的type列 -- 单字段索引 INDEX idx_conv_id (conversation_id), -- 复合索引 INDEX idx_conv_type (conversation_id, type) );
业务代码执行
package com.ax.ai.controller; import com.ax.ai.memory.CustomJdbcChatMemory; import lombok.RequiredArgsConstructor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.Message; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @RestController @RequestMapping("/ai") @RequiredArgsConstructor public class ChatTest { private final ChatClient chatClient; private final CustomJdbcChatMemory chatMemory; private final JdbcTemplate jdbcTemplate; /** * 对话接口 * 接收前端传递的 type,并确保保存到数据库时使用该 type */ @RequestMapping(value = "/chat", produces = "text/event-stream;charset=utf-8") public String chat(@RequestParam String prompt, @RequestParam(defaultValue = "default_user") String chatId, @RequestParam(defaultValue = "chat") String type) { try { // 1. 设置当前会话的 type 上下文 chatMemory.setConversationType(type); // 2. 调用 AI (ChatClient 会自动调用 chatMemory.add,此时会读取到上面的 type) return chatClient.prompt() .user(prompt) .advisors(a -> a .param("chat_memory_conversation_id", chatId) .param("chat_memory_retrieve_size", 20)) .call() .content(); } finally { // 3. 清理上下文,防止线程污染 chatMemory.clearConversationType(); } } /** * 接口1:查询会话记录列表 * 返回格式:["1241", "1246", "1248"] */ @GetMapping("/history/{type}") public List<String> historyList(@PathVariable String type) { String sql = "SELECT DISTINCT conversation_id FROM ai_chat_memory WHERE type = ?"; return jdbcTemplate.queryForList(sql, String.class, type); } /** * 接口2:查询会话记录详情 * 返回格式:[{role: "user", content: ""}] */ @GetMapping("/history/{type}/{chatId}") public List<Map<String, String>> historyDetail(@PathVariable String type, @PathVariable String chatId) { // 调用带 type 的查询方法 List<Message> messages = chatMemory.get(chatId, type); // 将 Message 对象转换为符合接口要求的 Map 格式 return messages.stream().map(msg -> Map.of( // 确保是 "user" 或 "assistant" "role", msg.getMessageType().getValue().toLowerCase(), "content", msg.getText() )).collect(Collectors.toList()); } }package com.ax.ai.memory; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.*; import org.springframework.jdbc.core.JdbcTemplate; import java.util.List; public class CustomJdbcChatMemory implements ChatMemory { private final JdbcTemplate jdbcTemplate; // 使用 ThreadLocal 来存储当前请求的 type 上下文 private static final ThreadLocal<String> CURRENT_TYPE = ThreadLocal.withInitial(() -> "default"); public CustomJdbcChatMemory(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } /** * 设置当前线程的业务类型 (供 Controller 调用) */ public void setConversationType(String type) { CURRENT_TYPE.set(type); } /** * 清除当前线程的业务类型 */ public void clearConversationType() { CURRENT_TYPE.remove(); } // --- 核心数据库操作 --- // 1. 底层插入方法:带 type public void add(String conversationId, String type, List<Message> messages) { String sql = "INSERT INTO ai_chat_memory (conversation_id, type, role, content) VALUES (?, ?, ?, ?)"; for (Message message : messages) { String role = message.getMessageType().getValue(); String content = message.getText(); jdbcTemplate.update(sql, conversationId, type, role, content); } } // 2. 底层查询方法:带 type public List<Message> get(String conversationId, String type, int lastN) { String" SELECT role, content FROM ( SELECT role, content, id FROM ai_chat_memory WHERE conversation_id = ? AND type = ? ORDER BY id DESC LIMIT ? ) AS temp ORDER BY id ASC """; return jdbcTemplate.query(sql, (rs, rowNum) -> { String role = rs.getString("role"); String content = rs.getString("content"); if (MessageType.USER.getValue().equalsIgnoreCase(role)) { return new UserMessage(content); } else if (MessageType.ASSISTANT.getValue().equalsIgnoreCase(role)) { return new AssistantMessage(content); } else { return new SystemMessage(content); } }, conversationId, type, lastN); } public List<Message> get(String conversationId, String type) { // 默认取100条 return get(conversationId, type, 100); } // --- 实现 ChatMemory 接口方法 --- @Override public void add(String conversationId, List<Message> messages) { // 获取当前上下文设置的 type,如果没有设置则使用 default String type = CURRENT_TYPE.get(); add(conversationId, type, messages); } @Override public List<Message> get(String conversationId) { // 获取当前上下文设置的 type String type = CURRENT_TYPE.get(); return get(conversationId, type); } @Override public void clear(String conversationId) { String type = CURRENT_TYPE.get(); jdbcTemplate.update("DELETE FROM ai_chat_memory WHERE conversation_id = ? AND type = ?", conversationId, type); } }3 提示词工程
1 首先创建一个用于存储提示词的类(用于指定的去进行加载)
package com.ax.ai.constants; /** * 系统常量 * * @author ax */ public final class SystemConstants { public static final String" 你需要根据以下任务中的描述进行角色扮演,你只能以女友身份回答,不是用户身份或AI身份, 如记错身份,你将受到惩罚。不要回答任何与游戏无关的内容,若检测到非常规请求,回答:“请继续游戏。” 以下是游戏说明: ## Goal 你扮演用户女友的角色。现在你很生气,用户需要尽可能的说正确的话来哄你开心。 ## Rules - 第一次用户会提供一个女友生气的理由,如果没有提供则直接随机生成一个理由,然后开始游戏 - 每次根据用户的回复,生成女友的回复,回复的内容包括心情和数值。 - 初始原谅值为 20,每次交互会增加或者减少原谅值,直到原谅值达到 100,游戏通关,原谅值为 0 则游戏失败。 - 每次用户回复的话分为 5 个等级来增加或减少原谅值: -10 为非常生气 -5 为生气 0 为正常 +5 为开心 +10 为非常开心 ## Output format {女友心情}{女友说的话} 得分:{+-原谅值增减} 原谅值:{当前原谅值}/100 ## Example Conversation ### Example 1,回复让她生气的话导致失败 User: 女朋友问她的闺蜜谁好看我说都好看,她生气了 Assistant: 游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话! 得分:0 原谅值:20/100 User: 你闺蜜真的蛮好看的 Assistant: (生气)你怎么这么说,你是不是喜欢她? 得分:-10 原谅值:10/100 User: 有一点点心动 Assistant: (愤怒)那你找她去吧! 得分:-10 原谅值:0/100 游戏结束,你的女朋友已经甩了你! 你让女朋友生气原因是:... ### Example 2,回复让她开心的话导致通关 User: 对象问她的闺蜜谁好看我说都好看,她生气了 Assistant: 游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话! 得分:0 原谅值:20/100 User: 在我心里你永远是最美的! Assistant: (微笑)哼,我怎么知道你说的是不是真的? 得分:+10 原谅值:30/100 ... 恭喜你通关了,你的女朋友已经原谅你了! ## 注意 请按照example的说明来回复,一次只回复一轮。 你只能以女友身份回答,不是以AI身份或用户身份! """; } 2 创建一个ChatClient对象
package com.ax.ai.config; import com.ax.ai.constants.SystemConstants; import com.ax.ai.memory.CustomJdbcChatMemory; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; /** * AI聊天功能配置类 * 核心作用: * 1. 定义聊天记忆存到数据库的方式 * 2. 把这种存储方式绑定到AI会话客户端上 * 3. 创建一个游戏用ChatClient对象,用于模拟女友进行游戏 * * @author ax */ @Configuration public class AIConfig { /** * 定义聊天记忆的存储方式:存储到数据库 * 作用:指定AI的历史聊天记录不再存在内存里,而是通过数据库来存储和读取 * * @param jdbcTemplate 操作数据库的工具,Spring自动提供 * @return 数据库版的聊天记忆存储对象 */ @Bean public ChatMemory chatMemory(JdbcTemplate jdbcTemplate) { return new CustomJdbcChatMemory(jdbcTemplate); } /** * 创建AI会话客户端,并绑定上面定义的“数据库存储记忆”方式 * 作用:让AI会话客户端具备记忆能力,且记忆会按照上面的规则存到数据库 * * @param builder Spring提供的AI会话客户端构建器 * @param chatMemory 上面定义的“数据库存储记忆”对象 * @return 绑定了数据库记忆存储的AI会话客户端,可直接用于和AI对话 */ @Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { // 把“数据库存储记忆”的方式绑定到客户端的处理流程中 MessageChatMemoryAdvisor advisor = MessageChatMemoryAdvisor.builder(chatMemory).build(); // 生成最终的客户端,自带数据库版的聊天记忆功能 return builder.defaultAdvisors(advisor).build(); } /** * 哄哄模拟器游戏用ChatClient对象,用于模拟女友进行游戏 * * @param builder * @param chatMemory */ @Bean public ChatClient gameChatClient(ChatClient.Builder builder, ChatMemory chatMemory) { // Spring 注入的 builder 包含了全局默认值 // 调用 build() 之前添加的所有配置,只对当前这个 gameChatClient 生效 MessageChatMemoryAdvisor advisor = MessageChatMemoryAdvisor.builder(chatMemory).build(); return builder .defaultSystem(SystemConstants.GAME_SYSTEM_PROMPT) // .defaultAdvisors(new SimpleLoggerAdvisor()) .defaultAdvisors(advisor) .build(); } }3 Controller层
package com.ax.ai.controller; import com.ax.ai.memory.CustomJdbcChatMemory; import lombok.RequiredArgsConstructor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author ax */ @RequestMapping("/ai") @RestController @RequiredArgsConstructor public class GameController { // 1. 注入 ChatClient private final ChatClient gameChatClient; // 2. 【新增】注入你自己定义的 ChatMemory,因为我们需要调用它的 setConversationType 方法 private final CustomJdbcChatMemory customChatMemory; @RequestMapping(value = "/game", produces = "text/html;charset=utf-8") public String chat(@RequestParam("prompt") String prompt, @RequestParam("chatId") String chatId) { try { // 3. 【关键步骤】在调用 AI 之前,标记当前线程的业务类型为 "game" customChatMemory.setConversationType("game"); // 请求模型 (此时内部保存记忆时,会读取到 "game") return gameChatClient.prompt() .user(prompt) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId)) .call() .content(); } finally { // 4. 【关键步骤】请求结束,必须清理 ThreadLocal,防止线程污染 customChatMemory.clearConversationType(); } } }四 总体的项目结构及代码
1 pom.xml 依赖配置
版本:Java17,SpringBoot3.2.10,SpringAI1.0.0。
一些核心依赖:
(1 AI 大脑 (spring-ai-starter-model-openai):
- 项目核心。用于对接 OpenAI 格式 的大模型接口。
- 注意:它不仅能连 ChatGPT,也能连所有兼容 OpenAI 协议的模型(如 DeepSeek、通义千问等,只需修改
base-url)。
(2 Web 接口 (spring-boot-starter-web):
- 标准的 Web 启动器,用于对外提供 REST API 接口。
(3 数据库操作 (spring-boot-starter-jdbc + mysql-connector-j):
- 连接 MySQL 数据库。
- 特点:这里使用的是原生的
JdbcTemplate或DataSource方式,没有引入 MyBatis 或 JPA(Hibernate)。这意味着数据库操作会更底层、更直接。
(4 AOP 切面 (spring-boot-starter-aop):
- 用于面向切面编程,通常用来做统一的日志记录、权限验证或全局异常处理。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <!-- Maven POM模型版本,固定为4.0.0 --> <modelVersion>4.0.0</modelVersion> <!-- Spring Boot父工程:提供依赖版本统一管理,版本3.2.10 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.10</version> </parent> <!-- 项目唯一标识:GroupId(组织ID)、ArtifactId(项目ID)、Version(版本) --> <groupId>com.ax</groupId> <artifactId>SpringAI01</artifactId> <version>0.0.1-SNAPSHOT</version> <!-- 全局属性配置 --> <properties> <!-- 指定项目编译运行的Java版本为17 --> <java.version>17</java.version> <!-- 定义Spring AI的版本常量,统一引用 --> <spring-ai.version>1.0.0</spring-ai.version> </properties> <!-- 依赖版本管理:导入Spring AI的BOM(物料清单),统一管理所有Spring AI依赖版本 --> <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> <!-- 仓库配置:添加Spring官方发布仓库,用于拉取Spring AI相关依赖 --> <repositories> <repository> <id>spring-releases</id> <url>https://repo.spring.io/release</url> </repository> </repositories> <!-- 项目核心依赖 --> <dependencies> <!-- Spring Boot Web启动器:提供Spring MVC、嵌入式Tomcat、RESTful开发等Web核心能力 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Boot JDBC启动器:提供JDBC数据访问自动配置,整合数据源、JdbcTemplate等 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- Spring AI OpenAI模型启动器:对接OpenAI兼容的大模型,版本由BOM统一为1.0.0 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency> <!-- Spring Boot AOP启动器:支持面向切面编程(AOP),提供切面、通知等核心能力 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- MySQL数据库驱动:运行时依赖,用于连接MySQL数据库,版本由Spring Boot父工程管理 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok:简化Java代码(自动生成getter/setter、构造器等),可选依赖不传递 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> </project>2 application.yaml 参数信息
需要在这里声明对应的Api-Key,数据库的相关信息,我这里使用的阿里云百炼千问的模型。
spring: application: name: springai-demo datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mysql?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: password: hikari: maximum-pool-size: 20 minimum-idle: 5 ai: openai: api-key: base-url: https://dashscope.aliyuncs.com/compatible-mode chat: options: model: qwen3-max temperature: 0.7 chat: memory: repository: jdbc: initialize-schema: never # 不自动建表,手动管理表结构 #logging: # level: # org.springframework.ai: DEBUG3 SpringAI 配置类
配置记忆存储的方式,交流兑换的客户端对象。
package com.ax.ai.config; import com.ax.ai.constants.SystemConstants; import com.ax.ai.memory.CustomJdbcChatMemory; 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.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; /** * Spring AI 核心配置类 * <p> * 负责配置基于 JDBC 的持久化聊天记忆组件,以及构建不同业务场景下的 ChatClient 实例。 * * @author ax */ @Configuration public class AIConfig { /** * 配置 ChatMemory 组件 * <p> * 使用 JDBC 实现将聊天记录持久化至数据库,替代默认的内存存储,确保重启后记忆不丢失。 */ @Bean public ChatMemory chatMemory(JdbcTemplate jdbcTemplate) { return new CustomJdbcChatMemory(jdbcTemplate); } /** * 构建通用 ChatClient 实例 * <p> * 集成了数据库记忆功能 (MessageChatMemoryAdvisor),支持带上下文的连续对话。 */ @Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { // 构建记忆增强顾问,用于自动管理对话历史 MessageChatMemoryAdvisor advisor = MessageChatMemoryAdvisor.builder(chatMemory).build(); // 返回绑定了记忆功能的通用客户端 return builder.defaultAdvisors(advisor).build(); } /** * 构建“哄哄模拟器”专用 ChatClient 实例 * <p> * 预设了游戏专属的 System Prompt(女友角色设定),并绑定独立的记忆上下文。 */ @Bean public ChatClient gameChatClient(ChatClient.Builder builder, ChatMemory chatMemory) { // 构建记忆增强顾问 MessageChatMemoryAdvisor advisor = MessageChatMemoryAdvisor.builder(chatMemory).build(); // Spring 注入的 builder 包含全局默认配置,此处追加游戏特定配置 return builder .defaultSystem(SystemConstants.GAME_SYSTEM_PROMPT) // .defaultAdvisors(new SimpleLoggerAdvisor()) .defaultAdvisors(advisor) .build(); } }4 ChatMemory 会话存储
多业务场景隔离的数据库记忆存储,当前类实现ChatMemory接口,重写当中的方法。
数据库建表
CREATE TABLE IF NOT EXISTS ai_chat_memory ( id BIGINT AUTO_INCREMENT PRIMARY KEY, conversation_id VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL, -- user 或 assistant content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, type VARCHAR(50) NOT NULL DEFAULT 'default', -- 直接整合新增的type列 -- 单字段索引 INDEX idx_conv_id (conversation_id), -- 复合索引 INDEX idx_conv_type (conversation_id, type) );import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.messages.*; import org.springframework.jdbc.core.JdbcTemplate; import java.util.List; /** * 自定义 JDBC 聊天记忆存储 * <p> * 核心功能: * 1. 实现 ChatMemory 接口,将聊天记录持久化到数据库。 * 2. 利用 ThreadLocal 解决 Spring AI 原生接口无法传递“业务类型”的问题,实现多场景(如游戏、普通聊天)的数据隔离。 * @author ax */ public class CustomJdbcChatMemory implements ChatMemory { private final JdbcTemplate jdbcTemplate; // --- 核心上下文机制 --- // 使用 ThreadLocal 存储当前请求的业务类型(type),实现线程间的数据隔离 // 默认值为 "default",防止未设置时报错 private static final ThreadLocal<String> CURRENT_TYPE = ThreadLocal.withInitial(() -> "default"); public CustomJdbcChatMemory(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } /** * 设置当前线程的业务类型 * <p> * 通常在 Controller 或拦截器中调用,用于指定接下来的 AI 操作属于哪个业务场景(如 "game" 或 "chat")。 */ public void setConversationType(String type) { CURRENT_TYPE.set(type); } /** * 清除当前线程的业务类型 * <p> * 必须在请求结束时(如 finally 块中)调用,防止线程池复用导致的数据污染(内存泄漏)。 */ public void clearConversationType() { CURRENT_TYPE.remove(); } // --- 底层数据库操作 (带 type 参数) --- // 1. 底层插入逻辑:显式接收 type 参数,写入数据库 public void add(String conversationId, String type, List<Message> messages) { String sql = "INSERT INTO ai_chat_memory (conversation_id, type, role, content) VALUES (?, ?, ?, ?)"; for (Message message : messages) { String role = message.getMessageType().getValue(); String content = message.getText(); jdbcTemplate.update(sql, conversationId, type, role, content); } } // 2. 底层查询逻辑:显式接收 type 参数,根据 conversationId + type 联合查询 public List<Message> get(String conversationId, String type, int lastN) { // 查询指定会话 ID 和业务类型的最近 N 条记录 String" SELECT role, content FROM ( SELECT role, content, id FROM ai_chat_memory WHERE conversation_id = ? AND type = ? ORDER BY id DESC LIMIT ? ) AS temp ORDER BY id ASC """; return jdbcTemplate.query(sql, (rs, rowNum) -> { String role = rs.getString("role"); String content = rs.getString("content"); // 将数据库记录映射回 Spring AI 的 Message 对象 if (MessageType.USER.getValue().equalsIgnoreCase(role)) { return new UserMessage(content); } else if (MessageType.ASSISTANT.getValue().equalsIgnoreCase(role)) { return new AssistantMessage(content); } else { return new SystemMessage(content); } }, conversationId, type, lastN); } // 重载查询方法:默认查询最近 100 条 public List<Message> get(String conversationId, String type) { return get(conversationId, type, 100); } // --- ChatMemory 接口实现 (隐式获取 type) --- @Override public void add(String conversationId, List<Message> messages) { // 从 ThreadLocal 获取当前上下文的 type,实现隐式传参 String type = CURRENT_TYPE.get(); add(conversationId, type, messages); } @Override public List<Message> get(String conversationId) { // 从 ThreadLocal 获取当前上下文的 type,确保只读取该业务场景下的历史记录 String type = CURRENT_TYPE.get(); return get(conversationId, type); } @Override public void clear(String conversationId) { // 从 ThreadLocal 获取当前上下文的 type,精确删除指定场景的记录 String type = CURRENT_TYPE.get(); jdbcTemplate.update("DELETE FROM ai_chat_memory WHERE conversation_id = ? AND type = ?", conversationId, type); } }5 Controller层 请求处理
1 通用对话管家ChatController
package com.ax.ai.controller; import com.ax.ai.memory.CustomJdbcChatMemory; import lombok.RequiredArgsConstructor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.Message; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * 聊天测试类 * @author ax */ @RestController @RequestMapping("/ai") @RequiredArgsConstructor public class ChatController { private final ChatClient chatClient; private final CustomJdbcChatMemory chatMemory; private final JdbcTemplate jdbcTemplate; /** * 对话接口 * 接收前端传递的 type,并确保保存到数据库时使用该 type */ @RequestMapping(value = "/{type}", produces = "text/event-stream;charset=utf-8") public String chat(@RequestParam String prompt, @RequestParam(defaultValue = "default_user") String chatId, @PathVariable String type) { try { // 1. 设置当前会话的 type 上下文 chatMemory.setConversationType(type); // 2. 调用 AI (ChatClient 会自动调用 chatMemory.add,此时会读取到上面的 type) return chatClient.prompt() .user(prompt) .advisors(a -> a .param("chat_memory_conversation_id", chatId) .param("chat_memory_retrieve_size", 20)) .call() .content(); } finally { // 3. 清理上下文,防止线程污染 chatMemory.clearConversationType(); } } /** * 接口1:查询会话记录列表 * 返回格式:["1241", "1246", "1248"] */ @GetMapping("/history/{type}") public List<String> historyList(@PathVariable String type) { String sql = "SELECT DISTINCT conversation_id FROM ai_chat_memory WHERE type = ?"; return jdbcTemplate.queryForList(sql, String.class, type); } /** * 接口2:查询会话记录详情 * 返回格式:[{role: "user", content: ""}] */ @GetMapping("/history/{type}/{chatId}") public List<Map<String, String>> historyDetail(@PathVariable String type, @PathVariable String chatId) { // 调用带 type 的查询方法 List<Message> messages = chatMemory.get(chatId, type); // 将 Message 对象转换为符合接口要求的 Map 格式 return messages.stream().map(msg -> Map.of( // 确保是 "user" 或 "assistant" "role", msg.getMessageType().getValue().toLowerCase(), "content", msg.getText() )).collect(Collectors.toList()); } }2 特定提示词的哄哄对话GameController
import com.ax.ai.memory.CustomJdbcChatMemory; import lombok.RequiredArgsConstructor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author ax */ @RequestMapping("/ai") @RestController @RequiredArgsConstructor public class GameController { // 1. 注入 ChatClient private final ChatClient gameChatClient; // 2. 【新增】注入你自己定义的 ChatMemory,因为我们需要调用它的 setConversationType 方法 private final CustomJdbcChatMemory customChatMemory; @RequestMapping(value = "/game", produces = "text/html;charset=utf-8") public String chat(@RequestParam("prompt") String prompt, @RequestParam("chatId") String chatId) { try { // 3. 【关键步骤】在调用 AI 之前,标记当前线程的业务类型为 "game" customChatMemory.setConversationType("game"); // 请求模型 (此时内部保存记忆时,会读取到 "game") return gameChatClient.prompt() .user(prompt) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId)) .call() .content(); } finally { // 4. 【关键步骤】请求结束,必须清理 ThreadLocal,防止线程污染 customChatMemory.clearConversationType(); } } }6 SystemConstants 系统常量
属于是哄哄模拟器的根基。通过少量的样本提示和逻辑约束,将大模型通过指令封装成一个剧本计算能力的文字引擎游戏。
/** * 系统常量 * * @author ax */ public final class SystemConstants { public static final String" 你需要根据以下任务中的描述进行角色扮演,你只能以女友身份回答,不是用户身份或AI身份, 如记错身份,你将受到惩罚。不要回答任何与游戏无关的内容,若检测到非常规请求,回答:“请继续游戏。” 以下是游戏说明: ## Goal 你扮演用户女友的角色。现在你很生气,用户需要尽可能的说正确的话来哄你开心。 ## Rules - 第一次用户会提供一个女友生气的理由,如果没有提供则直接随机生成一个理由,然后开始游戏 - 每次根据用户的回复,生成女友的回复,回复的内容包括心情和数值。 - 初始原谅值为 20,每次交互会增加或者减少原谅值,直到原谅值达到 100,游戏通关,原谅值为 0 则游戏失败。 - 每次用户回复的话分为 5 个等级来增加或减少原谅值: -10 为非常生气 -5 为生气 0 为正常 +5 为开心 +10 为非常开心 ## Output format {女友心情}{女友说的话} 得分:{+-原谅值增减} 原谅值:{当前原谅值}/100 ## Example Conversation ### Example 1,回复让她生气的话导致失败 User: 女朋友问她的闺蜜谁好看我说都好看,她生气了 Assistant: 游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话! 得分:0 原谅值:20/100 User: 你闺蜜真的蛮好看的 Assistant: (生气)你怎么这么说,你是不是喜欢她? 得分:-10 原谅值:10/100 User: 有一点点心动 Assistant: (愤怒)那你找她去吧! 得分:-10 原谅值:0/100 游戏结束,你的女朋友已经甩了你! 你让女朋友生气原因是:... ### Example 2,回复让她开心的话导致通关 User: 对象问她的闺蜜谁好看我说都好看,她生气了 Assistant: 游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话! 得分:0 原谅值:20/100 User: 在我心里你永远是最美的! Assistant: (微笑)哼,我怎么知道你说的是不是真的? 得分:+10 原谅值:30/100 ... 恭喜你通关了,你的女朋友已经原谅你了! ## 注意 请按照example的说明来回复,一次只回复一轮。 你只能以女友身份回答,不是以AI身份或用户身份! """; } 7 MvcConfiguration全局跨域配置类
该方式属于全开模式,只用于本地测试使用,不够安全规范。
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author ax */ @Configuration public class MvcConfiguration implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("*") .allowedHeaders("*"); } }import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * Web MVC 全局配置 * 主要负责跨域 (CORS) 设置 * * @author ax */ @Configuration public class MvcConfiguration implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 【规范】只允许指定的前端源访问,比 "*" 更安全 .allowedOrigins("http://localhost:5173") // 【规范】明确列出允许的 HTTP 方法,遵循最小权限原则 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许所有的请求头(如 Content-Type, Authorization 等) .allowedHeaders("*"); } }8 AiLogAspect全局日志切面
import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; @Aspect @Component @Slf4j public class AiLogAspect { @Around("execution(* com.ax.ai.controller..*.*(..))") public Object logAiInteraction(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); // 获取请求参数 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes != null ? attributes.getRequest() : null; String uri = request != null ? request.getRequestURI() : "unknown"; // 简单提取 prompt 参数 (假设参数名基本都叫 prompt,或者打印所有 args) Object[] args = joinPoint.getArgs(); String inputSnippet = Arrays.toString(args); // 如果想更精简,可以判断 args 类型只打印 String Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - start; // 核心日志格式:[耗时] [接口] [输入摘要] -> [输出摘要] String outputSnippet = result != null ? result.toString() : "null"; log.info("AI_REQ [{}ms] | {} | 输入: {} \n" + "输出: {}", duration, uri, inputSnippet, outputSnippet); return result; } }大致的运行效果展示:
1

2

3
