【手搓 AI Agent 从 0 到 1】第七课:记忆——让 Agent 跨对话记住信息
📌 前置知识:已完成第一课至第六课
🎯 本课目标:让 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 怎么存?
最简单的方式:一个字符串列表。
class AgentMemory:
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 类:
class AgentMemory:
"""
智能体记忆系统(第七课引入)
最简单的记忆实现:一个字符串列表。
存储 AI 认为值得长期记住的事实。
"""
def __init__(self):
self.memories: list[str] = []
def add(self, fact: str) -> None:
"""存储一条记忆"""
if fact and fact.strip() and fact not in self.memories:
self.memories.append(fact.strip())
print(f"🧠 存入记忆:{fact.strip()}")
def get_all(self) -> list[str]:
"""获取所有记忆"""
return self.memories.copy()
def search(self, keyword: str) -> list[str]:
"""按关键词搜索记忆(简单版)"""
keyword = keyword.lower()
return [m for m in self.memories if keyword in m.lower()]
def remove(self, fact: str) -> bool:
"""删除一条记忆"""
if fact in self.memories:
self.memories.remove(fact)
print(f"🗑️ 删除记忆:{fact}")
return True
return False
def clear(self) -> None:
"""清空所有记忆"""
self.memories.clear()
print("🧹 所有记忆已清空")
def count(self) -> int:
"""记忆条数"""
return len(self.memories)
注意几个细节:
① 去重。 add() 里检查 fact not in self.memories——同一条事实不会重复存储。
② 返回副本。 get_all() 返回 self.memories.copy(),不返回原始列表的引用。外部代码修改返回值不会影响内部数据。
③ search() 是简单版。 只做了关键词匹配,没有语义检索。够用就行——复杂检索是以后的事。
3.2 带记忆的对话:run_with_memory()
def run_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()
# 构建记忆上下文字符串
if memory_context:
memory_str = "你记住了以下关于用户的信息:\n" + "\n".join(
f"- {item}" for item in memory_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:"""
for attempt in range(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)
if parsed and "reply" in parsed:
# 如果 AI 决定保存新记忆,自动存入
if parsed.get("save_to_memory"):
self.memory.add(parsed["save_to_memory"])
return parsed
return None
逐段拆解这段代码的设计考量:
① 记忆检索——self.memory.get_all()
每次对话前,先从记忆系统里取出所有存储的事实。目前是"全部取出",后续可以改成"按相关性检索"。
② 记忆拼入 Prompt
if memory_context:
memory_str = "你记住了以下关于用户的信息:\n" + "\n".join(f"- {item}" for item in memory_context)
else:
memory_str = "你目前没有关于用户的记忆。"
模型不能直接访问你的记忆系统。它只能看到 Prompt 里的内容。所以你必须在每次对话时,把记忆"加载"到 Prompt 里——这和第六课把状态拼进 Prompt 是同一个思路。
③ AI 控制存储——save_to_memory 字段
AI 决定是否存储,但真正执行存储的是你的代码:
if parsed.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 基础场景:记住名字
from agent.agent import Agent
agent = Agent(model="qwen2.5:7b")
# 第一次交互:告诉 Agent 你的名字
print("=== 对话 1 ===")
r1 = agent.run_with_memory("我叫小明,是一名后端开发工程师")
if r1:
print(f"Agent: {r1['reply']}")
# 第二次交互:问它记不记得
print("\n=== 对话 2 ===")
r2 = agent.run_with_memory("你还记得我的名字和职业吗?")
if r2:
print(f"Agent: {r2['reply']}")
# 查看记忆内容
print(f"\n🧠 当前记忆({agent.memory.count()} 条):")
for m in agent.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")
if r3:
print(f"Agent: {r3['reply']}")
# 第四次交互:综合提问
print("\n=== 对话 4 ===")
r4 = agent.run_with_memory("帮我推荐一个学习路线,结合我的技术栈")
if r4:
print(f"Agent: {r4['reply']}")
# 查看全部记忆
print(f"\n🧠 当前记忆({agent.memory.count()} 条):")
for m in agent.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}" for item in memory_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 课
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)