news 2026/9/26 6:36:39

AI记忆系统实战:从零搭建大模型长期记忆服务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI记忆系统实战:从零搭建大模型长期记忆服务

你有没有过这种体验:和AI助手聊得正深入,它忽然完全不记得你十分钟前说的话;换个新对话,又要从头开始自我介绍一遍。我搞这个名叫ai-memory的项目,起因就是受不了这种"金鱼式"对话体验。当时我正好在给一个客服机器人做长期迭代,发现用户反复问同样的问题、业务方反复抱怨"它怎么一点都不懂我",于是动了念头:与其每次把上下文硬塞进窗口里,不如给AI单独建一套记忆系统。这个项目做下来,核心就三件事:把对话里值得记的东西抽出来、存起来、在合适的时候塞回去。

这篇文章适合两类人。一类是正在做Agent、客服机器人、AI助手的开发者,想给产品加上长期记忆却不知道从哪下手;另一类是单纯对"AI为什么记不住事"好奇的技术爱好者,想搞明白市面上那些记忆方案内部到底怎么转。我会把整个项目的设计思路、技术选型、写入链路、检索注入、踩坑实录全部摊开讲,目标是你看完就能照着搭一套自己的AI记忆服务。

1. 项目概述与设计思路拆解

1.1 为什么AI天生"记不住事"

先说一个很多人忽略的底层事实:大语言模型本身没有记忆,它的"记忆"只在上下文窗口存活。窗口内的Token就像黑板上的粉笔字,对话一结束、进程一关,黑板就被擦得干干净净。更麻烦的是,即便在单次对话内,一旦超过上下文长度,早期信息也会被截断或者相对权重变低,模型对"十几轮以前你提到过你养了只猫"这种事,经常是两眼一抹黑。

所以业界普遍的做法是"外挂记忆",也就是把模型本身当成一个纯粹的大脑,真正的记忆放在外面。这也是我在项目立项时做的第一个判断:ai-memory 不该绑定任何具体模型,它应该是一个独立于LLM之外的服务。不管底层接的是GPT、Claude还是国产开源模型,记忆服务都提供统一的读写接口。这样推理引擎可以随时换、版本可以随时升,记忆数据却不受影响。这个"存储与推理分离"的思路,后来被证明是正确的——我至少换了三次模型,记忆库一次都没动过。

1.2 记忆分层的总体设计

做记忆系统最忌讳的是把所有信息一股脑塞进一个池子。我的做法是参考认知科学里的记忆分类,做了三层划分:

第一层叫短期工作记忆,对应最近几轮对话的完整原文,保留在会话上下文中。第二层叫情景记忆,记录"用户在某个时间点做过什么事"——比如"2025年3月12日用户反馈登录页面加载慢"。第三层叫语义记忆,是用户稳定的偏好和画像——"用户偏好简洁回复""用户公司使用的是微服务架构"。这三层的更新频率、重要程度、查询方式完全不同,混在一起会导致系统臃肿且检索混乱。

对应到代码实现上,我用了两张表来支撑这三层:主表conversations存短期原文,memories表存提炼后的事实和偏好。每次对话结束后,系统后台异步执行一次"记忆提取 + 写入",就像人睡一觉后大脑在整理白天的经历一样。这个异步设计很关键,因为它保证了对话主链路的低延迟,提取过程再慢也不影响用户交互。

1.3 与市面上主流记忆方案的差异

做之前我也调研过Mem0、Zep、Letta这些现成方案。坦白说它们各有亮点,但我最终选择自己写,原因有三。第一,自托管和可控性:很多开源方案部署包里有额外的服务依赖,运维成本不低,我希望整个项目用一个Docker Compose就能拉起来。第二,定制检索策略:我需要针对客服场景做"实体+语义+时间衰减"的复合检索,通用方案在这块的灵活性不够。第三,也是最重要的,想彻底搞懂原理。直接调别人的库,出了问题你只能干瞪眼;自己从零搭一遍,向量检索、重排序、记忆冲突解决这些细节才能门儿清。

当然,如果只是想快速验证效果,直接集成Mem0没毛病。但如果你的产品有特殊的领域术语或行业属性,我还是建议自研。AI记忆本质上是很业务化的事情,通用的"记住用户喜欢猫"容易,但要记住"用户是三级经销商,下单要过财务审批流"这种带强逻辑关系的信息,没有一套自己的抽取规则很难做好。

2. 核心架构与关键技术选型

2.1 两条链路一根主线

