ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

自研AI记忆系统:场景化语义单元实现多客户端共享与同步

自研AI记忆系统:场景化语义单元实现多客户端共享与同步 1. 先说结论mem0 在我的场景里哪儿不对味最近半年我一直在折腾一个多客户端 AI 工作流网页端一个对话助手命令行终端一个 Agent手机端还有一个随手记的入口后面又加了一个浏览器插件。这几个客户端后端接的模型还不完全一样——有的用 Claude有的用本地跑的 Qwen还有一些小任务直接丢给便宜的快模型。本来想着给它们统一配一个记忆层社区里呼声最高的就是 mem0我花了两天时间把它的 Python 库接进去又在自己的服务器上跑了它的 API 服务最后测试了一圈还是决定自己写。先说清楚我并不是说 mem0 不行。事实上 mem0 在给单一 AI 应用做长期记忆这个方向上是做得挺成熟的尤其是它把记忆的增删改查封装得比较干净还有一套自动抽取实体和关系的 pipeline。但我的需求不是一个 AI 回忆上次聊天而是多个 AI 客户端共享同一份记忆并且保持语义一致。这两个目标听起来像一回事实际拆开之后完全不是同一件事。mem0 本质上把记忆当成应用的附属模块它没有站在客户端矩阵的角度考虑问题。我需要的是一个独立的记忆服务所有客户端都往里面读写而且读写的数据要能被不同模型理解和使用。正是这几个差异让我决定放弃 mem0。下面的内容我会把当时的评估过程、具体卡点、以及最终自研的架构和代码细节都翻出来讲一遍希望能给和我有类似需求的人一个参考。2. 我对 mem0 的三层拆解它做了哪三件事哪一件卡住了我当时为了搞清楚为什么 mem0 用起来别扭我把它的工作方式拆成了三层来看。2.1 第一层记忆抽取与存储mem0 接收用户和 AI 的对话文本通过 LLM 自动抽取用户偏好事实信息历史决策等结构化片段然后把这些片段向量化存到向量数据库里。这一层做得确实好我把它接到一个单机对话应用里它能够自动把我的项目偏好、常用术语、之前聊过的技术选型都记下来后续对话能准确引用。但这一层有个隐含前提它默认所有记忆都归属于同一个用户、同一个应用上下文。它的 API 里虽然有 user_id 和 agent_id 之类的参数但这些 id 本质上只是给存储分了个桶并没有解决同一个语义事实在不同客户端如何被复用的问题。举个例子我在网页端告诉 AI我部署服务喜欢用 Docker Compose不用 K8s这条记忆会被抽取并存储。但我在命令行终端问 AI 关于部署方式时终端里的 Agent 如果没有主动去查询 mem0 关于用户部署偏好的记忆它依然是不知道的——因为 mem0 只是被动的记忆库查询时机和注入逻辑完全靠应用自己实现。2.2 第二层记忆检索与注入mem0 提供查询接口你可以在每次对话前根据当前消息去检索相关记忆然后把记忆拼进 system prompt。这个链路很清晰我也按它的方式实现了。问题出在检索的语义相关上。mem0 默认使用 embedding 做向量检索它返回的是语义相近的记忆片段而不是当前任务真正需要的记忆。实际操作中我发现跨客户端场景里真正要命的不是找得到相似的而是不要找到不相关的。比如我在手机端问了一句帮我看看服务器日志结果检索出来一大堆之前聊过服务器硬件选型的记忆这些记忆和看日志这个动作毫无关系只是字面上都包含服务器。mem0 没有上下文过滤机制它的检索分数只代表语义距离不代表任务意图。我需要自己加过滤逻辑这等于把记忆系统最核心的部分——什么时候用哪条记忆——完全甩回给我自己。那我还不如从底层就把记忆结构设计成按任务场景组织。2.3 第三层同步与跨客户端协调这是最卡的一层。mem0 的官方库和服务端都是围绕单一应用设计的。它有服务端部署模式多个客户端可以通过 REST API 连同一个服务端这解决了数据集中存储的问题。但一旦两个客户端同时写记忆或者某个客户端需要知道这条记忆是不是另一台设备上的 AI 刚刚写进来的mem0 就完全没有给答案。我当时做了一个简单的压力测试开两个进程同时往同一个 mem0 实例里写入不同的对话记忆然后观察结果。结果出现了两类问题一类是并发写入导致的覆盖因为我传给它的来源标识只有字符串它不会去做合并另一类是读到的记忆列表顺序不稳定导致客户端每次构造 prompt 时记忆排列都不一样进而影响模型的回答一致性。这种不可控感对一个要做成基础设施的东西来说是不能接受的。3. 我实际需要的记忆系统长什么样语义、场景与同步把 mem0 的卡点理清楚之后我花了一个晚上列需求清单最终确定了自研系统最核心的三个设计原则。3.1 记忆不是聊天记录而是可复用的语义单元大多数记忆系统直接把历史消息丢进去或者抽取成一句话存起来。这种粒度太粗。我把记忆拆成四种语义单元实体人、项目、服务器、工具、属性实体的属性如生产服务器 IP 是 10.0.0.5、关系两个实体之间的关系如项目 A 部署在服务器 B 上、场景规则当用户提到部署时优先使用 Docker Compose。聊天记录本身不存只存从记录里提炼出来的这些单元。好处的例子我在手机端让 AI 帮我给服务器 C 加一条防火墙规则AI 需要知道服务器 C 是 Ubuntu 系统防火墙是 ufw这条属性。这些属性可能是在网页端聊了很久之后才被确认的。如果只存聊天记录手机端 Agent 要翻很久才能找到但存成实体属性一次检索就能拿到。3.2 记忆必须带场景标签不能只有时间戳mem0 返回的记忆只有内容和分数我每次都要琢磨这条记忆在什么场景下该用。所以自研系统给每条记忆加了scene字段比如deploy、server_ops、coding、daily。每个客户端在发起对话时先声明当前场景记忆检索只查对应场景加上全局场景。这样就解决了我在 2.2 里遇到的服务器日志问题——用户部署偏好是deploy场景的不会在server_ops场景下被捞出来。场景标签不仅是过滤条件也是记忆写入的依据。我不会把网络上的各种琐碎都塞进去只有能被归类到已知场景里的信息才作为长期记忆写入其他内容就当成普通对话上下文用完即弃。3.3 跨客户端同步必须处理冲突合并我之前压测 mem0 时遇到的覆盖问题本质是缺少冲突处理。自己的系统里我规定每条记忆都有一个uuid和一个updated_at。写入时以uuid source updated_at作为唯一约束如果两个客户端同时写同一条实体的属性系统不会直接覆盖而是把两个值都保留并标记为待确认。后续任何客户端读取到多条候选值时会把它们都呈现给用户让用户选择哪个是对的一旦用户确认这条记忆的status变成confirmed其余候选值变为rejected。这个改造让记忆系统的行为变得可预期了。我用两个终端窗口模拟两个客户端同时写入同一个实体属性系统稳定地产生了两个候选值而不是静默覆盖。这种宁可多给用户一次选择也不要悄悄丢信息的原则我认为是记忆系统最值得认真设计的地方。4. 自研系统架构与关键实现从数据库表设计到检索代码这一节我直接贴核心设计包括数据库表结构、记忆写入流程、检索流程和 prompt 注入逻辑。我用的存储是 SQLite 一个轻量向量索引没有上专门的向量数据库理由是数据量还不到那个量级SQLite 完全扛得住。4.1 数据库表设计五张表覆盖全部记忆语义我建了五张表entities实体表、properties属性表、relations关系表、scenes场景定义表、operations操作日志表。关键字段如下CREATE TABLE entities ( id TEXT PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL, -- person / project / server / tool ... scene TEXT NOT NULL, status TEXT DEFAULT active, -- active / archived created_at TEXT, updated_at TEXT ); CREATE TABLE properties ( id TEXT PRIMARY KEY, entity_id TEXT NOT NULL REFERENCES entities(id), key TEXT NOT NULL, -- os, ip, deploy_method ... value TEXT NOT NULL, source TEXT, -- web / cli / mobile / plugin status TEXT DEFAULT confirmed, -- confirmed / candidate / rejected updated_at TEXT ); CREATE TABLE relations ( id TEXT PRIMARY KEY, from_entity TEXT NOT NULL, to_entity TEXT NOT NULL, relation_type TEXT NOT NULL, -- deploys_on / depends_on / manages ... scene TEXT NOT NULL, confirmed INTEGER DEFAULT 0, created_at TEXT );operations表专门记录每次读写操作的详情包括哪个客户端、改了哪个字段、旧值是什么、新值是什么。这个表最初只是为了调试后来发现它对新客户端接入特别有用——新客户端首次启动时可以拉取最近 100 条操作记录快速重建上下文相当于一种记忆回放。4.2 记忆写入抽取、归类、去重、写库每次对话结束时记忆服务会拿到完整的对话记录然后调用 LLM 做一次抽取。我用的 prompt 很简单但很有效请从以下对话中提取需要长期记忆的信息要求 1. 识别关键实体人名、项目名、服务器名、工具名 2. 提取实体的属性和实体之间的关系 3. 判断每条信息属于哪个场景deploy/server_ops/coding/daily 4. 如果信息是临时性的如今天我看了某个新闻忽略 5. 输出 JSON 数组格式为: [{type:property,entity:prod-server,key:os,value:ubuntu 22.04,scene:server_ops}]拿到 JSON 后先按entity key查库如果已经存在相同值就跳过如果存在不同值把新值插成candidate状态。这个去重逻辑避免了每条对话都产生大量重复记忆也保证了后续合并冲突时有据可依。4.3 记忆检索场景过滤 向量召回 关键词精排检索时我先根据当前场景从 SQLite 里捞出所有该场景的记忆单元然后对它们做 embedding。这里有一个取舍如果场景很大全量 embedding 会有点慢所以我加了一层关键词预筛先用当前消息里的命名实体去匹配entities表找到关联的子图再对子图里的属性做向量排序。实际就三步实体匹配、子图展开、向量排序。我用的是 sentence-transformers 里的all-MiniLM-L6-v2模型在本地跑延迟大概几十毫秒完全可以接受。关键代码如下def retrieve_memories(query, scene, top_k10): # 1. 实体匹配 matched_entities find_entities_in_text(query) # 2. 子图展开查询这些实体的属性、关系、关联实体 memory_candidates collect_subgraph(matched_entities, scene) # 3. 向量排序 query_vec embed(query) scored [(m, cosine(query_vec, embed(m.full_text()))) for m in memory_candidates] scored.sort(keylambda x: x[1], reverseTrue) return [m for m, s in scored[:top_k]]这里的collect_subgraph是核心它会把一个实体相关的属性、与它有关系的其他实体、以及那些实体的关键属性都捞出来。比如查询提到prod-server它会返回 prod-server 的 os、ip、部署方式以及它关联的域名、项目等实体。这种图式召回比单纯向量检索更适合跨客户端场景原因是一个客户端可能只知道实体名另一个客户端已经记住了它的很多属性图检索能把属性串起来。4.4 Prompt 注入模板让不同模型都能正确使用记忆记忆检索出来之后要变成模型能直接用的上下文。我定义了一个统一的记忆区块模板无论后端是什么模型都采用同样的格式以下是你知道的长期记忆请基于这些记忆回答用户问题。记忆按场景组织 [场景: server_ops] - prod-server 的操作系统为 ubuntu 22.04来源web - prod-server 的防火墙为 ufw来源web - user 倾向于使用 Docker Compose 管理服务来源cli [场景: deploy] - project-api 部署在 prod-server 上使用 Docker Compose模板里我故意标了来源这样模型如果发现来源冲突可以根据上下文做合理判断。实测下来Claude 和 Qwen 对这个格式的理解都不错都能准确引用记忆。这个模板比我之前用的相关记忆如下要有效得多因为场景标注帮助模型知道什么时候该用哪条记忆。5. 从零到一踩过的坑并发、嵌入模型和客户端接入下面是开发过程中最真实的几个教训每个都花了不少时间。5.1 SQLite 并发写的坑WAL 模式必须开第一版我用的是默认的 SQLite 配置结果多客户端并发写时频繁报database is locked。后来发现是自己的原因没有开 WAL 模式。SQLite 在 WAL 模式下允许读写并发虽然同时只有一个写事务但不会再锁死整个库。我建连时统一执行PRAGMA journal_modeWAL; PRAGMA busy_timeout5000;busy_timeout 也很重要它让写操作在遇到锁时等待而不是直接报错。开了这两个之后并发写的问题基本消失。不过要注意WAL 模式在 NFS 这类网络文件系统上不可靠所以我把服务端部署在本地 SSD 上没有用网络盘。5.2 嵌入模型选择的实测对比通用模型不如领域小模型一开始我图省事直接用 OpenAI 的 embedding API后来发现两个问题一是每次检索都要等网络延迟不稳定二是费用虽然不高但团队其他人接入时还得配 key。后来换成本地模型选了all-MiniLM-L6-v2作为主力但这个模型对中文实体名的匹配效果一般尤其是技术名词缩写。我测试了三个模型all-MiniLM-L6-v2、bge-small-zh-v1.5、m3e-small。在 200 条中文技术记忆的测试集上bge-small-zh-v1.5对服务器部署防火墙这类词的召回明显更好但英文缩写如APICLI上又略弱。最后我的方案是同时跑两个小模型中文用 bge英文用 MiniLM检索时把两个向量拼起来做混合相似度。虽然计算量多一点但在 10 万条记忆以内这个成本可以忽略。5.3 客户端接入协议HTTP 太繁琐改成轮询本地文件我一开始给各客户端提供 REST API 来读写记忆但接入时发现太繁琐每个客户端都要维护认证、重试、序列化。后来我改了一个极简方案在每台设备上跑一个轻量 agent 进程它维护与中心服务的同步客户端只需要读写本地一个 JSON 文件即可。这个 JSON 文件就是当前场景的记忆快照客户端启动时读取会话结束前写回。agent 进程负责把快照同步到中心服务并拉取其他设备上的变更。效果立竿见影命令行终端和浏览器插件接入时间从半天缩短到半小时。这也符合一个原则——客户端越简单接入越没有心理负担。中心服务再怎么复杂都行但暴露给客户端的接口一定要薄。6. 自研系统弥补了 mem0 的哪些短板差异化能力清单把系统跑起来之后我重新做了一次对比测试发现自研版本在自己的场景里确实比 mem0 好用主要体现在四个方面。6.1 跨客户端记忆一致性我在手机端记录了一条生产环境禁止直接改动数据库然后在网页端问 AI 关于数据库操作的建议它立刻引用了这条规则并且用上了禁止这个触点。这个效果在 mem0 里不是做不到而是需要我再写很多胶水代码。我的系统从数据结构上就把跨客户端的一致性内建了。6.2 场景隔离减少幻觉和误用mem0 类型检索倾向于把一个领域的知识错误地用到另一个领域。我的场景标签加上图式召回让服务器硬件选型的记忆在服务器运维场景下的权重降到了很低的水平。实测在 50 个跨场景问题上自研版的误用率只有 3 个而我用 mem0 做同样测试时是 11 个。6.3 人工确认机制让记忆可信属性被多个来源写入时会进入candidate状态等待用户确认。这个机制虽然看起来多了一步但它显著提高了记忆的整体可信度。我可以在 UI 上把待确认项列出来用户点一下确认就转正。长期使用下来记忆库里的垃圾信息少很多不会出现用户说 A又说 A 不对这种事。6.4 完全本地化和可审计我的系统核心是 SQLite 本地模型中心服务是可以离线的。所有记忆数据都留在自己的服务器上没有第三方 API 参与。同时operations表记录了每一次变更我可以回答这条记忆是什么时候从哪儿进来的。对内部工具来说这种可审计性非常重要。7. 如果你也想自研最小可运行版本怎么搭最后给想试试自研记忆系统的人一条明确路径。不要一上来就搞微服务、向量数据库、分布式同步先搭一个能跑的闭环一步步加功能。我这个版本从代码量上说其实很小核心服务大概 800 行 Python。7.1 第一步定义 JSON 记忆文件格式先在本地目录建一个memory.json格式和数据库表一一对应{ entities: [ {id: prod-server, name: prod-server, type: server, scene: server_ops} ], properties: [ {entity_id: prod-server, key: os, value: ubuntu 22.04, source: web, status: confirmed} ] }客户端读写这个文件跑通后再考虑换成 SQLite。我一开始用 JSON 文件跑了两个星期支撑了三个客户端完全没问题。这个阶段的核心目标是验证记忆的抽取和注入是否有用而不是追求存储性能。7.2 第二步实现 LLM 抽取和检索脚本写两个脚本extract.py和retrieve.py。前者输入对话输出 JSON 记忆片段后者输入查询和场景输出记忆列表。这是整个系统最核心的人工智能部分有这两个脚本整个记忆系统就算成立了。其他什么同步、冲突、UI 都可以后置。7.3 第三步加同步和冲突标记当多个客户端开始频繁读写时再考虑加服务端、加uuid、加候选状态。到这一步你已经能自然理解为什么我要设计那些表结构了——不是凭空设计而是踩了坑之后反过来补的。提示如果你的项目只需要给一个 AI 应用加记忆没有多客户端共享需求直接用 mem0 是更省事的选择。不要为了炫技而自研自研的前提是你对记忆的语义和同步有了自己的明确理解。最后分享一个个人体会这次自研最大的收益不是省掉了某个依赖而是我彻底搞懂了 AI 记忆系统应该怎么设计——记忆不是数据库里的静态记录而是不断被校验、被场景化、被多客户端共同维护的动态知识。手里有一份自己能完全掌控的记忆协议后面接任何新模型、新客户端都轻松很多。
返回列表