ARTICLE DETAIL

资讯详情

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

MemTether:为AI客户端构建共享记忆层,解决多客户端上下文割裂

MemTether:为AI客户端构建共享记忆层,解决多客户端上下文割裂 1. 项目概述为什么我需要MemTether先说一下我自己的处境。我日常的工作流里至少同时开着三个AI客户端Claude Code用于主力编码、ChatGPT的Web端用于文档分析和资料整理、还有本地跑着的Open WebUI接各种开源模型做实验。问题来了这三个客户端各聊各的互不串门。上午在Claude里讨论完一个项目的技术选型下午切到ChatGPT想让它基于同一个背景继续写方案它一脸茫然仿佛我们从未见过。这种体验割裂感非常重。我试过几种常规解法把关键上下文手动复制粘贴到每个新会话或者用系统提示词塞一大段背景资料。短期对付一下还行时间一长完全撑不住。上下文越堆越长每个新会话都要重新喂一遍而且喂的内容还可能过时——项目方向变了旧记忆还在各个客户端里躺着反而污染新会话的判断。所以我做了MemTether。这是一个开源工具定位是AI客户端的共享记忆层它给多个AI客户端提供一个统一的外部记忆存储与检索服务任何接入了MemTether的客户端都能读写同一份记忆数据。简单说你在Claude Code里记录的决策结论切到ChatGPT继续对话时它能直接调出来不用再重复解释一遍来龙去脉。这个工具适合谁用如果你和我一样日常在多客户端之间切换做复杂任务、需要跨会话保持连贯上下文、或者想给AI助手建立一个长期稳定的人设背景库那MemTether就是冲着你做的。它不挑客户端——走标准HTTP接口任何能自定义API Base URL或支持MCPModel 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关键词limit5检索记忆以Open WebUI为例它支持在配置里设置工具调用URL你可以把自定义函数挂进去。我在项目仓库里提供了Open WebUI的接入示例用一个Python脚本把Open WebUI的对话勾子对接MemTether实现对话结束前把关键结论写入记忆和每条用户消息进来时先拉相关记忆拼进上下文两个逻辑。从设计取舍上说HTTP接口是最通用的方案什么客户端都能接MCP是体验最顺滑的方案AI自己就能完成记忆的读写决策。两个通道不冲突同一个MemTether实例可以同时服务多个不同接入方式的客户端。4. 实操过程本地部署与客户端接入详解4.1 环境准备与Docker部署MemTether对运行环境的要求很低。我的开发机是一台老MacBook AirM1芯片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] DescriptionMemTether Memory Service Afterdocker.service Requiresdocker.service [Service] Restartalways ExecStart/usr/bin/docker start -a memtether ExecStop/usr/bin/docker stop memtether [Install] WantedBymulti-user.target4.2 接入Claude CodeMCP方式这是我最常用的接入方式。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 Schemaopenapi: 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_asciiFalse, indent2)在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痛定思痛回到了一个极简但每个字段都有明确用途的设计。就是前面展示的那个Schematype、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 decisionCLI的好处是你能在不开任何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客户端是即用即走的临时会话而记忆库是长期积累的资产。这个思路至少目前看对多客户端工作流是有效的。
返回列表