上篇介绍了实现短期记忆的核心类和交互流程,这篇用一个基于redis的例子,来打造一个spring ai短期记忆的聊天助手。<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency><dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId></dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>1.1.2.3</version> <scope>compile</scope> </dependency><dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-memory-redis</artifactId> <version>1.1.2.2</version> <!-- 建议与你 dashscope starter 的版本保持一致或相近 --></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency></dependencies>
1)注意这里依赖的是阿里云百炼spring-ai-alibaba-starter-dashscope 2)我们这里还需要引入spring-ai-alibaba-starter-memory-redis,是因为阿里云百炼提供了一个名为JedisRedisChatMemoryRepository的类,它其实就等于上一篇文章里介绍的RedisChatMemoryRepository.3)另外因为我们需要使用到redis,JedisRedisChatMemoryRepository依赖于jedis,所以需要引入spring-boot-starter-data-redis和jedis.@Configurationpublic class AppConfig { @Bean public JedisRedisChatMemoryRepository jedisRedisChatMemoryRepository() { return JedisRedisChatMemoryRepository.builder() .host("192.168.xx.xxx") .port(6379) .keyPrefix("redischatmemoryrepository:test") .build(); } @Bean public ChatMemory chatMemory(JedisRedisChatMemoryRepository repository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(repository) .maxMessages(10) .build(); } @Bean public ChatClient chatClient(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) { MessageChatMemoryAdvisor memoryAdvisor=MessageChatMemoryAdvisor.builder(chatMemory).build(); return chatClientBuilder .defaultAdvisors( memoryAdvisor // 挂载记忆顾问 ) .defaultOptions(DashScopeChatOptions.builder().withMultiModel(true).build()) .build(); }}
1)注入JedisRedisChatMemoryRepository Bean,配置redis的ip,port: @Bean public JedisRedisChatMemoryRepository jedisRedisChatMemoryRepository(RedisConnectionFactory connectionFactory) { return JedisRedisChatMemoryRepository.builder() .host("xxx.xxx.xxx.xxx") .port(6379) .keyPrefix("redischatmemoryrepository:test") .build(); }
这里要注意.keyPrefix("redischatmemoryrepository:test"),这个是用来指定持久化到redis中历史消息的键名前缀,意思是:如果我的sessionId为user-007,那么redis中的键名就是redischatmemoryrepository:testuser-007,另外需要注意,持久化到redis中历史消息的数据结构为队列,这个可从JedisRedisChatMemoryRepository的saveAll方法看得出来:@Overridepublic void saveAll(String conversationId, List<Message> messages) { Assert.hasText(conversationId, "conversationId cannot be null or empty"); Assert.notNull(messages, "messages cannot be null"); Assert.noNullElements(messages, "messages cannot contain null elements");String key = getKeyPrefix() + conversationId; List<String> messageJsons = messages.stream().map(this::serializeMessage).toList();try (RedisConnection connection = redisTemplate.getConnectionFactory().getConnection()) { connection.keyCommands().del(key.getBytes());if (!messageJsons.isEmpty()) {byte[][] values = new byte[messageJsons.size()][];for (int i = 0; i < messageJsons.size(); i++) { values[i] = messageJsons.get(i).getBytes(); } connection.listCommands().rPush(key.getBytes(), values); } } }
其中connection.listCommands().rPush(key.getBytes(), values)正是将messages插入到redis列表的尾部. @Bean public ChatMemory chatMemory(JedisRedisChatMemoryRepository repository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(repository) .maxMessages(10) .build(); }
@Bean public ChatClient chatClient(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) { MessageChatMemoryAdvisor memoryAdvisor=MessageChatMemoryAdvisor.builder(chatMemory).build(); return chatClientBuilder .defaultAdvisors( memoryAdvisor // 挂载记忆顾问 ) .defaultOptions(DashScopeChatOptions.builder().withMultiModel(true).build()) .build(); }
注意这里我们需要挂载MessageChatMemoryAdvisor进行请求前后的拦截.@RestControllerpublic class ChatController {private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient = chatClient; } @GetMapping("/chat") public String chat(@RequestParamString message, @RequestParam(defaultValue = "default-user") String sessionId) { System.out.println("========== 收到 sessionId: " + sessionId + " =========="); // 使用 ChatMemory.CONVERSATION_ID 常量,更安全 //必须用ChatMemory.CONVERSATION_ID,否则报错 String response = chatClient.prompt() .user(message) .advisors(advisor -> advisor .param(ChatMemory.CONVERSATION_ID, sessionId) // 使用常量 ) .call() .content(); return response; }}
第一轮:http://localhost:8080/chat?message=请问什么是大模型?&sessionId=user-007llen redischatmemoryrepository:testuser-007
第二轮:http://localhost:8080/chat?message=那么有哪些分类?&sessionId=user-007这里很明显,大模型记住了上下文,在我没有明确指明是有哪些分类下,它仍然能够正确作答.llen redischatmemoryrepository:testuser-007
第三轮:http://localhost:8080/chat?message=我如何学习?&sessionId=user-001llen redischatmemoryrepository:testuser-007
所以,从上面的两篇文章,我们掌握了spring ai实现短期记忆助手的原理和实践,在这个基础上,我们还要做进一步的优化,例如对于历史消息,我们如何压缩其大小?又如,如何实现长期记忆?