news 2026/10/10 7:14:06

给AI加记忆:从存储选型到检索注入的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
给AI加记忆:从存储选型到检索注入的工程实践

1. 从"claude-mem"这个名字说起:它到底想解决什么

第一次看到claude-mem这个命名,我的直觉是:这是一个围绕对话记忆做文章的项目。拆开来看,"claude" 指向的是对话式 AI 的交互场景,"mem" 显然是 memory 的缩写。合在一起,它要处理的核心问题就浮出水面了——如何让 AI 在多次对话之间记住东西。

如果你用过任何对话式 AI 工具,一定遇到过这种尴尬:昨天跟它聊了半天的项目背景、技术选型、命名规范,今天开个新会话,它像失忆一样,什么都不知道,你得从头再讲一遍。这种"每次对话都是陌生人"的体验,是当前大多数对话式 AI 产品的通病。claude-mem这类项目存在的意义,就是给 AI 装上一个"外挂大脑",把跨会话的信息持久化下来,下次对话时自动带回来。

它适合谁?三类人最该关注。第一类是重度依赖 AI 辅助编程的开发者,你每天跟 AI 讨论代码架构、调试思路,这些上下文如果能沉淀下来,效率提升是肉眼可见的。第二类是做 AI 应用开发的工程师,你可能需要在产品里集成类似的记忆能力,claude-mem的实现思路可以直接借鉴。第三类是对 AI 记忆机制好奇的技术爱好者,想搞清楚"记忆"这件事在工程上到底怎么落地。

这篇文章我不打算写成一份干巴巴的 API 文档翻译。我想做的是:把这个项目背后的设计逻辑拆开,讲清楚它为什么这么设计、每一步操作背后的意图是什么、实际用起来会遇到哪些坑。因为我自己在给 AI 加记忆这件事上踩过不少坑,很多经验是文档里不会写的。

先说一个反直觉的结论:给 AI 加记忆,难点从来不在"存",而在"取"和"用"。存东西谁都会,写个文件、塞个数据库就完事了。但当你存了几百条记忆之后,怎么在正确的时间把正确的那条捞出来、怎么控制它不污染当前对话、怎么避免记忆越积越多反而拖慢响应——这些才是真正考验设计的地方。claude-mem的价值,恰恰在于它对这几个问题给出了自己的答案。

2. 记忆系统的三层结构:原始记录、压缩摘要与索引

要理解claude-mem这类项目,得先建立一个心智模型:一个能用的记忆系统,绝对不是"把所有对话历史存下来"这么简单。那样做的结果就是上下文爆炸,AI 被一堆无关信息淹没,反而变笨。真正合理的结构是分层的,我把它归纳为三层。

2.1 第一层:原始会话记录(Raw Session Log)

最底层是原始记录。每一次对话的完整内容——用户说了什么、AI 回了什么、调用了哪些工具、产生了什么结果——都会被原样保存下来。这一层的特点是全量、不加工、可追溯。

为什么要有这一层?因为它是所有上层加工的"事实来源"。你后面做的摘要、提取的关键信息、生成的索引,全都是从原始记录里派生出来的。如果原始记录丢了或者被篡改了,上层的一切都成了无源之水。我在实际项目里就吃过亏:早期为了省空间,只存了摘要不存原文,结果后来想重新调整摘要策略时,发现原始信息已经找不回来了,只能干瞪眼。

这一层的存储选型通常有两种思路。轻量方案是直接落成 JSON 或 JSONL 文件,一行一条记录,追加写入,简单可靠。重量方案是进数据库,SQLite 是最常见的选择,因为它零配置、单文件、支持全文检索。claude-mem这类工具一般会优先选 SQLite,原因后面会细说。

2.2 第二层:压缩摘要(Compressed Summary)

原始记录的问题是太长了。一次深度对话可能几千上万字,你不可能每次都把全部历史塞进上下文。所以需要第二层:把原始记录压缩成摘要。

