
“ai-memory” 这个词我在好几个技术社区里都看到有人在聊。有人把它当成一个产品方向有人把它当成一套提示词模板还有人干脆把它理解成“给大模型接一个长期记忆插件”。我自己的理解更朴素ai-memory 的本质是把“记不住”这个痛点从模型层面搬到工程层面来解决。模型上下文窗口再大也是有限的但人的记忆是长期的、结构化的、带时间戳的。这篇就围绕我自己实操过的一套本地化 AI 记忆系统聊聊从设计思路到落地实现的完整过程。这套东西能干什么简单说就是你随手记下的想法、读过的文章片段、项目的关键决策、聊过的重要结论都会被自动切块、向量化、索引然后在你想起来“我好像三个月前讨论过某个问题”的时候用自然语言把它捞出来。它适合所有长期做知识管理的人尤其是独立开发者、研究员、技术写作的人以及所有觉得“笔记越记越乱搜也搜不到”的用户。1. AI记忆到底在解决什么问题1.1 传统笔记工具的失效场景我先说一个很多人都有过的体验。你往笔记软件里存了一篇关于数据库索引优化的文章三个月后你写代码遇到了一个慢查询问题你很清楚自己“肯定看过相关的东西”但在笔记里怎么翻都找不到因为当时存的标题是“性能调优杂记”而你现在搜的是“联合索引失效”。全文检索的机械匹配在这里暴露了它的极限它匹配字符不匹配语义。人的记忆不一样。人脑会对信息做蕴涵式的编码——你记的不是原文而是“这段内容大概在讲什么、跟什么问题相关”。这就是语义索引。AI 记忆的核心工作就是把人的这种语义化编码方式用嵌入模型embedding model和向量检索复刻到数字系统里。所以 ai-memory 不是给模型加个缓存而是给你自己加一个外挂第二大脑。1.2 记忆系统与问答系统的本质区别很多人会把 ai-memory 和 RAG检索增强生成搞混其实两者的目标完全不同。RAG 的目标是让模型在回答问题时能引用外部知识它的经典场景是“客服机器人回答产品手册里的问题”。它的核心评测指标是“答案准不准”记忆在这里只是手段。ai-memory 的目标则是“把你的过去变成一个可查询的资产”。它的场景是“我去年写的那个方案里当时的假设条件是什么”。评测标准是“能不能找回你记忆中模糊但真实存在过的信息”。所以 ai-memory 系统在召回环节更强调时间语义、上下文关联和重要性加权普通的 RAG 不管这些它只管“相似度最高的 Top-K 个片段”。1.3 为什么要做本地化而不是纯云端这里有一个非常实际的选择是直接用某个商业平台的“记忆功能”还是自己搭一套本地系统商业平台确实方便但有两个我无法回避的问题。第一我的笔记里包含大量客户项目的内部决策记录这些内容我不能接受被用来训练任何外部模型第二记忆系统的核心价值在于长期积累一旦平台调整政策或者服务关闭积累的记忆资产可能一夜归零。所以我选择了本地化方案。所有嵌入计算、向量存储、召回逻辑都在自己机器上跑数据以标准格式SQLite JSON落盘任何时候都能整体导出。这个取舍的本质是在牺牲一点初期便利性的前提下换取对记忆资产的所有权和控制权。对于长期使用的工具这个选择我至今认为是对的。2. 系统设计分层架构与核心组件选型2.1 四个层级的职责划分我把整套系统分成四层采集层、处理层、存储层、召回层。每层只干一件事。采集层负责接收原始输入包括文本、Markdown 片段、URL 抓取内容、甚至微信聊天记录导出的纯文本。处理层做标准化清洗、分段、提取时间戳、生成嵌入向量。存储层负责把原始文本和向量分别落盘。召回层接收查询请求执行语义检索再根据时间衰减和重要性规则重新排序。为什么非得分层因为如果你的采集逻辑和存储逻辑耦合在一起后期想换一种嵌入模型就得把所有采集代码全部重写。分层之后嵌入模型只跟处理层打交道存储层只认“我已经拿到了向量”接口固定替换模型只需重跑一遍处理层。这是我踩过一次大坑之后得到的教训——第一版我把向量库调用直接写在了采集函数里后来模型从text2vec-base-chinese换成bge-m3的时候改动量差点让我想把整个仓库删了重来。2.2 嵌入模型怎么选中文场景的硬性约束嵌入模型是整个系统里技术含量最高也最容易翻车的组件。选型逻辑说白了就三点中文效果好不好、向量维度合不合理、本地能不能跑得动。我实测过的几个模型情况如下表模型维度中文效果本地推理速度适用场景text2vec-base-chinese768良好快纯中文短文本CPU可跑bge-small-zh-v1.5512良好快中文为主、资源受限bge-m31024优秀中等中英混合、长文本text-embedding-3-small1536优秀API调用可接受数据出境的场景我的最终选择是bge-m3原因不是它分数最高而是它同时支持稠密检索和稀疏检索未来如果想做混合检索不需要再换模型。代价是它的 1024 维向量占用空间比较大1 万条文本片段大概占用 40MB 左右对于个人知识库来说完全能接受。如果你每天记录量特别大、机器配置又旧那bge-small-zh-v1.5是更务实的选择512 维的检索速度几乎翻倍效果差距在个人笔记场景里不太明显。2.3 向量库选型从轻量到重量向量库的选择决定了整个系统的复杂度上限。我先说结论个人本地记忆系统直接放弃 Milvus 那类分布式向量库杀鸡不用牛刀。我测试过三种方案第一种sqlite-vec扩展。SQLite 加上向量检索能力跟主数据表存在同一个文件里零额外服务进程备份就把一个文件拷走。如果你记忆规模在十万条片段以内这是我认为最优的方案。第二种Chroma 独立向量库。它自带客户端和服务端分离模式优点是生态成熟网上资料多支持元数据过滤缺点是它单独占用端口、单独维护数据库文件等于一个系统里多了一个需要关心的小黑盒。第三种PostgreSQL pgvector。如果你的机器上本来就跑着 PostgreSQL那这个方案很顺手一个数据库解决所有问题但前提是你愿意维护一个完整的数据库服务。我的取舍是主存储用 SQLite 存元数据和原文向量检索用sqlite-vec扩展两条腿走路备份时一个文件搞定。下面的初始化代码展示了我实际的建库方式import sqlite3 conn sqlite3.connect(memory.db) conn.enable_load_extension(True) conn.load_extension(sqlite-vec) conn.execute( CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0( id INTEGER PRIMARY KEY, embedding FLOAT[1024] ); ) conn.execute( CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, source TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, importance REAL DEFAULT 0.5, access_count INTEGER DEFAULT 0 ); )这种设计的妙处在于vec_memories里的 id 跟memories里的 id 一一对应检索的时候先用向量表找到最相似的 id 列表再 JOIN 回主表取原文和元数据。整个流程不引入任何额外的网络请求所有操作都在一个事务里从查询到拿到原文通常只需要几十毫秒。2.4 分块策略决定检索效果的上限所有人一开始都会低估分块chunking的重要性。分块分得太粗比如整篇文章算一个向量检索时匹配粒度太粗返回的片段里一半内容跟问题无关分块太细语义被切碎嵌入向量表达的信息不完整。我实测下来对于中文技术笔记512 字左右、按段落边界自然切开的效果最好。这个数字不是拍脑袋定的我对比了 128、256、512、1024 四档参数在同一批测试问题上的召回命中率。512 字档位的命中率明显高于 256 和 128原因是太短的块丢失了上下文而 1024 档位的优势并没有比 512 高多少却显著增加了检索的噪声。分块时还需要保留一个关键元数据每个块在原文中的位置和所属文章标题。我的习惯是把标题和段落拼接成一个前缀比如【性能优化笔记】联合索引失效的场景分析这样嵌入模型在生成向量时天然就把主题信息带进了语义表达里。做不做这一步检索效果能差出两个档次。3. 从零搭建 ai-memory 的完整实操流程3.1 准备阶段环境与依赖整个系统只有三个硬依赖Python 3.10、嵌入模型推理环境、sqlite-vec。我用的是sentence-transformers加载bge-m3模型这里有一个细节需要注意bge-m3的英文全称是BAAI/bge-m3它在 HuggingFace 上的模型文件比较大约 2GB首次运行会自动下载建议提前手动下载并放到本地缓存目录避免在后续流程中因为网络问题中断。安装命令如下pip install sentence-transformers sqlite-vec安装sqlite-vec时有个坑它在 PyPI 上的包名跟导入名不完全一致安装用sqlite-vec导入用sqlite_vec。我第一次导入的时候报ModuleNotFoundError查了半天才发现是两个不同的命名。如果加载扩展报错需要检查你的 Python 环境对应 SQLite 版本是不是太老——sqlite-vec要求 SQLite 3.38 以上老版本的 Linux 系统自带 SQLite 常常不达标可以单独编译新版本。3.2 核心实现记忆写入函数记忆写入是整个系统的入口函数它做的事情很简单接收一条文本 → 清洗 → 分块 → 生成向量 → 存入数据库。但这里面有一个意想不到的设计点写入时给每条记忆打一个“重要性分”。重要性分的范围从 0 到 1这个值初始来自输入时的侧面信息比如你是否手动标记了“重要”系统会默认给 0.5。它的作用在后面的召回排序环节才会体现当相似度分数相差不大时重要性高的记忆应该排在前面。为什么要引入这个维度因为人的记忆本来就不是平等存储的——你对“今天午饭吃了什么”的记忆权重显然应该低于“项目的架构决策记录”。from sentence_transformers import SentenceTransformer import sqlite_vec model SentenceTransformer(BAAI/bge-m3) def add_memory(content, sourcemanual, importance0.5): cleaned clean_text(content) chunks split_memory(cleaned, chunk_size512) for chunk in chunks: embedding model.encode(chunk).astype(float32) cursor conn.execute( INSERT INTO memories(content, source, importance) VALUES(?, ?, ?), (chunk, source, importance) ) chunk_id cursor.lastrowid conn.execute( INSERT INTO vec_memories(id, embedding) VALUES(?, ?), (chunk_id, embedding.tobytes()) ) conn.commit()这个函数看起来简单但embedding.tobytes()这一步很容易写错。sqlite-vec存向量用的是裸字节流不是 numpy 数组也不是 JSON 字符串。如果你存错了格式检索时不会直接报错而是返回一堆乱序的相关度结果排查起来非常痛苦。我后来封装了一个工具函数专门做向量的序列化/反序列化统一控制这个边界没有再翻过车。3.3 核心实现记忆召回函数召回函数是用户真正感知到“AI 记忆”威力的地方。它的逻辑分三步第一步把用户的问题同样编码成向量第二步在向量表里做 Top-K 近似检索第三步对召回结果重新排序融合时间衰减和重要性加权。import numpy as np TIME_DECAY 0.3 # 时间衰减系数 def retrieve(query, top_k5): query_vec model.encode(query).astype(float32) rows conn.execute( SELECT v.id, v.distance, m.content, m.created_at, m.importance, m.access_count FROM vec_memories v JOIN memories m ON v.id m.id ORDER BY v.distance LIMIT ? , (top_k * 3,)).fetchall() results [] for row_id, distance, content, created_at, importance, access_count in rows: age_days (datetime.now() - parse_time(created_at)).days decay_factor np.exp(-TIME_DECAY * age_days / 365.0) final_score (1 - distance) * 0.7 importance * 0.2 decay_factor * 0.1 results.append((final_score, content, created_at)) results.sort(keylambda x: x[0], reverseTrue) return results[:top_k]这个排序公式里的三项权重是我调了很多次才定下来的。距离分也就是语义相似度给 0.7 是合理的因为它毕竟是最核心的召回依据重要性 0.2 保证被手动标过重要内容的笔记不容易被埋没时间衰减只占 0.1原因是“旧记忆”不代表“不重要”——恰恰相反能时隔多年还想起来的东西往往才是真正有长期价值的。3.4 完整的触发链路怎么让记忆系统“活”起来如果记忆系统的入口只有手动添加用一段日子就会被遗忘。我给它加了三个自动触发入口。第一个是文件监听我有个inbox/目录往里面扔任何 Markdown 或 TXT 文件脚本检测到新文件就自动抽取内容写入记忆。第二个是命令行快捷导入python memory.py add 这句话会被记下来适合随手记录。第三个是定期扫描每周日晚自动扫描我当周的笔记软件导出文件把新增内容全部检测一遍。这三个入口覆盖了“主动记录”之外的大部分场景。尤其是文件监听我把浏览器的“发送到笔记”插件输出目录直接指向了inbox/这样我在网上看到一段有启发的文字点一下插件系统就自动把内容抓取、分块、嵌入、入库整个链路不需要我打开任何代码工具。实测效果是这样的三个月前我写了一条笔记内容是“分布式锁用 Redis 实现时注意主从切换会导致锁丢失参考 Redlock 的争议”。三个月后的某天我在排查一个偶发性重复写入问题随手在记忆系统里查“分布式锁 安全问题”第一条返回的正是这条三个月前的记录还带着当时的完整上下文。那一刻我意识到这个系统真正生效了。4. 常踩的坑与排查技巧4.1 向量维度不一致最隐蔽的启动崩溃bge-m3输出 1024 维向量sqlite-vec建表时也必须声明FLOAT[1024]两边一旦不一致写入时不会报错但查询时会出现诡异的“返回结果为空”或者“距离全是 0”。这个问题难排查因为它藏得很深。我的建议是加一道自检代码写入完成后模拟一条查询断言返回结果数量不为零。用 CI 的思路管本地系统虽然听起来有点小题大做但对于这种会消失几天后再次启用的个人项目再小的自检都能省下大量排查时间。4.2 中文语义检索偏弱不要迷信单一向量模型如果你用的模型本身中文预训练语料不够充分你会发现检索“性能优化”时返回的结果里混着一堆关于“性能测试流程”的内容。因为这两个概念在语义空间里确实相近但它们在你的记忆里可能根本不属于同一类问题。解决办法是引入关键词过滤作为前置条件。我的实现是在分块时同步提取每个文本块的关键词列表存在独立字段里。召回时先用规则从查询里提取 2-3 个核心名词按照这些词原文是否出现在记忆片段里做一次过滤在某些领域这种关键词加权的效果比单纯加模型参数更好。纯向量检索在英文长尾问题上表现不错但中文这种语义颗粒度更细的语言混合信号永远强过单一信号。这也是为什么我选了支持稀疏检索的bge-m3——它的稀疏向量本质上就是强化版的关键词信号。4.3 记忆库膨胀后检索变慢个人记忆系统这类工具的怪圈是用得越久数据越多价值越大但检索也会越来越慢。我跑到 5 万条片段时一次 Top-K 检索耗时到了 300 毫秒左右虽然还能接受但已经有了可感知的卡顿。我用两种手段把耗时压了回去。第一是给vec_memories表增加分区机制按年份拆分虚拟表每次并行查询当前年加前三年。第二是启用sqlite-vec的量化索引把浮点向量量化为 8-bit 整数存储召回精度大约损失两个百分点但检索速度提升了将近三倍。对个人记忆场景来说为了两倍速的响应损失一点点召回精度是完全划算的。4.4 时间衰减造成“重要旧记忆被雪藏”这是我在设计时间权重后遇到的逆向问题。当时间衰减系数TIME_DECAY设置得过大比如取 0.8你会发现三个月前的记忆在排序里基本抬不了头。但人的直觉恰恰相反你问“上次数据库主从切换是什么时候解决的”你期望的是三个月前那篇排第一而不是昨天刚记的午饭清单。我的修正方式是在召回函数里增加一条保护规则如果某条记忆的access_count字段大于 3说明它被反复访问过应该在计算分数时额外加权。这跟人脑的“重复记忆强化”机制是一样的——一个知识被反复调用说明它在你的认知体系里地位很重要。加了这个逻辑之后老记忆被雪藏的问题明显缓解了。4.5 隐私与安全本地系统也不能光膀子虽然是本地系统但数据安全的习惯不能丢。我对memories表里的原文做了字段级加密用cryptography库的Fernet对称加密密钥存在独立的 key 文件里。这样即使数据库文件被拷贝走没有密钥也读不出正文。同时文本清洗时有一个敏感信息扫描器用正则和实体识别把手机号、身份证号、银行卡号这类的信息替换成脱敏占位符再入库。我承认这一层加密给调试带来了不便——现在查看数据库内容需要先写一段解密逻辑但考虑到记忆系统里存的是一个人几年的想法和决策记录这部分谨慎我认为是必须的。5. 从个人工具到长期系统的扩展方向5.1 多端同步一套记忆多端访问本地系统的短板是只有一台机器能访问。我的应对方案是把 SQLite 数据库放在坚果云或者 Syncthing 同步目录里所有设备上的客户端共享同一个数据库文件。这个方法初听起来有点野路子但实测下来很稳因为sqlite-vec的读取只发生在进程内数据库同步在文件层完成。所有设备不同时写入的话基本不会遇到锁冲突。手机端我写了一个极简的 PWA 界面只提供查询和添加两个核心操作。查记忆这件事在手机上完成度最高因为大多是碎片时间想起来的一段模糊线索。真正写长笔记还是在电脑上完成PWA 界面负责“快速不打断思路”的那一下。5.2 自动摘要与记忆压缩长期运行之后还有一种噪声问题同一话题的碎片记录越来越多。每个月我都在做一次记忆压缩任务流程是把一个月内同一主题的片段汇总调用本地大模型生成一篇精简摘要把摘要作为新记忆入库原始碎片则标记为“已归档”。这一步相当于人类的“睡眠记忆巩固”——把短时记忆整合成长期记忆。全流程调用本地模型推理不把任何文本送到外部 API。跑了一段时间后记忆库的质量明显提升。我打开记录浏览的时候不再面对一堆重复的碎片每个主题都能看到一条清晰的整合摘要需要细节时再深入展开。5.3 它不完美但它改变了我的信息管理方式我现在所有的新项目启动都会先把相关思路丢进 ai-memory一周后自动汇总一份知识结构。写技术方案时查历史记录几乎成了肌肉记忆。要说这套系统的缺点也很明显搭建初期成本不低嵌入模型的选型和调优需要不少试错检索效果在极冷门的垂直领域仍然不稳定本地推理对老机器不友好。但就长期价值而言我认为这是值得投入的。我们已经习惯了让大模型替我们生成内容但很少有人反过来做一件事用模型帮助你更好地理解自己过去积累的信息资产。ai-memory 给我的启发是AI 最大的价值不一定在“创造新东西”也可能在“把你已经拥有的东西重新组织成真正可用的状态”。最后分享一个我实操里最想提醒别人的技巧别在第一版就追求完美。先用默认参数跑起来每天添几条真实笔记两周之后回头看检索效果再决定要不要调分块大小、换嵌入模型、加时间衰减。记忆系统是数据驱动的没有真实积累一切调参都是空中楼阁。跑起来让数据帮你说话。