找回密码
 立即注册
首页 业界区 业界 7. Spring AI 对话记忆 + 结构化输出

7. Spring AI 对话记忆 + 结构化输出

米嘉怡 4 天前
7. Spring AI 对话记忆 + 结构化输出

@
目录

  • 7. Spring AI 对话记忆 + 结构化输出

    • 对话记忆

      • PromptChatMemoryAdvisor
      • 配置聊天记录最大存储数量
      • 配置多用户隔离记忆
      • 原理源码$
      • 数据库存储对话记忆
      • Redis存储
      • 多层次记忆架构 痛点

    • 结构化输出

      • 基础类型:
      • Pojo类型:
      • 原理


  • 最后:

对话记忆

大型语言模型 (LLM) 是无状态的,这意味着它们不会保留先前交互的信息。
  1. @Test
  2.     public void testChatOptions() {
  3.         String content = chatClient.prompt()
  4.                 .user("我叫小兔子 ")
  5.                 .call()
  6.                 .content();
  7.         System.out.println(content);
  8.         System.out.println("--------------------------------------------------------------------------");
  9.        content = chatClient.prompt()
  10.                 .user("我叫什么 ?")
  11.                 .call()
  12.                 .content();
  13.         System.out.println(content);
  14.     }
复制代码
1.png

那我们平常跟一些大模型聊天是怎么记住我们对话的呢?实际上,每次对话都需要将之前的对话消息内置发送给大模型,这种方式称为多轮对话。
2.png

