
1. 我为什么会去做一个叫 ai-memory 的东西先交代一下背景。我之前做过几个基于大语言模型LLM的应用最头疼的问题不是模型回答得不好而是它什么都记不住。用户上午在对话里交代过我在备孕不能推荐含酒精的护肤品下午再问护肤建议模型就会一本正经地推荐含视黄醇的产品客户上周说预算只有五万这周让他看方案模型已经把报价干到八万去了。不是模型笨而是每次请求都是独立的上下文窗口关掉就归零它根本没有记忆这个概念。这就是 ai-memory 这个项目最初的出发点给 LLM 一个真正可用的长期记忆系统让它在多轮对话、跨会话场景里能够记住用户的关键信息、偏好和历史行为而不是靠把整段聊天记录塞进上下文窗口里硬凑。市面上已经有一些记忆框架但要么太重要么和具体业务耦合太深要么对中小团队不够友好。我想做一个轻量、可拆解、能在生产环境里真正跑起来的记忆模块于是就有了这个项目。这个项目解决的问题很具体模型上下文窗口有限无法承载长期历史聊天记录越长推理成本和延迟越高而且无关信息会稀释有效信息更关键的是没有记忆机制个性化体验就无从谈起。适合谁看如果你在做客服机器人、AI 助理、教育陪练、陪伴类应用或者任何需要记住用户的场景这篇文章应该对你有用。我不会把标准答案抄一遍给你而是把我从零搭建 ai-memory 的思考过程、踩坑经历、选型理由和最终的架构完整摊开。你能看到的不只是代码还有那些文档里不会写的判断依据。2. 拆解记忆的本质需求不是记住聊天记录这么简单很多人一提 AI 记忆第一反应就是把历史消息存到数据库下次查出来拼进去这是最粗浅的理解也是最容易翻车的方案。原因在于记忆和聊天记录是两回事。聊天记录是原原本本的流水账记忆是从流水账里提炼出来的、对后续决策有影响的稳定状态。2.1 先分清三种记忆层级我在设计 ai-memory 时一开始就把记忆分成了三层因为它们的存储形态、访问频率、失效策略完全不同。第一层是工作记忆也就是当前会话内的短期记忆。它不需要持久化存储只需要在上下文窗口内维护一个结构化的状态比如当前正在讨论的项目用户尚未说完的需求刚提到的一个关键数字。这一层的核心诉求是让模型在当前对话里不丢线索。实现上通常是对话状态追踪dialogue state tracking可以用简单的 JSON 结构加提示词约束也可以用更复杂的槽位填充模型。第二层是情景记忆对应跨会话的用户画像和事实偏好。用户是谁、做什么工作、有什么禁忌、偏好什么风格这些信息相对稳定适合用键值对或结构化摘要来存。这一层最容易被忽视的细节是事实偏好会随时间变化所以每条记忆需要有存储时间、来源会话、置信度不能只存一份静态文本。第三层是语义记忆对应从历史对话里提炼出的泛化知识。比如用户过去三个月都在问 Python 相关的问题最近突然开始问前端这个转变本身就是一条值得记录的语义记忆。语义记忆通常依赖向量化语义检索因为它不是一条精确的事实而是一个可以匹配相关场景的模式。这三层如果混在一起设计后面必然乱套。我见过不少团队把用户偏好直接拼进提示词结果系统塞满了过时的、互相冲突的记忆模型反而被误导。分层的意义在于每一层有不同的生命周期管理策略你可以单独清理、单独更新、单独设置访问权限。2.2 记忆的四个基本操作写入、读取、更新、遗忘记忆系统本质上是一个特殊的数据库只是它的读写接口要面向自然语言。我在 ai-memory 里把核心操作抽象成了四个写入从对话中提取值得记住的信息。关键问题是什么值得记不是每一句话都值得存存得太多检索质量会下降存得太少又达不到记忆效果。读取根据当前对话上下文召回相关的历史记忆注入到提示词中。关键问题是召回什么、召回多少、以什么顺序排列。更新当新信息和旧记忆冲突或补充时修改已有记忆而不是无脑追加。遗忘记忆过期、置信度降低、用户明确要求删除时从存储中移除或降权。这四个操作听起来简单实际实现时每一步都有坑。但把它们拆出来之后至少有了一个可以谈好坏的框架。做记忆系统不是能存能取就行而是要看这四个操作的综合质量。后面我会逐个聊我在 ai-memory 里的具体做法。2.3 一个容易忽略的前提记忆必须可解释、可干预这是我在项目推进到中期才意识到的重要问题。普通数据库出错了查一下表就知道原因记忆系统如果存进了一条错误信息用户根本不知道模型为什么突然改变态度。所以 ai-memory 从一开始就遵循三个原则记忆项必须带元数据来源、时间、置信度记忆必须能被用户显式删除系统层面必须能审计某条记忆被哪次会话读取过。这三点在客服、金融、医疗场景里不是可选项是底线。如果你只想做一个玩具 Demo可以跳过但如果你想上生产请务必在设计的第一天就考虑进去。3. 记忆的存储选型为什么我没有依赖单一方案存储是记忆系统的地基。在这一块我调研了很多方案也实际对比过 Redis、SQLite、PostgreSQL 加向量扩展、专门的向量数据库最后发现一个问题单一存储方案满足不了三层记忆的混合需求。3.1 结构化记忆用关系模型语义记忆用向量索引用户偏好、基本档案这类事实型记忆最适合用关系表存储。它的特点是字段清晰、可以精确过滤、可以方便地按条件更新。比如用户在 2024年 3月 15日 提到过对海鲜过敏这就是一条结构化事实你用关系表存后面做精确查询和更新都非常顺手。而语义记忆不同。用户在某次对话里说了我最近在忙一个电商项目需要在双十一之前上线这句话可能隐含了大量可复用的背景信息但你很难把它精确地拆成字段。它更适合被向量化之后存入一个支持相似度检索的索引中等用户再聊电商、聊上线计划、聊双十一的时候系统能把这个片段语义召回回来。所以我在 ai-memory 里做了一个混合存储设计存储层数据类型选型理由工作记忆会话状态、槽位Redis / 内存低延迟、TTL 自然过期情景记忆用户事实、偏好SQLite / PostgreSQL强一致、支持精确更新、容易审计语义记忆对话片段、泛化知识向量索引如 sqlite-vec / 专用向量库语义相似度召回、跨主题关联这个组合看起来并不炫技但它在生产环境里非常实用。你不必为一个简单项目引入庞大的专用向量数据库也不必把所有记忆都塞进 JSON 字段里硬扛。3.2 为什么 SQLite 反而成了我最推荐起步的选择很多人的第一反应是直接用 pgvector 或者专门的向量库我一开始也是这么想的但实际跑下来发现对绝大多数中小团队来说SQLite 是一个被低估的起点。原因有三个第一零运维成本。下载即用不用部署服务端不用处理连接池和鉴权对实验阶段的项目非常友好。第二支持 JSON 扩展和全文检索。你可以用一个 SQLite 表同时存结构化字段和序列化的上下文摘要再用 FTS5 做关键词检索中规中矩但够用。第三通过 sqlite-vec 这样的扩展可以直接在 SQLite 里做向量检索让结构化查询和语义检索走同一条数据通路。数据量在一百万条向量以内sqlite-vec 的性能完全够用查询延迟通常在几十毫秒级别。等你真的遇到性能瓶颈再从 SQLite 平滑迁到 PostgreSQL 加 pgvector迁移路径不会痛苦因为你在 SQLite 阶段写的数据访问层是抽象的。3.3 给向量索引起名字和组织的方式会影响召回质量这算是一个纯粹的经验教训。用向量索引存记忆的时候如果只是把整段对话历史无脑切块、向量化、插入召回效果一定差。原因在于语义检索对文本粒度极其敏感。我在 ai-memory 里使用了一种摘要—切片—关键词三级索引结构。对话结束后先让 LLM 生成一个包含核心事实的摘要作为第一级索引然后把对话按语义边界切成长度几百字的小段每段单独向量化作为第二级索引最后通过实体识别抽取关键词关联到结构化存储层作为第三级索引。检索时三级结果做融合重排再交给模型判断哪些真正有用。这套设计的核心逻辑是摘要保证了对全局信息的覆盖切片保证了细节线索不被淹没关键词提供了精确冷启动手段。三者互为备份任何单一通路失效其他通路还能顶上。4. 记忆写入模块我是怎么过滤和抽取值得记的信息如果说存储是骨架写入模块就是记忆系统的感知器官。它决定了哪些信息能够进入记忆也决定了记忆库的质量上限。4.1 不是所有对话内容都值得写入记忆我在最初的版本里犯过一个经典错误把用户说的每句话都尝试提取记忆点。结果系统一天存储了几千条碎片记忆后面检索时召回了一堆毫无用处的信息反而干扰模型判断。后来我给写入模块加了一个四层过滤机制信息量过滤提取出的候选记忆必须先经过一个信号强度判断句子过于平淡、信息熵过低的不写入比如嗯好的我再看看这种。时效性过滤明确指向一次性事件而非持续性状态的不写入长期记忆比如今天下午三点开会应该写入工作记忆而不是情景记忆。冲突检测与已有记忆冲突的新信息不直接追加而是触发更新流程或挂起等待人工确认。敏感信息过滤身份证号、银行卡号等敏感信息直接在写入前拦截只存脱敏特征。通过这个过滤机制之后存储进记忆库的信息量大幅缩减但有效性反而更高了。我实测过在同样的检索条件下加了过滤机制的记忆库最终回答准确率提升了接近 15 个百分点。4.2 基于 LLM 的抽取 规则约束的兜底比单靠 LLM 稳得多我的写入实现会让 LLM 负责语义理解层面的抽取但会同时在系统层用规则约束它的输出格式。具体做法是给 LLM 一个结构化的输出模板要求它返回 JSON字段包括记忆类型、内容摘要、实体列表、置信度、需要更新的已有记忆 ID。系统在拿到这段 JSON 之后再做一层程序化校验比如判断时间戳是否为 ISO 格式、置信度是否在 0 到 1 之间、敏感信息是否被成功挡掉。单靠 LLM 抽取的问题在于输出不稳定偶尔会编造不存在的记忆或者把用户开玩笑的话当成事实。规则约束的存在价值不是替代 LLM而是给 LLM 的疯狂输出装一个护栏。所有经过抽取模块的记忆都必须在程序层通过类型白名单和字段约束的校验不合格的直接丢弃并记录日志。这套双保险让我的记忆写入错误率降到了可接受范围。4.3 记忆置信度和来源追踪是一种保险机制我见过的同类项目里很少有人在设计写入模块时就引入置信度。但我强烈建议你这么做。置信度是一个 0 到 1 的数值它反映的是这条记忆的可信程度。有些记忆是用户明确陈述的置信度给 0.9有些是模型从上下文里推测的置信度给 0.6有些是经过了多个来源交叉验证的置信度可以给到 0.95。置信度在后续至少有三个用途第一召回阶段按置信度排序高置信度的记忆优先展示第二更新阶段如果新信息置信度更高就覆盖旧信息第三遗忘阶段优先清理低置信度、长时间未命中的记忆。这是一个投入很小但回报很高的设计点。5. 记忆读取与召回这才是真正拉开体验差距的地方写入做得再好如果读取环节不能把对的记忆在对的时间送到模型面前一切都是白搭。5.1 召回不是相似度 Top-K这么简单我做第一版召回时想得很简单把当前上下文向量化和语义记忆库做余弦相似度检索取 Top-10 拼进提示词。结果非常糟糕模型经常被无关的记忆干扰。后来我发现问题出在三个方面当前上下文本身没有做结构化拆解直接向量化导致检索目标过于宽泛单一向量检索无法区分用户的长期偏好和会话中的临时状态Top-K 的结果直接拼接没有做和当前问题的相关性重排。所以我重新设计了召回链路改成三步走。第一步对当前上下文做意图识别判断用户当前处于什么任务场景第二步按照场景标签去结构化记忆层做精确过滤同时用当前上下文向量去语义记忆层做相似度检索第三步把两路结果合并用一个小型的重排序模型或基于规则的打分函数根据相关性、新鲜度、置信度加权排序。这个链路跑通之后记忆召回的精准率提升显著而且有一个额外的好处你可以精确控制每一条记忆进入提示词的顺序和位置优化模型对重要性排序的感知。5.2 记忆要从被动检索升级到主动触发真正的记忆系统不应该每次都等用户提问才去翻记忆。它应该像人一样在合适的时刻主动想起相关的事。这个合适的时刻在工程上也是有规律可循的。我在 ai-memory 里实现了一个简单的主动触发机制对话中每一轮处理完成后系统会评估当前上下文与记忆库的相关度。如果相关度超过一个阈值就把相关记忆打包进下一轮提示词如果不超过就保持轻量上下文不额外加记忆。这个机制最关键的设计是阈值可调不同业务场景对记忆敏感度的要求完全不同。客服场景希望多一点主动记忆闲聊场景则应该克制避免给用户一种被监控的感觉。主动触发还有一个用户感知层面的重要作用体验好的记忆不是无时无刻地在体现而是在最需要的时刻恰好出现。这个分寸感靠提示词是调不出来的必须在系统架构层掌握。5.3 控制注入提示词中的记忆量不是越多越好上下文窗口越来越大了动不动就是几万甚至几十万的 token于是一些人觉得反正装得下就把记忆一股脑全塞进去。这个思路是错的。记忆注入过多首先会稀释模型对当前对话核心目标的注意力。实验数据也表明过于臃肿的上下文反而降低模型对关键信息的利用率。其次注入过多记忆意味着每次请求都要消耗更多 token成本急剧上升。还有一个更隐蔽的问题记忆里的信息可能与当前对话矛盾注入得越多冲突概率越高模型越容易陷入混乱。我给自己定的原则是单轮对话注入的记忆不超过 5 到 8 条每条记忆压缩成一句话总长度控制在 300 到 500 token 以内。这个数值不是拍脑袋定的是我在多组实验里对比出来的甜点区。小于这个范围模型感知不到个性大于这个范围收益递减甚至转负。6. 更新与遗忘记忆维护里最脏最累也最容易被忽略的活如果只实现了读写记忆系统用上几个月就会变得又脏又乱。原因很多用户搬家了、换工作了、口味变了系统里却还存着三年前的旧信息。这时候记忆不仅没用反而是负担。6.1 冲突检测与版本化更新我在系统里给每条记忆加了一个版本号。当写入模块提交一条与已有记忆同主题的新信息时系统不是简单覆盖而是进入一个更新流程如果新记忆置信度高于旧记忆则直接覆盖并在 changelog 里记录旧版本如果新记忆置信度低于旧记忆则保留旧记忆把新记忆挂到候选队列等待验证如果两者置信度接近但内容矛盾则标记为需要用户确认在下一次对话中主动向用户提问。这个版本化策略的好处是你不会因为一次模型的误判就永久破坏用户画像。即使某次抽取错了也可以通过回滚机制恢复。6.2 遗忘策略生命周期管理长期跑下来的记忆库一定会过时所以遗忘机制不是可选项。我实现了三个遗忘入口时间衰减每条记忆都有一个 last_accessed 字段长期未被召回的语义记忆系统会定期下调它的优先级最终移入冷存储用户主动清除在对话中模型检测到用户表达出不要再记住我这个信息的意思时触发删除流程定期重审每周对低置信度、低命中次数的记忆做一次批量清理避免库无限膨胀。你可能觉得遗忘机制不重要但事实上一个能诚实面对遗忘的记忆系统反而更容易获得用户的信任。因为用户能感觉到这个系统不会一直拿旧信息来烦他。7. 生产环境实测我在 ai-memory 上踩过的记忆相关的坑这章写的全是我自己跑过的真实案例没什么华丽的理论但都是实测真话。7.1 坑一向量召回把反例也招进来了有一次用户问我不吃什么系统本来该召回用户不吃香菜结果因为向量检索的语义相似度偏好把用户最喜欢吃香菜这条也召回了。我查了日志发现原因是库里有一篇早期的对话切片内容是香菜我一口都不吃这句话被向量化之后和提问的语义距离其实很近但它的情感倾向是完全反的。这个坑的教训是语义相似度不等于命题一致性向量检索对否定、转折、反讽这类语言现象非常迟钝。后来我在存储层给每条记忆增加了议题标签和情感极性两个字段召回后先做一次规则层过滤把同议题但极性相反的候选记忆排除掉问题才基本解决。7.2 坑二多轮更新时记忆相互覆盖导致信息丢失用户在 3 月说我在北京工作系统存下5 月说我搬到上海了系统识别到冲突更新成在上海工作。这个逻辑看起来是对的但问题是用户在搬家过程中还提到过很多其他关联信息比如新公司在张江上下班要坐 13 号线。这些信息因为没有绑定到同一个记忆实体 ID 上在更新时没有被联动处理结果出现了在上海工作但每天坐北京 13 号线的荒唐画像。这个体验教训让我明白光更新单条记忆是不够的还得做关联记忆的联动。后来我引入了实体中心化的存储模型所有记忆项围绕实体用户、公司、地点、项目组织更新一个实体属性时相关实体的关联记忆也会进入待更新队列。7.3 坑三记忆命中率热力图是天气图不是瀑布图我原以为记忆的使用频率会呈幂律分布几条核心记忆高频命中大量冷门记忆被遗忘。但实际统计下来记忆命中率的热力图更像是天气图区域性的热点不断漂移今天客户全在聊退款政策明天全在聊配送范围。这意味着依靠静态遗忘策略很危险。你必须把近期时效热点纳入记忆召回的加权因子里。我给所有记忆增加了一个热度指数每轮会话结束后由后台任务统一更新对近期高频涉及的实体和议题加权对冷门议题降权这样记忆库才能跟着业务焦点共舞。8. 一个最小可用架构总结照抄即可的骨架最后把 ai-memory 当前的完整架构归纳成一个可以直接照搬的骨架你能在项目里复用多少算多少。核心组件一共六个输入接口接收所有对话文本做清洗、分段、意图识别抽取与过滤模块负责生成候选记忆执行四层过滤输出结构化记忆项存储层包含 SQLite 或 PostgreSQL 的关系表、向量索引、缓存层 Redis召回与重排模块负责从存储层混合检索候选记忆打分排序按规则注入提示词更新与遗忘模块负责冲突检测、版本化更新、时间衰减、用户主动删除审计日志模块记录每一次记忆写入、读取、更新、删除操作生成可用日志。在提示词组装层面我的内置模板里有三个明确的记忆插槽用户基本档案、近期相关事实、历史对话摘要。每次组装时由召回模块按分数填充空的插槽就不填避免硬塞。如果你从零开始搭一个 ai-memory我强烈建议这样做先用 SQLite 加最简单的 JSON 存储跑通闭环只在提示词里加入根据以下用户记忆信息回答一个模板插槽跑通之后再逐步引入向量检索和分层架构。千万不要第一步就上微服务、上专用向量数据库、上复杂的抽取流水线那样你大概率会在三个月后被维护成本拖垮。9. 后续演进方向与我对记忆系统的一点真实体会ai-memory 目前已经在我自己的几个对话类项目里稳定运行。比起初版现在的版本在召回精准率、存储成本、维护复杂度上都好了很多但我知道它离一个真正类人的记忆系统还差得远。接下来我想尝试的方向有三个第一是让记忆系统支持跨设备、多会话之间的协同记忆同一个用户在不同入口留下的信息能自动合并第二是加入对用户隐性反馈的感知比如用户对某个推荐的点击率、忽略率用来动态修正记忆的权重第三是让记忆系统具备更强的抽象能力能自动归纳出用户的思维模式而不仅仅是记住事实。第三个方向很难但它才是记忆系统真正的天花板。最后分享一点我自己的真实体会。做记忆系统最大的成就感不是模型记住了多少东西而是用户在某个时刻突然说你居然记得我之前说过的事然后明显更愿意继续聊下去。这个瞬间会让我觉得之前处理的那些脏活——过滤、去重、冲突检测、遗忘——都是值得的。我踩过的坑不算少也花了很多时间在看起来不重要的细节上。但记忆系统这个方向有一个好处它解决的问题足够真实只要你愿意一步一步把架构做扎实用户是能感受到差异的。希望你在这个方向上的第一版比我的第一版少走一些弯路。