首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深入Spring AI:刨析 ChatMemory

深入Spring AI:刨析 ChatMemory

原创
作者头像
有一只柴犬
发布2026-01-12 15:56:20
发布2026-01-12 15:56:20
2170
举报
文章被收录于专栏:人工智能人工智能

1、序言

我们回顾一下,在《深入Spring AI与OpenAI集成:实现智能对话系统》一文中,我们实现上下文记忆的代码:

代码语言:java
复制
public Flux<String> chatWithMemoryStream(String conversationId, String message) {
    
        ChatClient.StreamResponseSpec resp = ChatClient.builder(openAiChatModel)
                // 设置历史对话的保存方式,这里我们使用内存保存
                .defaultAdvisors(new PromptChatMemoryAdvisor(chatMemory))
                .build()
                .prompt().user(message)
                .advisors(advisor ->
                        // 设置保存的历史对话ID
                        advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
                                // 设置需要保存几轮的历史对话,用于避免内存溢出,因为这里我们没做持久化
                                .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 50)
                ).stream();
        return resp.content();
    }

实现上下文记忆时,我们定义了advisor来保存历史对话ID,advisor机制我们前面的篇幅已经介绍过了。而在defaultAdvisors(new PromptChatMemoryAdvisor(chatMemory))这个中传入了一个chatMemory对象,这个就是我们今天要介绍的。

2、如果没有ChatMemory

大语言模型(LLM)本身是无状态的,也就是意味着模型本身是不会保留以往历史的交互信息的。这样局限性就会很大,我们不希望AI跟我们聊天的时候没有上下文语境。

2.1、传统的Chat对话

当我们不使用ChatMemory来对AI进行多轮提问时:

代码语言:java
复制
@RestController
public class NoMemoryChatController {

    private final ChatClient chatClient;
    public NoMemoryChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/no_memory")
    public String no_memory(@RequestParam("text") String text) {
        UserMessage userMessage = new UserMessage(text);
        String content = chatClient.prompt(new Prompt(userMessage)).call().content();
        System.out.println(content);
        return content;
    }

}

第一次提问:

第二次提问:

会发现两次提问没有上下文语境,响应并不连贯。

2.2、手动实现memory

假如我们把上下文的信息都保存起来,然后每次提问的时候都把这个信息发给LLM:

代码语言:java
复制
@RestController
public class MyMemoryChatController {

    private final ChatClient chatClient;
    public MyMemoryChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }


    // 定义一个集合,将所有问答信息保存起来
    private final List<Message> historyMessages = new CopyOnWriteArrayList<>();

    @GetMapping("/my_memory")
    public String my_memory(@RequestParam("text") String text) {
        UserMessage userMessage = new UserMessage(text);
        historyMessages.add(userMessage);

        // 将保存的历史信息传递给 ChatClient
        String content = chatClient.prompt(new Prompt(historyMessages)).call().content();
        System.out.println(content);
        return content;
    }
}

第一次提问:

第二次提问:

这样就已经初步完成了上下文记忆能力,回答看起来具有连贯性了。

2.3、弊端

那么这样简单的实现有什么弊端呢?明显的人一下子就看出来了,典型的问题有以下几个:

  • 会话没有隔离。我们将多次请求的信息都保存到了当前上下文的hisotyMessages中,直接将其传给了LLM,而没有对会话进行关联。理想状态下,应该是一个会话对应一个上下文。正如我们上文提到的AdvisorContext一样。
  • 内存数据丢失。数据保存在内存中,并没有进行持久化保存,当程序中断或服务器断电等情况时,内存数据会丢失。
  • Token不一致。熟悉AI大模型的应该都知道,每个大模型对于输入或输出的文本内容均有token限制。因为不同的token对应算力要求是不一样的,而我们所定义的list是通过java代码实现的,java代码可没有token限制这一说。因此很容易导致java代码保存进去的内容与实际chat的token不一致。

Spring AI当然也是考虑到这点了,因此有了ChatMemory的实现。

3、ChatMemory概述

Large language models (LLMs) are stateless, meaning they do not retain information about previous interactions. This can be a limitation when you want to maintain context or state across multiple interactions. To address this, Spring AI provides a ChatMemory abstraction that allows you to store and retrieve information across multiple interactions with the LLM.

大型语言模型 (LLM) 是无状态的,这意味着它们不会保留有关以前交互的信息。当您希望在多个交互中维护上下文或状态时,这可能是一个限制。为了解决这个问题,Spring AI 提供了一个 ChatMemory 抽象,允许您在与 LLM 的多次交互中存储和检索信息。

