news 2026/8/21 7:06:58

Day 16 · 长任务与记忆:让 Agent 记住上下文

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Day 16 · 长任务与记忆:让 Agent 记住上下文

「AI Python 系列」第 01 栏 · AI 时代的 Python 办公自动化

全栏 18 篇 · 零成本跟完

品牌:梅雅达编程笔记


摘要:前面两篇搭好了 ReAct Agent,但它有个致命问题——每次对话都是"新生",上一轮说过的话下一轮就忘。本篇给 Agent 加装双层记忆系统:短时记忆用对话列表维持上下文连贯,长时记忆用 JSON 文件做跨会话持久化。本篇使用 GLM-4.7-Flash,永久免费,4 轮对话约消耗 5000 token,零成本跑完。

关键词:Agent 记忆、上下文管理、JSON 持久化、长任务、System Prompt 注入


环境与前置条件

项目版本 / 说明
操作系统Windows 10/11、macOS、Linux 均可
Python3.9 及以上(本文用 3.12 验证)
依赖库json(标准库)、llm_client(本专栏封装的 LLM 调用模块,见 Day 04)
LLM 提供商智谱 GLM-4.7-Flash(免费),代码中可切换 DeepSeek / Qwen
前置知识需先完成 Day 14-15,理解 ReAct Agent 和 Function Calling 的基本原理
预计耗时阅读 15 分钟 + 动手验证 10 分钟

开篇 · 一个让我翻车的真实场景

Day 14-15 我们做了能调用工具的 ReAct Agent,我兴冲冲拿给学生用。结果第一个学生就反馈:“老师,它记性怎么这么差?”

事情是这样的:学生先跟 Agent 说"我叫小明,今年 10 岁",Agent 回复"你好小明"。然后学生紧接着问"我叫什么",Agent 直接回了句"抱歉,我不知道你的名字"。

学生当场就愣住了,然后跑来找我告状。

我一开始还以为是模型出问题了,排查了半天才发现——根本不是 AI 不够聪明,是代码层面压根没把上一轮对话传过去。每次调 API 都是一次独立请求,模型本身无状态,你不把历史消息塞进去,它当然什么都不记得。

这就像你跟一个人打电话,每说一句话就挂断再重拨,对方怎么可能记得你刚才说了什么?

找到原因后,我给 Agent 加了一套双层记忆系统。今天这篇就讲这个事情——怎么让 Agent 真的"记住你"。


一、为什么要分两层?一层 JSON 不行吗?

我第一版其实是只用了一个 JSON 文件,把所有东西都往里塞。跑了两天就发现不对——每次对话都要读写磁盘,速度肉眼可见地变慢。打开 JSON 文件一看,好家伙,光对话历史就涨了好几 KB。

后来我改成只存在内存里,速度是快了,但程序一关全没了。用户第二天打开又得重新自我介绍,体验极差。

折腾了两版之后我才意识到:这两种记忆的性质完全不同,必须分开处理。

维度短时记忆(内存)长时记忆(磁盘)
生命周期程序关了就没了写进文件,重启还在
访问频率每轮对话都要读写只在"记住/回忆"时才动
丢了代价低——当前对话断就断了高——用户数据永久丢失
适合存什么当前聊天的上下文用户名、偏好、长期事实

热数据放内存,冷数据落磁盘——这个思路不是我发明的,数据库早就这么干了。但在 Agent 里实现起来其实很朴素。


二、短时记忆:就是个列表,别想复杂了

短时记忆的实现特别简单——一个 Python 列表,每轮对话把用户消息和 AI 回复都append进去,就这样。

classMemoryAgent:def__init__(self,memory_file="agent_memory.json",provider="glm"):self.client=LLMClient(provider=provider)self.memory_file=memory_file self.history=[]# 短时记忆:就是个列表self.long_term=self._load_memory()# 长时记忆:从文件加载

对话的时候,把最近的历史拼成文本传给模型:

defchat(self,message):self.history.append({"role":"user","content":message})recent=self.history[-10:]# 只取最近10条context="\n".join([f"{'用户'ifm['role']=='user'else'助手'}:{m['content']}"forminrecent])response=self.client.chat(context,system_prompt=system_prompt,temperature=0.5,max_tokens=500)self.history.append({"role":"assistant","content":response})returnresponse

