ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

AI记忆模块设计:从短期上下文到长期向量检索的完整落地指南

AI记忆模块设计:从短期上下文到长期向量检索的完整落地指南 “ai-memory”这个词我盯了很久。它不是某个开源库的名字也不是什么新奇框架而是所有做AI应用的人迟早都要面对的那堵墙模型没有记忆。你上午跟它聊过的需求细节下午它就忘得一干二净每次对话都像第一次见面。我自己在做一个带长期服务性质的AI助手时被这个问题折磨到怀疑人生最后干脆自己动手把“记忆”从灵光一现变成了工程模块。这篇就聊聊我这个ai-memory方案的完整设计思路、落地的代码骨架以及那些文档里不会写、实测才发现的坑。## 1. 先想清楚AI要什么记忆为什么我们方案要区分短期和长期### 1.1 一句话说清ai-memory在解决什么问题做AI应用最常遇到的场景是用户觉得你在跟他“装失忆”。他昨天问你“我家猫叫年糕”今天问“年糕昨天吐了怎么办”如果你的助手连“年糕是谁”都不知道整个对话就崩了。ai-memory要解决的就是这件事让AI能够跨会话地记住用户说过的话、表达过的偏好、发生过的事实并在合适的时候自动调用这些信息。它的核心价值有三块。第一块是上下文的连续性也就是把“一次性的问答”变成“有来有往的陪伴”这个在健康助手、学习辅导、个人管家这类场景里几乎是刚需。第二块是个性化输出同样一个问题面对喜欢简洁回复的用户和喜欢详细分析的用户答案风格应该不一样而只有记忆模块能帮你把“喜欢简洁”这个画像沉淀下来。第三块是减少重复输入成本用户不必每次重新介绍自己的状态和背景这个体验差异是决定性的。这东西适合谁适合所有正在做对话型AI应用、并且发现“单轮问答做不到事儿”的开发者。不管你是做大模型套壳应用、垂直行业客服还是个人知识库助手只要存在“用户粘性”这个需求你就绕不开记忆模块。我自己是把这套思路嵌进了日常用的个人助理里从最开始的一次性脚本慢慢迭代成了现在这套可复用的方案。### 1.2 短期记忆和长期记忆的拆分逻辑设计记忆系统前我第一件事是分清“记住”和“理解”的区别。大模型自带的上下文窗口本质上是一块短时记忆板它在处理当前对话时很灵活、很准确但窗口一滚前面的内容就被挤掉了。而我们平时说“记住某件事”指的是过了很久还能凭一个线索想起来这就是长期记忆。所以我把ai-memory一开始就拆成了两层。短期记忆层是一个会话内的工作记忆它保存的是当前几次对话里的关键信息比如用户刚才提到的某个具体数字、某个待办事项、正在讨论的方案选项。这层不需要写向量库直接存在Redis或者内存dict里就行它的生命期就是当前会话作用是让模型在上下文窗口内不丢细节。长期记忆层才是重头戏它保存的是用户跨会话的画像、偏好、重要事实和关键事件比如“用户养了一只叫年糕的美短”、“用户偏好晚上学习”、“用户的父亲有高血压病史”。这层的数据要持久化要支持语义检索要在后续对话中主动召回。为什么要拆开我吃了教训的。刚开始我把所有历史聊天全塞进一个库结果每次检索都带出来一大堆无关紧要的“今天天气不错”“好的谢谢”这类寒暄。拆开之后我只把“有保留价值”的事实沉淀到长期层短期层则负责处理会话的即时上下文两层各司其职效果一下子清爽了很多。### 1.3 方案选型为什么放弃“全量拼接”改用“检索增强向量库”最简单的记忆方案是什么把历史聊天记录全部拼进prompt里让模型通读一遍再回答。看起来省事实际上根本跑不动。我不是没试过当历史对话超过几万字时token成本陡增模型响应变慢而且真正关键的线索被淹没在海量无意义文本中注意力机制根本照顾不过来。你让模型“通读”一整年的聊天记录再回答今天的问题它连重点都找不到。所以我的选型思路变成不追求让模型看过所有记忆而是追求在需要的时候把最相关的记忆捞出来喂给模型。这就是检索增强生成RAG的基本思路而我用的是向量检索作为主力。为什么不直接用关键词匹配因为用户表达同一件事的方式太灵活了。他上个月说“我最近经常失眠”这个月问“褪黑素能吃吗”这两句话没有一个共同关键词但语义高度相关。只有把文本转成向量用余弦相似度去找语义邻居才能在表达不一致时仍然召回有用的记忆。这也是我选向量数据库作为长期记忆存储核心的根本原因。至于底层的具体组件我用的是Chroma加一个开源Embedding模型后面第3节会展开讲。## 2. 记忆模块的落地设计数据结构、写入策略、检索策略### 2.1 记忆单元MemoryItem怎么设计不管底层用什么存储记忆在业务层都应该是一个带有丰富字段的结构体。我定义的核心字段如下memory_id记忆的唯一ID后续更新、删除都靠它。user_id记忆归属人多用户系统里必须有。content记忆正文一句话尽量独立成义。memory_type记忆类型我分成“fact事实”、“preference偏好”、“event事件”、“state状态”四种。importance重要性分数0到1决定记忆的留存优先级和召回时的权重。timestamp记忆产生的时间戳。last_access_at最近一次被召回的实践用于评估活跃度。access_count被召回的次数配合timestamp做热度衰减。embeddingcontent的向量表示供相似度检索。metadata预留的可扩展字段比如来源会话ID用于回溯。这里想多说一句memory_type。刚开始我没分类型结果检索回来一个“用户今天吃了面包”和一个“用户的猫叫年糕”模型不知道该重视哪个。后来发现事件类记忆往往有时效性吃了面包这件事一周后就没意义而事实类和偏好类记忆有长期价值猫叫年糕两年后还有用。分类之后检索时就能对不同类型施加不同的时间衰减策略效果立竿见影。importance字段也特别关键。它决定哪些记忆会被写进长期层。我制定了一套评分规则涉及用户身份和关系的给0.9涉及健康、安全、重大决策类的给0.8表达明确偏好的给0.7一般的流水账事件给0.4以下直接不进长期库。这套规则不是凭空拍的是我在人工翻检真实对话日志后总结出来的。### 2.2 记忆写入从对话流里提炼“可记忆信息”记忆写入不能做“全量备份”要做“提炼沉淀”。我的方案是一个三层漏斗实测下来效果很稳定。第一层是规则过滤。我会维护一个几乎不可能命中的白名单模板过滤掉“嗯”、“好的”、“谢谢”、“哈哈哈哈”这类纯寒暄和语气词。同时过滤掉所有“反义疑问句”和“命令式请求”比如“你能帮我查一下吗”就不该记因为这是个动作请求不是事实。第二层是LLM提炼。每轮关键对话后系统会调用模型把当前轮次里的记忆点抽取出来。我通常用这样的提示词模板“从对话中提取值得长期记住的信息包括用户的事实信息、偏好、明确表达的态度。只输出JSON列表不要解释如果没有值得记忆的输出空列表。”这个操作一次调用的成本很低但受益很大因为它能把一句很长的口语压缩成一条干净的结构化记忆。第三层是去重与合并。写入前先把新句子的向量和库内已有记忆做一次相似度查询如果相似度过高直接丢弃如果半相似则更新原有记忆的内容而不是新增条目。比如用户第一次说“我家的猫叫年糕”一个月后说“年糕现在六斤了”系统不应该新增一条“年糕六斤”的记忆而应该去更新原来那条关于年糕的事实补全体重信息。值得注意的是MemGPT这类方案是把记忆操作当成模型的工具调用来实现的也就是模型自己决定“现在该写记忆了”。我早期也是这个思路让模型在每次回答时把自己觉得重要的内容写进库。但实测不稳定模型经常产生幻觉把没聊过的事写进去。后来我改成了混合式规则保证高置信度的硬条件LLM负责提炼语义最终由我代码里的评分逻辑决定写不写。牺牲了一点智能化换来了非常高的记忆可信度。### 2.3 记忆检索不能只靠向量要打通“语义布尔时间衰减”三路检索这块是我迭代最久、踩坑最多的部分。一开始真的就只按向量相似度召回问题很明显语义相似但完全无关的记忆被捞上来。举个例子“我最近在学Python”和“Python是一种蛇”语义高度接近可用户如果是在聊宠物后面的记忆就是纯噪音。我现在的检索流程是三路召回 融合排序。第一路是向量召回按cosine相似度在Embedding索引里找top N这一路负责“语义泛化”。第二路是布尔过滤用关键词和实体匹配做硬过滤。比如系统先从用户最近的一句话里抽取实体猫、年糕、吐然后强制要求候选记忆里至少要包含一个实体词这一步能把向量检索带偏的候选直接枪毙。第三路是时间衰减加权同一份记忆如果是一周前的和一年前的权重要不同。融合排序时我用最朴素的加权公式final_score 0.6 * vector_similarity 0.3 * keyword_hit_score 0.1 * recency_score再减去一个基于last_access_at的遗忘项。这个公式虽然简单但各项系数的取值是我人工标了200条测试样本后调出来的比直接用现成的Rerank模型更可控也更省资源。如果后续数据量大了再上真正的大模型Rerank不迟。召回数量的选择上长期记忆我一般取5到10条短期记忆取最近2到3轮的完整记录。这里有个经验性规律给模型的记忆数量超过10条模型往往陷入信息过载回答反而变差因为prompt里塞了太多相关性不足的东西。宁缺毋滥。### 2.4 记忆的更新与遗忘机制记忆系统的难点不在“存进去”而在“多久之后它还有用”。我的做法有两个核心机制。一个是遗忘曲线。每条记忆都有一个last_access_at系统每天跑一次定时任务对所有记忆的importance分数做衰减计算new_importance old_importance * 0.95^(days_since_last_access)。当重要性低于0.3就移出长期库转存到冷存储的归档区。这一步能有效控制向量库规模检索性能才稳得住。跑了一段时间我发现事实类记忆用户职业、宠物名字的衰减因子要设得更低0.98左右而事件类今天去了哪里则要设到0.9甚至更高否则就会出现“半年前的琐事还赖在活跃区不走”的尴尬局面。另一个是冲突解决。当写入新记忆时如果发现和旧记忆在同一实体下产生了矛盾比如用户上个月说“我不喜欢喝咖啡”今天说“我开始学喝手冲了”系统不会简单覆盖而是把旧的偏好记忆标记为superseded新记忆的metadata里记录“此条覆盖了之前的偏好”。这样检索时模型看到的是“用户过去不喜欢咖啡但已经改变”能做出更细致的判断而不是简单粗暴地认为用户爱喝咖啡。这套更新机制的价值说到底一句话记忆系统不能只做加法必须同时做减法和纠错。没有遗忘机制的记忆库三个月后就是一座垃圾山。## 3. 从零搭建一个可用的ai-memory模块完整代码与参数解析### 3.1 技术选型与最小依赖我用了一套组合成本低、维护省、单机就能跑非常适合中小项目和个人项目起步。向量存储用Chroma它是嵌入式向量数据库pip install就能用底层默认支持持久化到本地磁盘不需要单独部署服务。Embedding模型用sentence-transformers里的all-MiniLM-L6-v2这个模型体积很小约80MB中文效果够用且CPU上算得快没有GPU也能跑得动。如果对中文效果要求更高可以换BAAI/bge-small-zh-v1.5后面我会提到差异。短期记忆层用Redis单纯存最近几轮对话没有Redis也可以用内存字典代替。这里不讲究只要能快速读写就行。业务逻辑用Python FastAPI封装成独立服务方便被其他应用调用。这套组合我跑了大半年Chroma的持久化稳得很除非你频繁kill -9进程否则没遇到数据损坏的情况。### 3.2 核心代码记忆存储类MemoryStore先定义一个MemoryStore类负责长期记忆的写入、查询、更新、删除。代码我大幅简写了但跑起来没问题你可以直接抄。import uuid import datetime from typing import Optional import chromadb from chromadb.utils import embedding_functions class MemoryStore: def __init__(self, persist_dir: str ./memory_db): # 使用默认的all-MiniLM-L6-v2作为embedding函数 self.embed_func embedding_functions.SentenceTransformerEmbeddingFunction( model_nameall-MiniLM-L6-v2 ) self.client chromadb.PersistentClient(pathpersist_dir) self.collection self.client.get_or_create_collection( nameuser_memory, embedding_functionself.embed_func, metadata{hnsw:space: cosine}, ) self.archives [] # 冷存储这里用list简化演示 def add_memory( self, content: str, user_id: str, memory_type: str fact, importance: float 0.6, metadata: Optional[dict] None, ) - str: memory_id str(uuid.uuid4()) now datetime.datetime.now().isoformat() doc_metadata { user_id: user_id, memory_type: memory_type, importance: importance, created_at: now, last_access_at: now, access_count: 0, } if metadata: doc_metadata.update(metadata) # 写入前的去重检查语义相似度 0.92 视为重复 dup self.search(content, k1, user_iduser_id) if dup and dup[0][score] 0.92: # 更新原记录而不是新增 self.update_memory(dup[0][memory_id], contentcontent) return dup[0][memory_id] self.collection.add( ids[memory_id], documents[content], metadatas[doc_metadata], ) return memory_id def search( self, query: str, k: int 5, user_id: Optional[str] None, min_score: float 0.3, ) - list[dict]: results self.collection.query( query_texts[query], n_resultsk * 3, where{user_id: user_id} if user_id else None, ) hits [] for idx, doc_id in enumerate(results[ids][0]): score 1 - results[distances][0][idx] # cosine距离转相似度 if score min_score: continue metadata results[metadatas][0][idx] hits.append({ memory_id: doc_id, content: results[documents][0][idx], score: score, memory_type: metadata.get(memory_type), importance: metadata.get(importance, 0.5), last_access_at: metadata.get(last_access_at), }) # 检索到就更新活跃度用于遗忘机制 self.update_metadata(doc_id, { last_access_at: datetime.datetime.now().isoformat(), access_count: metadata.get(access_count, 0) 1, }) # 按 importance score 融合排序 hits.sort(keylambda x: x[importance] x[score], reverseTrue) return hits[:k] def update_memory(self, memory_id: str, content: str): # 简化的更新逻辑实际生产环境按需处理 pass def update_metadata(self, memory_id: str, new_metadata: dict): self.collection.update(ids[memory_id], metadatas[new_metadata])这里有个细节值得讲查询时用了n_resultsk*3先多召回一些候选再在业务层做融合排序和过滤。这样做的原因是向量检索本身只能给语义相似度但没法感知“重要性”和“时效性”两者必须在业务层组合而不能把希望全压在向量库里。另外Chroma的PersistentClient是现在推荐的方式老版本的Client(settings...)写法已经过时了别用过期API踩坑。### 3.3 在对话流程中接入记忆模块光有MemoryStore还不够要让它真正干活得在对话流程里把它接进去。我的完整流程是用户发来一句消息。用MemoryStore.search查长期记忆找出与该消息相关的背景信息。从Redis里取最近2-3轮对话作为短期记忆。用这两部分拼出一个“记忆增强”的System Prompt。调用LLM生成回答。调用LLM提炼本轮的长期记忆点符合评分规则的就写入MemoryStore。核心的Prompt拼接我这样写的SYSTEM_PROMPT_TEMPLATE 你是一个有记忆能力的AI助手。以下是你记忆中关于该用户的信息 长期记忆 {long_term_memory} 最近对话 {recent_chat} 请结合上面的记忆信息回答用户的问题。如果记忆信息与当前问题无关请忽略。 回答时不要暴露记忆管理的细节。对于你不确定的事实明确表示不确定。 用户当前问题{user_query} 接入后你会立刻发现一个质的区别同样一个问题“年糕今天吐了”之前助手只会干巴巴说“建议观察是否有其他症状”加入记忆后它会说“年糕是你养了两年的美短吧如果呕吐还伴随精神不好建议尽快去检查”。这个体验差异几乎就是“工具”和“陪伴”的差别。### 3.4 关键参数多少合适默认值、范围和建议参数是记忆系统最容易被忽略但影响巨大的部分。我把每个关键参数都列出经验值embedding模型维度all-MiniLM-L6-v2是384维bge-small-zh是512维。维度高不代表效果好但存储和计算成本会上升。个人项目384维足够。检索top_k长期记忆建议5-10条短期记忆2-3轮。我测过top_k20的效果模型开始泛泛而谈明显被杂音干扰。min_score阈值0.3是个比较稳妥的起点。设太高0.6以上会漏召回设太低会混入大量无关内容。重复判定阈值0.92。低于0.85容易重复新增高于0.96又拦住了本应合并的相似记忆。遗忘衰减因子事实类0.98事件类0.9偏好类0.95。这个要按业务调。时间衰减权重检索排序里recency权重0.1是起点如果业务侧重近期偏好可以调到0.2但别超过0.3否则会过度牺牲长期事实的召回。这些参数看起来很多但思路是一致的每个参数都在平衡“记住”和“忘掉”之间的度。没有一劳永逸的配置必须结合自己的业务场景去回测。我自己的项目里也还在不断微调。## 4. 实际使用中踩过的坑和排查思路### 4.1 检索回来的记忆张冠李戴语义相似但完全无关这是我最先遇到的坑。向量检索对“语义相似”的理解和人对“相关”的理解并不完全一样。用户聊“Python”时系统把“养了一条蟒蛇的人”的记忆捞出来了从向量空间看这两条确实近但在业务场景里一点都不相关。排查思路是不要只看score要看检索命中的具体文本。我把每次检索的query和top5命中结果全部记录到日志里跑了几天后发现凡是score在0.6以下的结果九成以上是噪音。所以解决手段是一是调高min_score到0.4-0.5二是引入实体匹配作为硬过滤把向量检索的候选集里强制要求至少一个实体词命中。双管齐下“张冠李戴”的比例会显著下降。### 4.2 记忆越积越多召回质量反而直线下降刚跑通的时候我像囤积症一样把什么东西都往记忆库里塞三个月后活跃记忆到了一万多条检索速度肉眼可见地变慢而且每次召回的质量都在下降因为库里累积了大量“昨天吃了苹果”这类过期事件。解决办法就是老老实实做遗忘。我把所有类型为event的记忆的life time默认设为7天到期后直接沉入冷存储state类30天fact和preference不设硬过期走重要性衰减。同时每天跑一次清理任务把importance低于0.3的全归档。跑了两周后库规模从一万多降到两千多召回精度反而上来了。### 4.3 时效性背刺用户上周说喜欢这周说不要了长期记忆最大风险之一是“过期偏好”。有用户上周反复说“我最爱喝美式”系统就铁律一样地记住了。这周他开始问“拿铁怎么选豆子”结果系统带着偏见推荐了一堆美式相关直接翻车。后来我加了一道“显式变更检测”。当新写入的记忆和旧记忆在同一个实体上产生冲突时把旧记忆标记superseded而不是覆盖删除检索时模型会同时看到新旧两条并带有时间戳由模型自己判断当前应该采信哪条。这样比任何硬编码的规则都灵活。同时把偏好的recency权重调高两周内的新偏好能在排序中压制半年前的老偏好。### 4.4 常见问题速查表问题现象可能原因解决办法召回的长期记忆和当前话题无关向量相似度不等于业务相关性增加实体硬过滤调高min_score阈值记忆库膨胀检索变慢缺少遗忘机制定期执行importance衰减与冷归档用户明确改口旧偏好仍然生效没有冲突解决策略同一实体冲突时标记superseded让模型判断Embedding偶尔返回乱码LLM提炼阶段幻觉用规则白名单过滤只在置信度高的轮次写入短期对话重复写入长期库没有去重机制写入前做相似度去重0.92以上走更新而非新增模型回答显得“记忆疲劳”prompt里记忆条数过多限制长期记忆5-10条优先注入高重要性内容提示如果遇到记忆召回的内容让模型“胡说八道”优先怀疑是检索到了不相关或已过期的高importance记忆先用可视化工具把每次注入prompt的记忆打印出来人工核对一遍比改什么参数都管用。## 5. 从Demo到真实场景的落地建议### 5.1 先厘清你的业务到底需要哪几层记忆不是所有应用都需要完整的长期记忆模块。我个人的经验是一次性客服问答类的应用做短期记忆就够了上长期记忆纯属浪费需要复购或陪伴的消费级应用助手、陪伴、健康、教育长期记忆是命根子企业知识库对话场景则更重视“知识记忆”也就是可以检索的文档库而不是用户的个人事实。所以在动手前先想清楚你的场景里记忆的主体是“用户”还是“知识”。用户画像类记忆用我前面这套方案没问题知识类记忆则需要额外接文档切分、段落级向量索引的RAG体系两者可以共存但逻辑边界要划清楚。混在一起的结果就是聊着聊着用户想听的个人反馈没出来反而吐出一堆百科知识。### 5.2 记忆质量怎么评估先人工后自动化记忆系统不像搜索引擎很难有明确的相关性指标。我的评估办法很朴素准备30个典型的测试对话场景覆盖“新用户引入”“旧事实召回”“偏好变更”“无效记忆干扰”四类每跑一个场景人工检查两个问题——该记的是否记了不该记的是否没记然后给系统打分。这个“记准率”达标之后再谈召回率。等人工评估稳定后我再建立自动化回归集。把30个场景写进测试脚本每次改参数就全量跑一遍确保不会修复一个bug又破坏另一个。这套办法谈不上高大上但实际操作中非常管用比那些纸上谈兵的“评估框架”实在得多。### 5.3 扩展方向我自己下一步打算怎么试目前做的还是单模态文本记忆。下一个我准备试的方向是多模态记忆比如用户发了一张猫的照片系统要能把“这只猫是年糕”这个视觉信息沉淀下来下次仅仅提到“年糕”便能回显出图片线索。技术路线大概率是走CLIP风格的多模态Embedding统一把所有记忆都映射到同一向量空间这里头肯定又有不少值得记录的坑。另外我想尝试的是记忆可视化把记忆库里的内容定期生成一个摘要呈现给用户确认让用户主动删掉不愿意被记住的条目。一方面隐私保护是正经需求另一方也能够利用人的反馈来纠偏系统记忆比全自动管理更稳。这些方向目前我自己也还在探索有结论了再来分享。结尾关于“给AI装记忆”这件事我最真实的体感绕了一大圈最大的体会其实是记忆模块不难写难在“什么该记、什么该忘”的取舍这本质上是产品价值观的取舍。我自己从无脑囤积到主动遗忘从全量拼接到检索增强中间踩过的坑几乎都来自一个错误判断——以为“记住得越多越聪明”。实际上克制的记忆系统才更可靠。最后分享一个最实用的小技巧在开发初期一定要把每次的query、召回结果、最终注入memory log全部打印出来你会比任何测试工具都更快发现自己系统的弱点。给AI装上记忆说到底仍然是在给产品补上“对用户的尊重和了解”但每一步都要落到实处一点懒都偷不得。
返回列表