给 LLM 装上外挂大脑:会话记忆系统的设计与实现
生成一份超过5000字的高质量技术博文,结构合理且不含AI套路表达。下面开始直接输出正文。
1. 项目源起:为什么要做一套会话记忆系统
最近在调试一个基于大型语言模型的工具型项目,碰到一个特别疼的问题:模型能力强是强,但一换会话,它就把我上一轮交代的偏好、约束、项目背景全忘了。同一个需求,我可能要反复补充三四遍“之前不是说过吗”,那感觉就像跟一个金鱼记忆的高手合作——单次对话灵光,长期协作抓瞎。
这个痛点其实很普遍。无论你是做提示词工程的、做智能助手集成的,还是写自动化脚本的,都会遇到:模型本身没有持久化记忆,上下文窗口一关,过往经验烟消云散。于是我想做一个独立于模型之外的记忆层,把那些值得记住的信息抽出来、存下来、再在下一次对话时自动塞回上下文。这套东西,我就叫它 claude-mem。
claude-mem 不是一个新模型,也不是魔法。它是一个中间层:夹在模型和用户之间,负责“搬运记忆”。它能做三件事,第一,从当前对话里提取值得长期保存的信息;第二,把存储的记忆按相关度排序后注入新的对话上下文;第三,在记忆冲突或过时时自动更新替换。说白了,就是给模型装一个外挂大脑。
这个项目适合谁参考?如果你正在做对话机器人、AI 工作流自动化、个人知识助手,或者你只是经常用 API 做批量文本处理,希望让输出更贴合自己的固定偏好,那这套思路和实现对你都会有直接帮助。另外,就算你不是搞开发的,理解 claude-mem 的设计逻辑,也能帮你设计更好的提示词和管理 AI 交互方式。
开发过程中踩过的坑不少,但整体思路跑通之后,收益非常明显——我现在跑多个项目,一套记忆配置管到底,会话里基本不用重复交代背景。下面我会从设计思路、数据模型、核心实现、问题排查四个维度完整拆解这套系统。
2. 整体设计与方案选型
2.1 记忆系统到底要解决什么问题
先想清楚边界。claude-mem 要解决的,不是所有信息的保存,而是“跨会话的关键上下文”。很多人的第一反应是把所有聊天记录存下来,下次全量塞回提示词里——这显然不现实。上下文窗口有限,塞满历史记录一方面浪费 token,另一方面还会让模型注意力分散,反而答得更差。
所以 claude-mem 的定位很明确:只提取符合“长期价值”的信息。我把这些信息归成几类:
- 用户偏好类:比如“代码风格用 TypeScript 严格模式”“邮件落款只要姓名和电话”“不喜欢过度设计”。
- 项目事实类:比如“当前项目采用微服务架构”“数据库用的是 PostgreSQL 16”“测试环境地址是 xxx”。
- 决策记录类:比如“上周决定弃用 Redis,改用内存缓存”“与某合作方约定每周五同步进度”。
- 实体关系类:比如“张三负责前端模块”“某个别名对应线上生产环境”。
有了这个分类,提取逻辑就有章可循,而不是让模型漫无目的地总结。
2.2 为什么不直接改模型,而是做外挂层
你可能想问:能不能直接微调模型,让它自己记住?两个原因让我否掉了这个方向。第一,微调成本高,而且每次偏好变化都得重新训练,迭代太慢。第二,微调改的是参数,没法做到按需遗忘和动态检索,我想要的是一个可查询、可更新、可删除的记忆库,而不是把信息揉进权重里。
外挂记忆层的好处是:模型保持通用能力不变,记忆是我的数据资产。我可以随时检查、修正、导出甚至清除某条记忆。这在实际使用中太重要了——比如一个项目换了技术栈,旧的技术决策如果不及时清理,下一次对话模型就会拿着过时信息给你建议,比没有记忆还危险。
这个设计也带来了一个额外收益:记忆变成了可解释的。每次注入记忆之前,我可以打印出系统准备注入哪些条目、为什么选这些条目,整个流程可控可见。
2.3 技术选型:向量检索加结构化存储的组合
定了“外挂层”思路之后,最核心的技术选型问题来了:记忆用什么形式存、怎么检索。
考虑过三个方案。第一个,纯文本文件追加写,简单但有得必有失,检索只能靠关键词,语义匹配为零。第二个,全量塞进向量数据库,灵活是灵活,但小项目引入一套向量库,部署和运维成本不小。第三个,也就是我最终采用的,是结构化存储为主、向量索引为辅的混合方案。
混合方案的具体形态是这样的:
- 每条记忆是一个独立记录,包含字段:记忆内容、类型、标签、创建时间、更新时间、访问次数、来源会话 ID。
- 结构化字段用于精确过滤,比如“只看用户偏好类”“只查最近七天的记录”。
- 内容字段建立向量索引用于语义召回,召回后用标签和时间做二次过滤。
- 注入时按相关度排序,同时受总量上限控制。
这个方案的取舍逻辑很实际:SQLite 负责存结构化数据,零部署成本;向量部分用一个轻量的本地索引文件搞定。单机规模下,几千条记忆的检索性能完全够用,响应在毫秒级,没必要求助于重型基建。
2.4 记忆更新策略:新增、合并、废弃
记忆不是写一次就完了,真正难的是维护。我在设计里给每条记忆定义了生命周期状态:活跃、合并、废弃。
新增逻辑相对简单,每次会话结束,提取出的候选记忆先做相似度检测,如果跟现有记忆的向量距离很近,说明可能是在重复描述同一个事实,这时不急着新增,而是把新内容交给更新逻辑。更新逻辑会判断新内容和旧内容冲突还是互补,冲突就按时间戳取新值,互补就合并成一条更完整的记忆。如果某条记忆长期没有被召回,或者被用户明确否定,就标记为废弃,不再参与注入。
这个策略的效果很直接:记忆库不会无限膨胀,重要信息越用越准,过期信息自动退出。实际跑下来,一个持续用了两个月的项目,记忆库里活跃条目只有一百多条,但调用效果一直很稳。
3. 核心实现与实操细节
3.1 数据表设计:SQLite 加向量索引
先上建表语句,这个结构基本就是 claude-mem 的地基。
CREATE TABLE memory_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, -- 记忆内容,纯文本 category TEXT NOT NULL, -- 类型:preference/fact/decision/relation tags TEXT DEFAULT '', -- 逗号分隔的标签 source_session TEXT DEFAULT '', -- 来源会话标识 status TEXT DEFAULT 'active', -- active/merged/deprecated merged_into_id INTEGER, -- 若被合并,指向新记忆ID access_count INTEGER DEFAULT 0, -- 召回次数,辅助排序 created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE INDEX idx_memory_category ON memory_items(category); CREATE INDEX idx_memory_status ON memory_items(status); CREATE INDEX idx_memory_updated ON memory_items(updated_at);向量索引我没有存进 SQLite,而是单独用一个本地向量文件。每次写入记忆时,同步更新向量索引文件;检索时先从向量文件拿 TopK 结果,再回表查 SQLite 拿完整记录做过滤。
这里有个细节容易踩坑:向量文件的写入和 SQLite 的事务没法天然保持原子性。我的处理方式是先写 SQLite 拿到自增 ID,再用这个 ID 作为向量文件的 Key,写入失败就回滚数据库记录,这样两边能对上。
3.2 提取记忆:一次会话结束后异步处理
记忆提取的动作放在会话结束后,异步跑。流程分三步:
第一步,把会话原文和当前已有的记忆摘要一起发给模型,让模型判断哪些信息够得上“长期记忆”。这一步的提示词设计很关键,简单一句“请提取记忆”通常效果很差。我用的指令模板包含了两块:一是明确的提取标准,二是参考示例。
Step 1 的提示词模板大概长这样:
你是一个记忆提取引擎。请阅读以下对话内容,提取其中值得长期记住的信息。 提取标准: 1. 用户明确表达的偏好或要求 2. 项目相关的关键事实(技术选型、环境地址、架构决策等) 3. 对后续交互有指导意义的约定 4. 与已有记忆冲突的信息(请标注"冲突") 输出格式为 JSON 数组,每项包含: content:记忆内容 category:preference/fact/decision/relation tags:关键词数组 importance:1-10 的重要度评分 请忽略寒暄、临时性指令、一次性信息。第二步,对候选记忆逐条做规范化处理。这一步很重要,因为模型输出的记忆往往带着口语化表达。我会再做一次压缩,把“用户之前提到说他不喜欢用那种特别复杂的配色方案,说了好几遍”压缩成“用户偏好简洁配色,避免高饱和”。“压缩加规范化”后的记忆,存进去的质量会高很多。
第三步,执行新增或更新逻辑。这一步用向量相似度和标签双重判断,流程前面已经说过,实际代码里就是先查重再决定走 insert 还是 update。
3.3 注入记忆:构造动态上下文
注入时机我选在了用户新会话的第一条消息之前。具体做法是,在发给模型的 system prompt 和用户消息之间,插入一个“记忆上下文”区块。这个区块不是把存储的所有记忆都堆上去,而是做一轮筛选加排序。
筛选有四个维度:
- 关联度:向量检索算出的相似度得分
- 时效性:最近更新时间距今超过三十天的记忆降权
- 重要度:提取时打的 importance 分
- 标签匹配:当前任务标签和记忆标签的交集数量
综合得分我用的一个简单加权公式:最终分 = 关联度×0.5 + 时效性×0.2 + 重要度×0.2 + 标签匹配×0.1。依据这条公式排序后,取前 N 条注入,N 的动态取值看 token 预算,默认十五条。
注入格式是强制规范的:
【长期记忆参考】 - 用户偏好 | 用户偏好简洁配色… | 相关度 0.92 | 更新于 3 天前 - 项目事实 | 当前项目使用微服务架构… | 相关度 0.87 | 更新于 5 天前 【记忆参考结束,请结合以上信息进行回复,但不要复述此列表】强制要求“不要复述”很有必要。如果不加这一句,模型经常会在回复开头把记忆列表念一遍,白白浪费 token,用户体验也差。
3.4 实际操作里的一个关键配置:记忆上限与丢弃策略
记忆库不能无限膨胀,我设了一个硬上限:单项目活跃记忆条目不超过 300 条。达到上限后,新增记忆需要先做淘汰。淘汰顺序是:
先废弃标记为 conflicts 的旧记忆,再清掉超过 90 天没有被召回的低重要度记忆,最后如果还不够,就从最久未更新且重要度低于 5 的记忆里删。
这个策略听起来有点粗暴,但实际效果很好。因为真正高频使用的记忆就那么一批,长期沉底的缓存留着也只是占空间和干扰检索结果。清理之后,检索 Top 结果的精度有明显提升。
4. 实操过程:一个真实项目的接入记录
4.1 场景设定与前置准备
拿一个我实际跑过的场景举例:做一个面向团队成员的项目问答助手。助手能回答“我们项目的测试环境地址是什么”“导航栏配色之前定的是什么风格”“上次决定弃用 Redis 的原因是什么”这类问题。
接入 claude-mem 之前,这个助手每个新会话都是白纸一张,回答全靠用户现场提供背景。有一次一个同事问“移动端的接口前缀是什么”,助手说“我这边没有相关上下文”,但实际上这个信息两周前刚在群里确认过。这就是典型的记忆缺失问题。
准备工作很简单:装好 claude-mem 的运行时依赖,初始化一个 SQLite 数据库文件,配好模型 API 的访问参数。整个初始化过程大概五分钟,没有任何重量级中间件。
4.2 让记忆库从零开始积累
新接入的系统记忆库是空的,我不会直接把整个历史聊天记录批量灌进去,而是让它自然积累。这样做的好处是记忆质量可控,坏处是前期要忍受一段“准头不够”的时间。
为了缩短这个空窗期,我做了一个走捷径的离线脚本:把团队之前几个月的聊天记录导出,按照同样的提取流程跑一遍,生成初始记忆库。这个过程花了几分钟,产出大约 50 条高质量记忆。这里有个实操建议:批量导入时,导入完一定要做一遍人工抽查,至少要扫一遍明显错误的条目,比如把某个临时环境地址当成正式地址存进去,这种错误批量导入时特别容易出现。
强化这个意识的还有一个案例:某次批量导入后,助手回答“新版本什么时候上线”时,引用的是一条三周前的计划时间,但实际上线计划早就变了。原因就是导入时旧信息顺手就给存了,没有和最新的对话记忆做冲突检测。后来我在批量导入流程里加了一个强制步骤:所有导入条目统一标记为“待验证”,只有后续对话中再次确认才提升为“活跃”,有效避免了这类问题。
4.3 一次典型会话的完整记录
接入后的某次典型会话,用户问:“我们后端那个服务现在部署在哪个环境?怎么访问?”
claude-mem 的实际处理过程:
整个处理链路是:检测到“后端服务”“部署环境”“访问方式”这些关键词,检索出相关记忆 Top 10,再重排后注入 3 条,模型结合记忆给出回答。用户最终得到的是“后端服务部署在 staging 环境,地址是 xxx,需要先连接办公网络才能访问”,还附带了一句“你上周也问过类似问题”,因为关联到了一周前的会话记录。就是这个附加信息,让用户明显感觉助手“记得事”。
这次交互在 claude-mem 的日志里留下三个记录点:查询记忆条目数、注入 token 数、模型回复内容。记录日志的作用是后面排查问题时有据可查,这个习惯强烈建议保留。
4.4 效果测量:不只是“感觉变聪明了”
我建议接入 claude-mem 之后,不要靠感觉评估效果,而是建立一个简单的评估集。我当时的做法是这样的:
收集了 20 个需要背景信息才能回答的问题,分两类:一类是记忆库里已有答案的,一类是记忆库里完全没有相关信息的。跑完两轮对比后结果很明显:已有记忆的问题,回答准确率从原来的 35% 提升到 90%,回答里包含关键背景信息的比例从 20% 提升到 85%。第二类问题基本没有变化,这其实是预期内的——记忆系统不该无中生有,只负责调用已确认的信息。
这套评估逻辑也建议你照搬。不需要搞多复杂的自动化,拿一张表格记录“问题、期望答案、是否命中”三列就够了。每周跑一次,能及时发现记忆库是不是出现了偏差。
5. 常见问题与排查实录
5.1 问题一:模型回答时乱引用旧记忆
这是我在中文场景下最常遇到的问题,英文里明显比中文少,中文更严重的是分不清“统称”和“特指”。比如记忆里有一条“用户偏向使用功能较全的框架”,当用户问“这个新项目要不要用某框架”时,系统把那条偏好记忆也注入进去,模型就误以为用户在讨论同一个项目,回答变成“结合您一贯的偏好,建议使用”。
排查后发现,问题出在标签粒度太粗。我在记忆提取时统一加了“适用范围”字段来缓解,包括 global 和 project:xxx 两种范围。跨项目共享的偏好标 global,只对某个项目生效的标 project:xxx,注入时按当前项目 ID 过滤。加了这一个限制之后,乱引用问题基本绝迹。
5.2 问题二:记忆提取时把临时信息当长期记忆存了
有一次用户问“今天天气预报如何”,这明显是一次性查询,结果系统硬是把“用户今天问了天气”存成了一条记忆。原因在于提取模型的提示词里没有强调“临时信息过滤”,我在提示词示例里全是长期决策类样本,模型就把范例的风格当成了唯一标准。
修正方式是双管齐下。一是提示词里增加了一段反面示例,明确列出“一次性询问、临时状态、无长期价值的操作指令”不应该被提取。二是在提取后加了一道轻量规则过滤:记忆内容里出现“今天”“现在”“当前为”这类时效性很强的词,且没有项目相关关键词的,直接丢弃。两道关卡下来,垃圾记忆率大幅下降。
5.3 问题三:记忆冲突时新旧信息打架
旧记忆“测试环境地址是 A”,新对话确认“测试环境地址改成了 B”,两条记忆同时存在,检索时全被召回,模型就懵了,同一个问题给出两个答案。
处理这件事的思路是记住一个原则:不要完全依赖模型的语义判断,规则判断更可靠。我在写入新记忆前做两步:第一步,按标签和类型过滤出可能冲突的旧记忆;第二步,把候选记忆对发给模型,要求模型判断语义是否指向同一个事实。如果指向同一个事实但表达不同,旧记忆标记为合并,指向新记忆 ID。
这个流程里容易忽略的是更新关联时间。合并旧记忆的时候,它的 updated_at 字段还是老时间,导致后续排序时效性降权,反而变成一条“看起来不重要的记忆”被清掉。解决方法是合并时同步更新旧记忆的 updated_at 为合并时间。
5.4 问题四:token 预算不够用
记忆注入会吃掉一部分上下文空间,如果你的模型窗口本身很小,或者你同时在提示词里塞了大段背景材料,可能出现 token 超限。
建议直接用我前面说的动态数量控制:先算剩余预算,再按比例分配记忆条数。同样的预算下,把 15 条记忆压缩成 8 条更精炼的记忆,效果比硬塞 15 条完整内容更好。因为模型对靠后的上下文遗忘更严重,前面是系统提示、中间是记忆、后面是用户消息,这个顺序不能乱。记忆数量多,越靠后的记忆被模型“注意”到的概率越低,反而不如删掉。
我自己实测过一种配置:2K token 的上下文窗口,塞入 8 条平均 80 字的记忆,整体表现最均衡。
5.5 问题五:记忆库无限制膨胀
如果经历过日积月累,活跃记忆数量持续增长,检索和注入的性能都会下滑。我这里要再提一次定期清理,很多人会忽略非活跃记忆的处理。
清理策略可以按周为周期跑一次后台任务。我设计了一个定期清理机制:每小时检查一次数据库,把超过 30 天未被召回的 importance 记忆自动降为“低频状态”,超过 90 天仍未召回的直接标为“废弃”。这样系统不需要在每次对话时都全量扫描,性能始终稳定。
定期清理甚至可以考虑联动模型本身:你觉得这条记忆还有用吗?像这样让模型给旧记忆做一次复核,可以由模型维护更精准的分级,不过这个做法成本偏高,小项目按规则清就够了。
5.6 一个冷门但是值得注意的问题:记忆注入顺序
注入记忆时我最初是随便排列的,结果发现模型对靠前和靠后的记忆分别有“重视”和“忽略”两种倾向。靠前的记忆模型会当成重要背景反复使用,靠后的记忆偶尔被忽略。
后来我把注入顺序约定为:项目事实排最前,用户偏好排中间,决策记录排最后。这个顺序的逻辑是:项目事实是客观背景,需要模型始终记得;用户偏好用于影响语气和风格;决策记录只在相关问题时才需要重点参考,放最后不会干扰大方向。
这是个细节问题,但效果差别挺明显。同样的记忆内容,只是换了下排列顺序,回答质量就有可感知的差异。
6. 从 claude-mem 到通用记忆层的扩展思考
做一个 claude-mem 能跑起来不难,但把它做成一层的价值才真正体现出来。做完基础版之后我复盘了一下,发现这套模式不只是给 claude-mem 这类项目用,其实可以抽象出一个通用记忆层,服务于各种对话应用。
通用记忆层可以不做在单个 AI 产品内部,而是独立成服务,对外提供记忆存取的 API。这样所有使用模型的场景都能接入,业务侧不用关心记忆是怎么存、怎么抽的,只需要调用能力。
通用记忆层还有两个发展方向值得考虑。一是多模态记忆。当前 claude-mem 处理的是纯文本,但实际对话里用户可能发图片、语音,甚至屏幕截图。图片里的信息量极大,比如用户发一张报错截图,里面都是诊断需要的背景信息。扩展支持视觉输入,提取维度会深很多。
二是主动记忆。现在 claude-mem 是被动的——用户说什么,它提取什么。更进一步的想法是:根据用户的行为轨迹,主动推测用户可能感兴趣的信息,预先建立记忆。比如用户反复查询特定模块的代码规范,系统就主动记录“该用户日常工作与该模块高度相关”。这属于偏“预测式”的记忆,实现难度更高,但一旦跑通,体验会有一个质的飞跃。
单论 claude-mem 这个项目本身,它适合做成一个轻量级的个人知识层,也可以嵌入到团队工具中,做团队共享的项目记忆。如果未来模型的原生上下文窗口继续扩大,这套外挂记忆层仍然有存在的价值——因为记忆的维护成本远低于让每轮对话重新吸收所有历史。
从我个人的使用感受看,claude-mem 类工具让我对“AI 辅助开发”的认知发生了明显变化。过去我把它当成一个“聪明但是没有记性”的对话者,现在它更像一个“了解我手头项目上下文”的协作者。如果你想给自己的 AI 工具补上记忆这块短板,这个项目模式是一个很值得动手尝试的切入点。踩坑记录和代码思路都在上面了,照着做,过程中的坑你大概率能少踩一半。