你可能会问:为什么不把全部历史都传给模型?

我一开始还真是这么干的——结果聊了 20 轮之后,API 直接报 token 超限的错误。这才想起来模型的上下文窗口是有限的。后来我改成[-10:]只取最近 10 条,问题就解决了。

10 条这个数字也不是精确计算出来的,就是试了几次发现够用:用户说"那个文件""刚才那个"之类的指代,模型基本都能回溯到。如果你的场景需要更长的回溯,可以调大到 15-20 条,但别贪多,token 消耗是实打实的。


三、长时记忆:一个 JSON 文件搞定

长时记忆要解决的是"关掉程序再打开,之前记住的东西还在"。我用 JSON 文件来做——够简单,够直观,不需要装额外的库。

3.1 加载与保存

def_load_memory(self):ifos.path.exists(self.memory_file):try:withopen(self.memory_file,"r",encoding="utf-8")asf:returnjson.load(f)except(json.JSONDecodeError,IOError):passreturn{"facts":{},"notes":[]}def_save_memory(self):withopen(self.memory_file,"w",encoding="utf-8")asf:json.dump(self.long_term,f,ensure_ascii=False,indent=2)

这里有个我踩过的坑:有一次我调试的时候手动 Ctrl+C 强杀了程序,结果 JSON 文件写到一半就断了,变成了非法格式。下次启动 Agent 的时候直接报错崩溃。后来我加了try/except捕获JSONDecodeError,文件坏了就给个空结构重新来,至少不会让整个程序挂掉。

不过说实话,这么做有个隐患——用户的记忆文件坏了你静默丢弃,用户可能根本不知道。生产环境最好加个日志记录,或者把损坏的文件备份成.bak再新建。

3.2 记住与回忆

defremember(self,key,value):self.long_term["facts"][key]=value self._save_memory()returnf"已记住:{key}={value}"defrecall(self,key):value=self.long_term["facts"].get(key)ifvalue:returnf"回忆起来:{key}={value}"returnf"没有关于「{key}」的记忆"

每次remember都立即写盘。这样做最安全,不会丢数据。但有个代价——如果你频繁调用 remember(比如让它批量记住一堆东西),磁盘 IO 会很频繁。我个人使用场景下感觉不到延迟,但如果是服务端多人并发用,建议改成"攒一批再写"的模式,并且加文件锁。

3.3 笔记:给没有明确 key 的信息留个位置

除了键值对,还有种信息不太好塞进 facts——比如用户说"下周要交报告"。这不是一个"key=value"能表达的东西,更像一条备忘录。所以我加了一个 notes 列表,每条带时间戳:

defadd_note(self,note):timestamp=datetime.now().strftime("%Y-%m-%d %H:%M")self.long_term["notes"].append({"time":timestamp,"note":note})self._save_memory()returnf"已记录笔记:{note}"

简单总结下分工:有明确名字的东西(用户名、所在城市)存 facts;事件、提醒、随手记存 notes。


四、最关键的一步:记忆怎么"喂"给模型

记忆存好了还不够。模型看不到你的 JSON 文件,它只能看到你传给它的文本。所以必须在每轮对话时,把记忆格式化后塞进 system prompt

这个环节折腾了我一阵。一开始我把记忆拼成一个长字符串直接丢进去,结果发现模型有时候会"无视"我注入的信息。后来我学聪明了,在 system prompt 里加了明确的规则,告诉模型"这些是你记住的信息,要在回答中用到"。

格式化函数长这样:

def_format_memory_for_prompt(self):parts=[]ifself.long_term["facts"]:facts="\n".join([f" -{k}:{v}"fork,vinself.long_term["facts"].items()])parts.append(f"已知信息:\n{facts}")ifself.long_term["notes"]:notes="\n".join([f" - [{n['time']}]{n['note']}"forninself.long_term["notes"][-5:]])parts.append(f"近期笔记:\n{notes}")return"\n\n".join(parts)ifpartselse"暂无记忆"

然后在chat里把它拼进 system prompt:

