【手搓 AI Agent 从 0 到 1】第七课:记忆——让 Agent 跨对话记住信息

张开发
2026/4/21 6:13:34 15 分钟阅读

分享文章

【手搓 AI Agent 从 0 到 1】第七课:记忆——让 Agent 跨对话记住信息
前置知识已完成第一课至第六课本课目标让 AI 在多次独立交互之间记住信息不再每次都失忆核心概念记忆存储 / 记忆检索 / 上下文整合 / 显式记忆管理前言上节课我们给 Agent 加了循环。它终于能在一次任务里反复思考、逐步推进了。但有一个问题——# 第一次调用agent.run_loop(我的名字叫小明,max_steps3)# Agent 处理完毕状态清零# 第二次调用agent.run_loop(我叫什么名字,max_steps3)# AgentAgent 不知道你叫什么。因为在第二次调用时AgentState被重置了所有信息清空。它不记得上一次对话里发生了什么。这不奇怪——第六课的状态只在一次循环内有效。循环一结束state.reset()一调全部归零。但真实的助手不是这样的。你告诉 ChatGPT “我正在学 Python”半小时后再问我在学什么它还记得。因为它有跨对话的记忆。这就是第七课要解决的问题。一、上下文 vs 记忆两件完全不同的事在动手之前先理清一个很容易混淆的概念。1.1 上下文Context上下文就是当前 Prompt 里的所有内容。第二课的多轮对话就是上下文——你把历史消息存进self.history每次调用时拼进 promptmessages[{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]:按关键词搜索记忆简单版keywordkeyword.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_contextself.memory.get_all()# 构建记忆上下文字符串ifmemory_context:memory_str你记住了以下关于用户的信息\n\n.join(f-{item}foriteminmemory_context)else:memory_str你目前没有关于用户的记忆。user_promptf你是一个有记忆能力的智能体助手。根据用户输入和你记住的信息来回复。{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}请返回 JSONforattemptinrange(3):responseself.client.chat.completions.create(modelself.model,messages[{role:system,content:self.system_prompt},{role:user,content:user_prompt},],temperature0.0,)textresponse.choices[0].message.content parsedextract_json_from_text(text)ifparsedandreplyinparsed:# 如果 AI 决定保存新记忆自动存入ifparsed.get(save_to_memory):self.memory.add(parsed[save_to_memory])returnparsedreturnNone逐段拆解这段代码的设计考量① 记忆检索——self.memory.get_all()每次对话前先从记忆系统里取出所有存储的事实。目前是全部取出后续可以改成按相关性检索。② 记忆拼入 Promptifmemory_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 次—— 老规矩temperature0.0—— 记忆读写需要确定性四、运行示例4.1 基础场景记住名字fromagent.agentimportAgent agentAgent(modelqwen2.5:7b)# 第一次交互告诉 Agent 你的名字print( 对话 1 )r1agent.run_with_memory(我叫小明是一名后端开发工程师)ifr1:print(fAgent:{r1[reply]})# 第二次交互问它记不记得print(\n 对话 2 )r2agent.run_with_memory(你还记得我的名字和职业吗)ifr2:print(fAgent:{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 )r3agent.run_with_memory(我主要用 Python 和 Go最近在学 AI Agent)ifr3:print(fAgent:{r3[reply]})# 第四次交互综合提问print(\n 对话 4 )r4agent.run_with_memory(帮我推荐一个学习路线结合我的技术栈)ifr4:print(fAgent:{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 过长到时候加截断。记忆太杂导致检索不准到时候加语义检索。但第一步就是把存和取这个基本能力建立起来。七、常见问题QAgent 不保存信息到记忆怎么办A检查几件事① Prompt 里是否有明确的 few-shot 示例教 AI 什么时候该存②save_to_memory字段的验证逻辑是否正确③ 用户输入里是否确实包含值得记住的信息帮我写个函数确实不需要记。给小模型7B加清晰的示例尤其重要。QAgent 记住了错误的信息怎么办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 有什么区别Aself.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 课

更多文章