ai-memory实践:给大模型装外部记忆的完整方案
发布时间:2026/9/26 13:16:46来源:尧图网络
ai-memory这名字最近在圈子里讨论度不低。光看标题就够直白给AI装上记忆。大模型本身是典型的“三秒记忆”——每个会话都是独立的上一轮聊完的东西下一轮就跟你装不认识了。我这次实践的就是给一个智能助手搭一套外部记忆模块让它跨会话记住用户身份、偏好、历史结论这恰好就是ai-memory这个项目要解决的核心问题。整个方案覆盖记忆存储、语义检索、上下文注入三块做完之后我最大的感受是记忆本身不复杂难的是知道记什么、忘什么、什么时候把记忆拿出来用。这篇稿子就把整个落地方案、核心代码和踩坑过程完整拆一遍适合正在做智能客服、个人助理、大模型应用的开发者参考。1. 项目解读给AI装记忆本质是装一套“外部大脑”1.1 为什么大模型天生没记忆大模型本质是一个函数给它一段文本它返回下一段最可能的文本。它的知识全部固化在训练参数里能回答“怎么写营销文案”但记不住“你上一轮说过你正在创业”。这种状态叫无状态。真实产品里用户第一轮说“我是做智能硬件的产品经理团队十个人”第二轮直接问“帮我写一份新品发布会邀请函”模型根本不记得他是硬件公司更不知道他有什么新品可发。这种割裂感在聊天机器人、AI助手、智能客服里非常致命。有人第一反应是把历史对话全塞进上下文不就行了问题是上下文窗口有上限。GPT-4级别128K的窗口听起来很大但长对话累计下来几轮就顶到天花板。而且token就是成本每次请求把几千字历史反复发送开销成倍涨。更麻烦的是信息量过大之后模型注意力会被稀释越旧的内容越容易被忽略表现为“明明把文档塞进去了模型还是答非所问”。所以通用方案不是塞原文而是把记忆“提炼”出来放到模型上下文之外需要时再取回来。这就是ai-memory这类项目的底层逻辑外部记忆层。它把记忆从模型参数和上下文窗口中解耦做成独立存储用的时候按需召回。整套系统拆成三件事写入、检索、注入。写入把对话浓缩成一条条结构化记忆检索根据当前问题找到最相关的记忆注入把记忆拼回输入上下文。三个环节串起来AI才有“我认识你”的体验。1.2 “存-取-用”三层结构是怎么设计的我用一个最简单的比喻来拆解记忆系统就像图书馆。写入相当于采购新书并把书编目检索相当于用户查书目找书注入相当于把选中的书放在桌面上供阅读。每层职责单一才能分别调优。写入层是整个系统的重头。不是所有对话都值得记寒暄、临时问题、闲聊统统过滤掉。我通常在一轮对话结束后用模型把多轮对话压缩成结构化条目比如“用户是硬件产品经理”“用户偏好简洁设计风格”“用户当前在准备新品发布会”。这些条目才是能长期复用的记忆原文对话反而是噪音。存储层负责把记忆条目转为向量存进向量数据库。向量就是一句话在语义空间里的坐标相似意思的句子坐标接近。这也是记忆检索和传统SQL查库最大的区别——传统数据库要靠精确关键词匹配但用户不会每次都用同样的词描述同一件事。今天说“我喜欢简洁的界面”两周后问“有什么干净点的笔记软件”“简洁”和“干净”在语义空间里是邻近的向量检索就能匹配上。检索层是保障体验的关键。每次用户发出新问题先把问题转成向量然后去库里找相似度最高的记忆。这步有几个参数直接影响效果召回数量、相似度阈值、时间衰减。后面会详细讲。注入层是把记忆用回对话里的最后一步。我习惯把检索出来的记忆拼进system prompt的“用户档案”区块同时限定向模型声明“以下是关于用户的已知信息与当前问题相关才使用。”不让记忆反客为主也不让模型编造细节。1.3 为什么最终选了“向量检索外部存储”做这个方案之前我认真对比过两条路线全量拼接和微调。全量拼接前面说了token成本线性增长、窗口受限、注意力被稀释。微调呢开销大且频率跟不上记忆是实时变化的用户昨天还在做智能硬件今天可能转行做餐饮了总不能每个用户改一次模型。微调适合给模型注入稳定领域知识不适合做个性化记忆。向量检索加外部存储是折中最优解存储廉价写入实时检索按需。成本可控体验也自然。这套路和RAG检索增强生成底层是同一套技术把知识外置按问题动态召回。ai-memory本质上就是RAG在个人语境下的应用只不过回取的不是文档片段而是用户画像和历史结论。工具选型上我建议中小项目一开始别上重武器。我用的是Chroma作为向量库本地轻量、零运维、支持元数据过滤。等数据大到千万级以上再考虑换Qdrant或Milvus。嵌入模型线上方案用OpenAI的text-embedding-3-small本地开发用bge-small-zh-v1.5中文场景下效果不错、速度也快。2. 核心细节拆解记忆的存取都有讲究2.1 记忆条目的数据模型怎么设计很多初做记忆系统的人直接把一段对话原文丢进向量库这其实是个坑。原文太啰嗦、噪声大、检索时容易匹配到一堆无意义片段还白白占存储。我的做法是归一化为结构化条目。每一条记忆包含下面这些字段{ id: mem_123456, type: preference, content: 用户喜欢简洁配色反感弹窗广告, importance: 0.8, created_at: 2025-01-10T14:22:31, source_session: session_8899, access_count: 3, last_access_at: 2025-01-18T09:05:12 }字段不多但每个都有用途。type区分记忆类型我这边用了四种fact表示客观身份信息preference表示偏好和态度task_summary表示任务进展chat_log表示有价值的结论性对话。不同类型在检索时权重不同比如用户问“帮我写个方案”fact和preference的优先级就高。importance字段是置信度模型抽取记忆时自我评估高重要度记忆进入长期库低重要度的放在短期缓存定期清理。access_count和last_access_at是实现记忆翻新的基础一个记忆被反复用到它就该在排序时获得加分这模拟了人的记忆强化机制。注意一点content本身也讲究。存储前要保证一句话信息完整主谓宾齐全。比如“喜欢简洁设计”这种话单独看还能理解但“喜欢简洁”被抽出来就太模糊了。所以在提炼阶段我会让模型把零散信息补全成完整表达这也是后面检索成功率的一个隐含影响因素。2.2 写入侧会话结束后的“记忆提炼”写入时机我选在会话结束后。在线抽取会拖慢响应速度而且用户每说半句话就要触发一次记忆更新大部分都不值得写。我的做法是当会话暂停或被判定为结束时把这一段的对话丢给一个提炼Prompt输出结构化记忆。下面是实际在用的抽取模板extract_prompt 你是一个记忆提炼器。根据以下对话提取值得长期记住的内容。 只输出JSON数组每条记录包含: - content: 一句话描述主谓宾完整 - type: fact | preference | task_summary - importance: 0到1的小数0.7以上才值得长期记忆 不要提取: 临时寒暄、一次性问题、任何证件号码、银行卡号、密码等敏感信息。 对话记录 {conversation} 用这个Prompt调一次模型返回JSON批量入库。这样做有几层考虑一是过滤无效信息二是把口语化表达转成结构化描述三是天然加了隐私边界。实测下来大部分闲聊都抽不出记忆而能抽出的都是真实可复用的信息写入质量直接决定了后续检索质量这步值得做细。2.3 检索侧召回数量、相似度阈值、时间衰减怎么调检索逻辑看似简单问题转向量查向量库返回前K条。但三个参数调不好效果差距很大。先把参数含义讲清楚。Top-K我常用3到5。太少容易漏太多会稀释当前对话的注意力。有一次我调到10系统把“用户喜欢猫”这条老记忆也拉出来了导致模型在推荐笔记软件时非要扯一句“考虑到你养猫”这就喧宾夺主了。3到5条是安全区间。相似度阈值需要结合你用的向量库距离算法理解。Chroma默认返回的是L2距离值越小表示越相似。不同距离算法、不同嵌入模型相似度分布差异很大所以别直接抄网上的阈值要拿你自己的数据实测。我的经验是先用0.5左右的宽松阈值跑看召回结果里的噪音比例再逐步收紧到刚好过滤掉无关记忆的位置。阈值太紧会导致“存了但检索不出来”这是最常见的问题之一。时间衰减是让近期记忆权重更高的手段。我在排序时对每个记忆分数乘以衰减系数decay 0.9 ** days_ago final_score similarity * decay意思是30天前的一条记忆相似度即使很高分数也会被压到约0.04基本沉底。同时我给检索加了一层“记忆翻新”被召回的记忆access_count加1access_count高的记忆在排序时加一个小权重。这套组合下来系统对近期的偏好变化很敏感又不会彻底忘掉长期稳定的事实。2.4 注入侧给上下文留出“记忆专区”检索到的记忆怎么放进对话同样有讲究。直接拼进用户消息里会很突兀模型容易把记忆当成新对话内容导致格式混乱。我统一放在system prompt里单独做一块_SYSTEM_PROMPT_TEMPLATE 你是我的AI助手。请根据下面的已知信息回答用户问题。 已知信息如果与当前问题相关请合理使用如果不相关请忽略不要主动提及。 【用户记忆档案】 {memory_blocks} 【当前对话规则】 1. 回答要简洁、直接 2. 基于事实不确定的内容不要编造 为什么要加“不相关就忽略”这句不加的话模型经常把记忆里最显眼的一条当成主题比如用户问天气它非要把“用户喜欢咖啡”也带上毫无意义。加了这句话之后模型能判断相关性检索结果里混入的一些轻微噪音也不影响最终输出。记忆冲突也要处理。旧记忆说“用户喜欢A”新记忆说“用户现在用B”两个都在库里排序时可能同时被召回模型就蒙了。我的解决办法是写入检测新入库的preference和fact类记忆如果与已有记忆内容冲突直接执行覆盖或淘汰旧条目。冲突检测条件不能太严格我的判断标准是有重叠实体且主题同一比如都提到“笔记软件”且结论不同才算冲突。3. 实操从零搭一个最小可用的ai-memory模块3.1 技术选型和环境准备先列环境。Python版本我用的是3.10向量库Chroma嵌入模型默认OpenAI接口本地可以切bge-small-zh-v1.5。安装命令pip install chromadb openai如果用本地嵌入加一句pip install sentence-transformersChroma选它的理由是够轻一个Python进程就能跑数据落盘在本地目录不需要单独部署服务。项目初期数据量不大时完全够用等要上生产了再迁移到Qdrant也容易因为抽象层在Memory类里换存储后端不用改业务代码。3.2 核心代码Memory类我习惯把记忆操作封装成一个类对外暴露三个方法从对话里提炼并写入、单条写入、召回。业务方完全不关心底层是Chroma还是别的只调用add和recall。import json import chromadb import chromadb.utils.embedding_functions as embedding_functions from openai import OpenAI client OpenAI() class MemorySystem: def __init__(self, collection_nameai_memory): self.chroma_client chromadb.PersistentClient(path./memory_store) self.ef embedding_functions.OpenAIEmbeddingFunction( api_keyyour-api-key, model_nametext-embedding-3-small ) self.collection self.chroma_client.get_or_create_collection( namecollection_name, embedding_functionself.ef ) def _generate_id(self): return fmem_{uuid.uuid4().hex} def add_memory(self, content, mem_type, importance0.8, source_session): mem_id self._generate_id() self.collection.add( ids[mem_id], documents[content], metadatas[{ type: mem_type, importance: importance, created_at: int(time.time()), source_session: source_session, access_count: 0 }] ) return mem_id def extract_and_add_memories(self, conversation_text, source_session): extract_prompt 你是一个记忆提炼器。根据以下对话提取值得长期记住的内容。 只输出JSON数组每条记录包含: - content: 一句话描述主谓宾完整 - type: fact | preference | task_summary - importance: 0到1的小数0.7以上才值得长期记忆 不要提取: 临时寒暄、一次性问题、任何证件号码、银行卡号、密码等敏感信息。 对话记录 {conversation} resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: extract_prompt.format(conversationconversation_text)}], temperature0 ) raw resp.choices[0].message.content try: records json.loads(raw) except json.JSONDecodeError: records [] added_ids [] for record in records: if record.get(importance, 0) 0.7: continue mid self.add_memory( contentrecord[content], mem_typerecord[type], importancerecord[importance], source_sessionsource_session ) added_ids.append(mid) return added_ids def recall(self, query, top_k5, min_score0.3): results self.collection.query( query_texts[query], n_resultstop_k ) memories [] for idx in range(len(results[documents][0])): doc results[documents][0][idx] meta results[metadatas][0][idx] dist results[distances][0][idx] if dist min_score: continue # 时间衰减设定记忆有效期约30天 import math days_ago (time.time() - meta[created_at]) / 86400 decay 0.92 ** days_ago score (1 / (1 dist)) * decay memories.append({ content: doc, type: meta[type], score: round(score, 4), created_at: meta[created_at] }) memories.sort(keylambda x: -x[score]) return memories[:3]注意两个细节。一个是Chroma的Collection创建问题如果重复创建同名Collection会抛异常所以用get_or_create_collection更安全。另一个是阈值传进去的min_score代表距离上限实际我结合L2距离直接判断过滤。排序时我没有直接用原始距离而是转成一个1/(1dist)的相似度分再做时间衰减这样分数更直观也方便调试。3.3 主流程接入把记忆拼进system prompt对话主流程接入很直接。每次用户发新问题时调recall把召回记忆拼成文本再注入system prompt然后调模型。def chat_with_memory(user_prompt, memory_system, session_id): memories memory_system.recall(user_prompt, top_k5) memory_blocks \n.join( [f- {m[content]} (相关度:{m[score]}) for m in memories] ) system_prompt f你是我的AI助手。请根据下面的已知信息回答用户问题。 已知信息如果与当前问题相关请合理使用如果不相关请忽略不要主动提及。 【用户记忆档案】 {memory_blocks} 【当前对话规则】 1. 回答要简洁、直接 2. 基于事实不确定的内容不要编造 resp client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: system_prompt}, {role: user, content: user_prompt} ], temperature0.7 ) return resp.choices[0].message.content一句话总结这个流程用户说话 → 查记忆 → 把记忆写进system prompt → 让模型回答问题。记忆是加强信息不是直接回答。这保证即使召回不完美模型也不会被坏记忆带偏。3.4 本地效果实测从“不认识你”到“记得你”我用一个模拟场景实测效果。第一轮对话结束后调用提炼写入第二轮新开一个会话问推荐看模型是否能利用记忆。# 第一轮会话 conversation1 用户我最近在做一个智能硬件产品目标用户是喜欢极简风格的年轻人。 用户这个产品最重要的是无感交互我特别讨厌那种到处弹窗的提示方式。 memory_system MemorySystem() memory_system.extract_and_add_memories(conversation1, source_sessionsession_001) # 第二天用户开了新会话完全不提背景 reply chat_with_memory(帮我推荐几款适合记录灵感的笔记软件, memory_system) print(reply)实测中系统召回了两条核心记忆“用户正在做智能硬件产品”“用户偏好极简风格反感弹窗提示”。最终回答是考虑到你偏好极简风格并且不喜欢弹窗提示我建议优先看这几款笔记软件Flomo界面干净、无广告弹窗、Notion可自定义但需要一些配置、Apple备忘录原生简洁。其中Flomo比较适合快速记录灵感。没有记忆时同一个问题的回答大概率是通用罗列Notion、Obsidian、印象笔记不会考虑“讨厌弹窗”这个约束。这个差异就是记忆系统的价值所在不是回答得更准确而是回答得更“像懂你的人”。4. 常见问题与排查技巧实录4.1 明明存了记忆为什么检索不出来这个问题我踩过不只一次。表现是库里明明有相关记忆但recall返回空或者全是无关内容。排查顺序如下先降相似度阈值试试——很多时候是距离阈值设得太紧把本应命中的结果挡在门外。再看记忆条目的content是不是太短或太口语化比如“喜欢简洁”这种断片式文本向量含义模糊很难和后续问题匹配上。解决速度最快的办法是提高写入时的提炼质量。我在提取Prompt里特意加了“主谓宾完整”的约束效果立竿见影。比如“用户喜欢简洁配色”比“简洁”强太多。如果感觉还不够可以用多路召回同一问题分别查原始对话摘要和结构化记忆合并去重后再排序命中率明显提升。4.2 召回了无关记忆把回答带偏了这跟检索阈值太松或者Top-K过大有关。有一次我为了“不漏”把Top-K调到10结果模型把“用户喜欢咖啡”都用来回答“如何优化登录流程”输出极不专业。这事的根因不是单纯的数量问题而是模型无法判断相关性。解决方案有两条线。一条是调参收窄Top-K到3到5提高相似度下限。另一条是给模型更多约束在system prompt里强调“不相关就忽略”。把这两条同时做掉基本能把噪音影响压到最低。4.3 记忆越积越多膨胀到没法用记忆库无限增长是必然的不清理迟早出问题。我用的组合策略是一是定时淘汰importance低于0.5且access_count为0的条目超过30天直接删除二是会话级总结历史会话的原始对话摘要保留在长期库但限制条数三是同类合并多次出现的相同主题记忆自动合并只保留最新版本。4.4 新旧记忆打架用户偏好变了系统还留着旧偏好这是最常见的冲突场景。比如用户之前说“我喜欢Notion”后来又说“Notion太复杂了换Flomo”。两条记忆同时存在检索时都可能命中模型就混乱。写入侧做冲突检测是最有效的手段在新偏好写入前检查同主题旧记忆并标记为过期。我的简化方案是删旧写新——检测content里是否有相同实体且type相同的记忆有就先delete再add。4.5 安全与遗忘能力是硬要求最后说一个容易被忽视但必须重视的点记忆系统要有遗忘能力。用户在对话里可能无意暴露手机号、地址、公司内部信息系统不能机械全记住。除了在提炼Prompt里强调不抽取敏感信息还要提供删除接口让用户能主动清除某类记忆。这也是“可以被遗忘”的合规底线。做AI记忆记得住是技术忘得掉是责任。问题现象常见原因排查方向解决方案存了但取不出阈值过严、内容残缺检查距离阈值和记忆文本放宽阈值、提升提炼完整性取出但用不上Top-K过大、阈值过松检查召回结果噪音占比收紧参数、增加相关性约束回答被带偏模型盲目使用记忆查看注入模板措辞加“不相关就忽略”声明记忆爆炸缺少淘汰机制统计库内无效条目清理策略合并压缩新旧矛盾写入未做冲突检测检查同主题记忆删旧写新我个人实际做下来最大的体会是ai-memory这类项目存储和检索反而是简单的真正的门槛在“记忆策略”。什么时候记、记什么、怎么防止模型乱用记忆这些决策没有标准答案完全取决于你的产品形态。对话式助手可以激进一点多存偏好知识库问答就要保守只存稳定事实。当初我做第一批测试时也觉得把对话全扔进向量库就行结果被各种噪音折磨得够呛。后来老老实实把提炼、衰减、冲突处理都加上效果才算稳定。建议你动手做的时候先跑通流程再逐项调策略不要一上来就堆功能。
网站建设高端定制企业官网