news 2026/10/26 7:53:37

给大语言模型外挂持久记忆:基于SQLite与向量检索的claude-mem实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
给大语言模型外挂持久记忆:基于SQLite与向量检索的claude-mem实践

1. 项目缘起:为什么我要自己动手做一套记忆系统

用大语言模型做开发的人,大概都经历过这种抓狂时刻:昨天在对话里跟模型反复确认过的接口命名规范、数据库字段类型、甚至整个项目的目录结构,今天开一个新会话,它全忘了。你得把之前聊过的上下文重新贴一遍,贴少了它理解偏,贴多了又挤占宝贵的上下文窗口。更别提那些跨天、跨周的长周期项目,每次重启对话都像换了一个新同事,磨合成本高得离谱。

claude-mem这个项目,就是冲着这个痛点去的。它的核心目标很朴素:给 Claude 这类大语言模型外挂一套持久化的记忆层,让模型在多次会话之间能够记住关键信息,包括项目约定、用户偏好、历史决策、常用代码片段等等。说白了,就是给模型配一个"笔记本",它会自己往里记,也会在需要的时候自己翻出来看。

这套东西适合谁?我认为有三类人特别需要:一是长期用 Claude 做同一套代码库开发的工程师,二是把 Claude 当作日常助理、需要它记住个人习惯的重度用户,三是想在自己产品里集成"有记忆的 AI 助手"的开发者。哪怕你只是偶尔用用,理解这套记忆机制的设计思路,对你写提示词、管理上下文也有实打实的帮助。

我前后折腾了大概三周,踩了不少坑,也总结出一套相对稳定的方案。下面我把整个设计思路、核心实现、实操步骤和避坑经验完整地摊开讲,你可以直接照着复现。

2. 整体架构设计:记忆系统到底该怎么分层

2.1 为什么不能只靠"把历史对话全塞进去"

很多人第一反应是:记忆嘛,简单,把之前的对话记录全部拼到 system prompt 里不就行了。我一开始也这么干过,结果很快就撞墙了。上下文窗口是有限的,Claude 虽然支持很长的上下文,但你每轮对话都塞几万字的旧记录,成本和延迟都会飙升,而且模型对超长上下文中间部分的注意力是会衰减的,塞得越多,真正关键的信息反而越容易被淹没。

所以claude-mem的核心设计原则是:记忆要分层、要压缩、要按需检索。不是把所有东西都记住,而是记住"值得记的",并且在"需要的时候"才调出来。这跟人脑的工作方式其实很像——你不会记得昨天中午吃了什么,但你会记得重要客户的偏好。

2.2 三层记忆结构的设计

我把记忆分成三层,这个分层是整个系统的骨架:

层级名称存储内容生命周期检索方式
L1工作记忆当前会话的即时上下文单次会话直接拼接
L2短期记忆近期会话摘要、临时决策数天到数周时间衰减加权
L3长期记忆项目约定、用户偏好、稳定事实长期持久语义相似度检索

L1 就是当前对话本身,不需要额外处理。L2 是"最近发生过什么",比如上周我们讨论过要重构某个模块,这种信息有时效性,过一阵子可能就不重要了。L3 是"永远成立的事实",比如"这个项目用 TypeScript 严格模式"、"用户偏好函数式写法"。

为什么要这么分?因为不同层级的记忆,检索策略和淘汰策略完全不同。L3 你几乎不会删,L2 你需要定期清理和降权,L1 用完即弃。如果混在一起管理,你会陷入"到底该不该删这条记忆"的无尽纠结。

2.3 存储选型:为什么我最终选了 SQLite 加向量索引

存储方案我试过三种:纯 JSON 文件、纯向量数据库、SQLite 加向量扩展。

纯 JSON 文件最简单,读写直观,但一旦记忆条目上千,每次全量加载和线性扫描就慢得没法用,而且并发写入容易损坏文件。纯向量数据库(比如专门的向量存储服务)检索很强,但部署重、依赖多,对个人项目来说杀鸡用牛刀,而且它不擅长存结构化的元数据。