SpringAi提供了一个ChatMemory的组件用于存储聊天记录,允许您使用 LLM 跨多个交互存储和检索信息。并且可以为不同用户的多个交互之间维护上下文或状态。
可以在每次对话的时候把当前聊天信息和模型的响应存储到ChatMemory, 然后下一次对话把聊天记录取出来再发给大模型。
  1. `
  2. //输出 名字叫徐庶
复制代码
但是这样做未免太麻烦! 能不能简化? 思考一下!
3.jpeg

4.gif
用我们之前的Advisor对话拦截是不是就可以不用每次手动去维护了。 并且SpringAi早已体贴的为我提供了ChatMemoryAutoConfiguration自动配置类
  1. <dependency>
  2.   <groupId>org.springframework.ai</groupId>
  3.   spring-ai-autoconfigure-model-chat-memory</artifactId>
  4. </dependency>
复制代码
  1. @AutoConfiguration
  2. @ConditionalOnClass({ ChatMemory.class, ChatMemoryRepository.class })
  3. public class ChatMemoryAutoConfiguration {
  4.         @Bean
  5.         @ConditionalOnMissingBean
  6.         ChatMemoryRepository chatMemoryRepository() {
  7.                 return new InMemoryChatMemoryRepository();
  8.         }
  9.         @Bean
  10.         @ConditionalOnMissingBean
  11.         ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
  12.                 return MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).build();
  13.         }
  14. }
复制代码
所以我们可以这样用:
PromptChatMemoryAdvisor

SpringAi提供了 PromptChatMemoryAdvisor 专门用于对话记忆的拦截
  1. @SpringBootTest
  2. public class ChatMemoryTest {
  3.     ChatClient chatClient;
  4.     @BeforeEach
  5.     public  void init(@Autowired
  6.                       DeepSeekChatModel chatModel,
  7.                       @Autowired
  8.                       ChatMemory chatMemory) {
  9.         chatClient = ChatClient
  10.                 .builder(chatModel)
  11.                 .defaultAdvisors(
  12.                     // PromptChatMemoryAdvisor拦截器 就会自动将我们与大模型的历史对话记录下来
  13.                         PromptChatMemoryAdvisor.builder(chatMemory).build()
  14.                 )
  15.                 .build();
  16.     }
  17.     @Test
  18.     public void testChatOptions() {
  19.         String content = chatClient.prompt()
  20.                 .user("我叫徐庶 ?")
  21.         //
  22.                 .advisors(new ReReadingAdvisor())
  23.                 .call()
  24.                 .content();
  25.         System.out.println(content);
  26.         System.out.println("--------------------------------------------------------------------------");
  27.         content = chatClient.prompt()
  28.                 .user("我叫什么 ?")
  29.                 .advisors(new ReReadingAdvisor())
  30.                 .call()
  31.                 .content();
  32.         System.out.println(content);
  33.     }
  34. }
复制代码
配置聊天记录最大存储数量

你要知道, 我们把聊天记录发给大模型, 都是算token计数的。
大模型的token是有上限了, 如果你发送过多聊天记录,可能就会导致token过长。
如下是大模型存储的 token 历史条数上限。
5.png

并且更多的token也意味更多的费用, 更久的解析时间. 所以不建议太长
(DEFAULT_MAX_MESSAGES默认20即10次对话)
一旦超出DEFAULT_MAX_MESSAGES只会存最后面N条(可以理解为先进先出),参考MessageWindowChatMemory源码
  1. @Bean
  2.    ChatMemory chatMemory(@Autowired ChatMemoryRepository chatMemoryRepository) {
  3. // MessageWindowChatMemory 创建一个历史对话存储的配置,
  4.         return MessageWindowChatMemory
  5.                 .builder()
  6.                 .maxMessages(10)  // 设置最大存储 10 条
  7.                 .chatMemoryRepository(chatMemoryRepository).build();
  8.     }
复制代码
配置多用户隔离记忆

如果有多个用户在进行对话, 肯定不能将对话记录混在一起, 不同的用户的对话记忆需要隔离
  1. @Test
  2.     public void testChatOptions() {
  3.         String content = chatClient.prompt()
  4.                 .user("我叫徐庶 ?")
  5.         // 注意:这里要先构建一个 ChatMemory的 Bean,和上面类似,这里我们设置历史对话的用户ID
  6.                 .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID,"1"))
  7.                 .call()
  8.                 .content();
  9.         System.out.println(content);
  10.         System.out.println("--------------------------------------------------------------------------");
  11.         content = chatClient.prompt()
  12.                 .user("我叫什么 ?")
  13.                 .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID,"1"))
  14.                 .call()
  15.                 .content();
  16.         System.out.println(content);
  17.         System.out.println("--------------------------------------------------------------------------");
  18.         content = chatClient.prompt()
  19.                 .user("我叫什么 ?")
  20.                 .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID,"2"))
  21.                 .call()
  22.                 .content();
  23.         System.out.println(content);
  24.     }
复制代码
会发现, 不同的CONVERSATION_ID,会有不同的记忆
6.png

原理源码$

主要有前置存储
MessageWindowChatMemory
具体存储实现
ChatMemoryRepository
7.png

数据库存储对话记忆

默认情况, 对话内容会存在jvm内存会导致:

  • 一直存最终会撑爆JVM导致OOM。
  • 重启就丢了, 如果已想存储到第三方存储进行持久化
springAi内置提供了以下几种方式(例如 Cassandra、JDBC 或 Neo4j), 这里演示下JDBC方式

  • 添加依赖
  1.         <dependency>
  2.             <groupId>org.springframework.ai</groupId>
  3.             spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
  4.         </dependency>
  5.         
  6.         <dependency>
  7.             <groupId>org.springframework.boot</groupId>
  8.             spring-boot-starter-jdbc</artifactId>
  9.         </dependency>
  10.         
  11.         <dependency>
  12.             <groupId>com.mysql</groupId>
  13.             mysql-connector-j</artifactId>
  14.             <scope>runtime</scope>
  15.         </dependency>
复制代码

  • 添加配置(目前我们的需要创建一个schema-mysql.sql 文件,就是一个 SQL 脚本,在后面有配置)SPRING_AI_CHAT_MEMORY 表存储用户的历史对话,数据库,我们自行定义将该数据表存储到那个数据库中即可。
  1. spring.ai.chat.memory.repository.jdbc.initialize-schema=always
  2. spring.ai.chat.memory.repository.jdbc.schema=classpath:/schema-mysql.sql
复制代码
如下是 MySQL 的配置:
  1. spring:
  2.   datasource:
  3.     username: root
  4.     password: 123456
  5.     url: jdbc:mysql://localhost:3306/springai?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&
  6.     driver-class-name: com.mysql.cj.jdbc.Driver
复制代码

  • 配置类
  1. @Configuration
  2. public class ChatMemoryConfig {
  3.     @Bean  // JdbcChatMemoryRepository 是已经被封装好自动装配好了,就可以使用
  4.     ChatMemory chatMemory(@Autowired JdbcChatMemoryRepository chatMemoryRepository) {
  5.         return MessageWindowChatMemory
  6.         .builder()
  7.         .maxMessages(1)  // 设置存储为上面我们传的变量的 jdbc 的存储方式
  8.         .chatMemoryRepository(chatMemoryRepository).build();
  9.     }
  10. }
复制代码

  • resources/schema-mysql.sql(目前1.0.0版本需要自己定义,没有提供脚本),创建这个SPRING_AI_CHAT_MEMORY 数据表,来存储用户的历史对话
  1. CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
  2.     `conversation_id` VARCHAR(36) NOT NULL,
  3.     `content` TEXT NOT NULL,
  4.     `type` VARCHAR(10) NOT NULL,
  5.     `timestamp` TIMESTAMP NOT NULL,
  6.     INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`, `timestamp`)
  7.     );
