☰
MemTether:为AI客户端构建共享记忆层,解决多客户端上下文割裂
2026/10/4 17:30:07 网站建设 项目流程

1. 项目概述:为什么我需要MemTether

先说一下我自己的处境。我日常的工作流里至少同时开着三个AI客户端:Claude Code用于主力编码、ChatGPT的Web端用于文档分析和资料整理、还有本地跑着的Open WebUI接各种开源模型做实验。问题来了,这三个客户端各聊各的,互不串门。上午在Claude里讨论完一个项目的技术选型,下午切到ChatGPT想让它基于同一个背景继续写方案,它一脸茫然,仿佛我们从未见过。

这种体验割裂感非常重。我试过几种常规解法:把关键上下文手动复制粘贴到每个新会话,或者用系统提示词塞一大段背景资料。短期对付一下还行,时间一长完全撑不住。上下文越堆越长,每个新会话都要重新喂一遍,而且喂的内容还可能过时——项目方向变了,旧记忆还在各个客户端里躺着,反而污染新会话的判断。

所以我做了MemTether。这是一个开源工具,定位是"AI客户端的共享记忆层":它给多个AI客户端提供一个统一的外部记忆存储与检索服务,任何接入了MemTether的客户端都能读写同一份记忆数据。简单说,你在Claude Code里记录的决策结论,切到ChatGPT继续对话时它能直接调出来,不用再重复解释一遍来龙去脉。

这个工具适合谁用?如果你和我一样,日常在多客户端之间切换做复杂任务、需要跨会话保持连贯上下文、或者想给AI助手建立一个长期稳定的"人设背景库",那MemTether就是冲着你做的。它不挑客户端——走标准HTTP接口,任何能自定义API Base URL或支持MCP(Model Context Protocol)的客户端都能接。

至于为什么叫MemTether,意思是"把记忆拴住"。AI客户端本身是无状态的,每次对话都是一次性的,MemTether做的就是在外面加一根绳子,把本该飘走的记忆拉住,让不同客户端共享这同一根绳。

2. 记忆分散问题的本质与MemTether的架构思路

2.1 AI对话为什么记不住事

先拆一个底层问题:AI客户端为什么不自带长期记忆?以大语言模型的工作机制来说,模型本身就是一个固定的参数集合,每次推理时能"看到"的只有当前Prompt里的内容。对话历史之所以能延续,是因为客户端把前面的消息重新拼进Prompt里再发给模型。一旦会话结束、上下文窗口被清理,这些历史就没了。

这里有个关键限制:上下文窗口再大也是有限的,动不动几万token看着多,一旦对话拉长、中间夹着代码和长文档,很快就会被耗尽。如果让客户端自己管理"记忆",无非是两种做法:一是把所有历史都塞进上下文,这必然撞上窗口上限;二是做摘要压缩,但摘要折损信息,且这个过程在各个客户端里是互相隔离的。

更深一层的问题是架构层面的:大多数AI客户端把"对话历史"和"业务上下文"混在一个会话里。一个项目决策、一个技术方案、一个用户偏好,全都以时间线的形式堆在同一串消息里,检索和定位变得极其困难。你问AI"我们之前定的数据库方案是什么",如果那个结论发生在八百条消息之前,它大概率给不出准确答案。

2.2 MemTether的解法:记忆与模型解耦

MemTether的核心思路是:把记忆从对话时间线里彻底拆出来,变成一个独立的、可检索的、结构化的存储层。对话历史仍然留在客户端那边,但那些值得长期保存的知识点——项目决策、用户偏好、技术结论、人物关系、任务状态——在对话进行的同时被提取出来,写入MemTether的存储后端。

这里我把"记忆"定义得很窄:不是把整个聊天记录存下来,只存那些真正有跨会话复用价值的"事实型信息"。比如"项目A采用PostgreSQL,原因是团队熟悉且需要JSONB功能"是一条记忆;而"用户今天问了三次天气"不值得存。定义窄的好处是检索质量高、不污染上下文,存储量也小。

