ARTICLE · 1154885
spring-ai2学习文档(第一天)
spring-ai2学习文档(第一天)使用springboot创建一个快速启动的项目,这里不做说明,自己创建就好了。 项目版本说明 Java 17 Spring AI 2.0.1 Spring Boot 4.1.1 Maven 3.6 申请一个大模型的api-key 我习惯使用DeepSeek大模型,所以我申请的是DeepSeek的api-key登录DeepSeek的开放平台,在官网申请所需要的key。 最基础的项目搭建 本项目的pom.xml文件内容 进行项目的初步配置 其中的${SA_STUDY_API}即为申请的api-key,我是将这个key放入了环境变量中,那么官方也建议我们把key放入环境变量中,那么放入环境变量的步骤请看各系统中存放环境变量的步骤。 创建一个controller文件测试项目是否可用 TestController.java 测试请求示例 执行后看浏览器中是否输出对应的回答。 思维模式 DeepSeek 模型支持在思维模式和非思维模式之间切换。思维模式可以通过 Thinking 选项进行控制,该选项接受 Thinking.ENABLED(思维模式)或 Thinking.DISABLED(非思维模式)。当禁用思维时,模型不会生成任何推理内容。 还可以通过构建器便捷方法 enableThinking() 和 disableThinking() 切换思维模式,或者通过 spring.ai.deepseek.chat.thinking.type 属性进行全局配置。 聊天记忆 大语言模型(LLM)是无状态的,这意味着它们不会保留先前交互的信息。当您想在多次交互中维护上下文或状态时,这可能会成为一种限制。为了解决这个问题,Spring AI 提供了聊天记忆(chat memory)功能,允许您在与 LLM 的多次交互中存储和检索信息。 ChatMemory 抽象允许您实现各种类型的记忆以支持不同的用例。消息的底层存储由 ChatMemoryRepository 处理,其唯一职责是存储和检索消息。由 ChatMemory 的实现来决定保留哪些消息以及何时删除它们。策略的示例可以包括保留最后 N 条消息、在特定时间段内保留消息,或者保留消息直至达到特定的 Token 限制。 在选择记忆类型之前,了解聊天记忆(chat memory)和聊天历史(chat history)之间的区别至关重要。 聊天记忆 (Chat Memory)。大语言模型保留并在整个对话中用于维持上下文感知的信息。 聊天历史 (Chat History)。完整的对话历史记录,包括用户与模型之间交换的所有消息。 ChatMemory 抽象旨在管理聊天记忆。它允许您存储和检索与当前对话上下文相关的消息。然而,它并不适合用于存储聊天历史。如果您需要保留所有已交换消息的完整记录,您应该考虑使用其他方法,例如依赖 Spring Data 来高效存储和检索完整的聊天历史记录。 使用会话ID 每个聊天记忆操作都以会话 ID(conversation ID)为键。ChatMemory 和 ChatMemoryRepository 抽象使用此标识符来决定读取、追加或删除哪个会话,并且必须始终显式提供该标识符。 会话 ID 是界定会话范围的唯一要素。系统会针对精确提供的会话 ID 来存储和检索消息,因此选择如何分配这些 ID 决定了交互是被分组在一起还是彼此独立。 在多用户应用程序中,使每个用户的会话 ID(如果一个用户可以有多个会话,则每个会话的 ID)唯一,以便每个用户的消息都保留在他们自己的会话中。常用的方法是在服务器上根据当前用户或会话派生该 ID,而不是使用固定值或共享值。 当应用程序规模增长时,以下几种实践有助于保持会话条理清晰: 记忆类型 在我的项目中只介绍了三种记忆类型: 增加一个Bean的配置: 在controller中增加chatMemory的配置: 增加一个接口用来测试: Mysql数据库 1.增加依赖: 2.application.yml增加 MySQL 配置 3.配置类 AiChatMemoryConfig.java 4.Controller 测试接口 ChatController.java 5.直接操作 Repository(手动读写数据库,不经过 Advisor) 6.测试请求示例 执行后去 MySQL 数据库spring_ai_chat的SPRING_AI_CHAT_MEMORY` 表,即可看到持久化的对话记录。 MySQL 重要注意事项 Redis 替换之前 MySQL (JdbcChatMemoryRepository) 方案,SpringAI2.0.1 Redis 内存仓库 artifact 和配置前缀已经改动,注意不要沿用旧版本配置前置要求:Redis 服务可用,Redis 必须开启 RedisSearch 模块!(核心坑点,不装会直接报错) 1.Maven 依赖 pom.xml 2.application.yml 配置文件 **配置前缀:spring.ai.chat.memory.repository.redis.** 2.0.1 版本不再是 spring.ai.chat.memory.redis ! 3.配置类 RedisChatMemoryConfig.java 4.Controller 接口(测试接口) 重要坑说明(必看) 2. 2.0.1 版本依赖名已经修改,不要用旧的 spring-ai-autoconfigure-model-chat-memory-redis,会自动配置失效Spring Fra... 3. 对比之前 Jdbc 版本: 4. 记忆 TTL:time-to-live 配置会话过期,到期自动删除 Redis 里的对话记录,适合客服会话场景 测试用例 模型能记住你叫张三,会话存在 Redis,重启 SpringBoot 服务记忆不会丢失。
创建项目
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>4.1.1</version><relativePath/></parent><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-deepseek</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>2.0.1</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
server:port: 10000spring:application:name: spring-ai2-studyai:deepseek:api-key: ${SA_STUDY_API}base-url: https://api.deepseek.com/chat:model: deepseek-v4-flash
private final ChatClient chatClient;public TestController(DeepSeekChatModel chatModel) {this.chatClient = ChatClient.builder(chatModel).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()).build();}/*** 简单输出为字符串*/@GetMapping("/chat")public String chat(@RequestParam(name = "question", defaultValue = "你是谁?") String question) {return chatClient.prompt().user(question).call().content();}/*** 流式输出*/@GetMapping("/streamChat")public Flux<String> streamChat(@RequestParam(name = "question", defaultValue = "你是谁?") String question) {return chatClient.prompt().user(question).stream().content();}
GET http://127.0.0.1:8080/chat?question=你好,我叫李四GET http://127.0.0.1:8080/streamChat?question=我叫什么名字?
/*** 非思维模式* @param question 问题* @return Flux*/@GetMapping(value = "/streamChat1", produces = "text/plain;charset=UTF-8")public Flux<String> streamChat1(@RequestParam(name = "question", defaultValue = "你是谁?") String question) {// 关闭思维模式DeepSeekChatOptions chatOptions = DeepSeekChatOptions.builder().thinking(DeepSeekApi.ChatCompletionRequest.Thinking.DISABLED).build();Prompt prompt = new Prompt(question, chatOptions);return chatClient.prompt(prompt).stream().content();}
// Build the conversation ID from the current user/session so each// user's messages are kept in their own conversation.String conversationId = currentUser.getId() + ":" + httpSession.getId();chatClient.prompt().user(userInput).advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)).call().content();
为每个用户分配一个独立的会话 ID,并在单个用户可以拥有多个会话时,为每个会话分配独立的 ID。 在服务器上从用户或会话中派生会话 ID,而不是在不同用户之间重复使用固定值。 在列出或删除会话时,针对属于当前用户的会话 ID 进行操作,而不是针对所有会话 ID。
内存存储库 Mysql数据库 Redis内存存储库
@Beanpublic ChatMemory chatMemory() {return MessageWindowChatMemory.builder().maxMessages(10) // 保留最近的10条.build();}
publicTestController(DeepSeekChatModel chatModel, ChatMemory chatMemory) {this.chatClient = ChatClient.builder(chatModel).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()).build();}
/*** 放在jvm内存中的聊天记忆 MessageWindowChatMemory* @param question 问题* @return Flux*/@GetMapping(value = "/streamChat2", produces = "text/plain;charset=UTF-8")public Flux<String> streamChat2(@RequestParam(name = "question", defaultValue = "你是谁?") String question) {return chatClient.prompt().user(question).advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "xupf")).stream().content();}
<!-- Spring AI 2.0.1 JDBC ChatMemory Repository --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId><version>2.0.1</version></dependency><!-- SpringBoot JDBC --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- MySQL驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- OpenAI模型starter,按需替换成通义千问/文心一言等 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-openai</artifactId><version>2.0.1</version></dependency>
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/spring_ai_chat?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: 你的mysql密码driver-class-name: com.mysql.cj.jdbc.Driverai:chat:memory:repository:jdbc:# 开发环境:always 自动建表;生产环境改成 never,用flyway/liquibase管理DDLinitialize-schema: always
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;import org.springframework.ai.chat.memory.JdbcChatMemoryRepository;import org.springframework.ai.chat.memory.MessageWindowChatMemory;import org.springframework.ai.chat.model.ChatModel;import org.springframework.ai.chat.client.ChatClient;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;@Configurationpublic class AiChatMemoryConfig {@Beanpublic JdbcChatMemoryRepository jdbcChatMemoryRepository(DataSource dataSource) {return JdbcChatMemoryRepository.builder().dataSource(dataSource)// MySQL方言自动识别,不需要手动指定.build();}/*** 消息窗口记忆:最多保留N条消息,超出会淘汰旧消息*/@Beanpublic MessageWindowChatMemory messageWindowChatMemory(JdbcChatMemoryRepository jdbcChatMemoryRepository) {return MessageWindowChatMemory.builder().chatMemoryRepository(jdbcChatMemoryRepository).maxMessages(10).build();}/*** 对话记忆Advisor,自动读写MySQL会话历史*/@Beanpublic MessageChatMemoryAdvisor messageChatMemoryAdvisor(MessageWindowChatMemory chatMemory) {return MessageChatMemoryAdvisor.builder(chatMemory).build();}/*** ChatClient 注入记忆Advisor*/@Beanpublic ChatClient chatClient(ChatModel chatModel, MessageChatMemoryAdvisor memoryAdvisor) {return ChatClient.builder(chatModel).defaultAdvisors(memoryAdvisor).build();}}
import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class ChatController {private final ChatClient chatClient;public ChatController(ChatClient chatClient) {this.chatClient = chatClient;}/*** 同一个conversationId,自动从MySQL读取历史对话*/@GetMapping("/chat")public String chat(@RequestParam String conversationId,@RequestParam String message) {return chatClient.prompt().user(message).advisors(a -> a.param(MessageChatMemoryAdvisor.CONVERSATION_ID, conversationId)).call().content();}}
@Autowiredprivate JdbcChatMemoryRepository repository;// 添加消息到会话repository.add("session_001", new UserMessage("你好,我叫李四"));// 查询会话全部历史消息List<ChatMessage> messages = repository.find("session_001");// 删除整个会话所有记录repository.delete("session_001");
GET http://127.0.0.1:8080/chat?conversationId=session001&message=你好,我叫李四GET http://127.0.0.1:8080/chat?conversationId=session001&message=我叫什么名字?
字符集:必须 utf8mb4,否则 emoji 会存储异常; initialize-schema:生产环境一定改成 never,使用上面的 SQL 脚本提前建表; 限制:JdbcChatMemoryRepository 会忽略工具调用消息,function call 场景不要使用这个实现; 主键:CONVERSATION_ID + SEQUENCE_ID 联合主键,保证同一会话消息有序; 时间时区:连接串务必带上 serverTimezone=Asia/Shanghai,避免时区报错。
Redis 必须安装 RedisSearch 模块!
普通 Redis 默认不带搜索模块,启动会报错。 本地推荐:Redis Stack(一键集成 RedisSearch) 云 Redis:阿里云 / 腾讯云企业版 Redis 开启 RedisSearch 插件
访问:http://127.0.0.1:8080/chat?conversationId=u001&msg=我叫张三 再访问:http://127.0.0.1:8080/chat?conversationId=u001&msg=我叫什么名字?