考试通知
会话AI长期记忆实现:claude-mem外挂记忆架构与工程实践 我自己一直在折腾大模型对话类的应用前阵子搞了一个叫claude-mem的小项目说白了就是给“会话型 AI”加长期记忆的插件。现在大部分模型 API 虽然在单次会话里能记住几万字的上下文但只要你把窗口关掉或者新建会话它立刻把之前聊过的东西忘得干干净净。做实际产品的时候这个痛点极其致命——用户昨天刚交代过的偏好今天一问就“失忆”了体验非常割裂。claude-mem的思路就是用“外部持久化记忆 动态注入上下文”的方式让模型跨会话记住用户偏好、历史关键事实和长期任务的进度。这个项目我自己测了好几个版本从最初挂在主流程里的一个 Python 脚本慢慢改成带本地索引、自动摘要、过期清理的独立服务。这篇文章把我的设计思路、核心实现和踩过的坑完整梳理一遍。适合正在做私人助理类应用、客服机器人、知识库问答系统的开发者参考尤其是那些被“模型记忆只有一扇窗”逼到想骂人的朋友。我会尽量把关键代码和取舍理由都讲清楚方便你直接拿去改。1. 项目整体设计与思路拆解1.1 为什么不能直接把全部历史塞进 Prompt业界最常见的“伪记忆”方案就是把历史会话记录原封不动拼进 system prompt。这在早期确实糊弄过不少人但用一阵就发现两个硬伤第一是token 开销飞速膨胀聊个二十轮可能就两千多字再往后每次请求都背着越来越重的历史包袱第二是无效信息噪音太大寒暄、临时话题、重复确认这些话占了绝大部分真正对后续对话有价值的可能就那么几句。模型被无效上下文干扰后反而更容易答偏。这也是claude-mem一开始就放弃“全量注入”路线的原因。真正的长期记忆应该做到三件事只留关键事实、能按需检索、主动做时间衰减。不是把流水账背下来而是像人一样记住“用户养了一只叫年糕的猫”忘掉“用户昨天下午三点说了一句好的”。1.2 外挂记忆模块的典型架构这个项目的整体结构并不复杂本质上是在模型调用层前面加了一层“记忆代理”。我用最朴素的方式组织记忆写入层实时从每轮对话中抽取出“值得记下来的信息”存储层本地 SQLite 数据库按 session、实体、摘要三种粒度存检索层根据当前对话内容用关键词向量混合打分召回相关记忆注入层把召回结果压缩成不超过一定字数的记忆片段追加到 prompt 前部维护层定期对记忆做摘要合并、过期清理和去重。从部署形态上看它就是一个 HTTP 服务模型服务商的那套 SDK 照常用只不过每次发请求前先从记忆服务拉一次相关记忆。我特意没有做成需要改模型调用底层的深度集成因为不同框架的调用链差异太大。做成独立服务的好处是以后换模型厂商、换前端框架记忆这块完全不用动。1.3 设计取舍轻量可自部署优先做这个项目之前我考虑过直接用别人做好的现成方案但调研了一圈发现要么太重要上向量数据库集群要么太“黑盒”你想调一下抽取规则只能改源码。claude-mem的理念是能跑在树莓派上的记忆工具所以存储直接用 SQLite向量召回用本地的 TF-IDF 风格权重算法兜底后续再决定要不要挂外部向量库。这样一个单文件服务就能搞定全部功能部署成本极低。这种取舍有一个很实际的收益用户可以完全掌控自己的对话记忆数据所有记录都以明文 SQLite 文件存放在本地机器上不依赖任何云端同步。这对那些注重隐私、不希望对话内容经过额外第三方服务的场景特别友好。代价则是语义检索的聪明程度比接一个成熟向量库要弱一些但通过混合打分和同义词扩展日常够用。2. 核心数据模型与记忆抽取策略2.1 三类记忆粒度核心记忆、会话摘要、实体画像设计存储结构时我把记忆分成了三层每一层的生命周期和写入策略都不一样。最初我是把所有信息一股脑塞进一张表结果“三小时前聊的电影”和“用户对辣椒过敏”混在一起检索权重一塌糊涂。后来拆成了三张核心表问题立刻清晰很多。实体记忆entities用户明确表达或上下文里反复出现的稳定事实比如“用户是产品经理”“用户喜欢深色主题”“用户的项目的名字叫某内部系统”。这类记忆半衰期最长通常不会被自动清除。会话摘要summaries每一次完整会话结束后用抽取模型生成一小段结构化摘要记录“这次对话处理了什么任务、得出什么结论、遗留什么问题”。会话摘要按时间递减权重但不过期用于横向定位历史上下文。临时事件events粒度最小、时效性最强的信息比如“用户今天提到下周要出差”。这类记录默认 72 小时后就进入待压缩状态。前端进行检索时三类记忆分别打分并汇总。这样用户的长期画像不会被临时事件冲淡而近期任务类信息也不会被埋进陈年旧事里。这个分层结构我调了很久才稳定下来比较适合做成通用模板。2.2 关键表结构与字段设计SQLite 的表结构我尽量精简但有几个字段是必须认真设计的。直接看建表语句CREATE TABLE IF NOT EXISTS entities ( id INTEGER PRIMARY KEY AUTOINCREMENT, entity_type TEXT NOT NULL, -- person / preference / project / other content TEXT NOT NULL, -- 记忆内容如 用户对花生过敏 confidence REAL DEFAULT 0.8, -- 抽取置信度 source_session TEXT, -- 来源会话 ID first_seen_at TEXT DEFAULT (datetime(now)), last_seen_at TEXT DEFAULT (datetime(now)), seen_count INTEGER DEFAULT 1, weight REAL DEFAULT 1.0 ); CREATE TABLE IF NOT EXISTS summaries ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, summary_text TEXT NOT NULL, task_status TEXT DEFAULT open, -- open / done / blocked created_at TEXT DEFAULT (datetime(now)), access_count INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, occurred_at TEXT NOT NULL, expire_at TEXT NOT NULL, processed INTEGER DEFAULT 0 );entities表里的confidence和weight非常关键它们直接参与检索排序。confidence表示“我们对这条记忆的判断有多确定”如果用户在对话里明确纠正过这条信息我会把 confidence 降到 0.3 以下甚至直接标记删除。weight则表示“这条记忆当前对全局对话的重要性”比如用户反复提到某个项目细节这个项目的相关实体 weight 就会上升。有些朋友可能觉得用 SQLite 存这种数据不够“高级”但我实测下来单机几万条记忆的查询速度完全无感。而且 SQLite 的备份、迁移、导出都极方便一个文件拷走就是全量备份。2.3 记忆抽取从对话流里捞出“值得记的东西”模型的输出质量很大程度上取决于记忆抽取的质量。claude-mem的抽取环节采用“规则 轻量模型判断”的两段式设计。第一段用规则做粗筛包括包含明确偏好词“我喜欢”“我不喜欢”“我习惯”“千万别”包含人称指代后跟稳定属性“我是”“我在”“我有”对话中出现两次以上的专有名词抓取中文人名、英文人名、项目代号、产品名。粗筛命中的句子进入候选列表。第二段把候选句子拼接成一批请求一次模型接口做结构化抽取返回 JSON[ { entity_type: preference, content: 用户不喜欢在方案里使用红色作为主色调, confidence: 0.9, expire_in_hours: null }, { entity_type: event, content: 用户计划下周三去北京出差, confidence: 0.85, expire_in_hours: 168 } ]因为不是每轮对话都需要实时抽取成本和延迟顶不住我在项目里加了一个“延迟抽取”队列当前轮对话结束、用户超过 40 秒没有输入时后台把这个 session 最近几轮的文本拿去批量抽取。这样体验上几乎无感又能保证记忆及时落库。实测中这一招比每轮同步抽取少消耗大约 70% 的模型调用量。2.4 记忆修正与用户反馈闭环光有写入没人纠错记忆库很容易堆积垃圾信息。我专门设计了“记忆反馈”接口模型在对话中发现之前提取的信息与用户当前表述矛盾时会调用工具接口去做更新。比如用户两轮前说“我下周在上海”后来又纠正“改去杭州了”此时如果系统把“上海”当成硬记忆注入就会导致回答穿越。矛盾处理策略很简单不覆盖而是新增一条新记忆然后给旧记忆打上superseded标记同时把两边的 confidence 互换。查询时只召回最新状态但历史信息仍保留在库里方便追溯。这个设计起初只是我拍脑袋想的没想到后来在复盘对话质量时特别有用——它能告诉你模型是不是在频繁“误解”用户的更新意图。3. 实操过程与核心环节实现3.1 项目模块结构与运行流程我把服务拆成五个模块每个模块只负责一件事调试起来非常省脑子mem_capture.py对话采集与抽取调度mem_index.py负责实体、摘要、事件的写入和更新mem_search.py混合检索与打分排序mem_inject.py将检索结果格式化为可注入的上下文mem_main.pyHTTP 服务入口绑定本地端口。运行时主流程大概是用户发消息后端对话服务先把历史请求发送给记忆服务带上当前用户 ID记忆服务执行检索将相关记忆拼成一段不超过 800 字的memory_context返回对话服务把memory_context拼到 prompt 的 system 部分再正常调用模型接口模型返回后对话服务把“用户消息 模型回复”异步回传记忆服务记忆服务把文本放入抽取队列在空闲时完成写入和更新。整个流程里对话服务和记忆服务只通过标准 HTTP 的 JSON 通信不共享任何内部状态。我甚至可以在聊天过程中随时重启记忆服务对话那边最多感知到一个短暂的检索超时不影响主流程。3.2 检索打分关键词加向量怎么融合mem_search是这个项目里我调试最久的部分。因为本地不想引入太大的模型我选择用一个轻量的 TF-IDF 风格关键词打分再加可选向量相似度的混合方案。关键词打分部分的核心是把当前用户输入拆分成词项然后逐条扫描记忆表算重叠词覆盖率、词频权重和时效衰减。逻辑上很像搜索引擎的 BM25但我做了一些简化def keyword_score(query_terms, memory_text, last_seen_at): overlap len(set(query_terms) set(memory_terms)) if overlap 0: return 0.0 coverage overlap / max(len(query_terms), 1) recency max(0.0, 1.0 - (now - last_seen_at).days / 30.0) return coverage * 0.7 recency * 0.3向量相似度部分我保留成可插拔接口默认直接跳过。如果你本地已经跑了一个 embedding 服务比如用轻量模型把文本转成向量只需要在配置里填一个 endpointmem_search就会自动把查出来的几条候选结果再做一轮语义排序。这样就算不提向量库纯靠关键词也能有个七八十分的水平接上向量后长尾的语义匹配比如用户说“上次聊的那个很贵的东西”也能隐约摸到相关记忆。3.3 注入模板让模型读懂记忆上下文记忆不是丢给模型就能生效的注入格式非常考验 prompt 功底。我踩过一次很蠢的坑刚开始直接把记忆列表逐条拼进去结果模型把记忆当成了对话历史的一部分开始回答“好的我记得您喜欢深色主题”显得又假又聒噪。后来我把注入模板改成了下面这种“事实清单”风格效果立刻正常了memory_block ## 关于用户的已知记忆 请将以下事实作为背景不要主动提及“我记得”之类的表述需要时自然引用 - [偏好] 用户偏好深色界面尤其是深蓝和黑灰配色 - [事实] 用户目前在做一个跨平台通知工具的开发项目代号 M - [近期] 用户上周提到希望把通知推送延迟控制在 5 秒内 这个模板有两个核心设计一是明确告诉模型“不要主动提及记忆”二是用“自然引用”替代“背诵式回应”。这样用户感知到的是“AI 居然知道我喜欢深色”而不是“AI 在秀它的记忆插件”。对于客服类场景还可以调整语气为“根据您的历史需求”这类偏正式表述但克制原则不变。3.4 会话摘要自动生成机制每次对话结束后台会触发一轮摘要生成。摘要不是简单把对话压缩而是按“任务型会话”的思路提取四要素目标、进展、结论、遗留项。我用一条精心设计的 prompt 做抽取抽取结果直接写进summaries表。{ goal: 用户希望搭建个人知识库搜索工具, progress: 已完成数据采集模块向量索引方案选了本地轻量方案, conclusion: 先用手动打标签方式做 MVP再决定是否引入重模型, follow_up: [需要对比至少两个候选 embedding 模型的精度] }摘要的意义不光是上下文记忆还承担着“跨会话任务延续”的作用。比如用户第二天回来说“继续搞那个搜索工具”模型检索到这条摘要后可以接着进度往下聊而不需要用户重新描述一遍需求。这一点做私人助理类应用时收益极高。3.5 历史记忆清理与过期策略记忆如果没有清理机制迟早会被垃圾塞满。我设计了三级清理事件级临时事件超过expire_at后进入待清理队列若该事件在后续会话中被重新提及则自动续期摘要级超过 60 天未访问的会话摘要每次检索时权重降为原来的 80%降到阈值以下就只保留外部索引不进注入 Context实体级实体绝不自动删除但如果连续 60 天未被触发且 confidence 低于 0.6会被标记为“休眠记忆”默认不参与召回除非用户主动搜索。我在实践里体会到记忆清理的本质是控制注入噪声。模型能力再强给它灌一堆过期的旧信息也容易带偏。把“休眠”但保留在库里的设计比直接物理删除更稳——用户某天突然问起一年前聊过的东西还能靠主动搜索找回来。4. 常见问题与排查技巧实录4.1 高频故障速查表这个项目在实际使用中遇到的最典型问题我整理了一张速查表方便你对照排查症状可能原因解决方案记忆完全没有注入检索结果为空或注入模板被禁用检查mem_search日志的关键词命中数调低召回阈值模型总是主动提起记忆注入模板中的引导词不对把“不要主动提及”改成“仅作为背景事实使用”记忆库膨胀很快抽取规则太敏感无关句子都入库提高规则粗筛门槛或调整置信度阈值到 0.85 以上旧记忆覆盖了新记忆忘记打superseded标记检查写入逻辑确认新事实写入时是否执行了旧记忆标记多用户数据互相串检索时没有按用户 ID 过滤给每个用户单独建一个 SQLite 文件或加统一的 user_id 条件抽取任务延迟太久队列积压空闲窗口不足把批量抽取的任务放到独立线程设置最长等待时间最坑的一种情况是记忆服务本身正常但对话服务在拼接 prompt 时把注入块的位置放错导致模型把记忆当成了系统指令。我建议把注入块放在 system 角色内容的中间位置前面保留原始系统设定后面留一小段“结合上文已知事实做出回复”的引导整体效果最好。4.2 模型回复质量反而不升反降怎么办有些朋友照着示例接了记忆服务发现加了记忆后模型回答反而变空泛、变啰嗦。我排查过几次原因出在“记忆条目过于碎片化”上。比如一条记了“用户喜欢蓝色”又一条记了“用户喜欢深色”还有一条“用户不喜欢花哨的配色”三条拼在一起以后模型反而不知道听哪条。解决办法是引入“记忆合并”机制同实体、同属性的记忆做相似度检测相似度超过阈值就自动合并成一条综合表述。合并不是字符串拼接而是做一次精简改写merged_content 用户偏爱深色系配色深蓝、黑灰不太接受高饱和度的花哨配色同时把原来旧条目的weight减半确保新合并条目在检索排序中优先冒出。这一招对偏好类记忆特别有效合并后回复的确定性和一致性明显提升。这个维护动作我起初是每周手动跑一次脚本后来写成了定时任务每天凌晨执行。4.3 检索召回率不足的调试技巧如果你发现很多该想起的记忆其实没被召回不要急着上向量库。先用几条典型的用户输入跑一遍mem_search的 debug 模式看关键词命中情况。大部分场景下问题出在“抽取时存进来的词和用户查询时用的词根本不是同一套表达”。我在项目中做了一层极轻量的同义词扩展只维护了一个小词典比如把“喜欢”“偏爱”“偏好”“中意”映射到同一组词根。这个词典只有几十条但覆盖了高频表达效果比想象中的好很多。更进一步可以对记忆内容做分词并存储关键词倒排表。没必要追求大而全的分词组件直接对中文按常用词表滑动匹配即可。倒排表顺手解决了模糊查询慢的问题现在几万条记忆在本地检索耗时能压到 50 毫秒以内对对话场景来说完全是可接受的。4.4 本地存储进程崩溃的数据恢复SQLite 虽然稳但服务进程如果被强杀偶尔会遇到database is locked或者 WAL 文件异常。我在项目中用了 WAL 模式并且在每次写库后不立即关闭连接而是用一个常驻连接池保证写入串行化大幅度降低锁冲突。如果真碰上数据库损坏SQLite 自带的.recover命令能救回绝大多数数据。另外我做了每日凌晨的全量备份直接复制数据库文件到backup/目录保留最近七天版本。一个小细节复制前要执行一次PRAGMA wal_checkpoint(TRUNCATE);不然 WAL 文件里还有未合并的数据冷拷贝可能会丢尾巴。5. 如果再往下做这几点强烈建议你先搞定5.1 记忆可视化调试后台写代码的人可能觉得命令行日志够了但实际用起来记忆数据还是“看得见”比较踏实。我后来给claude-mem加了一个极简的本地 Web 页面就是列出当前库里的全部记忆条目、来源会话ID、最近触发时间和权重支持搜索和手动删除。这个后台帮了大忙——用户反馈“AI 怎么老说我是做金融的”我打开后台一看原来是一条三个月前的错误记忆排在最前面。当场能改不用去翻代码。5.2 接入带时间感知的自动任务记忆服务里保留的“事件”数据天然适合做提醒类功能。比如用户提到“下周出差”事件表里已经有时间字段那就可以加一个定时任务在出差前一天触发一条推送消息。这块还不需要动模型纯粹是程序逻辑。很多人问记忆工具除了喂 prompt 还有什么用实际这就是最有用的延伸把对话里提到的待办事项和时间点结构化出来本身就是一个轻量级的日程系统。5.3 迁移到共享记忆层如果你同时接入了多个模型服务比如代码补全、聊天、语音助手都调不同的模型可以考虑把记忆服务独立成团队共享层。这样用户在 A 场景中告诉 AI 的偏好在 B 场景里也能生效。实现上只需要把 SQLite 换成 PostgreSQL把检索接口改成 REST 并加上鉴权整体架构不用大改。我这个项目最初就是个人自用后来发现这个思路完全可以作为团队内部的一个基础服务来维护。关于 claude-mem 项目的一些个人体会这阵子把这个记忆工具从零写到能稳定跑我最大的体会是记忆系统的难点不在存储而在判断“什么值得记”和“什么时候该忘”。模型本身已经给了很强的语义理解能力但作为开发者你需要通过精心的数据分层、检索排序和注入模板设计才能让这份理解真正服务于跨会话的场景。如果你也在做类似的对话应用我建议从小而美的方案开始先跑通核心闭环再逐步加复杂度。别一上来就上重型的向量库和分布式存储很多时候一个本地文件数据库加一套聪明的权重算法就已经能带来很明显的体验提升了。