Spring AI 实战:第七章、Spring AI Advisor机制之记忆大师

引言:当AI的记性比金鱼还差

  • 你:我叫张三,很高兴认识你
  • AI:很高兴认识你,张三! 如果你有任何问题或者需要帮助,请随时告诉我
  • 你:我叫什么
  • AI:抱歉,我无法知道你的名字。不过你可以告诉我一些关于你的信息,如果你愿意分享的话!

AI的内心OS:爱谁谁,我反正不知道

如上图所示,默认的AI连一条只有7秒记忆的金鱼都比不上,每次对话都像是"初次见面"。本篇通过 Spring AI 让大模型具备记忆功能,让AI变成《盗梦空间》里的"记忆宫殿管理员"。

一、记忆分类

在大模型应用场景中通常把记忆分为短期记忆和长期记忆

  • 短期记忆能记住当前对话中你提过的信息,但只限于这次聊天(关闭页面或刷新后消失);完全依赖你发给它的文字上下文(就像两个人聊天时,对方只能记住你刚才说过的话)

伪代码:模型输入自动包含历史对话

String prompt = "用户:我叫张三\n AI:你好 张三 \n 用户:我叫什么?"

output = chatClient.prompt(prompt).call().content() # 输出:"你叫张三。"

  • 模型本身无法真正长期记住你(所有对话默认独立),但可以通过技术手段模拟长期记忆,需要外部工具辅助(比如数据库、用户账号系统)

user_data =toString ( db.query("SELECT * FROM users WHERE id='xxx'") )

prompt = "用户信息:{user_data}\n 当前问题:推荐一首我喜欢的音乐"

output = chatClient.prompt(prompt).call().content() # 个性化推荐

差异对比:短期记忆是模型的"临时便签",长期记忆是它的"外接硬盘"

对比维度 短期记忆 长期记忆
存储位置 当前对话的上下文文本(临时输入) 外部数据库/知识库(需开发实现)
时效性 仅当前对话有效(关闭即消失) 永久保留(除非手动删除)
依赖技术 模型原生支持(上下文窗口) 额外开发(如数据库、RAG、用户系统)
容量限制 受模型上下文窗口限制(如128K tokens) 理论上无限(取决于存储空间)
修改方式 实时调整输入文本即可 需更新外部数据源
隐私性 默认不存储(相对安全) 需主动管理用户数据(合规风险)
典型场景 多轮对话、临时任务(如调试代码) 个性化服务(如医疗记录、订单历史)
用户感知示例 "AI记得我刚才让改的代码风格" "AI知道我是VIP客户,自动优先处理"
实现成本 零成本(上下文传递) 需开发资源(存储、API、安全措施)
默认支持 所有大模型均支持 需自行开发或使用企业级工具

二、短期记忆

2.1 Advisor

Advisor是一种用于在Spring应用中拦截、修改和增强与大模型交互的灵活而强大的方式。

2.1.1 SimpleLoggerAdvisor

可以通过SimpleLoggerAdvisor在大模型执行前后打印日志

添加logback.xml文件,把日志输出级别调整为debug

xml 复制代码
<!--
~ Copyright 2023-2024 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~      https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
    </encoder>
  </appender>

  <root level="debug">
    <appender-ref ref="STDOUT"/>
  </root>
  <logger name="org.springframework.ai.chat.client.advisor" level="DEBUG" additivity="true">
    <appender-ref ref="STDOUT"/>
  </logger>

</configuration>

设置Advisor:

java 复制代码
@GetMapping("/log")
public String log(@RequestParam(value = "input", defaultValue = "讲一个笑话") String input) {
    Prompt prompt = new Prompt(input);
    return chatClient.prompt(prompt).advisors(new SimpleLoggerAdvisor()).call().content();
}

输出:发起请求日志格式 o.s.a.c.c.a.SimpleLoggerAdvisor -- request,响应输出日志格式o.s.a.c.c.a.SimpleLoggerAdvisor -- response,在响应日志中可以看到token消耗等各种额外信息

