我最早意识到 AI 缺“记忆”这件事,是在一个特别尴尬的场景。头天晚上我花了大半个小时跟模型聊清楚一个数据清洗脚本的痛点,第二天早上打开新会话问它“那个脚本的问题还记得吗”,它回了我一句“请问你说的是哪个脚本”。那一刻我就明白,上下文窗口再大,也撑不起“记得用户”这件事。
后来我在自己的项目里折腾 claude-mem 这个记忆层方案,逐步把“给 AI 接上外挂记忆”从 demo 做成了能稳定跑的服务。很多人以为记忆就是“把历史消息都塞回上下文”,真做一遍就会发现完全不是这么回事:要判断什么值得记、什么该忘、怎么在用户问的时候自然想起来、怎么不让记忆反过来污染回答。这篇文章就把我整个落地的过程、踩过的坑、以及最后沉淀下来的方案完整写出来,给同样在做 AI 应用但被“上下文断片”折磨的朋友参考。
1. 上下文窗口不等于记忆:问题到底出在哪
1.1 大模型每轮对话其实都是一次“失忆重启”
先说一个很多人忽略的技术事实:模型本身是不保存对话历史的。你每次发消息,应用层都要把之前的聊天记录手工拼在一起,塞进上下文窗口,模型才“看起来记得”。窗口再大也只是临时容纳,不是长期记忆,一旦超出长度,要么截断,要么被更早的消息挤掉,而且每轮都重新传一遍历史,Token 成本也跟着涨。
我一开始也踩过这个误区:以为只要把上下文窗口从 4k 换到 16k,用户就不会觉得 AI 失忆了。结果窗口大了,问题反而更隐蔽——模型读的文本太长,中间的关键内容会被稀释,用户问“我之前提到的那个项目”,模型经常找不到那条信息。这跟人读书读到最后忘了开头是一样的,窗口大了但注意力是有限的,不可能靠堆历史解决问题。
1.2 记忆不是“历史记录”,它分三种
真正做记忆层,必须先区分三种性质完全不同的信息。
第一种是工作记忆,就是当前这轮对话里正在进行的状态,比如用户刚说“今天的会议推迟到下周”,这个临时信息只需要在当次会话内生效,不必跨会话保留。第二种是情景记忆,指的是过去具体发生过的对话事实,比如“上周用户聊过准备换工作,还在犹豫”。第三种是语义记忆,是从多次交流中归纳出的稳定偏好,比如“用户喜欢简洁回复,讨厌长篇大论”。
claude-mem 这个名字听上去像是给某个模型加记忆,实际做的是后两种:把跨会话的事实和偏好沉淀成可检索的长期记忆。工作记忆继续交给上下文处理,长期记忆走另外一套存取体系,两者不混在一起。这样每次请求带的历史可以控制在足够小的范围,而真正需要“记住”的东西由记忆库承担。
2. claude-mem 的核心架构:短期缓冲与长期仓库的分工
2.1 一条消息从进入到归档的完整流转路径
整个系统的核心思想其实很简单:把“对话过程”和“记忆沉淀”拆成两条线。用户发消息进来,主流程照常处理,把消息喂给模型回答;同时在后台跑一条异步链路,判断这段话里有没有值得长期记住的东西,如果有,就提炼成一条结构化记忆写入仓库。
我最终落地的路径是这样的:
用户消息到达应用后端,先进入会话缓冲,也就是当前对话的上下文管理模块。主线程把最近的若干轮消息组装好,直接请求模型并返回结果给用户,这个流程和普通聊天应用没有任何区别。与此同时,后端把这条用户消息丢进一个异步队列,记忆提取 worker 拿到消息后做两件事:第一,判断是否有值得保存的信息;第二,如果有,就生成一条短文本记忆,做向量化,写入长期记忆库。
这套流程最关键的设计就是异步。最开始我偷懒,在同一个请求里先调用模型判断“这句话值不值得记”,再调用向量库写入,结果用户的每条消息都要多等一两秒。后来改成异步写入之后,用户完全没有感知,记忆存储的成功与否也不影响主对话的响应速度。这个取舍后来被证明是整个系统能稳定上线的前提。
2.2 数据模型:一张关键的记忆表
记忆库的表结构不需要很复杂,但有几个字段特别重要。我用的核心表大概长这样:
CREATE TABLE memories ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, session_id TEXT, content TEXT NOT NULL, memory_type TEXT NOT NULL, -- user_preference / user_fact / task_state / conversation_summary importance_score REAL NOT NULL, -- 0.0 - 1.0 embedding_id TEXT, created_at TIMESTAMP NOT NULL, last_accessed_at TIMESTAMP, access_count INTEGER DEFAULT 0, status TEXT DEFAULT 'active', -- active / superseded / archived / deleted source_message_id TEXT UNIQUE );这里每个字段都有自己的用途。user_id 是隔离的第一道锁,所有查询都必须带这个条件;content 是最终注入提示词的记忆原文,应该是一句完整、无歧义的自然语言,不要存碎片化的原始消息;memory_type 用来区分偏好、事实、任务状态,不同类型在检索排序时的权重不一样;importance_score 是记忆的长期价值打分,决定了它会不会被优先召回、会不会在清理时被保留。
还有一个容易忽略的细节:source_message_id 加唯一约束。因为写入是异步的,worker 可能重试,不加这个字段,同一条消息被处理两次就会出现重复记忆。这也是我踩过一次的坑——测试时发了同一条消息三次,库里出现三条几乎一样的记忆,后来加了这个唯一键才解决。
2.3 为什么必须用向量检索,而不是直接搜关键词
记忆检索的核心矛盾是:用户提问的说法,和当初存档时的说法,几乎不可能字面一致。用户三周前说的是“我最近在发愁周报怎么写更省时间”,三周后他问的是“那个自动生成周报的方法还有没有”,两句话没有任何共享关键词,全文搜索完全无能为力。
向量检索解决的就是这个语义鸿沟。把记忆文本和用户当前问题分别编码成向量,在向量空间里找到语义最接近的若干条记忆。这个思路在技术圈已经很成熟,但我建议不要一上来就上重型向量库,数据量小的时候,用轻量方案先跑通流程更重要。我在早期阶段就是先在本地的轻量关系库里存向量字段,配合应用层算余弦相似度,数据量到了几万条之后,才迁移到独立的向量库做分区存储。
向量检索也不是万能的,它召回的是“语义相似”而不是“绝对正确”,所以后面还需要一整套重排、过滤、注入的控制逻辑,这部分我会在下一章展开讲。
3. 检索注入实战:怎么让 AI 在合适的时候“想起来”
3.1 召回、重排、注入:三步把记忆变成模型的“已知信息”
每次用户发消息,记忆模块要做三件事。第一步,把当前的问题向量化,在用户的记忆空间里召回 topK 候选;第二步,对这些候选做重排,按照重要度、时间衰减、与当前问题的相关性综合打分,砍掉明显不相关的;第三步,把最终通过的记忆拼装成一段“已知信息”,插入到系统提示词里,再交给模型回答。
我实际使用的注入格式大概是这样:
[系统] 你是一个有帮助的助手。下面是你对用户的部分已知记忆,仅供参考: - 已知事实:用户目前居住在杭州,从事数据分析相关工作 - 长期偏好:用户喜欢分点回复,希望答案控制在 500 字以内 - 任务状态:用户正在处理一个销售报表的自动化生成脚本,卡在日期格式转换 如果上述记忆与当前问题无关,请忽略它们。请基于用户当前输入和对话历史回答。这里最核心的一句话是“如果与当前问题无关,请忽略它们”。一开始我没有写这句,结果模型遇到任何问题都会尽力往记忆上靠。用户问“今天天气怎么样”,模型非要提一句“根据您的记忆,您在杭州”,看着贴心,其实很烦人。给模型一条“记忆仅供参考”的逃生通道,回答质量会明显提升。
3.2 Token 预算控制:一次对话最多带多少记忆
记忆不是带得越多越好。每一条注入的记忆都吃 Token 配额,而且会干扰模型对当前对话的注意力。我给记忆区设了一条硬规则:单请求的记忆注入量不得超过整体输入预算的 30%。
举个例子,某次请求的输入预算总共 8k Token,系统提示词和当前对话已经占掉 4k,留给记忆区的只有 2.4k。按平均每条记忆 60 到 80 Token 估算,最多只能带 30 到 40 条。真到了这个数量,问答效果已经开始变差了。我最终把线上参数调成默认 15 到 25 条,效果最稳定。
重排的时候不能只看向量相似度。我自己的排序公式大概是:最终分 = 语义相似度 × 0.5 + 重要度 × 0.3 + 时间衰减系数 × 0.2。其中时间衰减系数由上次访问时间和创建时间共同决定,三个月前创建但最近频繁访问的记忆,分数反而高于昨天创建但再也没碰过的记忆。这套权重可以在应用里做成可配置项,不同业务调的侧重不一样。
3.3 召回翻车现场:撞车、漂移、记忆污染
这块是我最想分享的经验,因为光看文档根本学不到。
第一个经典翻车是“短 query 召回噪声”。用户发一句“你觉得呢”,这句的向量非常泛,几乎和库里所有记忆都有一些距离,但都没有特别近。结果召回回来的往往是随机散落的记忆,模型被“我不知道该不该把某个记忆拿出来”的情况弄得无所适从。我的解决办法是对短 query 做改写,比如检测到问题少于 5 个字,就把它扩展成“用户正在询问对某件事的看法,请结合已知信息辅助判断”,这样向量检索的语义中心会更明确。
第二个翻车是“记忆过时但没被察觉”。用户三个月前说“我在上海做项目”,系统一直把这条当成当前事实。用户后来问“你建议我住浦东还是浦西”,模型理所当然地帮他分析浦东浦西,完全不知道他人已经搬到南京了。这件事让我意识到:光有记忆没有时间意识是很危险的。后来所有记忆写入时都强制带上 created_at,检索时如果记忆创建时间超过一定阈值,注入文本前会加前缀“注:这是三个月前的信息,可能与当前情况不符”。
第三个翻车是偏好记忆的误用。用户去年说过一次“我喜欢简短回复”,模型在后续对话里就疯狂压缩答案,连用户明确说“这次请详细解释”的时候,依然只写三句话。本质原因是系统把一条临时偏好当成了永久的刚性约束。我的修复方式是区分偏好类型,关于表达风格的预设,只在用户没有明确指示时才作为默认值;用户一旦在当前对话表达了明确指令,当前指令优先级永远高于历史偏好。
4. 记忆写入与遗忘:比存储更难的治理问题
4.1 什么值得记,什么必须扔
我把值得长期记忆的信息分成四类:与用户身份相关的事实,比如所在城市、职业、家庭状况;用户明确表达的偏好,比如“不要发语音”“回复里多给数据”;正在推进的任务状态,比如“在写某个脚本,卡在权限问题”;还有跨多次对话反复出现的话题。不值得记的包括一次性的寒暄、临时地点、情绪化发泄,以及那些只对当前对话有用、过后毫无价值的信息。
判断要不要记,不能靠写死的规则,因为自然语言太灵活了。我实行的方案是让提取模型做分类,用一个轻量级调用完成:
请从下面的用户消息中提取值得长期记住的信息。只输出 JSON,不要解释。 { "should_remember": true/false, "memory_type": "user_fact|user_preference|task_state|none", "content": "一句话总结需要记住的信息", "importance": 0.0-1.0, "sensitive_level": 1/2/3 } 如果不需要记住,返回 {"should_remember": false}。这个方案的提取率大概是 6% 到 9%,不是每条消息都会产生记忆。我见过有人把每条对话都存进来,结果是记忆库膨胀得飞快,召回时全是噪声,比没记忆还不如。记住一个原则:少而精永远优于多而杂。
4.2 去重、冲突检测与“最新表述优先”
用户的信息是会变的。他第一次说“我在上海”,三个月后说“我现在搬到北京了”。如果系统把这两条都视为有效记忆,召回时两条同时出现,模型就会犯错。
我的做法是冲突检测加版本化处理。写入新记忆之前,先对同 user_id 下同类型的记忆做一轮相似度比对,如果发现高度相关的旧记忆,就把旧记忆标记为 superseded,新记忆作为最新版本生效。被替代的旧记忆不会立刻删除,只是不再参与常规召回,只有在用户主动追溯历史的时候,系统才会把旧版本翻出来。
这里面有一个细节:不能因为用户说过一次“我改主意了”就永远以最后一次为准。比如用户某天心情不好说“我不想再用这个产品了”,第二天又正常使用,如果系统把前天的气话当成最新偏好,就会一直呈现消极状态。所以我设置了“记忆冷却期”:高重要性的事实类信息,如果发生了冲突更新,需要等待一段时间并且用户后续没有再次否定,才正式替换旧版本。这个设计帮我挡住了不少“情绪化更新”。
4.3 遗忘机制:为什么记忆越少效果反而越好
记忆系统最大的风险不是“记不住”,而是“记得太多”。我观察过一个测试账号,用了两周之后,库里攒了一千两百多条记忆,每次召回都像大海捞针,真正重要的信息反而被淹没。
我最终采用两级遗忘策略。被动遗忘靠衰减:每条记忆维护一个综合分,由重要度和访问新鲜度共同决定,长时间没有被检索到的记忆会自动降权,降到阈值以下就进入归档区,不再参与召回。主动遗忘靠用户指令:用户明确说“忘掉我之前讲的那件事”,系统会立即做语义匹配,把所有相关记忆标记为 deleted。
每年的每月审计也是我坚持做的事:导出一份低访问量记忆清单,用一批量调用让模型读一遍,判断哪些记忆已经过时,把确认为过时的记忆批量归档。这个操作听起来很重,但一个月做一次,每次成本完全可控,换来的效果是召回精度能持续保持在稳定水平。记忆治理不是一个一次性任务,它是伴随系统生命周期持续运营的长期工作。
5. 从原型到稳定运行:性能、隔离与部署细节
5.1 异步写入链路和失败重试
前面提到过,记忆提取绝对不能设计在用户请求的主链路上,否则每句话都慢一两秒,用户能明显感知到卡顿。我用的方案是引入一个任务队列,把“消息归档”变成一个异步事件。
流程文字描述就是:前端回调进应用网关,会话服务正常生成回复并返回,这个阶段完全不需要知道记忆是否存在;同时把当前消息的 ID 和 user_id 发到任务队列;记忆提取 worker 从队列里拉任务,调用提取模型做分类;如果判定为需要记录,做向量化并写入记忆库;写入成功之后更新该用户的缓存时间线。
这个链路有三个容易翻车的地方。第一个是幂等,前面说了用 source_message_id 唯一约束,这能保证队列重试不会产生重复记忆。第二个是重试策略,worker 挂了不能静默失败,我给写入操作设计了三次指数退避重试,仍然失败就放进死信队列,等人工介入。第三个是削峰,遇到大量历史消息需要批量归档时,队列要控制消费速率,避免一次性把提取模型的额度打爆。
5.2 多用户隔离:记忆绝对不能“串台”
记忆是用户的隐私资产,多用户之间出现串台,性质比功能 bug 严重得多。这里不只要在代码里过滤,更要从架构上强制隔离。
我给每个用户分配一个独立的向量空间命名空间,所有检索和写入都限定在该命名空间内;关系型数据库里则使用 user_id 作为所有查询的强制过滤条件。两个地方同时起作用,哪怕某一层过滤条件写漏了,另一层还能兜住。这个方法不是多余,而是必要。
我吃过一次亏:做联调测试时,一个接口临时去掉了一处 user_id 校验,导致某个用户检索到了另一个用户的记忆。虽然只是测试环境,但那次之后我定了一条规矩:记忆模块的每个检索入口,必须同时有显式的命名空间检查和 user_id 条件,单靠一层隔离不做上线审核。
5.3 实测开销数据:一个项目的真实量级
为了给你一个直观的概念,我放在一个模拟项目 X 上遇到的真实数据。这个项目有 20 个测试用户,连续运行了 30 天:
| 指标 | 数值 | 备注 |
|---|---|---|
| 日均消息量 | 约 3000 条 | 包含群聊和单聊 |
| 日均新增记忆 | 180 - 220 条 | 提取率约 6% - 8% |
| 记忆库总量 | 约 9500 条 | 经过归档和去重 |
| 单次检索延迟 | 35 - 80 ms | 向量库本地部署 |
| 记忆注入 Token 增量 | 平均 850 Token/请求 | 约占输入预算 12% |
| 误召回率 | 约 6% | 指与当前问题完全不相关的记忆 |
这个量级整个系统完全撑得住,延迟主要消耗在模型响应上,记忆检索的那几十毫秒可以忽略不计。如果数据量继续涨到几十万条,我会考虑做一层热记忆缓存,把高频召回的固定事实直接放在应用内存里,减少对向量库的查询压力。
6. 记忆安全:这是功能,更是责任
6.1 敏感信息分级,不该进库的坚决不进
做记忆系统最怕的一件事,就是把用户的敏感信息原样存起来。用户可能在闲聊中说出手机号、住址、公司内部信息,这些内容如果进了长期记忆库,等于把敏感数据放在一个随时可能被检索到的位置。
我设计的处理规则很简单,分三级。一级信息可以直接记忆,包括昵称、兴趣爱好、工作行业、常用软件等。二级信息需要脱敏,比如手机号、家庭住址,记忆库只存“用户联系方式已验证”这个事实,绝不存完整号码和详细地址。三级信息直接禁止入库,包括密码、密钥、银行卡号、身份证号、疾病诊断等,提取模型如果识别到这类内容,只返回“已识别到敏感度较高的信息,拒绝存储”。
实现时就是在提取模型的 prompt 里写清楚分级规则,并要求输出 sensitive_level 字段,应用层再根据这个层级决定落库还是丢弃。我给写库接口加了一层强制校验,敏感级别为三级的记忆,即使模型误判,也会被后端的过滤规则拦截。
6.2 遗忘权:用户说删,就必须真的删
用户要求删除记忆的时候,系统必须做到从里到外彻底清除。这比写代码听上去难,因为数据可能存在多个副本:主数据库、向量库、缓存、备份。任何一处漏删,都可能在未来某次检索中“复活”。
我的删除流程是:先在记忆表里把目标记忆标记为 deleted,并写入一个不可见的墓碑清单;然后从主库删掉记录;最后调用向量库的删除接口删除对应向量。墓碑清单的作用是防止备份恢复后,已经删除的向量又通过缓存或其他路径被召回。这个清单不需要长期保留,但至少覆盖备份的留存周期。
这里有个容易被忽视的点:用户说“删除记忆”,可能不是指具体某一条,而是指“所有与我相关的记忆”。所以产品层面我设计了两个级别:单条删除和全量清除。全量清除会触发整个用户命名空间的清理流程,包括归档区和缓存层。删除操作的每一步都要留审计日志,方便出问题时追踪。
6.3 记忆投毒与“用户否定优先”原则
记忆投毒是我在研究安全边界时发现的一个不可忽视的问题。攻击者可以伪装成用户,发表一些误导性内容,让提取模型错误地写入一条完全不符合事实的记忆。比如某用户明明没有说过某句话,系统却把“用户表示自己非常喜欢某种危险操作”当成事实存了下来,后续所有对话都会被这条污染记忆误导。
防御不能只靠提取模型自觉。我加了两道防线。第一道是来源可信度:只有用户主动陈述的事实才允许写入高重要性档案,模型从闲聊里推断的内容,重要度会自动降档。第二道是用户否定优先:用户明确说“不对,我没说过这个”“我不喜欢这样”,系统会立刻把那类记忆全部标记为不可用,并以用户当前的否定表述为准。
这两个机制合起来,配合定期的记忆审计日志抽查,基本能挡住绝大多数记忆投毒场景。做记忆系统的人必须时刻记住:记忆的目的是更好地服务用户,而不是把未经核实的内容固化成对用户的错误画像。
做长期记忆服务一年下来,我的最大感触是:这个系统最难的从来不是存储和检索,而是搞清楚“什么时候该记住,什么时候该忘掉”。我现在养成了一个习惯,每周导出一份本周新增记忆清单,逐个扫一眼,看到那些“看似有用但永远不会被用到”的记忆就顺手清理。真正好用的记忆系统,不是让 AI 事无巨细地记住一切,而是让它在最关键的时候,恰好想起你最需要的那句话。