📌前置知识:已完成第一课至第六课
🎯本课目标:让 AI 在多次独立交互之间记住信息,不再每次都"失忆"
💡核心概念:记忆存储 / 记忆检索 / 上下文整合 / 显式记忆管理
前言
上节课,我们给 Agent 加了循环。它终于能在一次任务里反复思考、逐步推进了。
但有一个问题——
# 第一次调用agent.run_loop("我的名字叫小明",max_steps=3)# Agent 处理完毕,状态清零# 第二次调用agent.run_loop("我叫什么名字?",max_steps=3)# Agent:???Agent 不知道你叫什么。因为在第二次调用时,AgentState被重置了,所有信息清空。它不记得上一次对话里发生了什么。
这不奇怪——第六课的状态只在一次循环内有效。循环一结束,state.reset()一调,全部归零。
但真实的助手不是这样的。你告诉 ChatGPT “我正在学 Python”,半小时后再问"我在学什么",它还记得。因为它有跨对话的记忆。
这就是第七课要解决的问题。
一、上下文 vs 记忆:两件完全不同的事
在动手之前,先理清一个很容易混淆的概念。
1.1 上下文(Context)
上下文就是当前 Prompt 里的所有内容。
第二课的多轮对话就是上下文——你把历史消息存进self.history,每次调用时拼进 prompt:
messages=[{"role":"system","content":self.system_prompt}]messages.extend(self.history)# ← 这就是上下文messages.append({"role":"user","content":user_input})AI 能"记住"这次对话的前几轮,靠的就是上下文。
但上下文是临时的。程序一重启,self.history就没了。即使程序不重启,一旦你调了clear_history(),记忆也全清了。
1.2 记忆(Memory)
记忆是跨对话持久化存储的数据。
和上下文的区别:
| 上下文(Context) | 记忆(Memory) | |
|---|---|---|
| 生命周期 | 当前会话内 | 跨会话持久化 |
| 存储位置 | 内存中的列表 | 独立的存储(文件/数据库/对象) |
| 何时加载 | 每次对话自动带上 | 需要时主动检索加载 |
| 典型例子 | “刚才你说了 X” | “上次你告诉我你喜欢 Python” |
上下文是"短期记忆"——这次对话里的事,AI 知道。
记忆是"长期记忆"——上一次对话里的事,AI 还知道。
本课要做的就是:给 Agent 加上长期记忆。
二、记忆系统要解决什么?
一个记忆系统,不管简单还是复杂,都要回答三个问题:
2.1 存什么?
用户告诉 Agent 的事实。比如:
- “我叫小明”
- “我在学 Python”
- “我喜欢用 Vim”
- “我的项目 deadline 是下周五”
这些都是值得长期记住的信息。
2.2 怎么存?
最简单的方式:一个字符串列表。
classAgentMemory:def__init__(self):self.memories:list[str]=[]就这样。一个列表,存字符串。不需要向量数据库,不需要 embedding,不需要语义检索。先从最简单的开始。
2.3 什么时候存?
这是最关键的设计决策。
方案 A:自动存——每次对话结束,把整段对话存起来。
简单,但低效。90% 的对话内容不值得长期记忆。"帮我写个排序函数"这件事,下次对话不需要知道。
方案 B:让 AI 自己决定存什么。
在 AI 的输出里加一个字段:save_to_memory。当 AI 认为某条信息值得长期记住时,把它放进去。如果没什么值得记的,就返回null。
// 用户说 "我叫小明"{"reply":"你好,小明!","save_to_memory":"用户的名字是小明"}// 用户说 "帮我写个排序函数"{"reply":"好的,这是排序函数...","save_to_memory":null}这就是本课采用的方式。AI 控制存储,你控制执行。和第五课"请求与执行分离"的思路一脉相承。
三、代码实现
3.1 记忆类:AgentMemory
打开agent/agent.py,找到新增的AgentMemory类:
classAgentMemory:""" 智能体记忆系统(第七课引入) 最简单的记忆实现:一个字符串列表。 存储 AI 认为值得长期记住的事实。 """def__init__(self):self.memories:list[str]=[]defadd(self,fact:str)->None:"""存储一条记忆"""iffactandfact.strip()andfactnotinself.memories:self.memories.append(fact.strip())print(f"🧠 存入记忆:{fact.strip()}")defget_all(self)->list[str]:"""获取所有记忆"""returnself.memories.copy()defsearch(self,keyword:str)->list[str]:"""按关键词搜索记忆(简单版)"""keyword=keyword.lower()return[mforminself.memoriesifkeywordinm.lower()]defremove(self,fact:str)->bool:"""删除一条记忆"""iffactinself.memories:self.memories.remove(fact)print(f"🗑️ 删除记忆:{fact}")returnTruereturnFalsedefclear(self)->None:"""清空所有记忆"""self.memories.clear()print("🧹 所有记忆已清空")defcount(self)->int:"""记忆条数"""returnlen(self.memories)注意几个细节:
① 去重。add()里检查fact not in self.memories——同一条事实不会重复存储。
② 返回副本。get_all()返回self.memories.copy(),不返回原始列表的引用。外部代码修改返回值不会影响内部数据。
③search()是简单版。只做了关键词匹配,没有语义检索。够用就行——复杂检索是以后的事。
3.2 带记忆的对话:run_with_memory()
defrun_with_memory(self,user_input:str)->Optional[dict]:""" 使用记忆上下文运行智能体(第七课核心方法)。 流程: 1. 从记忆中检索所有存储的事实 2. 将记忆拼入 Prompt,让 AI 看到 3. AI 根据记忆和用户输入生成回复 4. 如果 AI 决定存储新信息,自动保存到记忆 Args: user_input: 用户输入 Returns: 包含 reply 和 save_to_memory 的字典,失败则返回 None """memory_context=self.memory.get_all()# 构建记忆上下文字符串ifmemory_context:memory_str="你记住了以下关于用户的信息:\n"+"\n".join(f"-{item}"foriteminmemory_context)else:memory_str="你目前没有关于用户的记忆。"user_prompt=f"""你是一个有记忆能力的智能体助手。根据用户输入和你记住的信息来回复。{memory_str}规则: 1. 只返回有效的 JSON 2. 不要任何解释,不要 Markdown 3. 直接以 {{ 开头,以 }} 结尾 4. 如果用户告诉你新信息(比如名字、偏好、项目信息),请保存到记忆中 5. 如果用户问到你记得的信息,请使用记忆来回答 6. JSON 格式:{{"reply": "你的回复内容", "save_to_memory": "要记住的事实" 或 null}} 示例: - 用户说"我叫小明" → {{"reply": "你好,小明!", "save_to_memory": "用户的名字是小明"}} - 用户问"我叫什么"且你记得"用户的名字是小明" → {{"reply": "你叫小明", "save_to_memory": null}} - 用户说"帮我写个函数" → {{"reply": "好的...", "save_to_memory": null}} 用户输入:{user_input}请返回 JSON:"""forattemptinrange(3):response=self.client.chat.completions.create(model=self.model,messages=[{"role":"system","content":self.system_prompt},{"role":"user","content":user_prompt},],temperature=0.0,)text=response.choices[0].message.content parsed=extract_json_from_text(text)ifparsedand"reply"inparsed:# 如果 AI 决定保存新记忆,自动存入ifparsed.get("save_to_memory"):self.memory.add(parsed["save_to_memory"])returnparsedreturnNone逐段拆解这段代码的设计考量:
① 记忆检索——self.memory.get_all()
每次对话前,先从记忆系统里取出所有存储的事实。目前是"全部取出",后续可以改成"按相关性检索"。
② 记忆拼入 Prompt
ifmemory_context:memory_str="你记住了以下关于用户的信息:\n"+"\n".join(f"-{item}"foriteminmemory_context)else:memory_str="你目前没有关于用户的记忆。"模型不能直接访问你的记忆系统。它只能看到 Prompt 里的内容。所以你必须在每次对话时,把记忆"加载"到 Prompt 里——这和第六课把状态拼进 Prompt 是同一个思路。
③ AI 控制存储——save_to_memory字段
AI 决定是否存储,但真正执行存储的是你的代码:
ifparsed.get("save_to_memory"):self.memory.add(parsed["save_to_memory"])又回到了那个老原则:AI 描述意图,你控制执行。AI 说"我要记住这件事",你的代码决定是否真的存进去(你可以加过滤、加校验、加审计)。
④ Few-shot 示例
Prompt 里给了两个示例,告诉 AI 什么时候该存、什么时候不该存。这对小模型(7B)尤其重要——没有示例,它可能什么都存或者什么都不存。
⑤ 老三样
- JSON 输出 +
extract_json_from_text()—— 第三课以来的标准操作 - 重试 3 次—— 老规矩
- temperature=0.0—— 记忆读写需要确定性
四、运行示例
4.1 基础场景:记住名字
fromagent.agentimportAgent agent=Agent(model="qwen2.5:7b")# 第一次交互:告诉 Agent 你的名字print("=== 对话 1 ===")r1=agent.run_with_memory("我叫小明,是一名后端开发工程师")ifr1:print(f"Agent:{r1['reply']}")# 第二次交互:问它记不记得print("\n=== 对话 2 ===")r2=agent.run_with_memory("你还记得我的名字和职业吗?")ifr2:print(f"Agent:{r2['reply']}")# 查看记忆内容print(f"\n🧠 当前记忆({agent.memory.count()}条):")forminagent.memory.get_all():print(f" -{m}")预期输出(类似):
=== 对话 1 === 🧠 存入记忆:用户的名字是小明 🧠 存入记忆:用户是一名后端开发工程师 Agent: 你好小明!很高兴认识你,作为一名后端开发工程师... === 对话 2 === Agent: 当然记得!你的名字是小明,职业是后端开发工程师。 🧠 当前记忆(2 条): - 用户的名字是小明 - 用户是一名后端开发工程师注意:两次run_with_memory()之间没有传递任何上下文。Agent 知道你叫小明,不是因为上一次对话的历史,而是因为记忆系统里存了这条事实。
4.2 累积记忆
# 第三次交互:告诉 Agent 更多信息print("=== 对话 3 ===")r3=agent.run_with_memory("我主要用 Python 和 Go,最近在学 AI Agent")ifr3:print(f"Agent:{r3['reply']}")# 第四次交互:综合提问print("\n=== 对话 4 ===")r4=agent.run_with_memory("帮我推荐一个学习路线,结合我的技术栈")ifr4:print(f"Agent:{r4['reply']}")# 查看全部记忆print(f"\n🧠 当前记忆({agent.memory.count()}条):")forminagent.memory.get_all():print(f" -{m}")记忆会逐步累积。每条值得记住的信息都被 Agent 主动存入,后续对话中自动加载。
4.3 记忆管理
# 按关键词搜索记忆print("搜索 'Python':",agent.memory.search("Python"))# 删除某条记忆agent.memory.remove("用户是一名后端开发工程师")# 清空全部记忆agent.memory.clear()五、与第六课的本质区别
把两课放在一起对比:
第六课(Agent Loop——循环内状态):
循环开始 → 步骤1 → 步骤2 → 步骤3 → 循环结束 → 状态清零 ↕ ↕ ↕ 状态共享 状态共享 状态共享状态在循环内部共享,但循环一结束就没了。
第七课(记忆——跨对话持久化):
对话1:你说 "我叫小明" → Agent 存入记忆 ↓ 记忆系统 ["用户的名字是小明"] ↓ 对话2:你问 "我叫什么" → Agent 从记忆中加载 → "你叫小明" ↓ 对话3:你说 "我用 Python" → Agent 存入新记忆 ↓ 记忆系统 ["用户的名字是小明", "用户使用 Python"] ↓ 对话4:你问 "我会什么" → Agent 从记忆中加载 → "你会 Python"记忆在完全独立的对话之间持久化。程序重启都没关系(只要你把记忆保存到文件里)。
| 第六课(状态) | 第七课(记忆) | |
|---|---|---|
| 生命周期 | 循环内 | 跨对话 |
| 存储方式 | 内存中的对象 | 独立的存储系统 |
| 谁控制读写 | 代码自动管理 | AI 决定存什么,代码决定怎么存 |
| 重置时机 | 每次循环开始 | 手动或程序重启 |
| 典型内容 | 步骤计数、动作历史 | 用户偏好、个人信息、项目上下文 |
状态解决"这次任务做到哪一步了"。记忆解决"这个用户是什么样的"。
六、关键洞察
6.1 记忆是数据存储,不是思考
这是本课最重要的洞察:
记忆 = 数据存储。不是意识,不是推理,不是隐藏的认知能力。
它就是一个列表,里面存着字符串。你可以get_all()看到全部内容,可以remove()删除,可以clear()清空。没有什么"AI 的内心世界"——就是朴实无华的数据。
这意味着记忆是完全可控的。你可以审计 AI 存了什么、删掉不该存的东西、在加载时做过滤。记忆对 AI 来说只是一个信息来源,和你从数据库里查一条记录没有本质区别。
6.2 AI 控制存储,你控制执行
和第五课"请求与执行分离"一样,记忆系统也遵循这个原则。
AI 通过save_to_memory字段建议存储什么,但真正执行存储的是self.memory.add()——你的代码。你想在存储前加过滤?加。想去重?加。想限制记忆条数?加。
AI 是建议者,你的代码是决策者。
6.3 模型不直接访问记忆
模型看不到你的AgentMemory对象。它只能看到你在 Prompt 里放的内容。
memory_str="你记住了以下关于用户的信息:\n"+"\n".join(f"-{item}"foriteminmemory_context)这段代码做的事情,就是把记忆"翻译"成模型能理解的文字,塞进 Prompt。模型"记住了"你的名字,其实是因为它在 Prompt 里看到了"用户的名字是小明"这句话。
理解这一点很重要——记忆的真正能力来自你的代码,不是来自模型。
6.4 简单就是强大
本课的记忆系统就是一个字符串列表。没有向量数据库,没有 embedding,没有语义检索。
但就这么简单的东西,已经能解决大部分跨对话记忆的需求。“用户叫什么”“喜欢什么”“项目是什么”——这些信息不需要复杂的检索,全部加载到 Prompt 里就够了。
先让功能跑起来,再考虑优化。记忆太多导致 Prompt 过长?到时候加截断。记忆太杂导致检索不准?到时候加语义检索。但第一步,就是把"存和取"这个基本能力建立起来。
七、常见问题
Q:Agent 不保存信息到记忆怎么办?
A:检查几件事:① Prompt 里是否有明确的 few-shot 示例,教 AI 什么时候该存?②save_to_memory字段的验证逻辑是否正确?③ 用户输入里是否确实包含值得记住的信息("帮我写个函数"确实不需要记)。给小模型(7B)加清晰的示例尤其重要。
Q:Agent 记住了错误的信息怎么办?
A:直接用memory.remove()删除错误记忆。记忆是显式存储,你可以随时检查和修改。这就是"记忆 = 数据存储"的好处——透明、可控。
Q:记忆太多,Prompt 太长怎么办?
A:三个方案:① 限制最大记忆条数,超过就删最旧的;② 只加载和当前输入相关的记忆(search()方法);③ 用摘要代替全量加载。本课的get_all()是最简单的方式,适合记忆不多(< 20 条)的场景。
Q:怎么让记忆持久化到文件?
A:很简单——在AgentMemory里加save_to_file()和load_from_file()方法,用json.dump()和json.load()就行。下次课程可以考虑加上。
Q:记忆和第二课的多轮对话 history 有什么区别?
A:self.history是上下文——本次会话内的对话记录,会话结束就清空。self.memory是长期记忆——跨会话持久化存储。两者互补:history 让 AI 记得"刚才说了什么",memory 让 AI 记得"上次聊了什么"。
八、下期预告
第八课:规划——让 Agent 学会拆解复杂任务
前七课,Agent 可以做决策、调工具、循环执行、记住信息了。但它的每一步决策都是"当场想"的——没有提前规划。
当你给 Agent 一个复杂任务:“帮我写一个网页爬虫,抓取新闻标题,按日期分类,存入数据库”——它不会先规划"第一步做什么、第二步做什么",而是直接开始随机行动。
下一课,我们给 Agent 加上规划能力——在动手之前先想好步骤,然后按计划执行。这是 Agent 从"能干活"到"会干活"的关键一步。
敬请期待!
完整代码获取
本课涉及的完整代码包括:
AgentMemory类——轻量级记忆存储系统run_with_memory()方法——带记忆的对话执行complete_example.py——演示模式 + 交互模式
关注公众号「开源情报局」,回复「Agent」获取。
标签
#Python#AI Agent#LLM#记忆系统#Ollama#Qwen#大模型#手搓Agent
本文为《手搓 AI Agent 从 0 到 1》系列教程第 7 课