memory_str=self._format_memory_for_prompt()system_prompt=f"""你是一个有记忆能力的智能助手。 你的长时记忆:{memory_str}规则: - 对话中自然地使用你记住的信息 - 如果用户说"记住xxx是yyy",主动调用记忆功能 - 如果用户问"你还记得xxx吗",从记忆中查找 - 保持简洁友好的风格"""

有两件事需要注意:

第一,notes 我取了[-5:]只给最近 5 条。原因跟对话历史截断一样——记忆越多 token 消耗越大,而且一周前的笔记对今天的对话通常没什么参考价值。

第二,这个方案的 token 成本会随记忆量线性增长。100 条 facts 大概占 500+ token,每轮对话都重复发送。如果你的 Agent 需要记住很多东西,后面可以考虑改成向量检索——只注入跟当前问题相关的记忆。这个进阶方案在文末有提到。


五、记忆在长任务里还有另一层作用

前面说的都是"记住用户是谁"这类基础功能。但记忆系统在更复杂的场景里作用更大——它可以充当任务的检查点

举个实际例子:假设让 Agent 帮你做一份月度报告,要读 Excel、统计品类、分析趋势、生成文字、输出 Markdown,5 个步骤。如果没有记忆,第 3 步的时候第 1 步读的数据已经丢了。有了记忆系统,每完成一步就把关键中间结果存下来:

# 第1步完成后agent.remember("销售数据_总行数","12453")agent.remember("销售数据_时间范围","2026-07-01 至 2026-07-31")agent.add_note("第1步完成:数据读取成功,共12453条")

更实际的价值在于中断恢复。我有一次让学生跑一个多步骤的数据处理任务,跑到第 4 步程序崩了(好像是内存不够)。如果没有记忆,得从头来。但有了记忆,重启后 Agent 看到 notes 里写着"前 3 步已完成",可以直接从第 4 步接着干。

这种"检查点"模式在很多工程系统里都有,游戏里的存档也是同一个思路。本篇的代码实现了基础记忆机制,检查点功能可以在此基础上扩展。


六、我踩过的三个坑

写到这里,说几个我在实际开发中真实遇到的问题,不是理论上的"可能出问题",是真真切切踩过的。

6.1 JSON 文件写到一半程序崩了

前面提到过一次,这里展开说。原因是_save_memory在写入过程中如果程序被强杀(Ctrl+C、断电、OOM),文件就只写了一半,JSON 格式不完整。下次启动时json.load()直接抛JSONDecodeError

我的处理方式是加了异常捕获,文件坏了就给空结构。但更好的做法是"先写临时文件,再原子重命名":

def_save_memory(self):tmp_file=self.memory_file+".tmp"withopen(tmp_file,"w",encoding="utf-8")asf:json.dump(self.long_term,f,ensure_ascii=False,indent=2)os.replace(tmp_file,self.memory_file)# 原子替换

os.replace在大多数操作系统上是原子操作,即使写入过程中断电,旧文件也不会被破坏。这个改动虽然小,但能避免很多莫名其妙的数据丢失。

6.2 两个人同时用,记忆会互相覆盖

这个坑是我把 Agent 部署成 Web 服务之后发现的。两个用户同时调用remember,都触发了_save_memory,后写的直接覆盖先写的——用户 A 的记忆把用户 B 的盖掉了。

如果只是个人本地用,这个问题不存在。但如果多人共用,必须加锁。Linux 上用fcntl,Windows 上用msvcrt.locking,或者干脆用filelock这个跨平台的库。

6.3 记忆越攒越多,JSON 越来越大

用了一个月之后打开 JSON 文件,里面存了几百条 facts。每次启动都要全量加载到内存,每轮对话都要把几百条 facts 格式化后塞进 system prompt——token 消耗肉眼可见地在涨。

目前我的临时方案是:facts 超过 50 条就手动清理一下。但这显然不是长久之计。后续准备加一个"淘汰策略",比如超过 30 天没被 recall 过的 facts 自动归档,notes 超过 3 个月的自动清理。更彻底的方案是做记忆总结——让 AI 把零散的记忆压缩成几条核心摘要。


七、完整代码与运行验证

核心代码都在下面了。llm_client.py是 Day 04 封装好的 LLM 调用模块,这里直接复用。

