SpringAI核心类MessageWindowChatMemory
摘要: MessageWindowChatMemory是一种基于计数的大模型会话消息窗口管理机制,核心功能是维护指定大小的消息窗口,确保消息总数不超过限制。其特点包括:1)严格的数量限制,仅保留最近的N条消息;2)系统消息的特殊处理,新系统消息会覆盖旧指令,保证唯一性;3)优先保留系统消息,在超出限制时优先删除普通对话消息。实现上通过process方法的滑动窗口算法,按FIFO顺序删除最旧的普通消
MessageWindowChatMemory
在大模型开发的过程中,往往需要模型记录会话的信息,这个时候需要消息的窗口函数可以解决这个问题。这个核心的会话消息窗口函数,可以用于用户-大模型会话的消息存储。
它主要分为以下2个属性:
private static final int DEFAULT_MAX_MESSAGES = 20;
private final ChatMemoryRepository chatMemoryRepository;
private final int maxMessages;
窗口的最大消息条数以及会话记忆的持久化策略。
源码的注释很好的解释了这个窗口函数的基本特性以及功能
- 严格的数量限制 (Hard Limit on Count)
原文:“…maintains a message window of a specified size, ensuring that the total number of messages does not exceed the specified limit.”
🔍 特征:这是一个基于 计数(Count-based) 的窗口,而不是基于时间(Time-based)的窗口。
⚙️ 行为:
无论消息发送的时间跨度多大(是 1 分钟前还是 1 小时前),只要总数超过了设定的 maxMessages(例如 10 条),就会立即触发清理机制。
系统只关心数量,不关心消息的“年龄”。 - 系统提示词指令的“唯一性”与“更新机制” (System Message Uniqueness & Update)
原文:“Messages of type SystemMessage are treated specially: if a new SystemMessage is added, all previous SystemMessage instances are removed from the memory.”
🔍 特征:互斥性。内存中同一时刻只能存在 一个 有效的 SystemMessage。
⚙️ 行为:
采用 “覆盖更新” 策略。
如果您发送了一个新的系统提示词(例如:“现在你是一个翻译助手”),代码会自动检测并删除之前所有的旧系统提示词(例如:“你是一个编程助手”)。
目的:防止多个系统指令在上下文中“打架”,导致模型困惑(例如既想当翻译又想当程序员)。 - 系统提示词指令的“优先保留权” (Priority Preservation)
原文:“Also, if the total number of messages exceeds the limit, the SystemMessage messages are preserved while evicting other types of messages.”
🔍 特征:特权保护。在资源不足(需要删除旧消息)时,SystemMessage 拥有 “免死金牌”。
⚙️ 行为:
当消息总数超标时,算法会优先删除普通的用户消息(UserMessage)和助手消息(AssistantMessage)。
即使那个唯一的 SystemMessage 是很久以前发的(位于列表头部),它也不会被当作“最旧的消息”被删除。
目的:确保大模型永远不会丢失“人设”或“核心指令”,这是对话能正常进行的基础。
一句话总结
它是一个“保人设、去旧存新”的智能过滤器:
始终只保留最近的 N 条 对话,但会自动替换旧人设,并且在空间不足时优先牺牲普通对话,死保当前人设不丢失。
其中消息添加的add方法的最核心逻辑为:process 方法
add 方法源码
@Override
public void add(String conversationId, List<Message> messages) {
Assert.hasText(conversationId, "conversationId cannot be null or empty");
Assert.notNull(messages, "messages cannot be null");
Assert.noNullElements(messages, "messages cannot contain null elements");
List<Message> memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId);
List<Message> processedMessages = process(memoryMessages, messages);
this.chatMemoryRepository.saveAll(conversationId, processedMessages);
}
process 滑动窗口算法核心逻辑
private List<Message> process(List<Message> memoryMessages, List<Message> newMessages) {
List<Message> processedMessages = new ArrayList<>();
Set<Message> memoryMessagesSet = new HashSet<>(memoryMessages);
boolean hasNewSystemMessage = newMessages.stream()
.filter(SystemMessage.class::isInstance)
.anyMatch(message -> !memoryMessagesSet.contains(message));
memoryMessages.stream()
.filter(message -> !(hasNewSystemMessage && message instanceof SystemMessage))
.forEach(processedMessages::add);
processedMessages.addAll(newMessages);
if (processedMessages.size() <= this.maxMessages) {
return processedMessages;
}
int messagesToRemove = processedMessages.size() - this.maxMessages;
List<Message> trimmedMessages = new ArrayList<>();
int removed = 0;
for (Message message : processedMessages) {
if (message instanceof SystemMessage || removed >= messagesToRemove) {
trimmedMessages.add(message);
}
else {
removed++;
}
}
return trimmedMessages;
}
滑动窗口裁剪逻辑深度解析
A. 遍历方向:从头到尾 (FIFO)
- 数据结构:processedMessages 是一个 ArrayList
- 排列顺序:
[最旧的消息, ..., 较旧的消息, ..., 最新的消息]
- 执行逻辑:
- for (Message message : processedMessages) 从索引 0 开始向后遍历。
- 结论:最先被检查的是最旧的消息。removed++ 计数时同时丢弃消息
B. 裁剪逻辑推演
📋 假设场景
- 限制数量:maxMessages = 5
- 当前列表(共 8 条):
[Sys(旧), User1, AI1, User2, AI2, User3, AI3, Sys(新)]
(注:假设 Sys(新) 包含在 newMessages 中)
- 需要删除数量:
messagesToRemove = 8 - 5 = 3
🔄 循环执行过程
| 步骤 | 当前消息 | 判断条件 | 动作 | 状态 (removed) |
|---|---|---|---|---|
| 1 | Sys(旧) |
instanceof SystemMessage 为 true |
✅ 保留 | 0 |
| 2 | User1 |
非 System 且 0 < 3 |
❌ 删除 | 1 |
| 3 | AI1 |
非 System 且 1 < 3 |
❌ 删除 | 2 |
| 4 | User2 |
非 System 且 2 < 3 |
❌ 删除 | 3 |
| 5 | AI2 |
非 System 但 3 >= 3 (配额用完) |
✅ 保留 | 3 |
| 6+ | User3, AI3, Sys(新) |
removed >= 3 始终成立 |
✅ 全部保留 | 3 |
🏁 最终结果列表
[Sys(旧), AI2, User3, AI3, Sys(新)]
// 共 5 条消息
核心代码逻辑
// 这段代码只负责把旧列表中的 SystemMessage 过滤掉(如果有新的话)
memoryMessages.stream()
.filter(message -> !(hasNewSystemMessage && message instanceof SystemMessage))
.forEach(processedMessages::add);
List<Message> trimmedMessages = new ArrayList<>();
int removed = 0;
// 关键点:这里是按顺序遍历 processedMessages
for (Message message : processedMessages) {
if (message instanceof SystemMessage || removed >= messagesToRemove) {
trimmedMessages.add(message); // 保留
}
else {
removed++; // 丢弃(计数器+1)
}
}
C. 这个顺序的含义
-
优先删除最旧的普通对话
- 由于是从头(最旧端)开始遍历,最早发生的 User/AI 对话会最先被牺牲。
- 这符合“保留最新上下文”的直觉
-
SystemMessage 的“不死金身”
- 机制:只要代码执行到 if (message instanceof SystemMessage …) 为真,该消息就会被无条件添加到结果列表中,完全不消耗 removed 配额。
- 潜在 Bug/特性:
- 如果前面的 filter 逻辑失效(例如 equals 比较失败),导致 Sys(旧) 没有被移除,这个循环会强行保留它。
- 这意味着即使是最旧的消息,只要是 SystemMessage,也能插队存活。
- 正常情况下的防御性编程
- 理想流程:前面的 filter 逻辑应该已经把所有旧的 SystemMessage 都剔除了。
- 实际进入循环时:列表中理论上只剩下一个位于末尾的 Sys(新)。
- 代码意图:此时的 if 判断主要是一种防御性编程。
- 虽然 Sys(新) 位于列表末尾,按顺序本来也轮不到删除它。
- 但加上这个判断,可以确保万一逻辑出现异常或边界情况,系统指令依然拥有最高优先级,绝不会被误删。
总结图示
原始列表 (Head -> Tail):
[旧Sys, 旧User1, 旧AI1, 旧User2, 旧AI2, 新User3, 新AI3, 新Sys]
^ ^ ^ ^ ^ ^ ^ ^
| | | | | | | |
| 删 删 删 保 保 保 保
| (1) (2) (3) (够数) (够数) (够数) (Sys保护)
|
保留 (Sys保护)
最终列表:
[旧Sys*, 旧AI2, 新User3, 新AI3, 新Sys]
(*注:正常情况下旧Sys应在第一步被 filter 移除,若未移除则会在此处被强制保留)
遍历顺序是从旧到新。策略是**“杀掉最旧的 N 个普通人,但绝不杀系统指令**”。这确保了大模型永远能收到最新的人设指令,同时尽可能保留最近的对话上下文。
更多推荐


所有评论(0)