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

<!--
~ 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:

@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.DefaultChatClient$DefaultChatClientRequestSpec 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.DefaultChatClientDefaultChatClientRequestSpec$2@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消息聚合器来采集返回结果,当有类似场景时可直接借用这段逻辑,避免重复造轮子
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<>()实现
@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

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来进行记忆隔离

  @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条记忆

    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的生成规则、失效策略在生产环节中起到关键作用

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/ 引入依赖;

    <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访问的代码

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 清楚记忆 ;
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操作来做一个简易版的记忆能力

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来测试依然保留了记忆。

@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的自定义实现,相信很容易这些拓展实现,自己可以动手试试吧~

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-jdbc</artifactId>
</dependency>

结语:从金鱼到最强大脑

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

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

最终效果:

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

AI的进化路线:
🐟 金鱼脑 → 🧠 最强大脑 → 🏛️ 记忆宫殿管理员

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liaokailin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值