在接入方式上,MemTether做了两手准备。第一是标准REST API,任何客户端只要能配置自定义API地址、能以编程方式调用HTTP接口,就能直接读写记忆。第二是MCP支持,MCP是Anthropic推的一个协议,目的就是让AI应用标准化地接入外部工具和数据源,Claude Code、Cursor等客户端原生支持MCP,所以它们可以走MCP通道,把记忆读写变成AI自己就能调用的"工具"。

2.3 为什么是服务端架构而非本地文件

我最早的原型就是个SQLite文件加几个函数,做成Python库直接嵌在客户端进程里。但很快发现这个设计有硬伤:如果记忆只在进程内存活,那"多个客户端共享"就不可能做到——Claude写进去的记忆,ChatGPT那边的进程根本读不到。

所以必须抽成独立服务。MemTether跑成一个常驻进程,监听一个本机端口(默认8765),所有客户端都通过网络访问它。这种设计多了一个进程要维护,但换来的是真正意义上的多客户端共享。部署形态上我提供Docker镜像,一条命令起服务,不折腾环境。存储后端默认是SQLite,够轻、够稳、零运维;数据量大或多人协作时可以切换到PostgreSQL,配置改个连接串就行。

架构上还有一个容易忽略的点:MemTether不做"记忆自动抓取"。也就是说,它不会偷听你每个客户端的对话内容去自作主张地存东西。所有写入都由客户端侧显式触发,或者由客户端里的AI在需要时主动调用写入接口。这样有两个好处:一是隐私可控,用户完全知道什么被记住了;二是避免误存,不会把闲聊的废话存进长期记忆库。

3. 核心设计拆解:这份记忆是怎么实现共享的

3.1 记忆的基本单位:条目(Entry)结构设计

先看一条记忆在MemTether里长什么样。我设计了一个统一的结构化Schema,每条记忆都是一个JSON对象:

{ "id": "uuid-v4", "type": "decision", "content": "项目A的数据库采用PostgreSQL 16,主因是团队熟悉且需要JSONB字段做配置存储", "tags": ["project-A", "database", "postgresql"], "client": "claude-code", "created_at": "2025-06-15T08:30:00Z", "updated_at": "2025-06-15T08:30:00Z", "last_accessed_at": "2025-06-15T09:00:00Z", "access_count": 3 }

说几个字段的设计意图。type是记忆的类型,我预设了几个枚举值:decision(决策)、preference(偏好)、fact(事实)、task_state(任务状态)、relationship(关系)。这个分类的作用是给检索提供语义过滤:你可以只查"最近的项目决策",而不需要把所有聊天记录翻出来。tags是给记忆打标签,用于精确过滤,比如想调出所有关于项目A的记忆,就按project-A这个标签查。

client字段记录这条记忆是哪个客户端写入的。有个朋友问我:既然是共享,为什么还要记来源?实际用下来这个字段非常关键——不同客户端的上下文风格不同,存储时保留来源可以帮你在检索结果里判断信息的可信度和适用场景。比如某条记忆来自代码库分析,那么它对写代码的任务权重应该更高。

access_count和last_accessed_at是配合使用的,记录每条记忆被检索命中的频率。这个数据的用途在后面的重排序阶段:高频访问的记忆在搜索结果里排名更靠前,因为大概率是长期有效的关键信息。低频访问的记忆如果超过一定时间没被命中,在存储量膨胀时可以进入归档区。

3.2 记忆检索:语义相似度还是精确匹配

记忆只有写没有读等于白做,检索才是核心。MemTether支持三种检索模式:

一种是关键词精确匹配,对content做分词处理,匹配tags里的标签。这种查得快、结果稳定,适合你已经知道要找什么的时候。比如想找"数据库选型"相关的记忆,直接按tags过滤即可。

第二种是SQL LIKE的模糊查询,适合只知道大概内容但记不清完整语境的情况。

