MemTether:开源AI记忆层,让所有AI客户端共享同一份长期记忆
1. 一切始于“AI客户端各说各话”的痛点1.1 我在五个AI客户端之间来回横跳过去一年我对AI工具的使用状态基本可以用“精神分裂”来形容白天在ChatGPT里查技术方案中午切到Claude让它帮忙写代码晚上又打开Kimi整理资料偶尔还开着本地跑的模型做一些私有数据的聊天。按理说工具越多越自由但实际用起来最大的感受是——它们谁都记不住我。同一个项目背景我在每个客户端里都要重新解释一遍。昨天在A客户端里确认过的技术选型今天到B客户端问的时候它一脸茫然。更崩溃的是有一次开周会前我想找一份之前在某个客户端里整理过的会议纪要我隐约记得内容但翻遍所有客户端的聊天记录都没找到最终只能凭记忆重写。那一刻我在想人类用AI协作这么久了为什么“记忆”这件事反而变得越来越碎片化我试过很多所谓的解决方案比如每次切换客户端的时候手动把上下文粘过去或者维护一份自己的笔记文档当作“记忆”用完再手动整理。这些法子要么太累要么根本不持久。真正的问题不在聊天记录本身而在于每个AI客户端都把对话上下文锁在自己的会话里底层数据完全不互通。你在这个模型里说过的话、确认过的偏好、写过的代码风格对另一个模型来说都是零。1.2 真正缺的不是聊天记录而是“记忆”聊天记录和记忆是两回事。聊天记录是过程态是流水账记忆是状态态是你希望AI在下一次对话中仍然能想起来的高密度信息。举个例子你在客户端A里说“我的后端服务叫notify-service用的是FastAPI部署在服务器上的/data/apps/notify目录”。这句话要完整聊上几十轮才会出现一次但它值得被记住。下一次你在客户端B里问“notify-service的代码在哪”如果B能直接给出答案那才叫“共享了记忆”。我经常用一个Git的类比来理解这件事多个AI客户端就像多个分支各自在工作区里改得热火朝天但原始终端被锁定在各自的仓库里没有人做合并。更离谱的是每次分支重建的时候所有历史提交都要重新手动 Cherry-pick 一遍等于回到石器时代。MemTether 想做的基本上就是这个“Git 合并主干”——把记忆沉淀成一份用户自己拥有的独立数据让任意一个AI客户端都能读取到它。我查了一圈当时市面上有MCPModel Context Protocol、有各家自带的记忆插件、也有类似“系统提示词管理工具”的东西。但MCP解决的是Agent“调用外部工具”这个问题不是“跨客户端共享用户记忆”的问题各家自带的记忆功能则永远绑死在自家生态里换个客户端就一切归零。我想要的是一个完全中立的、轻量的、协议开放的记忆层不绑定任何一家模型厂商也不强迫用户放弃现有的客户端。这是我决定动手做MemTether的直接原因。1.3 项目定位轻量、开源、自托管MemTether这个名字来自 Memory 和 Tether 的组合意思是“把记忆拴在一起”。它不是一个新AI客户端不自己去对话作图也不绑定某个模型。它就是一个放在用户本机或内网服务器上的记忆中心对外暴露HTTP API提供语义化记忆的写入、检索、注入能力。任何支持自定义API地址的客户端比如LobeChat、NextChat、Cherry Studio这类开源聊天前端甚至是你自己写的脚本都可以通过统一协议接入。开源是我的底线。记忆是用户自己的资产不应该被任何公司锁死。所以MemTether从第一行代码开始就按照MIT协议开源数据格式文档公开存储默认落在本地SQLite和本地向量索引里。用户可以选择完全离线运行不把任何数据交到第三方手里。这既是一种产品设计也是一种价值观选择。2. MemTether 的整体设计把记忆从客户端中解放出来2.1 记忆不是“聊天记录备份”设计MemTether时我给自己定了一条铁律只存高密度的记忆单元不存聊天流水。刚开始我犯过错天真地想把所有对话历史全文扔进数据库后来发现这条路走不通。原因很实在对话历史是上下文是过程里面大部分内容是临时指令、寒暄、调试过程中的尝试这些如果全部保留检索起来噪声极大而且很快会让存储膨胀、Token消耗飙升。后面我改成了“提取式记忆”的模型。系统会定期对对话内容做一次结构化提炼把其中值得长期记住的信息拆成一条条独立记忆。每条记忆有文本内容、一个类型标签比如事实、偏好、任务、上下文、一个重要度分值、来源客户端标识Client A / Client B、创建时间和最近访问时间等等。它可以是一句话“用户在写Python偏好类型注解”也可以是一段结构化描述“notify-service使用FastAPI框架部署路径是/data/apps/notify”。用一条条“事实片段”而不是整段对话来承载记忆好处很明显一是在检索时可以直接按语义相似度命中相关条目不用全文重扫二是跨客户端注入时我可以只注入那几条最相关的记忆控制上下文占用不把无用的闲聊灌进模型的窗口三是用户可以在管理界面上像看卡片一样浏览、编辑、删除自己的记忆真正做到“可检查、可遗忘”。2.2 一次性做对的三层架构接入层、记忆核心、存储层MemTether 的代码结构我保持得非常克制就三层第一层是接入层。它对外暴露一个兼容 OpenAI 格式的 HTTP 接口同时也有一些自定义的记忆管理端点。这里“兼容OpenAI格式”是个很重要的决定。因为市面上的开源客户端几乎都支持配置自定义的API Base URL如果我实现的是 OpenAI 的 /v1/chat/completions 协议那么几乎不需要改任何客户端代码只要把Base URL指向MemTether地址就能把对话请求经过它转发到真实模型服务。这相当于在客户端和模型之间插入一个“记忆旁路”。第二层是记忆核心也是最有技术含量的部分。它负责做对话文本的记忆提取、语义去重、冲突检测、重要度评分和衰减调度。说得直白一点就是决定“哪句话值得记”“新的记忆和旧记忆是否矛盾”“旧记忆太久没被用到要不要降权”。这部分相当于整个系统的大脑我会在下一章详细展开。第三层是存储层。结构化记忆和元数据落在SQLite里向量索引放在LanceDB里。选择SQLite是因为本地优先零运维配上LanceDB的本地向量索引既能做语义检索又不需要再单独起一个向量数据库服务。用户如果以后数据量特别大可以改成PostgreSQL加pgvector组合但这不在默认路径上保持开局简单很重要。2.3 和MCP、内置记忆功能的区别很多人一上来会问这跟MCP有什么区别通俗地解释MCP是让AI Agent“伸手去够外部工具”的协议比如你让Agent查个数据库、调用一下计算器MCP负责把工具暴露给模型。但MCP并不解决“用户在几个不同客户端之间共享同一份记忆”这个问题。你可以把MemTether理解为一种特殊的外部“记忆服务”但它不面向Agent工具调用而是面向用户与AI客户端的记忆沉淀与回灌。至于各家AI客户端自带的记忆功能它们本质上是为了增强自家产品体验绝对不是想让你带着记忆迁徙到别家。MemTether的立场是中立和开放我不管你连的是哪家模型甚至不在乎你是用LobeChat还是自写Python脚本调用只要通过HTTP协议连过来你的记忆就能互通。市面上确实也有一些“记忆插件”但大多绑定特定前端而MemTether把记忆层独立出来前端只是一个可替换的挂件。这才是我眼里真正能长期沉淀知识资产的架构。顺带说一句我并没有推翻MCP的意思。MemTether目前也预留了接口可以把记忆暴露成MCP工具给Agent使用两条路互不冲突。但核心的“共享记忆”能力一定得由独立的记忆层来做而不是落在某个客户端的私有插件里。3. 核心实现提取、检索与同步的细节3.1 记忆条目长什么样先看数据模型。一条记忆在MemTether里是这样存储的字段类型说明memory_idstring记忆唯一IDUUID格式contenttext记忆正文可多行memory_typeenumfact / preference / task / contextimportancefloat 0-1动态重要度影响注入优先级confidencefloat 0-1提取时的置信度source_clientstring来源客户端标识比如 lobe-chat / next-chatcreated_atdatetime创建时间last_accessed_atdatetime最近一次被检索命中的时间hit_countint被命中次数用于计算热度tagslist自定义标签比如 project:notify-serviceembeddingvector内容向量维度取决于所选嵌入模型linked_memorieslist关联记忆ID用于形成记忆簇这个模型不是一次性定稿的早期版本更简单但后来在实际使用中发现缺了不少东西。比如说 importance 一开始是静态的靠LLM给一个分值就不会变了。但时间一长很多“当时重要”的记忆会变成一次性的反倒是一些高频出现的偏好应该越来越靠前。我后来加了一套动态调整逻辑新记忆创建时获得初始重要度随后按时间衰减每条记忆如果被检索命中重要度就会回弹。类似搜索引擎里的热度排名只不过维度从“点击”换成了“被AI想起”。3.2 自动提取一份可复用的结构化Prompt记忆提取依赖一个LLM来做这是成本可控又效果最好的做法。提取任务说白了是一个信息压缩任务把几十轮的对话压缩成几条结构化记忆。我不希望这个功能依赖某一个特定模型所以实现上是“模型无关”的只要是一个正常的中文/英文对话模型就能胜任。核心逻辑是用一条固定Prompt做引导你现在负责从用户与AI的对话中提炼需要长期记住的信息。请忽略寒暄和临时指令只提取能满足以下条件的记忆 1. 关于用户的真实背景信息职业、技术栈、所在地、家庭成员等 2. 用户的明确偏好回答风格、代码风格、内容形式等 3. 用户的进行中任务和待办事项 4. 与特定项目相关的关键上下文框架选型、部署路径、命名约定等 输出格式为JSON数组每个元素包含 {content: 记忆正文, type: fact|preference|task|context, importance: 0.0到1.0的小数} 要求 - 每条记忆必须是语义完整的句子 - 避免重复如果多个对话片段指向同一事实只输出一条 - 不要输出单次会话的临时指令、不要输出与AI自身相关的内容这段Prompt看着简单但我在调参时踩了不少坑。最主要的是它经常把“对AI的指令”误当成“关于用户的记忆”。比如用户说“请你用Markdown格式回复”这只是一个临时的输出格式要求不该被当成长期偏好。解决办法是在Prompt里反复重申“只提取关于用户的长期信息不提取对AI的输出要求”并且在后处理时过滤掉首字母看起来像指令的条目。3.3 语义检索与去重冲突处理记忆写进去了最关键的还是“能不能找出来”。MemTether的检索采用“向量语义检索为主关键词过滤为辅”的策略。默认的嵌入模型用 bge-m3 系列它在中文语义匹配上的效果很不错又能在本地跑不需要额外申请API。把所有记忆的content编码成向量存进LanceDB用户在新的对话进来时先把问题文本转成向量然后做近似搜索取相近的前若干条。这里有个实际使用中的细节仅仅靠相似度分数并不够稳因为“用户下周要去杭州出差”和“用户下周要去北京出差”这两条记忆看起来很像但关键事实完全不同容易互相干扰。所以我在向量检索后加了一层过滤规则用户可以按 memory_type 过滤也可以加 tag 过滤。比如当客户端B是代码生成类客户端时注入侧优先注入 memory_typefact 和 memory_typepreference 的记忆把 task 和 context 类的记忆让位给计划类客户端。去重和冲突检测是更麻烦的一件事。新提取的记忆会跟已有记忆做一轮相似度比对如果和某条已有记忆的余弦相似度高于0.95就直接丢弃如果在0.85到0.95之间会打上“疑似重复”标签由后台定时任务做合并处理。真正的冲突检测是当两条记忆语义高度相似但关键信息不一致时比如用户在客户端A说“已经搬去北京了”但旧记忆里写的是“现在住上海”。这种规则系统很难100%自动判对所以我的设计是自动检测到冲突后在控制台弹出一条待人工确认的卡片用户看完手动选择保留哪一条。一开始确实费点事但时间长了你会发现这其实是给自己的知识库做了一次特别有价值的校验。3.4 注入与上下文控制记忆检索出来以后要怎么让目标AI客户端“想起来”是另一个讲究的地方。MemTether不会直接篡改用户的prompt而是在请求转发到真实模型之前把检索到的记忆烘焙成一段独立的上下文块拼接到system prompt里。[MemTether recalled memories] - (importance 0.92) 用户使用FastAPI开发后端服务项目代码位于服务器 /data/apps/notify - (importance 0.87) 用户偏好中文回复且喜欢在回答里附带可复现的代码片段 - (importance 0.75) 用户计划6月下旬出差杭州时间为下周三到周五这段文本会被插入到系统提示词的开头位置。之所以放在开头是因为大多数模型对系统提示词前部的注意力更高记忆放在偏后位置容易被长对话冲掉。这一步我用了一点小技巧注入的记忆不能贪多默认取前5条而且每条按重要度做了裁剪单条最长不超过120个汉字。测试下来如果注入十几条冗长的旧记忆模型会开始“迷失重点”回答质量和记忆的命中率都会下降。宁可少而准不要多而杂。接入层实现的是一个OpenAI兼容的 /v1/chat/completions 端点。它先接收客户端的正常请求从对话中提取用户最新消息作为检索query拿到相关记忆后拼接到system prompt再转发到你配置的真实模型网关。整个过程对用户和客户端完全透明这也是你不需要改客户端代码的核心原因。4. 实操一小时让两个客户端用上同一份记忆4.1 部署记忆中心MemTether的部署成本被刻意压到了最低目标是最低配置的云服务器或者一台本地电脑都能跑。我自己的实测环境是Python 3.11配一台2核4G的小主机整个服务加上索引库大概只吃300MB内存。先在仓库目录下建立好虚拟环境并安装依赖git clone https://example.com/memtether.git cd memtether python -m venv .venv source .venv/bin/activate pip install -r requirements.txt cp .env.example .env memtether init memtether start --port 8000为什么要走虚拟环境这是一个老生常谈但也确实必要的建议。MemTether依赖的numpy、fastapi那一堆库和系统自带的Python环境大概率版本冲突尤其是不同Linux发行版预装的Python包管理风格完全不同。用venv隔离之后怎么折腾都不影响系统环境。memtether init会生成一个.env配置文件里面最核心的几项是MEMTETHER_STORE_URLsqlite:///memtether.dbMEMTETHER_EMBEDDING_MODELbge-m3MEMTETHER_UPSTREAM_BASE_URLhttp://你的真实模型网关地址MEMTETHER_TOP_K5MEMTETHER_PORT8000初始化之后直接启动服务默认监听本机的8000端口。如果你想让它被局域网内的其他设备访问就把监听地址改成0.0.0.0同时记得在防火墙上放行端口。要注意一点MemTether默认不启用鉴权如果你打算在内网之外使用务必在接入层前面加一层认证或者只做内网穿透否则任何人都能往你的记忆库里写入垃圾数据。4.2 把LobeChat和NextChat接入接入以LobeChat和NextChat为例这两款是大家最常用的开源聊天前端。打开LobeChat的设置添加自定义模型服务商把API Base URL填成http://127.0.0.1:8000/v1API Key随便填一个占位符比如memtether-local模型名称填你实际需要使用的模型。保存之后就可以正常对话了。关键点在于MemTether对外暴露的是一个“透明转发”的OpenAI兼容接口。你在LobeChat里填的那个模型名最终会被转发到你配置的MEMTETHER_UPSTREAM_BASE_URL对应的真实模型服务上去。也就是说MemTether本身不自带模型它只负责“侧挂”记忆。这和直接填真实模型API地址的区别在于所有请求都会额外经过一次记忆注入而不仅仅是裸转发到模型。NextChat的配置更直观直接在自定义接口那一栏填同样的Base URL就行。如果你的设备上同时装了LobeChat和NextChat只需要在两边都加一个相同地址的自定义服务商它们就自动共享MemTether里的记忆了。我这里贴一下我常用的客户端配置对比方便你抄作业配置项LobeChatNextChatAPI Base URLhttp://127.0.0.1:8000/v1http://127.0.0.1:8000/v1API Key任意占位串任意占位串模型名任意转发到上游任意转发到上游系统提示词可留空可留空如果你用的不是这类开源前端而是自己写的Python脚本也没问题。MemTether的HTTP API完全开放直接调用/v1/memory/recall和/v1/memory/write端点即可。4.3 用一个场景验证全流程部署完成后我自己会跑一个非常简单的验证脚本用来确认流程有没有断。场景是这样的先在客户端A里说一句话“我下周要去杭州出差航班是周一早上九点。”这句话在被转发到真实模型的同时MemTether会在后台提取出两条记忆一条是“用户下周去杭州出差”的fact另一条是“航班时间周一早上九点”的task。这时打开MemTether的日志或者控制台能看到新写入的记忆卡片。然后切换到客户端B问“我下周的行程有什么安排”MemTether收到请求后会用这句话做向量检索把刚才两条记忆都捞出来拼进system prompt再转发给模型。客户端B的回答会直接说“你下周一早上九点去杭州出差”。这就完成了整个闭环。我第一次跑通这个闭环的时候说实话挺激动的因为整个过程你几乎感觉不到MemTether的存在但它确实把两个“互相不认识”的客户端串联起来了。客户端的对话是空的也好有历史也罢都不影响记忆层独立工作。测试的时候有个注意点别在同一个会话里反复用一模一样的话来测试因为记忆提取模块会对带重复内容的新对话做一次去重看起来像是“没反应”其实是去重生效了。我后来给控制台加了一个“显示最近写入记录”的面板就是为了在调试时能看到后台到底发生了什么。4.4 记忆控制台怎么管理命令行启动MemTether之后再开一个终端运行memtether ui --port 8080就能打开记忆管理界面。界面上按重要度倒序展示所有记忆条目每张卡片能看到内容、来源客户端、创建时间和命中次数。我设计了一套非常直白的操作按钮编辑、删除、标记隐私、加标签、设为待定。我自己的使用习惯是每晚看一眼“今日新增记忆”把明显是噪声的条目删掉值得长期保留的加一个项目标签。比如我的所有工作相关记忆都会打上project:notify-service类似的标签后续在客户端对话时只要提到项目名检索命中率会高很多。这个标签系统实战中非常管用它相当于给记忆库建了一堆抽屉语义检索负责找内容标签负责让用户主动分类。还有一个值得说的功能是“记忆归档”。当某条记忆的importance衰减到0.1以下且超过两周没有被命中它会被自动标记为“归档”。归档不会删除只是不再参与默认的检索注入除非主动搜索。这个机制有效防止了记忆库被陈旧信息淹没。我第一次意识到必须做归档是跑了两个星期后打开控制台发现累积了快两千条记忆而其中大部分是这种“周三下午开会”“买的耳机到了”级别的信息扔进检索池只会污染结果。5. 踩坑记录与使用建议5.1 遇到的几个典型问题速查表做MemTether的过程说不上顺利每个功能背后都是一串真实踩出来的坑。这里整理一个速查表应对你上手后最可能撞上的几个问题现象原因解决办法客户端回复越来越慢注入的记忆太多或上下文过长调低MEMTETHER_TOP_K默认5即可记忆库里全是废话提取Prompt对“临时指令”过滤不严升级到新版提取Prompt并开启疑似重复过滤客户端B说“我不记得”检索没命中记忆或命中后注入位置不对检查日志确认有没有recall数据确认system prompt拼接正常旧记忆和新事实冲突未开启冲突检测或冲突卡片没人处理开启冲突检测模式定期浏览待处理卡片局域网其他设备连不上监听地址还是127.0.0.1改成0.0.0.0并在云厂商安全组放行端口多次对话后记忆重复写入去重阈值太低提取出的相似记忆没被拦截把相似度阈值调到0.9以上最让我印象深刻的是第一次在局域网里部署时我把监听地址写成了127.0.0.1但忘了改回来手机上的App怎么都连不上一度以为是客户端配置错了。排查了半天才发现是监听地址的锅。这类细节其实都写在文档里但实际操作时就是容易忽略。5.2 对普通用户的建议先小范围用一个客户端MemTether虽然开源且好用但它毕竟是面向“愿意折腾”的用户群体的。如果你是第一次接触这类工具我的建议是先只接入一个客户端跑几天看看自动提取出来的记忆质量如何。质量稳定了再加第二个客户端。不要一上来就把所有客户端都接上否则记忆库会被大量重复提取的早期垃圾塞满体验会很糟糕。另外一个很重要的建议是别把敏感信息塞进记忆库。MemTether支持你完全离线运行但如果你为了用更好的嵌入模型而接入了云API发送到云端的内容就包含记忆文本。我在代码里写了一个简单的敏感词过滤规则身份证号、手机号、银行卡号格式的文本默认不进记忆库。但这只是最后一道保险真正的第一道保险应该是你自己的意识。能不上云的记忆就留在本地。还有一个心得是定期“喂”记忆。我每隔一周会主动在某个客户端里说一遍自己的近期项目状态比如“notify-service目前核心功能已经稳定接下来要优化日志模块”这样MemTether会把它提取到记忆库里相当于手动给各个客户端同步工作状态。一开始觉得怪怪的但习惯了之后发现这是对“记忆”最有效的主动管理方式——模型不知道的东西你说一遍它就知道了而且一次说给全局听。5.3 开源细节与项目后续方向MemTether在当前版本里已经包含了我认为一个合格开源项目必须有的东西README里有项目背景和快速开始示例有标准的MIT许可证有issue模板和PR模板Github Actions里配了单元测试和lint检查。我特意把数据格式文档单独放了一份因为如果记忆数据格式是封闭的那“用户拥有自己的记忆”就是一句空话。后续我想做的事情也很明确一是把多用户隔离做好现在是一份共享记忆库后面可能会支持多Profile方便家人合用一个设备时互相不干扰二是增加对更多嵌入模型的一键配置让用户不用改代码就能换文本向量模型三是提供一个简单的迁移工具支持把现在某个客户端的历史对话一次性导入记忆库让已有的聊天记录也能转化为结构化的记忆资产。我个人其实很清楚MemTether不会一夜之间改变AI工具的生态。但如果你也像我一样在几个AI客户端之间来回切换并且受够了每次都从头解释自己是谁这个项目会让你重新找到一点掌控感。记忆本来就是你自己的东西把它从各家客户端的黑盒里拽回来放在一个你自己说了算的地方这才是合理的做法。