复制代码

  • 测试
  1. @SpringBootTest
  2. public class ChatMemoryTest {
  3.     ChatClient chatClient;
  4.     @BeforeEach
  5.     public  void init(@Autowired
  6.                       DeepSeekChatModel chatModel,
  7.                       @Autowired
  8.                       ChatMemory chatMemory) {
  9.         chatClient = ChatClient
  10.                 .builder(chatModel)
  11.                 .defaultAdvisors(
  12.                         PromptChatMemoryAdvisor.builder(chatMemory).build()
  13.                 )
  14.                 .build();
  15.     }
  16.     @Test
  17.     public void testChatOptions() {
  18.         String content = chatClient.prompt()
  19.                 .user("你好,我叫徐庶!")
  20.                 .advisors(new ReReadingAdvisor())
  21.                 .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID,"1"))
  22.                 .call()
  23.                 .content();
  24.         System.out.println(content);
  25.         System.out.println("--------------------------------------------------------------------------");
  26.         content = chatClient.prompt()
  27.                 .user("我叫什么 ?")
  28.                 .advisors(new ReReadingAdvisor())
  29.                 .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID,"1"))
  30.                 .call()
  31.                 .content();
  32.         System.out.println(content);
  33.     }
  34. }
复制代码
可以看到由于我设置.maxMessages(1)数据库只存一条
8.png

扩展:其实我可以让其存储到我们的所有历史对话,因为数据库就是存储数据的吗,而且存储起来的数据可能还是可以用于我们分析测试用户的对话的。那我们取数据库的时候,可以获取最新一条的数据即可。
Redis存储

如果你想用redis , 你需要自己实现ChatMemoryRepository接口(自己实现增、删、查)
Redis 存储更快,我们如果一般就是仅仅只是临时存储用户的 100 条记录什么的,就存到 Redis 当中就好了,超过了 100 条记录,也是会存储到用户最新的那 100 条记录,后续的内容也不用持久化了,丢了就丢了,反正都是用户的一个临时历史对话记录而已。性能也是比 MySQL 更快的。
但是alibaba-ai有现成的实现:(还包括ES)
https://github.com/alibaba/spring-ai-alibaba/tree/main/community/memories
  1.       <properties>
  2.         <jedis.version>5.2.0</jedis.version>
  3.       </properties>
  4.       
  5.       <dependency>
  6.         <groupId>com.alibaba.cloud.ai</groupId>
  7.         spring-ai-alibaba-starter-memory-redis</artifactId>
  8.       </dependency>
  9.       
  10.       
  11.       <dependency>
  12.         <groupId>redis.clients</groupId>
  13.         jedis</artifactId>
  14.         <version>${jedis.version}</version>
  15.       </dependency>
复制代码
  1. spring:
  2.   ai:
  3.     memory:
  4.       redis:
  5.         host: localhost
  6.         port: 6379
  7.         timeout:  5000
  8.         password:
复制代码
  1. @Configuration
  2. public class RedisMemoryConfig {
  3.     @Value("${spring.ai.memory.redis.host}")
  4.     private String redisHost;
  5.     @Value("${spring.ai.memory.redis.port}")
  6.     private int redisPort;
  7.     @Value("${spring.ai.memory.redis.password}")
  8.     private String redisPassword;
  9.     @Value("${spring.ai.memory.redis.timeout}")
  10.     private int redisTimeout;
  11.     @Bean
  12.     public RedisChatMemoryRepository redisChatMemoryRepository() {
  13.         return RedisChatMemoryRepository._builder_()
  14.                 .host(redisHost)
  15.                 .port(redisPort)
  16.                 // 若没有设置密码则注释该项
  17. //           .password(redisPassword)
  18.                 .timeout(redisTimeout)
  19.                 .build();
  20.     }
  21.    
  22.     @Bean  // RedisChatMemoryRepository 被我们上面 Bean 注入了
  23.     ChatMemory chatMemory(@Autowired RedisChatMemoryRepository redisMemoryRepository) {
  24.         return MessageWindowChatMemory
  25.         .builder()
  26.         .maxMessages(1)  // 设置存储为上面我们传的变量的 jdbc 的存储方式
  27.         .chatMemoryRepository(redisMemoryRepository).build();
  28.     }
  29. }
复制代码
多层次记忆架构 痛点

