“你的名字是?”这个问题看起来特别简单,但如果你亲手做一个能稳定回答这个问题的 Agent,会发现它背后绕不开一个更大的话题:Agent 的记忆到底有几种。
大模型本身是“无状态”的,每生成一段话都像是第一次醒来。你昨天告诉它的名字,今天再问,它可能就答不上来,甚至随口编一个。想让 Agent 真正记住事情,必须在模型外面搭一套记忆系统。我前段时间在一个模拟项目里给智能体做了完整的记忆体系,从短期缓存到长期事实库都过了一遍,踩了不少坑。这篇文章不聊空泛概念,只讲我实际做下来看到的几种记忆、各自实现逻辑,以及“你的名字是?”这句话是怎么被稳稳接住的。
1. 先看整体:Agent 的记忆系统是怎么设计的
1.1 无状态模型和有状态智能体之间的那层“夹层”
我见过不少把 Agent 做成纯对话框的项目,用户问一句它答一句,稍微多问几轮就开始胡言乱语。原因很简单:模型本身没有记忆,上下文窗口里的内容用完就丢。要让一个 Agent 表现出“记得你”,你必须把记忆变成系统设计的一部分,而不是模型的隐藏能力。
拿我做的模拟项目来举例。需求是一个能长期陪伴用户、帮用户整理待办事项的智能体,要求它记住用户的称呼、偏好、历史任务,并且在下一次对话里还能准确说出来。这听起来不难,但真正动手后才发现,光靠把聊天记录塞进上下文里是完全行不通的:塞少了记不住,塞多了窗口爆炸,塞进去的内容里还有一堆噪声。最后逼着我重新梳理了一遍“记忆”到底该怎么分。
那套做法,本质上是在模型外面加了一个夹层。模型只负责“理解和生成”,记忆系统负责“存取和筛选”。夹层下面通常分成两块:一块是跟着本次对话走的短期记忆,另一块是跨会话保留的长期记忆。两块的载体、更新策略、召回策略都不一样,混在一起用必出事。
1.2 工程实现上常见的三类分法
如果你是做算法或后端出身,可能更关心怎么落地。目前业界比较通用的分法有三类,我分别列一下。
第一类是按生命周期分:工作记忆和长期记忆。工作记忆就是当前会话窗口里能直接看到的内容,比如系统提示词、最近的几轮对话、临时变量;长期记忆则是存在外部数据库、只在需要时才被取回的内容,比如用户上个月说过的某句话、用户喜欢的颜色。
第二类是按内容性质分:情景记忆、语义记忆、程序性记忆。这个概念从认知科学借来的。情景记忆记录的是“什么时候发生了什么”,比如“1月10日用户说他要出差”;语义记忆记录的是“世界是什么样的”,比如“用户的名字叫某某,职业是设计师”,不带时间属性;程序性记忆记录的是“怎么做”,比如怎么调用计算器、怎么生成表格,这类记忆通常固化在代码或工具配置里。
第三类是按存储位置分:上下文内记忆、外部存储记忆、模型参数记忆。上下文内记忆不需要额外开发,加个 System Prompt 就能用,但容量极小;外部存储记忆需要自己造轮子,能存很多,但召回质量决定体验;模型参数记忆是训练时灌进权重的知识,比如“世界上最高的山是哪座”,这类不是你一个 Agent 应用能改的,只能借用。
这三类分法不冲突,只是看问题角度不同。我给团队画图时会把它们套在一起看:工作记忆对应上下文内,长期记忆对应外部存储,语义记忆和情景记忆都是长期记忆的“子类型”,程序性记忆则横跨代码和配置。这张图想清楚后,后面写代码才不迷糊。
1.3 回答标题里的问题:名字在记忆里到底算什么
你问一个 Agent “你的名字是?”,它要答对,至少需要两样东西。
一样是身份锚点。Agent 的名字、性格、说话风格,这些属于长期记忆里的语义记忆,应该被放在一个稳定、可检索、又有较高优先级的位置。另一样是当前上下文。如果这个名字不在本次对话的上下文里,模型自己是不会“想起来”的,得靠记忆系统在生成前把它取回来放到窗口里。
换句话说,“你的名字是?”这个问题,本质上是在考验记忆系统的“召回”能力,而不是模型的“推理”能力。很多项目翻车,都是因为把名字写死在某个不那么明显的位置,或者压根没写,结果 Agent 每次回答名字都不一样。这一点我会在第3部分拿实际代码展开。
2. 四种核心记忆的细节与实操要点
2.1 工作记忆:窗口里的临时便签
工作记忆是最容易被误解的一种记忆。有人觉得它就是聊天记录,其实不完全是。
它更像你办公桌上的一叠便签:只服务当前这件事,办完就扔。在 Agent 里,它的载体通常是上下文窗口,内容包含系统提示词、当前对话历史、外部召回的知识片段、临时推理中间结果。我给 Agent 配工作记忆时,第一原则是“能少则少”。别把用户去年说过的一句话也塞进去,那该由长期记忆负责。
但工作记忆也不是永远全部放窗口里的。实际上,随着对话变长,你必须在“保留完整细节”和“给长期记忆腾位置”之间做取舍。我常用的做法是滑动窗口加摘要:最新几轮对话原样保留,更早的对话被抽成摘要,摘要仍然留在工作记忆里,具体的原始内容存进长期记忆。这样模型看得到上下文脉络,又不会被几百轮历史撑爆。
需要注意一个参数:窗口长度。窗口不是越长越好。同样长度的上下文塞太多信息,模型对关键信息的注意力会下降,还可能导致回答变慢、变贵。我在模拟项目里把最新对话保留在10轮以内,超过10轮就开始做摘要压缩,效果明显比硬塞20轮要好。
2.2 情景记忆:记下“什么时候发生了什么”
情景记忆是长期记忆里最像“日记”的一种。它记录的是具体事件,比如“用户昨天反馈过登录报错”“用户上月25号说要去参加行业会议”。
这类记忆的存储格式通常是事件条目,每一条都带时间戳。我建议不要把原始聊天记录直接当情景记忆存,因为原始记录噪声太多、太碎片化。更靠谱的做法是先用模型抽取事件:每轮对话结束后,判断“这段对话里有没有值得长期记住的事”,没有就跳过,有就抽成一条结构化记录,再写进存储。
抽取的时候有一个坑:模型会把“用户随口一说”当成“重要事件”。比如用户说“今天好累啊”,如果你把这条存成情景记忆,下次召回时它毫无用处。我后来加了一道重要性过滤器,用几个启发式规则判断,比如是否包含具体时间、是否涉及任务承诺、是否涉及个人偏好变化,过滤完再入库。实测下来存储量降了将近一半,召回准确率反而上升。
情景记忆的召回策略也跟前两者不一样。工作记忆是“全量在窗口里”,情景记忆是“按需取回”。每次生成前,系统会把用户最近输入的向量和事件库做一次相似度检索,找出最相关的三四条历史事件,塞进上下文。这样既不占空间,又能让 Agent 说出“你上周说过要改方案,后来怎么样了”这样有人情味的话。
2.3 语义记忆:记下“用户是谁”
语义记忆和情景记忆容易搞混。区别在于:情景记忆回答“发生过什么”,语义记忆回答“是什么样的”。前者有时间属性,后者没有。举个例子,“用户今天上午说喜欢喝美式”是情景记忆,“用户喜欢喝美式”是语义记忆。
名字、职业、生日、偏好、长期目标,这些东西都该进语义记忆。我在项目里把语义记忆单独存成一张“用户画像表”,字段包括称呼、语言风格、常用工具、偏好设置、固定约束等等。每轮对话结束后,抽取模块会尝试从对话中提炼事实,如果发现新事实或原有事实发生变化,就更新这张表。
语义记忆的更新要特别小心。用户说“我最近在学画画”,这是可信的新事实,可以存;但用户说“我讨厌这个功能”,这可能只是一时情绪,不该马上写进长期画像。我的经验是:涉及长期属性的描述才更新语义记忆,涉及单次事件的描述都归到情景记忆。判断标准可以在提示词里写清楚,让模型只做“分类员”,不要做“记录员”。
召回语义记忆的时候,另一个关键点是时效性。如果用户已经改了名字,旧名字还挂在画像里,召回时就会出现“张冠李戴”。我在下一部分会专门讲这个问题的排查。
2.4 程序性记忆:记得住技能,做得出动作
程序性记忆听起来很玄,但它往往是最先被实现的一种“记忆”。Agent 的工具列表、工作流配置、API 调用的参数模板,都属于程序性记忆。
比如你的 Agent 会查天气,那“天气查询工具怎么用、参数是什么、入参格式是什么”这些知识,要么写死在代码里,要么写在系统提示词里,要么注册进工具的 schema 里。这些东西不会随对话改变,却决定了 Agent 能不能做实事。
我在项目里没把程序性记忆做成动态数据库,而是做成一套静态工具注册表加一套流程编排文件。理由很简单:技能和动作需要稳定性,用代码管理比让模型自己改更安全。如果你想做得进阶一点,可以让 Agent 在收到新指令时,动态地把一个新工具的描述和调用逻辑存入自己的工具清单,这也是近期很多自主 Agent 在尝试的方向。但老实说,动态增删工具非常容易翻车,生产环境建议先锁定工具集,只开放参数级别的调节。
2.5 记忆的写入与召回节奏:一切都看触发器
上面四种记忆都明白了之后,你会意识到:记忆不是把所有东西一股脑存进去就行。真正的难点在节奏。
我的实践是把记忆操作都挂在事件循环的关键节点上。每次用户发消息进来,先做一次召回流程,把相关的长期记忆取回放进工作记忆;模型生成完之后,再做一次写入流程,把这一段对话里值得记的抽取出来,分门别类入库。两个流程都靠触发器控制,不是每一轮都写,也不是每轮都查。
写入触发条件我设为三选一:出现了新事实;原有事实发生变化;对话中产生了明确的待办事项或任务结论。召回触发条件则宽松一些,每次生成前都做,但会控制召回条数和评分阈值,防止无关记忆干扰生成。这套节奏,被我总结成一句话:写入要克制,召回要及时,更新要显式。
3. 完整落地:做一个能记住名字的 Agent
3.1 技术选型:为什么用缓存加向量库加结构化表
到这一步,可以开始动手了。先交代一下我用过的技术组合,然后说说为什么这么选。
工作记忆用的是进程内缓存加滑动窗口摘要,没有引入额外组件,但为了持久化会话状态,也用了带过期时间的键值缓存。这部分是纯粹的“给模型看”的,东西要新、要快、不需要检索,所以用缓存特别合适。
长期记忆里,情景记忆我放在向量库。为什么?因为场景是“我可能不记得具体词汇,但记得大概意思”。用户说“上次那个出行计划”,系统用向量检索能匹配到“出差”“订酒店”相关事件,模糊匹配能力是向量库的强项。
语义记忆则放结构化表。名字、职业、偏好这些字段固定、更新低频,用表格存可以精确读取,也方便做去重和更新。而且可以给每个字段加一个“更新时间”和“置信度”,方便判断这条记忆是否还该信。
你可能会问:为什么不能全部用向量库?因为向量召回是“相似度优先”,它适合模糊场景,不适合精确查询。你问“用户的姓名是什么”,用相似度检索可能误召回来一堆名字,但结构化表可以直接按用户 ID 取字段。我见过有人硬把所有记忆都塞向量库,结果名字经常答错,就是这个原因。
3.2 核心流程拆解:从收到消息到生成回复
有了选型,接下来是整个对话循环的设计。我用代码结构来说明,这是模拟项目里精简后的伪代码,但流程和真实实现完全一致。
第一步,接收用户输入后,先查工作记忆缓存,拿到当前会话的历史摘要和最近对话。
def get_working_memory(user_id, session_id): recent = cache.lrange(f"session:{session_id}:recent", 0, 9) summary = cache.get(f"session:{session_id}:summary") return {"recent": recent, "summary": summary}第二步,做长期记忆召回。把用户输入向量化,分别查情景记忆和语义记忆。这一步决定了生成时能看到哪些“过去的事”。
def recall_long_term_memory(user_id, text_embedding): events = vector_store.search(collection="events", embedding=text_embedding, top_k=3, score_threshold=0.7) profile = profile_table.get(user_id) # 结构化画像 return events, profile第三步,拼装最终的上下文。我会给模型一份包含三块的提示词:第一块是身份锚点,名字写在这里;第二块是从语义记忆里取回的画像;第三块是召回的事件列表和最近的对话历史。
def build_prompt(context): system = "你是小诺,用户的智能助手。你的名字固定为小诺,不要随意更改。" return [ {"role": "system", "content": system}, {"role": "user", "content": f"用户画像:{context.profile}"}, {"role": "user", "content": f"最近相关事件:{context.events}"}, *context.recent_history ]第四步,模型生成回复的同时,异步跑写入流程。把用户本轮对话和 Agent 的回复吸收进记忆系统。
def write_memory(user_id, user_message, agent_reply): new_facts = extract_facts(user_message) # 抽取语义事实 new_events = extract_events(user_message) # 抽取情景事件 if new_facts: profile_table.update(user_id, new_facts) if new_events: vector_store.add(collection="events", items=new_events, timestamp=now())这套流程看起来不复杂,但真正值得花时间的不是代码本身,而是三个模块:抽取、召回和时效控制。这三个模块我分别调了很多版,下面讲参数和经验。
3.3 参数配置与调优经验
先给一组我实测后比较好用的参数,然后逐个解释。
- 工作记忆保留最近对话轮数:10轮
- 摘要压缩触发阈值:对话超过14轮时触发
- 情景记忆召回 top_k:3
- 情景记忆召回相似度阈值:0.7
- 语义记忆更新触发:检测到新事实或事实冲突
- 记忆写入时机:模型生成完成后异步执行
top_k 设成 3 而不是 10,是因为召回进来的内容如果太多,生成的时候反而容易迷失。阈值 0.7 是要找到一个平衡点:太高了什么都召回不到,太低了召回一堆无关内容。我试过 0.5,效果非常差,用户问“上次那个方案”,系统把“上次那个饭局”也带进来了,上下文瞬间被污染。
对话轮数设 10 是我经验里的甜点值。10 轮以内的信息新鲜且完整,超过 10 轮后,关键信息开始被闲聊稀释,正好做摘要。摘要本身我建议用轻量模型生成,不用把整套对话都丢给最强模型,成本低效果也够。
另外推荐一个处理:给召回的历史事件加一条“时间衰减”逻辑。如果一条事件离现在超过30天且从未被再次提及,降低它的优先级;超过90天直接不参与召回。这样 Agent 不太会拿三个月前的过期信息来回答现在的问题。
3.4 身份锚定的细节:名字不该只说一遍
身份锚定这个话题值得单独强调一次。很多人以为在系统提示词里写了“你是小诺”就完事了,但实际操作中我发现一个问题:如果上下文后面跟着一堆历史对话,模型很容易被对话里的称呼带跑,有些用户会叫它“小诺”,有些用户误叫它“小智”,它可能就顺着改了自己的名字。
我的解决方法是三层锚定。第一层,系统提示词里固定写“你的名字是小诺”并加一句“如果用户叫别的名字,请温和纠正并重申自己叫小诺”。第二层,在用户画像里存一个名为“助手称呼”的字段,每次生成前跟画像一起取回。第三层,如果用户主动说“以后叫你小诺吧”,写入语义记忆时同步更新这个名字字段,并且在下一次对话的身份锚定描述里引用这个字段。
这样做的好处是:名字不是“一段被写死的文字”,而是“一条可以被确认和修改的记忆”。它既稳定,又有跟随真实互动的能力。我实测过,加了这三层锚定后,Agent 被用户故意叫错名字时的出错率从大约30%降到了5%以下,稳定了很多。
3.5 隐私与边界:记忆能存,但要有开关
这是不得不提的一环。让 Agent 记住用户的名字、偏好、历史事件,意味着你在收集用户的个人信息。我在设计这个方案时加了两个开关:全局记忆开关和字段级开关。用户关闭之后,系统不再写入新的情景记忆和语义记忆,召回也立刻停用,只保留当前会话的工作记忆。
另外,定期清理机制也要有。长时间无活跃的画像可以归档,用户主动删除历史时要把结构化数据和向量数据一起删干净。别小看这件事,很多系统翻车,就是因为重启了记忆功能但忘了清理旧的中间表,结果把不该出现的历史带出来了。我在下面一节会讲这类问题的实际排查。
4. 常见问题与排查技巧实录
4.1 症状:Agent 答错了自己的名字
现象是用户问“你叫什么”,Agent 说出了一个从没设置过的名字。这个问题的根因几乎都在身份锚定不够强,或者是上下文里的旧内容覆盖了系统提示词。排查顺序:先看系统提示词里名字是否清晰,再看用户画像里是否有冲突的称呼,最后看是不是之前某轮对话里名字被悄悄改了。
我遇到过一次非常隐蔽的情况:某个用户的聊天记录里有“你以后叫小智吧”,模型确实执行了更新,把助手名改成小智。但因为身份锚定没有第三层引用,后续所有对话里它都坚定地叫自己小智。修复方式就是在锚定逻辑里增加字段引用,并且加了一条异常检测:如果模型回复里出现了不同于系统设定名字的自我称呼,告警并回滚。
4.2 症状:名字张冠李戴
用户 A 和用户 B 都在同一个 Agent 里存在长期记忆,用户 A 问“我叫什么”,Agent 回答出了用户 B 的名字。这个问题十有八九出在召回时没有按用户隔离。
解决办法非常简单:所有长期记忆的读写必须带上 user_id 作为过滤条件。向量库检索时除了用相似度,还要加一个 metadata 过滤,只检索当前用户的事件集合。如果你用的是结构化表,更是天然可以用 where 条件隔离。这个 bug 的隐蔽点在于,统计数据看起来很好,名字准确率很高,但你不知道那些错误答案恰恰集中发生在多用户共用场景里,所以单测时一定要模拟多用户并发。
4.3 症状:该记住的没记住
用户上次说了自己的名字,这次问 Agent“我叫什么”,Agent 一点印象都没有。最可能的问题不是召回失败,而是写入阶段就没把名字存下来。我之前遇到过抽取模型把“我叫某某”判断为“不重要内容”直接过滤掉的情况,原因是抽取提示词里写了一个“忽略寒暄类消息”的规则,把自我介绍归到了寒暄。
排查时先查画像表,看 user_id 下有没有名字字段。没有的话,问题就出在写入侧。修复方式是把“包含姓名、称呼、年龄、职业等身份属性”设为抽取的硬规则,优先级高于其他规则。还加了一条兜底:如果对话中出现“我叫”“我的名字是”这类句式,无论模型怎么判断,强制写入语义记忆。
4.4 症状:上下文失控,生成质量越来越差
对话超过 20 轮后,Agent 开始忘事,回答变啰嗦,甚至出现明显错误。这通常不是记忆系统坏了,而是工作记忆没有做好摘要压缩。如果 Recent 队列无限增长,模型窗口很快会被旧对话占满,新召回的长期记忆反而没位置。
排查方法很简单:监控每次请求的上下文 token 数,如果持续逼近窗口上限,看摘要任务是否被跳过或失败。摘要逻辑失败往往是静默的——它可能抛了异常,但主流程没感知到。我给摘要任务加了超时和降级机制,摘要生成失败时就启用最淘汰策略,只保留最近5轮,宁可少一点也不能让窗口爆炸。
4.5 一张速查表:症状、原因、处理办法
| 症状 | 可能原因 | 处理办法 |
|---|---|---|
| 答错名字 | 身份锚定弱,或名字被历史对话污染 | 三层锚定,字段级引用,异常回滚 |
| 张冠李戴 | 召回未按用户隔离 | 读写都带 user_id,向量库加 metadata 过滤 |
| 该记住的没记住 | 抽取规则把关键事实当寒暄过滤 | 身份属性设为硬规则,句式兜底强制写入 |
| 记住的是过期信息 | 语义记忆只在新增时不更新 | 每次写入检查冲突,更新旧字段并记录版本 |
| 上下文爆炸 | 工作记忆无限增长,摘要失败 | 滑动窗口加摘要,摘要失败降级只保留最近5轮 |
| 隐私风险 | 缺少开关和清理机制 | 加全局开关、字段级开关、定期归档清理 |
4.6 三条防坑建议
最后再给三条经验,不一定所有人都认同,但我觉得很值。
第一,记忆系统的设计优先级应该是“不能错”高于“记得多”。一次张冠李戴的伤害,远大于一百次“什么都没记住”。所以召回条数宁少勿多,阈值宁高勿低,写入规则宁严勿松。
第二,“忘记”也是一种能力。Agent 不需要记住所有东西。给记忆加时间衰减、加置信度、加用户删除入口,这些“反向设计”往往比怎么存更重要。很多产品翻车,翻在“太想记住用户了”。
第三,线上记录一定要可观测。我在项目里给每个记忆操作打了日志,写入了哪条、召回了哪条、为什么过滤了哪条,全部可查。排查上面这些问题时,没有日志基本等于盲猜。哪怕早期多花半天时间把日志埋好,后面能省出来的时间绝对不止半天。
我自己重新梳理完这套记忆体系之后再回头看那句话,“你的名字是?”,其实已经是整个系统最核心的一个测试用例了。名字的背后是身份锚定、用户隔离、语义抽取、召回策略、隐私边界这一整套东西。把这个简单问题答好,Agent 才算真正有了一点“连续存在”的样子。下次你再问一个智能体“你是谁”的时候,不妨想想,它到底是在读哪一层记忆里的答案。