上个月和一个做企业级 AI 助手的朋友聊架构,他一句话点醒我:“Demo 里的 Agent 什么都能干,一上生产就变成了个没记性的傻子。”这话一点不夸张。早期我也干过把对话历史塞进一个 List、再整段拼进 Prompt 的操作,内部演示跑得飞起。可真的接上真实用户之后,问题一个接一个冒出来:上下文长度被撑爆、多轮意图老是对不上、用户上个礼拜改过的偏好完全记不住。这时候才意识到,Agent 的“记忆”不是往 Prompt 里多塞几轮历史记录,它需要一个独立设计、可水平扩展、能治生命周期管理的服务来承载。
这篇文章就是围绕“企业级 Agent Memory Service”这件事写的。我不会只推某个具体的开源项目,而是把我在几个项目里做记忆模块时的完整思考链拆开:记忆到底要存哪些东西、存储引擎怎么选、写入和遗忘的流程怎么设计、分片和缓存怎么做、以及真正上了生产之后踩过的几个记忆相关的事故。适合正在做 Agent 开发的工程师、正在设计记忆模块的技术负责人,以及准备给 Agent 产品搭后端服务的架构师。
1. 先想清楚:Agent 的记忆到底需要存什么?
很多人第一步就做反了:先兴致勃勃去选向量数据库,再回来看数据模型。我建议反过来,先把“记忆”这个词拆开,因为 Agent 的记忆不是一块铁板,不同种类的记忆,生命周期、访问频率、存储要求完全不同。
1.1 按用途把记忆拆成四层
我在设计时习惯把 Agent 的记忆分成四类:工作记忆、情景记忆、语义记忆、程序记忆。这不是学术定义,是为了方便做存储设计和容量规划。
- 工作记忆(Working Memory):当前会话内的临时状态,比如用户上一句话说的“待办事项草稿”、正在填写的表单数据。它高频读写、生命周期极短,会话结束或者一段时间不活跃就应该清理。它更适合放在进程内内存或者 Redis 这类高速缓存里,连持久化都是次要的。
- 情景记忆(Episodic Memory):用户和 Agent 之间发生过的事实性交互记录,比如“上周二让助手生成了季度报表”。它带有时间维度,回答“上次那个任务进行到哪了”这类问题时要用。这类数据量大,按时间和用户维度增长,适合文档型存储加向量索引。
- 语义记忆(Semantic Memory):从交互中抽象出来的稳定事实和偏好,比如“用户偏好简洁回复”“用户公司主营跨境电商”。它的特点是低频更新、高价值、查询频繁。这正是向量检索的主要战场,通常需要抽取出结构化条目。
- 程序记忆(Procedural Memory):Agent 学习和沉淀下来的任务执行模式,比如处理某类工单时的工具调用习惯。这类记忆在实践中往往直接沉淀为配置规则、工具定义或代码模板,很少需要数据库,更多是版本治理。
这四类记忆可以整理成一张访问模式表,方便后续对接存储层:
| 记忆类型 | 生命周期 | 访问模式 | 一致性要求 | 典型载体 |
|---|---|---|---|---|
| 工作记忆 | 分钟级 | 高频读写 | 弱一致,可丢失 | 内存 / Redis |
| 情景记忆 | 长期 | 追加写 + 按时间/条件查 | 中 | 文档库 + 向量索引 |
| 语义记忆 | 中长期 | 低频更新 + 高频查询 | 强 | 结构库 + 向量索引 |
| 程序记忆 | 长期 | 读多写少 | 强 | 配置中心 / 代码仓库 |
1.2 Memory Service 的边界是什么
拆完记忆类型,下一个问题就是:什么是“Memory Service”?它绝对不只是封了一层数据库读写。我理解的 Memory Service 要承担的是记忆的完整生命周期:写入、索引、查询、更新、遗忘、订正、衰减、导出。还可以打个比方,如果 Agent 是人,Memory Service 就是他的海马体加长期记忆系统,你总不能让大脑直接拿硬盘来存东西。
我见过不少项目把记忆做成服务里的一个 Repository 类,这在小场景下没问题,但到了企业级,记忆往往是多个 Agent 共用的。A 助手写入的记忆,B 助手要能读到;用户通过 Web 端修改偏好,Agent 侧要能感知。如果有记忆一条都散落在各个 Agent 进程里,这些协作场景全都玩不转。记忆服务在组织形态上应该是一个独立的中间件:有明确的 API、有数据模型、有可观测性、有权限控制。到这一步,选型才有了讨论的前提。
2. 存储引擎选型:向量库、关系库、消息队列各管哪一层
先说结论:没有哪个数据库能同时承接全部记忆类型,真实的 Memory Service 一定是多种存储组件组合出来的。选型的核心是搞清楚“哪个组件承担哪一层记忆”,而不是找一个万能数据库。
2.1 向量数据库的横向对比
语义记忆的检索绕不开向量化,所以向量数据库往往是选型的重头。我用过的几个主流方案,一句话感受如下:
- Milvus/Zilliz Cloud:定位是大规模生产级。支持 Collection 分片、分区、混合检索(向量 + 标量过滤),生态组件完整。缺点是运维偏重,它依赖 etcd、对象存储、消息组件,小团队自建需要一定的运维储备。
- Qdrant:Rust 写的,单机性能很好,Payload 过滤能力强,API 设计得非常直观,Docker 一条命令可以跑起来。我在中等规模项目里用得很顺手,它自带 WAL,数据可靠性比一般纯检索库要好。
- Chroma:轻量、本地开发友好,向量化、持久化开箱即用,适合原型验证。但它是为实验场景设计的,高并发生产环境里我遇到过一些一致性和性能波动,不太适合直接扛企业级流量。
- pgvector:PostgreSQL 的向量扩展,适合中小团队。最大优势是复用已有 PG 运维栈,向量数据和业务元数据在同一套事务里,过滤条件能走 SQL,强一致性好。缺点是向量索引的构建和海量扩展能力弱于专业向量库,上亿向量之后会比较吃力。
| 方案 | 部署复杂度 | 生产级扩展 | 过滤能力 | 一致性 | 推荐场景 |
|---|---|---|---|---|---|
| Milvus | 高 | 强 | 中 | 最终一致 | 大型独立服务 |
| Qdrant | 中 | 强 | 强 | WAL 保障 | 中型生产 / 独立服务 |
| Chroma | 低 | 弱 | 中 | 一般 | 原型验证 |
| pgvector | 低 | 中 | 极强(SQL) | 强 | 已有 PG 栈的小团队 |
选型时我有一个基本判断标准:如果你的记忆条目本身有大量的元数据过滤需求(按用户、按时间、按业务线过滤),Qdrant 的 payload 过滤和 pgvector 的 SQL 过滤都比 Milvus 早期版本的标量过滤好写很多。反过来,如果你要处理的是几十亿向量、要求极致的水平扩展,那 Milvus 这种分布式架构是主流选择。至于 Chroma,我现在只把它当作本地调试工具用,偶尔用它快速验证 embedding 效果。
2.2 Redis 适合当热记忆层,但不适合当记忆本体
很多团队图省事,用 Redis 存所有记忆,每个 Key 就是一段 JSON。它做短期记忆确实很顺手:TTL 自清理、读写快、数据结构灵活。但如果你把 Redis 当作唯一事实来源,数据丢失风险太高了。Redis 毕竟是个缓存性质的系统,持久化机制在生产抖动时不能满足企业级记忆的可靠性要求。
我的做法是把 Redis 定位为“热记忆加速层”,保存最近 N 小时的记忆条目,服务查询时先走缓存,再回落数据库。长期记忆的权威数据仍然放在向量库或关系库里。这也刚好应对第 3 章要讲的冷热分层。
2.3 消息队列在这里不是“选不选”的问题,而是选哪个的问题
一旦记忆写入开始依赖 LLM 抽取、向量化这些耗时操作,同步写入就会把用户请求拖得很慢。所以记忆服务里消息队列不是可选项,而是绕不开的异步底座。我实际用过 Kafka、RabbitMQ、RocketMQ,它们在这个场景下适合做的事完全不同。
- Kafka:高吞吐、消息可回放、分区有序。记忆写入链路最适合它,因为写入量大且需要消费者故障后重放。我把记忆事件流直接落在 Kafka,消费者挂了可以从上一次 offset 继续,不会漏掉记忆。
- RabbitMQ:路由灵活、延迟队列好用,适合任务分发和需要复杂路由的轻量场景。在记忆服务里,它更适合做后台任务的调度,比如“延迟 10 分钟再对某条记忆做后验证”。
- RocketMQ:事务消息和延时消息是强项,适合“本地事务和消息发送强一致”的场景。如果你的记忆服务本身要操作数据库事务,又希望通过事务消息保证写入一致性,RocketMQ 是很好的选择。
| 特性 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 吞吐量 | 极高 | 中高 | 高 |
| 消息重放 | 原生支持 | 较弱 | 支持 |
| 事务消息 | 较弱 | 不支持 | 强 |
| 延迟消息 | 需自研 | 原生支持 | 原生支持 |
| 适合记忆场景 | 记忆事件流、异步写入 | 任务分发、延迟处理 | 强一致事务写入 |
如果你刚开始搭建记忆链路,我建议优先上 Kafka 做异步写入和重放兜底。后面每一步消费逻辑有问题都能靠重放来修复,这个兜底能力在生产环境太值钱了。
3. 记忆的写、查、忘、改:核心流程才是服务的灵魂
选好存储只是第一步,真正决定 Memory Service 好不好用的,是记忆条目怎么定义、写入链路怎么设计、检索和遗忘怎么治理。
3.1 记忆条目先定义好 Schema,再写代码
很多团队把记忆当成自由文本,嵌入向量库就撒手不管。结果检索出来一堆语义相似但没用的片段,权重、时效、归属都分不清。我建议每条记忆都按结构化条目建模,包含元数据、内容、权重、时间线。下面是我在一个项目里实际用过的记忆条目 JSON 结构,字段可以按业务裁剪:
{ "memory_id": "mem_8f3k...", "tenant_id": "t_1001", "user_id": "u_20547", "memory_type": "semantic", "content": { "subject": "user.preference", "attribute": "reply_style", "value": "concise", "source_text": "以后回复我尽量简短一些" }, "embedding_version": "bge-m3-v1.4", "importance_score": 0.87, "confidence_score": 0.92, "created_at": "2026-01-15T10:24:00Z", "updated_at": "2026-01-15T10:24:00Z", "expire_at": null, "source_event_id": "evt_a1b2...", "tags": ["user.preference", "style"] }这里有几个字段我特别想强调:memory_type决定后续用哪条检索链路;importance_score给遗忘机制做排序依据;source_event_id是幂等消费的关键,后面专门讲;embedding_version是踩坑事故的根源,第 5 章会展开。
3.2 写入链路:把 LLM 抽离出核心路径
记忆写入不是一个同步动作。对话结束时,Agent 把原始对话丢给记忆抽取服务,由 LLM 提炼成结构化记忆条目,再走异步写入。这套流程的完整链路大概是:
- 对话回合结束,Agent 发送原始交互数据到 Memory Service 的写入 API。
- 记忆抽取服务调用 LLM,将原始文本转为候选记忆条目(JSON),并给重要性和置信度打分。
- 候选条目经过校验(Schema 检查、敏感词过滤、去重判断)后,发布到 Kafka 的
memory.commands主题。 - 记忆写入消费者从 Kafka 拉取消息,生成向量,对向量库执行 upsert,同时更新元数据表。
- 写入完成后,向热缓存层同步最新记忆,并返回确认事件。
幂等在这里是底线。Kafka 消息可能被重复消费,网络重试也可能带来重复请求。我坚持用source_event_id做唯一键,写入一律走 upsert 而不是 insert。如果同样的source_event_id已经存在,直接跳过。不然用户会看到同一条记忆出现两次,检索时也会反复查到相同内容。
3.3 检索策略:纯向量召回是不够的
检索质量决定 Agent 的“记性”好不好。纯向量检索的问题在于它只懂语义相似,不懂时间、重要性和事实约束。同一个用户问你“我上次改的地址是什么”时,向量上可能召回像但不对的历史片段。
我实际使用的检索策略是“向量召回 + 元数据过滤 + 重排截断”的组合拳。具体流程可以用这个伪代码描述:
recalled = vector_store.query( embedding=embed_user_query(query), filter={ "tenant_id": tenant_id, "user_id": user_id, "memory_type": ["semantic", "episodic"], "expire_at": {"$gt": now} }, top_k=50 ) recalled = rerank_by_time_and_importance(recalled, now) recalled = deduplicate_by_subject(recalled) final = truncate_to_budget(recalled, max_tokens=1200)关键点有三个。第一,元数据过滤必须先做,把范围钉在对应的用户和租户上,不然跨用户串记忆就是安全事故。第二,向量只解决“像不像”,重排阶段要按时间衰减和 importance_score 综合排序,否则几个月前的泛泛记录会把最新偏好挤掉。第三,召回结果不能全部塞给 Agent,要给 token 预算并截断,长篇大论的记忆会污染 Agent 的上下文。口径上,我一般把对单次对话注入的记忆量控制在总上下文窗口的 20%-30% 以内。
3.4 遗忘机制:和写入一样重要
记忆不是越多越好。塞满历史的 Agent 会变得又慢又糊涂,遗忘是记忆服务里最容易偷懒但又最不该偷懒的部分。我把遗忘任务拆成三路:基于 TTL 的自动过期、基于重要性和容量的策略淘汰、用户触发的强制删除。
TTL 最简单,适合临时性记忆,直接靠数据库过期时间清理。策略淘汰需要扫描低 importance 分数的记忆,定期归档或删除。用户强制删除是为了满足数据合规,删掉用户明确要求遗忘的数据时,要同步删缓存、删向量记录,不能只删数据库里的主记录。每次遗忘操作都要落审计日志,谁能删、删了什么、什么时候删的,都该查得到。
3.5 更新与冲突:同一事实,以谁为准
真实环境里,记忆之间经常打架。用户这个月说“我喜欢详细报告”,上个月说“简短一点就行”,系统里两条语义记忆都是可信的,但语义上已经冲突了。直接覆盖不可取,一起保留又会让 Agent 混乱。
我的策略是为每条语义记忆维护updated_at和confidence_score两个排序字段。检索时优先取置信度更高的版本,置信度差不多则取时间更新的一条。如果检测到新写入的记忆与旧记忆在 subject 上重复,后台任务会将旧条目标记为 superseded,而不是物理删除。这既保留了时间线,又避免在检索时被旧记忆干扰。
4. 可扩展性设计:从单体一路走到分片与缓存
可扩展性是企业级和 Demo 之间最本质的差别。构建 Memory Service 时,我习惯先把容量模型摆出来,再往里填技术方案。
4.1 容量估算先做,扩展策略后定
假设一条语义记忆的文本约 200 字节,对应的 768 维 float32 向量约 3KB,再加上元数据索引,整体可以按 4KB 每条估算。如果每天新增 100 万条记忆,向量存储相当于每天增长 4GB,一个月就是 120GB,一年 1.4TB。这还没算上情景记忆的原文存储和日志开销。有了这个数字,你就能判断:单机支撑几个月没问题,但按年规划,分片和冷热分层不是可选项,而是必选项。
4.2 分片策略:按租户 Hash,还是按时间
分片维度直接决定后续运维体验。按租户 Hash 分片的好处是数据分布均匀、单租户访问可以路由到固定分片,坏处是大租户会产生热点。按时间分片适合情景记忆这种时序追加模型,但跨时间段检索要聚合多个分片,复杂度会上来。
我实际采用的分片方式是复合策略:主分片按tenant_id哈希,每个分片内部再按时间做冷热分层。热点租户识别出来后,直接把它的记忆迁移到独立分片甚至独立实例。没有一种分片策略是一劳永逸的,关键是你得在写入 API 里预留重分片的能力,否则数据倾斜出现时只能干瞪眼。
4.3 缓存设计:Read-Through 与 Write-Behind 搭配
读多写少是记忆访问的常态。我给记忆服务设计了 Read-Through 缓存:查询先走 Redis,命中就直接返回;不命中则回源向量库和元数据表,再把结果写回缓存。短期记忆则用 Write-Behind 模式,先更新缓存、再异步写回持久层,降低写入延迟。这里要特别注意,Write-Behind 意味着数据可能短暂不一致,必须有补偿任务周期性地检查缓存和持久层的差异,不然缓存一抖动就会丢记忆。
4.4 多租户隔离:三种模式按客户价值选
企业级场景一定绕不开多租户。三种常见模式分别是“独立实例”、“独立 Collection/Schema”、“共享实例 + 行级隔离”。三者的隔离强度、成本和运维复杂度差异很大,没有绝对的好坏。
| 隔离模式 | 隔离强度 | 成本 | 典型场景 |
|---|---|---|---|
| 独立实例 | 最强 | 最高 | 金融、政务级大客户 |
| 独立 Collection/Schema | 中强 | 中 | 中型客户,私有定制需求 |
| 共享 + 行级 tenant_id | 逻辑隔离 | 最低 | SaaS 标准产品 |
我自己的经验是,大部分场景用第三种起步,但前提是所有读写 API 都必须强制带上tenant_id过滤,索引也必须覆盖这个字段。否则一旦漏掉某个查询条件,用户数据互相串,这在企业级场景中是致命事故。
5. 踩坑实录:几个真实事故的完整排查链路
选型指南写得再漂亮,都不如把生产环境里的真实事故拿出来讲得透彻。这几个坑我都亲手踩过,回报一下排查思路,希望你能直接绕开。
5.1 事故一:升级 embedding 模型,旧向量全部“失忆”
现象:某天检索召回率暴跌,明明库里有高度相似的内容,Agent 却查不到。日志里没有报错,只是命中内容质量突然下降。
排查过程:先看检索日志,确认查询向量没有异常;再随机抽取库里的向量和查询向量做相似度对比,发现距离全面变大。这时才意识到,几天前我们刚升级了 embedding 模型,旧数据向量还是老的维度分布,新查询向量来自新模型,两边根本不在同一个向量空间里。
修复方案:给所有记忆条目加embedding_version字段,写入时按新版本生成向量;同时启动离线重向量任务,分批把旧向量用新模型重新生成后覆盖。这个事故给我的教训是,embedding 模型升级必须当成数据库迁移一样管理,要有版本记录、有回滚方案、有重放任务,绝不能只改一行调用就上线。
5.2 事故二:记忆膨胀,Prompt 被塞爆
现象:用户的单轮响应越来越慢,Token 费用明显上涨。查了一下 Prompt,发现系统把 70 多条记忆全部注入了上下文,很多是几个月前的琐碎记录。
排查过程:追踪记忆注入逻辑,发现检索阶段没有设置 top_k 上限,也没有对召回结果做重要性截断。向量库把“相似的都返回了”,Agent 上下文窗口又有限,于是不得不强行塞入大量低质量记忆。
修复方案:在检索链路中增加严格的预算控制,召回 50 条后按时间和重要性重排,再按 token 预算截断到 1200 字以内。同时给每条记忆设置importance_score,低于阈值的记忆只在特定任务时才检索。修复后响应延迟下降明显,费用也回归正常。
5.3 事故三:Kafka 重复消费,同一记忆出现两遍
现象:用户发现系统重复记住同一件事,对话里出现“你刚才说过了”的尴尬情况。数据库里也确实查到了两条内容完全相同的记忆。
排查过程:先怀疑业务逻辑没有去重,再看消费者日志,发现确实有重复消费的情况,Kafka 消费端因处理超时触发了 rebalance,导致同一条消息被并发消费了两次。
修复方案:增加以source_event_id为唯一键的 upsert 写入逻辑,重复消息直接跳过;同时给消费者配置幂等表,记录最近处理过的 event_id。加了这一层之后,无论 Kafka 怎么重放,记忆都不会重复写入。
5.4 事故四:热点租户打爆单个分片
现象:整体服务没崩,但某个分片的 CPU 持续打满,该租户下的记忆读写明显变慢,甚至影响相邻租户的检索。
排查过程:看分片监控,发现数据量分布严重倾斜,一个大客户的写入量是其他租户的几十倍。所有记忆都按租户哈希分发,流量全部都打到了同一个分片上。
修复方案:把热点租户迁移到独立分片,同时给它配置单独的写入限流和缓存资源;中间件层增加租户流控,避免单个大客户把共享资源耗尽。后续我把这个逻辑做成了自动识别任务,实时监测分片负载,超过阈值就触发迁移。
这些事故都发生在很常规的工程细节上,没有一个是“高级算法”问题。正因如此,它们才更值得重视——企业级可扩展性不是靠某一个惊艳设计方案实现的,而是靠把每个基础环节的可靠性做扎实。
6. 选型决策表:按团队规模直接抄作业
最后给一套可以直接套用的选型决策。这里按团队规模和业务体量分了三档,你可以根据自己的实际情况选。
6.1 中小团队 / 日活几千到几万
推荐组合:PostgreSQL(pgvector)+ Redis + 定时任务队列。这个组合的好处是技术栈收敛,PG 承担业务元数据和向量数据,Redis 做热记忆缓存,定时任务负责记忆压缩和遗忘清理。不需要专门引入 Kafka,用 PG 的LISTEN/NOTIFY或简单的任务表即可。别一开始就上分布式组件,运维复杂度会吃掉你的开发效率。
6.2 中等规模 / 日活几十万到几百万
推荐组合:Qdrant + Kafka + Redis + 独立 Memory Service。Qdrant 用 Docker 或托管服务部署,维护成本远低于 Milvus;Kafka 负责记忆事件流和异步写入;Redis 承担热记忆缓存;记忆服务独立成组件,对外只暴露 API,内部实现写入、抽取、检索和遗忘逻辑。这个组合在扩展性和运维复杂度之间最平衡。
6.3 大型多租户 SaaS / 千万级数据量
推荐组合:Milvus(或云上的托管向量库)+ Kafka + Redis + 对象存储 + Flink。Milvus 解决向量数据的大规模分布式存储,对象存储承担原始对话和记忆归档,Flink(或类似流处理框架)负责记忆的聚合、去重、压缩和跨分片统计。这个方案成本高、运维重,但它是能支撑真正企业级 SLA 的形态。
6.4 我最终落地的一套参考架构
我最近一个中型项目是这样的组合:Qdrant 存语义记忆和情景记忆的向量,PostgreSQL 存记忆元数据和审计日志,Redis 存工作记忆和热检索层,Kafka 串联记忆抽取、写入、重放的全链路。原因是这套组合能让我用最小的运维资源覆盖最大的功能需求。选型不是给自己找最美的组件,而是找最适合你团队当前运维能力的组件。
我个人在多次项目里最大的体会是:构建 Memory Service 的顺序应该先是定义记忆模型和访问模式,再选存储引擎,最后才谈扩展方案。很多人一上来就引入 K8s、Milvus、Flink 全家桶,结果数据量还没起来,先被组件复杂度拖垮了。先把记忆条目、写入链路、遗忘机制跑通,再带着监控数据做分布式演进,这条路会顺畅得多。最后一个小建议:无论选哪套方案,都要给记忆服务做一个“可解释”的后台,能随时把某个用户的记忆列表 dump 出来人工检查。这个能力在排查事故和召回率问题的时候,比任何监控面板都管用。