☰
Agent记忆不跟工具搬家:分层存储与可迁移架构实战
2026/10/2 0:16:49 网站建设 项目流程

1. 为什么 Agent 的记忆总在“搬家”

做过 Agent 项目的人大概都有过这种体验:今天用某个框架搭了一个能记住用户偏好的助手,明天想换个编排引擎或者换一家模型服务,结果发现之前攒下来的对话历史、用户画像、任务上下文全都带不走。要么导出成一份谁都不认识的 JSON,要么干脆锁死在某个平台的数据库里,迁移一次等于重做一次。标题里说的“记忆不跟着工具搬家”,戳的就是这个痛点。

我自己从最早用 LangChain 的ConversationBufferMemory,到后来自己写 Redis 存对话,再到尝试各种带长期记忆的 Agent 框架,前后踩了不下十次“记忆迁移”的坑。最夸张的一次是给一个客服 Agent 换了底层编排框架,结果三万多条历史对话的向量索引全部作废,因为新旧框架对 embedding 的存储结构定义完全不一样。那次之后我就下定决心,记忆层必须和工具层解耦。

这篇内容适合两类人看:一类是正在做 Agent 开发、被记忆管理搞得焦头烂额的工程师;另一类是想入门 AI Agent、但还没意识到“记忆”这件事有多麻烦的初学者。我会把记忆分层的思路、存储选型、迁移方案、并发处理、以及实际落地时那些文档里不会写的坑,全部摊开讲一遍。核心观点就一句话:Agent 的记忆应该是一份独立于任何框架的资产,工具可以换,记忆不能丢。

2. Agent 记忆到底分几层,别再混着存了

2.1 短期记忆和长期记忆的本质区别

很多人一上来就把所有对话往一个数据库里塞,这是最典型的错误。短期记忆和长期记忆在生命周期、访问频率、存储介质上的要求完全不同,混在一起存,查询会越来越慢,成本会越来越高。

短期记忆指的是当前会话窗口内的上下文,它的特点是生命周期短、读写频繁、容量有限。比如用户问“帮我订明天去上海的机票”,Agent 需要记住前面几轮提到的出发城市、时间偏好、舱位要求。这些信息在会话结束后基本就没用了,或者只需要保留一个摘要。短期记忆通常放在内存或者带 TTL 的缓存里,比如 Redis 设置 30 分钟过期,或者直接放在进程内存里。

长期记忆则是跨会话、跨时间持续存在的信息,特点是生命周期长、写入频率低、需要检索。比如用户说“我对花生过敏”,这条信息应该在三个月后的对话里依然生效。长期记忆需要持久化存储,并且通常需要支持语义检索,也就是用向量数据库来做相似度匹配。

我见过太多项目把这两者塞进同一张 MySQL 表,结果就是每次对话都要全表扫描历史记录,QPS 一上来直接崩。正确的做法是分层存储,短期用 Redis 或内存,长期用向量库加关系库的组合。

2.2 双网络记忆模型的启发

热词里提到的“双网络记忆模型”和“记忆=score+时间半衰期”其实指向同一个思路:记忆不是平等对待的,每条记忆都应该有一个权重,这个权重由重要性和时效性共同决定。

我参考这个思路设计了一套打分机制。每条长期记忆在写入时计算一个基础分数,来源包括:用户显式声明的信息(比如“我住在北京”)给高分,Agent 推断出来的信息(比如从对话中推测用户可能喜欢咖啡)给低分。然后引入时间半衰期,比如设定半衰期为 30 天,那么一条 30 天前的高分记忆,现在的有效分数会衰减到原来的一半。

具体公式可以简化为:

effective_score = base_score * (0.5 ** (days_elapsed / half_life))

检索的时候按 effective_score 排序,低于阈值的记忆直接不返回。这样做的好处是,过时的、不重要的记忆会自动沉底,不需要手动清理,也不会干扰当前对话。

2.3 记忆编码:从 1 到 100 的粒度控制

“1到100记忆编码大全”这个热词听起来夸张,但背后是一个真实需求:记忆的粒度需要可控。粗粒度的记忆比如“用户是一个程序员”,细粒度的记忆比如“用户上周三提到他在用 Python 3.11 写一个爬虫项目”。