22:12:13.367 [http-nio-8080-exec-3] DEBUG o.s.a.c.c.a.SimpleLoggerAdvisor -- request: AdvisedRequest[chatModel=OpenAiChatModel [defaultOptions=OpenAiChatOptions: {"streamUsage":false,"model":"qwen-plus","temperature":0.7}], userText=讲一个笑话, systemText=null, chatOptions=OpenAiChatOptions: {"streamUsage":false,"model":"qwen-plus","temperature":0.7}, media=[], functionNames=[], functionCallbacks=[], messages=[], userParams={}, systemParams={}, advisors=[org.springframework.ai.chat.client.DefaultChatClientDefaultChatClientRequestSpec 1 @ 76 a 2010 f , o r g . s p r i n g f r a m e w o r k . a i . c h a t . c l i e n t . D e f a u l t C h a t C l i e n t 1@76a2010f, org.springframework.ai.chat.client.DefaultChatClient 1@76a2010f,org.springframework.ai.chat.client.DefaultChatClientDefaultChatClientRequestSpec 2 @ 62 e d 924 c , o r g . s p r i n g f r a m e w o r k . a i . c h a t . c l i e n t . D e f a u l t C h a t C l i e n t 2@62ed924c, org.springframework.ai.chat.client.DefaultChatClient 2@62ed924c,org.springframework.ai.chat.client.DefaultChatClientDefaultChatClientRequestSpec 1 @ 64 c 6 c e 4 a , o r g . s p r i n g f r a m e w o r k . a i . c h a t . c l i e n t . D e f a u l t C h a t C l i e n t 1@64c6ce4a, org.springframework.ai.chat.client.DefaultChatClient 1@64c6ce4a,org.springframework.ai.chat.client.DefaultChatClientDefaultChatClientRequestSpec2@60f9560a, SimpleLoggerAdvisor], advisorParams={}, adviseContext={}, toolContext={}]

22:13:15.899 [http-nio-8080-exec-4] DEBUG o.s.a.c.c.a.SimpleLoggerAdvisor -- response: {

"result" : {

复制代码
"metadata" : {

  "finishReason" : "STOP",

  "contentFilters" : [ ],

  "empty" : true

},

"output" : {

  "messageType" : "ASSISTANT",

  "metadata" : {

    "refusal" : "",

    "finishReason" : "STOP",

    "index" : 0,

    "id" : "chatcmpl-45f6e0df-a0ec-930f-9ed1-f79aa134a443",

    "role" : "ASSISTANT",

    "messageType" : "ASSISTANT"

  },

  "toolCalls" : [ ],

  "media" : [ ],

  "text" : "好的!来一个轻松的笑话:\n\n有一天,小明去商店买饮料,他问老板:"老板,你们这里可乐摇一摇会爆炸吗?"\n\n老板笑着说:"不会不会,都是经过严格质量检查的。"\n\n小明想了想,又问:"那牛奶摇一摇会爆炸吗?"\n\n老板愣了一下,说:"牛奶摇一摇......可能会过期!" 😄"

}

},

"metadata" : {

复制代码
"id" : "chatcmpl-45f6e0df-a0ec-930f-9ed1-f79aa134a443",

"model" : "qwen-plus",

"rateLimit" : {

  "requestsLimit" : null,

  "requestsRemaining" : null,

  "requestsReset" : null,

  "tokensLimit" : null,

  "tokensRemaining" : null,

  "tokensReset" : null

},

"usage" : {

  "promptTokens" : 11,

  "completionTokens" : 82,

  "totalTokens" : 93,

  "generationTokens" : 82,

  "nativeUsage" : {

    "completion_tokens" : 82,

    "prompt_tokens" : 11,

    "total_tokens" : 93,

    "prompt_tokens_details" : {

      "cached_tokens" : 0

    }

  }

},

"promptMetadata" : [ ],

"empty" : false

},

"results" : [ {

复制代码
"metadata" : {

  "finishReason" : "STOP",

  "contentFilters" : [ ],

  "empty" : true

},

"output" : {

  "messageType" : "ASSISTANT",

  "metadata" : {

    "refusal" : "",

    "finishReason" : "STOP",

    "index" : 0,

    "id" : "chatcmpl-45f6e0df-a0ec-930f-9ed1-f79aa134a443",

    "role" : "ASSISTANT",

    "messageType" : "ASSISTANT"

  },

  "toolCalls" : [ ],

  "media" : [ ],

  "text" : "好的!来一个轻松的笑话:\n\n有一天,小明去商店买饮料,他问老板:"老板,你们这里可乐摇一摇会爆炸吗?"\n\n老板笑着说:"不会不会,都是经过严格质量检查的。"\n\n小明想了想,又问:"那牛奶摇一摇会爆炸吗?"\n\n老板愣了一下,说:"牛奶摇一摇......可能会过期!" 😄"

}

} ]

}