这里的"压缩"不是简单截断,而是有损但保真的信息提炼。理想情况下,一段关于"用户决定用 PostgreSQL 而不是 MySQL,因为需要 JSONB 字段和更强的并发写入能力"的对话,压缩后应该保留"技术选型:PostgreSQL,理由:JSONB + 并发写入"这样的核心结论,而丢掉中间的寒暄和试错过程。

压缩的时机很关键。我的经验是不要实时压缩,而是在会话结束或者达到一定长度阈值时批量处理。实时压缩会拖慢每次交互的响应速度,而且会话还没结束,你也不知道哪些信息最终是重要的。等会话告一段落再压缩,信息完整度更高,压缩质量也更好。

2.3 第三层:索引与检索层(Index & Retrieval)

光有摘要还不够,你得能快速找到它。第三层就是索引层,它决定了"当用户提出一个新问题时,系统怎么知道该调出哪几条历史记忆"。

常见的检索策略有三种,各有适用场景:

检索方式原理优点缺点
关键词匹配基于词频、倒排索引快、可解释、零依赖无法处理语义相近但用词不同的情况
向量语义检索把文本转向量,算余弦相似度能理解语义、召回率高需要嵌入模型、有计算成本
混合检索关键词 + 向量加权融合兼顾精确与语义实现复杂、需要调参

claude-mem这类项目通常从关键词匹配起步,因为它的依赖最少、最容易跑起来。等你用出感觉了,再考虑引入向量检索。我个人的建议是:先用最简单的方案跑通闭环,别一上来就上向量数据库。很多人卡在"选哪个嵌入模型""用哪个向量库"上纠结半天,结果连最基本的记忆存取都没跑通,本末倒置了。

3. 为什么存储层我最终选了 SQLite 而不是文件或向量库

这一节专门聊聊存储选型,因为这是整个项目里最容易纠结、也最容易选错的地方。我在不同项目里分别用过纯文件、SQLite、以及专门的向量数据库,最后在claude-mem这种场景下,我的结论是:SQLite 是性价比最高的起点。

3.1 纯文件方案的致命伤

一开始我也觉得,存记忆嘛,写个 JSON 文件不就行了?简单直接。但用不了多久就会撞墙。

第一个问题是并发写入。如果你同时开着多个会话,或者后台有个进程在压缩历史,多个写入方同时操作一个文件,很容易出现内容覆盖或者文件损坏。你得自己实现文件锁,而文件锁这东西在不同操作系统上行为还不一样,跨平台就是个坑。

第二个问题是检索效率。当记忆条目涨到几千条,你想按关键词搜一下,纯文件方案只能把整个文件读进内存再遍历。每次查询都要全量加载,内存和 CPU 都吃不消。你可能会说"那我分文件存",但分文件之后又面临"怎么快速定位到哪个文件"的问题,绕来绕去还是得建索引。

第三个问题是结构化查询。你迟早会需要这样的查询:"找出最近一周内、关于数据库选型、且被标记为重要的记忆"。纯文件方案处理这种多条件组合查询,基本等于自己手写一个数据库。

3.2 SQLite 的甜点区

SQLite 恰好卡在一个非常舒服的位置。它是单文件的,部署零负担,复制一个.db文件就等于备份了全部记忆。它又支持完整的 SQL,你可以用WHERE、JOIN、LIKE甚至全文检索(FTS5 扩展)来做复杂查询。并发方面,SQLite 的 WAL 模式(Write-Ahead Logging)能支持多读单写,对于记忆系统这种"读多写少"的场景完全够用。

更关键的是,SQLite 自带 FTS5 全文检索扩展。你不需要引入任何外部依赖,建一个虚拟表就能实现关键词检索,还支持 BM25 排序。这意味着你的"第三层索引"可以直接用 SQLite 原生能力搞定,省掉一整套向量库的运维成本。

