☰
AI Agent记忆机制实战:三层记忆架构设计与踩坑修复
2026/10/21 9:15:39 网站建设 项目流程

AI Agent学习日记写到第四天,进度不算快,但今天确实解决了一个让我卡了两天的问题。前三天我一直在折腾“让Agent能跑起来”:从概念、框架选型到搭出一个能调用工具的多轮对话Demo,过程比想象中顺利。但真开始做“看起来像Agent”的任务时,打击来得很快:Agent问用户“你刚刚不是说要用A方案吗,怎么又改成B了?”——它根本不记得自己说过的话。更尴尬的是,任务跑到一半被用户打断,恢复正常对话后,Agent把之前的中间结果全丢了,从头开始算。

所以第四天我没有继续追新的Agent框架特性,而是老老实实把“记忆”这块补上。今天从会话记忆、工作记忆、长期记忆三个层面入手,把之前写的Demo从“无状态对话机器人”升级成真正带记忆的Agent循环,中途还踩了一个很有意思的坑。这篇日记把完整过程记录下来,包括代码、参数配置、排错链路和实测对比,给同样卡在“Agent记不住事”这一关的朋友做个参考。

1. 为什么第四天非要把“记忆”啃下来

很多教程喜欢把Agent拆成“感知、决策、行动、记忆”四大模块,但真正动手写代码的人会发现,记忆是其他三个模块的基础。没有记忆,Agent的“决策”就是每次从零开始,遇到需要连续推理的任务会反复出错。

1.1 前三天的进度与卡点

简单回顾一下前三天的路线。第一天主要理概念,搞清楚Agent和大模型API直接调用的区别:大模型是“你说一句,它答一句”,Agent则是“你给个目标,它自己拆解、调工具、看结果、再决定下一步”,本质是一个循环。第二天我装好环境,选了一个比较轻量的开源Agent开发框架作为基础,用仓库里的聊天示例跑通了一遍“模型调用+工具调用”的链路。第三天试着给Agent加了一个“查天气”的工具函数,让它能根据用户提问决定是否调用外部接口。

到第三天结束时,Demo长这样:用户说“帮我查一下北京的天气”,Agent会调用工具,拿到返回结果后组织语言回复。表面看没问题,但只要你开启第二轮对话,立刻露馅:

  • 用户:“顺便看看上海。”
  • Agent:“上海今天的天气是晴,23度。”(这个能靠关键词命中)
  • 用户:“那和北京比哪个更冷?”
  • Agent:“你说的是哪个北京?北京天气我还没查过。”(它忘了上一轮刚查过北京)

还有一个更影响实际使用的场景:用户在两轮之间改变了业务规则,比如“改成只显示最高气温,不显示最低气温”,Agent会在后续回答里继续输出旧格式。原因很简单,每次大模型调用都是无状态的,所有历史信息只能靠调用方手动拼进提示词,不拼进去就等于没发生过。

1.2 为什么先解决记忆,而不是先优化规划能力

我当时犹豫了一下,是不是应该先把Agent的“规划能力”做深一点,比如支持复杂的任务拆解、多工具组合。但仔细想就会发现,规划和记忆其实是强耦合的:规划依赖当前任务状态,而任务状态本身就是记忆的一种。连“上一步做了什么”都存不住,再强的规划也白搭。

另外一个原因是成本。每次调用模型都要重新传全部对话历史,如果历史一直无限增长,很快会把上下文窗口塞满,费用和延迟也会跟着涨。记忆机制不是简单地“把对话都存起来”,而是要设计一套存取策略:哪些必须每次带,哪些压缩后再带,哪些落库需要时才取。这个设计越早做,后面加功能越省心。

2. 给Agent装的“记忆”分哪三层

今天动手前,我先把记忆需求梳理了一遍,最后按“会话记忆、工作记忆、长期记忆”三层来设计。这个分层思路很多系统都在用,实际效果也最直接。

2.1 三层记忆的分工与边界

我用自己的话说清楚这三层的区别:

  • 会话记忆:当前对话窗口内的上下文,一般存在内存里。它管的是“这次对话中用户刚才说了什么、Agent已经回答了什么”。轮数少、token撑得住的时候,直接截取最近N轮拼进提示词就行。
  • 工作记忆:当前任务执行过程中的中间状态。比如Agent已经拆解了哪几个子任务,哪个子任务执行到哪一步,工具返回的中间结果是什么。我实现的时候用了一个任务状态字典,每次循环结束都更新。
  • 长期记忆:跨会话的核心知识和用户偏好,存在外部存储中,通常是向量数据库或普通数据库。比如用户偏好“报告里只显示最高气温”,这个偏好过了三天再见面也应该记住。

