找回密码
 立即注册
首页 业界区 安全 spring-ai 学习系列(8)-上下文记忆-多轮对话 ...

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

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

  • 有1个变量A,它的值是5,那么A加1后,变成几?
  • A再乘上3,结果是多少?
  • A再减1,现在是多少?
直接用postman 之类的工具发给ollama,入参示例:
  1. {
  2.     "model": "qwen3:0.6b",
  3.     "messages": [
  4.         {
  5.             "role": "user",
  6.             "content": "有1个变量A,它的值是5,那么A加1后,变成几?"
  7.         }
  8.     ],
  9.     "stream": false
  10. }
复制代码
响应:
  1. {
  2.         "model": "qwen3:0.6b",
  3.         "created_at": "2025-07-20T02:22:48.9488355Z",
  4.         "message": {
  5.                 "role": "assistant",
  6.                 "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。"
  7.         },
  8.         "done_reason": "stop",
  9.         "done": true,
  10.         "total_duration": 15604468300,
  11.         "load_duration": 34368500,
  12.         "prompt_eval_count": 28,
  13.         "prompt_eval_duration": 42765400,
  14.         "eval_count": 598,
  15.         "eval_duration": 15526323700
  16. }
复制代码
1.png

继续,问第2个问题:这次就回答不上来了
2.png

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

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

啥意思呢?messages是1个list,我们可以每次问新问题时,把先前问过的问题按顺序都塞到里面,这样大模型从前往后,依次理解这些问题,就能回答出来了,即:
  1. {
  2.     "model": "qwen3:0.6b",
  3.     "messages": [
  4.         {
  5.             "role": "user",
  6.             "content": "有1个变量A,它的值是5,那么A加1后,变成几?"
  7.         },
  8.         {
  9.             "role": "user",
  10.             "content": "A再乘上3,结果是多少?"
  11.         },
  12.         {
  13.             "role": "user",
  14.             "content": "A再减1,现在是多少?"
  15.         }
  16.     ],
  17.     "stream": false
  18. }
复制代码
5.png

这次回答对了!很多高上大的东西,捅破窗户纸后,会发现实现方式居然这么朴实无华!
理解原理后,实现起来就非常容易了,无非是在工程应用层面,弄1个存储(redis/mysql/jvm内存中都行),把历史上的问题都存下来,下次提问时,都扔给它就好。spring-ai就是这么做的,下面演示最基本的In Memory模式(即:保存到本机内存中)
一、添加pom依赖
6.gif
7.gif
  1. 1         <dependency>
  2. 2             <groupId>org.springframework.ai</groupId>
  3. 3             spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
  4. 4             <version>1.0.0</version>
  5. 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个方法定义:
8.png

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