我的做法是把记忆分成几个层级:事实层、偏好层、事件层、摘要层。事实层存不变的属性,偏好层存喜好,事件层存具体发生过的事,摘要层存对话的压缩总结。每一层用不同的编码方式,事实层和偏好层用结构化字段,事件层和摘要层用向量加元数据。

这样设计之后,迁移的时候只需要导出这四层的数据,换任何框架都能重新加载,因为结构是自定义的,不依赖任何框架的内部格式。

3. 记忆与工具解耦的架构设计

3.1 为什么要把记忆层独立出来

工具会换,框架会换,模型会换,但记忆是跟着业务走的。把记忆层独立成一个服务,好处有三个:第一,迁移成本低,换框架只需要改调用接口;第二,可以复用,多个 Agent 共享同一份用户记忆;第三,便于调试,记忆的读写有独立的日志和监控。

我现在的项目里,记忆层是一个独立的 Python 服务,对外暴露 REST 和 gRPC 两种接口。Agent 框架通过 HTTP 调用它,不直接碰数据库。这样即使我把编排框架从 LangChain 换成别的,记忆层完全不用动。

3.2 存储选型:向量库加关系库的组合拳

存储选型上,我试过纯向量库、纯关系库、以及两者组合。结论是组合最优。

关系库(我用的是 PostgreSQL)存结构化的事实和偏好,比如用户 ID、属性名、属性值、创建时间、基础分数。向量库(我用的是 Milvus,也试过 Qdrant)存事件和摘要的 embedding,附带元数据指向关系库里的记录。

为什么不只用向量库?因为事实类查询需要精确匹配,比如“查用户 ID 为 123 的所有偏好”,向量库做这个很别扭。为什么不只用关系库?因为语义检索需要向量相似度,关系库做不了。

两者通过一个统一的 memory_id 关联,查询的时候先走关系库拿结构化数据,再走向量库拿语义相关的事件,最后合并排序。

3.3 接口设计:让记忆层对框架无感知

接口设计的关键是不暴露任何框架特有的概念。我定义了四个核心接口:

  • write_memory(user_id, content, memory_type, metadata):写入一条记忆
  • read_memory(user_id, query, top_k, memory_types):检索记忆
  • update_memory(memory_id, content, metadata):更新记忆
  • delete_memory(memory_id):删除记忆

所有参数都是通用类型,content 是字符串,metadata 是字典,memory_type 是枚举。框架调用的时候只需要把对话内容传进来,不需要关心底层是 Redis 还是 PostgreSQL。

这样设计之后,我甚至可以用同一个记忆层同时服务两个不同的 Agent 框架,一个用 LangChain,一个用自己写的编排逻辑,互不干扰。

4. 实操:从零搭一个可迁移的记忆层

4.1 环境准备与依赖安装

先列一下我用的技术栈和版本,避免版本不一致导致踩坑:

  • Python 3.11
  • PostgreSQL 15
  • Milvus 2.3(单机版用 Docker 起)
  • Redis 7(短期记忆用)
  • FastAPI 0.104(记忆层服务)

安装依赖:

pip install fastapi uvicorn psycopg2-binary pymilvus redis sentence-transformers

embedding 模型我用的是all-MiniLM-L6-v2,体积小、速度快,适合本地部署。如果对精度要求高,可以换成更大的模型,但推理成本会上升。

4.2 数据库表结构设计

PostgreSQL 里建两张表,一张存记忆主体,一张存记忆的元数据索引:

CREATE TABLE memories ( memory_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id VARCHAR(64) NOT NULL, memory_type VARCHAR(32) NOT NULL, content TEXT NOT NULL, base_score FLOAT DEFAULT 1.0, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_user_type ON memories(user_id, memory_type);

Milvus 里建一个 collection,字段包括 memory_id、embedding、user_id、memory_type:

from pymilvus import CollectionSchema, FieldSchema, DataType fields = [ FieldSchema(name="memory_id", dtype=DataType.VARCHAR, max_length=64, is_primary=True), FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=384), FieldSchema(name="user_id", dtype=DataType.VARCHAR, max_length=64), FieldSchema(name="memory_type", dtype=DataType.VARCHAR, max_length=32), ] schema = CollectionSchema(fields)