整个ai-memory的架构可以用两句话讲完:写链路负责把对话变成记忆,读链路负责把记忆变成上下文。写链路是离线的、异步的,对实时性几乎没要求;读链路是在线的,需要保证极低延迟,否则会影响对话响应速度。两条链路中间靠一个统一的存储层连接。

读链路里还有个容易被忽略的优化点:记忆检索通常发生在LLM调用之前。也就是说,当用户输入一条新消息,系统先把当前提问转成向量,去向量库里捞相关记忆,再把记忆拼成系统提示词的一部分,最后才把完整的Prompt交给模型。整个过程增加的网络开销如果控制在30~50毫秒内,体感上是无感的。我实际压测下来,最耗时的不是向量检索本身,而是消息队列里堆积的写入任务,所以在写链路上我做了单独的worker进程,和在线服务隔离部署,避免互相干扰。

2.2 向量数据库选型:Qdrant还是Chroma还是pgvector

向量数据库是这个项目的核心存储组件。我先后试过三种,最终固定在了Qdrant上,先看对比:

维度ChromapgvectorQdrant
部署复杂度极简,嵌入式依赖PostgreSQL单容器,独立服务
百万级向量性能一般中等较好,自带过滤索引
向量过滤支持有限依赖SQL支持Payload过滤
内置混合检索不支持不支持支持稀疏向量
运维难度低中中低

选Qdrant的关键原因有三个:一是它的Payload过滤能力很强,可以给每段记忆打上user_id、memory_type、created_at等元数据标签,查询时先过滤再检索,精度和速度都能兼顾;二是它支持稀疏向量(Sparse Vector),这意味着我可以直接在数据库层面做关键词与语义的混合检索,而不用额外引入Elasticsearch;三是对Docker部署非常友好,一个qdrant/qdrant镜像就完事,本地调试也不折腾。

如果你只是想在内网小规模试用,pgvector其实也够用了,毕竟可以直接复用已有的PostgreSQL,少维护一个组件。但从项目后续扩展的角度看,Qdrant的Payload过滤机制能省掉大量应用层代码,而且它独立于业务数据库,写坏了大不了清空重建,不会把业务数据一起拖下水。

2.3 数据模型与记忆类型设计

记忆的存储结构我设计了五类字段:user_id(归属哪个用户)、type(记忆类型)、content(记忆内容)、metadata(业务附加信息)、vector(内容向量)。其中type字段尤其重要,它决定了系统如何对待这条记忆。

我把type分为四类:fact(客观事实,如"公司位于上海")、preference(用户偏好,如"喜欢表格汇总,不喜欢大段文字")、episode(事件记录,如"上周三处理过退款订单")、warning(禁忌事项,如"不要说'亲'这种称呼,用户明确表示反感")。你可能会问,直接用自然语言描述不就行了,为什么要分这么细?因为在检索时,不同类型会被赋予不同的权重。比如用户说"我要处理退款",warning类型的记忆必须高亮展示,这是绝对不能踩的坑;而episode类型的记忆可以低权重出现,作为背景参考。

实践下来,这种类型标签带来的调试收益远大于我最初的预期。你可以肉眼检查一条记忆结果,直观判断"这条答案是不是被错误的记忆带偏了"。如果所有记忆长得都一样,出问题时你只能一脸懵。

3. 实操:记忆写入链路完整实现

3.1 对话接入与预处理

写链路的第一步是拿到原始对话。这里有个设计细节:不是所有对话都要进记忆系统。我当时定了一条规则——核心业务会话全量记录,闲聊会话只做摘要。实现方式是给SDK传一个session_type参数,值为core或chat。之所以这么设计,是因为客服场景里用户可能在对话里穿插"今天天气不错""你们公司几点下班"这类闲聊,全量存入会导致记忆库膨胀,且大概率污染后续检索。

预处理阶段还需要干一件事:把对话拆分成"用户轮次"和"助手轮次",并标注消息时间戳。注意,提取记忆时不仅看用户说了什么,也要看助手回复了什么。举个例子,用户问"能不能优惠",助手回复"目前对新客有5%折扣"。如果只记用户的话,你丢失了"该用户可享受新客折扣"这个关键信息。所以我的预处理逻辑是成对提取,User消息负责触发,Assistant消息负责补充可记忆的业务回执。

3.2 LLM异步提取记忆:提示词设计

这是写链路的大脑环节。我设计了一套专门的Prompt,让LLM从"用户-助手"消息对中抽出结构化记忆。Prompt模板大致长这样:

你是一名记忆提取引擎。请从对话中提取需要长期保存的关于用户或业务的关键信息。 提取原则: 1. 只提取确定性的、未来可能用到的信息,不提取猜测性内容。 2. 如果用户提到的内容与已有记忆冲突,以最新对话为准,并标注"conflict"。 3. 每条记忆控制在50字以内,用陈述句描述,不要留口头语。 4. 如果没有值得提取的信息,返回空数组,不要硬编。 输出JSON格式: {"memories": [{"type": "fact|preference|episode|warning", "content": "记忆内容", "confidence": 0-1}]} 对话内容(用户与助手轮流): {{conversation}}

这个Prompt里有三个容易被忽视的点。一是**"冲突检测"指令,如果没有它,当用户说"我搬到北京了"而你库里有"用户在上海"时,系统会同时检索出两条互相矛盾的记忆,模型就困惑了。二是置信度字段**confidence,低于0.6的记忆会被直接丢弃或者存到草稿区等待人工审核,这能有效过滤LLM的幻觉。三是字数限制,让LLM把"好的好的,这个问题我帮我问问财务再说哈"这种噪音过滤掉,只留下"需要财务确认才能答复"的实质信息。

3.3 向量化与存储实现

提取出来的记忆是纯文本,要进向量数据库必须转成向量。我用的Embedding模型是BAAI/bge-m3,中文效果好、支持8192长度、并且能输出稀疏向量,正好配合Qdrant的混合检索。部署上我用FastAPI包了一个向量化服务,接口就一个/embed,输入文本输出768维稠密向量加稀疏向量。

存储的核心代码如下:

import qdrant_client from qdrant_client.models import PointStruct, VectorParams, Distance client = qdrant_client.QdrantClient(host="localhost", port=6333) # 创建集合,配置稠密+稀疏双向量 client.recreate_collection( collection_name="ai_memory", vectors_config={ "dense": VectorParams(size=768, distance=Distance.COSINE), }, sparse_vectors_config={ "sparse": {} } ) # 写入一条记忆 point = PointStruct( id=memory_id, vector={ "dense": dense_vector, "sparse": sparse_vector }, payload={ "user_id": user_id, "type": memory_type, "content": content, "created_at": timestamp, "confidence": 0.92 } ) client.upsert(collection_name="ai_memory", points=[point])

一个容易踩的坑是recreate_collection在正式环境千万别乱调。我早期调试时图省事,每次改完配置就重建集合,结果线上库被清了一次,几百条真实记忆全没了,只能从备份恢复。正确做法是先get_collection检查是否存在,不存在才创建,修改配置时用update_collection。

3.4 记忆去重与冲突解决

单纯提取会带来另一个问题:同一件事用户在不同时间说了好几遍,库里存了好几份相似甚至相同的记忆,检索时它们同时命中,白白消耗上下文空间。我加了两个机制解决这个问题。

第一是语义去重。新提取的记忆先跟库里最近30天的高频记忆做一次向量相似度检索,如果余弦相似度大于0.92,就认为两者是同一件事,新记忆不写入,只更新旧记忆的时间戳和置信度。这个阈值是调出来的,一开始用的0.85,结果把"用户喜欢用微信沟通"和"用户喜欢用企业微信沟通"这种有明显差异的记忆也合并了,后来改成0.92才稳定。

第二是冲突标注。当新记忆与旧记忆的向量距离很近但内容有矛盾时(比如一个说"上海",一个说"北京"),系统不直接删除旧记忆,而是把新记忆标上conflict=true,并附上resolve_at时间戳。检索时,冲突记忆默认给低权重,只有最新一条会进入上下文。为什么不全删?因为我发现用户经常说反话或者中途改主意,万一新记忆本身是错的,旧记忆还能作为兜底证据,方便人工介入排查。

4. 实操:记忆检索与上下文注入

4.1 检索触发策略:全局加局部

读链路要解决的核心问题是"什么时候把记忆注入Prompt"。最粗暴的做法是把该用户所有记忆一次性注入,对于重度用户来说,记忆可能几千条,Prompt直接爆炸。我采用的是全局注入 + 局部检索的双层策略。

全局注入的是一条经过压缩的"用户画像速览"。每当用户进行新会话时,系统会拉取该用户最近的Top 3条preference和warning记忆,投喂给LLM作为长期人格参考。这部分是固定的,每次对话都有,但内容极简,占不了多少Token。

局部检索则是根据当前用户消息,实时去向量库捞Top K条相关记忆。这里需要精细设计的是过滤条件:必须带user_id过滤,否则会检索到其他用户的记忆——这个错误我还真犯过,上线第一天就出现了"A用户问产品价格,模型却回答出B用户的专属折扣",排查了一下午才发现是检索接口漏传了user_id。

