我们回顾一下,在《深入Spring AI与OpenAI集成:实现智能对话系统》一文中,我们实现上下文记忆的代码:
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对象,这个就是我们今天要介绍的。
大语言模型(LLM)本身是无状态的,也就是意味着模型本身是不会保留以往历史的交互信息的。这样局限性就会很大,我们不希望AI跟我们聊天的时候没有上下文语境。
当我们不使用ChatMemory来对AI进行多轮提问时:
@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;
}
}第一次提问:

第二次提问:

会发现两次提问没有上下文语境,响应并不连贯。
假如我们把上下文的信息都保存起来,然后每次提问的时候都把这个信息发给LLM:
@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;
}
}第一次提问:

第二次提问:

这样就已经初步完成了上下文记忆能力,回答看起来具有连贯性了。
那么这样简单的实现有什么弊端呢?明显的人一下子就看出来了,典型的问题有以下几个:
Spring AI当然也是考虑到这点了,因此有了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是个抽象接口,我们看一下org.springframework.ai.chat.memory.ChatMemory的源码:
/**
* 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);
}要使用ChatMemory其实很简单,Spring AI会自动装配一个ChatMemory的bean,我们只需要使用:
@Autowired
ChatMemory chatMemory;即可直接使用。ChatMemory提供了多种持久化机制,默认情况下会使用内存存储库来存储消息(InMemoryChatMemoryRepository),并使用 MessageWindowChatMemory 实现来管理对话历史记录。如果已经配置了不同的存储库(例如,Cassandra、JDBC 或 Neo4j),Spring AI 将改用该存储库。
Spring AI提供了多种不同的存储类型的实现,ChatMemory 抽象允许我们自己实现各种类型的内存以适应不同的用例。
InMemoryChatMemory 是ChatMemory的默认实现。他使用 ConcurrentHashMap 将消息存储在内存中。
/**
* 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方法也是如此:

ChatMemory是个标准接口,我们完全可以自定义属于我们自己的ChatMemory。只要实现ChatMemory接口,完成方法的实现即可。 这里我们尝试实现一个基于文件持久化的能力。并且使用不同的会话ID生成不同的文件存储。
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) {
}
}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");
}@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);
}
});
}@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<>();
}@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);
}
}到此,基本就自定义完成了。我们直接继承到Chat中试下:
@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能力。
Spring AI的ChatMemory模块设计简洁而强大,通过清晰的接口定义和灵活的扩展点,为开发者提供了构建复杂对话系统的基础设施。理解其内部实现机制有助于开发者根据具体需求进行定制和优化,构建更高效、更智能的对话应用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。