2.1.2 源码分析

查看SimpleLoggerAdvisor的关键代码实现:

  • aroundCall在调用chain.nextAroundCall(advisedRequest)之前触发before()执行完成触发observeAfter做日志输出,nextAroundCall会执行大模型的调用(这个在上一章以解释过)
  • aroundStream是支持流式响应的输出,流式请求需要在整个请求响应完成后再执行日志打印,因此通过MessageAggregator消息聚合器来采集返回结果,当有类似场景时可直接借用这段逻辑,避免重复造轮子
java 复制代码
private AdvisedRequest before(AdvisedRequest request) {
        logger.debug("request: {}", this.requestToString.apply(request));
        return request;
    }

    private void observeAfter(AdvisedResponse advisedResponse) {
        logger.debug("response: {}", this.responseToString.apply(advisedResponse.response()));
    }


@Override
    public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {

        advisedRequest = before(advisedRequest);

        AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);

        observeAfter(advisedResponse);

        return advisedResponse;
    }

    @Override
    public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {

        advisedRequest = before(advisedRequest);

        Flux<AdvisedResponse> advisedResponses = chain.nextAroundStream(advisedRequest);

        return new MessageAggregator().aggregateAdvisedResponse(advisedResponses, this::observeAfter);
    }

2.1.3 Advisor实体关系

Spring AI内置一个MessageChatMemoryAdvisor来提供聊天记忆功能(Advisor机制在上一篇中已解读过,不熟悉可以再读一读)

2.2 MessageChatMemoryAdvisor

在Spring AI中内置很多Advisor,其中MessageChatMemoryAdvisor可以用来提供聊天记忆功能

  • MessageChatMemoryAdvisor接收一个ChatMemory对象,传递InMemoryChatMemory是一个内存记忆的ChatMemory类型,通过Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>()实现
java 复制代码
@Slf4j
@RestController
public class MemoryController {

    private ChatClient chatClient;

    public MemoryController(ChatClient.Builder builder) {

        this.chatClient = builder.build();
    }


    private InMemoryChatMemory inMemoryChatMemory = new InMemoryChatMemory();

    @GetMapping("/memory/chat")
    public String chat(@RequestParam(value = "input", defaultValue = "讲一个笑话") String input) {
        Prompt prompt = new Prompt(input);
        return chatClient.prompt(prompt).advisors(new MessageChatMemoryAdvisor(inMemoryChatMemory)).call().content();
    }


}

功能测试:先告诉它我是一名java程序员然后让它推荐一本适合我的书籍,在两次对话过程中,已经成功记住第一次预设的身份,应用重启后失效

GET http://127.0.0.1:8080/memory/chat?input=我是一名java程序员

你好,Java程序员 👨‍💻 很高兴认识同行~

作为开发者,我可以帮你:

🔸 调试代码:遇到报错或意外行为?贴片段我来分析

🔸 技术建议:从Spring到JVM优化,或最新框架选型(比如Quarkus vs Micronaut)

🔸 面试备战:算法题解、设计模式场景题、并发难题等

🔸 工具链:Maven/Gradle技巧、Docker集成、性能调优工具(Arthas?)

需要哪方面的支持?或者最近在做什么有趣的项目吗? 😄