记忆多=聪明(大模型记录了多了用户的历史对话记录,就更加能够理解我们,实现我们的需求了), 但是记忆多会触发 token 上限(每个大模型的 token 是有上限的,不可以无限的存储。)
要知道, 无论你用什么存储对话以及, 也只能保证服务端的存储性能。
但是一旦聊天记录多了依然会超过token上限, 但是有时候我们依然希望存储更多的聊天记录,这样才能保证整个对话更像“人”。
多层次记忆架构(模仿人类)

  • 近期记忆:保留在上下文窗口中的最近几轮对话,每轮对话完成后立即存储(可通过ChatMemory); 10 条
  • 中期记忆:通过RAG检索的相关历史对话(每轮对话完成后,异步将对话内容转换为向量并存入向量数据库) 5条
  • 长期记忆:关键信息的固化总结

    • 方式一:定时批处理
      + 通过定时任务(如每天或每周)对积累的对话进行总结和提炼
      + 提取关键信息、用户偏好、重要事实等
      + 批处理方式降低计算成本,适合大规模处理
    • 方式二:关键点实时处理
      + 在对话中识别出关键信息点时立即提取并存储
      + 例如,当用户明确表达偏好、提供个人信息或设置持久性指令时


结构化输出

基础类型:

以Boolean为例 , 在 agent 中可以用于判定用于的内容2个分支, 不同的分支走不同的逻辑
  1. ChatClient chatClient;
  2. @BeforeEach
  3. public  void init(@Autowired
  4.                   DashScopeChatModel chatModel) {
  5.     chatClient = ChatClient.builder(chatModel).build();
  6. }
  7. @Test
  8. public void testBoolOut() {
  9.     Boolean isComplain = chatClient
  10.     .prompt()
  11.     .system("""
  12.             请判断用户信息是否表达了投诉意图?
  13.             只能用 true 或 false 回答,不要输出多余内容
  14.             """)
  15.     .user("你们家的快递迟迟不到,我要退货!")
  16.     .call()
  17.     .entity(Boolean.class);  // 结构化输出,让大模型输出 Boolean.class java当中的布尔值类型
  18.     // 分支逻辑
  19.     if (Boolean.TRUE.equals(isComplain)) {
  20.         System.out.println("用户是投诉,转接人工客服!");
  21.     } else {
  22.         System.out.println("用户不是投诉,自动流转客服机器人。");
  23.         // todo 继续调用 客服ChatClient进行对话
  24.     }
  25. }
复制代码
Pojo类型:

用购物APP应该见过复制一个地址, 自动为你填入每个输入框。 用大模型轻松完成!
9.png
  1. public record Address(
  2.     String name,        // 收件人姓名
  3.     String phone,       // 联系电话
  4.     String province,    // 省
  5.     String city,        // 市
  6.     String district,    // 区/县
  7.     String detail       // 详细地址
  8. ) {}
  9. @Test
  10.     public void testEntityOut() {
  11.         Address address = chatClient.prompt()
  12.         // .systemshi 是一个系统提示词,优先级更高
  13.                 .system("""
  14.                         请从下面这条文本中提取收货信息
  15.                         """)
  16.                 .user("收货人:张三,电话13588888888,地址:浙江省杭州市西湖区文一西路100号8幢202室")
  17.                 .call()
  18.                 .entity(Address.class);  // 大模型会根据文本内容,将其中的用户的对话信息识别存储到我们的 Address的对象类
  19.         System.out.println(address);
  20.     }
复制代码
  1. public record Address(
  2.     String name,        // 收件人姓名
  3.     String phone,       // 联系电话
  4.     String province,    // 省
  5.     String city,        // 市
  6.     String district,    // 区/县
  7.     String detail       // 详细地址
  8. ) {}
复制代码
原理

10.jpeg

ChatModel或者直接使用低级API:
  1. @Test
  2.     public void testLowEntityOut(
  3.            @Autowired DashScopeChatModel chatModel) {
  4.         // BeanOutputConverter 转换器
  5.         BeanOutputConverter beanOutputConverter =
  6.                 new BeanOutputConverter<>(ActorsFilms.class);
  7.         String format = beanOutputConverter.getFormat();
  8.         String actor = "周星驰";
  9.         String template = """
  10.         提供5部{actor}导演的电影.
  11.         {format}
  12.         """;
  13.         PromptTemplate promptTemplate = PromptTemplate.builder().template(template).variables(Map.of("actor", actor, "format", format)).build();
  14.         ChatResponse response = chatModel.call(
  15.                 promptTemplate.create()
  16.         );
  17.         ActorsFilms actorsFilms = beanOutputConverter.convert(response.getResult().getOutput().getText());
  18.         System.out.println(actorsFilms);
  19.     }
复制代码
最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
11.gif


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册