news 2026/10/6 11:33:14

多AI客户端记忆共享:我用MCP和SQLite给ChatGPT与Claude接上外置大脑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多AI客户端记忆共享:我用MCP和SQLite给ChatGPT与Claude接上外置大脑

我平时干活离不开AI,ChatGPT、Claude、本地Ollama、手机上的几个助手App轮着用。工具一多问题就来了:每个客户端都有自己的对话记忆,但它们彼此完全不互通。上午在ChatGPT里敲定的技术方案,下午到Claude那边问一个接口细节,它一脸茫然。这种记忆碎片化让我实在忍不了了,于是自己动手写了一个开源工具MemTether,核心就一件事——让多个AI客户端共享同一份记忆,聊天界面还是各家自己的,但“记忆”统一由外面这层服务管。之所以叫这个名字,tether就是“拴住、绑定”的意思,把分散在各个客户端里的上下文,通过一个公共存储层绑在一起。

这个工具适合谁?开发者、知识工作者、重度AI使用者,凡是日常要在两个以上客户端切换的人都用得上。它解决的不是单个客户端“记性好不好”的问题,而是“我换了个工具、我的上下文丢了”的协作断层。接下来我把设计思路、技术选型、完整实现和踩过的坑都摊开讲,你可以直接照着复现。

1. 这个项目到底解决什么问题

1.1 多客户端之间的记忆断层

我先描述几个具体场景,你大概率也遇到过。

场景一:我在ChatGPT里讨论了一套微服务的拆分方案,明确了每个模块的边界和数据流向。下午切到Claude想让它基于这个方案写一份接口文档,它完全不知道上午聊了什么,我只能把整个方案重新贴一遍,浪费大量token。

场景二:本地Ollama跑着一个私有模型,我喜欢让它处理一些不便于传到云端的文本。但它上下文窗口有限,超过一定轮数就开始忘前面的约束条件。每次都得把“你是一个熟悉我们项目的助手,以下三条规则必须遵守”这种话重复粘贴。

场景三:手机上的AI助手和桌面端不互通。我在手机上问了一个问题,回到电脑前想接着追问,结果对话记录对不上,手头又懒得去翻聊天历史。

这些问题本质上是同一个:每个客户端都维护一份封闭的记忆,没有公共的、可被多个客户端读写的记忆层。所谓“AI有没有长期记忆”,在单个客户端内部其实是模型靠上下文窗口和客户端自带的存储实现的,但换成跨客户端,就完全是空白。

1.2 为什么不去做一个全家桶客户端

可能有人会说:你直接用某个全家桶产品不就行了?比如在一个平台里把文档、对话、知识库全包进去。我也试过,但有两个问题。

第一,全家桶把你锁死在它的生态里。很多客户端的对话记录、知识库格式都是私有的,导出困难,跨平台迁移更麻烦。我既想在桌面端用,又想在手机上用,还想接本地模型,全家桶很难同时满足。

第二,工具之间本来就有分工。ChatGPT适合快速头脑风暴,Claude在长文本写作上表现更好,本地模型处理敏感数据更安心。我希望保留各自的长处,只是让它们共用一套“记忆底子”。所以我选择做一个独立的、开源的中转记忆层——MemTether不替代任何客户端,只是给所有客户端加一个外置大脑。

1.3 合适的人群和不合适的场景

这个工具最适合的人群是:需要同时维护多个AI助手、并且希望它们之间的上下文能连贯的人。典型用户包括独立开发者、研究员、内容创作者、经常用AI做技术方案的人。

但也得说清楚哪些场景不适合。如果你只是偶尔用一次AI,对话之间确实没有延续需求,那这个工具对你来说就是过度设计。如果你的记忆内容涉及极其敏感的商业机密,且对数据隔离要求非常高,那你要做的是私有化部署所有模型,而不是把记忆集中到一个第三方服务上——虽然MemTether本身完全本地运行,但你的实际使用环境决定了风险边界。

2. 架构设计与技术选型

2.1 总体结构:记忆服务与客户端之间的三层关系

MemTether的整体结构分为三层:客户端层、协议层、存储层。

客户端层就是你现在用的各种AI工具,它们负责对话和生成。协议层是我定义的公共记忆接口——支持通过MCP(Model Context Protocol)调用,也支持通过一个OpenAI兼容风格的小代理接入。存储层是一个SQLite数据库,保存记忆条目。