(小提示:如果想分享代码,可以用代码块格式,我会更清晰阅读哦~

GET http://127.0.0.1:8080/memory/chat?input=推荐一本适合我的书籍

根据你的Java程序员身份,我会针对不同需求推荐几本经典书籍,并标注适用阶段和方向,供你选择:


一、核心进阶(必读)

  1. 《Effective Java》(Joshua Bloch)

    • 👍 适合:中高级开发者

    • 📌 内容:Java最佳实践(泛型、并发、设计模式等),第三版包含Java 8-9新特性

    • 🌟 豆瓣评分:9.5

  2. 《Java并发编程实战》(Brian Goetz)

    • 👍 适合:想深入JUC包、线程安全的开发者

    • 📌 从理论到实战,涵盖锁、原子类、线程池等

    • ⚠️ 注意:需要一定基础


二、架构与设计

  1. 《深入理解Java虚拟机》(周志明)

    • 👍 适合:对JVM底层、GC调优感兴趣

    • 📌 国内原创经典,涵盖类加载、内存模型等

  2. 《Spring实战》(Craig Walls)

    • 👍 适合:Spring生态开发者(Boot/Cloud)

    • 📌 案例驱动,新版涵盖响应式编程


三、新趋势与扩展

  1. 《Java函数式编程》(Venkat Subramaniam)

    • 👍 适合:学习Lambda/Stream API、函数式思维

    • 📌 幽默易懂,代码示例丰富

  2. 《云原生Java》(Josh Long)

    • 👍 适合:云原生/微服务方向

    • 📌 Spring Cloud + Kubernetes实战


四、补充经典

  • 《Head First设计模式》(图文并茂入门设计模式)

  • 《重构:改善既有代码的设计》(Martin Fowler,必读)


你的选择建议:

如果想优先读一本,推荐从 《Effective Java》 开始(常看常新),如果项目涉及并发则选Goetz的书。需要更具体的推荐可以告诉我你的当前目标(如面试/性能优化/新项目技术选型等) 😊

(电子版资源可能需要自行搜索哦~)

除了在ChatClient发起调用时设置MessageChatMemoryAdvisor,还可以在ChatClient构建时设置默认的advisors

java 复制代码
public class MemoryController2 {

    private ChatClient chatClient;

    private InMemoryChatMemory inMemoryChatMemory = new InMemoryChatMemory();

    public MemoryController2(ChatClient.Builder builder) {

        this.chatClient = builder.defaultAdvisors(new MessageChatMemoryAdvisor(inMemoryChatMemory)).build();

    }

    @GetMapping("/memory/chat2")
    public String chat(@RequestParam(value = "input", defaultValue = "讲一个笑话") String input) {
        Prompt prompt = new Prompt(input);
        return chatClient.prompt(prompt).call().content();
    }

}

三、记忆隔离:防止张冠李戴

问:如果多个用户同时聊天,如何防止AI把张三的咖啡订单发给李四?

答:可以通过会话ID来进行记忆隔离

java 复制代码
  @GetMapping("/memory/user/chat")
    public String chatByUser(@RequestParam(value = "input", defaultValue = "讲一个笑话") String input, @RequestParam(value = "userId", defaultValue = "123456") String userId) {
        Prompt prompt = new Prompt(input);
        return chatClient.prompt(prompt).advisors(new MessageChatMemoryAdvisor(inMemoryChatMemory, userId, AbstractChatMemoryAdvisor.DEFAULT_CHAT_MEMORY_RESPONSE_SIZE)).call().content();
    }

使用<font style="color:rgba(0, 0, 0, 0.9);">MessageChatMemoryAdvisor的另外一个构造方法,<font style="color:rgba(0, 0, 0, 0.9);">defaultConversationId表示会话ID,<font style="color:rgba(0, 0, 0, 0.9);">chatHistoryWindowSize表示获取最新的N条记忆

java 复制代码
    public MessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int chatHistoryWindowSize) {
        this(chatMemory, defaultConversationId, chatHistoryWindowSize, Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER);
    }

按照前后顺序发出如下三个请求来测试

GET http://localhost:8080/memory/user/chat?input=你好,我是张三&userId=zhangsan

你好,张三!😄 我是AI助手,你可以叫我小深,只要你愿意随时分享,我都会认真倾听、尽力帮忙!

GET http://localhost:8080/memory/user/chat?input=你知道我是谁吗?&userId=lisi

目前,我无法直接识别你的身份,因为我们之前的对话记录不会保留,而且我没有访问用户个人信息的权限。不过,如果你愿意,可以告诉我一些关于你的信息(比如兴趣、职业等),这样我可以更好地为你提供帮助! 😊

GET http://localhost:8080/memory/user/chat?input=你知道我是谁吗?&userId=zhangsan

你好,张三!😊 作为AI助手,我无法直接知道现实中的"你是谁"哦~每次对话都是新的开始,除非你主动告诉我更多信息(比如兴趣、需求等),我会根据聊天内容尽量提供贴合你的帮助!

有什么想聊的或需要建议的吗?比如:

  • 日常:天气、心情、趣事分享

  • 实用:学习/工作技巧、生活小窍门

  • 深度话题:科技、哲学、冷知识...

等你随时开口~ 🌟

