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客户端都能读取到它。
我查了一圈,当时市面上有MCP(Model 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_id | string | 记忆唯一ID,UUID格式 |
| content | text | 记忆正文,可多行 |
| memory_type | enum | fact / preference / task / context |
| importance | float 0-1 | 动态重要度,影响注入优先级 |
| confidence | float 0-1 | 提取时的置信度 |
| source_client | string | 来源客户端标识,比如 lobe-chat / next-chat |
| created_at | datetime | 创建时间 |
| last_accessed_at | datetime | 最近一次被检索命中的时间 |
| hit_count | int | 被命中次数,用于计算热度 |
| tags | list | 自定义标签,比如 project:notify-service |
| embedding | vector | 内容向量,维度取决于所选嵌入模型 |
| linked_memories | list | 关联记忆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_type=fact 和 memory_type=preference 的记忆,把 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_URL=sqlite:///memtether.dbMEMTETHER_EMBEDDING_MODEL=bge-m3MEMTETHER_UPSTREAM_BASE_URL=http://你的真实模型网关地址MEMTETHER_TOP_K=5MEMTETHER_PORT=8000
初始化之后直接启动,服务默认监听本机的8000端口。如果你想让它被局域网内的其他设备访问,就把监听地址改成0.0.0.0,同时记得在防火墙上放行端口。要注意一点:MemTether默认不启用鉴权,如果你打算在内网之外使用,务必在接入层前面加一层认证或者只做内网穿透,否则任何人都能往你的记忆库里写入垃圾数据。
4.2 把LobeChat和NextChat接入
接入以LobeChat和NextChat为例,这两款是大家最常用的开源聊天前端。打开LobeChat的设置,添加自定义模型服务商,把API Base URL填成http://127.0.0.1:8000/v1,API Key随便填一个占位符,比如memtether-local,模型名称填你实际需要使用的模型。保存之后就可以正常对话了。
关键点在于,MemTether对外暴露的是一个“透明转发”的OpenAI兼容接口。你在LobeChat里填的那个模型名,最终会被转发到你配置的MEMTETHER_UPSTREAM_BASE_URL对应的真实模型服务上去。也就是说,MemTether本身不自带模型,它只负责“侧挂”记忆。这和直接填真实模型API地址的区别在于:所有请求都会额外经过一次记忆注入,而不仅仅是裸转发到模型。
NextChat的配置更直观,直接在自定义接口那一栏填同样的Base URL就行。如果你的设备上同时装了LobeChat和NextChat,只需要在两边都加一个相同地址的自定义服务商,它们就自动共享MemTether里的记忆了。我这里贴一下我常用的客户端配置对比,方便你抄作业:
| 配置项 | LobeChat | NextChat |
|---|---|---|
| API Base URL | http://127.0.0.1:8000/v1 | http://127.0.0.1:8000/v1 |
| API 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客户端之间来回切换,并且受够了每次都从头解释自己是谁,这个项目会让你重新找到一点掌控感。记忆本来就是你自己的东西,把它从各家客户端的黑盒里拽回来,放在一个你自己说了算的地方,这才是合理的做法。