在开发多客户端 AI 智能体的过程中,我发现记忆这个环节太容易变成“说起来重要、做起来次要、用完就忘”的附庸品。先后在几个项目里试过 mem0,也看它在社区里被反复推荐,但最终我还是决定自己写一套跨客户端记忆共享系统。这不是为了凑轮子,而是因为我的使用场景和 mem0 的设计假设存在根本性冲突。这篇文章就把我踩过的坑、做过的取舍、以及最终落地的方案完整记录下来,希望能给同样被“AI 没有长期记忆”折磨的开发者一点参考。
我没有把记忆做成某个大模型的外部插件,也没有让所有对话都走一个云端大脑,而是把它设计成一个独立的“记忆总线”,让不同客户端负责采集,统一服务负责沉淀和检索,各端读到的记忆像同一本日记在不同人手里翻看一样,内容一致但视角不同。这套系统的核心思路,是用事件流替代快照式存储,用统一记忆 API 替代客户端各自维护缓存,再用向量检索加摘要压缩来平衡精确度和成本。整个方案跑了大半年,稳定接入过 Web 端、命令行工具、Telegram 机器人,还有一个小型的桌面助手,下面把每层的设计和代码思路都拆开讲。
1. 先说说为什么最终放弃 mem0
mem0 的定位很讨喜:开箱即用,支持从对话里抽取记忆,也支持向量检索,很多教程里都说“接入两个函数就能让 AI 记住你是谁”。我一开始也是被这种低门槛吸引过去的,但真正把它放到多客户端环境里跑起来,问题比想象中多。
1.1 mem0 的核心假设与我的场景错在哪里
mem0 的典型工作方式是:每次对话结束后,把当前这条对话、相关用户 ID、以及可选的元数据丢给它,它内部做主提取、去重、更新,然后存进向量库和数据库。这个设计听起来很合理,但它默认了一个前提:记忆的写入是“以会话为中心”,而且所有会话最好都能被统一收集进同一个后端。
我的项目偏偏不是这种形态。用户可能早上在网页端跟智能体讨论了项目需求,下午用命令行工具查资料,晚上又让桌面助手帮忙安排日程。这三个入口背后的 AI 逻辑不同,模型供应商也不同,甚至有时候调用的是本地小模型。如果每个客户端都各自接入 mem0,那它们只会形成三个互不相通的记忆孤岛;如果我想让它们共享同一个 mem0 实例,就必须把三个客户端的对话数据全部汇集到同一个 API Key 下,这又等于让自己的客户端变成纯数据搬运工,中间还要处理用户身份映射、敏感信息过滤、离线状态缓存等一系列问题。
还有一个更实际的痛点:mem0 偏向“谁调用谁管理”,它自己不太关心客户端之间的写入冲突。比如用户先在 Web 端说“我的项目代号是北极星”,又在命令行里说“项目代号改成猎户座”,这时候两条记忆其实应该是同一条实体的更新,但 mem0 只按内容相似度去判断,很可能把两条信息当成不同记录存下来,最后检索的时候同时出现两个“项目代号”,让 AI 回答自相矛盾。
1.2 数据归属、隐私边界和运维这三笔账
多客户端环境下,数据的归属和隐私边界非常棘手。mem0 在单客户端场景下只要区分 User ID 就够了,但我的系统里,一条记忆至少要有三个维度的归属:属于哪个用户、来源于哪个客户端、允许被哪些客户端读取。举个例子,用户在手机聊天里透露的个人习惯,可以同步给桌面助手用;但用户在命令行里粘贴的内部接口文档片段,就不应该被网页端随意检索出来。mem0 没有这种基于源的权限过滤,我自己不得不在外面包一层服务来改写和分流数据,绕了一圈等于还是自己实现了半个系统。
运维层面也不轻松。自托管 mem0 意味着要维护向量数据库、后端服务、以及它依赖的一些基础组件。我的项目本来就是个人开发为主,不可能再搭一套完整的向量库集群。当时的项目阶段,我更需要的是一种轻量又能演进的设计:先跑起来,数据量和复杂度上来以后再平滑替换成更强的存储和索引。mem0 虽然也支持多种存储后端,但它的接口抽象偏向“完整版方案”,想砍掉一部分功能组件,反而要读不少源码来确认哪些是硬依赖。
当然,mem0 本身是个好项目,很多设计也值得学习,只是它的“默认路径”和“多客户端记忆共享”这个目标之间,隔着一层很厚的胶水。如果你只需要把记忆交给某个单一大模型应用,mem0 是省事的;但如果你要把记忆当作独立基础设施来设计,那自己做核心层反而更可控。
2. 跨客户端记忆共享系统的整体设计思路
决定自己写之后,我没有急着写代码,先花了不少时间把需求拆清楚。记忆系统最忌讳的就是“先存起来再说”,因为后面你会发现,存下来的数据有一大半在真正需要时根本检索不出来。这套系统从架构上就明确了三层结构,分别管好“采集什么”、“怎么存”和“怎么读”。
2.1 用统一记忆事件流替代分散的会话摘要
我首先定义一个名为 MemoryEvent 的核心事件结构,所有客户端都按这个结构上报记忆,服务的核心职责是接收、校验、持久化这些事件,而不是主动去各个客户端“抓取”对话记录。
这个事件结构看起来很简单,但它解决了一个大问题:把“发生了什么事”和“这件事值不值得记”分开。各客户端只需要负责“如实上报发生了什么”,至于要不要把这条信息提升为长期记忆,由服务端决定。比如用户说“今天天气不错”,这通常不值得记;但用户说“我不喜欢喝咖啡因饮料”,这就值得存。如果每个客户端各自做判断,它们可能用不同的阈值和规则,结果就是同样一句话,有的客户端存了,有的没存,跨端检索的时候数据对不上。
@dataclass class MemoryEvent: user_id: str client_id: str event_type: str # profile, preference, fact, task, context content: str importance: float = 0.5 # 由客户端初步打分,服务端可覆盖 timestamp: float = 0.0 metadata: dict = field(default_factory=dict)事件流的好处在于可重放。就算服务端的记忆提取逻辑升级了,也不需要把历史数据全部重新整理一遍,只要对已持久化的事件做一次重放处理就行了。短期的实现里可以直接把整条 content 和它的 embedding 存下来,高价值事件再做一次摘要压缩,形成“原始事件 + 向量索引 + 摘要缓存”三层存储。这套分层看起来多占了一点存储,但实际查询时非常有底气:先检索原始内容保证召回率,再用摘要给大模型省 token。
2.2 为什么选了“记忆总线 + API”而不是让客户端直连数据库
一开始我考虑过最简单粗暴的方案:每个客户端直接连接同一个 SQLite 文件或者同一张 Postgres 表,谁都能读,谁都能写。这个方案对个人项目有诱惑力,但它经不起两个推敲:第一,客户端一多,表结构和字段演进会变成噩梦,你不可能为了让一个命令行工具读记忆就去改全表结构;第二,记忆的读取往往需要组合条件,比如“最近 7 天”、“只要偏好类”、“来自网页端但不来自手机端”,这些逻辑散落在每个客户端里,重复实现会迅速失控。
所以我把服务端做成一个独立的记忆 API,客户端只做两件事:上报事件、发起查询。中间所有的过滤、合并、摘要、权限判断都在 API 层完成。这个设计带来的直接收益是:新客户端接入只需面对一个 HTTP 接口,而不是一套数据表结构;查询参数可以统一规范化,避免每个客户端写得不一样。
在实际实现时,我用 FastAPI 包了一层轻量服务,核心只有五个接口:上报事件、查询记忆、按时间聚合、删除/遗忘、状态同步。数据存储则先用 SQLite 打底,后续再平滑切换到 PostgreSQL。SQLite 在单机场景下足够稳,又不引入额外运维,是我的过渡首选。
from fastapi import FastAPI, Depends from pydantic import BaseModel app = FastAPI() class EventIn(BaseModel): user_id: str client_id: str event_type: str content: str importance: float = 0.5 metadata: dict = {} @app.post("/v1/events") def report_event(event: EventIn, session=Depends(get_db_session)): event_id = save_event(session, event) return {"status": "ok", "event_id": event_id}2.3 记忆的“三个读取层次”:原始事实、偏好画像、任务状态
查询接口没有滥用向量搜索。很多开发者一提到记忆检索就先上 embedding 相似度,但实际项目里,记忆的读取需求至少分成三类,每类的检索策略完全不同。
第一类是“原始事实查询”,比如“我上次设置的备份路径是什么”“这个项目的上线日期是哪天”,这类问题适合用结构化字段或者关键词匹配,向量检索反而会因为语义扩散召回不相关的数据。第二类是“偏好画像查询”,比如“这个用户喜欢简洁回答还是详细解释”,这类信息更适合做汇总,把一个用户所有偏好类事件合并成一份动态画像。第三类是“任务状态查询”,比如“代码部署到哪一步了”,这类记忆的关键是时间线和状态机,每一条记忆其实是一个状态变更事件,读取时更需要按时间和顺序重放,而不是只找最相似的那一句。
我按这三个层次设计了不同的查询模式,并在 API 层做了统一路由。用户发起查询时,可以指定 memory_type 为 fact、profile 或 task,系统会自动选择匹配的检索链路。这样既避免所有查询都堆到向量库里,也方便后续对慢查询做针对优化。
3. 核心模块的实现与实操细节
整体架构定下来后,最琐碎的工作就开始了。这一部分我把每个关键模块的实现思路和代码要点都摆出来,重点讲那些“文档里不会写、但实现时一定会遇到”的细节。
3.1 记忆提取:不能只靠大模型,要有一套兜底规则
记忆提取是这个系统的灵魂。如果提取不准,后面存多少都白搭。我的方案是“规则预筛 + 大模型摘要 + 人工可干预”三层漏斗。
规则预筛的目的是挡掉明显不值得记的内容。比如纯寒暄、系统错误提示、重复的琐碎状态,这些内容就算硬交给大模型提取,也只是白白消耗 token,还容易把噪音存进来。我维护了一个轻量规则引擎,按事件类型判断:包含时间、地点、名称、偏好、任务状态等关键词的内容,直接进入下一层;否则标记为低置信度,只保留短期缓存,不提升为长期记忆。例如“早上 9 点开会”这类内容一定会进长期记忆,而“好的”“嗯嗯”这样的回复基本会被直接过滤掉。
大模型摘要层只在规则判定“这可能值得记,但不充分”的时候触发,属于性价比所在。这一步我通常用小模型就能跑,比如 Qwen 系列或者本地部署的小参数模型,因为任务很简单:把用户消息压缩成一条结构化的记忆描述,并且不去做发散性推理。在提示词设计上,我把任务定得非常明确:“根据用户说的话,提取用户偏好、指定的事实信息、当前任务状态三种类型中的一种,输出为 JSON,不要推测,不要补全信息。”这样能显著减少模型自由发挥带来的数据污染。
人工可干预层也很重要,尽管绝大多数时候不会触发。我在管理后台留了一个简单的记忆审核列表,有任何一条记忆被用户或客户端标记为“不准确”的时候,它会进入待修正队列,由我或者用户手动确认后删除或改写。记忆系统一旦面向真实用户,就一定会遇到“AI 记错了”的投诉,没有纠正通道的纯自动系统是要出事的。
3.2 存储层:SQLite 起步,但为 PostgreSQL 预留了接口
存储层我用 SQLite 做骨架,但在 SQLAlchemy 层上严格走 ORM,不写任何 SQLite 专属 SQL。这样的话,以后把数据库连接串改成 PostgreSQL 的 URL,代码基本不用动。表结构上,我设计了三条核心表:memory_events 存原始事件,memory_items 存提升后的长期记忆,memory_pins 存用户显式置顶的条目。分开存的原因很简单:原始事件和长期记忆的生命周期不同,原始事件可能只需要保留 30 天,而长期记忆,比如用户的职业身份、家庭情况,通常需要长期保留。
memory_items 表是主查询对象,字段包括 user_id、memory_type、content、embedding、source_clients、status、importance、created_at。其中 source_clients 用 JSON 数组存储,记录这条记忆来源于哪个客户端,以及允许哪些客户端读取。这个字段在 mem0 里是没有的,但它却是跨客户端记忆共享的基石,没有它,“跨端不串门”的安全边界就无从谈起。
class MemoryItem(Base): __tablename__ = "memory_items" id = Column(String, primary_key=True) user_id = Column(String, index=True) memory_type = Column(String, index=True) # fact / profile / task content = Column(Text) embedding = Column(JSON, nullable=True) source_clients = Column(JSON, default=list) status = Column(String, default="active") importance = Column(Float, default=0.5) created_at = Column(DateTime, default=datetime.utcnow)关于 embedding 的存储,我一开始没上专门的向量数据库,直接存在 JSON 字段里。数据量在万条以内时,内存里跑余弦相似度完全没问题,Python 的一次耗时在几十毫秒级别。真的到了万条以上,再把 embedding 字段迁移到专门的向量数据库也不迟,ORM 层改一个查询函数就行。这种“临时方案但留好接口”的策略,非常适合个人项目。
3.3 记忆检索与合并:同一条记忆如何保持“唯一且更新”
记忆检索阶段最让我头疼的不是向量召回,而是合并。用户经常会用不同方式描述同一件事,比如“我住在杭州”“我目前在杭州工作”“家在浙江杭州”,这三条信息从文本看差异很大,但本质是同一事实。如果系统不做合并,AI 回复时就会看到三份近似的记忆,轻则浪费 token,重则给出矛盾的答案。
我的合并策略分两步。第一步是实体归一化,把用户、地点、项目名、时间等实体统一编码,比如“杭州”“浙江杭州”“Hangzhou”都映射到同一个实体 ID。第二步是基于语义相似度和时间近邻的合并:当两条记忆的 embedding 相似度高于 0.92,并且属于同一用户、同一实体时,系统保留最近更新的那条作为 active,旧的那条标记为 archived,但不会物理删除,方便追溯。
这一个合并过程在每次写入新记忆时异步触发。我会把新来的记忆先放进 pending 队列,服务定时批量处理,避免单次请求响应被慢合并拖住。合并结束后,再对这条记忆的心智摘要做一次刷新,比如用户画像类型记忆会重新用大模型汇总出最新版。这里的核心经验是:合并一定要异步,绝不能在写接口的同步链路里做,否则每一次用户上报记忆都可能卡几百毫秒,体验非常差。
3.4 客户端接入协议:从 SDK 到 Webhook 的几种形态
客户端接入协议我前后迭代了三版。第一版是纯 SDK 方式,代码里直接 import 客户端库,调用记忆服务的 API。这种方式对于我自己维护的 Python 实用工具最方便,但它要求每个接入方都需要走一套依赖安装流程,对于一些轻量场景还是显得笨重。
第二版加入了 Webhook 形态:客户端只管往一个回调地址推事件,由记忆服务负责解析。这个思路很适合同一个客户端有多个前端入口的情况,只要前端事件统一走后端,再让后端转发到记忆 API 即可。后来我又增加了一个命令行辅助工具 mctl,它可以读取 stdin 中的对话内容,并按照最简单的-t profile -c "用户偏好简洁"这种参数格式上报事件。工具本身只有二百多行,但让“随手记录一条记忆”变成了一种零成本操作。
每个客户端在首次接入时,都会分配到一个 client_id,而这个 id 同时决定了默认的权限边界。比如 client_id 为 web 的客户端,默认只能访问 source_clients 中包含 web 的记录,想要读取 mobile 端的记录,需要在 API 请求中显式声明 cross_client=true 并且通过授权校验。这里我踩过一个坑:最初所有客户端共享一个密钥,结果手机端和网页端在测试环境经常互相读到对方的测试记录。后来严格按客户端粒度签发密钥,同一用户不同端才被隔离。
3.5 权限与隐私:记忆共享并非“全都让所有端看到”
权限与隐私设计我有意识做重了,因为记忆数据比普通日志更敏感。用户可能愿意让 AI 记住自己的生日、习惯、工作项目细节,但未必愿意让第三个 App 也读走这些信息。所以在记忆 API 层,每次查询都要经过三层检查。
第一层是身份校验,确认请求来自已注册的用户;第二层是客户端权限校验,确认该客户端有权访问目标记忆的来源端;第三层是内容级校验,一些 metadata 中被标记为 private 的记忆,任何客户端都无法通过常规查询接口读取,只有用户手动显式授权时才会临时放开。这样三层下来,代码量看似多了不少,但换来的安全边界非常清晰。尤其以后如果要把这个系统开放给第三方客户端,这种权限结构可以直接复用,不需要重新设计。
4. 实操中的代码骨架与关键配置
前面讲了不少设计,这一节我给出一份可以直接照着改的最小可运行版本。仓库结构只列必要文件,运行依赖也只有 FastAPI、SQLAlchemy、sentence-transformers、numpy 这几个,尽量降低上手成本。
4.1 最小化的项目结构与核心代码
我建议的最小化结构是这样的:
memory_system/ ├── app.py # FastAPI 入口,路由注册 ├── models.py # ORM 模型 ├── memory_service.py # 核心逻辑:提取、合并、检索 ├── vector_store.py # 轻量向量相似度工具 ├── requirements.txt └── mctl.py # 命令行上报工具核心的写入函数很简单,逻辑都在 memory_service 里。它接收 MemoryEvent,先做规则预筛,再判断是否需要做大模型摘要,最后写入 memory_items 和触发异步合并。
def process_event(event: MemoryEvent): if not prefilter(event.content): save_short_term(event) return item_id = upsert_memory_item(event) if event.importance >= 0.7: background_summarize(item_id) if event.event_type in ("profile", "task"): trigger_merge(item_id) return item_id这里有几个容易忽略的细节:upsert_memory_item不是简单的 insert,它要先去查一下是否存在同实体相似度达标的 active 记录,有就更新,没有才插入。background_summarize使用线程池处理,防止阻塞主线程。trigger_merge也只放一个合并请求到队列,而不是立即执行。
4.2 向量检索的最小实现:先跑起来再考虑扩展
向量检索我用了最直白的 numpy 余弦相似度。初始化的时候把当前用户的所有 memory_items 预加载成矩阵,查询时只计算当前用户的子集。
def search_similar(embedding, user_records, top_k=5): matrix = np.array([r.embedding for r in user_records]) if len(matrix) == 0: return [] emb = np.array(embedding) scores = cosine_similarity(matrix, emb) top_indices = scores.argsort()[-top_k:][::-1] return [(user_records[i], scores[i]) for i in top_indices if scores[i] >= threshold]之所以按用户先做裁剪,是考虑到记忆数据天然按用户隔离,不做全局向量搜索可以显著降低误召回。实测在 5000 条用户记忆内,这个纯 numpy 实现耗时不超过 20ms,完全够用。等数据规模上来以后,再把这里的user_records换成专门向量库的接口,改动非常小。
4.3 同步与冲突处理:两个客户端同时写的时候怎么办
多客户端并发写入是绕不开的问题。我的方案是乐观锁加上事件 ID 去重。每条 MemoryEvent 在客户端生成时就带上唯一 ID,即使网络重发也不会重复入库。在更新同一条记忆时,用 updated_at 字段做版本判定,如果服务端已有记录的新鲜度更高,则拒绝客户端覆盖,而是合并两个来源标记。
def upsert_memory_item(event): event_id = event.metadata.get("event_id") if event_id and get_event(event_id): return None existing = find_similar_active(event.user_id, event.content) if existing and existing.updated_at > event.timestamp: existing.source_clients = list(set( existing.source_clients + [event.client_id] )) mark_dirty(existing.id) return existing.id return insert_new(event)这套逻辑在真实运行中帮我挡掉了很多次重复写入。比较典型的情况是:用户在某端开着自动记忆上报,又在另一个端手动用 mctl 上报了同一句话,如果没有事件 ID 去重,数据库马上就会出现两条一模一样的记忆。
5. 部署运行与配套工具落地
很多项目死在“代码写完了但跑不起来”这个环节,主要是部署路径和日常维护工具没跟上。这个系统我在部署时也做了不少调整,这里把运行环境和数据备份决策一并记录。
5.1 部署形态与模型调用方式
整套系统我跑在一台普通云主机上,配置是 2 核 4G,部署方式极其简单:FastAPI 挂到 gunicorn + uvicorn worker,SQLite 放在本地磁盘,向量模型用 sentence-transformers 的轻量版本。如果后续用户量增长,我会把 SQLite 换成 PostgreSQL,再引入 Redis 做缓存,但现阶段没必要为不存在的并发浪费精力。
大模型摘要层的调用,我没有单独部署一套模型服务,而是直接调外部模型的 HTTP API。数据安全方面,由于都是用户自己的记忆数据,且我明确在服务条款里告知用户数据会被用来优化记忆功能,这才放心走远模型。这一点要提醒一下:如果你的系统涉及敏感业务数据,最好把摘要模型换成内网部署,避免记忆内容经过第三方服务。
5.2 命令行工具 mctl:让随手记成为肌肉记忆
mctl 是本项目里最“不起眼但高频使用”的组件。它不需要任何配置文件,直接用命令参数提交一条记忆事件。
mctl set --type profile --content "用户偏好简洁的技术回答" --client cli mctl set --type task --content "已完成登录模块重构,下一步是记忆系统的权限测试" mctl query --type task --limit 5这个工具看着简单,但在实际工作里帮了很大忙。以前想给 AI 记点东西,要么打开网页端对话框,要么改代码加日志,现在直接在终端里敲一行就好。它还支持从 stdin 读取内容,方便配合其他脚本做流水线处理。命令行工具的维护成本不高,但交互效率非常高,非常适合开发者自己日常使用。
5.3 数据备份与恢复策略
记忆数据对这套系统来说就是核心资产,数据丢失比服务宕机更可怕。所以我每天做一次 SQLite 文件快照,另外每七天做一次全量导出,导出格式是 JSON Lines,方便日后迁移到其他平台。恢复流程也通过实际操作验证过:先停服务,再替换数据库文件,最后重启服务,整个过程十分钟以内可以完成。这个简单流程值得每一位做自托管 AI 项目的人提前演练一遍,不要等数据丢了才手忙脚乱。
6. 常见问题与排查技巧实录
任何系统跑起来之后,真正花时间的不是写新功能,而是排查各种边角问题。这一节我挑出六个最有代表性的问题,每个都附上实际解决过程。
6.1 记忆重复写入,检索结果出现“双胞胎”
最开始的版本经常出现同一条记忆被保存两次,用户汇报、定时任务抓取、手动补充都可能出重复数据。排查时我先查了数据库日志,发现两条记录只差几十毫秒,基本可以断定是并发场景下的去重缺失。后来在 upsert 流程中引入 event_id 幂等键,并且在 memory_items 表上建立了一个复合唯一索引user_id + event_id,问题立刻消失。所有会接收外部事件的系统,都应该在设计的第一天就考虑幂等性,不要等数据脏了再回头补。
6.2 向量检索召回内容不准确
有段时间用户反馈 AI 总把“用户喜欢详细讲解”和“用户喜欢示例代码”搞混。检查后发现是 embedding 模型的粒度不够,它把两种偏好的语义向量算得太接近了。我没有立刻更换 embedding 模型,而是先给查询入口加了 key_filter 参数,要求查询 profile 类记忆时只召回 memory_type 为 profile 的记录。分类过滤比向量相似度可靠得多,这一步整改之后召回准确率上升了不少。向量检索是“召回手段”,不是“精准筛选”,能用结构化条件过滤的就应该先过滤。
6.3 跨客户端数据串味
这个问题在授权机制上线前出现过一次。当时两个客户端共用默认密钥,导致网页端查到了命令行端上报的记忆。修复方法是切换成每个客户端独立的密钥,并在查询接口强制校验 source_clients 权限。现在跨端访问必须显式声明 cross_client=true,并且服务端会记录每一次跨端访问审计日志。多客户端系统的安全设计不能只靠应用层自觉,必须在 API 层面强制兜底。
6.4 大模型摘要太自由,凭空编造记忆
有一段时间大模型把“用户周末可能会去爬山”这种推测直接存成了事实,后续 AI 还会主动确认“我记得您周末要去爬山”。大模型的自由发挥就是记忆系统最危险的地方。我把提示词改成了“只提取用户明确表达的偏好和事实,禁止推测和补全”,并且把输出格式严格限制为 JSON,其中增加了一个confidence字段。低于 0.7 置信度的内容默认进入草稿区,不会直接生效。这个方法效果很明显,编造率大幅下降。在记忆系统里,宁可漏记,不可乱记。
6.5 记忆过期,旧状态影响当前决策
任务类记忆天然有时效性。比如用户半年前说“正在用 Python 写爬虫”,现在可能已经切换成 Go 做后端。如果 AI 一直引用旧记忆,用户体验会非常差。我引入了时间衰减策略:任务类记忆超过 30 天没有被刷新,在检索里的权重自动下降 20%,超过 90 天降为 50%,超过 180 天则需要在查询时显式带上 include_old=true 才能召回。偏好类记忆则不衰减,因为它通常更稳定。时效策略必须按记忆类型区分,一刀切衰减会把“长期偏好”也误伤。
6.6 服务重启后内存索引丢失,查询变慢
早期版本把向量索引全放内存,服务一重启第一次查询就慢得离谱。排查之后改成“启动时预热 + 后台定时重建”的策略:服务启动 5 秒内先加载一批高频用户的向量,其余用户在第一次访问时触发懒加载。另外,每次写入新记忆时都会异步更新该用户的内存缓存,保证查询性能不衰退。这个优化做完后,重启服务的体验好了很多,第一轮查询不再卡顿。
7. 从单机到多端协作的下一步演进
这套系统目前稳定服务于我的个人智能体矩阵,但距离“通用记忆基础设施”还有不少可以演进的方向。多客户端 AI 协作越来越普遍,记忆系统需要在更复杂的生态里承担更多职责。这里分享三个我认为最有价值的扩展方向,不涉及具体落地的代码,只给思路。
第一个方向是支持“记忆订阅与推送”。现在的检索模式是被动查询,智能体需要自己去问“我记住了什么”。更自然的模式是服务端主动推送:当某个实体的记忆发生重大变化时,订阅了这个实体标签的客户端能收到通知。比如用户更新了工作单位,所有引用这个信息的智能体都能同步刷新,而不是等下一次对话时才发现信息已过时。这对智能体决策的实时性会有明显提升。
第二个方向是跨用户知识共享但保持隐私隔离。现在系统以单用户为边界,但很多场景需要“团队记忆”。例如一个小团队一起做项目,管理员希望团队成员都能读取项目相关的非敏感信息。权限模型要增加一个 group 维度,而当前系统的 user_id 隔离模型暂时还不支持这种共享。这是下一步很值得做的能力,因为多 AI 协作越来越常见,团队级记忆也会成为刚需。
第三个方向是记忆可信度评分。目前只有 importance 和 status 两个维度,但实际运行时,一条记忆是否可靠,还取决于来源客户端的可靠性、用户是否有过纠错记录、信息与已知事实是否冲突。引入可信度评分后,AI 在引用记忆时可以自动加一句“这条信息来自某某来源,历史准确率约 90%”,避免一本正经地错。在真实产品中,这种透明度能显著提升用户信任度。
回到最初的问题:为什么不用 mem0,而是自己写?现在答案很明确。当我需要的只是一个单大模型应用的记忆插件时,mem0 完全够用;但当我需要一套服务多客户端、跨权限、可演进、能安心长期依赖的记忆系统时,它缺的不只是某个功能点,而是整套架构取向。自己做这套系统,前期确实多花了不少时间,但它让我的智能体矩阵真正共享了一套统一记忆,也让后续接入任何新客户端都变成几行代码的事。如果让我再选一次,我依然会这样做,而且如果你也有类似的多客户端协作需求,我的建议是先从一个小而精的记忆 API 开始,不要一上来就被复杂的厂商方案绑住手脚。