构建跨AI助手的通用记忆层:从向量检索到浏览器扩展实践
1. 项目概述:一个被归档的浏览器记忆层工具
如果你和我一样,经常在ChatGPT、Claude、Perplexity这些不同的AI助手之间切换,肯定会遇到一个共同的烦恼:每次对话都像是第一次见面。你需要在每个新对话里重复介绍自己是谁、你的项目背景是什么、你之前讨论过哪些关键点。这不仅效率低下,也让AI助手的“智能”体验大打折扣,因为它们缺乏一个持续、连贯的“记忆”。
今天要聊的这个项目——mem0-chrome-extension,正是为了解决这个问题而生的。它的核心目标,是构建一个“通用记忆层”(Universal Memory Layer)。简单来说,它就像给你的所有AI助手装上一个共享的“外置大脑”。无论你在哪个平台的聊天窗口,这个浏览器扩展都能在后台默默工作,自动从你的对话中提取关键信息(比如你的职业、项目细节、偏好设定),并将这些“记忆”存储起来。当你在另一个AI助手那里开启新对话时,它又能智能地检索出相关的记忆,并自动注入到上下文中,让AI“记得”你之前说过什么。
这个想法非常棒,它直击了当前多AI工具并用的核心痛点。然而,正如项目仓库顶部那个醒目的警告框所示,这个Chrome扩展项目目前已经被归档(Archived),不再被官方主动维护。这意味着,虽然它的代码开源(MIT协议),你可以自由地 fork 和二次开发,但不会再收到官方的功能更新或 bug 修复。项目团队将重心转向了他们的核心产品 Mem0 。对于我们这些技术爱好者或开发者来说,这既是一个遗憾,也是一个机会。我们可以深入剖析这个项目的设计思路、技术实现,理解它如何工作,并思考如何借鉴其理念,甚至基于它构建更符合自己需求的自托管方案。
2. 核心设计思路与技术架构解析
2.1 什么是“跨LLM记忆层”?
在深入代码之前,我们得先搞清楚它要解决的根本问题。当前的AI大语言模型(LLM),无论是ChatGPT、Claude还是其他,本质上都是“无状态”的。你提供给模型的对话历史,就是它全部的“记忆”。一旦对话窗口关闭或上下文长度耗尽,模型关于这次对话的一切“记忆”就消失了。虽然一些平台(如ChatGPT)推出了官方的“记忆”功能,但它们是孤立的、平台绑定的。
mem0-chrome-extension 的野心在于打破这种孤岛。它的设计思路可以概括为:“一个中心化的记忆存储服务 + 一个分布式的浏览器端采集与注入代理”。
- 记忆的采集(Capture):扩展程序需要监听你在不同AI助手网页(如chat.openai.com, claude.ai, perplexity.ai)上的对话。它不能简单粗暴地截取所有文本,那样会包含大量无关信息。它需要智能地判断哪些是“值得记忆”的用户输入和AI回复,比如包含具体事实、个人偏好、任务指令的语句。
- 记忆的存储与向量化(Store & Embed):采集到的文本片段被发送到后端的Mem0 API服务。该服务的核心任务是将这些文本转换成“向量”(即一组数字),这个过程叫做“嵌入”(Embedding)。语义相近的文本,其向量在数学空间中的距离也更近。这些向量连同原始文本,被存储到专门的向量数据库中。
- 记忆的检索(Retrieve):当你在某个AI助手开始新对话或输入新消息时,扩展程序会将当前对话的上下文(或你的新问题)也发送给Mem0 API。API服务将其同样转换为向量,然后在向量数据库中进行相似度搜索,找出与你当前语境最相关的几条历史“记忆”。
- 记忆的注入(Inject):检索到的相关记忆文本,被扩展程序巧妙地“注入”到你当前对话的上下文中。对于ChatGPT或Perplexity,它可能模拟“按回车”发送一条预设的系统提示;对于Claude,它可能通过点击一个按钮来触发。这样,AI模型在生成回复时,就能“看到”这些来自过去的上下文,从而实现“记忆”效果。
2.2 项目技术栈与模块拆解
虽然项目已归档,但其技术选型依然值得学习。作为一个Chrome扩展,它必然包含以下核心部分:
- 扩展基础结构(Manifest V3):项目根目录的
manifest.json文件是扩展的“身份证”和“说明书”,定义了扩展的名称、权限、后台脚本、内容脚本、弹出页面等。Mem0扩展需要申请诸如activeTab、storage以及访问特定网站(如*://chat.openai.com/*)的权限,以便与AI助手页面交互。 - 内容脚本(Content Scripts):这是与网页交互的核心。扩展会向ChatGPT、Claude等目标网站注入特定的JavaScript脚本。这些脚本运行在网页的上下文中,可以访问DOM,监听用户的输入事件、消息发送事件,从而捕获对话内容。同时,它们也负责在页面上添加Mem0的交互按钮(如Claude页面上的那个按钮),并处理点击事件。
- 后台服务脚本(Background Service Worker):在Manifest V3中,后台页面被服务脚本(Service Worker)取代。它负责协调整个扩展的工作:管理用户登录状态(如Google Sign-In)、与Mem0 API服务器通信、处理从内容脚本发来的消息(如“捕获到一条新消息”、“请求检索相关记忆”)、以及将检索到的记忆发送回对应的内容脚本。
- 弹出页面(Popup):点击工具栏图标后出现的小窗口。通常用于展示快速设置、登录状态、记忆概览(Dashboard)或手动触发同步操作。
- 构建与开发流程:项目使用
npm进行依赖管理和构建。package.json文件里定义了开发脚本(如npm run build)。构建过程很可能使用了像webpack或vite这样的打包工具,将源代码(可能是TypeScript)编译、打包、压缩,输出到dist目录,以供Chrome加载。
注意:由于项目已归档,直接使用其预编译的
dist文件或按照原有流程构建,可能会遇到依赖过时、API接口变更或兼容性问题。将其作为一个学习案例或二次开发的起点更为合适。
2.3 记忆处理的核心逻辑猜想
尽管我们无法直接运行其后端服务,但通过扩展的行为,可以推断Mem0 API的核心工作流程:
- 文本分块与清洗:从对话中捕获的原始文本可能很长且杂乱。API首先需要对其进行清洗(去除无关HTML标签、代码块标记等),并切割成大小合适的“文本块”(Chunking)。分块策略(如按句子、按段落、按固定长度重叠滑动窗口)直接影响记忆检索的质量。
- 嵌入模型(Embedding Model):这是记忆系统的“大脑”。Mem0很可能使用了如OpenAI的
text-embedding-3-small、Cohere的嵌入模型或开源的all-MiniLM-L6-v2等模型,将文本块转换为高维向量。模型的选择决定了系统对语义理解的深度和精度。 - 向量数据库(Vector Database):存储和快速检索海量向量的关键。常见的选择有Pinecone、Weaviate、Qdrant,或者使用PGVector插件的关系型数据库。当用户查询时,系统计算查询向量的嵌入,并使用近似最近邻(ANN)算法在数据库中快速找到最相似的几个向量(即相关的记忆文本块)。
- 记忆的“相关性”与“新鲜度”:一个好的记忆系统不能只返回“最相似”的记忆,还要考虑记忆的“新鲜度”(最近使用的记忆可能更重要)和“重要性”(用户明确标记或多次提及的信息可能更重要)。这需要在检索算法中加入时间衰减因子或重要性权重。
3. 从零开始:手动构建与部署指南
既然原项目不再维护,如果我们想拥有一个类似的功能,最可靠的路径就是理解其原理后,自行搭建一个简化版或寻找替代方案。下面我将提供一个基于开源工具链的、可自托管的实现思路。
3.1 环境准备与工具选型
我们将构建一个最小可行系统,包含一个简单的后端API和一个修改版的浏览器扩展。
后端API(替代Mem0 API):
- 语言:Python,因其在AI生态中库丰富。
- 框架:FastAPI,轻量且异步支持好,适合构建API。
- 嵌入模型:选用Hugging Face 上的开源句子转换模型,如
sentence-transformers/all-MiniLM-L6-v2。它体积小、速度快、效果不错,且可离线运行,无需OpenAI API密钥和费用。 - 向量数据库:为了简化,我们使用ChromaDB。它是一个轻量级、易嵌入的向量数据库,可以直接集成在Python应用中,无需单独部署服务器。
- 存储:用户的记忆文本和向量将保存在本地目录(由ChromaDB管理)。
浏览器扩展(修改版):
- 我们将以原mem0扩展为蓝本,修改其通信逻辑,使其指向我们自建的API。
- 需要修改的核心文件是负责与后端通信的JavaScript模块(通常位于
src/services/api.js或类似位置)。
3.2 后端API服务搭建步骤
首先,搭建我们的记忆服务器。
# 1. 创建项目目录并初始化 mkdir my-mem0-server && cd my-mem0-server python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install fastapi uvicorn sentence-transformers chromadb pydantic # 2. 创建主应用文件 app.pyapp.py内容如下:
from fastapi import FastAPI, HTTPException, Header from pydantic import BaseModel from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings import uuid from datetime import datetime from typing import List, Optional app = FastAPI(title="My Mem0 API") # 初始化模型和数据库 print("Loading embedding model...") embedding_model = SentenceTransformer('all-MiniLM-L6-v2') print("Model loaded.") # 使用持久化存储,数据会保存在 `./chroma_data` 目录 chroma_client = chromadb.PersistentClient(path="./chroma_data") # 获取或创建一个以用户ID命名的集合(Collection),相当于每个用户的独立记忆库 def get_user_collection(user_id: str): collection_name = f"memories_{user_id}" try: collection = chroma_client.get_collection(name=collection_name) except: # 如果不存在则创建,指定我们使用的嵌入模型维度(384 对于 all-MiniLM-L6-v2) collection = chroma_client.create_collection(name=collection_name, metadata={"hnsw:space": "cosine"}) return collection # 数据模型定义 class MemoryItem(BaseModel): text: str source: Optional[str] = "web" # 来源,如 “chatgpt”, “claude” metadata: Optional[dict] = {} # 可存放时间戳、对话ID等 class QueryItem(BaseModel): text: str user_id: str top_k: Optional[int] = 5 # 返回最相关的几条记忆 # API端点1:存储记忆 @app.post("/api/memory") async def store_memory(item: MemoryItem, user_id: str = Header(...)): """接收一段文本,生成向量并存储到对应用户的记忆库""" if not item.text.strip(): raise HTTPException(status_code=400, detail="Text cannot be empty") collection = get_user_collection(user_id) # 生成文本向量 embedding = embedding_model.encode(item.text).tolist() # 生成唯一ID memory_id = str(uuid.uuid4()) # 准备元数据 metadata = item.metadata.copy() metadata.update({ "source": item.source, "timestamp": datetime.utcnow().isoformat(), "text_preview": item.text[:100] # 存储预览便于调试 }) # 添加到集合 collection.add( embeddings=[embedding], documents=[item.text], metadatas=[metadata], ids=[memory_id] ) return {"status": "success", "id": memory_id, "message": "Memory stored."} # API端点2:检索相关记忆 @app.post("/api/query") async def query_memory(query: QueryItem): """根据查询文本,检索对应用户记忆库中最相关的记忆""" collection = get_user_collection(query.user_id) # 将查询文本转换为向量 query_embedding = embedding_model.encode(query.text).tolist() # 在集合中查询 results = collection.query( query_embeddings=[query_embedding], n_results=query.top_k ) # 整理返回结果 memories = [] if results['documents']: for i, doc in enumerate(results['documents'][0]): memories.append({ "text": doc, "metadata": results['metadatas'][0][i], "distance": results['distances'][0][i] # 距离越小越相关 }) return {"query": query.text, "memories": memories} @app.get("/") async def root(): return {"message": "My Mem0 API is running."}关键点解释:
SentenceTransformer加载本地模型,首次运行会从Hugging Face下载,之后离线使用。ChromaDB以持久化模式运行,所有数据保存在本地chroma_data目录,不同用户的记忆通过不同的Collection隔离。user_id通过HTTP请求头传递,这是一个简单的身份验证方式。在实际产品中,你需要更安全的认证(如JWT)。/api/memory接口用于存储记忆,/api/query用于检索记忆。
启动服务:
uvicorn app:app --reload --host 0.0.0.0 --port 8000现在,你的本地记忆API就在http://localhost:8000运行了。你可以用curl或 Postman 测试:
# 存储记忆 curl -X POST "http://localhost:8000/api/memory" \ -H "Content-Type: application/json" \ -H "user_id: alice" \ -d '{"text": "I am a software engineer working on a Python backend project.", "source": "chatgpt"}' # 检索记忆 curl -X POST "http://localhost:8000/api/query" \ -H "Content-Type: application/json" \ -d '{"text": "What do I do for work?", "user_id": "alice", "top_k": 3}'3.3 修改浏览器扩展以对接自建API
接下来,我们需要修改原mem0扩展,让它与我们刚搭建的API对话。
获取并解构原扩展代码:
git clone https://github.com/mem0ai/mem0-chrome-extension.git cd mem0-chrome-extension浏览代码结构,找到网络请求模块。通常会在
src目录下,比如src/lib/api.ts或src/services/mem0Service.js。关键修改点:找到所有向
https://api.mem0.ai或类似域名发起请求的代码。你需要将其替换为你本地或你部署的后端地址,例如http://localhost:8000。同时,修改请求的数据格式,使其符合我们自定义的API接口。- 原代码可能类似:
async function storeMemory(text, source) { const response = await fetch('https://api.mem0.ai/v1/memories', { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ text, source }) }); return response.json(); } - 修改后应类似:
async function storeMemory(text, source) { // 假设我们从扩展的存储中获取当前用户的ID const { userId } = await chrome.storage.local.get(['userId']); const response = await fetch('http://localhost:8000/api/memory', { method: 'POST', headers: { 'Content-Type': 'application/json', 'user_id': userId || 'default-user' // 传递用户ID }, body: JSON.stringify({ text, source }) }); return response.json(); } - 同样,修改记忆查询函数,指向
http://localhost:8000/api/query。
- 原代码可能类似:
处理用户身份:原扩展使用Google登录,我们简化处理。可以在扩展弹出页面(Popup)增加一个输入框,让用户手动设置一个唯一的
userId,并将其保存在chrome.storage.local中。所有后续请求都携带这个userId。更新构建与加载:
- 运行
npm install和npm run build(确保Node.js环境)。 - 打开Chrome的
chrome://extensions,开启“开发者模式”。 - 点击“加载已解压的扩展程序”,选择修改后项目下的
dist目录。
- 运行
重要提示:自建API使用
http://localhost,Chrome扩展出于安全限制(CORS策略),可能需要你在后端API中配置正确的CORS头,或者在启动Chrome时添加--disable-web-security标志(仅限开发测试,极不安全)。生产环境必须使用HTTPS并正确配置CORS。例如在FastAPI中,可以添加CORSMiddleware。
4. 深入实操:内容脚本与页面交互的细节
要让扩展在第三方网页上工作,内容脚本(Content Script)是关键。它需要精准地识别页面结构,并与之交互。
4.1 监听与捕获对话消息
以ChatGPT网页为例,其对话消息通常被包裹在特定的DOM元素中,可能带有>// content-script.js (function() { 'use strict'; // 1. 监听DOM变化,因为聊天消息是动态加载的 const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length) { mutation.addedNodes.forEach((node) => { // 检查新添加的节点是否是消息元素 if (node.nodeType === 1 && node.matches('[data-message-author-role="user"]')) { const userMessageText = extractTextFromNode(node); if (userMessageText) { // 发送到后台脚本,请求存储记忆 chrome.runtime.sendMessage({ type: 'CAPTURE_MEMORY', data: { text: userMessageText, source: 'chatgpt' } }); } } }); } } }); // 开始观察整个body的变化 observer.observe(document.body, { childList: true, subtree: true }); // 2. 辅助函数:从复杂的DOM节点中提取纯净的文本 function extractTextFromNode(node) { // 克隆节点以避免修改原DOM const clone = node.cloneNode(true); // 移除可能存在的按钮、代码块工具栏等无关元素 clone.querySelectorAll('button, .code-block-toolbar, .copy-button').forEach(el => el.remove()); // 返回文本内容,并清理多余空白 return clone.textContent?.trim().replace(/\s+/g, ' '); } // 3. 接收来自后台脚本的指令,例如注入检索到的记忆 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === 'INJECT_MEMORIES') { const memories = request.data.memories; const combinedText = memories.map(m => m.text).join('\n'); // 将记忆文本填入输入框,并模拟发送(这需要非常谨慎,且因网站而异) // injectIntoInputBox(combinedText); console.log('Memories to inject:', memories); } }); })();
实操心得:
- 选择器策略:网页结构会频繁更新。不能依赖固定的CSS选择器。更稳健的方法是寻找相对稳定的数据属性(如
>问题现象可能原因 排查步骤与解决方案 扩展图标不显示或无法点击 1. 扩展未成功加载。
2.manifest.json配置错误。
3. 后台服务脚本崩溃。1. 打开 chrome://extensions,检查扩展是否已启用,是否有错误信息。
2. 检查浏览器控制台(Console)是否有扩展相关的错误。
3. 检查后台脚本(Service Worker)的控制台:在chrome://extensions页面,点击对应扩展的“背景页”链接。内容脚本未在目标网站生效 1. manifest.json中的content_scriptsmatches模式未覆盖目标URL。
2. 脚本注入失败。1. 确认目标网站的完整URL是否匹配 matches模式(如*://chat.openai.com/*)。
2. 在目标网站按F12打开开发者工具,在“Sources”标签页的“Content scripts”部分查看你的脚本是否已加载。无法与本地API通信(CORS错误) 浏览器因同源策略阻止了跨域请求。 1.后端解决:在FastAPI应用中添加CORS中间件(见下文代码)。
2.开发临时方案:使用webpack-dev-server代理请求,或使用允许CORS的浏览器扩展(如Moesif CORS)。
生产环境必须使用方案1。记忆检索不准确或无关 1. 嵌入模型不适合你的文本类型。
2. 文本分块策略不佳。
3. 查询文本本身不明确。1. 尝试不同的开源嵌入模型(如 paraphrase-multilingual-MiniLM-L12-v2对多语言支持更好)。
2. 调整分块大小和重叠度。对于对话,可以尝试按“问答对”作为一个块。
3. 在查询时,可以尝试将当前对话的最后几条消息一起作为查询文本,以提供更丰富的上下文。FastAPI CORS 配置示例(添加到
app.py开头):from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["chrome-extension://你的扩展ID"], # 生产环境应指定确切来源 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )获取你的扩展ID:在
chrome://extensions页面,找到你的扩展,其URL类似chrome-extension://abcdefghijklmnopqrstuvwxyz123456/,其中的字符串就是ID。5.2 项目优化与功能扩展思路
虽然基础版本已经能工作,但一个健壮、好用的系统还需要更多打磨:
- 记忆的更新与去重:当前方案只是不断添加记忆。现实中,信息会过时或重复。需要实现记忆的更新(用新信息覆盖旧信息)和去重(基于向量相似度,避免存储几乎相同的记忆)。
- 记忆的分类与打标:允许用户手动或自动(通过LLM总结)为记忆打上标签,如“工作”、“个人”、“学习”、“项目A”,便于更精细的检索和管理。
- 前端交互增强:在AI助手的输入框附近添加一个不显眼的图标,点击后可以预览即将被注入的记忆,并允许用户手动选择或编辑,给予用户完全的控制权。
- 支持更多AI平台:除了ChatGPT、Claude、Perplexity,还可以适配Gemini、Coze、国内的大模型平台等。这需要为每个平台编写特定的内容脚本,以适配其独特的页面结构。
- 离线模式探索:将嵌入模型和向量数据库全部集成到扩展中,使用浏览器的
IndexedDB存储向量。这样所有数据处理完全在本地进行,隐私性最强,但会受限于浏览器的存储空间和计算能力。
这个被归档的mem0-chrome-extension项目,为我们提供了一个绝佳的“跨AI记忆”概念验证。通过拆解其原理并动手搭建一个简化版本,我们不仅获得了完全掌控自己数据的隐私优势,也更深刻地理解了构建此类智能工具的技术细节与挑战。技术的演进往往如此,一个项目的终点,可能是更多人自定义起点。