importosimportjsonfromllm_clientimportLLMClientclassMemoryAgent:def__init__(self,memory_file="agent_memory.json",provider="glm"):self.client=LLMClient(provider=provider)self.memory_file=memory_file self.history=[]# 短时记忆self.long_term=self._load_memory()# 长时记忆def_load_memory(self):ifos.path.exists(self.memory_file):try:withopen(self.memory_file,"r",encoding="utf-8")asf:returnjson.load(f)except(json.JSONDecodeError,IOError):passreturn{"facts":{},"notes":[]}def_save_memory(self):# 先写临时文件,再原子替换,防止写到一半崩溃tmp_file=self.memory_file+".tmp"withopen(tmp_file,"w",encoding="utf-8")asf:json.dump(self.long_term,f,ensure_ascii=False,indent=2)os.replace(tmp_file,self.memory_file)defremember(self,key,value):self.long_term["facts"][key]=value self._save_memory()returnf"已记住:{key}={value}"defrecall(self,key):value=self.long_term["facts"].get(key)returnf"回忆起来:{key}={value}"ifvalueelsef"没有关于「{key}」的记忆"defadd_note(self,note):fromdatetimeimportdatetime timestamp=datetime.now().strftime("%Y-%m-%d %H:%M")if"notes"notinself.long_term:self.long_term["notes"]=[]self.long_term["notes"].append({"time":timestamp,"note":note})self._save_memory()returnf"已记录笔记:{note}"defchat(self,message):# 把长时记忆注入 system promptmemory_str=self._format_memory_for_prompt()system_prompt=f"你是一个有记忆的助手。已知信息:\n{memory_str}\n自然地使用这些信息回答。"# 短时记忆:记录对话,只取最近10条self.history.append({"role":"user","content":message})recent=self.history[-10:]context="\n".join([f"{'用户'ifm['role']=='user'else'助手'}:{m['content']}"forminrecent])response=self.client.chat(context,system_prompt=system_prompt,temperature=0.5,max_tokens=500)self.history.append({"role":"assistant","content":responseor"(API异常)"})returnresponsedef_format_memory_for_prompt(self):parts=[]ifself.long_term.get("facts"):facts="\n".join([f" -{k}:{v}"fork,vinself.long_term["facts"].items()])parts.append(f"已知信息:\n{facts}")ifself.long_term.get("notes"):notes="\n".join([f" - [{n['time']}]{n['note']}"forninself.long_term["notes"][-5:]])parts.append(f"近期笔记:\n{notes}")return"\n\n".join(parts)ifpartselse"暂无记忆"

怎么跑

目录结构:

day16_code/ ├── memory_agent.py # 本篇代码 ├── llm_client.py # Day 04 的封装,直接复用 └── requirements.txt
cdday16_code pipinstall-rrequirements.txt python memory_agent.py

跑起来之后

试试这几步,感受一下记忆是怎么工作的:

你: /remember 用户名=梅雅达 已记住:用户名 = 梅雅达 你: /remember 所在城市=北京 已记住:所在城市 = 北京 你: 你好,我是梅雅达 助手: 你好梅雅达!你在北京工作,有什么需要帮忙的吗? 你: 你还记得我叫什么吗? 助手: 你叫梅雅达,在北京工作。

然后退出程序。你会发现当前目录多了一个agent_memory.json

{"facts":{"用户名":"梅雅达","所在城市":"北京"},"notes":[]}

重新运行python memory_agent.py,直接输入"我叫什么"——如果它还能回答出来,说明记忆确实落盘了。

如果重启后记忆丢了,检查两点:①memory_file路径是不是相对路径导致写到了别的目录(建议用绝对路径);② 程序有没有当前目录的写权限。


八、写在最后

Agent 的记忆系统说到底就是状态管理——模型本身是无状态的,每一次 API 调用都是一个独立的请求。我们要做的是在外部维护状态,然后在对话时把相关状态"喂"给模型。

短时记忆管当前对话的连贯性,长时记忆管跨会话的数据持久化,两者通过 system prompt 注入汇合。这个分层思路不只适用于聊天机器人——你以后做更复杂的 Agent,不管是自动化报表、数据分析还是工具编排,都会用到类似的状态管理机制。

