很多用过AI产品的朋友都有一个共同感受:模型明明很聪明,却总像个“金鱼”——聊完一轮再开新话题,它完全不记得你五分钟前说过什么。哪怕同一个对话窗口里,稍微偏离主题一会儿,它也会把之前的偏好、结论、甚至你自己明确交代过的事情忘得一干二净。这个问题的本质,就是AI缺少一个真正可用的“记忆模块”。
我最近手头正好在折腾一个叫ai-memory的侧写项目,目标是给通用对话模型做一套轻量的记忆层,让它能跨会话记住用户偏好、历史决策和任务状态,而不是靠每次把整段历史一股脑塞进上下文里碰运气。这篇文章就把这套方案从原理到落地完整梳理一遍,包括我踩过的坑、做过的取舍,以及哪些思路在真实场景下才靠谱。
1. 先搞清楚“AI记忆”到底卡在哪一层
1.1 模型天生没有记忆,这是架构决定的
首先要破除一个很常见的误解:大家总觉得“AI记不住东西”是因为产品做得不够好,换个更强的模型就能解决。实际上,从大模型的底层架构来看,它压根就没有“记忆”这个概念。模型的核心能力是计算条件概率——给定一串历史Token,预测下一个Token的概率分布。这是一个无状态的过程,或者说,它的“状态”完全通过上下文里的Token来承载。
这意味着什么?模型既不知道“你是谁”,也不记得“你刚才说过什么”,它只是在对当前看到的这一整段文本做续写。所谓的对话历史、系统提示词、检索到的资料,本质上都只是在拼接输入Token。一旦这些Token从上下文中滑出窗口,模型对它们的存在就一无所知了。
所以做“AI记忆”,本质上不是给模型装一个脑子,而是在模型外面做一套缓存和索引系统。记忆的任务从“让它记住”,变成“在需要的时候,把该记住的内容重新放到它眼前”。
1.2 “上下文窗口变大”不等于“会记忆”
每次新一代模型发布,厂商都在卷上下文窗口:从4K到32K,再到128K、1M。听起来像是“记忆容量”翻了上百倍,但实际用起来根本不是那么回事。
第一,长上下文的成本增长非常快。模型处理输入的时间复杂度通常随Token数增长,实际服务的算力开销也基本是线性甚至超线性的。每次请求都带上一百多万Token,延迟和费用都会让人肉疼。
第二,更关键的是,模型对超长上下文的“关注度”并不均匀。实测下来,大多数模型对中间部分的敏感度会明显下降,哪怕是两百万上下文的模型,也经常出现在中间插入的指令压根不生效的情况。所以单纯堆窗口,既贵又不可靠。
第三,从产品层面讲,用户需要记住的信息往往不是“所有历史对话”,而是其中极小一部分关键偏好和状态。把一个月前的日常寒暄也原封不动地塞进上下文,除了浪费资源,没有任何帮助。
1.3 一句话定义清楚这个项目要解决的问题
我把ai-memory要处理的问题浓缩成三件事:
- 记住什么:从对话流里抽取值得长期保留的信息(用户偏好、关键事实、任务进展、显式的“记住”指令)。
- 存在哪里:用结构化的存储(关系表、向量索引、键值对)把信息沉淀下来,而不是丢掉或塞在原文里。
- 什么时候想起:在新会话开始、回答前、追问时,按需把相关的记忆片段召回并注入提示词。
搞清楚这三点之后,整个系统就不是什么黑魔法了,反而更像一个传统的“应用层缓存 + 索引 + 检索”架构。难的不是单点技术,而是怎么把各个环节合理地串起来,并且控制好精度和成本。这也是我这篇文章想重点展开的东西。
2. 短期记忆的工程化:上下文管理的四种主流思路
长期记忆是理想,短期记忆才是日常。现阶段绝大多数AI应用的对话体验,靠的还是短期上下文管理。ai-memory的第一版,也是从这个问题切入的。
2.1 滑动窗口:最朴素但依然在用的兜底方案
所谓滑动窗口,就是只保留最近N轮对话,更早的直接丢弃。这也是很多模型API默认的行为,或者开发者偷懒时最容易想到的处理方式。
它的好处是简单、可控、几乎不会出错。就算模型忘了一些老信息,至少不会因为上下文污染给出莫名其妙的回答。
但它的缺陷也极其明显:如果用户在第3轮告诉你“我在北京工作”,在第30轮问你“我应该去上海发展吗”,模型根本不知道这两个信息之间存在冲突。所以滑动窗口只能让它“想起来最近的”,做不到“想起关键的”。
2.2 摘要压缩:信息的折旧与精炼
比滑动窗口进一档的做法是“摘要记忆”。每次对话到达一定长度后,把前面的历史交给模型生成一段摘要,作为后续对话的“固定前缀”。
这个方案的优点是能在有限的Token预算里装下尽量多的有效信息。但实际操作中要注意几个细节:
- 摘要频率:不能每两轮就摘要一次,否则模型反复在做重复劳动,成本翻倍。我一般会设定在对话轮数或者Token数超过阈值的时刻触发。
- 分层摘要:一次摘要容易丢失细节。更稳的做法是分层——短对话先做“会话级摘要”,多个会话再做“主题级摘要”,有点像写周报和月报的关系。
- 摘要本身的失真:模型在压缩时经常会把一些具体数字、人名、偏好丢掉,只留下泛泛的概括。这一点需要靠抽取式摘要做互补。
2.3 意图驱动的动态召回:只在你需要的时候“想起来”
这是ai-memory真正花心思的地方。核心思路是:不是把整段历史常驻在上下文里,而是在每次回答前,先判断“当前这个问题,需要哪些历史信息”,再去记忆库里做检索,把最相关的几条拼装进提示词。
打个比方,你家里有几千本书,你不会全部堆在书桌上,而是在写论文的时候才去书架上挑出几本跟题目最相关的。动态召回就是这个挑书的过程。
它的好处非常明显:
- 上下文Token占用大幅下降,长期来看成本更低。
- 相关性高,回答精准度更好,不像滑动窗口那样用过期的历史噪声干扰判断。
- 天然支持跨会话记忆,因为检索范围是历史库里全部的“书”,而不只是最近几轮。
当然,这套方案也引入了新的问题:怎么抽取和存历史信息?怎么判断相关?检索不出来怎么办?这就必须引入结构化记忆和向量检索技术了,也是下面第三、四节的重点。
2.4 混合策略:把记忆抽象成不同衰减速度的缓存
实际项目中,我建议不要把方案做“单选题”。更合适的是混合策略:短期高频信息用滑动窗口,中期用摘要,长期关键信息走结构化存储+动态召回。
这跟操作系统的多级缓存异曲同工:L1、L2、L3缓存各有分工,缺一不可,适配不同访问频率的数据。AI记忆也一样,如果你只用其中一种,要么贵、要么蠢、要么记不住。
3. 长期记忆的骨架:从向量检索到结构化记忆
跨会话、跨主题的长期记忆,是ai-memory另一块核心拼图。这里我踩过的坑比较多,尤其“凡是记忆就要上向量库”这个惯性思维,实话说浪费了我很多时间。
3.1 向量检索的光环与阴影
过去两年,大家谈论AI记忆,十有八九绕不开Embedding加向量数据库。思路很简单:把历史信息切成段落,用模型转成向量存起来;新对话来了,把用户的问题也转成向量,做相似度检索,取Top-K。
这个思路本身没有错,在开放域、难以结构化的信息检索上,向量确实是最合适的工具。但它有两个被严重低估的问题:
- 召回不精准:向量检索是“像不像”的问题,但在记忆场景里,很多时候用户要的不是“相似内容”,而是精确事实。比如用户问“我之前选的套餐是什么”,你返回几条“用户在选择套餐时说过……”的段落,如果其中没有具体套餐名,照样白搭。
- 更新和冲突棘手:向量库里存的信息是快照式的。用户今天说“喜欢去A餐厅”,明天说“不喜欢A餐厅了”,两次都会被存进去。检索的时候,模型同时看到两段矛盾的内容,到底听哪个?
所以我现在给ai-memory定下的原则是:能用结构化表示的,优先结构化存储;只有无法结构化的开放内容,才走向量检索。
3.2 实体化记忆:给信息贴上标签
结构化记忆的核心是“实体-属性-值”三元组。例如:
- (用户,常居城市,北京)
- (项目Alpha,技术栈,Python)
- (李经理,职位,算法主管)
- (用户偏好,回复语气,简洁专业)
这套结构的最大好处,是支持精确查询和覆盖更新。用户改了偏好,直接把对应键值更新掉就行,不会出现新旧两条记录打架的问题。而且结构化数据天然适合放进关系型数据库或者对象存储里,查询效率极高。
我把记忆实体分成三类:
- 用户属性类:静态或低频修改的画像信息,比如姓名、城市、职业、语种偏好。
- 事实陈述类:对话中明确提到的关键事实,比如“他上周去过深圳分公司”。
- 状态/任务类:当前正在推进的事情,比如“正在准备Q3营销方案,周二前截止”。
每一类设定了不同的生命周期和置信度,这样在召回时能分清轻重缓急。
3.3 混合检索:RRF融合排序
真正落地的时候,单个检索通道总是不够稳。ai-memory最终做的是一套混合检索逻辑:
- 结构化查询:先精准匹配实体和属性,比如用户ID加偏好类型。
- 向量检索:只对非结构化的对话片段做召回。
- 关键词匹配/BM25:处理包含专业名词、产品名、地名的查询。
三条通道的结果到了融合层,我不是直接相加,而是用**RRF(Reciprocal Rank Fusion)**做排序:每个结果按倒数的排名得分加总,避免向量那条通道的乱排序污染整体的相关性。这一步虽然代码量不大,但对最终效果提升非常明显。
3.4 记忆写回:别只往库里塞,还要做清洗
把用户说的话直接抽出来存上,是最容易造出“脏数据”的操作。我见过很多项目里充斥着:
- “用户:我觉得这个功能不太好”(谁觉得?哪个功能?上下文丢了)
- “用户:过两天再来”(过两天是哪天?)
- “模型:好的,我会记住您的偏好的”(AI的客套话混进了记忆)
所以我在写回前加了一道“记忆清洗”流程:先判重,再补全上下文,再更新旧记录而不是新增。这个流程调好了之后,整个记忆库的可靠性才有了基本保障。具体怎么实现,我放到第二节再细说——因为它是目前最容易踩坑、也最值得反复打磨的地方。
4. 记忆模块的落地架构与核心代码
理论说完,来点实在的。下面是我在ai-memory里用的完整记忆模块架构,以及跑通核心链路的关键代码。项目本身是Python写的,纯标准库加少量依赖,刻意避开重型框架,方便你直接改造成自己项目的模块。
4.1 整体数据流向
记忆模块分四层,每一层职责单一,方便单独改和测试:
采集层
监听对话流,接收用户新发的消息,同时拿到上一轮上下文和系统当前状态。
提取层
调用模型,从对话里抽取出“可记忆片段”,输出结构化的JSON。这一步也是记忆清洗的主要场所。
存储层
把结构化记忆写进SQLite表,把非结构化片段做向量化和入向量库。这里我会选用轻量的向量库(比如Chroma或者纯NumPy的暴力检索),避免为了一个Demo上重型的分布式组件。
召回层
按需从以上数据源中取Top-K记忆片段,组装成固定格式的提示词前缀,塞回主模型。
4.2 提取与清洗模块示例
记忆提取是整个链路里最依赖模型判断的地方。我给模型设计了一套Prompt,要求它只抽取三类信息:用户偏好、事实陈述、任务状态。同时给一条硬规则:不确定的不抽,交给我自己判断。
import json from openai import OpenAI client = OpenAI() # 你自己的API配置 memory_extract_system = """ 你是记忆提取引擎。从对话中抽取值得长期保存的信息,按以下格式输出JSON: { "memories": [ {"type": "preference", "topic": "restaurant", "content": "喜欢川菜"}, {"type": "fact", "topic": "job_location", "content": "办公地点在北京朝阳"}, {"type": "task", "topic": "report", "content": "Q3报告下周二前提交"} ] } 规则: 1. 只输出确定的、明确提到的信息,不要猜测。 2. 忽略模型自身的客套话和寒暄。 3. 如果一条信息已存在旧版本,允许输出"operation": "update"和对应旧topic。 4. 没有值得记的信息时,输出空列表。 """ def extract_memories(user_message: str, assistant_message: str) -> dict: user_prompt = f"用户消息:{user_message}\n助手消息:{assistant_message}" resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": memory_extract_system}, {"role": "user", "content": user_prompt} ], temperature=0.2, ) raw = resp.choices[0].message.content.strip("`json").strip() try: return json.loads(raw) except Exception: return {"memories": []}这个模块看起来简单,但真正决定效果的是那条“Update”规则。如果没有更新机制,记忆库会因为信息冲突越堆越乱;有了它,才能保证用户改口之后模型能跟着改。
4.3 结构化存储与去重更新
存储层我选了SQLite,一行一个记忆条目。核心字段包括用户ID、记忆类型、主题、内容、更新时间、置信度。
import sqlite3 import time class MemoryStore: def __init__(self, db_path="memory.db"): self.conn = sqlite3.connect(db_path) self.conn.execute(""" CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, type TEXT NOT NULL, topic TEXT NOT NULL, content TEXT NOT NULL, confidence REAL DEFAULT 0.8, updated_at REAL NOT NULL, UNIQUE(user_id, type, topic) ) """) self.conn.commit() def upsert(self, user_id: str, mem_list: list) -> None: for m in mem_list: self.conn.execute(""" INSERT INTO memories (user_id, type, topic, content, confidence, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(user_id, type, topic) DO UPDATE SET content=excluded.content, confidence=excluded.confidence, updated_at=excluded.updated_at """, ( user_id, m["type"], m["topic"], m["content"], m.get("confidence", 0.8), time.time() )) self.conn.commit()这里有个非常容易被忽视的坑:UNIQUE约束里我加入了user_id。如果忘记这个条件,不同用户的记忆会被彼此的写入互相覆盖,尤其是在多租户应用里,这属于必炸级别的低级错误。
4.4 记忆召回与上下文拼装
召回层我现在用的是一个“混合先用结构化,后用向量兜底”的策略。结构化数据靠SQL直接按topic匹配,向量数据靠一个简单的余弦相似度函数。
import numpy as np class MemoryRetriever: def __init__(self, store: MemoryStore, embed_fn): self.store = store self.embed_fn = embed_fn self.vec_cache = {} # 简单的内存缓存,省略持久化细节 def retrieve(self, user_id: str, query: str, top_k: int = 5) -> list: # 通道1: 精确topic匹配 sql_result = self.store.conn.execute( "SELECT type, topic, content, updated_at FROM memories WHERE user_id = ?", (user_id,) ).fetchall() # 通道2: 计算语义相似度 q_vec = self.embed_fn(query) scored = [] for row in sql_result: mtype, topic, content, updated_at = row mem_key = f"{mtype}:{topic}:{content}" # 生产环境用缓存或预计算,避免每次都重新embed m_vec = self.embed_fn(mem_key) score = float(np.dot(q_vec, m_vec) / (np.linalg.norm(q_vec) * np.linalg.norm(m_vec) + 1e-9)) scored.append((score, content, mtype, topic)) scored.sort(reverse=True) return scored[:top_k] def build_memory_prompt(memories: list) -> str: if not memories: return "" lines = ["<memory>"] for _, content, mtype, topic in memories: lines.append(f"- [{mtype}] {topic}: {content}") lines.append("</memory>") return "\n".join(lines)这个实现足够跑通链路。但必须提醒一句:每次查询都对所有记忆做embedding,只能在小规模数据(几千条以内)时用。一旦记忆数量上去,必须改成向量索引或者预计算Embedding,否则延迟会让你崩溃。
4.5 接入主模型
接进主模型的方式不复杂,就是把召回结果当作一个特殊的记忆前缀,拼在用户消息之前。
def chat_with_memory(user_id: str, user_msg: str): memories = retriever.retrieve(user_id, user_msg) memory_block = build_memory_prompt(memories) final_user_content = f"{memory_block}\n\n用户问题:{user_msg}" # 继续走正常的模型调用需要注意,记忆块和用户问题之间要有明确的分隔标记,建议在System Prompt里同时注一句“记忆块内信息可能有部分过期,请以用户最新表达为准”。这句话能极大减少模型因为旧记忆和当前消息冲突时的纠结。
5. 踩坑复盘:记忆系统失效的五个真实案例
整个ai-memory开发过程中,技术方案倒不是最耗时的,反而是各种隐蔽的“失效模式”让我反复折腾。挑几个有代表性的说,希望能帮你提前避开。
5.1 新鲜度误判:模型“想起”了过期记忆
第一批测试时,我问它“我喜欢吃什么”,它正确回答“川菜”。几周后我改口说“最近在戒辣”,再问同样的问题,它依然坚定地回答“川菜”。原因很快就定位到了——旧记忆的置信度和权重没降,新记忆的置信度虽然更高,但检索排序没有把时间折扣计算进去。
修这个坑的思路:召回打分时,对updated_at做衰减。两天内的记忆权重满分,十四天后权重降到六成,一个月后降到三成。宁可让某些老记忆不出现,也不能让它们和当前事实抢风头。
def time_decay_score(updated_at: float, current_time: float) -> float: days = (current_time - updated_at) / 86400 return max(0.3, 1.0 - 0.05 * days)把时间因子合成进最后的排序得分里,记忆新鲜度的问题基本就解决了。
5.2 记忆覆盖的粒度错位:改了一条,丢了一片
最开始做Update时,我以“内容字符串”作为判重键。结果用户今天说“我喜欢吃川菜,但是不吃辣”,明天说“我最近也尝试吃粤菜”,两条信息被当成了完全不同的条目。但如果用“topic”做更新键,又容易把“喜欢川菜”和“喜欢日本料理”合并成一条,丢失了可能的多个偏好。
目前的折中方案是:同一类型下同一主题,允许多条记忆绑定不同子主题,以用户显式推翻为真正的“覆盖触发”。比如用户说“最近在戒辣”,我们会把“吃辣偏好”标记为“已变更”,而不是尝试把“喜欢川菜”这条吃掉。要让模型知道,旧结论虽然被推翻,但过程信息仍然有一定参考价值——只是排序要靠后。
5.3 上下文污染:记忆太多,反而让回答变笨
有个很反直觉的坑:召回的数量不是越多越好。调试过程中,我把Top-K从3调到10,本意是“多给模型一点参考”,结果模型经常被无关记忆干扰,甚至拒绝回答一些本来很明确的问题。
比如用户问“明天天气如何”,我召回了一条“用户上次提到更喜欢下雨天”——模型就开始纠结“不知道明天是不是用户喜欢的天气”,回答变得非常拧巴。
经验值:单次对话注入的记忆块控制在3~5条为佳,且每条用独立标签包起来。如果某条记忆和当前问题显然无关,宁可不召,也别硬塞。
5.4 模型“自说自话”被写入记忆库
另一类脏数据源于模型生成的客套话。有一段对话里,模型说“好的,我会转达给产品经理”,提取层居然把这句话当真,存成了“用户向产品经理转达了意见”。这类污染在记忆库里越堆越多,会让后面的检索结果越来越不像样。
规避方案有两个:一是提取阶段就加一条硬性规则——只有用户消息中的原话才值得抽取,助手消息默认跳过;二是在存储之前做一次“质疑过滤”,对置信度低于阈值的内容先丢弃,不浪费存储空间。
5.5 向量检索召回遗漏:为什么长尾问题总是找不到答案
做开放域问答时,我把用户历史消息全部切块进了向量库。结果有用户问“我之前说过工作室装修风格要工业风吗?”,向量检索返回的都是“装修”“工作室”相关的泛文本,偏偏没把那条关键句子排进Top-K。
后来我翻日志发现,那段原始文本里出现“工业风”的地方,正好被我一句话切割点切成了两半。这种情况在向量检索里特别典型:信息边界和检索需求往往不在同一个位置。
最后解决的思路是:不只拿“对话段落”做切分,再额外维护一层“主题级摘要索引”。把每次会话的摘要和核心表态单独存成向量条目,检索时和段落级结果做融合。这就把召回率提升了不少。
6. 经验沉淀:给同样想做AI记忆的开发者几句忠告
跑完了整个项目,我对“给AI做记忆”这件事有了比较完整的一线认知。按照这几个原则做,基本上能规避掉绝大部分坑:
- 不要试图让模型“记住”,要让它在需要时“找到”。记忆系统的价值在于高效的存取,而不是模型本身的参悟。
- 结构化字段优先于自由文本。能用数据库解决的问题,别全部丢给向量搜索。
- 记忆写成“状态”而不是“历史”。旧信息可以有日志,但活跃层只保留最新、最准的一版。
- 注入提示词的记忆必须限量、可辨识、可覆盖。这样模型才知道哪些是参考,哪些是用户最新意愿。
- 上线后先跑两周日志,再优化召回权重。光靠离线测试永远发现不了真实用户到底会说什么、改什么、忘什么。
另外,记忆的安全边界要盯紧。用户隐私数据和长期记忆之间,必须有一道清晰的权限闸门。当前对话上下文里能说的话,不代表应该被永久写入用户的Profile。尤其是涉及个人身份、位置、联系方式等敏感字段,系统得在最前面挡住提取逻辑,不能什么都往库里塞。
ai-memory这个项目,我目前的版本已经稳定跑在个人工作流里,帮我把知识库问答、周报生成、旅行规划的几个场景都串了起来。下一步想试的方向是给不同的记忆实体加“遗忘曲线”,让系统主动清理超过保质期的淡记忆,而不是一味积累。这个方向做透了,AI应用在“可靠”和“贴心”两个维度上应该都能再上一个台阶。