Spring AI中的ChatMemory是对话记忆管理模块,负责在AI对话过程中维护和存储对话历史上下文。它允许开发者以灵活的方式管理对话状态,确保AI模型能够基于完整的对话历史生成连贯的响应。

核心功能包括:

  • 存储对话消息: ChatMemory 能够保存用户和 AI 之间的对话消息。 ——历史会话存储功能
  • 检索对话历史: 它允许你根据会话 ID 检索之前的对话消息。——会话隔离
  • 管理会话: 提供了按会话 ID 组织和管理对话历史的能力。——会话组织管理能力
  • 清除历史: 可以清除特定会话或所有会话的聊天历史。
  • 控制历史长度: 一些实现允许你限制每个会话保留的历史消息数量。

看起来完美解决了我们上面提到的几个问题。

ChatMemory是个抽象接口,我们看一下org.springframework.ai.chat.memory.ChatMemory的源码:

代码语言:java
复制
/**
 * The ChatMemory interface represents a storage for chat conversation history. It
 * provides methods to add messages to a conversation, retrieve messages from a
 * conversation, and clear the conversation history.
 * 
 * ChatMemory 接口代表着聊天对话历史的存储。它提供了向一个对话添加消息、从一个对话检索消息以及清除对话历史的方法。
 * 
 * @author Christian Tzolov
 * @since 1.0.0
 */
public interface ChatMemory {

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

    // 默认实现根据会话ID:conversationId 来 添加message的方法
    default void add(String conversationId, Message message) {
       this.add(conversationId, List.of(message));
    }
    
    // 添加会话消息

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

    // 根据会话ID:conversationId 获取保存的消息,lastN 获取最近几条
    List<Message> get(String conversationId, int lastN);

    
    // 清除某个会话消息
    void clear(String conversationId);

}

3.1、快速使用

要使用ChatMemory其实很简单,Spring AI会自动装配一个ChatMemory的bean,我们只需要使用:

代码语言:java
复制
@Autowired
ChatMemory chatMemory;

即可直接使用。ChatMemory提供了多种持久化机制,默认情况下会使用内存存储库来存储消息(InMemoryChatMemoryRepository),并使用 MessageWindowChatMemory 实现来管理对话历史记录。如果已经配置了不同的存储库(例如,Cassandra、JDBC 或 Neo4j),Spring AI 将改用该存储库。

3.2、支持的存储类型

Spring AI提供了多种不同的存储类型的实现,ChatMemory 抽象允许我们自己实现各种类型的内存以适应不同的用例。

3.2.1、InMemoryChatMemory

InMemoryChatMemory 是ChatMemory的默认实现。他使用 ConcurrentHashMap 将消息存储在内存中。

代码语言:java
复制
/**
 * The InMemoryChatMemory class is an implementation of the ChatMemory interface that
 * represents an in-memory storage for chat conversation history.
 *
 * This class stores the conversation history in a ConcurrentHashMap, where the keys are
 * the conversation IDs and the values are lists of messages representing the conversation
 * history.
 * 
 * InMemoryChatMemory 类是 ChatMemory 接口的一个实现,它代表着聊天对话历史的内存存储。
 * 这个类将对话历史存储在一个 ConcurrentHashMap 中,其中键是会话 ID,值是代表该会话历史的消息列表。
 * @see ChatMemory
 * @author Christian Tzolov
 * @since 1.0.0 M1
 */
public class InMemoryChatMemory implements ChatMemory {

    Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();