-- 建一个带全文检索的记忆表 CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, content TEXT NOT NULL, summary TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, importance INTEGER DEFAULT 0 ); -- FTS5 虚拟表,用于关键词检索 CREATE VIRTUAL TABLE memories_fts USING fts5( content, summary, content='memories', content_rowid='id' );

上面这段建表语句是核心。memories表存实际数据,memories_fts是全文索引。查询的时候用MATCH语法,SQLite 会自动走索引,几千条数据毫秒级返回。

3.3 什么时候才该上向量库

我不是说向量库没用,而是说别过早引入。向量检索真正的优势场景是:你的记忆条目已经上万,且用户查询的措辞和记忆里的措辞差异很大,关键词匹配召回率明显不够用。

判断标准很简单:如果你发现用户问"数据库怎么选的",而记忆里存的是"存储引擎决策",关键词搜不到,这时候向量检索才有价值。在此之前,SQLite 的 FTS5 加上一些同义词扩展,足够撑很久。

提示:如果你确实需要向量检索,也不必推翻 SQLite。可以保留 SQLite 存原始数据和元信息,另外用一个轻量向量索引(比如基于 faiss 的本地索引)做语义召回,两者用 ID 关联。这样既拿到了语义能力,又没丢掉 SQL 的灵活性。

4. 记忆的写入时机与压缩策略:什么时候记、记多少

存储层定了,接下来是更烧脑的问题:什么时候往记忆里写东西,写多少。这个决策直接决定了记忆系统的质量。写得太勤,记忆里全是噪音;写得太懒,重要信息又漏掉了。

4.1 触发写入的三种时机

我在实践中总结出三种触发时机,可以组合使用。

第一种是会话结束触发。当一次对话明确结束(用户关闭会话、或者超过一定时间没有新消息),把整段会话做一次压缩,提取要点写入记忆。这种方式的优点是信息完整,能看到对话的全貌;缺点是如果会话很长,压缩时可能丢失细节。

第二种是长度阈值触发。当当前会话的 token 数或者消息条数超过某个阈值(比如 50 条消息),就触发一次中间压缩,把前面的内容先固化下来。这样做的好处是防止会话过长导致上下文溢出,同时也能在会话中途就保住关键信息。

第三种是显式标记触发。允许用户或 AI 主动标记"这条信息很重要,记下来"。比如用户说"记住,我们这个项目用 TypeScript 严格模式",系统就应该立刻把这条写入记忆,而不是等会话结束。这种方式的精准度最高,但依赖用户主动操作。

实际用下来,我的建议是以会话结束触发为主,长度阈值触发为辅,显式标记作为补充。三者结合,既不会漏掉重要信息,也不会产生太多噪音。

4.2 压缩不是摘要,是信息蒸馏

很多人把压缩理解成"写个摘要",这是误区。摘要只是把长文变短,而压缩的目标是保留决策相关的信息,丢弃过程性的噪音。

举个例子。一段原始对话是这样的:

用户:我想给项目加个缓存层,用 Redis 还是本地缓存好? AI:这取决于几个因素。如果多实例部署,Redis 更合适;如果单机,本地缓存更简单。 用户:我们是多实例部署的。 AI:那建议 Redis。不过要注意缓存穿透和雪崩问题。 用户:好,那就 Redis 吧,顺便把缓存 key 的命名规范定一下。

如果只是做摘要,可能会写成"讨论了缓存选型,决定用 Redis"。但信息蒸馏应该提取出结构化的决策记录:

决策:缓存层选型 结论:使用 Redis 理由:多实例部署,需要共享缓存 待办:定义缓存 key 命名规范 风险提示:需处理缓存穿透与雪崩

看出区别了吗?蒸馏后的信息是可执行的、结构化的,下次 AI 读到这条记忆,立刻就知道"这个项目用 Redis,因为多实例,还要注意穿透雪崩"。这比一句模糊的摘要有用得多。