这样的分层设计有一个好处:记忆服务不关心对话是怎么生成的,也不关心生成模型是谁。它只负责两件事——接收来自任意客户端的“写入记忆”请求,以及响应“查找记忆”请求。客户端与客户端之间不需要互相知道对方的存在,它们都只与MemTether通信。我把这种模式称为“星型共享”:中心是记忆服务,外围是各客户端。

2.2 对接协议的选型:为什么主推MCP

当时我可以选择的对接方式有好几种:一种是直接改客户端源码,适合开源客户端;一种是给客户端做插件;还有一种是走HTTP API。但最后我主推MCP。

MCP是模型上下文协议,相当于给AI应用提供的标准“目录服务”。它的核心逻辑是让客户端可以动态发现外部工具能力并调用。目前越来越多客户端原生支持MCP,比如Claude Desktop、一些开源的IDE助手、知识库工具等。也就是说,我不用为每个客户端写专门的插件,只要把我的记忆能力封装成MCP server,支持MCP的客户端直接就能用。

当然,现在还有大量客户端不支持MCP。所以我额外做了一个OpenAI兼容的代理层,让那些只认OpenAI API格式的客户端也能通过自定义base_url接入。这个代理接收到请求后,会先从记忆库捞出相关内容,塞进system prompt里再转发到真正的模型接口。这样等于把“记忆注入”做在了代理层,客户端无感知。

2.3 存储层:先从SQLite开始,再谈向量检索

记忆存储我第一版直接用SQLite。为什么不是MySQL、PostgreSQL或者向量数据库?因为我需要的是零依赖、单文件、备份容易的存储方案。MemTether的定位是一个可以跑在树莓派或者一台老笔记本上的轻量服务,SQLite完全够用。

但只靠SQLite的LIKE查询是不够的——用户搜索“上周说的数据库选型”这种模糊语义,关键词匹配会很糟糕。所以在存储层我留了扩展接口:数据库内容会同步导出到向量索引中,支持语义检索。向量检索的索引可以选sqlite-vec、Chroma或者纯本地运行的嵌入模型。默认情况不开,因为会增加部署复杂度,但架构上已经预留了位置。

2.4 记忆模型:短期摘要和长期事实分开管

记忆不是简单地把聊天记录塞进数据库。我设计了两类记忆:短期对话摘要和长期事实记录。

短期摘要用于捕获一段对话的轮廓,比如“这次对话确定了项目采用模块化架构,讨论了登录模块的授权方案”。它的特点是更新频繁,随着对话推进不断被覆盖和压缩。长期事实则是一些稳定的结论和用户偏好,比如“用户偏好Python,前端用Vue,数据库用PostgreSQL”,在每次新对话开始时都应该注入给模型。

区分这两类的意义在于写入策略不同:短期摘要走滚动更新,窗口内的东西会随对话生成不断重写;长期事实走去重和冲突覆盖,不会因为同一个话题聊了三次就出现三条重复记录。这个设计直接避免了记忆库变成垃圾场。

3. 核心实现与接入实操

3.1 项目结构一览

把MemTether拆开看,核心文件不多:

memtether/ ├── config.yaml # 服务端配置 ├── store.py # SQLite存储与检索 ├── server.py # HTTP API服务 ├── mcp_server.py # MCP服务 ├── proxy.py # OpenAI兼容代理 └── requirements.txt # fastapi, uvicorn, mcp, openai

整体不到一千行代码,跑起来很轻。下面我挑关键部分讲实现。

3.2 搭建记忆存储与检索服务

先看store.py的核心逻辑。我对记忆条的字段设计是:type区分short_summary/long_term,content是正文,source标记来自哪个客户端,importance表示重要程度,created_at和updated_at用于时间衰减。