4.2 混合检索、评分与重排序

只靠向量检索有个明显短板:对专有名词和实体匹配不够精准。比如客服场景里用户报出一串订单号"ORD-20250312-001",向量检索可能因为语义上没学过这种格式而给出低分。所以我做了向量检索 + 关键词检索的混合模式,评分公式如下:

final_score = 0.65 * cosine_similarity(dense_query, dense_mem) + 0.35 * bm25_score(sparse_query, sparse_mem)

这条公式里的两个权重不是拍脑袋定的,我做了对照组实验。把权重调成0.5/0.5时,长文本语义匹配的记忆(比如"用户提到希望售后响应时间不超过2小时")的排序明显变差;调成0.8/0.2时,订单号、产品型号这类精确匹配的记忆经常漏掉。最后停在0.65/0.35,算是语义和字面匹配的均衡点。

重排序环节我用了一个轻量策略:在拿到Top 30候选后,先做一层硬性过滤,把conflict=true且created_at不是最新的结果踢掉;然后按以下规则微调排名:warning类型记忆排名+5分,preference+3分,fact不加分,episode-2分,同时按时间衰减系数exp(-0.05 * days_since)进行调整。做完这些处理后只取前8条作为最终注入记忆。

4.3 上下文注入与Prompt组装

记忆检索完了,如何优雅地放进Prompt是个技术活。直接平铺记忆列表会加重模型的阅读负担,我采用的是分类分区块的组裝方式:

【用户固定偏好】 - 偏好数据导出的格式是Excel,不接受PDF。 - 沟通时请勿使用"亲"等过度亲昵称呼。 【历史事实】 - 用户公司当前使用的是Spring Cloud微服务架构。 - 用户在本平台开通过企业版套餐。 【近期事件】 - 2025-03-10:用户反馈过一次登录超时问题。 - 2025-03-14:用户咨询过发票抬头变更流程。 【当前对话】 {{current_user_message}}

这种结构的优势在于,给模型提供了明确的"信息来源"参照系。模型在生成回复时如果用到记忆里的内容,会自然基于对应分区的事实去回答,而不是把记忆和当前对话混成一锅粥。我实际对比过,没有分区结构时,模型偶尔会把"用户以前的诉求"和"用户现在的诉求"混为一谈;加上分区之后,这个现象几乎消失了。

4.4 遗忘机制与隐私保护

记忆系统建起来容易,收尾难。如果不做遗忘机制,记忆库会堆积大量过期信息,并且带来隐私合规风险。我在项目里做了三层遗忘策略。

第一层是主动遗忘:每条记忆都带expire_at字段,episode类型的默认生命周期是90天,fact类型是180天,preference和warning不自动过期。过期记忆由一个定时任务清除,每天凌晨跑一次。第二层是用户侧遗忘:提供DELETE /memories/{user_id}接口,用户如果要求"忘记我",系统立即删除该用户全部记忆,并且在日志级别也做脱敏处理。第三层是敏感信息过滤:提取Prompt里明确加了指令"如果对话中出现身份证号、银行卡号、完整手机号,不要提取,直接返回'敏感信息已忽略'",同时在写入前还会过一次正则匹配,匹配到敏感格式就直接丢弃。

这个部分虽然看起来不性,但绝对是上线前必须做扎实的功课。记忆系统最大的风险不是记不住,而是记了不该记的东西。

5. 常见问题与排查技巧实录

5.1 记忆注入后模型变"傻"了

有段时间我接到反馈,说加了记忆系统后,模型的回答质量反而不如不带记忆的时候。查了一圈发现是注入的上下文太杂乱。比如用户问"你们的API调用限制是多少",系统却把"用户昨天吐槽过文档不清晰"这种低相关度的记忆也注入了,模型开始东拉西扯说些有的没的。

解决办法是给检索环节加了一个相关性硬门槛:最终分数低于0.62的记忆,一个字都不注入。同时把注入条数从10条降到8条。别小看这个阈值,它是个极其有效的手段。宁可让模型不知道,也不能让它被错误信息干扰。测试下来,模型回答的准确率反而提升了约15%。

5.2 记忆库膨胀严重

运行一个月后,我的记忆库涨到了几十万条向量,检索延迟从30毫秒涨到200多毫秒。排查后发现是重复记忆太多,用户每次进客服说"我要查订单"都会被抽取成一条新的episode记忆。去重机制当时只针对fact和preference,漏掉了episode类型。

