AI Agent 的记忆系统怎么设计?从短期记忆、长期记忆到 RAG,讲透 Memory 工程化
作者:王仕宇(JavaPub)
现在做 AI Agent,大家都很喜欢讲一个词:Memory,也就是记忆。
一个 Agent 如果能记住用户的偏好、项目的技术栈、之前做过的事情,确实会比每次都从零开始的聊天机器人好用很多。
但真正开始开发以后,你会发现事情没那么简单。
明明已经给 Agent 接入了向量数据库,它还是会忘记之前的要求。明明项目已经升级到 MySQL 8,它却还在按照旧版本分析问题。甚至用户已经明确修改了需求,Agent 过几轮对话又把旧需求拿出来了。
所以我觉得,Agent 的记忆问题,不是简单接入一个向量数据库就能解决的。
真正要做的是一套完整的记忆系统。
一、上下文不等于记忆
先说一个很多人容易混淆的问题。
假设用户说:
"帮我开发一个 Go 项目,使用 Gin、GORM 和 MySQL。"
模型在当前对话里当然记得。
因为这句话就在上下文中。
但如果对话越来越长,早期内容被压缩,或者用户明天重新打开一个会话,模型可能就不知道这个项目使用什么技术栈了。
这时候,我们通常会考虑把信息保存起来。
但这里有两种完全不同的数据。
第一种是会话上下文:
text
用户刚才问了什么?
Agent 刚才调用了什么工具?
最近一次执行结果是什么?
第二种是长期记忆:
text
项目使用 Go + Gin。
ORM 使用 GORM。
数据库使用 MySQL 8。
项目采用 Docker Compose 部署。
前者服务于当前任务。
后者可以跨会话复用。
所以我更喜欢把它们分开管理。
Context 负责当前推理,Memory 负责跨时间保留有价值的信息。
二、不是所有聊天记录都值得保存
这是我觉得做 Memory 最容易踩的坑。
有些 Agent 的记忆系统非常简单:
用户说一句话,就存进向量数据库。
Agent 回答一句话,也存进去。
执行一次工具,再存进去。
最后数据库里有几万条记录。
看起来记忆很多,实际上检索质量可能越来越差。
比如:
text
用户:数据库使用 MySQL 5.7。
三个月后:
用户:我们已经升级到 MySQL 8 了。
如果直接把两句话都存进去,Agent 以后检索时就可能同时找到:
text
MySQL 5.7
MySQL 8
然后随机选择一个。
这显然不合理。
因为这里发生的不是新增知识,而是:
已有事实发生了变化。
所以 Memory 系统不能只会 Insert。
还必须支持:
text
Create
Update
Invalidate
Delete
也就是记忆的创建、更新、失效和删除。
三、我更建议把 Agent 记忆分成四类
如果让我设计一个 Agent Memory 系统,我会先分成四种。
1. Working Memory:工作记忆
保存当前任务直接需要的信息。
例如:
json
{
"task_id": "task_10001",
"current_step": "fix_api",
"current_file": "handler/user.go",
"last_error": "invalid request payload"
}
这类信息变化很快。
任务结束后,未必需要长期保留。
2. Episodic Memory:事件记忆
保存过去发生过的重要事情。
例如:
text
2026-10-20:
用户排查过一次 Docker OOM。
最终原因:
容器 memory limit 配置过小。
解决方式:
调整资源限制并优化应用内存。
这类记忆的价值在于:
下次出现相似问题时,Agent 可以参考历史经验。
但历史经验不能直接当作当前故障的根因。
仍然需要重新检查证据。
3. Semantic Memory:事实记忆
保存相对稳定的知识。
例如:
text
项目使用 Go。
数据库版本是 MySQL 8。
部署使用 Docker Compose。
API 使用 RESTful 风格。
这类信息可以跨任务复用。
4. Procedural Memory:过程记忆
保存工作流程和操作规范。
例如:
text
修改 Go 代码以后,
必须执行 go test ./...
修改 Docker Compose 以后,
需要执行 docker compose config。
生产环境数据库变更,
必须经过人工审批。
这类记忆不只是告诉 Agent"知道什么"。
而是告诉它:
做事情时应该遵循什么流程。
不过需要注意,真正重要的安全规则和权限边界,仍然必须由程序强制执行,不能只放在可检索的 Memory 里。
四、一个 Memory 记录应该包含哪些字段?
很多人设计记忆表,可能只有:
text
id
content
embedding
这对于简单语义搜索够用,但对于真正的 Agent,我觉得还不够。
至少还应该考虑:
json
{
"memory_id": "mem_10001",
"scope": "project",
"subject": "project_backend",
"key": "database_version",
"value": "MySQL 8",
"source": "user_confirmed",
"confidence": 1.0,
"status": "active",
"created_at": "2026-10-20T10:00:00Z",
"updated_at": "2026-10-28T10:00:00Z"
}
这里几个字段非常重要。
scope 表示作用范围。
比如是用户级、项目级还是某个任务级。
source 表示信息来源。
是用户明确确认的,还是 Agent 从日志里推测的?
confidence 表示证据可信程度。
但它不能被误认为模型生成的百分数就是真实概率。
status 表示记忆是否仍然有效。
如果数据库从 MySQL 5.7 升级到了 MySQL 8,旧记忆就应该失效,而不是继续参与普通检索。
五、Memory 的作用域必须设计清楚
这是非常重要的一点。
假设同一个用户维护三个项目:
text
项目 A:
Go + Gin + MySQL
项目 B:
Java + Spring Boot + PostgreSQL
项目 C:
Python + FastAPI + Redis
如果所有记忆都放在一个集合里,没有明确作用域,Agent 就很容易混淆。
比如正在开发项目 A,却检索到项目 B 的 PostgreSQL 配置。
所以我更建议至少区分:
text
User Memory
Project Memory
Task Memory
Session Memory
检索时先做权限和作用域过滤,再做语义检索。
例如:
text
当前用户:
user_1001
当前项目:
project_A
当前任务:
task_2001
那么 Agent 只应该读取当前任务需要、且当前用户有权限访问的记忆。
Memory 检索首先是权限问题,其次才是相似度问题。
这一点在多租户 SaaS 系统里尤其重要。
六、向量数据库到底负责什么?
向量数据库主要解决的是:
语义相似性检索。
比如用户问:
"之前那个容器内存问题是怎么解决的?"
记忆里保存的是:
text
Docker 容器因为 cgroup memory limit 导致 OOMKilled。
虽然用户没有直接说:
text
cgroup
OOMKilled
但 Embedding 可以帮助找到语义相近的记录。
大致流程:
text
用户问题
↓
Embedding
↓
Vector Search
↓
候选记忆
↓
过滤与重排
↓
注入 Agent 上下文
但需要注意:
向量数据库解决的是"可能相关"。
并不天然解决:
text
哪条记忆最新?
哪条记忆已经失效?
哪条记忆来自用户确认?
哪条记忆属于当前项目?
这些问题必须由业务层处理。
所以我认为:
Vector Search 是 Memory 系统的一个组件,不是 Memory 系统本身。
七、RAG 和 Memory 也不是一回事
现在很多人会把 RAG 和 Memory 混在一起。
但两者关注点并不完全相同。
RAG 更像:
text
用户提出问题
↓
检索相关知识
↓
将知识提供给模型
↓
生成回答
它经常检索:
text
技术文档
产品手册
代码仓库
企业知识库
Memory 更关注:
text
过去发生了什么?
用户已经确认了什么?
当前任务进行到哪里?
哪些信息发生了变化?
比如:
text
项目 README
可以作为 RAG 的知识来源。
而:
text
用户昨天明确要求不能修改数据库结构
更适合作为项目或任务记忆中的约束。
两者可以结合,但最好不要在数据模型上完全混为一谈。
八、真正困难的是 Memory Write
很多人以为记忆系统最难的是检索。
但我反而觉得:
真正难的是决定什么值得记住。
比如用户说:
"今天这个接口又报 500 了。"
这句话需要长期保存吗?
不一定。
但如果后面经过排查,确认:
text
某个服务的 HTTP 500
是因为连接池没有正确释放连接。
那么这个经过验证的故障结论就可能值得保存。
所以我更喜欢这种写入流程:
text
对话 / 工具结果
↓
提取候选记忆
↓
判断是否值得长期保存
↓
识别作用域
↓
检查是否已有同类事实
↓
更新或新增
↓
保存来源和时间
而不是:
text
所有消息
↓
全部 Embedding
↓
直接入库
尤其用户没有要求持久记忆时,还需要遵循产品的隐私设置和数据保存规则。
不是任何用户信息都适合自动存储。
九、Memory Update 比 Memory Insert 更重要
继续看数据库版本这个例子。
旧记忆:
json
{
"key": "database_version",
"value": "MySQL 5.7",
"status": "active"
}
用户后来明确说:
"项目已经升级到 MySQL 8 了。"
系统不应该简单新增一条:
json
{
"key": "database_version",
"value": "MySQL 8"
}
而应该根据同一项目、同一事实键以及新的确认信息,更新当前有效版本。
可以保留历史记录:
text
MySQL 5.7
状态:superseded
MySQL 8
状态:active
这样既保留变更历史,也不会让旧配置继续干扰当前回答。
不过并不是所有冲突都意味着旧记忆应该被覆盖。
比如:
text
开发环境使用 MySQL 8。
生产环境使用 MySQL 5.7。
这两句话可能同时正确。
所以更新记忆前,必须先检查作用域、时间和语义。
不能仅仅因为内容相似,就互相覆盖。
十、Agent 应该什么时候检索记忆?
我不建议每次模型调用都把所有长期记忆取出来。
比如用户只问:
"1 + 1 等于几?"
根本没有必要检索整个项目知识库。
但如果用户说:
"继续昨天的用户管理模块。"
那 Memory 就非常重要。
所以我更喜欢:
text
用户问题
↓
判断是否需要历史上下文
↓
识别用户 / 项目 / 任务
↓
检索候选记忆
↓
过滤过期和无关内容
↓
注入必要记忆
检索也可以按类型进行。
比如:
text
继续项目开发
优先检索任务状态、项目规范和最近决策。
而:
text
这个错误以前发生过吗?
优先检索历史事件记忆。
这样比无差别向量搜索更准确,也更省 Token。
十一、记忆太多,也会污染上下文
假设系统从数据库里检索了 100 条记忆。
然后全部塞给模型。
可能出现:
text
用户偏好
旧项目配置
历史错误
临时调试信息
无关日志
真正有用的只有三条。
这不但浪费 Token,还可能降低判断质量。
所以 Memory Retrieval 应该有明确的预算。
例如:
yaml
memory:
max_candidates: 30
max_selected: 8
max_context_tokens: 3000
这些数字只是示例。
真正使用时,还需要根据模型上下文和任务复杂度调整。
更重要的是重排逻辑。
我一般会考虑:
text
相关性
时效性
来源可靠性
作用域
当前有效状态
而不是单纯按向量相似度排序。
最相似的记忆,不一定是最应该相信的记忆。
十二、Memory 必须允许"不确定"
比如 Agent 从日志中发现:
text
Redis timeout
于是推断:
text
可能是 Redis 网络不稳定。
这个结论还没有经过验证。
如果 Memory 系统直接保存成:
text
Redis 网络存在问题。
以后 Agent 可能反复引用它。
这样一次不可靠的推断,就逐渐变成系统里的"既定事实"。
我觉得这是非常危险的。
所以最好区分:
text
Observed
Inferred
Confirmed
例如:
json
{
"fact": "Redis 网络可能不稳定",
"evidence_type": "inferred",
"status": "unverified"
}
当后续检查确认:
text
真正原因是磁盘 IO 导致 Redis 延迟。
就应该纠正原来的推断。
不能让未经验证的假设长期污染 Agent 的知识库。
十三、一个实际的 Agent Memory 架构
如果让我基于 Go 或 Python 做一个 Memory 服务,我会先设计成:
text
AI Agent
│
┌──────────┴──────────┐
│ │
▼ ▼
Memory Reader Memory Writer
│ │
▼ ▼
Scope Filter Fact Extractor
│ │
▼ ▼
Hybrid Search Conflict Checker
│ │
▼ ▼
Reranker Memory Updater
│ │
└──────────┬──────────┘
│
▼
Memory Store
│
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
MySQL Vector DB Object Store
MySQL 可以负责:
text
记忆元数据
作用域
状态
来源
更新时间
向量数据库负责:
text
语义检索
对象存储可以保存:
text
大型日志
任务产物
历史快照
当然,这只是一个可扩展的架构。
对于小项目,完全可以先使用 PostgreSQL 加 pgvector,或者一个关系数据库配合简单的全文搜索。
甚至刚开始用 SQLite 都可以。
不要一做 Memory 就上复杂的分布式系统。
先把数据模型和更新规则设计清楚,比选哪一个向量数据库更重要。
十四、Memory 需要一个"来源引用"
这是我觉得很值得做的能力。
比如 Agent 回答:
"你的项目使用 MySQL 8。"
它最好能够知道:
这个信息来自哪里?
例如:
text
来源:
用户在项目讨论中确认。
确认时间:
2026-10-28。
作用域:
project_A。
如果来源是:
text
Agent 根据旧配置推断。
那可信程度就不一样。
对于一些重要信息,我甚至建议在 Agent 内部保留:
text
source_id
source_type
verified_at
这样以后发生冲突,可以回到原始来源核对。
这和我们前面讲的 Agent 证据链,其实是同一个思路。
记忆不只是保存结论,还应该保存结论从哪里来。
十五、长期记忆需要遗忘机制
很多人希望 Agent:
"永远记住所有事情。"
但从工程角度看,我觉得不一定是好事。
比如:
text
一次性的临时调试端口
已经失效的测试环境地址
三个月前的错误日志
旧版本的部署参数
这些信息如果长期保留,反而可能干扰 Agent。
所以我更喜欢给 Memory 增加生命周期管理。
例如:
text
临时任务状态:
任务结束后归档。
临时故障推断:
一段时间后复核或失效。
稳定项目规范:
长期保留,变更时更新。
历史事故记录:
归档保存,需要时检索。
注意,这里的遗忘不是简单定期删除所有旧数据。
而是根据记忆类型,决定保留、归档、失效或删除。
对于个人数据,还需要支持用户查看、更正和删除的权限。
这才是一个真正可用的 Memory 系统。
十六、Agent 的 Memory 也需要评测
前面写过 AI Agent 的评测体系。
Memory 同样需要测试。
比如准备一组场景:
| 测试场景 | 预期行为 |
|---|---|
| 用户确认新技术栈 | 更新当前有效记忆 |
| 旧版本信息仍然存在 | 不把旧配置当作当前事实 |
| 两个项目使用不同数据库 | 不跨项目混淆 |
| 信息来自未经验证的推断 | 保留不确定性 |
| 用户要求继续昨日任务 | 找到正确任务状态 |
| 检索结果与当前文件冲突 | 核对来源和时间 |
| 用户无权访问其他项目 | 不返回其他项目记忆 |
| 用户要求删除个人记忆 | 按产品的数据删除机制处理 |
我觉得这里尤其要测试:
记忆冲突、过期信息和跨项目隔离。
因为真正危险的 Memory 错误,很多不是"完全没记住"。
而是:
记住了错误的事情,还特别自信地拿出来用。
十七、王仕宇的观点:Memory 最难的不是记住,而是知道什么该相信
我是王仕宇,JavaPub。
我现在越来越觉得,Agent 的记忆系统和传统数据库其实有很多相似之处。
比如:
text
Memory Write
≈
数据写入
Memory Update
≈
数据更新
Memory Scope
≈
租户隔离
Memory Conflict
≈
数据一致性
Memory Expiration
≈
生命周期管理
Memory Retrieval
≈
查询优化
这些问题本来就是后端工程里经常处理的。
但 Agent 又多了一层难度:
模型可能会把推测当成事实。
也可能把过期事实当成当前事实。
所以真正成熟的 Memory 系统,不应该只是:
text
Embedding
+
Vector Database
+
Top K
而应该是:
text
事实提取
+
来源管理
+
作用域隔离
+
冲突处理
+
状态更新
+
语义检索
+
时效控制
这一整套机制。
我觉得对于后端工程师来说,这反而是一个很值得深入的方向。
因为它不只是 Prompt Engineering。
而是真正涉及数据模型、检索、权限、状态和一致性的系统设计。
最后
我是王仕宇,JavaPub。
AI Agent 想要长期工作,就绕不开 Memory。
但一个好的记忆系统,不是把用户所有聊天记录都存进向量数据库。
而是能够区分:
哪些信息只是当前任务的临时上下文,哪些是已经确认的项目事实,哪些是可以复用的历史经验,哪些又是需要严格遵守的工作流程。
更重要的是,它需要知道:
什么信息已经过期,什么结论没有得到验证,哪些记忆只属于某个项目,以及什么时候应该更新或删除。
如果只记住一句话:
AI Agent 的记忆能力,不取决于它存了多少条数据,而取决于它能否在正确的时间、正确的作用域内,找到当前有效、来源可靠、真正有用的信息,并且知道什么时候应该修正旧记忆。
本文由王仕宇(JavaPub)整理与分享,持续分享后端开发、AI Agent 和工程化实践。