1. 为什么我们要自己动手做本地 AI 记忆系统
做 AI Agent 开发的人都有一个共同的痛点:每次对话结束,上下文一清空,Agent 就像失忆了一样。你昨天跟它聊了两个小时的项目架构,今天再打开,它完全不记得你是谁、在做什么。这不是模型能力的问题,而是记忆层缺失的问题。
市面上做记忆的方案我基本都试过一遍。云端方案比如各种 Memory API,延迟高、数据不在自己手里、按调用量收费,长期跑下来成本不低。纯本地方案呢,要么是简单的向量数据库加检索,要么是粗暴地把历史对话塞进上下文窗口,前者检索精度差,后者 token 消耗爆炸。我们想要的是一个真正能在本地跑起来、有结构化记忆管理、能跟 Agent 框架无缝对接的记忆系统。
这个项目的核心目标很明确:在本地环境构建一套完整的 AI 记忆系统,支持记忆的写入、检索、更新、遗忘,并且通过 MCP 协议暴露给上层 Agent 调用。说白了,就是给 Agent 装一个"海马体"。
适合谁来参考这篇内容?如果你正在做 Agent 开发、对 MCP 协议感兴趣、或者单纯想给自己的本地 AI 助手加一个长期记忆能力,这篇东西应该能帮你少走一些弯路。我们目前是在找技术合伙人一起推进,所以下面会把技术选型、架构设计、实操细节都摊开来讲,方便判断方向是否匹配。
2. 记忆系统的整体架构设计思路
2.1 为什么选 MCP 作为对外接口
MCP(Model Context Protocol)是 Anthropic 推出的一个开放协议,本质上是一个软件协议,用来标准化 AI 模型与外部工具、数据源之间的交互方式。很多人第一次听到 MCP 会跟硬件协议搞混,其实它跟 LSP(Language Server Protocol)是同一类东西——定义了一套通信规范,让不同的客户端和服务端能互相理解。
我们选 MCP 作为记忆系统的对外接口,理由有三个:
第一,解耦。记忆系统不应该绑定任何一个 Agent 框架。今天你用某个框架,明天换一个,记忆层不应该跟着重写。MCP 提供了一层标准化的抽象,任何支持 MCP 的客户端都能直接调用。
第二,生态兼容。现在主流工具都在往 MCP 上靠,比如 Codex 可以接入各种 MCP Server,Dify 也支持浏览器 MCP,Hermes Agent 同样有 MCP 接入能力。我们把记忆系统做成 MCP Server,等于天然接入了整个生态。
第三,本地优先。MCP 支持 stdio 和 SSE 两种传输方式,stdio 模式下所有数据都在本地进程间流转,不需要网络暴露,这对隐私敏感的场景非常关键。
2.2 记忆的分层模型
我们借鉴了认知科学里人类记忆的分类方式,把 AI 记忆分成三层:
| 记忆类型 | 对应人类记忆 | 存储周期 | 典型内容 |
|---|---|---|---|
| 工作记忆 | 短期记忆 | 单次会话 | 当前对话上下文、临时变量 |
| 情景记忆 | 长期记忆 | 持久化 | 历史对话摘要、事件记录 |
| 语义记忆 | 知识记忆 | 持久化 | 用户偏好、事实性知识、实体关系 |
工作记忆其实就是上下文窗口本身,不需要额外存储。真正需要系统管理的是情景记忆和语义记忆。情景记忆回答的是"发生了什么",语义记忆回答的是"我知道什么"。这两者的检索策略、更新频率、衰减机制都不一样。
2.3 存储层的选型考量
存储层我们最终选了SQLite + 向量扩展的组合,而不是一上来就上专业的向量数据库。原因很实际:
- SQLite 零配置、单文件、跨平台,本地部署没有任何额外依赖
- 通过 sqlite-vec 或 sqlite-vss 扩展可以获得向量检索能力
- 结构化数据(实体、关系、时间戳)和向量数据放在同一个文件里,事务一致性有保障
- 备份就是复制一个文件,迁移成本极低
如果后期数据量上来了,可以平滑迁移到 Qdrant 或 Milvus,因为我们的存储层做了抽象接口,切换只需要改配置。
提示:不要一上来就追求"最强"的向量数据库。本地场景下,数据量通常在几万到几十万条记忆之间,SQLite 加向量扩展完全够用,运维复杂度低得多。
3. 核心模块拆解与关键技术点
3.1 记忆写入管道
记忆写入不是简单地把对话存下来就完事。原始对话里充斥着大量噪音——寒暄、重复、无关信息。如果全部存进去,检索时会被大量低质量记忆淹没。
我们的写入管道分四步:
第一步,分块(Chunking)。把长对话按语义边界切分成记忆单元。这里不能用固定长度切分,否则会把一个完整的意图切碎。我们用的是基于对话轮次和语义相似度的混合切分策略:先按对话轮次切,然后计算相邻轮次的 embedding 相似度,相似度低于阈值的作为切分点。
第二步,摘要(Summarization)。每个记忆单元生成一个简短摘要,用于后续的快速检索和展示。摘要用本地小模型跑,比如 Qwen2.5-3B 这个量级,速度快、成本低。
第三步,实体抽取(Entity Extraction)。从记忆单元里抽出人名、项目名、技术栈、时间等结构化信息。这些实体是构建语义记忆的基础。比如"我们决定用 Rust 重写解析器"这条记忆,会抽出实体:项目=解析器,技术栈=Rust,决策类型=重写。
第四步,向量化(Embedding)。把摘要和原始文本分别向量化,存入向量索引。摘要向量用于粗筛,原始文本向量用于精排。
# 记忆写入管道的核心逻辑示意 def write_memory(raw_dialogue: str, session_id: str): chunks = semantic_chunk(raw_dialogue) for chunk in chunks: summary = local_llm.summarize(chunk) entities = extract_entities(chunk) summary_vec = embed(summary) content_vec = embed(chunk) memory_id = db.insert_memory( session_id=session_id, raw_text=chunk, summary=summary, entities=entities, summary_vec=summary_vec, content_vec=content_vec, created_at=now(), access_count=0, importance=compute_importance(chunk, entities) ) return memory_id3.2 记忆检索策略
检索是记忆系统最核心的能力。我们的检索走的是混合检索路线,结合了向量相似度、关键词匹配、时间衰减和重要性加权。
具体来说,给定一个查询,系统会:
- 用查询的 embedding 在摘要向量索引里做 ANN 搜索,取 Top-K 候选
- 对候选做关键词 BM25 匹配,补充向量检索可能漏掉的精确匹配
- 对每个候选计算综合得分:
score = w1 * vector_sim + w2 * bm25_score + w3 * importance + w4 * recency其中 recency 用指数衰减函数计算,importance 在写入时根据实体密度和内容长度估算。权重 w1 到 w4 是可配置的,不同场景可以调。
- 取综合得分最高的 N 条记忆,用原始文本向量做精排
- 返回最终结果,同时更新这些记忆的 access_count 和 last_accessed
注意:时间衰减的系数不要设得太激进。我们一开始用了半衰期 7 天的设置,结果发现很多有价值的长期记忆被压下去了。后来改成 30 天,效果好很多。记忆系统不是新闻推荐,老记忆不一定没价值。
3.3 记忆更新与遗忘机制
记忆不是只写不删的。一个没有遗忘机制的记忆系统,最终会变成一个垃圾场。
我们的遗忘策略分三种:
被动遗忘:长期未被访问且重要性低的记忆,逐步降低检索权重,但不删除。这相当于"想不起来但还在潜意识里"。
主动遗忘:用户显式要求删除某条记忆,或者记忆内容被新记忆覆盖(比如用户改了偏好),旧记忆标记为失效。
合并压缩:多条相似记忆定期合并成一条更抽象的记忆。比如用户在不同时间说了五次"我喜欢用 Python",合并成一条"用户偏好 Python"的语义记忆,原始五条降级为佐证材料。
合并压缩用一个定时任务跑,每周一次,用本地 LLM 做摘要合并。
3.4 MCP Server 的实现要点
MCP Server 需要暴露几个核心工具(Tool):
memory_write:写入新记忆memory_search:检索记忆memory_update:更新指定记忆memory_forget:删除或失效记忆memory_stats:返回记忆系统统计信息
每个工具用 JSON Schema 定义输入输出,MCP 客户端会自动生成调用界面。
# MCP Server 工具定义示意 @mcp.tool() def memory_search(query: str, top_k: int = 5, memory_type: str = "all") -> list: """检索相关记忆 Args: query: 检索查询 top_k: 返回条数 memory_type: 记忆类型过滤,可选 all/episodic/semantic """ results = hybrid_retrieve(query, top_k, memory_type) return [ { "id": r.id, "summary": r.summary, "content": r.raw_text, "score": r.score, "created_at": r.created_at } for r in results ]传输层我们同时支持 stdio 和 SSE。stdio 用于本地 Agent 直接调用,SSE 用于需要跨进程或跨设备的场景。
4. 实操部署与关键环节实现
4.1 环境准备与依赖安装
本地部署这套系统,硬件门槛不高。我们测试下来,一台 16GB 内存的机器就能跑得很流畅。如果本地还要跑 embedding 模型和摘要模型,建议 32GB 起步。
依赖清单:
# 核心依赖 pip install mcp sqlite-vec sentence-transformers pip install fastapi uvicorn # SSE 模式需要 # 本地模型(可选,也可以用 API) pip install llama-cpp-python # 或者用 ollama ollama pull qwen2.5:3b ollama pull nomic-embed-textembedding 模型我们用的是nomic-embed-text,768 维,本地跑速度快,中文效果也还行。如果对中文检索精度要求更高,可以换bge-m3,但显存占用会大一些。
4.2 数据库初始化
SQLite 的向量扩展需要手动加载。初始化脚本如下:
import sqlite3 import sqlite_vec def init_db(db_path: str = "memory.db"): conn = sqlite3.connect(db_path) conn.enable_load_extension(True) sqlite_vec.load(conn) conn.enable_load_extension(False) conn.executescript(""" CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, raw_text TEXT NOT NULL, summary TEXT NOT NULL, entities TEXT, -- JSON memory_type TEXT DEFAULT 'episodic', importance REAL DEFAULT 0.5, access_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_accessed TIMESTAMP, is_active INTEGER DEFAULT 1 ); CREATE VIRTUAL TABLE IF NOT EXISTS memory_vectors USING vec0( memory_id INTEGER PRIMARY KEY, summary_vec FLOAT[768], content_vec FLOAT[768] ); CREATE INDEX IF NOT EXISTS idx_session ON memories(session_id); CREATE INDEX IF NOT EXISTS idx_active ON memories(is_active, importance); """) return conn提示:sqlite-vec 的向量表是虚拟表,不能直接跟普通表做 JOIN。我们的做法是先查向量表拿到 memory_id 列表,再用
WHERE id IN (...)查普通表。多一次查询,但逻辑清晰。
4.3 记忆写入的完整流程
实际跑起来,一次记忆写入大概涉及这些步骤。我拿一段真实对话举例:
用户说:"我最近在做一个本地记忆系统,用 SQLite 存向量,通过 MCP 暴露给 Agent 用。"
系统处理流程:
- 分块:这句话作为一个完整记忆单元,不切分
- 摘要:生成"用户正在开发基于 SQLite 和 MCP 的本地记忆系统"
- 实体抽取:
{"项目": "本地记忆系统", "技术栈": ["SQLite", "MCP"], "用途": "Agent"} - 向量化:摘要和原文分别 embed
- 重要性计算:实体密度高(3 个实体 / 1 句话),重要性给 0.75
- 入库
整个过程在本地跑,3B 模型做摘要大概 200ms,embedding 大概 50ms,总延迟在 300ms 以内。这个速度对于对话场景完全够用。
4.4 检索效果调优实录
检索调优是最花时间的部分。我们前后调了三轮,记录一下关键节点。
第一轮:纯向量检索,Top-5。问题是经常召回语义相似但实际无关的记忆。比如查"记忆系统架构",会召回"系统架构设计模式"这种泛泛的内容。
第二轮:加入 BM25 关键词匹配,权重各占 50%。精确匹配好了,但语义泛化能力下降。用户换个说法就检索不到。
第三轮:改成向量为主(权重 0.6)、BM25 为辅(权重 0.25)、重要性 0.1、时间衰减 0.05。这个配比在我们自己的测试集上 MRR(平均倒数排名)从 0.42 提升到 0.68。
调参这件事没有银弹,必须用自己的数据测。我们建了一个 200 条的测试集,每条查询标注了正确答案,每次改权重就跑一遍看指标。
4.5 与 Agent 框架的对接
MCP Server 跑起来之后,Agent 侧的接入很简单。以 stdio 模式为例,在 Agent 的配置里加上:
{ "mcpServers": { "memory": { "command": "python", "args": ["-m", "memory_server", "--db", "./memory.db"], "env": { "EMBED_MODEL": "nomic-embed-text", "SUMMARY_MODEL": "qwen2.5:3b" } } } }Agent 启动时会自动拉起 MCP Server 进程,之后就可以通过标准 MCP 协议调用记忆工具了。
这里有个坑要注意:stdio 模式下 MCP Server 的日志不能往 stdout 打,否则会污染协议通信。所有日志走 stderr 或者写文件。我们一开始没注意,调试信息直接 print,导致 Agent 侧解析协议失败,排查了半天。
5. 常见问题与排查技巧实录
5.1 记忆检索召回率低怎么办
这是最常见的问题。排查顺序建议这样走:
先确认 embedding 模型是否适合你的语言和领域。中文场景用英文模型效果会打折扣。然后检查分块策略,如果记忆单元切得太碎,单条记忆信息量不足,检索自然不准。再然后看权重配置,是不是时间衰减太强把老记忆压没了。
我们遇到过一个典型案例:用户问"之前说的那个方案",检索不到。原因是"那个方案"没有具体关键词,纯向量检索也匹配不到。后来我们在写入时额外生成了一层"假设性问题",就是让 LLM 预判这条记忆可能被什么问题检索到,把这些假设问题也向量化存进去。召回率明显提升。
5.2 记忆冲突怎么处理
用户今天说喜欢 A,明天说喜欢 B,两条记忆冲突了。我们的处理策略是:新记忆写入时,先检索是否有语义冲突的旧记忆,如果有,把旧记忆标记为superseded,并在新记忆里记录supersedes字段指向旧记忆。检索时默认只返回 active 记忆,但保留追溯能力。
5.3 性能瓶颈排查
本地跑,性能瓶颈通常在这几个地方:
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 写入慢 | LLM 摘要耗时 | 换更小模型或异步写入 |
| 检索慢 | 向量索引未建好 | 检查 vec0 表是否正常 |
| 内存占用高 | embedding 模型常驻 | 用 ONNX 量化版或按需加载 |
| 数据库膨胀 | 未做压缩合并 | 检查合并任务是否在跑 |
我们实测下来,10 万条记忆的库,检索延迟在 80ms 左右,写入延迟 300ms,内存占用 2GB 上下(含模型)。这个数据供参考。
5.4 MCP 连接问题速查
MCP 相关的坑主要集中在连接层:
- Codex 找不到 MCP:检查配置文件路径和 JSON 格式,Codex 对配置格式比较严格
- Hermes Agent 接入失败:确认 Hermes 版本支持 MCP,老版本需要升级
- SSE 模式连不上:检查端口是否被占用,防火墙是否放行
- 工具调用超时:默认超时可能太短,记忆检索如果涉及大量计算,需要在客户端调大超时
注意:不同 MCP 客户端的实现细节有差异,同一个 Server 在不同客户端上表现可能不一样。建议先在官方 Inspector 工具里测通,再接入具体客户端。
5.5 数据安全与隐私
本地记忆系统最大的优势就是数据不出本地。但有几个点还是要注意:
数据库文件要设置合理的文件权限,避免其他用户读取。如果用了 SSE 模式暴露端口,一定要绑定 127.0.0.1 而不是 0.0.0.0。备份文件同样包含敏感信息,加密存储。如果记忆里涉及密钥、密码这类内容,写入前做一次敏感信息过滤。
6. 后续扩展方向与技术合伙人需求
这套系统目前跑通了核心链路,但离"好用"还有距离。我们接下来想推进的方向包括:记忆的可视化界面,让用户能直观看到 Agent 记住了什么;多模态记忆支持,把图片、音频也纳入记忆体系;记忆的跨设备同步,同时保持本地优先;以及更精细的记忆权限控制,不同 Agent 能访问的记忆范围不同。
找技术合伙人这件事,我们看重的是对 Agent 记忆这个方向有真实兴趣,而不只是把它当成一个练手项目。具体来说,希望你有以下至少一个方向的积累:MCP 协议或类似协议的实际开发经验;向量检索或 RAG 系统的调优经验;本地 LLM 部署和推理优化经验;或者 Agent 框架的深度使用经验。
合作方式灵活,可以是代码贡献、架构设计、或者产品方向上的共创。我们目前没有融资压力,节奏比较从容,更在意把东西做扎实。
如果你看到这里觉得方向对路,欢迎带着你的想法和经历来聊。这个领域现在还在早期,很多问题没有标准答案,正适合一起摸索。