合理设计会话ID的生成规则、失效策略在生产环节中起到关键作用

java 复制代码
public class SessionId {
    /
     * ID(需要有合适的规则)
     */
    private String id;

    /
     * 失效时间,控制会话的有效性
     */
    private Date expireTime;

    /
     * 创建时间
     */
    private Date createTime;
}

四、永恒的记忆

InMemoryChatMemory是内存记忆,应用重启后会发现记忆直接丢失, 如果想要把记忆永久保存下来,可以考虑使用MysqlRedis等存储,那我们用Redis来快速实现一个。

2.1 Redis的安装

采用Spring集成Docker的方式引入RedisDocker本地的安装在前面章节中已经完成,直接通过https://start.spring.io/ 引入依赖;

java 复制代码
    <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-redis-store-spring-boot-starter</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-docker-compose</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-spring-boot-docker-compose</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

compose.yaml引入Redis镜像信息,配置访问端口为6379

添加一段测试Redis访问的代码

java 复制代码
package com.lkl.test.spring.docker;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@Slf4j
@RestController
public class RedisController {

    @Resource
    private RedisTemplate<String, String> redisTemplate;


    @GetMapping("/add/test")
    public String add() {
        String key = "test";
        String value = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(key, value);
        return "添加成功";
    }

    @GetMapping("/query/test")
    public String query() {
        String key = "test";
        return "查询成功:" + redisTemplate.opsForValue().get(key);
    }


}

应用启动后会自动拉取镜像,在Docker控制台可看到如下运行信息

Redis采用默认配置因此不需要在application.properties中配置参数信息;执行测试代码成功调用。

2.2 RedisChatMemory

InMemoryChatMemory实现ChatMemory接口,该接口定义如下

  • add表示往记忆中添加一条或者多条信息,其中conversationId表示会话ID
  • get表示获取记忆,lastN指定获取最新的n条记忆数据
  • clear 清楚记忆 ;
java 复制代码
public interface ChatMemory {

    // TODO: consider a non-blocking interface for streaming usages

    default void add(String conversationId, Message message) {
        this.add(conversationId, List.of(message));
    }

    void add(String conversationId, List<Message> messages);

    List<Message> get(String conversationId, int lastN);

    void clear(String conversationId);

}

可以利用Reids的List操作来做一个简易版的记忆能力