修复方案是在去重检索时不再限定记忆类型,对所有类型统一做相似度比对。同时给episode类型加了合并策略:如果用户一周内重复提出相同诉求,只保留最近两次的记录,更早的自动合并成"用户本周曾多次提交某类请求"这条概括性记录。这波优化后,库的增速降了大约60%。

5.3 检索结果张冠李戴

一个经典的Bug:用户A问"我该升级固态硬盘还是机械硬盘",系统给出的记忆提示却是用户B喜欢"雷蛇机械键盘"。原因就是Payload过滤条件在某个查询分支里漏掉了user_id。这类问题的排查思路很简单,在Qdrant的Query结果里打印payload,一眼就能看出user_id是否匹配。我建议在做任何检索逻辑调整时,日志里都要输出检索命中的payload信息,方便快速定位这种"串号"问题。

5.4 可视化管理与测试技巧

记忆系统是黑盒,你很难靠肉眼判断"这些记忆存得对不对"。我后来做了一个简易的记忆管理后台,按用户维度展示所有记忆列表,支持人工编辑和删除。虽然丑,但调试效率提高了很多。

测试方面我的经验是,做一套固定的"记忆回归用例集"。里面包含几十个典型的用户提问场景,每个场景都标注了期望注入的记忆类型和TOP1检索结果。每次改动检索逻辑,跑一遍用例集,对比输出差异。没有这套自动化校验,你改完算法永远不知道是变好了还是变坏了。

常见问题速查表

问题现象可能原因解决方案
模型引用其他用户的信息检索接口漏传user_id检查Payload过滤条件
记忆冗余、库膨胀过快episode类型缺少去重统一所有类型去重逻辑
注入后回答变差低相关度记忆太多添加0.62相关性硬阈值
新旧记忆冲突缺少冲突检测冲突标注+保留旧记忆兜底
敏感数据被存入库Prompt缺少过滤指令增加正则匹配+丢弃机制

做了这个项目后我自己最深的体会是,AI记忆系统真正的分水岭不在于"能记多少",而在于"该记的准确记,不该记的坚决不记,需要时能捞得上来,过期的能删得掉"。这四个环节环环相扣,任何一个出了毛病,整套系统都会变成负资产。

最后再分享一个实用技巧:给记忆库写一个简单的记忆审计日志,把每次写入、检索、删除操作都记下来。遇到用户投诉"AI怎么知道我的……"这种问题时,你能直接翻日志看是哪条记忆在被什么时候注入的,快速判断是Bug还是正常业务逻辑。这比上线后对着代码猜,省太多时间了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 6:36:37

Python面向对象编程:从类与实例到封装继承的实战指南

1. 从函数到类:什么时候该用面向对象,什么时候不该用很多 Python 初学者学到面向对象这一章时,会有一个很真实的困惑:我明明用函数也能把程序写出来,为什么非得搞一个类出来?我当年也有这个疑问&#xff0c…

作者头像 李华
网站建设 2026/9/26 6:36:33

Agent 安全执行工程笔记

摘要:工具调用是 agent 第一次把模型的决定落到真实系统、第一次产生副作用的环节。模型不是可信主体:它会被注入劫持、会误判、会被赋权过度。工具的安全执行由三道都在执行前的闸组成——权限决定能不能做,沙箱决定做坏了多大范围,human-in-the-loop 决定谁为后果拍板。本…

作者头像 李华
网站建设 2026/9/26 6:36:02

Substrate区块链开发实战:从选型到落地自定义链

做区块链开发这几年,Substrate 这个关键词出现的频率越来越高。它是 Parity 开源的一套区块链框架,也是 Polkadot 生态的技术底座。和很多人的第一反应不同,Substrate 不是一条现成的链,而是一套让你按需组装出自己链的开发框架&a…

作者头像 李华
网站建设 2026/9/26 6:34:39

深圳可靠的降本增效公司|全流程降本增效与企业长效成本管控方案

全球制造业赛道竞争日趋白热化,精细化成本管理,已然成为国内制造企业突破内卷、构筑核心竞争力的核心抓手。扎根深圳福田的深圳市华师华咨询有限责任公司,凭借两年多的深耕实干快速崛起,在一众咨询机构中脱颖而出。不同于行业内重…

作者头像 李华
网站建设 2026/9/26 6:33:32

FameView V7.6.20.2工业组态环境适配与信任链构建

1. 这不是普通软件安装:FameView V7.6.20.2 的“环境适配”本质是工业控制系统的信任链重建很多人第一次点开杰控科技FameView V7.6.20.2的安装包,下意识就双击下一步——结果卡在“检测.NET Framework版本”、报错“无法加载MSVCR120.dll”、或者启动后…

作者头像 李华