第三种是语义向量检索,这是和"AI记忆"最搭的一种方式。写入记忆时,MemTether会调用一个嵌入模型给content生成一个向量,存储到SQLite的vec0虚拟表里(SQLite-vec扩展)。查询时把用户的问题也转成向量,算余弦相似度取Top-K。

这里有个选型故事。嵌入模型最初我接的是OpenAI的text-embedding-3-small,效果很好但有个问题:用户在本地跑MemTether,每写一条记忆都要往外发一次请求,隐私和数据主权上过不去。后来我改为默认支持本地嵌入模型,通过Ollama跑nomic-embed-text或bge-m3这类开源模型,完全离线,延迟也低。如果你有更高精度的需求,也可以在配置里切换到云端的嵌入模型,API Key写进环境变量就行。

用户发出的检索请求,MemTether会先跑一个轻量的关键词匹配做初选(代价很低),然后用向量重排序把语义相近的结果顶到前面来。这种"BM25初选 + 向量精排"两段式检索是很多RAG系统的标准做法,好处是兼顾了召回率和延迟。

3.3 多客户端接入方式:MCP与HTTP双通道

先讲MCP通道。Claude Code、Cursor这类客户端在配置里加一段MCP Server地址就能用,以一个JSON文件为例:

{ "mcpServers": { "memtether": { "command": "npx", "args": ["-y", "@memtether/mcp-server"], "env": { "MEMTETHER_BASE_URL": "http://127.0.0.1:8765" } } } }

配置完成后,AI客户端会在对话中自动识别"这个信息值得长期保存"的场景,通过MCP工具调用写入接口;你在对话里问"我们之前是怎么决定的",AI会调用检索接口拉记忆后参考回答。这个通道对用户完全透明,交互自然。

再讲HTTP通道。对于不支持MCP的客户端,MemTether暴露了一组简单的REST接口。核心的就两个:

  • POST /api/memories:写入一条记忆
  • GET /api/memories/search?q=关键词&limit=5:检索记忆

以Open WebUI为例,它支持在配置里设置"工具调用URL",你可以把自定义函数挂进去。我在项目仓库里提供了Open WebUI的接入示例,用一个Python脚本把Open WebUI的对话勾子对接MemTether,实现"对话结束前把关键结论写入记忆"和"每条用户消息进来时先拉相关记忆拼进上下文"两个逻辑。

从设计取舍上说,HTTP接口是最通用的方案,什么客户端都能接;MCP是体验最顺滑的方案,AI自己就能完成记忆的读写决策。两个通道不冲突,同一个MemTether实例可以同时服务多个不同接入方式的客户端。

4. 实操过程:本地部署与客户端接入详解

4.1 环境准备与Docker部署

MemTether对运行环境的要求很低。我的开发机是一台老MacBook Air,M1芯片8GB内存,跑着服务、Ollama本地模型和三个AI客户端,整体还算流畅。以下是你需要准备的东西:

  • Docker(或用Python 3.11+直接跑源码,二选一)
  • 本地嵌入式模型(通过Ollama跑,非必须,没有也能用关键词检索)
  • 至少一个支持自定义API地址或MCP的AI客户端

如果你有Docker,部署就是一条命令的事:

docker run -d \ --name memtether \ -p 8765:8765 \ -v memtether_data:/app/data \ ghcr.io/memtether/memtether:latest

-v memtether_data:/app/data这行很重要,把数据目录挂载到了宿主机,否则容器一删记忆全没。数据落在SQLite文件里,日常备份直接把整个数据目录打包即可。

部署完验证服务是否正常,访问http://127.0.0.1:8765/health应该返回{"status": "ok"}。我习惯把它加到系统服务里,开机自动拉起来跑,用launchd或systemd都行,给你一段systemd配置做参考:

[Unit] Description=MemTether Memory Service After=docker.service Requires=docker.service [Service] Restart=always ExecStart=/usr/bin/docker start -a memtether ExecStop=/usr/bin/docker stop memtether [Install] WantedBy=multi-user.target

4.2 接入Claude Code(MCP方式)

