泥地锚 发表于 2025-7-20 12:26:11

spring-ai 学习系列(8)-上下文记忆-多轮对话

继续spring-ai学习之旅,大模型本身是无状态的,也就是每次请求对它来说,都是全新的,无记忆!比如:我们以ollama本地加载qwen3:0.6b模型为例,依次问它以下三个问题:

[*]有1个变量A,它的值是5,那么A加1后,变成几?
[*]A再乘上3,结果是多少?
[*]A再减1,现在是多少?
直接用postman 之类的工具发给ollama,入参示例:
{
    "model": "qwen3:0.6b",
    "messages": [
      {
            "role": "user",
            "content": "有1个变量A,它的值是5,那么A加1后,变成几?"
      }
    ],
    "stream": false
}响应:
{
        "model": "qwen3:0.6b",
        "created_at": "2025-07-20T02:22:48.9488355Z",
        "message": {
                "role": "assistant",
                "content": "<think>\n嗯,我现在要解决的问题是:有一个变量A,它的值是5,那么A加1后变成几?看起来好像挺简单的,但可能我有点困惑,特别是关于变量赋值的问题。让我仔细想想。\n\n首先,变量A的初始值是5,对吧?所以现在的问题应该是,当我们将A的值加上1之后,A变成了多少。不过,我是不是应该考虑在编程语言中,变量赋值的规则?比如在编程中,如果变量的值是5,那么在执行A += 1之后,变量的值应该变成6吗?对,如果是这样的话,答案应该是6。不过,有没有可能这里的问题是在数学上,或者只是简单的加法运算?\n\n不过,可能题目是在某种特定的上下文中,比如在数学问题中,或者可能题目本身存在一些陷阱。比如,有时候变量可能有不同的含义,或者在某些语言中,变量名可能与实际值不一致?不过题目中明确说变量A的值是5,所以应该是在数学运算范围内。\n\n或者,可能题目是想测试学生的数学基础,比如是否知道加法运算的规则。比如,5加1等于6,所以正确答案应该是6。这时候应该直接给出结果,不需要进一步的解释,对吧?\n\n不过,有没有可能题目中的变量A在某种情况下会被其他操作影响?比如,在某些编程语言中,变量名可能被修改,但题目中并没有提到其他操作,只是简单的加法。所以应该没问题。\n\n或者,可能题目中的变量A在某些情况下会变成其他值,比如在处理数组或者列表的时候?不过题目里并没有提到数组,所以应该还是简单的加法。\n\n再仔细检查一遍,题目是“有1个变量A,它的值是5,那么A加1后,变成几?”,所以直接的数学运算就是5+1=6,所以正确结果应该是6。可能这个问题本身是数学题,所以答案是6,不需要进一步的思考,只是直接给出结果。\n\n不过,有没有可能题目是在某种特定的数学问题中,比如涉及代数方程或者变量替换?比如,如果A是一个变量,那么加1之后,变量的值应该保持不变,对吗?不过,如果是这样的话,答案还是6,对吗?\n\n或者,可能题目中的“加1”指的是其他操作,比如在某种特定的编程语言中,变量名是A,但实际值可能不同?比如,在某些语言中,变量名可能被修改,但题目中已经明确说变量A的值是5,所以应该不需要考虑其他情况。\n\n总之,根据题目的信息,正确的答案应该是6,所以应该直接回答6。\n</think>\n\n根据题目描述,变量A的初始值为5,执行A += 1操作后,变量A的值变为:\n\n**6**\n\n因此,答案是:6。"
        },
        "done_reason": "stop",
        "done": true,
        "total_duration": 15604468300,
        "load_duration": 34368500,
        "prompt_eval_count": 28,
        "prompt_eval_duration": 42765400,
        "eval_count": 598,
        "eval_duration": 15526323700
}
继续,问第2个问题:这次就回答不上来了

继续,第3个问题:意料之中的回答不出来

那么,如何破局呢?
ollama的api文档里,有一行提示

啥意思呢?messages是1个list,我们可以每次问新问题时,把先前问过的问题按顺序都塞到里面,这样大模型从前往后,依次理解这些问题,就能回答出来了,即:
{
    "model": "qwen3:0.6b",
    "messages": [
      {
            "role": "user",
            "content": "有1个变量A,它的值是5,那么A加1后,变成几?"
      },
      {
            "role": "user",
            "content": "A再乘上3,结果是多少?"
      },
      {
            "role": "user",
            "content": "A再减1,现在是多少?"
      }
    ],
    "stream": false
}
这次回答对了!很多高上大的东西,捅破窗户纸后,会发现实现方式居然这么朴实无华!
理解原理后,实现起来就非常容易了,无非是在工程应用层面,弄1个存储(redis/mysql/jvm内存中都行),把历史上的问题都存下来,下次提问时,都扔给它就好。spring-ai就是这么做的,下面演示最基本的In Memory模式(即:保存到本机内存中)
一、添加pom依赖
1         <dependency>
2             <groupId>org.springframework.ai</groupId>
3             spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
4             <version>1.0.0</version>
5         </dependency>View Code友情提示:目前spring-ai-starter-model-chat-memory-repository-jdbc最新版本只有1.0.0,但是spring-ai-bom最新版却是1.1.0。
最新的ChatMemory接口,在1.1.0中,有1个方法定义:

 而spring-ai-starter-model-chat-memory-repository-jdbc 1.0.0版本中的ChatMemory接口