4.3 压缩的粒度控制

压缩粒度也是个需要调的参数。太粗,一条记忆里塞了太多不相关的信息,检索时容易误召回;太细,记忆条目爆炸,检索时噪音多。

我的经验法则是:一条记忆对应一个独立的决策点或事实点。上面那个缓存选型的例子,就是一条记忆。如果同一段对话里还讨论了日志方案,那就应该是另一条记忆。这样检索的时候,"缓存"相关的查询只会命中缓存那条,不会把日志方案也带出来。

实现上,可以在压缩阶段让 AI 输出一个 JSON 数组,每个元素是一条独立记忆,带topic、content、importance字段。这样写入数据库时天然就是结构化的。

# 压缩阶段的输出结构示例 compressed = [ { "topic": "缓存层选型", "content": "使用 Redis 作为缓存层,因多实例部署需共享缓存;需处理穿透与雪崩", "importance": 8 }, { "topic": "日志方案", "content": "日志采用结构化 JSON 输出,便于后续采集分析", "importance": 5 } ]

importance字段很有用,它让你在检索时可以按重要性加权,重要记忆优先返回。这个值可以让 AI 在压缩时自己评估,也可以后续人工调整。

5. 检索与注入:怎么让 AI"想起来"又不被记忆淹没

记忆存好了,最后一步也是最微妙的一步:在对话时怎么把相关记忆注入给 AI。这一步做不好,前面全白搭。

5.1 检索的触发时机

不是每次对话都需要检索记忆。如果用户只是说"你好",你去检索一堆历史记忆塞进去,纯属浪费。合理的做法是按需检索:只有当用户的新消息涉及某个主题时,才去记忆库里找相关内容。

判断"是否涉及"可以用一个轻量分类器,或者更简单——直接用用户消息去记忆库做一次相似度查询,如果最高相似度低于某个阈值,就认为没有相关记忆,不注入。这样既省了 token,又避免了无关记忆干扰。

5.2 注入的数量与排序

检索出来一堆记忆,不能全塞进去。我的经验是每次注入不超过 5 条,按相关性和重要性综合排序。

排序公式可以很简单:score = 相关性权重 * 相似度 + 重要性权重 * importance。两个权重根据你的场景调,比如你更看重相关性,就把相关性权重设高一点。

注入的时候,格式也很重要。不要直接把记忆原文贴进去,而是包一层说明,让 AI 知道这是"历史记忆"而不是"当前对话":

以下是与当前话题相关的历史记忆,供参考: [记忆1] 缓存层选型:使用 Redis... [记忆2] ... 请注意:这些是过去的信息,如与当前情况冲突,以当前对话为准。

最后那句"以当前对话为准"很关键。因为记忆可能过时,如果用户现在说"我们决定不用 Redis 了",AI 不能被旧记忆带偏。

5.3 记忆冲突的处理

说到冲突,这是记忆系统里一个容易被忽视但很要命的问题。同一个主题,可能在不同时间存了互相矛盾的记忆。比如三个月前存了"用 MySQL",一个月前存了"迁移到 PostgreSQL"。

处理冲突有两种思路。一种是时间优先,检索时按时间倒序,新的覆盖旧的。另一种是显式标记失效,当检测到新记忆与旧记忆冲突时,把旧记忆标记为deprecated,检索时默认过滤掉。

我更推荐第二种,因为它保留了历史,你能看到决策的演变过程。实现上就是给记忆表加一个status字段,检索时加WHERE status = 'active'。

字段作用取值示例
status记忆状态active / deprecated / archived
superseded_by被哪条新记忆取代记忆 ID
created_at创建时间时间戳

有了这套机制,当 AI 压缩新对话发现"缓存方案从 Redis 改成了本地缓存"时,就能自动把旧的 Redis 记忆标记为deprecated,并指向新记忆。检索时只返回 active 的,AI 拿到的永远是最新决策。

