给 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记忆内容 categorypreference/fact/decision/relation tags关键词数组 importance1-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 工具补上记忆这块短板这个项目模式是一个很值得动手尝试的切入点。踩坑记录和代码思路都在上面了照着做过程中的坑你大概率能少踩一半。