📌 前置知识:已完成第一课至第六课
🎯 本课目标:让 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 课

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