大模型记忆怎么做?从 ChatMemory 到生产级上下文管理
大模型本身其实没有记忆。
用户先说:
帮我找 Java 岗位
接着说:
薪资 25k 以上
第二句话之所以能被理解,是因为应用程序再次调用模型时,把第一句话也一起发送了过去。
所以所谓的大模型记忆,本质上就是:
保存历史消息
→ 选择需要的历史
→ 塞进下一次 Prompt
Spring AI 用 ChatMemory 封装了这个过程。
1. ChatMemory 做了什么?
最基本的流程是:
用户发送消息
↓
读取历史消息
↓
历史 + 当前消息
↓
发送给 LLM
↓
保存用户消息和模型回复
配合 MessageChatMemoryAdvisor 后,这些事情都可以自动完成。
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build()
)
.build();
业务代码不再需要自己拼聊天记录。
但生产环境真正麻烦的是另外三个问题:
上下文越来越长
服务重启记忆消失
多用户、多 Agent 会串话
所以真正的上下文管理,重点不是“保存聊天记录”,而是:
哪些消息应该保存,哪些消息应该发送给模型。
2. 第一层:智能裁剪上下文
最简单的做法是:
只保留最近 20 条消息
问题是消息价值不同。
比如:
我有 5 年 Java 分布式系统经验
价值很高。
而:
好的
嗯
收到
ok
几乎没有价值。
如果单纯按照时间淘汰,可能把重要信息删掉,却留下大量无意义消息。
因此可以设计三层裁剪。
第一层:过滤低价值消息
例如过滤用户发送的:
好的
收到
嗯
ok
减少无意义 Token 消耗。
第二层:按照 Token 控制窗口
LLM 真正限制的是 Token,而不是消息条数。
所以应该:
历史消息
↓
估算 Token
↓
超过预算
↓
从最早消息开始删除
同时始终保留最近几轮对话,保证当前交流连贯。
第三层:消息数量兜底
最后再设置:
最多 20 条消息
避免 Token 估算出现偏差。
最终变成:
过滤垃圾消息
↓
Token Budget
↓
Message Limit
↓
发送给 LLM
核心思想是:
上下文窗口不是越多越好,而是信息密度越高越好。
3. 第二层:全量存储,读取时裁剪
一个很值得沉淀的设计是:
存储和上下文窗口不要混为一谈。
例如聊天进行了 100 轮。
数据库或者文件中可以保存完整的:
1 ~ 100 轮
但调用模型时只读取:
摘要
+
最近 10~20 条重要消息
也就是:
完整历史
│
▼
Persistent
│
┌──────┴──────┐
│ │
数据恢复 调用 LLM
│
▼
动态裁剪
│
▼
Context
这样设计有几个好处。
第一,修改窗口大小时不需要重新生成数据。
第二,以后可以重新做摘要。
第三,出了问题可以回放完整对话。
因此:
写入保存事实,读取决定视图。
这个思想不只是适用于 AI,对缓存、日志、事件系统同样适用。
4. 第三层:长期记忆使用摘要
即使保存了完整历史,也不可能把几百轮聊天全部发给模型。
因此可以把早期对话压缩。
例如原来有:
用户想找 Java 后端
工作地点武汉
期望国企
薪资 20k+
5 年 Java 经验
熟悉 Spring Cloud、Redis、MySQL
……
让 LLM 压缩成:
用户具有 5 年 Java 后端经验,
熟悉 Spring Cloud、Redis、MySQL,
目标城市为武汉,
倾向国企岗位,
期望薪资 20k 以上。
之后上下文变成:
历史摘要
+
最近几轮完整聊天
+
当前问题
这是一种非常常见的上下文架构:
Long-term Memory
↓
Summary
Short-term Memory
↓
Recent Messages
↓
Prompt
可以理解为:
早期信息保存“语义”,近期信息保存“细节”。
5. 第四层:会话隔离
生产环境还有一个特别容易踩坑的问题:
串会话。
系统可能同时存在:
用户 A / 用户 B
微信 / 飞书 / 钉钉
私聊 / 群聊
Agent A / Agent B
所以不能只拿:
userId
作为 Conversation ID。
更合理的是组合:
userId
+
channel
+
conversation
+
chatType
+
agent
例如:
2:feishu:a1b2c3:private:job-agent
这样可以保证:
用户之间不串
渠道之间不串
私聊群聊不串
Agent 之间不串
这背后的通用原则是:
Conversation ID 本质上就是上下文的隔离边界。
设计会话 ID,其实就是在设计系统的“记忆作用域”。
6. Agent 记忆可以分成两类
这个地方也很值得沉淀。
会话记忆
例如:
刚才聊了什么
用户上一句话是什么
当前任务进行到哪一步
特点:
短期
变化快
跟当前 Agent 强相关
适合放 ChatMemory。
用户长期记忆
例如:
用户是 Java 开发
有 5 年经验
希望去武汉
倾向国企
特点:
长期稳定
跨会话
可能跨 Agent
更适合存到:
数据库
用户画像
长期 Memory Store
然后每次需要时注入。
所以不要把所有“记忆”都塞进 ChatMemory。
更合理的是:
用户画像 ──────┐
│
历史摘要 ──────┼──→ Prompt
│
近期聊天 ──────┘
7. 一套生产级上下文架构
最终可以抽象成:
用户消息
│
▼
Conversation ID
│
▼
完整历史存储
│
▼
读取历史
│
┌─────────┴─────────┐
▼ ▼
历史摘要 最近消息
│ │
└─────────┬─────────┘
▼
智能裁剪
│
┌──────┼──────┐
▼ ▼ ▼
过滤 Token 条数
│
▼
Prompt
│
▼
LLM
这才是所谓的“大模型记忆系统”。
8. 最值得记住的四句话
第一:
大模型没有记忆,记忆是应用程序构造出来的上下文。
第二:
存储历史和发送历史是两件事:历史可以全量保存,Prompt 必须动态裁剪。
第三:
短期记忆保留细节,长期记忆保存摘要和用户画像。
第四:
Conversation ID 决定了记忆边界,多 Agent 系统必须做好上下文隔离。
以后再做 AI Agent,只要遇到多轮对话,可以先想到这四个词:
Memory
Trim
Summary
Isolation
真正的生产级 ChatMemory,并不是“记得越多越好”。
而是:
在有限的上下文窗口里,把最值得模型知道的信息送进去。