# store.py import sqlite3 import json import time class MemoryStore: def __init__(self, db_path="memtether.db"): self.conn = sqlite3.connect(db_path, check_same_thread=False) self.conn.row_factory = sqlite3.Row self.conn.execute( """ CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, content TEXT NOT NULL, source TEXT NOT NULL, importance INTEGER DEFAULT 1, created_at REAL, updated_at REAL ) """ ) self.conn.commit() def write(self, type_, content, source, importance=1): ts = time.time() # 同一来源同一类型下,完全重复的内容只更新时间 cur = self.conn.execute( "SELECT id FROM memories WHERE source=? AND type=? AND content=?", (source, type_, content), ) row = cur.fetchone() if row: self.conn.execute( "UPDATE memories SET updated_at=?, importance=? WHERE id=?", (ts, importance, row["id"]), ) self.conn.commit() return row["id"] self.conn.execute( """ INSERT INTO memories (type, content, source, importance, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) """, (type_, content, source, importance, ts, ts), ) self.conn.commit() return self.conn.execute("SELECT last_insert_rowid()").fetchone()[0] def search(self, keyword, limit=10): # 进阶版本可以换成 sqlite-vec 或外部向量检索 cur = self.conn.execute( """ SELECT * FROM memories WHERE content LIKE ? ORDER BY updated_at DESC LIMIT ? """, (f"%{keyword}%", limit), ) return [dict(r) for r in cur.fetchall()]

这里有个容易忽略的细节:SQLite默认不支持并发写,而多个客户端可能同时写入记忆。我通过check_same_thread=False加上短暂的操作窗口来规避大部分并发问题,实际操作中压力远没有想象中大,因为AI客户端的请求频率本身不会特别高。如果你预期会有大量并发,建议加上一个简单的内存队列写锁。

HTTP API层用FastAPI实现,提供两个核心端点:

# server.py from fastapi import FastAPI from pydantic import BaseModel from store import MemoryStore app = FastAPI() store = MemoryStore() class MemIn(BaseModel): type: str content: str source: str importance: int = 1 class MemQuery(BaseModel): keyword: str limit: int = 10 @app.post("/memories/write") def write_mem(mem: MemIn): mid = store.write(mem.type, mem.content, mem.source, mem.importance) return {"id": mid, "status": "ok"} @app.post("/memories/search") def search_mem(query: MemQuery): results = store.search(query.keyword, query.limit) return {"results": results}

启动服务很简单:

uvicorn server:app --host 127.0.0.1 --port 8765

3.3 通过MCP接入客户端

MCP服务端我用的是FastMCP这个SDK,写起来很顺:

# mcp_server.py import json from mcp.server.fastmcp import FastMCP from store import MemoryStore mcp = FastMCP("MemTether") store = MemoryStore() @mcp.tool() def mem_write(type: str, content: str, source: str) -> str: """写入一条记忆,type可选short_summary或long_term""" return str(store.write(type, content, source)) @mcp.tool() def mem_search(keyword: str, limit: int = 10) -> str: """按关键词搜索记忆""" return json.dumps(store.search(keyword, limit), ensure_ascii=False) if __name__ == "__main__": mcp.run(transport="stdio")

在Claude Desktop里接入只需要编辑客户端的MCP配置文件:

{ "mcpServers": { "memtether": { "command": "python", "args": ["mcp_server.py"], "cwd": "/path/to/memtether" } } }

保存后重启客户端,模型就能通过mem_write和mem_search这两个工具来读写共享记忆了。我实测的效果是:在ChatGPT里讨论完的结论,可以手动写进记忆;切到Claude后,它会自己调用mem_search把相关上下文捞出来,然后基于这些内容继续回答。

3.4 给不支持MCP的客户端做兼容代理

考虑到还有很多客户端不支持MCP,我写了一个轻量代理proxy.py,逻辑不复杂:接受OpenAI格式的请求,先从记忆库查询相关内容,注入到system prompt,再转发给真正的模型服务。

# proxy.py import httpx from fastapi import FastAPI from pydantic import BaseModel from store import MemoryStore app = FastAPI() store = MemoryStore() UPSTREAM_URL = "https://api.example.com/v1/chat/completions" API_KEY = "your-api-key" class ChatRequest(BaseModel): model: str messages: list @app.post("/v1/chat/completions") async def chat(req: ChatRequest): # 从用户问题里提取一个查询词,这里先用最后一个用户文本 user_text = req.messages[-1].get("content", "") memories = store.search(user_text[:30], limit=8) memory_block = "" for m in memories: memory_block += f"- [{m['source']}] {m['content']}\n" if memory_block: sys_prompt = "以下是历史记忆,请在回答时优先参考:\n" + memory_block req.messages.insert(0, {"role": "system", "content": sys_prompt}) async with httpx.AsyncClient() as client: resp = await client.post( UPSTREAM_URL, headers={"Authorization": f"Bearer {API_KEY}"}, json=req.model_dump(), ) return resp.json()

