你有没有遇到过这种情况:跟一个 AI Agent 聊得正起劲,它前一秒还言之凿凿地分析你项目里的问题,后一秒换个话题,就把你十分钟前交代的背景信息忘得干干净净。你只好耐着性子把“我叫什么”“我在做什么项目”“我偏好什么风格”从头再说一遍,甚至说完了它还能再犯同样的错。
这不是模型智商的问题,而是 Agent 的“记忆”没做对。今天这篇《让 Agent 记住你》,核心关键词就两个:AI Agent和Agent Memory。我会从记忆分层开始,到手把手教你用 Redis 和向量数据库存记忆,再把多智能体框架的记忆设计捋一遍,最后附上我自己踩过的一堆坑。这篇偏向实操,适合已经在做 Agent 开发、或者准备从 Demo 往正式系统迁移的朋友,没有基础的读者也能跟着逐步复现。
1. 为什么总说“Agent 需要记忆”?先看一个真实对话
1.1 无状态 Agent 的“社交尴尬”
我拿一个实际发生过的场景来说。你让 Agent 当你的“项目助理”,第一轮你说:
“我叫王强,数据部门负责人,正在推进一个用户增长项目,核心指标是次月留存率,目标从 12% 提到 18%。”
Agent 回复得非常好,给了几个增长实验建议。第二轮你问:“那我把预算从 20 万加到 50 万,你帮我重新算一下优先级。”
Agent 也能答——它当前这一轮的上下文里还带着你的话。但第三轮,你突然切换话题问:“我上次说的项目,你觉得第一个实验应该怎么设计?”
如果这个 Agent 没有记忆机制,它只能回复一句“抱歉,我没有找到关于您项目的更多信息”。你第一轮说的名字、部门、指标、预算,全部丢失了。这就是典型的无状态 Agent 现象。
你可能会说,LLM 不是有上下文窗口吗?几十万 Token 还不够记吗?这就要说到本质了。
1.2 大模型的“天生失忆”
大模型本质上是一个无状态函数。每一次调用,模型都不记得上一次调用发生过什么。它给你的回答只依赖这一次传入的 messages 参数——系统提示词、历史对话、用户问题,仅此而已。
所谓“上下文窗口”,更像是办公桌上的一张白纸。窗口大,白纸就大,你可以在上面写更多信息,但这张白纸仍然是一次性的:对话结束、进程重启、会话切换,白纸就被扔掉了。
所以你会看到三个在实际项目中非常头痛的问题:
- Token 成本失控:每次把完整历史全部塞进 messages,调用越长价格越贵,甚至超过预算。
- 响应延迟上升:上下文越长,模型处理时间越长,用户等得越不耐烦。
- 关键信息稀释:一万条琐碎对话里混着一条关键需求,模型可能根本抓不住重点。
我见过不少团队一开始图省事,把所有对话记录都拼接进提示词,结果上线两周,账单爆炸,用户还是抱怨“它记不住我说过的话”。原因就在于他们混淆了“临时上下文”和“真正记忆”的区别。
所以接下来,我们要把记忆这件事做成分层设计。
2. 给 Agent 的记忆分个层:Context、工作记忆与长期记忆
2.1 记忆四层模型
早期讨论 Agent 架构的文章里,大家普遍把记忆拆成 4 层,这个分法到今天依然很实用:
| 记忆层次 | 生命周期 | 典型实现 | 类比 |
|---|---|---|---|
| 上下文记忆(Context) | 单次请求内 | messages 数组、系统提示词 | 临时便利贴 |
| 工作记忆(Working Memory) | 单次会话内 | Session 缓存、Redis | 办公桌上的草稿纸 |
| 长期记忆(Long-term Memory) | 跨会话、跨天 | 向量数据库、SQLite | 笔记本 |
| 外置知识库(External Memory) | 长期且结构化 | RAG 文档库、企业 Wiki | 图书馆书架 |
这四个层次之间的关系,像极了人脑的记忆机制:你处理眼前任务时用的是工作记忆,但那些重要的、需要长期记住的事情,会经过编码固化成长期记忆。Agent 也一样。
2.2 每一层到底存什么
先说上下文记忆。这一层是模型自带的能力,你把对话历史塞进 messages 就行,适合存当前这轮对话中必须用到的“即时状态”。它的问题是脆弱:一旦请求结束,什么都没了。
再说是工作记忆。它的作用是跨越用户同一次会话里的多次请求。用户在界面上跟你连续聊天,每句话都是一次 API 调用,但会话 ID 可以帮你把这些调用串起来。工作记忆一般放在 Redis 里,key 是 session_id,value 是对话消息列表或状态对象。它的特点是读写快,但容量有限,而且宕机就丢。
然后是长期记忆。这里才是“让 Agent 记住你”的关键。用户第二次进系统、明天再回来、下个月再回来,Agent 还能记得他的名字、偏好、历史结论和项目背景。长期记忆的核心诉求是跨会话持久化,而且要在海量信息里快速找到最相关的一部分,所以通常使用向量数据库。它保存的不是原始对话流水账,而是从对话中提炼出来的“事实条目”,比如“王强,数据部门负责人,关注次月留存率”。
最后是外置知识库。很多团队把它和长期记忆混在一起,其实有区别。外置知识库通常是文档类的确定性知识,比如企业制度、产品手册、行业报告;而长期记忆是用户的个性化信息。两者都可以用向量检索,但来源、更新机制、权限控制都不一样,架构上应该拆开。
2.3 各层怎么协作
我设计记忆系统时,会给它定一条清晰的“数据流向”:用户每次发起请求,Agent 先从长期记忆里把与该用户相关的记忆片段捞出来,再结合工作记忆中的会话上下文,组装成当前请求的 messages,交给大模型。对话结束后,由一条异步管线决定哪些新信息值得写入长期记忆。
打个比方:长期记忆是“你记得的关于这个用户的一切”,工作记忆是“这次聊天聊到哪了”,上下文记忆则是“模型眼前能看到的那几段话”。三层各管一段,缺一不可。这也是很多开源框架底层强化 Agent Memory 能力的通用逻辑。
3. 落地第一步:把 Context 窗口管理好
3.1 滑动窗口:代价最小的方案
如果你的系统还处在原型阶段,技术栈不复杂,最直接的记忆方案是滑动窗口——只保留最近 N 轮对话。这个方案看起来简单,但做的过程中有两个细节非常容易被忽略:按 Token 截断,而不是按“轮数”截断;保留系统提示词和最近用户意图,而不只是无脑保留最后几轮。
按轮数截断有个典型问题:某轮用户一次性贴了一大段日志,看起来是“一轮”,实际上吃掉了大量 Token。我建议用 tiktoken(OpenAI 开源的 Token 估算库)来做精确计算。示例代码如下:
import tiktoken def count_tokens(messages, model="gpt-4o"): enc = tiktoken.encoding_for_model(model) # 不同模型的计数规则略有差异,这里是通用估算 return sum(len(enc.encode(msg.get("content", ""))) for msg in messages) def trim_messages(messages, max_tokens=8000): """将消息列表裁剪到 max_tokens 以内,但永远保留系统提示词。""" system_parts = [m for m in messages if m["role"] == "system"] history_parts = [m for m in messages if m["role"] != "system"] while count_tokens(history_parts) > max_tokens: # 从头丢弃最早的对话,保留最近的 history_parts.pop(0) return system_parts + history_parts这段代码逻辑很直白:从最老的对话开始丢,丢到 Token 数低于阈值为止。这样做虽然粗暴,但在原型期特别稳,不会出现“对话太长导致请求直接报错”的问题。
3.2 轮转策略:摘要压缩代替简单丢
滑动窗口最大的弊端是信息无差别丢失。用户聊天时随口说的一句话,可能正是项目最关键的需求,但窗口一滑就被清掉了。所以稍微进阶一点的做法是摘要压缩。
具体做法是:当历史消息超过阈值时,不直接丢,而是调用一次大模型,把早期对话压缩成一段摘要。例如生成“用户目前项目背景、核心指标、已确认的决策”等结构化总结。压缩后的摘要放在消息列表最前面,代替那批原始对话。
这个方案成本比纯滑动窗口高一点,但效果提升非常明显。我在实际项目中,通常设置两层:如果总 Token 超过阈值的 50%,触发摘要压缩;如果压缩后仍超限,再启用滑动窗口丢弃最老对话。两者搭配,既有记忆的连续性,又控制了请求体大小。
注意:摘要压缩最好用单独的一次模型调用生成,不要和主对话混在一起。否则摘要生成失败会直接影响用户请求的响应。
3.3 落库:别让对话“流走”
无论你用滑动窗口还是摘要压缩,都建议把用户的原始对话在进入消息列表之前先落库。我常用的方案是存两份:一份原始 JSON 存 MySQL 或 MongoDB,用于审计和回溯;一份精简后的消息列表存 Redis,用于快速读取。
Redis 侧的存储结构我习惯这样设计:
# 会话历史,右侧追加新消息 RPUSH session:{session_id}:messages {message_json} # 记录最后访问时间,同时设置过期时间,避免无限堆积 EXPIRE session:{session_id}:messages 86400这个设计的优点是读取顺序天然对应对话顺序,配合 EXPIRE 可以自动清理一天前的临时会话数据,不会把 Redis 撑爆。如果你的对话交互很频繁,我会建议再加一个异步任务,每分钟把 Redis 里的增量消息同步到 MongoDB,防止 Redis 重启导致数据丢失。
4. 长期记忆怎么存:Redis 与向量数据库的选型与接入
4.1 Redis 适合存“工作记忆”,别让它硬扛语义检索
很多朋友一说 Agent 记忆就上 Redis,这没错,但得先分清 Redis 在记忆系统里扮演什么角色。Redis 本质是 KV 内存数据库,读写极快,适合存“结构化、精准查询”的信息,比如会话状态、用户 ID、最近对话轮次、任务进度。
我见过有人把几万条用户语义记忆直接塞进 Redis,然后靠模糊匹配去查,效果奇差。因为 Redis 的字符串匹配对语义无能为力,“留存率”和“用户回访比例”在 Redis 里就是两个完全不同的 key,根本匹配不上。跨句子找相关性,这件事必须交给向量检索。
所以 Redis 的定位应该是工作记忆层,典型使用方式:
import redis r = redis.Redis(host="localhost", port=6379, decode_responses=True) def save_working_memory(session_id, user_id, state): key = f"agent:session:{session_id}" r.hset(key, mapping={ "user_id": user_id, "state": state, # 比如 current_task, last_question "updated_at": time.time(), }) r.expire(key, 3600) # 工作记忆只保留 1 小时这满足工作记忆“读写快、临时性、高频访问”的全部要求。你想存更久、更结构化的数据,比如用户实名、岗位、项目偏好,放在这里也确实不合适。
4.2 向量数据库:长期记忆的“笔记本”
真正承担长期记忆的是向量数据库。它的核心思路是:把每个记忆条目用大模型转换成向量(一串浮点数),存进库中。等新问题来的时候,也把问题转成向量,然后通过余弦相似度或内积找到历史记忆里最相关的条目。
目前常用的向量库有 Chroma(轻量)、Qdrant(功能全)、Milvus(分布式)、Pinecone(云服务)。我自己的经验是,中小型项目和原型阶段优先用 Chroma,因为零部署、API 友好;等数据量到了几百万条以上,再迁移到 Milvus 或 Qdrant。
下面是一个用 Chroma 建设长期记忆的示例:
import chromadb from openai import OpenAI client = OpenAI() chroma_client = chromadb.PersistentClient(path="./agent_memory") collection = chroma_client.get_or_create_collection("memory_store") def add_long_term_memory(user_id, content, metadata=None): # 向量化记忆内容 embedded = client.embeddings.create( model="text-embedding-3-small", input=content ).data[0].embedding collection.add( ids=[f"{user_id}:{int(time.time()*1000)}"], embeddings=[embedded], documents=[content], metadatas=[{"user_id": user_id, **metadata}], ) def recall_memory(user_id, query, top_k=5): embedded_query = client.embeddings.create( model="text-embedding-3-small", input=query ).data[0].embedding results = collection.query( query_embeddings=[embedded_query], where={"user_id": user_id}, n_results=top_k ) return results["documents"]落库的内容可以是一条完整对话,也可以是提炼后的结构化记忆短语。但及时提炼很重要。如果你存的是“用户第 3 轮对话原文”,检索时大概率召回一段废话;如果你存的是“用户负责人:王强,正在推进留存率优化项目,目标 18%”,那召回出来的就是有效信息。至于怎么提炼,第六章展开讲。
4.3 长期记忆的写入门槛:宁缺毋滥
正因为长期记忆的检索质量直接决定 Agent 的“聪明程度”,写入内容必须设置门槛。我常用的筛选规则是:
- 事实性信息:用户的姓名、身份、公司、项目目标、偏好,优先写入。
- 稳定的偏好:用户明确说的“我喜欢简洁回答”“请用中文回复”,写入。
- 临时性闲聊:不写入,比如“今天天气不错”“哈哈好的”这类。
- 过期即失效的信息:可以写,但要带上时间戳,由后续逻辑判断时效性。
如果什么都往长期记忆里塞,召回时会发现大量低质量噪声,反而干扰大模型判断,这个坑相当隐蔽。很多人发现“为什么 Agent 越用越傻”,大概率是记忆污染的问题。
5. 多智能体框架里的记忆设计:看看别人怎么做的
5.1 ChatDev:用“继承”实现信息传递
如果你研究过 ChatDev 这种多智能体协作框架,会发现它处理记忆的思路特别有意思。ChatDev 模拟一个软件公司,有 CEO、CTO、程序员、测试员等多个角色智能体。他们之间不是简单的消息互发,而是通过“聊天链”传递信息:上游智能体的输出直接作为下游智能体的输入,并且每个智能体都能访问前面所有智能体留下的消息记录。
这种设计本质上就是链式记忆。它的优势是上下文非常连贯,信息不容易丢失;劣势是随着智能体数量增加,重复传入的历史会越来越长,Token 消耗非常夸张。后来社区讨论这个框架时,普遍建议每传一轮就做一次摘要压缩,而不是把原始消息一路传递到底。
5.2 MetaGPT:把记忆写成“文档”
MetaGPT 的做法更贴近实际工程:它让智能体之间不只靠聊天,而是通过结构化文档来协作。比如产品经理智能体写 PRD,架构师智能体读 PRD 输出设计文档,工程师再基于设计文档写代码。每个文档就是被固化的记忆。
这个思路的系统启发是:长期记忆不应该只是对话流水账,应该优先沉淀为结构化成果。对应到你自己的 Agent 里就是:与其在聊天里重复说“上次我们确认了技术栈选型用 Python”,不如在对话结束后自动生成一段项目纪要把这个决定记录下来。这样后续任何智能体、任何会话都能直接读文档,而不用翻聊天记录。这也是所有知识库类 Agent 设计的底层共识。
5.3 AutoGen:共享记忆池与多会话写入
AutoGen 是我个人用得比较多的框架,它的记忆设计相对系统化。AutoGen 支持在多个会话之间共享记忆池,也就是说,多个不同的 Agent 会话可以同时读写同一个记忆空间。你可以把记忆池细分成:
- 用户会话记忆:围绕特定用户的历史交互
- 任务状态记忆:当前任务的进度、待办事项
- 全局配置记忆:系统级约束、团队偏好
这样设计有个很实际的好处:你不用为每个 Agent 单独复制一套记忆,而是共享一个存储,通过命名空间隔离。会话 A 里用户说“我把预算加到 50 万”,会话 B 里另一个 Agent 在回答预算方案时也能直接读到这条信息。
5.4 综合来看:给自己的 Agent 设计一套分级记忆
看完了这些框架,你会发现它们都在做同一个核心动作:把“记忆”从单一的上下文窗口,升级成一套有命名空间、有层级、有持久化策略的存储系统。我最终落到自己项目里的设计是三层组合:
- 短期层:Redis 存当前会话消息,过期时间 1 小时到 24 小时不等。
- 中期层:MySQL 存用户画像、项目信息、偏好设置的字段化记忆。
- 长期层:向量数据库存对话摘要、决策记录、语义化记忆。
这三层通过一个统一的 MemoryManager 类对外暴露读写接口。上层的 Agent 逻辑不需要关心记忆存在哪,只需要调用memory.save(...)和memory.recall(...),底层自动路由到对应存储。这样后续要替换存储方案,也只需要改内部实现。
6. 从对话里自动提炼记忆:信息提取与检索增强
6.1 让 Agent 学会“做笔记”
长期记忆不能靠人工一条条录入,必须让 Agent 从对话里自动提炼。这是让 Agent “记住你”最关键也最容易被低估的一步。
我的做法是加一条异步的后处理管线:当一轮对话结束后,把这一轮的用户输入和 Agent 回复一起丢给一个提炼模型,让模型输出候选记忆条目。提示词大致长这样:
请从下面的对话中提取值得长期记住的事实信息,按 JSON 数组输出。 每条包含字段: - content: 记忆内容,一句话描述 - category: 可选值为 user_profile / project_info / preference / decision / other - importance: 重要程度 1-5,5 为最关键 对话内容: {user}: {query} {assistant}: {response}我实际用下来,让模型自己判断重要程度,再结合一个阈值过滤,效果比纯规则好用得多。以下是实际代码示意:
def extract_memories(query, response): prompt = f"""请从对话中提取值得长期记住的事实信息,按JSON数组输出。 每条包含字段 content、category、importance(1-5)。 对话: user: {query} assistant: {response} """ result = llm.chat(prompt, response_format="json") parsed = json.loads(result) # 重要程度>=4的才写入长期记忆 return [item for item in parsed if item["importance"] >= 4]当然,模型提取会有误判。比如用户开玩笑说“我是个天才”,模型可能把它当成用户画像写入。我的对策是写入前做一轮规则清洗:排除“猜测性表述”、排除“第一人称情绪词”、排除长度过短内容。清洗后的条目进入向量库,准确性会大幅提升。
6.2 检索增强:给召回结果排个序
长期记忆存好了,召回也做了,但我们会发现一个问题:向量检索出来的 top_k 结果不一定是最有用的。比如用户现在问的是“帮我看看上次的实验报告”,召回的 5 条记忆里可能有一条是半年前的老结论,这条显然不该排在前面。
所以你要在召回之后再加一道重排序(Rerank)。常见的做法有几种:
- 时间衰减:对较新的记忆增大权重,公式可以设为 score = similarity * alpha ^ (days_ago)。
- 规则加权:用户明确提到的实体(比如“留存率”“实验报告”)如果出现在记忆里,加分。
- 模型重排:把召回结果连同用户问题一起交给大模型,让模型挑出最相关的 2-3 条。
这三者我建议至少用第一种加第三种。原因很简单:向量相似度衡量的只是语义接近性,但不理解“当前任务的时间边界”。而大模型重排虽然聪明,但耗时较长;加一个时间衰减前置过滤能大幅缩小候选集,再让模型从 10 条里挑 3 条,成本和效果最平衡。
6.3 记忆检索和 RAG 的边界
很多朋友把“让 Agent 记住你”和 RAG(检索增强生成)划等号,其实两者侧重点完全不同。RAG 解决的是“Agent 不知道外部知识”,检索的是文档库、知识库、官网等静态资料;Agent Memory 解决的是“Agent 忘记了你这个人”,检索的是历史交互、用户画像、项目决策。
我在工程上会给二者分配不同的存储空间和检索链路。但它们的检索链路可以复用同一套向量库。比如我给记忆条目统一增加一个source字段,是 user_memory 还是 knowledge_base。查询的时候按 source 分流,互不污染。这个细节看起来简单,实际非常必要,我见过有人把用户个人记忆混进企业知识库,结果一个用户问问题,回答里蹦出了另一个用户的隐私信息,非常危险。
7. 记忆系统的排查清单与避坑指南
7.1 我踩过的三类典型问题
这套系统上线后,最常见的故障不是模型问题,而是记忆管道的问题。我按出现频率整理了下面这张表:
| 现象 | 根本原因 | 排查方向 |
|---|---|---|
| Agent 回答“不记得” | 长期记忆未命中 | 检查向量库是否写入成功;检查召回 top_k 是否太小;检查查询是否被 where 条件过滤掉 |
| Agent 答非所问,内容混乱 | 召回了过期或无关记忆 | 增加时间衰减权重;重排结果;调低 importance 阈值 |
| Redis 内存暴涨 | 会话消息无过期策略 | 统一设置 EXPIRE;定期清理空闲会话;淘汰老会话数据 |
| 对话延迟突然变高 | 长期记忆召回后再 Rerank 耗时 | 前置规则过滤缩小候选集;Rerank 模型改用小模型;可缓存高频用户记忆 |
| 用户 A 看到用户 B 的信息 | 记忆未按 user_id 隔离 | 检查向量库 where 条件;检查 Redis key 命名空间;做权限读校验 |
排查时一定要记住一条主线:先看“有没有写入”,再看“能不能查到”,最后才看“查到的对不对”。很多问题卡在第一步,模型提取出的记忆条目因为重要性评分太低被过滤了,或者写入时 embedding 失败,你却在检索环节折腾半天。
7.2 避坑心得:记忆不是越多越好
我做过几个项目后最大的体会是:记忆系统的目标不是记住一切,而是忘掉大部分,留下最关键的一小部分。人的记忆也有遗忘机制,Agent 也应该有。设置一个每周清理任务,把超过 90 天、importance 低于 3、且从未被召回的冷记忆归档或删除,既能降低存储成本,也能明显提升召回精度。
另外,长期记忆里的每条信息都要尽量带上时间戳和来源。只存“用户想要 18% 的留存率”是不够的,你还得知道这句话是哪天说的、是在哪个上下文里说的。这样当用户后来改口说“目标调整到 20%”,系统才有办法识别冲突并更新旧条目,而不是让两条矛盾信息同时留在库里互相打架。
7.3 一个小技巧:把时间信息展示给模型
最后分享一个我这几个月最常用的技巧。在组装 messages 时,我不仅会把召回的记忆内容放进去,还会把“记忆形成时间”一起展示给模型,例如:
记忆条目(2025-03-12):用户目标设定为次月留存率提升到 18%。 记忆条目(2025-04-20):用户提到预算增加到 50 万。这么做的原因是,大模型对“信息新旧”的判断能力远比你想象中强。只要把时间戳放在它面前,它自己就能合理权衡新旧信息的权重,不会机械地认为“所有召回记忆都是当下有效的”。这个技巧几乎零成本,但能在很多边界案例上救你一命。
个人实际操作里,我还会在主对话外独立跑一个小型日志任务,把每次“召回记忆的 ID + 是否被采纳”记录下来。跑一段时间你就能统计出哪些记忆条目是高频有用的,哪些是常年被召回但其实没帮助的。用这个数据反向调整提炼阈值和重排参数,比拍脑袋优化靠谱得多。这套体系做完之后,Agent 才真正像一个“认识你、记得你、知道你走到哪一步”的搭档,而不是一个每次见面都需要重新自我介绍的陌生人。