dim=384 是因为 MiniLM 的输出维度是 384,换模型的话这个值要跟着改。

4.3 写入与检索的核心逻辑

写入的时候,先算 embedding,然后同时写 PostgreSQL 和 Milvus。这里要注意事务问题,两个库没法做分布式事务,我的做法是先写 PostgreSQL,拿到 memory_id 之后再写 Milvus,如果 Milvus 写失败就回滚 PostgreSQL 的记录。

检索的时候分两步:先根据 query 算 embedding,去 Milvus 查 top_k 个相似记忆,拿到 memory_id 列表;然后去 PostgreSQL 查这些 memory_id 的详细信息和分数;最后按 effective_score 排序返回。

effective_score 的计算:

import math from datetime import datetime def effective_score(base_score, created_at, half_life_days=30): days_elapsed = (datetime.now() - created_at).days decay = 0.5 ** (days_elapsed / half_life_days) return base_score * decay

这个函数在检索时对每条记忆调用一次,排序后取前 N 条。

4.4 短期记忆的缓存策略

短期记忆用 Redis 的 List 结构存,key 是session:{session_id},每条消息是一个 JSON 字符串。设置 TTL 为 1800 秒,也就是 30 分钟无活动自动过期。

写入短期记忆:

import redis, json r = redis.Redis(host='localhost', port=6379, db=0) def write_short_term(session_id, role, content): key = f"session:{session_id}" message = json.dumps({"role": role, "content": content, "ts": time.time()}) r.rpush(key, message) r.expire(key, 1800)

读取的时候用lrange拿最近 N 条,拼成上下文传给模型。会话结束时,把短期记忆压缩成摘要,写入长期记忆。

5. 并发场景下记忆层怎么扛住压力

5.1 并发写入的冲突问题

“AI Agent 怎么扛并发”这个热词问到了点子上。记忆层的并发压力主要来自两个方面:一是多个会话同时写入,二是同一个用户的多轮对话快速写入。

PostgreSQL 本身能扛住一定的并发,但 Milvus 的写入在高并发下会有延迟。我的做法是引入一个写入队列,用 Redis 的 Stream 做缓冲,后台起一个消费者进程批量写入 Milvus。这样前端写入请求只需要写 PostgreSQL 和 Redis Stream,响应时间稳定在 10ms 以内。

批量写入的消费者逻辑:

def consume_and_write(): while True: messages = r.xreadgroup("memory_group", "consumer1", {"memory_stream": ">"}, count=100, block=1000) if messages: batch = [] for _, msgs in messages: for msg_id, data in msgs: batch.append(data) # 批量算 embedding 并写入 Milvus write_batch_to_milvus(batch) r.xack("memory_stream", "memory_group", msg_id)

5.2 读取缓存的命中率优化

检索是读多写少的场景,缓存能大幅降低延迟。我在记忆层前面加了一层 Redis 缓存,key 是query:{user_id}:{hash(query)},value 是检索结果的 JSON。TTL 设 300 秒。

缓存命中率的关键是 query 的归一化。用户问“我喜欢什么”和“我的偏好是什么”,语义相近但字符串不同,直接做 key 会命中不了。我的做法是先用 embedding 算相似度,如果和缓存里的 query 相似度超过 0.95,就直接返回缓存结果。

实测下来,在客服场景下缓存命中率能到 60% 以上,平均检索延迟从 80ms 降到 30ms。

5.3 记忆更新的幂等性保证

同一个用户可能在不同会话里重复说同一件事,比如“我住在北京”说了三次。如果每次都写一条新记忆,长期下来会有大量重复。

我的做法是在写入前先做一次相似度检查,如果已有记忆的 embedding 和待写入内容的相似度超过 0.9,就更新已有记忆的 base_score 和 updated_at,而不是新增。这样既避免了重复,又能让反复提到的信息获得更高的权重。

更新逻辑:

