1. 先搞清楚:AI的"记忆"和人类的记忆差在哪
做过大模型应用的人应该都有同一种挫败感:昨天刚和AI聊完一个项目的完整背景,今天新建一个会话,它就像完全没见过你一样,重新问一遍"你的项目具体是什么需求"。更气人的是,就算在同一个会话里,聊到第几十轮之后,你会发现它也渐渐开始"前言不搭后语"——早前说过的重要约束被它忘得一干二净。
我最早做客服机器人demo的时候,以为只要把历史对话全部塞进上下文就行,结果被打脸得很惨。后来我意识到一个根本问题:我们口中说的"AI记忆",和人类大脑的记忆机制完全是两回事。大模型本身并没有"记住你"的能力,它只有"在本次输入里看到过相关文字"的能力。模型权重里沉淀的是训练数据里的世界知识,那是它"天生就知道"的东西;而你在对话中提供的那些个性化信息,如果没有通过某种机制固化下来,这次会话一结束,对模型来说你就彻底归零。
打个比方,大模型就像一个患有严重短时失忆症的同事,他业务能力很强、知识面很广,但你每次见到他都必须重新自我介绍,所有交接事项都得写在便签上。如果便签没写全,或者便签纸条太多他翻不过来,那你的需求就真的会被忽略。
想通这一点之后,我不会再抱怨"AI怎么这么没记性",而是开始认真思考:怎么给这个"失忆的天才"搭一套外挂记忆系统。这就是我后来折腾ai-memory这个项目的起点。它解决的核心问题特别朴素:让一个本质无状态的模型,在跨会话、跨时段、跨任务的场景下,表现得像它"记得"你。
这篇文章不聊那些玄乎的"意识"或"认知",我只讲我踩过的坑、验证过的方案,以及一套可以直接落地的记忆系统设计思路。如果你正在做大模型应用、Agent、聊天机器人,或者任何需要"记住用户"的AI产品,这篇文章应该能帮你少走不少弯路。
2. 从上下文窗口说起:为什么所有AI应用最后都会撞上"记忆墙"
2.1 上下文窗口不是无限硬盘,而是转瞬即逝的便签纸
现在主流大模型的上下文窗口越开越大,从最早的4K、8K,到现在的128K、200K甚至更多,看起来"记忆空间"越来越充裕。但实际用下来你会发现,窗口变大只解决了"能不能装下更多内容"的问题,远远没解决"内容能不能被有效使用"的问题。
我先说一个所有做过长对话的人都经历过的现象:把一份5万字的项目文档塞进一个200K窗口的模型,让它基于文档写总结,它在开头部分的表现还行,但只要关键信息埋在文档中段,它就经常抓瞎,甚至虚构出文档里根本没有的内容。这不是模型能力不行,而是注意力分布的问题。学术界有个说法叫"lost in the middle",就是模型对长上下文中间部分的内容注意力显著下降,开头和结尾的信息更容易被利用。
这意味着什么?意味着就算你堆了200K的历史记录给模型,它真正能"有效记住"的,可能只相当于头尾那几十K的内容,中间全被"视而不见"了。所以我在设计ai-memory的时候,第一原则就是:不要试图把所有历史都塞进上下文,那是一条注定会撞墙的路。
2.2 除了注意力衰减,还有成本失控
还有一个很现实的问题:Token是要花钱的,而且长上下文的成本增长是线性的,甚至更糟。
我做过一个测算:假设一个客服Agent,平均每轮用户输入100个token,如果你想让它"记住"之前全部的100轮对话历史,那第100轮请求的input token就是100×100=10000个token,其中9900个token都在重复往昔内容。这一天下来几百个请求,Token费用哗啦啦地涨。到了第500轮,单轮请求就得上5万token,成本直接翻上天。
更要命的是,很多模型API按输入Token收费,这部分是不可省的。所以就算窗口足够大、注意力衰减可以接受,钱包也不答应。
2.3 "记忆墙"的本质:不管窗口多大,你总会走到撑爆的那天
我给这套问题起了个名字叫"记忆墙":无论上下文窗口扩到多大,只要应用是面向真实用户长期使用的,对话历史的累积量一定会超越窗口上限。你今天用200K够用,用户用三个月之后呢?六个月呢?
所以,任何想长期服役的AI应用,早晚都要面对一个问题:你得学会取舍,学会把最重要的信息提炼出来,丢掉那些不重要的细节。这个"提炼—存储—召回"的过程,就是我理解的ai-memory。
3. 四种主流记忆方案,我分别踩过的坑与选型心得
先给结论:目前市面上关于"AI记忆"的实现思路,归纳下来基本是四种,没有绝对的银弹,只有适不适合你的场景。
| 方案 | 思路 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|---|
| 全量上下文拼接 | 把历史对话全部塞入Prompt | 实现简单,信息无损 | 成本高、注意力衰减、撑爆窗口 | 短会话、一次性任务 |
| 摘要记忆 | 定期把旧历史压缩成摘要 | Token省,能覆盖长时段 | 摘要过程有信息丢失 | 长对话、会话内记忆 |
| 向量检索记忆 | 历史信息存入向量库,按语义相似度召回 | 跨会话、跨主题能力强 | 需要构建索引和检索链路 | 知识库型、跨会话记忆 |
| 分层记忆 | 短期+长期+工作记忆协同 | 接近人类认知,效果上限高 | 架构复杂,调优难度大 | 复杂Agent、助理型应用 |
3.1 全量拼接:新手村的必经之路
我刚起步时就是方案一,把整段聊天记录原封不动拼到system prompt里。头几天效果还不错,因为对话轮次少,模型读起来"记忆"完整,回复也显得很懂你。但大概到几十轮之后就开始露馅——模型开始遗忘早期信息,甚至出现自相矛盾的答复。
这个坑的教训是:上下文窗口不是拿来装原始日志的。早期信息被后续内容淹没,模型在解码时并不会均匀分配注意力。方案一只适合那些"一次会话就结束"的工具型应用,比如单次翻译、单次代码审查,一旦牵扯到长周期交互,它必然崩。
3.2 摘要记忆:省钱但不省心
被全量拼接坑了之后,我转向了摘要思路:每过N轮对话,让模型用一小段话总结之前的要点,然后丢弃原始对话,只保留摘要累积。这个做法Token开销确实降下来了,而且因为摘要质量高,模型反而更容易抓住重点。
但我很快发现两个新问题:
第一,摘要本身是不可逆的有损压缩。如果某个细节在第一次摘要时没被写进去,后面就永远找不回来了。比如用户在第5轮说过"我在上海工作,周末要带娃",第8轮摘要可能总结了工作相关的部分,但"带娃"这个信息就丢了,到第20轮聊到周末安排,模型完全想不起来。
第二,摘要会累积误差。我后面采用的策略是"摘要的摘要",也就是对旧摘要再做二次摘要,这种级联压缩很容易出现信息塌方:每压一次,重要信息就稀释一轮。
3.3 向量检索记忆:跨会话的救星,但别迷信
后来我研究社区里的开源方案,发现大家都在聊向量数据库、Embedding、RAG,于是我也照着搭了一套:把每轮对话切成片段,用Embedding模型转成向量存起来,用户提问时先做语义检索,把最相关的Top-K条历史记录拼进Prompt。
这个方案属实解决了"跨会话记忆"的问题——同一个用户昨天聊过的内容,今天通过检索完全可以找回来。我也确实把这个方案用到了ai-memory的核心链路里。
但我不建议把它当成万能药。我踩过几个坑:
- Embedding模型对口语、短文本的召回效果不太稳定。用户随口一句"那个事怎么样了",向量检索很容易召回一堆"那个""这个"相关的噪音片段,真正关键的实体反而没召回。
- 相似 ≠ 相关。语义接近的信息不一定是当前对话需要的。比如用户之前抱怨过"加载太慢",日后聊"网络优化",这两段话语义上沾边,但把抱怨历史当成事实约束塞进去,模型会被带偏。
- 纯检索没有时间维度。五年前的信息和五分钟前的信息在向量空间里权重一样,这会导致模型总在回顾旧事,忽略最新的情况。
3.4 分层记忆:最接近人类,也最难调
最后我调研的是MemGPT这类带分层记忆思路的方案,架构上把记忆分成"外部存储—主上下文—按需召回"三层:核心指令和最新对话常驻,历史细节走检索,外部大库做长尾沉淀。这套思路理论上最优雅,但实现复杂度直线上升。
我自己在ai-memory里的做法,是融合了方案二、三、四的混合架构,不追求极端纯粹,而是按实际场景取舍。具体设计我放到下一章展开。
4. 自己动手搭一个最小可用的ai-memory(含代码)
4.1 架构思路:三个核心接口
我最后敲定的ai-memory架构,高度浓缩下来就三个接口:remember(写入记忆)、recall(读取记忆)、forget(遗忘或更新记忆)。整个系统就围绕这三个动作转。
记忆的存储我分成两层:
- 短期记忆区:最近几轮的原始对话,带时间戳,直接存Redis或内存,供模型在短时上下文里使用。
- 长期记忆区:经过提炼的事件片段,转成向量存入向量库,同时保留结构化元数据(时间、用户ID、重要性分数、来源片段ID)。
为什么短期要保留原文、长期只存储提炼后的片段?我的实测结论是:短期对话的关键在于还原上下文细节,模型需要看到原话才能准确理解语气和意图;而长期记忆如果也堆原文,向量库会飞速膨胀,检索噪音指数上升。所以我会在写入长期区之前加一道"提炼"动作,让模型把本轮对话的核心要点压缩成一两句话,比如"用户偏好北欧风格家具,预算30万以内,最在意环保等级"。
4.2 数据结构的定义
我用Python写第一版原型,关键数据结构长这样:
from dataclasses import dataclass from datetime import datetime from typing import Optional @dataclass class MemoryRecord: user_id: str content: str # 提炼后的记忆内容 memory_type: str # "fact" 或 "preference" 或 "event" importance: float # 0~1,重要性评分 created_at: datetime last_access_at: datetime access_count: int source_id: Optional[str] = None # 来源对话片段ID embedding: Optional[list] = None我给每一条记忆都打了memory_type标签:事实型(fact,比如"用户是上海人")、偏好型(preference,比如"用户喜欢简洁风格")、事件型(event,比如"用户上个月提过服务中断问题")。这个标签在后面做召回过滤时非常有用——如果用户当前在聊偏好类问题,可以优先召回preference,而不是一堆历史事件。
4.3 写入路径:先提炼再入库存
写入路径的伪代码我直接贴出来,每一步我都做了注释:
def remember(user_id: str, conversation_segments: list[dict]): # 1. 先把原始片段写入短期区,保留近期上下文 short_term_store.add(user_id, conversation_segments) # 2. 当短期区超过阈值(比如20轮),触发提炼 if short_term_store.size(user_id) >= 20: summary_text = summarize(conversation_segments) importance = estimate_importance(summary_text) # 3. 生成Embedding向量 vec = embed(summary_text) # 4. 写入长期向量库 long_term_store.insert(MemoryRecord( user_id=user_id, content=summary_text, memory_type=classify(summary_text), importance=importance, created_at=now(), last_access_at=now(), access_count=0, embedding=vec )) # 5. 清空短期区中已提炼的部分 short_term_store.cleanup(user_id)这里面最关键的是estimate_importance。我一开始没做重要性过滤,什么信息都往长期存,结果向量库里塞了几百条"今天天气不错""我刚吃完午饭"之类的废话,检索时全是噪音。
我后来设计了一个简单的启发式评分规则:
- 出现第一人称偏好词(喜欢、不喜欢、希望、想要)→ 重要性+0.4
- 出现确定性实体(时间、地点、人名、金额、编号)→ 重要性+0.3
- 用户在后续轮次重复提及 → 重要性+0.2
- 纯情绪寒暄 → 重要性-0.5
这套规则粗糙,但实测对记忆召回精度的提升非常明显。如果想要更精细,可以接一个分类模型来打分,不过对于MVP阶段,"规则+大模型判断"的组合已经够用。
4.4 读取路径:按需召回,不是全量倒出
读取路径是ai-memory的灵魂。每当模型需要处理用户新消息时,我会先做记忆召回,再把命中的记忆注入system prompt。
def recall(user_id: str, query: str, top_k: int = 5) -> list[MemoryRecord]: # 1. 用当前用户提问生成查询向量 query_vec = embed(query) # 2. 在向量库中做Top-K相似度检索 hits = long_term_store.search(user_id, query_vec, top_k=top_k * 2) # 3. 用规则重排:优先最近、优先重要、优先同类型 hits.sort(key=lambda r: ( r.importance * 0.5 + recency_score(r.last_access_at) * 0.3 + recency_score(r.created_at) * 0.2 ), reverse=True) # 4. 取前K条返回 return hits[:top_k]注意我检索的时候会先多召回一批(top_k * 2),然后再用规则重排。为什么?因为向量相似度和对话实际相关性之间不是完全画等号的,多召回一批给规则留出重排空间,可以避免"最相似的几句话全是同一主题、但当前问题需要另一类信息"的尴尬。
我还有一个习惯:召回的记忆在注入Prompt时,一定要带上时间信息。
system_prompt = "你是用户助手。以下是关于该用户的部分长期记忆:\n" for mem in recalled: time_desc = mem.created_at.strftime("%Y年%m月%d日") system_prompt += f"[{time_desc}] ({mem.memory_type}) {mem.content}\n"为什么要带时间?因为模型没有时间观念,如果你把一条2023年的旧记忆和一条2024年的新记忆并列给它,它会默认为这些话都代表当前状态。明确标注时间后,模型在推理时至少多了一个维度去判断"这条信息可能过时了"。
4.5 刷新与巩固:记忆要用才不会被忘
记忆还有一个很重要的机制叫"巩固"(consolidation)。人类会把反复出现的信息从短时记忆转为长时记忆,我在ai-memory里模拟了这个过程:
- 每次某条记忆被召回并成功用于当前对话,它的
access_count加1,last_access_at更新。 - 当
access_count超过阈值(我设的是3),它的importance提升0.1,让它下次更容易被召回。 - 相反,长期未被访问的记忆,
importance会随时间缓慢衰减。
这个机制解决了一个问题:哪些信息才是用户真正在意的?用户的偏好是会变的。去年用户喜欢深色风格,今年聊了十次浅色方案,记忆系统如果还固执地推荐深色,就是劣质记忆。巩固和衰减机制让记忆系统具备一定自我纠偏能力。
5. 记忆污染与遗忘策略:比"记住"更难的是"忘对"
5.1 我踩过的最深的坑:把误解当成事实记住
如果只看到这里,你可能觉得ai-memory还挺顺利。说实话,初版跑通我也挺兴奋,直到上线测试了一个带记忆的客服机器人,翻车翻得我头皮发麻。
事情是这样的:机器人通过ai-memory记住了用户说过的一句话——"我这个订单你们是不是漏发了"。系统把这句话提炼成"用户反馈订单漏发",并作为fact(事实)存进了长期记忆。之后用户每次再提问,系统都会在Prompt里注入"订单漏发"这一记忆,结果就是机器人一直围绕"如何补发"来回答。但用户真正的问题其实是"我想改收货地址"。
问题出在哪?出在我的记忆系统无法区分"用户陈述的客观状态"和"用户当下的情绪化表达"。"漏发"只是用户的一句抱怨、一个待确认的疑问,并不是已经核实的事实。系统把假设当成了事实,等于把一条错误信息固化进了长期记忆。这种污染一旦写入,就会持续影响后续所有对话,而且因为记忆是"长期"的,它还会自我强化——每次被召回,access_count增加,重要性提升,于是更容易被召回,形成恶性循环。
5.2 解决方案:给记忆加上"可信度"和"遗忘通道"
我后来给MemoryRecord增加了两个新字段:confidence(可信度,0.2~1.0)和status(状态:pending / confirmed / expired)。
- 首次写入的新记忆,
confidence默认0.5,状态是pending(待确认)。 - 当用户后续对话中明确肯定或重复印证了这条信息,
confidence上调,状态转为confirmed,将来优先召回。 - 当后续信息与旧记忆矛盾时,系统不会急着删旧记忆,而是写入一条新记忆,并把旧记忆的
confidence下调。如果矛盾次数超过阈值,旧记忆自动标记为expired,退出召回范围。
这套机制用一句话概括:记忆系统不能只做加法,还要做减法。没有遗忘机制的记忆库,就是垃圾填埋场——信息越多,反而越难找到真正有用的那一条。
5.3 隐私问题:记忆必须能被用户看见和删除
做记忆系统还绕不开一个问题:隐私和数据主权。
我自己的产品在设计时遵循了三条原则:
- 记忆可见:用户能够在设置页里查看系统"记住了"自己哪些信息。这一步很重要,很多AI产品把记忆当黑盒,用户会不安。
- 一键清除:提供显式的
forget接口,用户清空所有记忆之后,系统不得在向量库残留。这个在工程设计上要注意,删除不能只删业务表,向量库和短期存储都要同步清。 - 敏感信息脱敏:写入长期记忆前,会跑一遍PII识别,把手机号、身份证、详细地址这类敏感内容打码。我不会把完整隐私存进向量库。
这三条不是"有更好",而是"必须有"。你做的是带记忆的产品,用户的信任就是产品生命线。
6. 实测评估:ai-memory到底提升了多少体验
6.1 我的测试数据集与评估指标
我一直觉得,记忆系统不能只听demo感觉,得用数据说话。我搭了一套评估流程,核心指标三个:
- 记忆召回准确率:对一批历史问题,检出的记忆中有多少条确实与答案相关。
- 记忆注入有效提升率:同一问题,"无记忆prompt"和"有记忆prompt"的答案质量对比,让第三方打分。
- 长会话稳定性:在50轮、100轮、200轮连续对话中,看回答质量是否发生明显劣化。
我用了一个规模不大的测试集:来自真实用户授权的客服对话200条,模拟用户画像偏好测试100条,加上我自己手工构造的50条跨会话场景。虽然样本不大,但足够反应用户规模小的时候系统表现。
6.2 测试结果与观察
先看结果:
| 指标 | 无记忆基线 | ai-memory初版 | 加过滤和遗忘机制后 |
|---|---|---|---|
| 跨会话信息保持率 | 约18%(全靠用户重新描述) | 约72% | 约83% |
| 回答中的记忆错误率 | 不适用 | 14% | 5% |
| 长对话(100轮)质量劣化 | 第40轮明显下降 | 第80轮才出现 | 稳定维持 |
这里要解释一下"信息保持率"怎么测的:我会在会话A里告诉系统一个关键约束(比如"预算不超过5000"),然后开启会话B询问"我上次说的预算范围是什么",能答出来就算保持成功。
初版14%的记忆错误率基本都来自我上面说的"事实误判"情况——模型把用户随口提的旧消息当作当前有效偏好,导致回答方向跑偏。加了confidence和expired机制之后,错误率降到了5%,这个我比较满意。
6.3 值得说说的调优细节
评估过程中有两个细节很值得分享:
第一,Top-K参数不是越大越好。我试过K=10、K=5、K=3。K=10的时候,模型确实能拿到更多背景,但被无关记忆带偏的概率也显著增加。K=3时精度高但覆盖场景窄,跨主题能力变弱。最后我定在K=5,是个相对平衡值。这个参数跟你使用的模型能力、向量库质量强相关,别照搬,要自己跑。
第二,Embedding模型的选择影响巨大。我同时测了通用中文Embedding模型和面向对话优化的模型,同一批测试数据,召回准确率能差将近15个百分点。如果你打算复刻这套方案,我建议优先用专门针对对话语义优化的Embedding模型,不要图省事拿通用文本分类模型顶上。另一个我踩过的坑是:不要对长文本做整体Embedding,超过一定长度后语义会被稀释,最好切成200~300字的小片段再入库。
7. 最后聊两句我对ai-memory的扩展想法
ai-memory这套架构目前已经在我手里的两个真实项目中服役:一个客服质检助手,一个个性化阅读推荐Agent。前者主要用短中期的对话记忆,后者更依赖长期的用户偏好刻画。
如果你也想跑通一个最小版本的ai-memory,我的建议是先别贪多:别一上来就搞分层、搞Agent调度、搞自动规划,先把"写入提炼、向量存储、按需召回"这条主线跑通,再逐步加可信度机制、遗忘机制、隐私脱敏。等这些基础扎实了,你会发现所谓AI记忆,本质上是给无状态的大模型补上一个"有选择地记、有分寸地忘"的外设。
我个人现在最想继续深入的方向,是把记忆的"冲突解决"做得更聪明——两条记忆互相矛盾时,系统不应只是简单看谁新、谁旧,而是要去推断用户当前的真实意图。这已经触及到"AI到底理不理解用户"的问题了。不过那是下一阶段的事了,先把基础工具用好,比什么都强。