这是我最常用的接入方式。Claude Code原生支持MCP Server,接入步骤就是往配置文件里加一段。在项目根目录创建.mcp.json:

{ "mcpServers": { "memtether": { "command": "npx", "args": ["-y", "@memtether/mcp-server"], "env": { "MEMTETHER_BASE_URL": "http://127.0.0.1:8765" } } } }

重启Claude Code后,敲/mcp应该能看到MemTether已经连接。我在工具的memories.md文档里给了几条推荐使用的自然语言指令,例如:"记住,当前项目采用单体架构,部署目标是自建K8s集群"——当你把这句话发给Claude时,它会识别为记忆写入请求,调用MemTether的保存工具。当你想回溯时,直接问:"我们项目架构是怎么定的?",它会先检索记忆再回答。

一个使用技巧是:在Claude Code的CLAUDE.md项目记忆文件里加一段说明,告诉Claude它有这个工具、什么时候该存、什么时候该查。我加的说明是这样:

你有一个外部记忆工具MemTether可用。当用户明确要求记住某事、或对话中出现重要的项目决策/技术结论时,应主动写入记忆;当用户询问过去讨论过的内容时,应优先从记忆中检索,不要猜测。

加上这段之后,Claude的主动记忆行为明显更靠谱了,不再需要我每句话都手动触发。

4.3 接入ChatGPT Web端(HTTP方式)

可能有人觉得奇怪:ChatGPT Web端怎么接外部服务?答案是走它的自定义GPTs或Action功能。在GPTs的配置面板里,你可以添加一个Action,填上MemTether的接口地址和OpenAPI Schema,然后自定义GPT就有了读写MemTether的能力。

在GPT Builder里新建一个GPT,命名"带记忆的工作助手"。Instructions里写清楚:它是一个有长期记忆的助手,在对话开始时会查询MemTether获取相关记忆;当用户提供新的关键信息时会主动保存。Actions里添加API Schema:

openapi: 3.0.0 info: title: MemTether API version: 1.0.0 paths: /api/memories/search: get: operationId: searchMemories parameters: - name: q in: query required: true schema: type: string responses: '200': description: Search results /api/memories: post: operationId: addMemory requestBody: content: application/json: schema: type: object properties: type: type: string enum: [decision, preference, fact, task_state] content: type: string responses: '200': description: Memory saved

认证方式选"无认证"(MemTether服务不做鉴权,因为默认只监听本地)。这样设置之后,ChatGPT就不再是"记忆每段会话都不串联"的状态了——它会在每次对话时先通过Action从MemTether拉取相关信息,再组织回答。

4.4 接入Open WebUI(工具调用方式)

Open WebUI走的路径不太一样,它本身支持"工具"(Function)功能,可以写Python函数来调用MemTether。我写了一个示例函数,你把它粘贴到Open WebUI的工具面板里即可:

import json, httpx MEMTETHER_URL = "http://127.0.0.1:8765" async def memory_search(query: str) -> str: """Retrieve relevant memories from MemTether. Use this function at the beginning of a conversation if the user references past discussions or decisions.""" async with httpx.AsyncClient() as client: resp = await client.get( f"{MEMTETHER_URL}/api/memories/search", params={"q": query, "limit": 5} ) data = resp.json() return json.dumps(data, ensure_ascii=False, indent=2)

在Open WebUI里启用这个工具后,模型在生成回复前会主动调用memory_search函数,把相关记忆拼入上下文。配合Open WebUI的"会话重置后自动注入记忆"自定义管道,效果非常接近"这个AI一直记得我"。

4.5 记忆检索效果实测

部署完成、接完客户端,我拿一个真实场景做了连续测试。第一步,在Claude Code里写入记忆:"MemTether项目的后端存储默认使用SQLite,当并发超过20时才需要迁移PostgreSQL。"第二步,关掉Claude Code会话,打开ChatGPT的GPTs,问:"我们那个工具的数据存储方案是怎么定的?"它通过Action检索,正确回答出了"默认SQLite,超过20并发迁移PostgreSQL"。第三步,回到Claude Code新开一个会话,问同样的问题,也能答对。