一个简单的类比:会话记忆是手上的便签纸,用完就丢;工作记忆是桌面的白板,任务进行时写满过程,任务结束就擦掉;长期记忆是抽屉里的笔记本,分类整理,需要的时候翻出来看。三者不是为了分层而分层,而是对应不同的生命周期和可信度。

2.2 存取逻辑:写入什么、什么时候取

“什么时候记、什么时候忘”比“怎么记”更重要。我整理了一条简单规则:会话记忆里明显的用户修正、新指令性偏好,可以写入长期记忆;执行过程中产生的临时结论,只写入工作记忆;工具返回的一次性数据,不入长期记忆。

检索时也需要分级。长期记忆不能每次都全部塞进提示词,那相当于没做。我的做法是先做一次召回:把会话记忆中的最新用户消息作为查询条件,去长期记忆里找最相关的3-5条记录,再拼进提示词。召回方式我用的是向量相似度匹配,对agent_kit自带的记忆组件做了封装;如果没有向量能力,先用关键词匹配也能顶一阵,但效果会差一些。

记忆类型生命周期存储位置写入时机读取时机
会话记忆当前对话窗口内存列表每轮用户消息+Agent回复后每次构造提示词时
工作记忆单个任务执行周期任务状态字典子任务完成或状态变更每次决策前读取
长期记忆跨会话向量库/数据库用户明确偏好、修正、重要约定新会话开始时召回

这个表格是我下午调完代码以后整理的,一开始分层没那么清晰,写了半天发现边界全糊在一起,最后重新梳理才顺。

3. 从零实现一个带三层记忆的Agent循环

理论梳理清楚以后,剩下的就是动手。我是在现有Demo的基础上改的,先搭最小可运行的版本,再逐步补参数细节。

3.1 环境与目录组织

今天用的还是前几天那个轻量框架,在它基础上加了一个记忆模块。目录大致长这样:

agent_work/ ├── main.py # Agent主循环入口 ├── memory/ │ ├── session_memory.py # 会话记忆:轮次列表与截断 │ ├── work_memory.py # 工作记忆:任务状态字典 │ └── long_term_memory.py # 长期记忆:向量化存取 ├── tools/ │ ├── weather.py # 已有的天气工具 │ └── report.py # 今天新加的报告格式工具 └── config.py # 模型配置、token预算、召回阈值

这样拆的好处是每类记忆独立文件,后面哪块出了问题直接看对应模块。我强烈建议别把所有记忆逻辑塞在一个文件里,不然后面改起来会痛到怀疑人生。

3.2 核心代码:会话记忆与工作记忆

先看会话记忆,本质就是一个带截断队列的消息列表。每次构造提示词时,把最近的消息按顺序拼进去,但超过预算就丢掉最早的消息。

# memory/session_memory.py from collections import deque class SessionMemory: def __init__(self, max_rounds=10): self.messages = deque(maxlen=max_rounds * 2) # 每条消息占1个,用户+助手算一轮 def add_user(self, content): self.messages.append({"role": "user", "content": content}) def add_assistant(self, content): self.messages.append({"role": "assistant", "content": content}) def to_prompt_list(self): return list(self.messages)

这个实现非常简单,maxlen帮我自动丢弃最老的消息。max_rounds=10是拍脑袋定的,因为我这边的模型上下文窗口比较大,10轮对话基本够用。如果用的窗口小,可以改成5甚至3,但低于3就容易出现“一聊多就失忆”的体感问题。

工作记忆我用一个字典来存,每次Agent执行循环时都会读写:

# memory/work_memory.py class WorkMemory: def __init__(self): self.state = { "pending_steps": [], # 待执行子任务 "completed_steps": [], # 已完成子任务 "last_tool_result": None, "current_goal": None, } def update(self, key, value): self.state[key] = value def snapshot(self): # 决策前把当前状态快照转成文本塞进提示词 return "\n".join( f"{k}: {v}" for k, v in self.state.items() if v is not None )

关键点是每轮决策前调用snapshot(),让模型看到当前进度。没有这一步,Agent经常会在多步任务中重复做已经完成的事。

3.3 长期记忆:写入与召回

长期记忆部分我封装了一个LongTermMemory类,底层用一个本地向量存储占位,真实项目可以换成任意向量数据库。写入时要把文本切成带时间戳的记录,方便追溯。

# memory/long_term_memory.py import time from agent_kit.embeddings import embed_text # 占位接口,实际会根据你的模型路由 class LongTermMemory: def __init__(self, store, top_k=4, threshold=0.35): self.store = store self.top_k = top_k self.threshold = threshold def remember(self, content, source="user"): embedding = embed_text(content) record = { "content": content, "source": source, "ts": time.time(), } self.store.upsert(embedding, record) def recall(self, query, top_k=None): top_k = top_k or self.top_k embedding = embed_text(query) candidates = self.store.search(embedding, top_k=top_k) # 阈值小于该值时丢弃,防止无关记忆被强行召回 return [ c.record["content"] for c in candidates if c.score >= self.threshold ]

