记忆的进阶:短期记忆、长期记忆与向量存储

张开发
2026/4/19 9:01:36 15 分钟阅读

分享文章

记忆的进阶:短期记忆、长期记忆与向量存储
上周调试一个多轮对话Agent遇到了一个典型问题用户问“帮我查下杭州天气”接着问“那明天呢”——结果Agent直接懵了反问“您说的是哪个城市”这让我意识到很多开发者把对话历史直接塞进Prompt就以为实现了“记忆”。其实这只是短期记忆的雏形离真正的多轮记忆系统还差得远。今天咱们就拆解记忆系统的三层设计短期记忆、长期记忆以及向量存储这个关键底座。短期记忆不只是对话历史堆叠短期记忆通常指当前会话的上下文。最常见的实现就是把最近N轮对话拼接成文本喂给LLM。但这里有个坑盲目堆叠历史会快速耗尽Token限额而且无关历史反而干扰当前回答。我常用的做法是滑动窗口重要性过滤。不是所有历史都平等重要系统指令、用户最近的关键请求、AI的关键回应这些需要优先保留。下面是一段简化代码展示如何做带权重的历史管理classShortTermMemory:def__init__(self,max_tokens2000):self.messages[]self.max_tokensmax_tokensdefadd(self,role,content,weight1.0):# weight 用于标记重要性系统指令weight2.0闲聊weight0.5self.messages.append({role:role,content:content,weight:weight,tokens:self._count_tokens(content)})self._trim()def_trim(self):# 按权重保留重要消息即使旧也不删current_tokenssum(m[tokens]forminself.messages)whilecurrent_tokensself.max_tokensandlen(self.messages)1:# 找权重最低的非系统消息删除candidates[ifori,minenumerate(self.messages)ifm[weight]1.5]# 系统消息weight2.0ifnotcandidates:breakidxmin(candidates,keylambdai:self.messages[i][weight])removedself.messages.pop(idx)current_tokens-removed[tokens]注意这里没有简单按时间删除而是结合权重。比如用户说过“我叫张三”这个信息weight可以设高即使过了10轮对话也要尽量保留。长期记忆向量存储不是万能药当信息需要跨会话持久化就进入长期记忆领域。很多人第一反应是“上向量数据库”但别急着写代码先想清楚你要存什么怎么检索我见过一个项目把用户所有对话都向量化存进Pinecone结果检索时总冒出无关内容。问题在于没有对记忆做结构化处理。长期记忆应该分层事实型记忆用户提供的明确信息如“我对花生过敏”。适合用键值对存储搭配向量检索做冗余备份。事件型记忆对话中发生的具体事件如“上周用户投诉了登录问题”。需要带时间戳的文本记录向量化时需考虑时间衰减。偏好型记忆用户隐含的偏好如“用户每次都要详细解释”。这类信息适合用标签系统向量检索效果反而不好。这里给个混合存储的例子classLongTermMemory:def__init__(self,vector_store):self.vector_dbvector_store# 比如Chroma或FAISSself.kv_store{}# 简单用字典示意实际可用Redisdefremember_fact(self,key,value,embeddingNone):# 键值对存一份self.kv_store[key]{value:value,timestamp:time.time()}# 向量存一份供语义检索ifembedding:self.vector_db.add(texts[f{key}:{value}],embeddings[embedding],metadatas[{type:fact,key:key}])defrecall(self,query_embedding,top_k3):# 先向量检索vector_resultsself.vector_db.similarity_search_by_vector(query_embedding,ktop_k)# 再键值精确匹配比如query里直接提到keyexact_matches[]forkeyinself.kv_store:ifkeyinquery_text:# query_text需另外传入exact_matches.append(self.kv_store[key])returnself._merge_results(vector_results,exact_matches)向量检索的常见坑相似度阈值设得太低召回一堆无关内容。建议动态阈值根据返回结果的相关性分数分布做调整。向量存储实战选型与优化市面上向量数据库很多但轻量级场景我常直接用FAISS本地存储。除非需要分布式和持久化才上Chroma或Weaviate。分享一个FAISS的实用片段importfaissimportnumpyasnpclassSimpleVectorStore:def__init__(self,dim1536):# OpenAI embedding维度self.indexfaiss.IndexFlatIP(dim)# 内积相似度self.texts[]self.metadata[]defadd(self,embeddings,texts,metadatas):# 记得归一化不然内积算cosine会出问题normsnp.linalg.norm(embeddings,axis1,keepdimsTrue)norms[norms0]1e-10embeddings_normembeddings/norms self.index.add(embeddings_norm.astype(float32))self.texts.extend(texts)self.metadata.extend(metadatas)defsearch(self,query_embedding,k4):# 查询向量也要归一化query_normquery_embedding/np.linalg.norm(query_embedding)scores,indicesself.index.search(query_norm.astype(float32).reshape(1,-1),k)# 过滤低分结果经验阈值0.7根据场景调results[]forscore,idxinzip(scores[0],indices[0]):ifscore0.7andidx!-1:results.append({text:self.texts[idx],metadata:self.metadata[idx],score:float(score)})returnresults重点提醒FAISS的IndexFlatIP要求向量已归一化否则算的不是cosine相似度。批量add比单条add快得多尽量攒一批再写入。索引文件记得定期保存到磁盘别只放内存。记忆的融合与取舍短期记忆和长期记忆怎么配合我的策略是每次生成回答前先从长期记忆中检索相关片段3-5条作为背景知识插入Prompt。短期记忆维护当前会话的完整流程。会话结束时判断哪些信息值得存入长期记忆比如用户明确说“记住这个”。判断是否存入长期记忆的启发式规则defshould_save_to_long_term(message,user_intent):# 规则1用户明确指令if记住inmessageorsave thisinmessage:returnTrue# 规则2包含个人信息简单关键词匹配personal_keywords[我叫,我住在,电话是,邮箱是]ifany(kwinmessageforkwinpersonal_keywords):returnTrue# 规则3AI总结的重要信息比如用户确认过的需求ifuser_intentconfirm_specification:returnTrue# 规则4对话中重复提及超过2次# ... 需要维护提及计数器returnFalse给实践者的几点经验别过度设计初期用简单的对话历史列表关键词匹配长期记忆就够了等真有需求再上向量存储。记忆要有遗忘机制长期记忆不是只进不出给记忆项加时间衰减权重定期清理低权重项。测试记忆系统时构造“跨会话查询”场景比如第1轮说“我喜欢蓝色”第10轮问“你知道我喜欢什么颜色吗”——很多基础实现会挂在这里。向量检索的召回结果一定要在UI/日志里展示出来临时调试亲眼看看到底召回了什么常有意想不到的发现。隐私敏感数据别进向量存储即使用也要加密或脱敏。见过有人把用户地址明文存向量库这是大忌。记忆系统本质是平衡艺术记住该记的忘记该忘的在需要时准确回想。这听起来像人的记忆但实现起来全是工程细节。下次当你看到Agent又“失忆”时先别怪模型查查你的记忆管道——大概率是数据没流对地方。

更多文章