这说明两个客户端确实共享了底层记忆存储。关键在于:我没有在任何一个客户端里重复粘贴背景资料,每个客户端只通过MemTether拿到它需要的那一小块记忆,省了上下文空间,也避免了信息不一致。

5. 记忆条目的Schema设计:为什么我放弃了"万能模板"

5.1 最开始的Schema长什么样

第一版MemTether的条目设计得很随便,就四个字段:id、content、timestamp、client。用起来发现一个问题:信息和信息之间没有结构差异,"用户喜欢简洁的回答"和"项目A后端用Go"存在同一个Schema里,检索时没法区分。虽然加关键词能筛掉一部分,但语义过滤基本做不到。

第二版我试图做"万能模板",设计了一套超级灵活的JSON Schema,相信把它用到极致就能覆盖所有记忆场景。结果这套模板复杂到,我自己作为开发者都嫌麻烦,AI客户端调用时更是经常传错格式——写入接口收到的数据千奇百怪,解析一大堆异常。我意识到走错方向了,模板灵活到等于没有模板。

5.2 最终的方案:小而够用的类型化Schema

痛定思痛,回到了一个极简但每个字段都有明确用途的设计。就是前面展示的那个Schema:type、content、tags、client四个业务核心字段加上若干时间戳和统计字段。没有再往里面塞更多东西了。

为什么不再加?因为AI客户端的文本表达能力远比你预设的Schema丰富,你定义的任何字段都可能限制它。与其追求完整的数据模型,不如定一个松散的骨架,让content承载主要信息,让tags承担索引功能,让type提供最小的分类维度。其他信息,比如来源链接、关联人物、截止日期,如果需要,写进content文本里有意义,写进扩展字段反而增加负担。

对于想要二次开发的用户,我会建议不要改动MemTether服务端的Schema逻辑,而是在应用层做扩展——客户端侧对特殊记忆类型做自定义解析,把解析结果拆成几个MemTether标签。比如你有一个"会议记录"类记忆,在客户端里切成"参会人"、"结论"、"待办"三个标签,分别存储和索引。这比在MemTether核心里硬加一个"会议记录"类型要灵活得多。

5.3 记忆覆盖与更新的处理策略

记忆库里如果存了一条过时信息,比没存更糟。我提供了两个更新入口:一是显式的覆盖接口,写入时带上相同的id,内容直接替换;二是在检索结果里客户端可以判断是否过时,把新结论作为新记忆写入,旧记忆利用last_accessed_at表现不佳、加上时间衰减因素,逐渐沉底,不再出现在搜索结果里。

对于"旧记忆沉底"这个逻辑,我是这样做的:置信度函数简化成时间和频次的加权。score = 0.6 * semantic_sim + 0.3 * frequency_bonus + 0.1 * recency_bonus。这个比例不是拍脑袋定的——sim占比最高保证相关性优先,frequency和recency各起辅助作用,避免冷门的但全新的记忆完全没机会浮上来。如果你想调整排序感官,这三个权重都可以通过环境变量覆盖。

5.4 手动管理记忆的工具

我用Rust写了一个小的CLI工具,能直接在命令行里查询、列出、删除记忆,对于日常管理非常方便。几个常用命令:

# 搜索关键词 "postgres" memtether-cli search "postgres" # 列出最近10条记忆 memtether-cli list --limit 10 # 删除某条记忆 memtether-cli delete <id> # 按类型过滤 memtether-cli list --type decision

CLI的好处是你能在不开任何AI客户端的情况下,直接检查记忆库里存了什么、有没有脏数据、哪些该清理。毕竟对AI自动写入的记忆,偶尔还是需要人工监督一下的。

6. 常见问题与排查技巧实录

6.1 MCP连接失败:npx拉包超时

症状:Claude Code启动时报MCP server failed to start。原因多半是首次运行npx下载包时网络不好,或者本机node版本太低。解决方法是先手动跑一次npx -y @memtether/mcp-server,预先下载好包并确认能启动。如果还是失败,检查node版本,需要16以上。我遇到过在老旧Node环境下npx一直报错,升级Node后瞬间解决。