最后我选了SQLite + 向量索引的组合。SQLite 负责存结构化的记忆条目(内容、类型、时间戳、权重、来源会话 ID),向量部分我用一个轻量的本地嵌入模型生成向量,存成 BLOB,检索时在内存里做余弦相似度计算。记忆条目通常也就几千条量级,内存里算相似度完全够快,实测一千条记忆的检索延迟在 20 毫秒以内。

提示:如果你记忆量真的很大(十万条以上),再考虑上专门的向量索引库。但对绝大多数个人和中小团队场景,SQLite 方案足够,而且零运维。

3. 核心机制拆解:记忆是怎么被写入和读出的

3.1 写入时机:什么时候该记一笔

记忆系统的成败,一半取决于"记什么"。如果什么都记,很快就会被垃圾信息淹没;如果记太少,又起不到作用。我在claude-mem里设定了几个明确的写入触发点:

第一个触发点是显式指令。当用户说"记住这个"、"以后都按这个来"、"这个规则很重要"时,系统直接提取当前对话的关键内容写入 L3。这是最可靠的信号,优先级最高。

第二个触发点是决策类对话。当对话中出现"我们决定用 X 而不是 Y"、"最终方案定为 Z"这类表述时,系统会自动抽取决策内容写入 L2。因为决策往往有时效性,所以放短期层。

第三个触发点是会话结束摘要。每次会话结束时,系统会让模型自己总结这次聊了什么、有哪些结论、有哪些待办,生成一段结构化摘要写入 L2。这一步很关键,它把一次可能很长的对话压缩成几百字的精华。

我用一段伪代码说明写入逻辑:

def should_write_memory(message, context): # 显式指令,最高优先级 if contains_explicit_marker(message): return ("L3", extract_content(message)) # 决策类表述 if contains_decision_pattern(message): return ("L2", extract_decision(message)) # 会话结束 if is_session_end(context): return ("L2", summarize_session(context)) return None

这里有个经验:不要指望模型自动判断"什么值得记"。我早期版本让模型自由发挥,结果它把"你好"、"谢谢"这种寒暄也记下来了,纯属噪音。后来改成基于规则的触发,准确率高了很多。规则可以简单,但一定要明确。

3.2 记忆条目的数据结构

每条记忆我都用统一的结构存储,这样检索和展示都方便:

{ "id": "mem_20240115_001", "layer": "L3", "type": "convention", "content": "项目使用 TypeScript strict 模式,禁止 any 类型", "keywords": ["typescript", "strict", "any"], "source_session": "sess_abc123", "created_at": "2024-01-15T10:30:00Z", "last_accessed": "2024-01-20T14:00:00Z", "access_count": 5, "weight": 0.95, "embedding": "<BLOB>" }

几个字段值得单独说。weight是记忆的权重,初始值根据来源定(显式指令给 0.9 以上,自动摘要给 0.6 左右),每次被检索命中并实际使用后,权重会小幅上调,长期不用则缓慢衰减。access_count和last_accessed用来做热度排序。keywords是我额外抽的,用于关键词粗筛,能大幅减少需要算向量的候选集。

3.3 检索策略:怎么在正确的时候调出正确的记忆

检索是记忆系统的另一半。我的检索流程分三步走:

第一步是候选集筛选。根据当前对话的内容,先做关键词匹配,把包含相关关键词的记忆捞出来。这一步很快,能把候选集从几千条缩到几十条。

第二步是语义相似度排序。对候选集里的每条记忆,计算它和当前对话的向量相似度,再结合权重、时间衰减、访问热度算一个综合分。综合分的公式我调了好几版,目前用的是:

score = 0.5 * similarity + 0.2 * weight + 0.2 * recency + 0.1 * frequency

其中recency是按时间衰减算的,越近的记忆分越高;frequency是访问频率归一化后的值。这个权重分配不是拍脑袋定的,我做过小规模的对比测试,0.5 给相似度是因为语义相关性还是最重要的,但纯靠相似度会漏掉那些"虽然字面不像但确实相关"的记忆,所以用权重和热度做补充。

第三步是注入上下文。把排在前 N 条的记忆(N 一般取 5 到 10)格式化成一段简洁的文本,拼到 system prompt 里。这里要注意格式,我用的格式是:

[相关记忆] - (约定) 项目使用 TypeScript strict 模式 - (偏好) 用户喜欢函数式写法,避免 class - (决策) 数据库选型定为 PostgreSQL

注意:注入的记忆条数不是越多越好。我实测下来,超过 10 条之后,模型对每条记忆的"重视程度"会下降,反而容易忽略关键信息。宁可精挑 5 条高相关的,也不要塞 20 条。

4. 实操落地:从零搭一套可用的记忆系统

4.1 环境准备与依赖安装

先把基础环境搭起来。我用的是 Python 3.10,主要依赖就三个:sqlite3(标准库自带)、numpy(算向量)、sentence-transformers(本地嵌入模型)。

pip install numpy sentence-transformers

嵌入模型我选的是all-MiniLM-L6-v2,这个模型体积小(约 80MB)、速度快、效果对记忆检索这种场景完全够用。别一上来就用大模型做嵌入,检索场景不需要那么高的语义精度,速度和资源占用更重要。

from sentence_transformers import SentenceTransformer model = SentenceTransformer('all-MiniLM-L6-v2')

第一次运行会下载模型,之后就走本地缓存了。如果你完全离线,可以提前把模型文件下好放到指定目录。

4.2 数据库初始化

建表语句我写得比较克制,只保留必要字段:

import sqlite3 def init_db(path="memory.db"): conn = sqlite3.connect(path) conn.execute(""" CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, layer TEXT NOT NULL, type TEXT NOT NULL, content TEXT NOT NULL, keywords TEXT, source_session TEXT, created_at TEXT NOT NULL, last_accessed TEXT, access_count INTEGER DEFAULT 0, weight REAL DEFAULT 0.5, embedding BLOB ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_layer ON memories(layer)") conn.execute("CREATE INDEX IF NOT EXISTS idx_created ON memories(created_at)") conn.commit() return conn

索引我只建了两个:按层级和按时间。关键词检索我用的是内存里的倒排索引,没走数据库,因为记忆量不大,内存索引更快。

4.3 写入流程的完整实现

写入函数要处理向量生成、关键词抽取、权重初始化这几件事:

import uuid from datetime import datetime def write_memory(conn, layer, mtype, content, source_session, weight=0.5): mem_id = f"mem_{uuid.uuid4().hex[:12]}" keywords = extract_keywords(content) embedding = model.encode(content) now = datetime.utcnow().isoformat() conn.execute(""" INSERT INTO memories (id, layer, type, content, keywords, source_session, created_at, last_accessed, access_count, weight, embedding) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?) """, (mem_id, layer, mtype, content, ",".join(keywords), source_session, now, now, weight, embedding.tobytes())) conn.commit() return mem_id

extract_keywords我用的是简单的 TF-IDF 加停用词过滤,没上复杂的 NLP 模型。实测下来,对记忆检索这种场景,简单方法的关键词质量已经够用,而且快得多。

4.4 检索流程的完整实现

检索是重头戏,我把三步走拆成三个函数:

def retrieve_memories(conn, query, top_k=5): # 第一步:关键词粗筛 query_keywords = set(extract_keywords(query)) candidates = keyword_filter(conn, query_keywords) # 如果粗筛结果太少,放宽到全量 if len(candidates) < top_k * 3: candidates = load_all_memories(conn) # 第二步:综合打分 query_vec = model.encode(query) scored = [] for mem in candidates: sim = cosine_similarity(query_vec, mem['embedding']) recency = compute_recency(mem['last_accessed']) freq = normalize_frequency(mem['access_count']) score = (0.5 * sim + 0.2 * mem['weight'] + 0.2 * recency + 0.1 * freq) scored.append((score, mem)) # 第三步:排序取前 K scored.sort(key=lambda x: x[0], reverse=True) return [mem for _, mem in scored[:top_k]]

compute_recency我用的是指数衰减,半衰期设成 14 天。也就是说,一条两周没被访问的记忆,它的时间分衰减到一半。这个半衰期是根据我的使用习惯调的,如果你做的是长期项目,可以调长到 30 天。

4.5 注入上下文的格式化

检索出来的记忆要拼成模型能读懂的格式。我用的模板很朴素,但效果稳定:

def format_memories(memories): if not memories: return "" lines = ["[相关记忆]"] for mem in memories: lines.append(f"- ({mem['type']}) {mem['content']}") return "\n".join(lines)

然后在构造请求时,把这段文本拼到 system prompt 的末尾。注意位置,我试过放开头和放末尾,放末尾效果更好,因为模型对 prompt 末尾的内容注意力更集中。

5. 踩坑实录:那些文档里不会写的经验

5.1 记忆污染:最隐蔽的坑

我遇到最头疼的问题是记忆污染。有一次模型在对话里随口说了一句"这个项目可能用 Redis 做缓存",结果被系统当成决策记了下来。之后每次检索都会把这条捞出来,模型看到就以为项目真的用 Redis,开始按 Redis 写代码。实际上我们根本没用 Redis。

这个坑的根源是:模型说的话不等于事实。模型在探索性讨论时会产生大量"假设性表述",这些不能当记忆。我的解决办法是给写入加一道过滤:只有用户明确确认过的内容(比如用户回复"对"、"就这么定"、"按这个来")才写入 L3,模型单方面的表述最多进 L2 且权重压到 0.3 以下。

提示:如果你发现模型开始"记错事",第一件事就是去查记忆库,看看是不是有污染条目。我建议每周花五分钟扫一眼最近写入的记忆,及时清理。

5.2 权重衰减太快导致"失忆"

早期版本我把时间衰减设得很激进,半衰期只有 7 天。结果一个跨月的项目,第二周开始模型就"忘了"第一周定的规范。后来我把 L3 层的时间衰减直接关掉——长期记忆本来就不该随时间衰减,它只应该在被证伪时删除。L2 层保留衰减,半衰期调到 14 天。这个调整之后,跨周期项目的体验好了非常多。

5.3 向量维度不匹配的报错

这个坑比较低级但很常见:换嵌入模型之后,旧记忆的向量维度和新模型对不上,检索时直接报错。我的处理方式是给每条记忆存一个model_version字段,检索时只加载当前模型版本生成的向量,旧版本的记忆要么重新生成向量,要么标记为待迁移。别偷懒直接混用,会出各种诡异的相似度结果。

5.4 常见问题速查表

现象可能原因排查方向解决办法
模型记错事实记忆污染检查 L3 层最近写入删除污染条目,加确认过滤
跨周期项目失忆衰减过快看 L3 是否被衰减关闭 L3 时间衰减
检索报维度错误模型版本不一致检查 embedding 维度加 model_version 字段
检索结果不相关关键词粗筛太严看候选集大小放宽粗筛或走全量
响应变慢注入记忆太多看 top_k 设置降到 5 到 8 条
记忆库膨胀写入无节制统计条目增长加写入频率限制和定期清理

5.5 定期清理与归档策略

记忆库不能只进不出。我设了一套清理规则:L2 层超过 60 天且从未被访问的记忆,直接删除;L3 层超过 180 天未访问的,降权到 0.2 并标记为"待确认",下次相关对话时让用户确认是否还有效。这套规则跑下来,记忆库能稳定在一个健康的规模,不会无限膨胀。

6. 进阶玩法:让记忆系统更聪明

6.1 记忆的自动合并与去重

用久了会发现,同一个事实可能被记了好几次,比如"项目用 TypeScript"可能在不同会话里被记了三遍。我在写入前加了一道去重检查:新记忆写入前,先跟同层级的现有记忆算相似度,如果超过 0.9,就不新增,而是把旧记忆的权重上调、访问时间刷新。这样既避免了冗余,又强化了重要记忆。

6.2 基于记忆的主动提醒

更进一步,我让系统在会话开始时主动检查:有没有"待办类"记忆还没完成?有没有"待确认"的 L3 记忆需要用户拍板?如果有,就在第一轮回复里主动提出来。这个功能让记忆系统从"被动检索"变成了"主动服务",体验提升很明显。实现上就是在构造 system prompt 时,额外拼一段"待处理记忆"。

6.3 记忆的可视化与手动管理

纯命令行管理记忆太反人类,我后来加了一个简单的 Web 界面,能按层级、类型、时间筛选记忆,支持手动编辑和删除。这个界面不复杂,用 Flask 加一个单页就搞定了,但对日常维护帮助巨大。强烈建议你也做一个,哪怕很简陋,能看见记忆库的全貌,心里才有底。

7. 我个人的几点实操体会

这套claude-mem我用了小半年,最大的感受是:记忆系统的价值不在于"记得多",而在于"记得准"。我见过太多人一上来就想把什么都记住,结果系统被噪音淹没,反而不好用。宁可少记,也要记准,这是第一原则。

第二个体会是规则比模型可靠。写入触发、去重、清理这些环节,能用规则就用规则,别指望模型自己判断。模型适合做语义理解和摘要,不适合做"该不该记"这种需要一致性的决策。

第三个体会是衰减策略要跟场景匹配。做短期项目的,衰减可以快;做长期维护的,L3 层干脆别衰减。没有万能参数,得根据自己的使用节奏调。

最后分享一个小技巧:如果你刚开始搭,别追求一步到位。先做最简单的"显式指令写入 + 关键词检索",跑起来,用一两周,感受一下哪里不顺手,再逐步加语义检索、权重衰减、自动摘要这些。记忆系统是个需要长期调优的东西,边用边改,比一次性设计完美要靠谱得多。

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

网络安全漏洞靶场实战指南:从入门到高阶

1. 漏洞挖掘靶场概述&#xff1a;从入门到实战的必经之路在网络安全领域&#xff0c;靶场就像武术训练中的木人桩&#xff0c;是安全从业者磨练技能的必备工具。我接触过的所有优秀渗透测试工程师&#xff0c;无一例外都有过"泡靶场"的经历。不同于真实环境的不可控性…

作者头像 李华
网站建设 2026/10/26 7:52:44

e2e测试实战:从profile1配置到Playwright核心用例设计

1. 从“e2e”这个标题说起&#xff1a;它到底指什么第一次看到“e2e”这个标题&#xff0c;很多人会愣一下。三个字母加一个数字&#xff0c;既不像一个完整的产品名&#xff0c;也不像一句能直接读懂的话。但如果你在软件测试、自动化工程或者质量保障这条线上待过一段时间&am…

作者头像 李华
网站建设 2026/10/26 7:52:09

L5级自动驾驶测试:挑战与方法论

1. 自动驾驶L5级测试的终极挑战当Waymo的测试车队在凤凰城街头24小时不间断运行时&#xff0c;工程师们面对的不仅是复杂的交通场景&#xff0c;更是一场关于软件测试边界的哲学思辨。L5级自动驾驶被业界称为"完全自动驾驶"&#xff0c;意味着车辆能在任何人类驾驶员…

作者头像 李华
网站建设 2026/10/26 7:52:08

OpenMontage 开放媒体编排:从时间线数据模型到 FFmpeg 渲染实践

1. 从“OpenMontage”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“OpenMontage”这个词&#xff0c;我脑子里蹦出来的第一反应是“开放蒙太奇”。蒙太奇是影视剪辑里最核心的手法之一&#xff0c;把不同镜头拼在一起产生新的含义。而“Open”这个前缀&#xff0c…

作者头像 李华
网站建设 2026/10/26 7:52:01

OpenMontage:开源自动蒙太奇,让AI视频片段轻松拼成叙事短片

不知道你们有没有遇到这种情况&#xff1a;用AI生成了几十个质量还不错的短视频片段&#xff0c;单个看每一段都挺有感觉&#xff0c;可真要把它们拼成一条有叙事感的成片&#xff0c;却发现手动拖剪辑轨道简直是一场灾难。片段之间的衔接、节奏、转场、声音匹配&#xff0c;每…

作者头像 李华
网站建设 2026/10/26 7:51:59

数据工程师如何用AI提升效率与反内卷

1. 数据工程师的生存现状与"反卷"契机凌晨两点的办公室里&#xff0c;显示器蓝光映照着数据工程师疲惫的面容——这可能是大多数同行最熟悉的场景。ETL任务卡住了、报表需求临时变更、数据仓库查询超时...这些日常琐碎正在吞噬我们的时间和精力。但最近半年&#xff…

作者头像 李华