乐于分享
好东西不私藏

Spring ai如何实现带短期记忆的聊天助手(下)

Spring ai如何实现带短期记忆的聊天助手(下)
  上篇介绍了实现短期记忆的核心类和交互流程,这篇用一个基于redis的例子,来打造一个spring ai短期记忆的聊天助手。
1.marven依赖
<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.
2.AppConfig
@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列表的尾部.
2)注入ChatMemory Bean
	  @Bean	  public ChatMemory chatMemory(JedisRedisChatMemoryRepository repository) {		  return MessageWindowChatMemory.builder()  				                 .chatMemoryRepository(repository)				                 .maxMessages(10) 				                 .build();	  }
这里注意保存的最大历史消息数目是10个.
3)注入ChatClient bean
  @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进行请求前后的拦截.
3.Controller
@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;    }}
4.测试
第一轮:http://localhost:8080/chat?message=请问什么是大模型?&sessionId=user-007
返回:
在redis-cli中查看键的长度:
llen redischatmemoryrepository:testuser-007
返回:2
第二轮:http://localhost:8080/chat?message=那么有哪些分类?&sessionId=user-007
这里很明显,大模型记住了上下文,在我没有明确指明是有哪些分类下,它仍然能够正确作答.
在redis-cli中查看键的长度:
llen redischatmemoryrepository:testuser-007
返回:4
第三轮:http://localhost:8080/chat?message=我如何学习?&sessionId=user-001
同样大模型记住了上下文.
在redis-cli中查看键的长度:
llen redischatmemoryrepository:testuser-007
返回:6
  所以,从上面的两篇文章,我们掌握了spring ai实现短期记忆助手的原理和实践,在这个基础上,我们还要做进一步的优化,例如对于历史消息,我们如何压缩其大小?又如,如何实现长期记忆?