def write_with_dedup(user_id, content, memory_type): embedding = encode(content) similar = milvus_search(embedding, user_id, top_k=1) if similar and similar[0].score > 0.9: update_memory_score(similar[0].memory_id, boost=0.1) else: insert_new_memory(user_id, content, memory_type, embedding)

6. 迁移实战:换框架不丢记忆

6.1 导出与导入的标准化格式

迁移的核心是格式标准化。我定义了一个 JSON Schema,所有记忆导出都遵循这个格式:

{ "version": "1.0", "exported_at": "2024-01-15T10:00:00Z", "memories": [ { "memory_id": "uuid", "user_id": "user123", "memory_type": "fact", "content": "用户住在北京", "base_score": 1.0, "created_at": "2024-01-01T00:00:00Z", "embedding": [0.1, 0.2, ...] } ] }

embedding 也一起导出,这样导入新系统时不需要重新算,节省大量时间。如果新系统用的 embedding 模型不同,那就需要重新编码,但至少原始内容还在。

6.2 从旧框架迁移的实操步骤

假设要从一个用 LangChain 内置记忆的项目迁移到自建记忆层,步骤是这样的:

第一步,写一个脚本遍历旧框架的记忆存储,把对话历史导出来。LangChain 的ConversationBufferMemory可以通过load_memory_variables拿到全部历史。

第二步,把历史对话按轮次切分,每一轮用户输入和 Agent 回复合并成一条事件记忆,调用记忆层的write_memory接口写入。

第三步,对重要的用户声明做提取,比如用正则或小模型识别“我是...”“我喜欢...”“我不要...”这类句式,单独写成事实记忆或偏好记忆。

第四步,验证迁移结果,随机抽几条旧对话,在新系统里检索,看能否召回相关记忆。

整个过程我实测下来,一万条对话的迁移大概需要 20 分钟,主要时间花在 embedding 计算上。如果导出时带了 embedding,时间能压缩到 5 分钟以内。

6.3 迁移后的验证清单

迁移完不能直接上线,要过一遍验证清单:

检查项验证方法通过标准
记忆总数对比新旧系统 count误差小于 1%
检索召回抽样 100 条 query召回率大于 90%
分数衰减检查 30 天前记忆的分数符合半衰期公式
并发写入压测 100 QPS无丢失、无重复
缓存命中监控 1 小时命中率大于 50%

这个清单我每次迁移都会跑一遍,能提前发现大部分问题。

7. 常见问题与排查技巧实录

7.1 记忆检索不准的排查思路

检索不准通常有三个原因:embedding 模型不适合当前语言、相似度阈值设得不对、记忆粒度太粗。

先检查 embedding 模型。如果用的是英文模型处理中文内容,效果会很差。换成多语言模型或者中文模型,召回率能提升一大截。

再检查阈值。Milvus 默认返回 top_k 个结果,但不带阈值过滤。如果 top_k 设成 10,可能后 5 个都是不相关的。我的做法是加一个相似度阈值,比如 0.7,低于这个值的不返回。

最后检查粒度。如果一条记忆里塞了太多信息,比如“用户喜欢咖啡、住在北京、是程序员”,检索“用户住哪”的时候可能召回这条,但信息不精确。拆成三条独立记忆,检索准确率会高很多。

7.2 记忆膨胀导致成本失控

长期运行的系统,记忆会越来越多,存储成本和检索延迟都会上升。我遇到过一个月涨了 50 万条记忆的情况,Milvus 查询明显变慢。

解决办法是定期做记忆压缩。把低分的、过时的记忆归档到冷存储,或者合并成摘要。我写了一个定时任务,每周跑一次,把 effective_score 低于 0.1 的记忆标记为归档,从 Milvus 里删除,但保留在 PostgreSQL 里备查。

归档逻辑:

def archive_low_score_memories(threshold=0.1): memories = pg_query("SELECT memory_id, base_score, created_at FROM memories") to_archive = [] for m in memories: if effective_score(m.base_score, m.created_at) < threshold: to_archive.append(m.memory_id) milvus_delete(to_archive) pg_update("UPDATE memories SET archived = TRUE WHERE memory_id = ANY(%s)", (to_archive,))