JSON 方案够轻量、够直观,适合个人使用和小规模场景。等记忆量涨到几百条以上,或者需要多人并发,就该考虑 SQLite 或向量数据库了。但在那之前,先用 JSON 把功能跑通再说。


你可以接着折腾的方向

加个"遗忘"功能。用户改名了、搬城市了,旧记忆就该能删掉。实现起来就一行del self.long_term["facts"][key],但别忘了处理 key 不存在的情况。

用向量检索替代全量注入。每轮对话把所有 facts 都塞进 prompt,记忆多了 token 扛不住。试试给每条记忆生成 embedding,查找时按相似度排序只注入 Top-3。sentence-transformers可以本地跑,也可以调 GLM 的 embedding API。

做一个"记忆总结"。对话超过 20 轮时,让 AI 把前面的内容总结成一段摘要存进 notes,然后清空短时记忆。核心逻辑大致是:if len(history) > 20→ 调模型总结 →add_note(summary)history.clear()。这个思路和 MemGPT 论文里的做法很像,感兴趣的可以去翻翻。


下期预告

Day 17 · 综合实战 A:日报/周报/月报自动化 Agent

Agent 有工具、有记忆了,该来干正事了。明天做一个报告自动化 Agent——读取经营数据,AI 分析趋势和异常,自动生成 Markdown 格式的日报/周报/月报。整合前面学的所有技能。


资源与工具

  • Python json 模块文档:https://docs.python.org/3/library/json.html
  • LLM 记忆机制综述(MemGPT):https://arxiv.org/abs/2304.11477
  • 智谱 GLM 模型文档:https://docs.bigmodel.cn/
  • 本专栏配套代码:CSDN 下载区

往期回顾

  • Day 15 · 多工具 Agent:让 AI 自主选择和组合工具

专栏:「AI Python 系列」第 01 栏 · AI+办公自动化

©️ 梅雅达编程笔记原创 · 首发 CSDN · 转载请注明出处

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/21 6:56:07

Control Registers

CR0 到 CR4 是 x86 架构中核心的控制寄存器(Control Registers),它们就像 CPU 的“主控开关”,决定了 CPU 的工作模式、内存管理机制和众多扩展特性的开启与关闭。按照功能把它们分成了“基础控制”和“特性控制”两大类。第一层&…

作者头像 李华
网站建设 2026/8/21 6:54:49

Slater项目集成BM25全文索引与Graphiti图查询能力解析

这次我们来看一个名为 Slater 的项目,它最近获得了全文 BM25 索引和 Graphiti 支持的能力更新。对于需要处理大量文本、进行高效检索和构建知识图谱的开发者来说,这无疑是一个值得关注的技术栈演进。本文将直接切入主题,分析 Slater 的核心能…

作者头像 李华
网站建设 2026/8/21 6:52:12

LLM API 速率限制(HTTP 429)的成因与稳健处理方案

最近在对接各类大模型 API 开发应用时,你是否也频繁遇到HTTP 429 Too Many Requests这个令人头疼的错误?尤其是在业务高峰期或批量处理任务时,这个错误会直接导致服务中断、用户体验下降,甚至引发数据丢失。HTTP 429并非简单的“网…

作者头像 李华
网站建设 2026/8/21 6:50:30

构建Codex多账号热切换方案:从令牌管理到自动化工作流

昨天下午,我正用 Codex 处理一个批量任务,突然界面卡死,紧接着就是熟悉的崩溃弹窗。重启、重装、清理缓存,一通操作下来,账号状态似乎又回到了“出厂设置”,之前的配置和上下文全没了。这已经不是第一次了。…

作者头像 李华
网站建设 2026/8/21 6:50:15

Mathorcup数学建模竞赛A题解析:动态资源调度与混合整数规划应用

1. 赛题核心与破题思路:从“资源调度”到“动态优化”每年四月的Mathorcup数学建模竞赛,对于很多数学建模爱好者来说,都是一次检验综合能力、挑战思维极限的绝佳机会。今年的A题,一眼看去是关于“资源调度”的经典问题&#xff0c…

作者头像 李华