先说我前两天踩的一个尴尬现场。用户在客服 Agent 里第三次提交收货地址,Agent 第三次反问:请提供您的收货地址。我当时真想顺着网线问它:上一轮我不是刚告诉过你吗?后来我把完整历史记录全部塞给模型,它倒是能答上来,可每轮都要把十几轮对话从头算一遍,响应肉眼可见地变慢,token 费用也肉眼可见地飙升。这个场景就是 AI Agent 记忆问题的典型现场:模型不笨,缺的是一个能让它“记得住你”的结构。
这篇文章是《走进AI Agent》系列的第三篇,专门聊记忆。我会先拆掉“上下文越长等于记忆越好”这个常见误区,然后给出三种从轻到重的落地方案,接着用 LangGraph 的思路带你把一套带记忆的 Agent 搭出来,最后聊聊我在生产环境里踩过的四个坑。想自己动手写 Agent 的开发者可以照着做,正在做 Agent 产品方案、被“记忆”两个字搞到头大的人,也能从中找到一个开始抓手。
1. 先拆一个误区:上下文窗口大,不等于记忆好
1.1 上下文窗口是“能放多少”,记忆是“该放什么”
现在各家大模型的上下文窗口越做越长,百万 token 级别也不稀奇。很多人第一反应是:那还要记忆模块干嘛,把聊天记录全塞进去不就完了?这个想法有一半是对的,长上下文确实让“临时不丢”变得容易了,但“全塞进去”不是记忆,而是把所有原料倒进锅里,让模型自己挑。
一百轮对话里可能只有三句话对当前问题有用,剩下九十七句全是噪音。模型在长上下文里找关键信息的本事确实在涨,但远没有你想的那么可靠,而且每次请求都全量发送,成本和时延都会跟着涨。我见过一个团队把两周的客服聊天全量塞进上下文,单次请求的 token 数从两千涨到一万二,账单涨了六倍,追问率反而没降多少。
记忆的本质是“取舍”:把什么放进上下文、用什么形式放、什么时候放、放多少。上下文窗口只是容器,记忆机制才是决定容器里装什么的那只手。没有了这只手,别说百万 token,就算给一个亿 token,模型也只是在一个更大的垃圾场里找针。
1.2 四种记忆类型:别再只盯着聊天记录
做记忆前先分类。早期做智能对话系统时,学术界习惯把记忆分成四类:工作记忆、情景记忆、语义记忆、程序记忆。放到 AI Agent 的工程语境里,可以这么理解:
工作记忆对应当前这轮任务的临时信息,比如用户上一个问题、刚才返回的中间结果,通常就放在 prompt 的上下文里,任务结束就清掉。情景记忆是对历史对话事件的记录,比如“用户上周三问过怎么注销账号”“他昨天对价格方案表达了不满”。语义记忆是从这些事件里提炼出来的稳定事实,比如“用户名是张三”“偏好使用微信支付”“家里养了一只猫叫煤球”。程序记忆则是 Agent 的行动经验,比如“遇到退款问题应该先查订单状态再走工单流程”,这类记忆很多时候不是存在对话记录里,而是固化在系统提示词、工具描述或者工作流配置里。
大多数人对“让 Agent 记住你”的理解还停留在情景记忆层面,也就是聊天记录。但如果只做聊天记录,Agent 顶多算是有“回放”能力,谈不上“懂你”。真正让用户体验到“被记住了”的,往往是语义记忆和用户画像:它得知道你是谁、你的偏好、你上次没办完的事。所以标题里说的“记住你”,我建议拆成两条线来做:一条线管对话历史,负责上下文连续性;另一条线管用户画像,负责个性化。
1.3 为什么全量塞历史,效果反而更差
很多人相信模型能力提升之后,长上下文会自然吸收掉记忆需求。我不否认长上下文会缓解一部分问题,但在当前阶段,全量塞历史有三个绕不开的硬伤。
第一是注意力稀释。关键信息埋在一堆无关历史里,模型容易记了后面忘了前面,尤其是那种长对话里只出现一次的关键细节。第二是信息冲突。用户昨天说“我喜欢喝美式”,今天改成“最近戒咖啡了”,两条都在上下文里,模型不知道应该信哪条。第三是成本和延迟。token 直接等于钱和响应速度,每轮多传几千 token,用起来就是肉眼可见的变慢。
所以结论很直接:长上下文要用来做记忆系统的兜底,而不是替代记忆系统。该做索引的做索引,该做摘要的做摘要,该做画像的做画像。接下来我们看具体怎么落地。
2. 三种主流记忆实现:从“有手就行”到“生产可用”
2.1 滑窗加摘要:最省事的记忆方案
最朴素的记忆方案其实是滑窗加摘要,它不需要向量数据库,不需要额外服务,很多开源项目里都能见到。思路很简单:只把最近 N 轮对话完整放进上下文,超过窗口的旧对话让大模型生成一段摘要,再和本次对话拼在一起。比如窗口设为 10 轮,prompt 结构就是:“以下是之前对话的摘要:[摘要];以下是最近对话:……”。
这个方案的好处是结构简单、成本低、容易调试。坏处也很明显,摘要是一次性的,随着时间拉长,早期细节会被压缩得面目全非。用户三个星期前提过一句“我对花生过敏”,如果当时没被写进摘要,后面就彻底丢了。所以滑窗加摘要更适合临时性场景,比如一次性任务、客服单轮问答,不太适合需要长期陪伴或深度个性化的产品。
2.2 向量检索:按需召回相关记忆
真正让“记住你”有质变的,是把历史记忆向量化,然后按相关性召回。做法不复杂:把一段有价值的对话切片,比如按用户意图切一段,或者按一轮问答切一块,用 embedding 模型转成向量,存进向量数据库。每当 Agent 要回答新问题时,先把问题转成向量,去库里做相似度搜索,找出最相关的若干条记忆,塞进 prompt。用到的数据库可以是 Qdrant、Milvus、Weaviate,也可以是 PostgreSQL 的 pgvector,小项目用 Chroma 或 FAISS 完全够。
这套方案解决了全量塞历史的注意力稀释问题,因为它每次只带几条最相关的记忆。但这不代表没有新问题:召回质量取决于切片质量、embedding 模型和相似度算法,而且“相关”和“重要”常常不是一回事。用户最关心的一条吐槽,可能在向量空间里和“冰箱制冷效果”更接近,而不是和“售后服务”更接近。所以向量检索只是基础,上面通常还要再接一个排序甚至重排的环节,具体我在第四章细说。
2.3 记忆服务与 MCP Memory:开箱即用
如果不想自己造轮子,市场上已经有一批现成的记忆方案。Mem0 把记忆分成短期和长期两层,能自动从对话中抽取需要长期保留的条目,也支持用户直接写入。Zep 是类似思路,内置了对话摘要、实体提取、时间线记忆,并且提供了专门 API,前后端都能接。另一个值得关注的方向是 MCP 协议里的 Memory 服务。MCP 把 Agent 的能力统一抽象成工具,记忆也可以做成一个独立的 memory server,通过标准协议暴露“保存记忆”“读取记忆”“搜索记忆”这些接口。
用现成服务的好处是省掉很多脏活累活,尤其是实体识别、记忆冲突处理和遗忘策略,自己实现一遍非常耗时。但坏处是数据链路会绕一圈,调试和定制受限,而且记忆数据往往和业务强相关,交给第三方之前要仔细想清楚数据合规和隐私边界,这个我在第五章展开。如果你用的是 Spring AI 这类 Java 框架,里面也有 ChatMemory、SearchMemory 之类的抽象,用法本质相同,一个是存历史,一个是按需查,别被框架之间的名词绕晕。
2.4 选型建议:到底选哪一级
我的判断标准很简单:先想清楚产品要的是“上下文连续”还是“长期个性化”。只要上下文连续,滑窗加摘要就够了;需要跨会话记住用户偏好,向量检索是主流选择;团队人力紧张且不介意数据链路多一跳,可以先上 Mem0 或 Zep 这类服务跑通业务,等量大了再自研。
没有最好的方案,只有最适合当前阶段的方案。做 Agent 最容易犯的错,就是第一个版本就上最重的架构,结果没调明白之前先把团队精力耗光了。另外,如果你的 Agent 是 multi-agent 架构,记忆的归属也要提前想清楚。我目前的经验是:共享记忆库放用户事实和任务状态,各子 Agent 的工作记忆本地保留,别一股脑全塞公共库,否则消息风暴会先把记忆库冲垮。
3. 实操:给 Agent 装一套“记得住你”的记忆系统
3.1 技术栈与整体数据流
这一节以 LangGraph 为例,讲一个可直接参考的实现骨架。选 LangGraph 不是因为它最流行,而是它有清晰的节点和状态模型,非常适合把记忆写入和记忆召回拆成独立节点。向量库我用 pgvector,因为存量业务多半已经有 PostgreSQL,可以少引入一个组件。embedding 模型用通用的文本嵌入模型即可,中文场景稍微关注一下分词和模型对中文的适配。
整体数据流是这样的:用户消息进入 Agent,先走记忆召回节点,用当前问题去查向量库,把命中的历史事实和用户画像塞进 prompt;大模型生成回复之后,走记忆写入节点,把这条有信息量的对话切片转成向量,并同步更新用户画像;最后把整轮对话追加到短期上下文。注意顺序很关键:先召回再生成,生成完再写入。如果把写入放在召回前,这一轮还没回答就先把问题记下来了,容易把无效信息也存进去。
存储结构不用设计得太复杂,核心字段可以参考下面这个 JSON 结构:
{ "user_id": "u_12345", "content": "用户提到家里养了一只猫,名字叫煤球", "embedding": [], "memory_type": "semantic", "confidence": 0.8, "created_at": "2025-06-01T10:00:00Z", "last_accessed_at": "2025-06-01T10:00:00Z" }3.2 记忆写入:什么值得存,存成什么样子
写入是最容易走偏的一步。很多人把每一条对话都存下来,结果向量库里全是“嗯”“好的”“谢谢”这类无意义内容,召回时又把它们带回来。我建议用一句话来判断:这条对话里是否包含以后可能用得上的事实、偏好或待办?如果只是寒暄或者流程性确认,不存。
存放的字段最好不要只有原文,尽量做一层信息提炼。比如原文是“我对价格已经无语了,你们自己看吧”,可以提炼为“用户对当前价格方案不满”,这样召回时语义更清楚,也方便后续做情绪分析。提炼可以用大模型做,但别每轮都调大模型去抽,成本会很高。我会给写入节点加两个触发条件:一是用户消息里包含明显的偏好词或事实词,比如“我喜欢”“我住在”“我是做……”;二是完成了一次带有结果的交互,比如提交了订单、改过收货地址。满足之一才进入提炼和写入。
3.3 记忆召回:什么时候查、查几条、怎么用
召回要做减法,而不是把库里所有相关记忆都拉出来。我的经验是召回数量控制在 3 到 8 条,具体看场景。客服问答 3 到 5 条就够,个性化推荐或陪伴式聊天可以放到 8 到 10 条。召回方式分两级:第一级用向量相似度粗筛,第二级按时间加权,或者把用户画像中明确的键值对直接带上。
这里有一个很容易踩的坑:永远不要让记忆原样堆叠在 prompt 里。最好给每条记忆加上时间和来源描述,比如“(2025年4月12日)用户提到他住杭州西湖区”。模型看到时间信息,就不会把一条去年的旧习惯当成今天的指令。还有一种召回是主动的,用户问“我的订单怎么样了”时,Agent 不一定真要从历史对话里猜,而是应该直接去查业务数据库。记忆系统管的是对话侧的信息,业务状态应该走正经的 API 和数据库查询。把这个边界划清楚,能避免很多“它明明知道但又答不上来”的奇怪事故。
3.4 用户画像:让临时记忆沉淀为长期认知
要让 Agent“记住你”,只靠对话历史还不够。我会在记忆系统里单独维护一张用户画像表,字段不固定,用键值对形式存储,比如“姓名:张三”“偏好支付:微信”“住址:杭州西湖区”“宠物:猫(煤球)”。它的更新方式和对话记忆不同:对话记忆是增量添加,用户画像是覆盖更新。每次写入新事实时,对比已有键值,如果冲突,以最新一次为准。
这一步非常关键,否则会出现一个场景:用户这周说“我现在不用微信支付了”,画像里却还留着上个月的“偏好支付:微信”。画像抽取也不需要把整本对话史都翻出来,每次对话结束后跑一次轻量级的画像更新 prompt,把当前这轮的更新点提取出来,再和旧画像合并。对于中文用户,我还有一个实用经验:姓名、地址、电话号码这类实体,用专门的信息抽取模型要比用通用大模型更稳。大模型适合做语义层面的偏好归因,不适合做严格的结构化抽取。
4. 从“记得住”到“记得准、记得对”:调优与修正
4.1 召回结果的重排与打分
向量检索跑出来的结果只能算候选集,直接塞给大模型往往差强人意。我之前做过一次对比:同一组问题,直接召回 Top5 写入,和加了一道重排之后的 Top5 写入,用户对“是否感觉被理解”的评分差了快 20%。
重排的常用做法有三种:一是加入时间衰减因子,太久远的记忆降权,近期记忆适当升权;二是按记忆类型加权,用户画像类记忆权重高于一次性的情景记录;三是用一个小排序模型或规则,对召回结果做融合。如果项目里没有专门的重排模型,最简单的做法是写几行规则:先定一个基础分,向量相似度给一个分数,时间衰减给一个分数,是否包含当前问题里的关键词再加分,最后按总分排序。这个规则先用脚本跑一段离线样本,手动看排序效果再逐步调参,比一上来就上模型务实得多。
4.2 记忆的更新、遗忘与冲突处理
记忆不是写完就完事了。时间久了会碰到三种情况:用户改主意了、用户对同一件事前后说法不一致、记忆条目已经过时。我的更新策略很简单:语义记忆和用户画像采用覆盖更新,以最近一次为准;情景记忆采用追加加时间戳,不覆盖旧记录,只标注新旧关系。
举个例子,用户先说“我在北京”,后来改到“我搬去上海了”,画像里直接覆盖成“城市:上海”,情景记忆里保留两条带时间的记录。这样既不会让旧信息污染当前判断,也保留了回溯能力。遗忘策略同样重要,大部分记忆如果不是用户主动要求,其实都不需要永久保存。可以设置一个保留周期,比如一年没访问过的情景记忆自动归档。这个过程不一定要删除,做成冷热分层也行:热数据进向量库,冷数据进对象存储。真要删除的场景也有,比如用户要求“把你记住的关于我的信息都删掉”,这属于合规操作,必须支持。所以从设计第一天起,每条记忆都要带上 user_id 和创建时间,别做成一个全局的大列表,否则后面连谁说了什么都查不出来。
4.3 允许用户纠正:Agent 要能“忘掉说错的话”
生产环境里最打击用户信任感的瞬间,不是 Agent 答不上来,而是它“记得”了一件错误的事,还反复根据错记忆做决策。比如用户明明从来不吃香菜,某次聊天时随口开玩笑说“我顿顿都吃香菜”,若被当成事实写进了画像,后续每次推荐都给他推香菜相关的东西,那就太灾难了。
所以记忆系统一定要留一个纠正入口。最简单的做法是,在识别到用户明确的否定表达时,比如“我不是……”“我说错了”“把这个忘了吧”,触发一次记忆修正流程,删除或覆盖对应条目。更稳妥的做法是,所有写入用户画像的事实都要带置信度,随口一提的置信度低,重复出现过或用“我确定”这类表达的置信度高。低置信度条目在召回时可以做弱化处理,避免一句玩笑话被当成铁律。
5. 实战中绕不开的四个坑
5.1 记忆污染:历史里的错话成了“事实”
记忆污染的典型路径是这样的:某条用户消息被错误抽取,存进画像,后续每次回答都被这个假事实影响,而且因为 Agent 自己记得了,它会越来越自信地使用这个错误信息,形成自洽的幻觉闭环。
要避免这个坑,只能从源头把关:写入时宁可少存,不要错存;对置信度低的条目做标记;召回出来后,在 prompt 里注明“以下记忆可能不准确,请结合当前对话判断”。最后这条很有效,相当于给模型一个允许质疑记忆的开关。别小看这句话,它能让模型在旧记忆和新信息冲突时,倾向于相信新信息,而不是被旧记忆带偏。
5.2 隐私与授权:记住你,也要守住边界
让 Agent 记住你,其实涉及大量个人信息。做产品时这条线一旦越界,不只是口碑问题,是合规问题。我的建议是:先拿到用户授权,再开始存长期记忆;在界面上让用户能看到“Agent 记住了哪些关于我的信息”,并提供一键清除入口。
技术上要做到按 user_id 隔离,千万不能出现多用户数据互相串的情况。多租户隔离和权限校验在记忆系统里比在业务系统里更容易被忽略,因为记忆数据混合了对话内容和结构化事实,抽数、上模型、做聚类的时候都容易误触敏感数据。哪怕只是一个内部 demo,也建议把隔离和授权从第一天就做好,不然后面补起来非常痛苦。
5.3 成本与延迟:每个记忆模块都不是免费的
给 Agent 加记忆,等于加了好几个额外环节:embedding 调用、向量检索、大模型摘要提炼、画像更新。这些环节都会增加成本和延迟。我见过一个项目,加了记忆模块后,一次对话的平均响应时间从 1.2 秒涨到 3 秒,成本涨了 40%。
优化思路有几个:embedding 结果做缓存,同一个问题短时间内不重复嵌入;记忆召回和画像更新改异步执行,让用户在回复完成后不感知额外等待;大模型提炼只对增量文本做,不每次都全量跑。小流量的 demo 可以不在乎这些,但只要想上线,就要把“记忆是有成本”这个概念刻在脑子里。每次新增一个记忆环节之前,先问一句:这一步不加行不行?很多所谓高级记忆,最后都只是给延迟和账单添堵。
5.4 测试与观测:没有记忆可视化,就等于盲调
最后一个坑也是最隐蔽的:记忆系统很难直接观察到。出了问题,你根本不知道是召回没召回来,还是召回了但模型没用,或者是写入时就写错了。所以我会在开发早期就把观测做进去,最简单的做法是每次请求的日志里记录三样东西:这轮召回了哪些记忆、每条记忆的得分、最终 prompt 里记忆部分的实际内容。
在调试界面里,最好能按 user_id 直接查看它的画像和最近写入的记忆。有了这些,你才能从“用户说体验不对劲”这种模糊反馈里,快速定位到具体是哪个环节出的问题。记忆系统本质上是一个数据系统,凡是数据系统,可观测性就是第一生产力,这个钱省不得。
最后再多说一句:记忆系统这东西,务实地讲,90% 的场景用滑窗加摘要再加一个简单用户画像就够了,真正需要上向量检索和记忆服务的,是那些用户会和 Agent 长期反复交互的产品。先把基础链路跑通,再考虑怎么让它更懂你。脑子里的坑越少,Agent 反而越像个正常人。