二个版本的方法签名不一样,导致这2个版本混用时,运行时会报错,希望将来1.1.0出来后,会统一。
为了解决使用最新版本的这个矛盾,可以参考 org.springframework.ai.chat.memory.MessageWindowChatMemory,自行定义1个新类
  1. package com.cnblogs.yjmyzz;
  2. import org.springframework.ai.chat.memory.ChatMemory;
  3. import org.springframework.ai.chat.memory.ChatMemoryRepository;
  4. import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
  5. import org.springframework.ai.chat.messages.Message;
  6. import org.springframework.ai.chat.messages.SystemMessage;
  7. import org.springframework.util.Assert;
  8. import java.util.ArrayList;
  9. import java.util.HashSet;
  10. import java.util.List;
  11. import java.util.Set;
  12. public class MyMessageWindowChatMemory implements ChatMemory {
  13.     private static final int DEFAULT_MAX_MESSAGES = 20;
  14.     private final ChatMemoryRepository chatMemoryRepository;
  15.     private final int maxMessages;
  16.     private MyMessageWindowChatMemory(ChatMemoryRepository chatMemoryRepository, int maxMessages) {
  17.         Assert.notNull(chatMemoryRepository, "chatMemoryRepository cannot be null");
  18.         Assert.isTrue(maxMessages > 0, "maxMessages must be greater than 0");
  19.         this.chatMemoryRepository = chatMemoryRepository;
  20.         this.maxMessages = maxMessages;
  21.     }
  22.     @Override
  23.     public void add(String conversationId, List<Message> messages) {
  24.         Assert.hasText(conversationId, "conversationId cannot be null or empty");
  25.         Assert.notNull(messages, "messages cannot be null");
  26.         Assert.noNullElements(messages, "messages cannot contain null elements");
  27.         List<Message> memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId);
  28.         List<Message> processedMessages = process(memoryMessages, messages);
  29.         this.chatMemoryRepository.saveAll(conversationId, processedMessages);
  30.     }
  31.     @Override
  32.     public List<Message> get(String conversationId, int lastN) {
  33.         Assert.hasText(conversationId, "conversationId cannot be null or empty");
  34.         List<Message> messages = this.chatMemoryRepository.findByConversationId(conversationId);
  35.         if (messages.size() > lastN) {
  36.             return messages.subList(messages.size() - lastN, messages.size());
  37.         }
  38.         return messages;
  39.     }
  40.     @Override
  41.     public void clear(String conversationId) {
  42.         Assert.hasText(conversationId, "conversationId cannot be null or empty");
  43.         this.chatMemoryRepository.deleteByConversationId(conversationId);
  44.     }
  45.     private List<Message> process(List<Message> memoryMessages, List<Message> newMessages) {
  46.         List<Message> processedMessages = new ArrayList<>();
  47.         Set<Message> memoryMessagesSet = new HashSet<>(memoryMessages);
  48.         boolean hasNewSystemMessage = newMessages.stream()
  49.                 .filter(SystemMessage.class::isInstance)
  50.                 .anyMatch(message -> !memoryMessagesSet.contains(message));
  51.         memoryMessages.stream()
  52.                 .filter(message -> !(hasNewSystemMessage && message instanceof SystemMessage))
  53.                 .forEach(processedMessages::add);
  54.         processedMessages.addAll(newMessages);
  55.         if (processedMessages.size() <= this.maxMessages) {
  56.             return processedMessages;
  57.         }
  58.         int messagesToRemove = processedMessages.size() - this.maxMessages;
  59.         List<Message> trimmedMessages = new ArrayList<>();
  60.         int removed = 0;
  61.         for (Message message : processedMessages) {
  62.             if (message instanceof SystemMessage || removed >= messagesToRemove) {
  63.                 trimmedMessages.add(message);
  64.             } else {
  65.                 removed++;
  66.             }
  67.         }
  68.         return trimmedMessages;
  69.     }
  70.     public static MyMessageWindowChatMemory.Builder builder() {
  71.         return new MyMessageWindowChatMemory.Builder();
  72.     }
  73.     public static final class Builder {
  74.         private ChatMemoryRepository chatMemoryRepository;
  75.         private int maxMessages = DEFAULT_MAX_MESSAGES;
  76.         private Builder() {
  77.         }
  78.         public MyMessageWindowChatMemory.Builder chatMemoryRepository(ChatMemoryRepository chatMemoryRepository) {
  79.             this.chatMemoryRepository = chatMemoryRepository;
  80.             return this;
  81.         }
  82.         public MyMessageWindowChatMemory.Builder maxMessages(int maxMessages) {
  83.             this.maxMessages = maxMessages;
  84.             return this;
  85.         }
  86.         public MyMessageWindowChatMemory build() {
  87.             if (this.chatMemoryRepository == null) {
  88.                 this.chatMemoryRepository = new InMemoryChatMemoryRepository();
  89.             }
  90.             return new MyMessageWindowChatMemory(this.chatMemoryRepository, this.maxMessages);
  91.         }
  92.     }
  93. }
复制代码
二、注入ChatMemory实例
  1.     @Bean
  2.     public ChatMemoryRepository chatMemoryRepository() {
  3.         return new InMemoryChatMemoryRepository();
  4.     }
  5.     @Bean
  6.     public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
  7.         return MyMessageWindowChatMemory.builder()
  8.                 .chatMemoryRepository(chatMemoryRepository)
  9.                 .maxMessages(10)
  10.                 .build();
  11.     }
复制代码
三、使用示例
  1.     @RequestMapping(value = "/conversation-stream", produces = "text/html;charset=utf-8")
  2.     public Flux<String> conversationStream(@RequestParam String conversationId, @RequestParam String prompt) {
  3.         // 1. 当前新问题,扔到聊天上下文中
  4.         chatMemory.add(conversationId, new UserMessage(prompt));
  5.         // 2. 把历史消息全都取出来
  6.         List<Message> history = chatMemory.get(conversationId, MAX_HISTORY_SESSION);
  7.         // 3. 所有历史消息,扔给大模型
  8.         return chatClient.prompt(new Prompt(history))
  9.                 .stream()
  10.                 .content();
  11.     }
复制代码
四、运行效果
浏览器里,开3个tab依次问这3个问题:
10.png

 
11.png

 
12.png

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

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册