6.2 客户端检索不到记忆:向量与关键词配置没对齐

症状:GPTs查"我们数据库是啥",返回空结果。排查顺序:先用CLI手动搜一下memtether-cli search "数据库",如果CLI搜得到而客户端搜不到,那问题肯定在客户端侧的query构造上,大概率是客户端把检索词处理成了完全不同的关键词。如果CLI也搜不到,检查是不是只开了向量检索但没有配本地嵌入模型,导致写入失败——所有写入都会在日志里留下错误记录,docker logs memtether看一眼就知道。

这里有个容易踩的坑:本地嵌入模型没启动时,写入接口不会直接报400,而是把向量字段留空、存成纯文本条目。这种条目在关键词检索时能搜到,但在向量检索时必然搜不到。如果你的嵌入模型不是常驻运行的,建议用Supervisord或systemd把它也管起来,保证和MemTether同生共死。

6.3 SQLite锁竞争:长时间写入后出现"database is locked"

这个在高频并发写入时遇到过一次。MemTether默认的SQLite模式是每个请求开一个连接,高频调用时会出现写入锁冲突。解决办法很直接:打开WAL模式。在服务启动配置里设置PRAGMA journal_mode = WAL;,写入并发能力会有量级提升。WAL模式下读和写可以同时进行,不会互相阻塞,这对"检索和写入同时发生"的日常使用场景是必须的。

6.4 记忆库膨胀:搜索速度越来越慢

记忆条目到几万条之后,不带索引的全文搜索会有可见延迟。优化手段有三个,按性价比排列:第一,给tags建索引,这是收益最大的,因为大多数搜索都带标签过滤;第二,给created_at建索引,配合时间范围过滤分页;第三,对content建FTS5全文索引,SQLite内置的全文检索引擎,分词和速度都够用。做完这三个优化,十万条记忆的查询延迟能维持在百毫秒级,日常使用感知不到。

6.5 一个答疑:MemTether直接存对话记录行不行

从技术上讲你可以写一个适配器把所有对话消息无一例外都灌进来,但我不建议这么用。原因有二:第一是检索噪声爆炸,存得越多,检索时无关结果越多,反而干扰客户端的判断;第二是目的偏离,MemTether定位的是事实型长期记忆,对话原始记录的保存应该是客户端侧的功能,不该混淆。我的使用建议是:预设一个小过滤器,只让包含关键词(比如"项目"、"决定"、"记住"、"偏好")的记忆通过,效果会好得多。

7. 扩展玩法:把MemTether当记忆底座还能做什么

走完基础部署接入,我一直在琢磨这个工具还能怎么玩。一个方向是接进智能体工作流。比如让两个agent协作——一个负责资料收集,一个负责方案设计,它们之间不需要直接对话,通过MemTether交换信息。agent A把搜集到的事实写入记忆,agent B从记忆里检索整理,解耦得很干净。其实我这个工具当初的原型就是为了解决多agent协作时的共享状态问题,后来才扩展成通用的记忆层。

另一个方向是多客户端差异化同步。MemTether支持给记忆打上可见性标记,比如某些记忆只对"代码类客户端"可见,某些只对"写作类客户端"可见。这样Claude Code和ChatGPT共享的是底层的"项目事实",而各自客户端私有的偏好设置可以隔离。我预留了一个scope字段在扩展字段里,目前是自由文本,你可以写all、coding、writing,检索接口里通过scope参数过滤。这个功能我还没做到UI层面,但API是支持的。

用下来最大的体会是:"记忆共享"的能力真正发挥作用,依赖的是把记忆结构化地沉淀,而不是简单的数据互通。MemTether实现的是前者。它从一个存储工具变成了一个基础设施——AI客户端是即用即走的临时会话,而记忆库是长期积累的资产。这个思路,至少目前看对多客户端工作流是有效的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询