这里最容易被忽略的就是threshold。不加阈值的后果是:不管用户问什么,系统都会“意思相近”地召回几条记忆,哪怕实际完全不相关。模型看到一堆牛头不对马嘴的回忆,表现反而比没有记忆更差。

3.4 串起来:Agent主循环

主循环把三层记忆整合进每次大模型调用中。流程很简单:拼提示词前,先取会话记忆、工作记忆快照、召回长期记忆,三段文本加系统指令一起交给模型。

# main.py 核心片段 from memory.session_memory import SessionMemory from memory.work_memory import WorkMemory from memory.long_term_memory import LongTermMemory import agent_kit.llm as llm session = SessionMemory(max_rounds=10) work = WorkMemory() long_term = LongTermMemory(store=vector_store, top_k=4, threshold=0.35) def build_prompt(user_input): # 1. 先做一次长期记忆召回 recalled = long_term.recall(user_input) # 2. 拼接系统提示词 + 召回记忆 + 工作快照 + 会话历史 system_prompt = "你是AI Agent,使用工具完成任务。用户偏好优先于默认行为。" prompt_parts = [ system_prompt, f"<长期记忆>\n{chr(10).join(recalled) if recalled else '(无)'}", f"<当前进度>\n{work.snapshot()}", "<对话历史>", ] prompt_parts.extend(f"{m['role']}: {m['content']}" for m in session.to_prompt_list()) prompt_parts.append(f"user: {user_input}") return "\n".join(prompt_parts) def run_once(user_input): work.update("current_goal", user_input) # 工作记忆:记录当前目标 prompt = build_prompt(user_input) # 略微宽松的采样参数,让Agent愿意尝试工具调用 response = llm.chat(prompt, temperature=0.4) # 解析模型输出中的工具调用意图(简化演示,实际用框架的解析器) if "<call_tool>" in response: tool_name, params = parse_tool_call(response) result = tools[tool_name].run(**params) work.update("last_tool_result", result) response = llm.chat(prompt + f"\ntool_result: {result}", temperature=0.3) session.add_user(user_input) session.add_assistant(response) # 简单规则:如果用户消息中出现“以后/下次/记住”等词,写入长期记忆 if any(word in user_input for word in ("以后", "下次", "记住", "偏好")): long_term.remember(user_input, source="user") return response

这只是最小可用版本。真正常见的写法是用框架里的Agent类覆盖build_prompt,但理解了这个循环,换到别的框架也只是换个接口名而已。

3.5 跑一个连贯场景验证

搭好以后,我跑了一个模拟场景,目标是检验“能不能记住跨轮偏好”:

用户: 以后查天气只显示最高和最低气温,不要湿度。 Agent: 好的,已记住这个偏好。 用户: 北京今天天气怎么样? Agent: 北京今天最高12度,最低3度。偏冷,建议穿外套。 用户: 那上海呢? Agent: 上海今天最高20度,最低14度。 用户: 广州也查一下。 Agent: 广州最高26度,最低19度。需要我对比一下三个城市吗?

第三轮“那上海呢”能正确调用工具并按新格式输出,说明那一条长期记忆已经生效。如果没有记忆,第二轮输出的格式大概率还是“晴,12度,湿度40%”这类旧模板。

4. 一次真实翻车:长期记忆被“污染”后的完整排查链路

这部分是今天最有价值的内容。下午信心满满地加了长期记忆之后,我高频测试了几十轮,很快发现一个诡异现象:Agent在某些对话里会反复引用一句根本不存在的话。

4.1 翻车现场

复现过程如下:

用户: 帮我记录一下,客户王总偏好用邮件沟通。 Agent: 好的,已记录。 用户: 下午有什么安排? Agent: 根据您之前的记录,王总偏好用邮件沟通……(莫名其妙出现了这句话) 用户: 我没问你王总,我问下午安排。 Agent: 您提到的王总偏好是重要信息,我会保留。下午您有两个会议……

这个输出看起来“很像样”,实际上完全跑偏了。“下午有什么安排”这个问题根本不涉及王总,但Agent却把这条记忆带进了当前上下文。第一次看到时我还以为是提示词拼错了,检查半天发现是长期记忆召回的锅。

4.2 排查链路

我逐步缩小范围,大致经过了四个步骤:

第一步:抓原始输入和输出,确认“多余回忆”从哪来。我把build_prompt里的<长期记忆>部分单独打印出来,发现“王总偏好用邮件沟通”确实出现在里面。问题锁定在召回环节,而不是模型幻觉。

第二步:检查向量相似度排序。我把“下午有什么安排”拿去查询向量库,发现“王总偏好用邮件沟通”排在前列。原因不复杂:两条消息都涉及“记录、偏好、沟通”等词汇,语义上存在表面相似度,而向量检索对“都是闲聊短句”的场景分辨力有限。

第三步:看阈值配置。我的threshold初始值设成了0.35,从召回结果看“王总”这条的相似度得分大约是0.42,刚好高于阈值,被当成相关记忆带进了提示词。

第四步:验证“去掉这条记忆”后问题是否消失。我临时把这条记录从库里删掉再跑同样问题,输出恢复正常。至此根因明确:召回阈值过低,加上长尾相似度记录混入,导致无关长期记忆被强塞进上下文。

4.3 修复方案与另一个坑

修复很直接:把threshold从0.35调整到0.55,同时增加一条规则——长期记忆里只有“明确的偏好、事实修正、关键约定”三类内容才允许写入,普通闲聊一律不进长库。

但调高阈值以后又踩到一个新坑:真实相关的记忆在某些表达方式下得分低于0.55,导致该记住的没记住。比如用户说“以后周报都改成周五发”,第二天问“周报哪天发”,Agent又不知道了。这是典型的召回率与精确率之间的拉锯。

最后我采用折中方案:核心记忆(用户明确说“记住”的那类)直接写进一条独立的高权重列表,每次构造提示词固定带上,不走向量召回;其他记忆走向量召回但阈值保持在0.50。这样既避免了污染,也保证了重要偏好不丢。

5. 有记忆与无记忆的实测对比结果

修完这个bug之后,我做了一组小实验来验证记忆机制的实际收益。实验条件保持一致:同一个模型、同一个工具集、同样的会话轮数。

5.1 四个测试场景

场景无记忆Agent表现有记忆Agent表现
跨话题回访(在A话题后突然切到B)切到B后彻底忘了A话题细节能在回答B时主动关联A话题信息
任务中断恢复(用户发新指令打断旧任务)旧任务被完全丢弃,需重新描述工作记忆保留旧任务进度,恢复成本低
偏好一次修改(报告格式变更)第二次又用旧格式输出立即按新格式输出,且后续保持一致
易混淆信息(两个相似实体名)高概率混淆两个实体属性长期记忆中实体关联后基本不混淆

每个场景我至少重复跑了5次,结论很稳定:有记忆之后,跨轮一致性和任务恢复能力提升明显,代价是首轮响应轻微变慢,因为要额外做一次记忆召回和记录写入。

5.2 几个有意思的观测点

第一个观测点是:记忆真正的不只是“存下来”,而是“在正确的时机取出来”。召回时机不对,哪怕存储内容完全正确,也会带来干扰。所以在真实项目里,我更倾向于“少而准”的长期记忆,而不是什么都往里塞。

第二个观测点是:模型输出格式会明显受历史记录影响。当历史消息里混入旧格式的工具返回结果时,即使新偏好已经写入记忆,模型也有一定概率继续沿用旧格式输出。类似现象要靠“重新措辞+贴出最新偏好”来压住,我是把最近一条记忆放在长期记忆区块的第一行,效果比随机排列好很多。

第三个观测点:不设审计机制会失控。如果长期记忆里混入一条错误事实,Agent会在所有后续对话中“自信地”(用错误事实回复),比“没有记忆”更可怕。所以我现在每写一条长期记忆都会记录写入时间和来源,方便排查。生产环境建议加一个“记忆审核/退出”指令,用户可以说“删掉那条记录”,Agent调用工具删除对应条目。

6. 最后说点今天的心得

动手之前,我以为加记忆无非是“多传几轮历史记录”,真正做下来才发现完全不是这么回事。三层记忆的职责划分、写入时机的判断、召回阈值的调校、记忆污染的防与治,每个小决策背后都是实际业务场景的取舍。

给同样在自学Agent的朋友一个可执行的建议:如果你的Agent刚跑通工具调用,先别急着加花哨的规划器和多智能体协作,把记忆这块补牢。具体从“会话历史能截断”和“用户偏好能跨对话保留”两个最朴素的需求开始,跑稳了再扩展。你今天多花一天时间把记忆折腾明白,后面至少能省下两周的调试时间。

还有一条自己的经验:改记忆策略前,先把旧策略下的完整上下文打印出来留档,不然你根本不知道Agent是“记错了”还是“压根没记”。今天这个bug排查之所以顺利,全程有日志可回溯,是最大的功臣。希望这篇日记对正在走同一条路的你有帮助。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询