这样,任何允许你自定义base_url的客户端,把地址指向http://127.0.0.1:8000/v1,就能自动享受记忆注入。

4. 记忆管理机制:写入、检索与更新

4.1 写入策略:让模型只记值得记的东西

刚开始我让模型把每轮对话都写入记忆,结果记忆库三五天就变成了一堆废话。后来改成:只有在对话中出现明确结论、偏好或者关键事实时才触发写入,原始聊天记录不直接入库。

实操上有两条经验。第一,短期摘要要定期压缩,比如每5轮对话把之前的摘要和新增内容重新合成一条新摘要,旧摘要标记为过期;第二,长期事实写入前要做归一化,比如“我用Python”“我在用python”应该合并成“用户使用Python”。归一化可以靠模型做,也可以在记忆服务里做规则匹配。

4.2 检索策略:关键词、时间衰减和语义搜索

检索直接影响记忆注入的质量。我用的是三级策略:

第一级是关键词匹配,就是上面store.search的实现,简单直接。第二级是时间衰减加权,近期更新的记忆权重更高。第三级是语义检索,需要把内容向量化后按余弦相似度召回。

时间衰减的实现也不复杂,在SQL里给updated_at加一个衰减系数即可:

SELECT * FROM memories WHERE content LIKE ? ORDER BY (importance * 0.5 + 1.0 / (strftime('%s','now') - updated_at + 1) * 3600) DESC LIMIT ?

这个公式的含义是:重要程度和新鲜度各占一部分,重要性高的记忆不会因为时间久就被甩出召回列表,同时最新写入的内容依然靠前。

4.3 冲突与覆盖:同一事实不同说法怎么处理

多个客户端反复写入同一类事实,很容易产生冲突。我的做法是:以source+type+content三者组合判断是否重复;对长期事实,允许设置“主题键”user_id+topic,同一主题下新写入的结论直接覆盖旧结论。

举个例子,用户昨天在ChatGPT里说“数据库用PostgreSQL”,今天在Claude里改成“数据库计划迁移到TiDB”。这两条记忆如果不做覆盖,就会同时存在,模型检索时不知道该听谁的。我在写入接口里加了一个可选参数topic,当topic相同时,新的长期事实写入会把旧记录标记为superseded,检索时默认排除。这样记忆库保持单一事实源。

4.4 权限与隐私隔离

记忆服务集中存放内容,隐私必须考虑。我默认做了几层隔离:一是监听地址默认127.0.0.1,不接受外部网络访问;二是接入需要token,调用API时必须带Authorization头;三是支持按namespace隔离,不同项目、不同用户的记忆物理存在同一个库里,但查询时通过namespace字段过滤。

如果你需要多人共用同一个记忆服务,我建议每个用户一个独立SQLite文件,而不是共用一张表。这样即使出错也不会串数据。

5. 实战中的坑与排查实录

5.1 常见问题速查表

现象原因解决办法
MCP工具在客户端里不出现MCP server没启动,或路径配置错误先手动执行mcp_server.py,确认stdio启动无报错
记忆检索返回空结果关键词过于宽泛,或写入时source不一致查看库里是否存在该条记录,调整检索关键词
中文乱码SQLite连接默认编码问题写入时统一用UTF-8,连接后执行PRAGMA encoding = "UTF-8"
记忆重复堆积缺少去重逻辑升级到支持topic覆盖的版本,或手动清理库
系统提示词被记忆撑爆检索结果过多把limit从10调低到5,并做内容截断
代理接入后模型行为异常记忆注入位置不对确认system prompt放在messages最前面

5.2 三个现场排查案例

第一个案例,MCP工具在客户端里始终不显示。查了一会儿才发现是cwd路径写错了,客户端启动子进程时找不到store.py模块,直接报错。解决方法是把MCP服务的启动脚本改成绝对路径,并且在配置里加上"env": {"PYTHONPATH": "/path/to/memtether"}。

第二个案例,记忆检索总是返回过时结果。因为我原本只用updated_at排序,但有些旧记忆的重要程度很高,反而被新写入的琐碎记录压下去了。后来我在排序公式里加大了importance的权重,才让重要结论保持稳定可见。