java 复制代码
package com.lkl.test.spring.docker.redis;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AbstractMessage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class RedisChatMemory implements ChatMemory {

    private final RedisTemplate<String, String> redisTemplate;
    private final ObjectMapper objectMapper;
    private final String memoryKeyPrefix = "chat:memory:prefix:"; // Redis Key 前缀

    public RedisChatMemory(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = new ObjectMapper();
    }

    /
     * 获取指定会话的最新 N 条消息(按时间倒序)
     */
    @Override
    public List<Message> get(String conversationId, int lastN) {
        String key = memoryKeyPrefix + conversationId;
        long totalMessages = redisTemplate.opsForList().size(key);

        if (totalMessages == 0) {
            return Collections.emptyList();
        }

        // 计算起始和结束索引(获取最后 N 条)
        long start = Math.max(0, totalMessages - lastN);
        long end = totalMessages - 1;

        // 获取指定范围的消息
        List<String> messageJsons = redisTemplate.opsForList().range(key, start, end);

        return messageJsons.stream().map(this::deserializeMessage).collect(Collectors.toList());
    }

    /
     * 添加消息到会话历史
     */
    @Override
    public void add(String conversationId, Message message) {
        String key = memoryKeyPrefix + conversationId;
        String messageJson = serializeMessage(message);

        // 使用 LPUSH 或 RPUSH 存储消息(LPUSH 表示最新消息在列表头部)
        redisTemplate.opsForList().rightPush(key, messageJson);


        // 可选:设置 Key 的过期时间(例如 30 天)
        redisTemplate.expire(key, 30, TimeUnit.DAYS);
    }

    /
     * 批量添加消息到会话历史(高性能实现)
     */
    @Override
    public void add(String conversationId, List<Message> messages) {
        if (messages == null || messages.isEmpty()) {
            return;
        }

        String key = memoryKeyPrefix + conversationId;
        ListOperations<String, String> listOps = redisTemplate.opsForList();

        // 序列化并批量添加消息
        messages.forEach(message -> {
            String messageJson = serializeMessage(message);
            listOps.rightPush(key, messageJson);
        });

        // 设置 Key 的过期时间(30天)
        redisTemplate.expire(key, 30, TimeUnit.DAYS);

        // 可以考虑使用 Redis Pipeline 批量操作(减少网络开销)
    }

    /
     * 清除指定会话的记忆
     */
    @Override
    public void clear(String conversationId) {
        redisTemplate.delete(memoryKeyPrefix + conversationId);
    }


    private String serializeMessage(Message message) {
        Map<String, Object> map = new HashMap<>();
        AbstractMessage abstractMessage = null;
        if (message instanceof AbstractMessage) {
            abstractMessage = (AbstractMessage) message;
        }

        map.put("type", abstractMessage.getMessageType().getValue());
        map.put("content", abstractMessage.getText());
        try {
            return objectMapper.writeValueAsString(map);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    private Message deserializeMessage(String json) {
        try {
            Map<String, String> map = objectMapper.readValue(json, Map.class);
            String type = map.get("type");
            String content = map.get("content");
            return switch (type) {
                case "user" -> new UserMessage(content);
                case "assistant" -> new AssistantMessage(content);
                default -> throw new IllegalArgumentException("Unknown type: " + type);
            };
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

}

构建RedisMemoryController来测试依然保留了记忆。

java 复制代码
@Slf4j
@RestController
public class RedisMemoryController {

    private ChatClient chatClient;

    private RedisChatMemory redisChatMemory;

    public RedisMemoryController(ChatClient.Builder builder, RedisTemplate<String, String> redisTemplate) {

        this.redisChatMemory = new RedisChatMemory(redisTemplate);

        this.chatClient = builder.defaultAdvisors(new MessageChatMemoryAdvisor(redisChatMemory)).build();

    }

    @GetMapping("/memory/redis")
    public String chat(@RequestParam(value = "input", defaultValue = "讲一个笑话") String input) {
        Prompt prompt = new Prompt(input);
        return chatClient.prompt(prompt).call().content();
    }

}

该自定义的记忆能力还可以继续优化,比如做记忆压缩,对长篇大论做总结后记忆、对敏感信息做剔除,不记录用户的隐私信息、性能监控关注Redis的内存使用情况

2.3 自己动手

在Spring AI中还内置JdbcChatMemoryCassandraChatMemoryNeo4jChatMemory,引入类似的pom即可;通过RedisChatMemory的自定义实现,相信很容易这些拓展实现,自己可以动手试试吧~

java 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-jdbc</artifactId>
</dependency>

结语:从金鱼到最强大脑

通过 Spring AI 的记忆功能,我们成功让 AI:

  • 记住了用户身份
  • 实现了多用户隔离
  • 做到了持久化存储

最终效果:

  • 用户:"和上次一样" → AI 秒懂
  • 用户:"你知道我的名字吗?" → AI:"当然,张三!"

AI的进化路线:

🐟 金鱼脑 → 🧠 最强大脑 → 🏛️ 记忆宫殿管理员

相关推荐
小小不董6 分钟前
Oracle OCP认证考试考点详解083系列09
linux·数据库·oracle·dba
feng9952010 分钟前
从巴别塔到通天塔:Manus AI 如何重构多语言手写识别的智能版图
大数据·人工智能·机器学习
Echo``16 分钟前
19:常见的Halcon数据格式
java·linux·图像处理·人工智能·windows·机器学习·视觉检测
南風_入弦21 分钟前
set autotrace报错
oracle
Rubypyrrha21 分钟前
Spring框架的设计目标,设计理念,和核心是什么 ?
java·spring
佩奇的技术笔记22 分钟前
Java学习手册:Spring 多数据源配置与管理
java·spring
白熊18829 分钟前
【计算机视觉】3d人体重建:PIFu/PIFuHD:高精度三维人体数字化技术指南
人工智能·计算机视觉·3d
知舟不叙1 小时前
使用OpenCV 和 Dlib 进行卷积神经网络人脸检测
人工智能·opencv·cnn
struggle20252 小时前
SurfSense开源程序是NotebookLM / Perplexity / Glean的开源替代品,连接到外部来源,如搜索引擎
人工智能·开源·自动化
亚里随笔2 小时前
Nemotron-Research-Tool-N1 如何提升大语言模型工具使用能力?
人工智能·语言模型·自然语言处理