6. 实操中踩过的坑与性能调优经验

前面讲的都是设计层面的东西,这一节聊点实在的——我自己在实现和调优过程中踩过的坑,以及最后怎么解决的。

6.1 坑一:记忆无限增长导致检索变慢

最开始我没做任何清理,记忆只增不减。跑到几千条的时候,检索开始明显变慢,而且召回质量下降——因为很多陈旧的、不再相关的记忆也在参与匹配。

解决办法是引入记忆生命周期管理。给每条记忆加一个last_accessed字段,记录它最后一次被检索命中的时间。定期跑一个清理任务,把超过一定时间没被访问、且重要性低的记忆归档(不是删除,是移到归档表)。这样活跃记忆集始终保持在合理规模。

-- 归档 90 天未访问且重要性低于 3 的记忆 UPDATE memories SET status = 'archived' WHERE last_accessed < datetime('now', '-90 days') AND importance < 3 AND status = 'active';

6.2 坑二:压缩时把关键数字丢了

有一次用户明确说了"超时时间设成 3000 毫秒",结果压缩后的记忆写成了"设置了合理的超时时间"。数字丢了,下次 AI 读到这条记忆,根本不知道具体值是多少。

这个坑的根源是压缩时过于追求"概括",把具体参数当成了细节丢掉。修复方法是在压缩提示词里明确要求保留所有具体数值、名称、路径、配置项。这些是"硬信息",概括不得。

提示:判断一条信息该不该保留,问自己一个问题——"如果丢掉它,下次还能准确复现这个决策吗?"如果答案是不能,那它就是硬信息,必须保留。

6.3 坑三:注入记忆后 AI 反而变笨

这个坑最隐蔽。有段时间我发现,注入记忆之后,AI 的回答质量反而下降了,经常答非所问。排查了半天才找到原因:注入的记忆里包含了大量与当前问题弱相关的条目,稀释了 AI 的注意力。

比如用户问"这个函数怎么写",检索出来的记忆里有"项目用 TypeScript""代码规范要求两空格缩进""上次讨论过命名风格"——这些虽然都跟项目相关,但对"怎么写这个函数"这个具体问题帮助不大,反而占用了上下文。

解决办法是提高检索的精度阈值。宁可少召回,也不要召回一堆弱相关的。把相似度阈值调高,只注入真正强相关的记忆。实测下来,注入 2 条高度相关的记忆,效果远好于注入 5 条泛泛相关的。

6.4 性能调优的几个实测数据

最后分享几个我实测的参数,供参考。测试环境是普通开发机,记忆库规模约 5000 条。

操作优化前优化后关键优化点
关键词检索约 800ms约 30ms建 FTS5 索引
记忆写入约 200ms约 15ms批量事务提交
压缩一次会话约 8s约 3s只压缩增量部分

写入优化那个点值得展开说。一开始我是一条一条INSERT,每条都触发一次磁盘同步,慢得离谱。改成攒一批(比如 50 条)用一个事务提交,速度直接提升一个数量级。SQLite 的事务开销主要在于每次提交的 fsync,批量提交把 fsync 次数降下来了。

# 批量写入示例 def batch_insert(conn, memories): conn.execute("BEGIN") for m in memories: conn.execute( "INSERT INTO memories (session_id, content, summary, importance) VALUES (?, ?, ?, ?)", (m["session_id"], m["content"], m["summary"], m["importance"]) ) conn.execute("COMMIT")

压缩优化那个点则是增量压缩的思路:不要每次把整个会话重新压缩一遍,而是只压缩上次压缩之后新增的消息。这样随着会话变长,压缩耗时不会线性增长。

7. 关于记忆系统的一点个人体会

做claude-mem这类东西,最大的感悟是:记忆系统的价值不在于记得多,而在于记得准、取得对。我见过太多项目,一上来就追求"全量记忆""永久记忆",结果做出来的东西又慢又笨,还不如没有。