第三个案例,同时接入三个客户端后,数据库出现了偶尔的locked错误。原因是FastAPI的多线程并发写入SQLite。解决方法是给write操作加了一个threading.Lock,把写入串行化。实测在个人使用场景下完全够用。

5.3 一些值得单拎出来的小经验

日志一定要打。我在server.py里给每次读写都加了一条结构化日志,记录source、type、耗时。出了问题翻日志能快速定位是哪个客户端导致的异常写入。

还有一个经验:不要一股脑把所有历史对话都变成记忆。记忆的价值在于提炼,不在于存储量。我大约每两三天会执行一次清理,把短期摘要里已经不再重要的条目删掉,长期事实里重复的内容合并。清理我用的是一个定时脚本,调用模型对旧记忆做一次摘要合并,效果比手动处理省心很多。

6. 对这个项目的复盘与后续打算

MemTether现在已经稳定跑在我一台迷你主机上,所有常用客户端都指向它。和之前的体验对比,最明显的变化是我在切换客户端时不用再花几分钟重新铺垫上下文,直接说“你记得我们上次聊的那个方案吗”就能继续。跨客户端的连续性带来的效率提升,比换一个更强的模型更直观。

复盘来看,这个项目的核心不在于代码量,而在于把“记忆”这件事从各个客户端手里捞出来,变成一个标准化的、可共享的基础设施。后续我打算加两块:一块是用向量检索替换关键词检索,让模糊语义的召回更准;另一块是做一个简单的Web管理面板,可以直接在浏览器里查看、删除、合并记忆条目。如果你也被多客户端记忆割裂的问题折磨,可以直接去仓库clone一份跑起来试试,跑通了记得来跟我反馈你的使用场景。我现在的日常就是靠这个小工具串起所有AI工具的,它已经是我工作流里离不开的一环了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 11:33:04

SIwave TDR仿真实战:精准定位PCB阻抗突变点

1. 为什么S参数不够用:从一次真实的调试翻车说起 刚入行那几年,我特别迷信S参数。板子打回来,往网络分析仪上一挂,S11看着挺漂亮,回波损耗在-20dB以下,插损曲线也平滑,心里就觉得稳了。结果板子…

作者头像 李华
网站建设 2026/10/6 11:33:03

阿里云SLB、ECS、OSS、RDS四件套迁移实战:顺序、命令与避坑指南

简介:这份文档面向云计算运维、后端开发与架构入门人员,围绕阿里云SLB、ECS、OSS、RDS四大核心服务,梳理各服务的概念体系与在系统数据迁移中的实际应用,帮助读者建立从负载均衡、云服务器到对象存储与关系数据库的整体认知。资源…

作者头像 李华
网站建设 2026/10/6 11:32:48

DeepSeek Harness桌面端实测:技能管理、内网部署与代码回退全指南

等了很久,DeepSeek Harness 官方桌面端总算是落地了。如果你一直在折腾 DeepSeek 的编码工作流,应该知道 Harness 这个项目的定位:它不是套壳聊天窗,而是一个把 DeepSeek 能力、本地技能包(Skills)、提示词…

作者头像 李华
网站建设 2026/10/6 11:32:13

OSPF排障必备:RFC2328中文版LSA与邻居状态机详解

简介:RFC2328中文版是一份面向网络工程师、运维人员和路由协议初学者的OSPF v2协议技术文档,系统梳理了链路状态路由的运作机制,可帮助读者理解RFC2328标准在实际组网中的设计与应用。整份资料仅含1个PDF文件,压缩包大小1.93MB&am…

作者头像 李华
网站建设 2026/10/6 11:30:33

开源组件搭建轻型AI中台,解决重复录入与对账难题

上周被一个做供应链的朋友问到:“你们上了那么多系统,怎么业务员还在对着Excel来回填数?月底财务对账还是靠人工怼流水?”这个问题我太熟了。绝大多数企业上了CRM、ERP、OA、财务系统,结果信息孤岛越垒越多&#xff0c…

作者头像 李华
网站建设 2026/10/6 11:30:00

2026年9月GitHub热榜:从生活管理到具身智能的开源项目实战拆解

每个月末我都会抽一个晚上,把当月GitHub热榜整个翻一遍,不为别的,就为了看看开源世界最近在往哪个方向走。2026年9月的榜单信息量很大,既有像howtolivebetter这样把生活管理做成工程化方案的项目,也有champ teleop这种…

作者头像 李华