最近有位朋友找我诉苦:他做的智能体,单轮问答看着挺聪明,多聊几轮就露馅——用户上午说“我喜欢偏口语化的回答”,下午问技术问题时,它照样甩出一堆书面术语;用户问“上次我那个项目整改了吗”,它愣是不知道“那个项目”是什么。这些症状指向同一个病根:没有一套真正的智能体记忆体系,只靠上下文窗口硬扛。
我接手看过很多类似的实现,大多数属于“日志型记忆”,就是把用户历史消息原样塞进提示词。Token一涨,响应变慢,而且混合了事实问答、用户偏好和临时话题,模型根本分不清哪些该长期记住、哪些用完即可忘。真正能用起来的方案,必须把知识库和长期记忆拆开,做好分工、写入与更新三条链路。这篇文章我就结合我落地过的一版智能体记忆系统,把这套体系怎么设计、怎么实现、怎么踩坑一次讲清楚,适合正在搭对话类智能体、想解决“记不住”问题的开发者参考。
1. 智能体记忆该分几层:从窗口、消息到画像
先明确一个容易混淆的点:我们常说的“记忆”不是一个东西,而是至少三类数据的统称。早期我做智能体时也犯过一刀切的错误,直到把记忆拆成三层,整个系统的可维护性才真正提上来。
1.1 工作记忆、知识库与长期记忆的定位差异
这三层可以类比成一个专职助理的工作状态:
- 工作记忆(Working Memory):就是当前这通电话里聊到哪了。对应智能体是当前会话的上下文,包括最近几轮的用户输入、助手回复、当前正在处理的任务状态。它最大的特点是短命、易失,对话一结束基本就可以丢弃。
- 知识库(Knowledge Base):相当于助理入职时读的公司制度、产品手册、常见FAQ。它是全局共享、相对稳定的权威事实源,所有用户问同一类业务问题,都应该从这里拿到一致答案。
- 长期记忆(Long-term Memory):相当于助理对你这个人的了解——你偏好什么风格、上个月办过什么事、最近有什么目标和禁忌。它按用户维度隔离,跨会话存续,需要持续不断地写入和更新。
注意我这里把知识库和长期记忆都列入“记忆体系”,但它们的分工完全不同。知识库是静态底料,长期记忆是动态画像。如果混在一起,容易遇到两难:知识库更新一次要全量重构,而用户画像每分钟都在变,两者根本没法用同一套更新节奏管理。
1.2 为什么不能只靠上下文窗口硬扛
很多人第一反应是:“现在模型支持几十万Token,把用户历史全塞进上下文不就行了?”我试过,短期Demo可以,生产环境撑不住。
首先是成本与延迟。假设一个用户聊了200轮,每轮平均800Token,光历史就有16万Token。每次请求都带着这么大一段做预填充,推理时间会明显拉长,按调用量计费的项目成本直接失控。为了让历史不超限,你只能不断截断,结果往往是保留了“更早但没用的寒暄”,丢掉了“最近但关键的决策”。
其次是信号干扰。把几十条“我喜欢这个”“我不喜欢那个”全部平铺在上下文里,模型容易在回答时被互相矛盾的表述带偏。比如用户几天前说“多举例”,今天说“直接给结论”,如果不做更新合并,模型看到两个历史都还在,就会摇摆不定。记忆体系要解决的不是“能不能塞下”,而是“该记住什么、该忘掉什么、以哪个版本为准”。
1.3 三层记忆的协作主线
我在实际项目中把三层记忆的关系画成一条流水线:
- 用户新消息进来,优先读取该用户的长期记忆摘要,拼装到提示词的“用户画像”段落。
- 判断用户当前问题是否涉及事实查证,如果涉及,就从知识库检索相关内容,而不是依赖模型内部知识。
- 模型生成回复后,把本轮关键信息异步提取出来,决定哪些写入长期记忆、哪些只停留在本轮工作记忆、哪些需要同步更新知识库索引。
这套主线的核心思路是“各司其职”:知识库负责提供稳定的答案底座,长期记忆负责提供个性化的用户视角,工作记忆负责衔接临时上下文。下面几节我把每部分的重点拆开讲。
2. 知识库与长期记忆的分工边界:什么内容该放哪里
这里是最容易让开发者犹豫的地方。我常被问到:“用户说‘我是企业客户’,这算知识库还是长期记忆?”答案不是非黑即白,但确实有一条可以执行的判定路径。
2.1 按来源、作用域和更新频率三个维度切分
我建议用三个问题来判断一条信息该放进哪类记忆:
- 它来自哪里?来自产品手册、业务规范、权威文档,基本归知识库;来自用户在对话中的自我表述、行为记录,基本归长期记忆。
- 它作用在谁身上?回答任何用户都需要的事实,放知识库;只对该用户有效的偏好和背景,放长期记忆;两者交叉的情况,优先长期记忆,因为个性化胜过通用规则。
- 它多久变一次?月度甚至季度才更新一次的内容,适合放知识库,通过变更流程统一维护;每次对话都可能变化的内容(比如“最近在看什么”“下周的计划”),必须进长期记忆。
举个例子。“售后支持的时间是工作日9点到18点”来自运营制度,所有用户适用,放知识库。“某用户明确说‘我晚上才有空处理工单’”来自个人表述,只影响该用户的沟通策略,放长期记忆。如果用户说“我们公司的制度是晚上处理”,这既包含个人偏好又包含单位规则,但真正影响对话决策的还是“对该用户应该安排在晚间”,所以仍记入长期记忆,知识库不需要改动。
2.2 用“事实一致性”和“个性化”做双重校验
放知识库的内容必须满足事实一致性,也就是同一个问题无论谁问、什么时候问,答案都应该稳定。知识库里的条目如果出现矛盾和滞后,受影响的是一整批用户。所以知识库的写入门槛要高:新增条目必须经过格式校验、同义词归并和简单的查重,不能把用户随口说的一句话当成事实灌进去。
长期记忆恰恰相反,它允许“事实的碎片化”。用户今天说“我更喜欢简洁”,明天说“这次帮我详细点”,这两个记忆可以共存,但系统要能按场景选择:如果用户明说“这次帮我详细点”,当前请求就应该临时覆盖画像里的默认偏好。这意味着长期记忆里除了存内容,还得有时间戳、置信度和来源。
我在精调阶段总结过一条好记的口诀:知识库回答“世界是什么”,长期记忆回答“你是谁、你要什么”。做到这一层,智能体的回答才不会时而“失忆”,时而“自作主张”。
2.3 一个容易忽视的交叉更新场景
知识库和长期记忆不是完全隔离的,它们可能互相触发更新。
最典型的是“用户业务属性变化”。比如用户一开始问的是免费版功能,后来咨询企业版套餐并明确表示“我们要升级”,这既需要读取知识库的企业版信息,又要在长期记忆里更新一条“该用户处于付费转化阶段,关注点偏价格和权限”。如果不更新长期记忆,下次用户再进来,智能体可能还在推荐免费版,导致明显的体验割裂。
另一种交叉更新是知识库本身错误被用户纠正。用户说“你们文档写的库存接口是POST,但我们实际调试是PUT”,经过校验确认后,应该修正知识库条目,同时也要在长期记忆里记一条“该用户曾反馈过库存接口疑义”,方便后续跟进。这种双向联动,靠单一记忆存储是实现不了的。
3. 长期记忆的写入:从对话里提炼什么、怎么存
长期记忆是整个体系里最需要精细设计的部分,因为它是动态的、跨会话的、且最容易写出“垃圾信息”的地方。写入做得不好,后面检索和更新都会出问题。
3.1 先定记忆Schema:别把原始文本直接塞进去
我见过不少实现是把用户闲聊的关键句直接存成文档,再向量化。这么做的问题是无法做字段级更新:“用户不喜欢每天早上九点的推送”和“用户最近改成每晚八点查看消息”会变成两条接近但冲突的向量记录,检索时可能同时命中,模型无所适从。
我建议给长期记忆设计一个轻量但足够的结构:
- user_id:记忆归属人。
- memory_type:偏好、事实、目标、行为事件等。
- topic:该记忆对象的主题,比如“推送时间”“回答风格”“绑定账号”。
- content:标准化摘要文本,不是原文,而是模型抽取后的结论。
- value:可选的结构化值,如时间:“20:00”。
- updated_at:最近一次更新时间。
- confidence:置信度,0到1。
- source:来源是用户直说、行为推断还是历史记录修正。
一个实际例子。用户说“别老在早上发消息,我都是晚上看”,抽取后可以存成:memory_type=偏好,topic=推送时间,content=用户偏好晚上查看消息,value=20:00,confidence=0.9。下次对话涉及到推送策略时,系统能精确取出这条偏好并执行。
3.2 用LLM做抽取,但要用规则兜底
信息抽取靠大模型,这件事本身没什么争议,但完全裸奔地让模型自由发挥也不行,有两类坑我实际踩过。
坑一是模型容易把临时话题当成长期偏好。用户说“今天特别烦,别给我推广告”,如果直接存成“用户不喜欢广告”,后续几天都会误判。我的办法是在抽取提示词里明确要求模型只抽取“具有跨会话价值的信息”,并列举典型类型:身份属性、长期偏好、可验证的事实、目标/计划、异常事件。临时情绪、一次性的指令、重复的寒暄都丢弃。
坑二是模型偶尔会生成幻觉字段,比如把用户没有提过的“职业:产品经理”补进去。我加了一层规则校验:如果content里出现用户原文中不存在的实体名词,该记忆条目标记待确认,置信度降到0.4以下,不参与主动召回。规则兜底不复杂,但能拦住大部分幻觉写入。
3.3 写入时机:对话结束异步处理,不在响应路径上阻塞
长期记忆的写入不应该直接卡在用户请求的响应链路里。最开始我图省事,在每次模型返回前同步调一次抽取,结果用户等待时间多了1到2秒,体验明显变差。
更稳的做法是对话结束或者一轮交互完成后,把本轮的文本快照放进一个异步队列,后台任务统一做抽取、去重、合并和落库。这样用户感知不到记忆写入的开销。如果对话特别长,可以每5轮做一次阶段性抽取,避免最后一次性处理大量文本导致信息互相淹没。
我在项目里用的队列非常朴素:把对话记录按会话ID存好,消费者拉取后调用抽取接口,产出结构化的记忆候选,再进入合并模块。整个链路对业务代码无侵入,出了问题也只是记忆滞后,不会影响正常问答。
4. 记忆的更新与遗忘:如何对抗时间、冲突和膨胀
写进去只是开始,更难的在于怎么更新已经存在的记忆。用户的信息在变、旧记忆会失效、新记忆会冲突,如果只做增量插入,长期记忆很快变成一锅粥。
4.1 覆盖、合并与并存:三种更新策略按场景选择
长期记忆更新不是简单的“把旧值删掉换成新值”。要根据信息类型选择策略。
- 覆盖(Replace):适用于明确的替代性信息。用户说“我现在用的是企业版,不是个人版了”,这种情况应该直接覆盖旧记忆,因为旧值已经失效。如果保留旧值,反而会干扰后续判断。
- 合并(Merge):适用于可叠加的偏好或事实。用户先说“喜欢技术深度内容”,后说“也想看点实践案例”,两者并不冲突,应该合并成“偏好技术深度+实践案例”。
- 并存(Keep Both):适用于可能因场景而变的信息。比如“用户工作日偏好上午沟通,周末偏好晚上沟通”,这两条不能互相覆盖,必须同时存在,并在检索时结合当前时间做加权。
判断用哪种策略,我一般看三点:新旧值是否逻辑互斥;更新时间是否接近;用户是否有明显的“改口”信号。如果用户只是补充信息,尽量合并;如果用户明确说“以后都按新的来”“不再是”,就放心覆盖。
4.2 时间衰减与置信度:让旧记忆慢慢退场
长期记忆里最危险的一类信息是“曾经重要,现在已经过期”。比如用户半年前提过“准备年底认证”,现在认证早结束了,这条记忆如果还在高权重召回,模型可能会回答“您还在准备认证吗”,很尴尬。
我给每条长期记忆加了一个衰减因子。召回排序时,分数 = 语义相似度×0.7 + 置信度×0.15 + 时间衰减系数×0.15。时间衰减系数按半衰期计算,比如对“目标计划”类记忆设30天半衰期,30天后权重减半,60天后降到四分之一。对“身份属性”类记忆设180天半衰期,避免用户只是随口说过一次职业,就被永久当成当前身份。
衰减不会立刻删除记忆,而是让它在排名中逐渐靠后。只有连续N次主动召回都没有被最终使用的记忆条目,才会进入归档流程。这样既避免误删,又保证旧信息不会一直抢占提示词空间。
4.3 定期压缩与隐私脱敏:控制体积,保护底线
随着使用时间拉长,长期记忆必然会膨胀。我在测试阶段就发现,有个重度用户跑了两个月,画像文件已经超过2万Token,再继续全量塞进提示词,效果反而变差。
解决办法是分层压缩。保留一份“最近30天的高置信记忆明细”,以及一份“全部记忆的月度摘要”。平时回复只用明细+摘要,当用户具体问起很久远的事情时,再触发深度检索,从归档存储里把细节找出来。这个思路和RAG的分级索引很接近,理解门槛不高,但很实用。
隐私是另一个常被低估的点。长期记忆不该存储明文账号、密码、手机号、身份证号等敏感字段。我在写入前会做脱敏:遇到手机号/邮箱,只保留掩码或哈希;对一些敏感的偏好描述,尽量抽象成行为标签而不是原文。公众号、内部工具类项目尤其要提前想清楚,记忆数据泄露一次,口碑就完了。
5. 完整链路实操:一次对话如何走过记忆读写
前面把设计讲完了,这节我直接给一条可落地的链路实现。不需要复杂框架,我用伪代码展示核心逻辑,读者可以在自己的智能体服务里直接参考。
5.1 一次用户请求的记忆访问流程
我实现的访问顺序是固定的,简单说成四步:
- 读取画像:请求进来后,先从长期记忆服务读出该用户的“高置信记忆摘要”,拼进系统提示词。
- 知识库检索:根据当前消息走向量检索或关键词检索,拿到候选知识片段,一并拼进上下文。
- 生成回复:模型结合画像和知识片段产出回答。
- 异步写入:响应返回后,后台根据对话内容抽取新记忆、更新旧记忆。
这四步最容易被忽略的是第一步和第四步的联动。很多人只做读取,不做写入,结果每次对话都从零开始。实际上每一次有价值的对话,都是在丰富后续对话的“记忆画像”。
5.2 核心模块伪代码
下面这段伪代码是长期记忆服务的最小骨架,重点在写入和更新策略:
class MemoryStore: def __init__self): self.memories = {} # key: (user_id, topic), value: MemoryEntry def recall(self, user_id, query, current_time): # 召回:先按相似度粗筛,再用置信度和时间衰减重排 candidates = [m for m in self.memories.values() if m.user_id == user_id] scored = [] for m in candidates: sim = embedding_similarity(query, m.content) decay = 0.5 ** ((current_time - m.updated_at).days / m.half_life) score = sim * 0.7 + m.confidence * 0.15 + decay * 0.15 if score > self.recall_threshold: scored.append((score, m)) scored.sort(reverse=True) # 取前K条,拼成用户画像摘要 return [m.content for _, m in scored[:self.top_k]] def memorize(self, user_id, extracted_memory, current_time): topic = extracted_memory["topic"] key = (user_id, topic) if key not in self.memories: self.memories[key] = new_entry(...) else: old = self.memories[key] if self.is_conflicting(old, extracted_memory): # 冲突且用户改口 -> 覆盖;场景相关 -> 并存 if extracted_memory.get("override", False): self.memories[key] = new_entry(...) else: self.memories[(user_id, topic + "#" + timestamp)] = new_entry(...) else: # 不冲突则合并补充内容 old.content = self.merge_text(old.content, extracted_memory["content"]) old.confidence = min(1.0, old.confidence + 0.1) # 同步更新衰减和更新时间这段代码看起来很短,但我实际开发时在is_conflicting和merge_text上花了不少功夫。冲突的判断不能只看文本相似度,还要结合时间差。如果新旧两条记忆间隔只有几分钟,多半是用户补充说明,不该粗暴覆盖;如果间隔了一个月且话题相同,基本可以认定是状态变更,覆盖更合理。
提示词层面,我会在系统提示里预留一个“长期记忆画像”段落,把召回结果拼进去,并写明“以下是你对用户的已知长期信息,回答时可结合,但不要编造”。这样模型既用得上记忆,又不会把记忆当成唯一依据。
5.3 实操过程中的量化效果
我拿这个方案在一套内部客服智能体上做过对比测试。测试分三组:只用知识库、用知识库+上下文窗口、用知识库+长期记忆全链路。结果显示:
- 只用知识库:单轮事实问答准确率不错(约92%),但涉及“用户之前提过”的问题,准确率直接掉到31%。
- 知识库+上下文窗口:短对话里还不错,但聊到30轮以上,响应耗时增加接近60%,且经常被早期无关内容干扰。
- 知识库+长期记忆:在第10轮以后仍然能准确引用用户早期偏好,长对话响应耗时的增长也稳定控制在10%以内。
指标不是重点,重点是这个对比说明了分工的价值。长期记忆不是替代上下文,而是从上下文里把真正值得跨会话留存的信息抽取出来,让上下文窗口专注“当前任务”,两者不抢位置。
6. 常见问题与排查技巧:我踩过的六个坑
最后这部分,整理几个实操中高频出现的问题。每个问题后面都附上我的排查思路。
6.1 记忆明明写入了,但下次对话就是召不回
这个坑90%出在阈值设置上。我一开始把召回阈值设得比较高,导致语义相似度稍微不足的记忆全被过滤掉。所谓“相似”不是用户原话,而是改写后的陈述。用户第一次说“我喜欢看案例”,下次问“有没有实际项目展示”,这两句话语义相近,但向量相似度往往只有0.6左右,阈值一旦设到0.75就召回失败。
排查办法:给召回模块加一个日志开关,记录每次候选记忆的相似度分数。一旦发现用户画像没有被加载,先看是不是所有候选都低于阈值,再决定是把阈值调低,还是把embedding模型换成更适配领域术语的模型。根据我的经验,阈值在0.55到0.65之间是一个比较安全的起点。
6.2 记忆互相冲突,模型回答前后不一致
这种情况通常是并存策略用多了导致的。我之前为了避免覆盖风险,把所有冲突记忆都做了并存,结果同一个人既有“偏好邮件沟通”又有“偏好电话沟通”,模型每次随机命中一条,用户自然觉得智能体精分。
后来我把策略改成:同一topic下,如果有新记忆写入,且新旧记忆时间差超过一天,默认以新记忆为准;如果时间差小于一天,才考虑并存。同时给每条记忆增加状态位,命中后没被采用的分支进入待归档。这套规则跑了一个月,回答一致性提升明显。
6.3 记忆膨胀导致提示词过长,生成质量下降
这个相对好排查。打开实际发送给模型的提示词,看记忆画像段落的Token占用。如果超过总提示词的三分之一,就需要启动压缩任务:把低置信度、久未命中的记忆摘出去,或者用一句摘要替换多条同类明细。压缩频率我建议一周一次,太频繁容易误伤近期记忆。
6.4 写入阻塞了用户响应
这个我前面说过,最简单的解法是异步化。但有一些实现会在响应返回前同步做“重要的”记忆写入,比如积分变化。这种情况可以采用“预写入+回滚”的思路:响应返回前先把关键事件写入临时表,异步确认对话结束后再落库。而不是在同步路径上调用大模型抽取,这是大部分延迟问题的根源。
6.5 用户改了偏好,但旧记忆还阴魂不散
覆盖策略没生效,多半是旧记忆仍然存在于向量索引里。我在项目里遇到过一次:关系库里已经把偏好更新成“简洁”,但向量库里的旧文本还能被检索到。这是因为向量库不会因为关系库更新而自动删除对应向量。
解决办法是维护一个“失效记忆ID”清单,召回时先对候选ID做过滤。删除或覆盖旧记忆时,不仅要更新主存储,还要同步发一条索引失效事件给检索模块。这个联动看起来是小事,但忘了它,前面所有更新策略都白做。
6.6 用户隐私数据被写入记忆,被其他请求误召回
这属于比较严重的合规问题。我的经验是在写入阶段做三层过滤:字段级脱敏、内容级禁用词检查、敏感话题归档隔离。比如用户提到“我们企业税号是XXX”,系统抽取时直接把税号替换为脱敏占位符,不存原文。同时在读取端,任何记忆召回都限定在userId作用域内,绝不跨用户读取。做到这两点,基本能避免大部分误用风险。
写在最后:关于记忆体系的一点体会
这套记忆体系我从第一版“全量塞上下文”改到“三层分工”,前后重构了两次,最深的体会是:记忆系统的复杂度不在于存储中间件选型,而在于写入策略和更新策略的取舍。你能写出一个向量库很容易,但要定义清楚“什么值得记、什么时候覆盖、什么时候遗忘”,需要反复打磨。
如果正在看这篇文章的你也准备给智能体加记忆,我的建议是先把知识库和长期记忆分开,不要混在一个库里。然后按“读取画像→检索知识→生成回复→异步写入→定期衰减”的顺序搭主干,前期先跑通再调优。每一个环节都不要贪多,先把用户偏好记忆做好,就已经能解决大部分“记不住”的痛点了。
按这个思路落地后再回头看,你会发现智能体不再是一问一答的无状态接口,而是真的像一位知道来龙去脉的助手。这也是我认为智能体记忆体系最值得投入的原因。