ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI记忆系统落地指南:从记忆分级到向量检索与遗忘策略

AI记忆系统落地指南:从记忆分级到向量检索与遗忘策略 这两年帮不少LLM应用做过“接脑子”的活绕不开的核心词就是 ai-memory。你大概也遇到过一模一样的问题上下文窗口明明越开越大模型能“看到”的内容越来越多但只要换一个Session或者隔几天再回来聊它就完全不记得你是谁、关心什么、上次做到哪一步了。本质上大模型没有“记住”只有“看到”。ai-memory要做的就是给AI补上真正的跨会话长期记忆把值得留存的对话沉淀下来、按需召回、适度遗忘。这篇文章我会把完整的落地过程摆出来——从记忆分级、数据建模、向量检索到遗忘策略再到你大概率会踩的坑。适合正在给AI应用加记忆能力的开发者、纠结工具选型的架构师以及想搞明白记忆系统和普通RAG有什么区别的朋友。1. 先别急着写代码AI记忆到底记什么、忘什么1.1 大模型的“失忆”是结构性的很多人以为模型记不住是因为Prompt写得不对其实问题出在根上。Transformer的注意力机制是在一个有限的上下文窗口里做计算的一旦输入超出窗口上限最前面的内容会被硬截断就算窗口足够大离当前回答较远的信息也会被注意力分布稀释表现就像是“看是看到了但想不起来”。更关键的是绝大多数应用在做接口调用时是Session隔离的。上一轮对话的内容如果不手动传给下一次请求模型就是无状态的。它擅长的是推理和生成不是回忆。这个“失忆”不是缺陷而是架构的默认行为。所以做AI记忆系统本质上不是在模型上打补丁而是在应用层加一个“外部笔记本”把重要信息在对话结束后写下来下次对话开始前再把相关片段翻出来放到Prompt里。模型不需要真的“记得”它只需要在关键时候“看到”我们递上去的笔记。1.2 四种记忆别混着存我一开始把所有聊天记录无脑塞进向量库结果就是召回出一堆“昨天午饭吃了什么”级别的噪声。后来把记忆分了层效果立刻不一样。现在我的划分是这样工作记忆当前会话最近几轮对话直接放在Prompt里不需要持久化清掉就清掉。情景记忆跨会话的历史片段比如“上周讨论过项目A的接口方案”需要支持语义召回。语义记忆用户的长期偏好和固定事实比如“用户是后端工程师偏好Python喜欢简洁方案”。变化慢、复用率高。程序记忆Agent会调用哪些工具、每个工具的参数约束。这部分多数场景放在系统配置里不需要做成向量记忆。这个分法的意义在于存储成本和召回策略不同。工作记忆追求低延迟语义记忆要长期稳定情景记忆需要时间衰减。如果全部混在一个集合里召回时优先级根本无法区分。1.3 先判断你的应用真需要长期记忆吗不是所有项目都该上记忆系统。我有个比较快的自测清单用户是否会说“上次我们聊到的那个事情”用户是否期待跨Session记住设置、偏好、进度应用是否需要在多轮交互中持续维护一个复杂目标三个答案如果都是“否”就不要给自己加戏。如果都是“是”再想清楚你要的是“助理型记忆”还是“知识库型记忆”。前者记用户状态和上下文后者记文档和事实。ai-memory通常偏向前者但核心实现都会落到同一套向量检索体系上所以这篇文章的方法对两边都适用。2. 记忆系统的四阶段链路写入、存储、召回、遗忘2.1 写入端从对话里抽出“值得记”的东西把原始聊天记录全部存进去是最省事但也最蠢的做法。对话里有大量寒暄、确认、重复信息直接入库会带来三个问题存储膨胀、召回噪声变大、隐私风险失控。我的方案是用LLM做结构化抽取而不是规则匹配。先给一个抽取Schema让模型只输出有价值的信息{ memory_text: 规范后的记忆描述, memory_type: preference | fact | progress | task, importance: 1-5, tags: [领域标签] }抽取Prompt大概长这样你是一个记忆抽取器。请从以下对话中提取值得长期记住的信息只输出JSON数组。抽取原则用户明确表露的偏好、确定的结论、个人事实、项目里程碑才需要记住寒暄、临时指令、隐私字段一律不抽。隐私字段包括手机号、密码、验证码、银行卡号。写入触发条件我一般设三条用户明确说“以后都按这个来”对话中产出了确定结论出现了个人信息或项目进度。满足任意一条就进入抽取流程。实操中还有一个双保险先用正则把手机号、密钥这类搞成脱敏标记再交给LLM抽取避免模型把敏感信息原样存进记忆库。2.2 存储端为什么向量数据库是最佳载体回忆通常是模糊的。用户不会说“请精确查询ID为xx的记录”而是说“我之前好像提过一个优化思路”。SQL那种精确匹配在这里完全用不上必须有语义检索能力。语义检索的基本流程是先把文本用Embedding模型转成向量让语义相近的文本在向量空间里靠在一起查询时也转成向量用余弦相似度或点积找到最近的几个邻居。常用存储方案对比方案优点缺点适合场景FAISS轻量、速度快不擅长payload过滤持久化要自己管单机、小规模Chroma上手快、零成本生产稳定性一般原型验证Qdrant过滤能力强、Rust性能好需要独立服务生产主力我目前选它Milvus分布式、海量部署运维复杂大规模平台pgvector复用已有PostgreSQL数据量大后性能弱不想引入新组件我的建议是先用Chroma把逻辑跑通再迁移到Qdrant或者pgvector。不要一上来就上Milvus运维成本会让你在还没看到效果时就先疯掉。2.3 召回端怎么把“过去”带回当前Prompt召回链路我固定成四步Query改写用户问“通知功能怎么样了”原始问题太口语化先扩展成“通知功能 进度 最近更新”再检索。向量化用和写入时完全相同的Embedding模型维度必须一致。召回候选集按相似度倒序取TopK一般在20条左右。重排与筛选过滤掉低于阈值的再按重要度和时间加权最后只保留3到8条。参数我给一个起步值TopK取20最终保留5条相似度阈值0.6到0.75之间具体要看你用的Embedding模型。这里有个特别重要的认知召回不是越多越好。旧记忆太密会稀释当前任务所以宁可少给也别把Prompt塞成一锅粥。2.4 遗忘端记忆不能只进不出没有遗忘机制的记忆系统迟早变成垃圾场。我采用的策略比较朴素但很有效时间衰减每条记忆越久没被访问参与召回的权重就越低。访问强化被成功召回一次就把access_count加一近期权重提升模拟人的“回看”行为。冲突淘汰新旧记忆矛盾时旧记忆标记为superseded不再参与正常召回。定期归档超过90天没有被访问的记忆从在线库搬到冷备库不参与检索只保留可回溯能力。这四件事我用一个后台任务每6小时跑一次。刚开始我也担心“遗忘掉用户重要信息”会被投诉实际上用户问一次就得触发一次召回召回后访问时间被刷新重要记忆不会被误杀真正被杀的都是陈年废料。3. 手写一个可落地的ai-memory模块3.1 技术栈与选型逻辑我用的是一套很朴素的组合刻意绕开厚重的框架Python 3.10Embedding中文场景用BGE-M3量大之后可以蒸馏成小模型向量库QdrantDocker单节点起步元数据SQLite只存结构化信息LLM抽取和改写任意主流模型即可gpt-4o-mini这个级别足够为什么不直接用LangChain的Memory组件封装太厚出问题你根本不知道是向量索引的问题还是Prompt拼接的问题。自己写一遍全链路也就几百行代码顺手还能把数据模型彻底吃透后面接任何框架都是降维打击。先装依赖pip install qdrant-client sentence-transformers openai pydantic启动Qdrant最省事的方式docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant3.2 记忆数据结构在Qdrant里建一个名为memory_records的Collection。向量维度要和你选的Embedding模型严格对应BGE-M3输出1024维OpenAI的text-embedding-3-small是1536维。每条记忆的Payload我固定这组字段dataclass class MemoryRecord: user_id: str session_id: str content: str memory_type: str # preference / fact / progress / task importance: float # 1-5 tags: list[str] created_at: datetime last_access_time: datetime access_count: int status: str # active / superseded / archived索引方面Qdrant里要把user_id、status、memory_type都建为keyword索引。别小看这个步骤没有索引的时候全表扫描能把延迟从10ms拖到300ms。3.3 写入流程实现写入的核心是“抽取—脱敏—向量化—入库”我封装成两个函数from qdrant_client import QdrantClient from qdrant_client.models import PointStruct, VectorParams, Distance client QdrantClient(hostlocalhost, port6333) def embed_texts(texts: list[str], model) - list[list[float]]: return model.encode(texts, normalize_embeddingsTrue) def save_memory(user_id: str, record: MemoryRecord, embedding: list[float]): client.upsert( collection_namememory_records, points[ PointStruct( idhash(record.session_id record.content[:50]), vectorembedding, payload{ **dataclasses.asdict(record), user_id: user_id, status: active, }, ) ], )写入触发放在对话流之外用一个异步队列去跑不能让用户等记忆写完才看到回复。抽取步骤交给LLM但我会限制输出条数一次最多抽5条避免模型把几句闲聊拆成20条“记忆”。这里有个心得写入前先做一次相似度检测如果当前要写的内容和已存在的记忆相似度超过0.9就跳过不写相当于去重。否则同一个设置被重复写十几次后续召回全部是冗余。3.4 召回流程实现召回是整个模块的核心我一步步说def recall_memories(user_id: str, query: str, top_k: int 20) - list[MemoryRecord]: # 1. 改写query扩写关键实体 rewritten_query rewrite_query(query) # 2. 向量化 query_vector embed_texts([rewritten_query], embed_model)[0] # 3. 向量检索强制带上user_id和status过滤 hits client.search( collection_namememory_records, query_vectorquery_vector, query_filterFilter( must[ FieldCondition(keyuser_id, matchMatchValue(valueuser_id)), FieldCondition(keystatus, matchMatchValue(valueactive)), ], ), limittop_k, with_payloadTrue, ) # 4. 重排相似度阈值过滤 时间权重调整 ranked [] for hit in hits: if hit.score 0.65: continue age_days (datetime.now() - hit.payload[last_access_time]).days time_boost 1.0 / (1.0 0.05 * age_days) final_score hit.score * 0.7 hit.payload[importance] * 0.1 time_boost * 0.2 ranked.append((final_score, hit)) ranked.sort(reverseTrue) return [hit.payload for _, hit in ranked[:5]]这里有个非常容易踩的坑user_id过滤必须同时出现在向量检索的Filter里而不是在拿到结果之后再过滤。否则用户A的查询会把用户B的记忆捞出来哪怕后面你手动过滤掉了也已经把不该看的向量内容暴露在了检索链路里。召回结果拼进System Prompt时我习惯带上一个免责声明以下是关于用户的历史记忆仅作参考。如果记忆与当前对话矛盾以当前对话为准。不然模型容易把一个过期状态当成事实来回答。3.5 遗忘与更新落地冲突处理的逻辑是这样用户把通知时间从早上9点改成11点。写入新记忆前先用“通知时间 设置”做一次检索找到旧记忆后将其status标记为superseded再写入新记忆。def mark_superseded(user_id: str, old_memory_id: str): client.set_payload( collection_namememory_records, payload{status: superseded}, points[old_memory_id], )后台定时的权重刷新我直接写一个Python脚本用cron跑每隔6小时扫描一次active记忆超过90天未访问改为archived超过30天未访问importance减0.5访问次数大于10importance加0.2模拟“用户反复回看说明重要”这套策略跑了两周后在线库的记忆条数不再快速增长召回结果的相关性明显变好。4. 工具选型哪些轮子值得自己造哪些坑别踩4.1 自研 vs 开源Memory框架现在市面上有不少现成的AI Memory方案比如Mem0、Zep、LangChain里的一堆Memory组件。我的态度是先用自研跑通再决定要不要换框架。方案核心能力适合场景主要顾虑Mem0自动抽取和更新记忆API简洁快速集成到已有应用定制性差内部逻辑黑盒Zep基于时间图谱能表达复杂关系需要记忆关联推理的项目部署重运维复杂度高LangChain Memory封装简单多种存储后端Demo和个人项目状态管理弱缺乏生产级遗忘机制自研可控性最高链路清晰核心用户长期使用、数据隔离要求高需要自己写抽取和遗忘逻辑如果你做的是内部工具直接上Mem0没毛病。如果是面向大量用户的SaaS产品我强烈建议自研或者至少把框架源码读透再做二次开发。记忆系统直接决定对话质量出了问题黑盒状态下你连排查的入口都找不到。4.2 Embedding模型是召回效果的第一变量召回效果70%由Embedding模型决定20%由检索参数决定只有10%在重排。我把几个主流方案摆出来模型向量维度中文效果成本场景适配BGE-M31024优秀长句理解强免费本地部署中文业务生产常用text-embedding-3-small1536良好按量付费不想维护模型服务text-embedding-3-large3072更强但贵高预算充足bge-large-zh1024中文专精免费纯中文场景换Embedding模型之后有个典型坑之前标定好的相似度阈值全部失效。原因不难理解不同模型产出的向量空间分布完全不同0.7的阈值在text-embedding-3-small上能正常召回换到BGE-M3可能就变成什么都召不出来。所以每次换模型都要重新拿一批真实对话测一遍阈值。5. 常见问题与排查实录5.1 召回结果驴头不对马嘴我遇到最多的症状是明明库里有一条很相关的记忆搜索结果却完全没影子。排查顺序按下面这张表来症状可能原因排查方向完全搜不到任何记忆向量维度不一致、集合名错打印embedding shape确认集合存在能搜到但相关度极低Embedding模型换了、阈值过高重新标定相似度阈值其他用户的记忆被召回检索Filter忘了加user_id检查Filter写跨用户隔离测试旧记忆频繁被召回superseded状态没过滤确认检索条件里带了statusactive排查时最有效的手段是“手动单条验证”单独取出一条已知记忆和query算一次相似度看分值在什么量级。如果单条相似度就低得可怜那问题一定在Embedding或query改写上而不是向量库。5.2 Prompt里的旧记忆越堆越多有人一开始设计得“豪爽”每轮把召回的全部历史记忆都塞进Prompt结果上下文窗口很快被打爆而且模型开始分不清哪条记忆重要。我的做法是给召回结果设硬上限最多5条每条记忆在Prompt里最多用两句话表达。如果一条记忆超过100个字就得在写入时就压缩成摘要。这样即使命中10条注入总量也能控制在600字以内。5.3 新旧记忆互相打架用户先说“通知改到早上9点”隔两天又说“还是11点吧”。如果不处理模型会在一次对话里同时看到两条矛盾记忆最后给出一个四不像的回答。解决思路就是前面说的superseded机制写入新“设置类”记忆前先做一次同类检索发现可能是冲突记忆就交给LLM做一句话判断确认矛盾后把旧记忆标记失效。这个方法不复杂但能让用户觉得AI“真的很懂”因为它知道自己改过主意。5.4 跨用户隐私边界这是我在早期版本踩过最重的一个坑。当时图省事检索时只按相似度召回没加user_id过滤结果一个用户问“我的服务器配置”回答里混进了另一个用户的服务器信息。这类问题一旦出现就是事故级。防御措施有两条所有写入和查询的代码路径必须携带user_id测试用例里固定写一条“跨用户隔离”用例每次发版前跑一遍确保用户A永远搜不到用户B的记忆。另外密码、验证码、银行卡这类信息从产品安全角度就不该进入记忆库哪怕脱敏了也别存。能用规则过滤掉的先用规则挡掉别把希望全寄托在LLM判断上。5.5 数据量涨上来之后的性能问题在十万条记忆以内向量库默认配置不会出问题。但到了几十万甚至上百万条就需要调索引参数了。我用的Qdrant配HNSW索引起步参数可以这样设VectorParams( size1024, distanceDistance.COSINE, hnsw_configHnswConfig( m16, ef_construction200, ef_search100, ), )我实测的感觉是在普通开发机上20万条向量单次检索压到15到30毫秒完全没问题。如果某次召回变慢先看生效的索引是不是用对了再看有没有在Filter里用了未建索引的字段。最后说一点体感最深的经验做ai-memory最难的不是代码而是“什么该记住、什么该忘掉”的判断。我前前后后调了一个多月发现最有效的优化不是换更好的Embedding而是把user_id过滤和冲突淘汰做对。如果你也在做类似的东西建议先把端到端场景跑通用户在第一天的会话里说了一个偏好第二天的会话里用完全不同的措辞问出来AI能准确对上那你的记忆系统就算立住了。
返回列表