松濤 發表於 2025-9-29 15:51:00

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

<h1 id="7-spring-ai-对话记忆--结构化输出">7. Spring AI 对话记忆 + 结构化输出</h1>
<p>@</p><div class="toc"><div class="toc-container-header">目录</div><ul><li>7. Spring AI 对话记忆 + 结构化输出<ul><li>对话记忆<ul><li>PromptChatMemoryAdvisor</li><li><font style="color: rgba(25, 30, 30, 1)">配置聊天记录最大存储数量</font></li><li>配置多用户隔离记忆</li><li>原理源码$</li><li>数据库存储对话记忆</li><li>Redis存储</li><li><strong><font style="color: rgba(6, 8, 31, 0.88)">多层次记忆架构 </font><font style="color: rgba(223, 42, 63, 1)">痛点</font></strong></li></ul></li><li><font style="color: rgba(6, 8, 31, 0.88)">结构化输出</font><ul><li><font style="color: rgba(6, 8, 31, 0.88)">基础类型:</font></li><li><font style="color: rgba(6, 8, 31, 0.88)">Pojo类型:</font></li><li><font style="color: rgba(6, 8, 31, 0.88)">原理</font></li></ul></li></ul></li><li>最后:</li></ul></div><p></p>
<h2 id="对话记忆">对话记忆</h2>
<p><font style="color: rgba(25, 30, 30, 1)">大型语言模型 (LLM) 是无状态的,这意味着它们不会保留先前交互的信息。</font></p>
<pre><code class="language-java">@Test
    public void testChatOptions() {
      String content = chatClient.prompt()
                .user("我叫小兔子 ")
                .call()
                .content();
      System.out.println(content);
      System.out.println("--------------------------------------------------------------------------");

       content = chatClient.prompt()
                .user("我叫什么 ?")
                .call()
                .content();
      System.out.println(content);
    }
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952447-672720479.png"></p>
<p><font style="color: rgba(25, 30, 30, 1)">那我们平常跟一些大模型聊天是怎么记住我们对话的呢?实际上,每次对话都需要将之前的对话消息内置发送给大模型,这种方式称为</font><font style="color: rgba(223, 42, 63, 1)">多轮对话</font><font style="color: rgba(25, 30, 30, 1)">。</font></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952561-1498264778.png"></p>
<p>SpringAi提供了一个ChatMemory的组件用于存储聊天记录,允许您使用 LLM 跨多个交互存储和检索信息。并且可以为不同用户的多个交互之间维护上下文或状态。<br>
可以在每次对话的时候把当前聊天信息和模型的响应存储到ChatMemory, 然后下一次对话把聊天记录取出来再发给大模型。</p>
<pre><code class="language-java">`

//输出 名字叫徐庶
</code></pre>
<p><font style="color: rgba(25, 30, 30, 1)">但是这样做未免太麻烦! 能不能简化? 思考一下!</font><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952470-1425362029.jpg"></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952521-351168643.gif">用我们之前的Advisor对话拦截是不是就可以不用每次手动去维护了。 并且SpringAi早已体贴的为我提供了ChatMemoryAutoConfiguration自动配置类</p>
<pre><code class="language-xml">&lt;dependency&gt;
&lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
&lt;artifactId&gt;spring-ai-autoconfigure-model-chat-memory&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<pre><code class="language-java">@AutoConfiguration
@ConditionalOnClass({ ChatMemory.class, ChatMemoryRepository.class })
public class ChatMemoryAutoConfiguration {

        @Bean
        @ConditionalOnMissingBean
        ChatMemoryRepository chatMemoryRepository() {
                return new InMemoryChatMemoryRepository();
        }

        @Bean
        @ConditionalOnMissingBean
        ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
                return MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).build();
        }

}
</code></pre>
<p><font style="color: rgba(25, 30, 30, 1)">所以我们可以这样用:</font></p>
<h3 id="promptchatmemoryadvisor">PromptChatMemoryAdvisor</h3>
<p>SpringAi提供了 PromptChatMemoryAdvisor 专门用于对话记忆的拦截</p>
<pre><code class="language-java">@SpringBootTest
public class ChatMemoryTest {
    ChatClient chatClient;
    @BeforeEach
    publicvoid init(@Autowired
                      DeepSeekChatModel chatModel,
                      @Autowired
                      ChatMemory chatMemory) {
      chatClient = ChatClient
                .builder(chatModel)
                .defaultAdvisors(
                  // PromptChatMemoryAdvisor拦截器 就会自动将我们与大模型的历史对话记录下来
                        PromptChatMemoryAdvisor.builder(chatMemory).build()
                )
                .build();
    }
    @Test
    public void testChatOptions() {
      String content = chatClient.prompt()
                .user("我叫徐庶 ?")
      //
                .advisors(new ReReadingAdvisor())
                .call()
                .content();
      System.out.println(content);
      System.out.println("--------------------------------------------------------------------------");

      content = chatClient.prompt()
                .user("我叫什么 ?")
                .advisors(new ReReadingAdvisor())
                .call()
                .content();
      System.out.println(content);
    }
}
</code></pre>
<h3 id="配置聊天记录最大存储数量"><font style="color: rgba(25, 30, 30, 1)">配置聊天记录最大存储数量</font></h3>
<p>你要知道, 我们把聊天记录发给大模型, 都是算token计数的。</p>
<p>大模型的token是有上限了, 如果你发送过多聊天记录,可能就会导致token过长。</p>
<p>如下是大模型存储的 token 历史条数上限。</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952550-2060816002.png"></p>
<p>并且更多的token也意味更多的费用, 更久的解析时间. 所以不建议太长<br>
(DEFAULT_MAX_MESSAGES默认20即10次对话)<br>
一旦超出DEFAULT_MAX_MESSAGES只会存最后面N条(可以理解为先进先出),参考MessageWindowChatMemory源码</p>
<pre><code class="language-java">@Bean
   ChatMemory chatMemory(@Autowired ChatMemoryRepository chatMemoryRepository) {
// MessageWindowChatMemory 创建一个历史对话存储的配置,
      return MessageWindowChatMemory
                .builder()
                .maxMessages(10)// 设置最大存储 10 条
                .chatMemoryRepository(chatMemoryRepository).build();
    }
</code></pre>
<h3 id="配置多用户隔离记忆">配置多用户隔离记忆</h3>
<p>如果有多个用户在进行对话, 肯定不能将对话记录混在一起, 不同的用户的对话记忆需要隔离</p>
<pre><code class="language-java">@Test
    public void testChatOptions() {
      String content = chatClient.prompt()
                .user("我叫徐庶 ?")
      // 注意:这里要先构建一个 ChatMemory的 Bean,和上面类似,这里我们设置历史对话的用户ID
                .advisors(advisorSpec -&gt; advisorSpec.param(ChatMemory.CONVERSATION_ID,"1"))
                .call()
                .content();
      System.out.println(content);
      System.out.println("--------------------------------------------------------------------------");

      content = chatClient.prompt()
                .user("我叫什么 ?")
                .advisors(advisorSpec -&gt; advisorSpec.param(ChatMemory.CONVERSATION_ID,"1"))
                .call()
                .content();
      System.out.println(content);


      System.out.println("--------------------------------------------------------------------------");

      content = chatClient.prompt()
                .user("我叫什么 ?")
                .advisors(advisorSpec -&gt; advisorSpec.param(ChatMemory.CONVERSATION_ID,"2"))
                .call()
                .content();
      System.out.println(content);
    }
</code></pre>
<p>会发现, 不同的CONVERSATION_ID,会有不同的记忆</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952448-796759544.png"></p>
<h3 id="原理源码">原理源码$</h3>
<p>主要有前置存储<br>
MessageWindowChatMemory<br>
具体存储实现<br>
ChatMemoryRepository</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952486-888743662.png"></p>
<h3 id="数据库存储对话记忆">数据库存储对话记忆</h3>
<p><font style="color: rgba(25, 30, 30, 1)">默认情况, 对话内容会存在jvm内存会导致:</font></p>
<ol>
<li><font style="color: rgba(25, 30, 30, 1)">一直存最终会撑爆JVM导致OOM。</font></li>
<li><font style="color: rgba(25, 30, 30, 1)">重启就丢了, 如果已想存储到第三方存储进行持久化</font></li>
</ol>
<p><font style="color: rgba(25, 30, 30, 1)">springAi内置提供了以下几种方式(例如 Cassandra、JDBC 或 Neo4j), 这里演示下JDBC方式</font></p>
<ol>
<li>添加依赖</li>
</ol>
<pre><code class="language-xml">      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.ai&lt;/groupId&gt;
            &lt;artifactId&gt;spring-ai-starter-model-chat-memory-repository-jdbc&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!--jdbc--&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-jdbc&lt;/artifactId&gt;
      &lt;/dependency&gt;


      &lt;!--mysql驱动--&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.mysql&lt;/groupId&gt;
            &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
      &lt;/dependency&gt;
</code></pre>
<ol>
<li>添加配置(目前我们的需要创建一个schema-mysql.sql 文件,就是一个 SQL 脚本,在后面有配置)SPRING_AI_CHAT_MEMORY 表存储用户的历史对话,数据库,我们自行定义将该数据表存储到那个数据库中即可。</li>
</ol>
<pre><code class="language-properties">spring.ai.chat.memory.repository.jdbc.initialize-schema=always
spring.ai.chat.memory.repository.jdbc.schema=classpath:/schema-mysql.sql
</code></pre>
<p>如下是 MySQL 的配置:</p>
<pre><code class="language-yaml">spring:
datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/springai?characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=UTC&amp;
    driver-class-name: com.mysql.cj.jdbc.Driver
</code></pre>
<ol>
<li>配置类</li>
</ol>
<pre><code class="language-java">@Configuration
public class ChatMemoryConfig {


    @Bean// JdbcChatMemoryRepository 是已经被封装好自动装配好了,就可以使用
    ChatMemory chatMemory(@Autowired JdbcChatMemoryRepository chatMemoryRepository) {
      return MessageWindowChatMemory
      .builder()
      .maxMessages(1)// 设置存储为上面我们传的变量的 jdbc 的存储方式
      .chatMemoryRepository(chatMemoryRepository).build();
    }

}
</code></pre>
<ol>
<li>resources/schema-mysql.sql(目前1.0.0版本需要自己定义,没有提供脚本),创建这个SPRING_AI_CHAT_MEMORY 数据表,来存储用户的历史对话</li>
</ol>
<pre><code class="language-java">CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
    `conversation_id` VARCHAR(36) NOT NULL,
    `content` TEXT NOT NULL,
    `type` VARCHAR(10) NOT NULL,
    `timestamp` TIMESTAMP NOT NULL,

    INDEX `SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX` (`conversation_id`, `timestamp`)
    );
</code></pre>
<ol>
<li>测试</li>
</ol>
<pre><code class="language-java">@SpringBootTest
public class ChatMemoryTest {


    ChatClient chatClient;
    @BeforeEach
    publicvoid init(@Autowired
                      DeepSeekChatModel chatModel,
                      @Autowired
                      ChatMemory chatMemory) {
      chatClient = ChatClient
                .builder(chatModel)
                .defaultAdvisors(
                        PromptChatMemoryAdvisor.builder(chatMemory).build()
                )
                .build();
    }
    @Test
    public void testChatOptions() {
      String content = chatClient.prompt()
                .user("你好,我叫徐庶!")
                .advisors(new ReReadingAdvisor())
                .advisors(advisorSpec -&gt; advisorSpec.param(ChatMemory.CONVERSATION_ID,"1"))
                .call()
                .content();
      System.out.println(content);
      System.out.println("--------------------------------------------------------------------------");

      content = chatClient.prompt()
                .user("我叫什么 ?")
                .advisors(new ReReadingAdvisor())
                .advisors(advisorSpec -&gt; advisorSpec.param(ChatMemory.CONVERSATION_ID,"1"))
                .call()
                .content();
      System.out.println(content);
    }
}
</code></pre>
<p>可以看到由于我设置.maxMessages(1)数据库只存一条</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952541-403157441.png"></p>
<p>扩展:<strong>其实我可以让其存储到我们的所有历史对话,因为数据库就是存储数据的吗,而且存储起来的数据可能还是可以用于我们分析测试用户的对话的。那我们取数据库的时候,可以获取最新一条的数据即可。</strong></p>
<h3 id="redis存储">Redis存储</h3>
<p>如果你想用redis , 你需要自己实现ChatMemoryRepository接口(自己实现增、删、查)</p>
<p>Redis 存储更快,我们如果一般就是仅仅只是临时存储用户的 100 条记录什么的,就存到 Redis 当中就好了,超过了 100 条记录,也是会存储到用户最新的那 100 条记录,后续的内容也不用持久化了,丢了就丢了,反正都是用户的一个临时历史对话记录而已。性能也是比 MySQL 更快的。</p>
<p>但是alibaba-ai有现成的实现:(还包括ES)</p>
<p>https://github.com/alibaba/spring-ai-alibaba/tree/main/community/memories</p>
<pre><code class="language-xml">      &lt;properties&gt;
      &lt;jedis.version&gt;5.2.0&lt;/jedis.version&gt;
      &lt;/properties&gt;
      
      &lt;dependency&gt;
      &lt;groupId&gt;com.alibaba.cloud.ai&lt;/groupId&gt;
      &lt;artifactId&gt;spring-ai-alibaba-starter-memory-redis&lt;/artifactId&gt;
      &lt;/dependency&gt;
      
      
      &lt;dependency&gt;
      &lt;groupId&gt;redis.clients&lt;/groupId&gt;
      &lt;artifactId&gt;jedis&lt;/artifactId&gt;
      &lt;version&gt;${jedis.version}&lt;/version&gt;
      &lt;/dependency&gt;
</code></pre>
<pre><code class="language-yaml">spring:
ai:
    memory:
      redis:
      host: localhost
      port: 6379
      timeout:5000
      password:
</code></pre>
<pre><code class="language-java">@Configuration
public class RedisMemoryConfig {

    @Value("${spring.ai.memory.redis.host}")
    private String redisHost;
    @Value("${spring.ai.memory.redis.port}")
    private int redisPort;
    @Value("${spring.ai.memory.redis.password}")
    private String redisPassword;
    @Value("${spring.ai.memory.redis.timeout}")
    private int redisTimeout;

    @Bean
    public RedisChatMemoryRepository redisChatMemoryRepository() {
      return RedisChatMemoryRepository._builder_()
                .host(redisHost)
                .port(redisPort)
                // 若没有设置密码则注释该项
//         .password(redisPassword)
                .timeout(redisTimeout)
                .build();
    }


   
    @Bean// RedisChatMemoryRepository 被我们上面 Bean 注入了
    ChatMemory chatMemory(@Autowired RedisChatMemoryRepository redisMemoryRepository) {
      return MessageWindowChatMemory
      .builder()
      .maxMessages(1)// 设置存储为上面我们传的变量的 jdbc 的存储方式
      .chatMemoryRepository(redisMemoryRepository).build();
    }
}
</code></pre>
<h3 id="多层次记忆架构-痛点"><strong><font style="color: rgba(6, 8, 31, 0.88)">多层次记忆架构 </font><font style="color: rgba(223, 42, 63, 1)">痛点</font></strong></h3>
<p>记忆多=聪明(大模型记录了多了用户的历史对话记录,就更加能够理解我们,实现我们的需求了), 但是记忆多会触发 token 上限(每个大模型的 token 是有上限的,不可以无限的存储。)</p>
<p>要知道, 无论你用什么存储对话以及, 也只能保证服务端的存储性能。</p>
<p>但是一旦聊天记录多了依然会超过token上限, 但是有时候我们依然希望存储更多的聊天记录,这样才能保证整个对话更像“人”。</p>
<p><strong><font style="color: rgba(6, 8, 31, 0.88)">多层次记忆架构(模仿人类)</font></strong></p>
<ul>
<li><strong><font style="color: rgba(223, 42, 63, 1)">近期记忆</font></strong><font style="color: rgba(6, 8, 31, 0.88)">:保留在上下文窗口中的最近几轮对话,每轮对话完成后立即存储(可通过ChatMemory); 10 条</font></li>
<li><strong><font style="color: rgba(223, 42, 63, 1)">中期记忆</font></strong><font style="color: rgba(6, 8, 31, 0.88)">:通过RAG检索的相关历史对话(每轮对话完成后,异步将对话内容转换为向量并存入向量数据库) 5条</font></li>
<li><strong><font style="color: rgba(223, 42, 63, 1)">长期记忆</font></strong><font style="color: rgba(6, 8, 31, 0.88)">:关键信息的固化总结 </font>
<ul>
<li><strong><font style="color: rgba(6, 8, 31, 0.88)">方式一:定时批处理</font></strong><br>
+ <font style="color: rgba(6, 8, 31, 0.88)">通过定时任务(如每天或每周)对积累的对话进行总结和提炼</font><br>
+ <font style="color: rgba(6, 8, 31, 0.88)">提取关键信息、用户偏好、重要事实等 </font><br>
+ <font style="color: rgba(6, 8, 31, 0.88)">批处理方式降低计算成本,适合大规模处理 </font></li>
<li><strong><font style="color: rgba(6, 8, 31, 0.88)">方式二:关键点实时处理</font></strong><br>
+ <font style="color: rgba(6, 8, 31, 0.88)">在对话中识别出关键信息点时立即提取并存储</font><br>
+ <font style="color: rgba(6, 8, 31, 0.88)">例如,当用户明确表达偏好、提供个人信息或设置持久性指令时 </font></li>
</ul>
</li>
</ul>
<p><font style="color: rgba(6, 8, 31, 0.88)"></font></p>
<h2 id="结构化输出"><font style="color: rgba(6, 8, 31, 0.88)">结构化输出</font></h2>
<h3 id="基础类型"><font style="color: rgba(6, 8, 31, 0.88)">基础类型:</font></h3>
<p><font style="color: rgba(6, 8, 31, 0.88)">以Boolean为例 , 在 agent 中可以用于判定用于的内容2个分支, 不同的分支走不同的逻辑</font></p>
<pre><code class="language-java">ChatClient chatClient;
@BeforeEach
publicvoid init(@Autowired
                  DashScopeChatModel chatModel) {
    chatClient = ChatClient.builder(chatModel).build();
}
@Test
public void testBoolOut() {
    Boolean isComplain = chatClient
    .prompt()
    .system("""
            请判断用户信息是否表达了投诉意图?
            只能用 true 或 false 回答,不要输出多余内容
            """)
    .user("你们家的快递迟迟不到,我要退货!")
    .call()
    .entity(Boolean.class);// 结构化输出,让大模型输出 Boolean.class java当中的布尔值类型

    // 分支逻辑
    if (Boolean.TRUE.equals(isComplain)) {
      System.out.println("用户是投诉,转接人工客服!");
    } else {
      System.out.println("用户不是投诉,自动流转客服机器人。");
      // todo 继续调用 客服ChatClient进行对话
    }
}
</code></pre>
<h3 id="pojo类型"><font style="color: rgba(6, 8, 31, 0.88)">Pojo类型:</font></h3>
<p><font style="color: rgba(6, 8, 31, 0.88)">用购物APP应该见过复制一个地址, 自动为你填入每个输入框。 用大模型轻松完成!</font></p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952648-862495914.png"></p>
<pre><code class="language-java">public record Address(
    String name,      // 收件人姓名
    String phone,       // 联系电话
    String province,    // 省
    String city,      // 市
    String district,    // 区/县
    String detail       // 详细地址
) {}

@Test
    public void testEntityOut() {
      Address address = chatClient.prompt()
      // .systemshi 是一个系统提示词,优先级更高
                .system("""
                        请从下面这条文本中提取收货信息
                        """)
                .user("收货人:张三,电话13588888888,地址:浙江省杭州市西湖区文一西路100号8幢202室")
                .call()
                .entity(Address.class);// 大模型会根据文本内容,将其中的用户的对话信息识别存储到我们的 Address的对象类
      System.out.println(address);
    }
</code></pre>
<pre><code class="language-java">public record Address(
    String name,      // 收件人姓名
    String phone,       // 联系电话
    String province,    // 省
    String city,      // 市
    String district,    // 区/县
    String detail       // 详细地址
) {}
</code></pre>
<h3 id="原理"><font style="color: rgba(6, 8, 31, 0.88)">原理</font></h3>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952573-1179590516.jpg"></p>
<p>ChatModel或者直接使用低级API:</p>
<pre><code class="language-java">@Test
    public void testLowEntityOut(
         @Autowired DashScopeChatModel chatModel) {
      // BeanOutputConverter 转换器
      BeanOutputConverter&lt;ActorsFilms&gt; beanOutputConverter =
                new BeanOutputConverter&lt;&gt;(ActorsFilms.class);

      String format = beanOutputConverter.getFormat();

      String actor = "周星驰";

      String template = """
      提供5部{actor}导演的电影.
      {format}
      """;

      PromptTemplate promptTemplate = PromptTemplate.builder().template(template).variables(Map.of("actor", actor, "format", format)).build();
      ChatResponse response = chatModel.call(
                promptTemplate.create()
      );

      ActorsFilms actorsFilms = beanOutputConverter.convert(response.getResult().getOutput().getText());
      System.out.println(actorsFilms);
    }
</code></pre>
<h1 id="最后">最后:</h1>
<blockquote>
<p>“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202509/3084824-20250929154952600-328630730.gif"></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/TheMagicalRainbowSea/p/19118920
頁: [1]
查看完整版本: 7. Spring AI 对话记忆 + 结构化输出