    @Override
    public void add(String conversationId, List<Message> messages) {
       this.conversationHistory.putIfAbsent(conversationId, new ArrayList<>());
       this.conversationHistory.get(conversationId).addAll(messages);
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {
       List<Message> all = this.conversationHistory.get(conversationId);
       return all != null ? all.stream().skip(Math.max(0, all.size() - lastN)).toList() : List.of();
    }

    @Override
    public void clear(String conversationId) {
       this.conversationHistory.remove(conversationId);
    }

}

从方法的调用链上,我们可以看到Spring AI分别在什么时候调用了InMemoryChatMemory的add和get方法:

可以看到他最终使用了我们前面介绍到了ProptChatMessageAdvisor和MessageChatMemoryAdvisor。这就跟我们前面讲解的advisor机制串起来了。

同样的,get方法也是如此:

4、自定义ChatMemory

ChatMemory是个标准接口,我们完全可以自定义属于我们自己的ChatMemory。只要实现ChatMemory接口,完成方法的实现即可。 这里我们尝试实现一个基于文件持久化的能力。并且使用不同的会话ID生成不同的文件存储。

4.1、实现ChatMemory接口

代码语言:java
复制
public class FileCacheChatMemory implements ChatMemory {
    @Override
    public void add(String conversationId, List<Message> messages) {
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {
    }

    @Override
    public void clear(String conversationId) {
    }
}

4.2、定义保存文件路径

代码语言:java
复制
private final Path basePath;

public FileCacheChatMemory(String basePathParam) {
    this.basePath = Paths.get(basePathParam);
    try {
        Files.createDirectories(this.basePath);
    } catch (IOException e) {
        throw new RuntimeException("Could not create base directory for chat history: " + this.basePath, e);
    }
}

private Path getFilePath(String conversationId) {
    return basePath.resolve(conversationId + ".txt");
}

4.3、实现add方法

代码语言:java
复制
@Override
public void add(String conversationId, List<Message> messages) {
    List<Message> retMessages = fileCacheMemory.computeIfAbsent(conversationId, k -> {
        List<Message> existingMessages = get(conversationId, Integer.MAX_VALUE);
        return new ArrayList<>(existingMessages);
    });
    retMessages.addAll(messages);
    Path filePath = getFilePath(conversationId);
    messages.forEach(message -> {
        try {
            Files.writeString(filePath, message.getText() + System.lineSeparator(),
                    Files.exists(filePath) ? StandardOpenOption.APPEND : StandardOpenOption.CREATE,
                    StandardOpenOption.WRITE);
        } catch (IOException e) {
            throw new RuntimeException("Could not write chat message to file: " + filePath, e);
        }
    });
}

4.4、实现get方法

代码语言:java
复制
@Override
public List<Message> get(String conversationId, int lastN) {
    if (fileCacheMemory.containsKey(conversationId)) {
        return fileCacheMemory.get(conversationId);
    }
    Path filePath = getFilePath(conversationId);
    if (Files.exists(filePath)) {
        try {
            List<String> lines = Files.readAllLines(filePath);
            List<Message> messages = lines.stream()
                    .map(UserMessage::new)
                    .collect(Collectors.toList());
            fileCacheMemory.put(conversationId, messages);
            return messages;
        } catch (IOException e) {
            throw new RuntimeException("Could not read chat history from file: " + filePath, e);
        }
    }
    return new ArrayList<>();
}

4.5、实现clear方法

代码语言:java
复制
@Override
public void clear(String conversationId) {
    fileCacheMemory.remove(conversationId);
    Path filePath = getFilePath(conversationId);
    try {
        Files.deleteIfExists(filePath);
    } catch (IOException e) {
        throw new RuntimeException("Could not delete chat history file: " + filePath, e);
    }
}

4.6、使用

到此,基本就自定义完成了。我们直接继承到Chat中试下:

代码语言:java
复制
@RestController
public class FileCacheChatMemoryController {

    private static final ChatMemory fileCacheMemory = new FileCacheChatMemory("F:\\MyFileCache");

    private final ChatClient chatClient;
    public FileCacheChatMemoryController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.defaultAdvisors(new MessageChatMemoryAdvisor(fileCacheMemory)).build();
    }

    @GetMapping("/file_memory")
    public String file_memory(@RequestParam String requestId, @RequestParam("text") String text) {
        UserMessage userMessage = new UserMessage(text);
        // 将保存的历史信息传递给 ChatClient
        String content = chatClient.prompt(new Prompt(userMessage))
                .advisors(
                        // 设置会话id
                advisor -> advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, requestId)
                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 50)
        ).call().content();
        System.out.println(content);
        return content;
    }
}

运行后查看结果:

理论上,会持久化生成属于会话11111的一份文件:

可以发现很容易就可以完成一个自定义的ChatMemory的集成。到这里,又是开始发挥各种创意的时候了~ 看官们可以根据自己的需求定义属于自己的memory存储,比如Redis?mongodb?Mysql?等等。甚至我们可以根据此进行记忆的分类、分区。如短期记忆?长期记忆等,以达到更符合我们要求的LLM能力。

5、小结

Spring AI的ChatMemory模块设计简洁而强大,通过清晰的接口定义和灵活的扩展点,为开发者提供了构建复杂对话系统的基础设施。理解其内部实现机制有助于开发者根据具体需求进行定制和优化,构建更高效、更智能的对话应用。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、序言
  • 2、如果没有ChatMemory
    • 2.1、传统的Chat对话
    • 2.2、手动实现memory
    • 2.3、弊端
  • 3、ChatMemory概述
    • 3.1、快速使用
    • 3.2、支持的存储类型
      • 3.2.1、InMemoryChatMemory
  • 4、自定义ChatMemory
    • 4.1、实现ChatMemory接口
    • 4.2、定义保存文件路径
    • 4.3、实现add方法
    • 4.4、实现get方法
    • 4.5、实现clear方法
    • 4.6、使用
  • 5、小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档