二个版本的方法签名不一样,导致这2个版本混用时,运行时会报错,希望将来1.1.0出来后,会统一。
为了解决使用最新版本的这个矛盾,可以参考 org.springframework.ai.chat.memory.MessageWindowChatMemory,自行定义1个新类
package com.cnblogs.yjmyzz;

import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class MyMessageWindowChatMemory implements ChatMemory {

    private static final int DEFAULT_MAX_MESSAGES = 20;

    private final ChatMemoryRepository chatMemoryRepository;

    private final int maxMessages;

    private MyMessageWindowChatMemory(ChatMemoryRepository chatMemoryRepository, int maxMessages) {
      Assert.notNull(chatMemoryRepository, "chatMemoryRepository cannot be null");
      Assert.isTrue(maxMessages > 0, "maxMessages must be greater than 0");
      this.chatMemoryRepository = chatMemoryRepository;
      this.maxMessages = maxMessages;
    }

    @Override
    public void add(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");

      List<Message> memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId);
      List<Message> processedMessages = process(memoryMessages, messages);
      this.chatMemoryRepository.saveAll(conversationId, processedMessages);
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {
      Assert.hasText(conversationId, "conversationId cannot be null or empty");
      List<Message> messages = this.chatMemoryRepository.findByConversationId(conversationId);
      if (messages.size() > lastN) {
            return messages.subList(messages.size() - lastN, messages.size());
      }
      return messages;
    }


    @Override
    public void clear(String conversationId) {
      Assert.hasText(conversationId, "conversationId cannot be null or empty");
      this.chatMemoryRepository.deleteByConversationId(conversationId);
    }

    private List<Message> process(List<Message> memoryMessages, List<Message> newMessages) {
      List<Message> processedMessages = new ArrayList<>();

      Set<Message> memoryMessagesSet = new HashSet<>(memoryMessages);
      boolean hasNewSystemMessage = newMessages.stream()
                .filter(SystemMessage.class::isInstance)
                .anyMatch(message -> !memoryMessagesSet.contains(message));

      memoryMessages.stream()
                .filter(message -> !(hasNewSystemMessage && message instanceof SystemMessage))
                .forEach(processedMessages::add);

      processedMessages.addAll(newMessages);

      if (processedMessages.size() <= this.maxMessages) {
            return processedMessages;
      }

      int messagesToRemove = processedMessages.size() - this.maxMessages;

      List<Message> trimmedMessages = new ArrayList<>();
      int removed = 0;
      for (Message message : processedMessages) {
            if (message instanceof SystemMessage || removed >= messagesToRemove) {
                trimmedMessages.add(message);
            } else {
                removed++;
            }
      }

      return trimmedMessages;
    }

    public static MyMessageWindowChatMemory.Builder builder() {
      return new MyMessageWindowChatMemory.Builder();
    }

    public static final class Builder {

      private ChatMemoryRepository chatMemoryRepository;

      private int maxMessages = DEFAULT_MAX_MESSAGES;

      private Builder() {
      }

      public MyMessageWindowChatMemory.Builder chatMemoryRepository(ChatMemoryRepository chatMemoryRepository) {
            this.chatMemoryRepository = chatMemoryRepository;
            return this;
      }

      public MyMessageWindowChatMemory.Builder maxMessages(int maxMessages) {
            this.maxMessages = maxMessages;
            return this;
      }

      public MyMessageWindowChatMemory build() {
            if (this.chatMemoryRepository == null) {
                this.chatMemoryRepository = new InMemoryChatMemoryRepository();
            }
            return new MyMessageWindowChatMemory(this.chatMemoryRepository, this.maxMessages);
      }

    }
}二、注入ChatMemory实例
    @Bean
    public ChatMemoryRepository chatMemoryRepository() {
      return new InMemoryChatMemoryRepository();
    }


    @Bean
    public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
      return MyMessageWindowChatMemory.builder()
                .chatMemoryRepository(chatMemoryRepository)
                .maxMessages(10)
                .build();

    }三、使用示例
    @RequestMapping(value = "/conversation-stream", produces = "text/html;charset=utf-8")
    public Flux<String> conversationStream(@RequestParam String conversationId, @RequestParam String prompt) {
      // 1. 当前新问题,扔到聊天上下文中
      chatMemory.add(conversationId, new UserMessage(prompt));
      // 2. 把历史消息全都取出来
      List<Message> history = chatMemory.get(conversationId, MAX_HISTORY_SESSION);
      // 3. 所有历史消息,扔给大模型
      return chatClient.prompt(new Prompt(history))
                .stream()
                .content();
    }四、运行效果
浏览器里,开3个tab依次问这3个问题:

 

 

文中示例代码 :yjmyzz/spring-ai-sample at day07
参考: 
Chat Memory :: Spring AI Reference
ollama/docs/api.md at main · ollama/ollama

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: spring-ai 学习系列(8)-上下文记忆-多轮对话