这样 Milvus 里只保留活跃记忆,查询速度快,成本也可控。

7.3 多用户记忆隔离的坑

多用户场景下,记忆隔离是必须的。我见过一个项目因为没做隔离,A 用户的偏好被 B 用户检索到了,直接导致线上事故。

隔离要在三个层面做:数据库查询必须带 user_id 条件,Milvus 检索必须带 user_id 过滤,缓存 key 必须包含 user_id。任何一层漏了都会出问题。

Milvus 的过滤表达式:

search_params = {"metric_type": "IP", "params": {"nprobe": 10}} results = collection.search( data=[embedding], anns_field="embedding", param=search_params, limit=10, expr=f'user_id == "{user_id}"' )

这个 expr 一定要加,不加就是全库检索,既慢又不安全。

7.4 记忆写入失败的兜底方案

写入失败不能影响主流程。我的做法是写入操作全部异步化,前端只负责把消息丢进队列,后台慢慢处理。如果队列积压,就降级为只写 PostgreSQL,Milvus 的写入延后。

降级逻辑:

def write_memory_safe(user_id, content, memory_type): try: write_to_pg(user_id, content, memory_type) write_to_stream(user_id, content, memory_type) except Exception as e: log_error(e) # 降级:只写 PG,标记待补 write_to_pg(user_id, content, memory_type, pending_embedding=True)

后台有一个补偿任务,定期扫描 pending_embedding 为 true 的记录,补算 embedding 并写入 Milvus。

8. 几个我踩过的坑和对应技巧

第一个坑是 embedding 模型升级导致的历史记忆失效。我一开始用 MiniLM,后来想换成效果更好的 BGE 模型,结果发现两个模型的向量空间不兼容,旧记忆的 embedding 在新模型下检索完全不准。解决办法是保留旧模型的编码器,迁移时用旧模型重新编码所有历史记忆,或者干脆双模型并行一段时间,逐步切换。

第二个坑是 Redis 短期记忆的序列化问题。我一开始用 pickle 序列化消息,后来发现不同 Python 版本之间不兼容,迁移环境后反序列化失败。换成 JSON 之后就没这个问题了,虽然体积大一点,但兼容性好。

第三个坑是 Milvus 的 collection 重建。有一次我改了 schema,想直接删了重建,结果忘了先导出数据,几万条记忆直接没了。从那以后我养成了习惯,任何 schema 变更前先跑一次全量导出,确认备份文件没问题再动手。

第四个坑是时间半衰期的参数选择。半衰期设太短,记忆衰减太快,用户上周说的话这周就忘了;设太长,过时信息一直干扰检索。我试过 7 天、30 天、90 天,最后在客服场景下选了 30 天,在个人助手场景下选了 90 天。这个参数没有标准答案,要根据业务场景调。

第五个坑是并发写入时的重复记忆。两个请求同时判断“没有相似记忆”,然后同时插入,结果出现两条一样的。解决办法是在 PostgreSQL 里对 user_id 加 content 的哈希做唯一索引,插入冲突时改为更新。

CREATE UNIQUE INDEX idx_unique_memory ON memories(user_id, md5(content));

这样即使并发插入,数据库层面也能保证不重复。

9. 记忆层的未来扩展方向

这套架构跑了大半年,稳定性没问题,但我还在持续优化。一个方向是引入记忆的重要性自动评估,用一个小模型对每条新记忆打分,而不是靠规则。另一个方向是记忆的跨用户共享,比如同一个团队的用户可以共享某些公共记忆,这在企业场景下很有用。

还有一个我比较感兴趣的方向是记忆的可解释性。现在检索出来一条记忆,只能看到内容和分数,但为什么这条记忆被召回、它对当前对话有什么影响,是不透明的。如果能给每条记忆附上一个“召回理由”,调试起来会方便很多。

这些扩展都不影响核心架构,因为记忆层和工具层已经解耦了,上层怎么变,底层的数据结构和接口都不用动。这也是我一开始坚持解耦的原因:工具会过时,框架会淘汰,但一份结构清晰、格式标准的记忆资产,可以一直用下去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询