真正好用的记忆,应该像一个靠谱的助理:你不需要它记住你说的每一句话,但它能在你需要的时候,准确想起那件关键的事。这背后是大量的取舍——什么该记、什么该忘、什么时候该想起来、想起来多少。这些取舍没有标准答案,只能根据你的具体场景去调。

如果你正准备给自己的 AI 应用加记忆能力,我的建议是:先用最简单的方案(SQLite + FTS5 + 会话结束压缩)跑通一个最小闭环,用起来,感受一下哪里别扭,再针对性地优化。别一上来就设计一个完美的架构,那样大概率会卡在设计阶段出不来。记忆系统这东西,是"用"出来的,不是"设计"出来的。

另外,记得给你的记忆加一个"遗忘"机制。会忘,才记得住重要的。

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

Grok 4.7在ARC-AGI-3上的抽象推理与状态建模能力解析

1. 这不是“又一个大模型榜单”&#xff0c;而是ARC-AGI-3评测体系下的一次关键压力测试“Grok 4.7 在 ARC-AGI-3 的评测成绩”——这个标题乍看像一条常规技术新闻&#xff0c;但如果你真去翻过ARC-AGI-3的原始论文、跑过它的测试集、或者在某次跨模型对比中被它卡在第7题反复…

作者头像 李华
网站建设 2026/10/10 7:13:40

Windows 下 Playwright 离线浏览器包安装与避坑指南

简介&#xff1a;这份资源是适配 Playwright 1.56.1 的 Windows 离线浏览器包&#xff0c;面向在隔离网络或内网环境中开展自动化测试的开发者与测试团队&#xff0c;解决无法联网下载浏览器内核、依赖安装受阻的问题。压缩包共 663 个文件&#xff0c;约 415.03MB&#xff0c;…

作者头像 李华
网站建设 2026/10/10 7:13:40

YashanDB社交场景实战:从选型到高并发架构设计与优化

YashanDB这几年在国内数据库圈子里讨论度确实高&#xff0c;主打Oracle兼容和国产化替代&#xff0c;但大多数人聊的都是“能不能平滑迁移”“TPCC能跑多少分”。我这次想换个角度聊&#xff0c;把它放到一个具体业务场景里——社交网络数据。说实话&#xff0c;社交业务的数据…

作者头像 李华
网站建设 2026/10/10 7:13:37

PS5全型号M.2 SSD扩容实操指南:从选盘到安装

如果你手头有一台 PS5&#xff0c;并且是那种“新作出了都想试试”的玩家&#xff0c;大概率已经在“删游戏、腾空间、下次再下”的循环里转过好几轮了。PS5 内置的 825GB 看着不小&#xff0c;真正可用也就 667GB 左右&#xff0c;碰到动辄 100GB 容量的新游戏&#xff0c;装两…

作者头像 李华
网站建设 2026/10/10 7:13:10

Codex CLI接入OpenAI兼容接口:config.toml逐行拆解与排错指南

如果你手里有一份 Codex CLI&#xff0c;但出于种种原因想把它接到一个支持 OpenAI 协议的兼容接口上&#xff0c;这篇配置拆解应该能帮你省掉不少弯路。所谓“OpenAI 兼容接口”&#xff0c;指的是那些 API 请求路径、参数格式、返回结构与 OpenAI 官方接口保持一致的第三方服…

作者头像 李华
网站建设 2026/10/10 7:12:47

React Native电商项目实战:鸿蒙跨端适配与性能优化复盘

电商类应用一直是移动端开发里最考验工程能力的场景&#xff0c;没有之一。商品列表要扛住长列表滚动、分类导航要处理多级联动、推荐位要兼顾曝光与性能、商家模块又涉及多角色状态管理&#xff0c;再加上购物车、下单、支付这些强交互链路&#xff0c;任何一个环节没处理好&a…

作者头像 李华