系列文章导航:AI系列文章导航目录-持续更新中
第17课:深入学习Agent记忆系统
📝 本文摘要:本文从零讲解Agent记忆系统------Agent从"一次性对话"到"持续进化"的关键基础设施。内容包括:①记忆是什么(定义、为什么Agent需要记忆、LLM无状态的本质问题);②记忆在Agent技术栈中的定位(三层架构图解、与上下文窗口的关系);③记忆类型全景(短期记忆/工作记忆/长期记忆/情景记忆,每种的定义、来源、实现方案);④记忆的核心操作(存储/检索/压缩/遗忘四大操作的原理与实现);⑤记忆检索策略(语义相似度/时间衰减/重要性加权/MMR多样性);⑥记忆与规划的协同(Plan-and-Execute如何利用记忆);⑦记忆与反思的协同(Reflexion如何利用记忆存储经验教训);⑧完整实战:从零构建一个有记忆的Agent;⑨工程最佳实践与常见陷阱。适合AI小白从零理解Agent记忆系统的本质、定位和实践。
没有记忆的Agent就像失忆症患者------每次对话都从零开始,犯过的错再犯一遍,学过的知识转头就忘。记忆系统是Agent从"能用"到"好用"、从"一次性"到"可持续"的关键跃升。
一、记忆是什么
1.1 一句话定义
Agent记忆 = Agent跨时间保持和利用信息的能力
类比: Agent记忆之于Agent ≈ 大脑记忆之于人类 ≈ 硬盘之于电脑
展开理解:
人类之所以能持续学习和进步,核心依赖记忆:
- 你记得昨天和同事讨论的方案,今天才能继续推进
- 你记得上次犯的错误,这次才能避免
- 你记得多年积累的专业知识,才能做出专业判断
Agent也一样。一个没有记忆的Agent,每次对话都是"全新的自己"------不知道你是谁、不知道之前聊了什么、不知道上次犯了什么错。这显然不是一个"智能"的体验。
1.2 为什么Agent需要记忆?(LLM无状态的本质问题)
核心问题:LLM本身是无状态的(Stateless)。
这是理解Agent记忆系统的第一个关键认知。很多初学者会误以为"LLM能记住之前的对话",但实际上:
LLM的真相:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
每次调用LLM API,都是一次独立的、无状态的请求。
你以为的:
用户: "我叫张三"
AI: "你好张三"
用户: "我叫什么?"
AI: "你叫张三" ← 你以为AI"记住"了
实际发生的:
第一次请求:
messages = [{"role":"user", "content":"我叫张三"}]
→ LLM回复: "你好张三"
第二次请求:
messages = [
{"role":"user", "content":"我叫张三"}, ← 应用程序把历史重新传了一遍!
{"role":"assistant", "content":"你好张三"}, ← 应用程序把历史重新传了一遍!
{"role":"user", "content":"我叫什么?"}
]
→ LLM回复: "你叫张三"
关键: LLM没有"记住"任何东西!
是应用程序(你写的代码)把历史重新传给了LLM。
LLM只是在"阅读"你传给它的文本,然后做出回应。
类比: LLM就像一个每天醒来都失忆的人,
但你每天早上给他一本日记,他读完日记就"知道"之前发生了什么。
日记 = messages数组 = 你的应用程序管理的"记忆"
这带来了三个根本性问题:
问题1: 上下文窗口有限
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
上下文窗口 = LLM一次能"看到"的最大文本量
模型 上下文窗口 约等于
GPT-4o 128K tokens ~10万字
Claude 3.5 200K tokens ~15万字
GPT-4o-mini 128K tokens ~10万字
Qwen2.5 32K-128K ~2.5-10万字
看起来很大?但实际使用中:
- System Prompt占 2K-5K tokens
- 工具定义占 1K-10K tokens(取决于工具数量)
- 每轮对话约 500-2000 tokens
- 工具返回结果可能很大(一次查询返回几千tokens)
→ 一个复杂Agent,聊20-30轮就可能撑满上下文窗口
→ 超出窗口的内容会被截断,Agent就"失忆"了
问题2: 无法跨会话保持信息
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
场景: 用户今天告诉Agent"我的项目用Go语言"
明天再来问"帮我写个HTTP服务"
没有记忆: Agent不知道你用Go,可能给你写Python
有记忆: Agent从长期记忆中检索到"用户偏好Go",直接写Go
问题3: 无法从经验中学习
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
场景: Agent上次帮你查数据库时用错了表名,你纠正了它
下次遇到类似问题,它又犯同样的错
没有记忆: 每次都是"新手",同样的错反复犯
有记忆: 把"反思"存入记忆,下次检索到"上次用错了表名",避免重复犯错
记忆系统就是解决这三个问题的基础设施:
记忆系统的三大价值:
1. 突破上下文窗口限制
→ 把历史信息压缩/存储到外部,按需检索回来
→ 不需要把所有历史都塞进上下文
2. 实现跨会话的信息持久化
→ 重要信息存入长期记忆(向量数据库/KV存储)
→ 下次会话时检索回来
3. 支持经验积累和持续学习
→ 反思和教训存入记忆
→ 下次遇到类似任务时参考
→ Agent越用越"聪明"
1.3 记忆系统的学术来源
Agent记忆系统的设计灵感来自认知科学对人类记忆的研究:
人类记忆模型 (Atkinson-Shiffrin, 1968):
感觉记忆 → 短期记忆 → 长期记忆
- 感觉记忆: 极短暂(<1秒),如视觉暂留
- 短期记忆: 短暂(20-30秒),容量有限(7±2项)
- 长期记忆: 持久,容量几乎无限
Agent记忆的对应:
- 感觉记忆 → 当前轮次的输入(用户刚说的话)
- 短期记忆 → 对话历史(messages数组)
- 长期记忆 → 持久化存储(向量数据库等)
关键论文:
1. "Generative Agents: Interactive Simulacra of Human Behavior"
(Park et al., 2023, Stanford)
→ 首次在Agent中实现完整的记忆系统(存储/检索/反思)
→ 25个AI角色在虚拟小镇中生活,靠记忆系统维持一致性
2. "Reflexion: Language Agents with Verbal Reinforcement Learning"
(Shinn et al., 2023)
→ 用记忆存储"反思",让Agent从失败中学习
3. "MemGPT: Towards LLMs as Operating Systems"
(Packer et al., 2023)
→ 把LLM的上下文窗口类比为"内存"
→ 长期记忆类比为"硬盘"
→ Agent自己管理"内存"和"硬盘"之间的数据交换
1.4 记忆在Agent技术栈中的定位
┌──────────────────────────────────────────────────────────────────┐
│ 第1层: LLM模型本体(GPT/Claude等) │
│ │
│ 无状态的推理引擎 │
│ 输入: messages(包含记忆注入的上下文) │
│ 输出: 回答 / 工具调用决策 │
│ │
│ ⚠️ LLM本身没有记忆!记忆是外部系统注入的 │
└──────────────────────────────────┬───────────────────────────────┘
│
┌──────────────────────────────────▼───────────────────────────────┐
│ 第2层: Agent编排层(你写的代码) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 记忆管理器(Memory Manager) │ │
│ │ │ │
│ │ 职责: │ │
│ │ 1. 管理短期记忆(对话历史的滑动窗口/摘要) │ │
│ │ 2. 管理工作记忆(当前任务状态、中间结果) │ │
│ │ 3. 与长期记忆交互(存储/检索/压缩/遗忘) │ │
│ │ 4. 在调用LLM前,把相关记忆注入到messages中 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 其他组件: Skill管理器、工具调用、规划器、反思器... │
└───────────┬──────────────────────────────────────┬───────────────┘
│ │
┌───────────▼───────────────┐ ┌───────────▼──────────────┐
│ 第3层-A: 工具层 │ │ 第3层-B: 记忆存储层 │
│ │ │ │
│ MCP Server / 本地工具 │ │ 向量数据库 (Chroma等) │
│ │ │ KV存储 (Redis等) │
│ │ │ 知识图谱 (Neo4j等) │
│ │ │ 文件系统 │
└───────────────────────────┘ └──────────────────────────┘
一句话总结定位:
记忆系统是Agent编排层的核心组件之一。
它负责"在正确的时间,把正确的信息,以正确的方式注入到LLM的上下文中"。
类比:
LLM = 一个超级聪明但失忆的专家
记忆系统 = 这个专家的秘书,负责在开会前把相关资料准备好放在桌上
秘书的工作:
- 知道哪些资料和当前会议相关(检索)
- 把冗长的资料整理成摘要(压缩)
- 会后把重要决定记录下来(存储)
- 过时的资料归档或销毁(遗忘)
二、记忆类型全景
2.1 四种记忆类型概览
┌─────────────────────────────────────────────────────────────────────────┐
│ Agent记忆类型全景 │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ 短期记忆 │ 工作记忆 │ 长期记忆 │ 情景记忆 │
│ Short-term │ Working │ Long-term │ Episodic │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 当前对话历史 │ 当前任务状态 │ 跨会话知识 │ 过往经历片段 │
│ │ │ │ │
│ 类比: │ 类比: │ 类比: │ 类比: │
│ 你正在进行的 │ 你桌上的 │ 你脑中的 │ 你的人生 │
│ 这段对话 │ 草稿纸 │ 专业知识 │ 经历回忆 │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 生命周期: │ 生命周期: │ 生命周期: │ 生命周期: │
│ 单次会话 │ 单次任务 │ 永久 │ 永久 │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 存储位置: │ 存储位置: │ 存储位置: │ 存储位置: │
│ messages数组 │ Agent State │ 向量数据库 │ 向量数据库 │
│ │ │ KV存储 │ + 时间标记 │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ 容量: │ 容量: │ 容量: │ 容量: │
│ 受上下文窗口 │ 受State大小 │ 几乎无限 │ 几乎无限 │
│ 限制 │ 限制 │ │ │
└──────────────┴──────────────┴──────────────┴──────────────┘
2.2 短期记忆(Short-term Memory)
是什么
定义: 当前会话中的对话历史,即messages数组中的内容。
本质: 就是你传给LLM的messages列表。
LLM能"看到"的所有历史信息,都在这里。
类比: 你正在和一个人面对面聊天,
你们之间说过的所有话 = 短期记忆
聊天结束(会话关闭),这些内容就"消失"了(除非你主动记录)
为什么需要管理
问题: 对话越来越长,messages数组越来越大
第1轮: messages = [system, user1, assistant1] → 1K tokens
第5轮: messages = [system, user1, a1, u2, a2, u3, a3, u4, a4, u5, a5] → 5K tokens
第20轮: messages = [system, ...40条消息...] → 20K tokens
第50轮: messages = [system, ...100条消息...] → 50K+ tokens
加上工具定义(5K) + 工具返回结果(可能很大)
→ 很快就会撑满上下文窗口
撑满后会怎样?
- 早期的对话被截断 → Agent"忘记"了早期的信息
- 或者API直接报错 → 程序崩溃
- 或者Token费用暴涨 → 成本不可控
管理策略概览
短期记忆的管理,本质上就是回答一个问题:
"当对话越来越长,我该怎么决定哪些信息保留、哪些信息丢弃?"
三种主流策略:
┌──────────────────────────────────────────────────────────────────────┐
│ 策略 │ 核心思想 │ 适合场景 │
├──────────────────────────────────────────────────────────────────────┤
│ 1. 滑动窗口 │ 只保留最近N轮 │ 简单对话、客服问答 │
│ 2. 摘要压缩 │ 早期对话压缩为摘要 │ 大多数生产场景(推荐) │
│ 3. Token预算管理 │ 按Token预算精确裁剪│ 对成本敏感的生产环境 │
└──────────────────────────────────────────────────────────────────────┘
实际生产中,这三种策略经常组合使用:
Token预算管理(控制总量)+ 摘要压缩(保留关键信息)+ 滑动窗口(保证最近对话完整)
策略1:滑动窗口(Sliding Window)
原理
最简单直观的策略: 只保留最近N轮对话,丢弃更早的。
类比: 就像一个固定大小的窗口在对话历史上滑动,
窗口内的保留,窗口外的丢弃。
对话历史: [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10]
窗口大小=3轮(6条): ↑─────────────── 保留 ────────────────↑
结果: messages = [system_prompt, msg5, msg6, msg7, msg8, msg9, msg10]
丢弃: msg1 ~ msg4 直接删除,不做任何处理
适用场景
✅ 适合:
- 简单的问答对话(每轮独立,不依赖早期信息)
- 客服场景(用户通常只关心当前问题)
- 原型开发阶段(快速实现,后续再优化)
- 对话轮次少的场景(如一次性任务Agent)
❌ 不适合:
- 长期项目协作(用户第1轮说的需求,第20轮还需要引用)
- 需要记住用户偏好的场景
- 多步骤复杂任务(中间步骤的结果可能在后面用到)
实现代码
python
from openai import OpenAI
client = OpenAI()
def sliding_window(messages: list, max_rounds: int = 10) -> list:
"""
滑动窗口策略: 只保留最近N轮对话
Args:
messages: 完整的对话历史
max_rounds: 保留的最大轮次(1轮 = 1条user + 1条assistant)
Returns:
裁剪后的messages列表
"""
# 始终保留system prompt(Agent的身份设定不能丢)
system_msgs = [m for m in messages if m["role"] == "system"]
non_system = [m for m in messages if m["role"] != "system"]
# 每轮 = 1条user + 1条assistant(大约)
max_messages = max_rounds * 2
if len(non_system) > max_messages:
non_system = non_system[-max_messages:]
return system_msgs + non_system
# 使用示例
messages = [
{"role": "system", "content": "你是一个编程助手。"},
{"role": "user", "content": "帮我写一个排序函数"}, # 第1轮
{"role": "assistant", "content": "好的,这是冒泡排序..."},
{"role": "user", "content": "换成快排"}, # 第2轮
{"role": "assistant", "content": "好的,这是快排..."},
{"role": "user", "content": "加上注释"}, # 第3轮
{"role": "assistant", "content": "好的,已加注释..."},
{"role": "user", "content": "再加上类型提示"}, # 第4轮(当前)
]
# 只保留最近2轮
trimmed = sliding_window(messages, max_rounds=2)
# 结果: [system, 第3轮user, 第3轮assistant, 第4轮user]
# 丢失: 第1轮和第2轮的内容(Agent不再知道用户最初要的是排序函数)
优缺点总结
优点:
✓ 实现极其简单,几行代码搞定
✓ 效果可预测(保留N轮就是N轮)
✓ 内存和Token消耗可控
✓ 不需要额外的LLM调用(零额外成本)
缺点:
✗ 早期重要信息会永久丢失
✗ 没有"重要性"判断,重要和不重要的消息一视同仁
✗ 窗口大小难以确定(太小丢信息,太大浪费Token)
✗ 不适合需要长期上下文的场景
策略2:摘要压缩(Summary Compression)
原理
核心思想: 当对话超过阈值时,不是直接丢弃早期对话,
而是用LLM把早期对话"压缩"为一段简洁的摘要。
类比: 就像你读完一本书后写的读书笔记。
原书可能有300页,但笔记只有2页,却保留了最关键的信息。
对话历史: [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8, msg9, msg10]
阈值=5轮: ↑────── 压缩为摘要 ──────↑ ↑────── 保留原样 ──────↑
结果: messages = [
system_prompt(末尾拼接了摘要),
msg5, msg6, msg7, msg8, msg9, msg10 ← 最近几轮保持原样
]
工作流程:
1. 检测: 对话轮次是否超过阈值?
2. 分割: 把对话分为"需要压缩的部分"和"保留原样的部分"
3. 压缩: 调用LLM,把早期对话生成一段摘要
4. 注入: 把摘要拼接到system prompt末尾
5. 组装: 摘要 + 最近几轮原始对话 = 新的messages
摘要注入位置的选择
摘要生成后,放在messages的哪个位置?这是一个重要的设计决策。
┌─────────────────────────────────────────────────────────────────────────┐
│ 方式 │ 做法 │ 推荐度 │
├─────────────────────────────────────────────────────────────────────────┤
│ 拼接到system prompt末尾 │ 身份设定+摘要合为一条│ ⭐⭐⭐⭐⭐ 推荐 │
│ 伪造user+assistant消息对 │ 用对话形式注入摘要 │ ⭐⭐⭐⭐ 可用 │
│ 使用developer role │ OpenAI专属role │ ⭐⭐⭐ 仅OpenAI │
│ 新增一条system消息 │ 多条system消息 │ ⭐⭐ 不推荐 │
└─────────────────────────────────────────────────────────────────────────┘
推荐方式: 拼接到system prompt末尾
messages = [
{"role": "system", "content": "你是一个全栈开发助手。\n\n## 对话历史摘要\n用户叫张三,在做一个Go项目..."},
user_msg_8, ← 最近几轮保持原样
assistant_8,
user_msg_9,
assistant_9,
user_msg_10 ← 当前消息
]
⚠️ 为什么不推荐新增一条system消息?
1. system role 应该是一条完整的消息,包含身份设定 + 动态上下文
2. 多条system消息在不同模型中的行为不一致(有的模型只看第一条,有的会合并)
3. 拼接在一起更容易管理Token预算(一条消息的Token好计算)
⚠️ 伪造user+assistant消息对的做法:
messages = [
{"role": "system", "content": "你是一个全栈开发助手。"},
{"role": "user", "content": "[对话历史摘要] 用户叫张三,Go开发者,项目用PostgreSQL..."},
{"role": "assistant", "content": "好的,我已了解之前的对话背景。"},
user_msg_8, ← 真正的当前对话
...
]
这种方式的好处是不修改system prompt,但缺点是"伪造"了一轮对话,语义上不够干净。
⚠️ OpenAI的developer role(2024年底新增):
messages = [
{"role": "system", "content": "你是一个全栈开发助手。"},
{"role": "developer", "content": "对话摘要: 用户叫张三,Go开发者..."},
{"role": "user", "content": "帮我写一个HTTP接口"},
]
这是最规范的做法,但目前仅OpenAI API支持,其他模型不兼容。
摘要Prompt的设计
摘要的质量直接决定了Agent"记忆"的质量。一个好的摘要Prompt应该:
1. 明确保留什么:
- 用户的身份信息(姓名、角色、背景)
- 用户的偏好和约束("我只用TypeScript"、"不要写注释")
- 重要的决定和结论("最终选了方案B")
- 关键的事实和数据("数据库用的PostgreSQL 15")
- 未完成的任务和待办事项
2. 明确丢弃什么:
- 寒暄和客套话
- 已经被推翻的方案
- 中间的试错过程(只保留最终结论)
- 重复的信息
3. 格式要求:
- 结构化(用列表或分类)
- 简洁(控制在200-500 tokens以内)
- 可操作(读完摘要就能继续工作)
适用场景
✅ 适合:
- 长期对话(几十轮以上)
- 项目协作场景(需要记住早期的需求和决定)
- 需要记住用户偏好的场景
- 大多数生产环境(推荐作为默认策略)
❌ 不适合:
- 对话内容高度精确(如法律文书、代码逐行审查)--- 摘要可能丢失关键细节
- 对延迟极度敏感的场景 --- 生成摘要需要额外的LLM调用
- Token预算极其紧张 --- 摘要本身也占Token
实现代码
python
from openai import OpenAI
client = OpenAI()
def summarize_and_compress(messages: list, max_rounds_to_keep: int = 5) -> list:
"""
摘要压缩策略: 超出阈值时,把早期对话压缩为摘要
Args:
messages: 完整的对话历史
max_rounds_to_keep: 保留原样的最近轮次数
Returns:
压缩后的messages列表
"""
system_msgs = [m for m in messages if m["role"] == "system"]
non_system = [m for m in messages if m["role"] != "system"]
if len(non_system) <= max_rounds_to_keep * 2:
return messages # 还没超阈值,不需要压缩
# 分割: 需要压缩的部分 vs 保留原样的部分
to_compress = non_system[:-(max_rounds_to_keep * 2)]
to_keep = non_system[-(max_rounds_to_keep * 2):]
# 用LLM生成摘要
compress_text = "\n".join([f"{m['role']}: {m['content']}" for m in to_compress])
summary_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""请将以下对话历史压缩为一段简洁的摘要。
要求:
1. 保留所有关键信息: 用户身份、偏好、重要决定、关键事实、未完成的任务
2. 丢弃: 寒暄、已被推翻的方案、重复信息
3. 格式: 用结构化列表,控制在200-500 tokens以内
4. 语气: 客观陈述,不加评价
对话历史:
{compress_text}
摘要:"""
}],
temperature=0.0
)
summary = summary_response.choices[0].message.content
# 组装新的messages(将摘要拼接到system prompt末尾)
# ⚠️ 注意: 不要新增一条system消息,而是把摘要追加到原有system prompt中
# 原因: system role应该是Agent的固定身份设定,摘要是动态上下文,
# 拼接在一起比分成多条system消息更规范、更可控
if system_msgs:
enhanced_system = {
"role": "system",
"content": system_msgs[0]["content"] + f"\n\n## 对话历史摘要\n{summary}"
}
return [enhanced_system] + to_keep
else:
# 没有system prompt时,用user消息注入摘要
summary_msg = {"role": "user", "content": f"[对话历史摘要] {summary}"}
ack_msg = {"role": "assistant", "content": "好的,我已了解之前的对话背景。"}
return [summary_msg, ack_msg] + to_keep
# 使用示例: 模拟一个超长对话
messages = [
{"role": "system", "content": "你是一个全栈开发助手。"},
# 假设前面已经有20轮对话...
{"role": "user", "content": "我叫张三,我在做一个Go项目"},
{"role": "assistant", "content": "好的张三,Go项目有什么需要帮助的?"},
{"role": "user", "content": "数据库用PostgreSQL"},
{"role": "assistant", "content": "好的,PostgreSQL是个好选择..."},
{"role": "user", "content": "帮我设计一个用户表"},
{"role": "assistant", "content": "好的,这是用户表设计..."},
{"role": "user", "content": "加上软删除"},
{"role": "assistant", "content": "好的,已加上deleted_at字段..."},
{"role": "user", "content": "再帮我写CRUD接口"},
{"role": "assistant", "content": "好的,这是CRUD接口..."},
{"role": "user", "content": "接口加上分页"}, # 当前消息
]
# 压缩后,早期对话变成摘要,最近3轮保持原样
compressed = summarize_and_compress(messages, max_rounds_to_keep=3)
# 结果: [
# {"role": "system", "content": "你是一个全栈开发助手。\n\n## 对话历史摘要\n- 用户: 张三\n- 项目: Go + PostgreSQL\n- 已完成: 用户表设计(含软删除)"},
# 第3轮user, 第3轮assistant, ← 保留原样
# 第4轮user, 第4轮assistant,
# 第5轮user ← 当前消息
# ]
进阶:增量摘要(Incremental Summarization)
上面的实现有一个问题: 每次压缩都要把所有早期对话重新生成摘要。
如果对话有100轮,每次都压缩前95轮,成本很高。
更好的做法: 增量摘要 --- 在已有摘要的基础上,只压缩新增的部分。
工作流程:
第10轮触发压缩: 压缩第1-5轮 → 生成摘要v1
第15轮触发压缩: 摘要v1 + 第6-10轮 → 生成摘要v2(在v1基础上追加)
第20轮触发压缩: 摘要v2 + 第11-15轮 → 生成摘要v3(在v2基础上追加)
这样每次只需要处理5轮新对话,而不是重新处理所有历史。
python
def incremental_summarize(
existing_summary: str,
new_messages: list,
client: OpenAI
) -> str:
"""
增量摘要: 在已有摘要基础上,融合新的对话内容
Args:
existing_summary: 已有的摘要(可能为空)
new_messages: 需要新压缩的消息
client: OpenAI客户端
Returns:
更新后的摘要
"""
new_text = "\n".join([f"{m['role']}: {m['content']}" for m in new_messages])
if existing_summary:
prompt = f"""你有一份已有的对话摘要,现在有新的对话内容需要融合进去。
已有摘要:
{existing_summary}
新的对话内容:
{new_text}
请生成更新后的摘要:
1. 保留已有摘要中仍然有效的信息
2. 融入新对话中的关键信息
3. 如果新对话推翻了之前的决定,更新摘要(删除旧的,加入新的)
4. 控制总长度在200-500 tokens以内
更新后的摘要:"""
else:
prompt = f"""请将以下对话压缩为一段简洁的摘要,保留所有关键信息:
{new_text}
摘要:"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.0
)
return response.choices[0].message.content
优缺点总结
优点:
✓ 保留了关键信息,不会像滑动窗口那样完全丢失
✓ 有效控制Token消耗(摘要通常只有200-500 tokens)
✓ 适用范围广,大多数场景都能用
✓ 增量摘要可以进一步降低成本
缺点:
✗ 摘要可能丢失细节(LLM判断"不重要"的信息可能实际很重要)
✗ 需要额外的LLM调用(增加延迟和成本)
✗ 摘要质量依赖Prompt设计(Prompt不好 → 摘要质量差)
✗ 不可逆 --- 一旦压缩,原始对话就丢失了(除非另外存储)
策略3:Token预算管理(Token Budget Management)
原理
核心思想: 设定一个固定的Token预算(如8000 tokens用于对话历史),
从最新的消息开始往回填充,直到预算用完为止。
与滑动窗口的区别:
滑动窗口: 按"轮次"裁剪(保留最近10轮)
Token预算: 按"Token数"裁剪(保留最近8000 tokens)
为什么按Token比按轮次更精确?
- 不同轮次的Token消耗差异巨大
- 一轮简单问答可能只有50 tokens
- 一轮代码生成可能有2000 tokens
- 按轮次裁剪无法精确控制成本
类比:
滑动窗口 = "行李箱只能放10件衣服"(不管衣服大小)
Token预算 = "行李箱只能装20公斤"(按重量精确控制)
Token预算的分配(典型配置):
┌─────────────────────────────────────────────────────┐
│ 模型上下文窗口: 128K tokens │
├─────────────────────────────────────────────────────┤
│ System Prompt: 2,000 tokens (固定) │
│ 工具定义(tools): 3,000 tokens (固定) │
│ 对话历史预算: 8,000 tokens (本策略管理) │
│ 当前用户消息: 2,000 tokens (预留) │
│ 模型输出预留: 4,000 tokens (预留) │
│ 安全余量: 1,000 tokens │
├─────────────────────────────────────────────────────┤
│ 总计: 20,000 tokens │
│ 剩余可用: 108,000 tokens (可按需扩展) │
└─────────────────────────────────────────────────────┘
注: 虽然模型窗口很大,但控制Token使用可以显著降低成本和延迟。
适用场景
✅ 适合:
- 对成本敏感的生产环境(按Token计费,需要精确控制)
- 需要精确控制延迟的场景(Token越多,推理越慢)
- 对话内容长度差异大的场景(有的轮次很短,有的很长)
- 需要与其他策略组合使用的场景(如: Token预算 + 摘要压缩)
❌ 不适合:
- 原型开发阶段(过度优化,增加复杂度)
- 对话轮次少的场景(没必要精确管理)
实现代码
python
import tiktoken
from openai import OpenAI
client = OpenAI()
def token_budget_trim(messages: list, max_tokens: int = 8000, model: str = "gpt-4o-mini") -> list:
"""
Token预算管理: 基于Token预算精确裁剪对话历史
Args:
messages: 完整的对话历史
max_tokens: 对话历史的Token预算上限
model: 模型名称(用于选择正确的tokenizer)
Returns:
裁剪后的messages列表
"""
encoding = tiktoken.encoding_for_model(model)
def count_tokens(msg):
"""计算单条消息的Token数"""
# 每条消息有固定的格式开销(role标记等)
tokens = 4 # <|im_start|>{role}\n ... \n<|im_end|>
tokens += len(encoding.encode(msg.get("content", "")))
if msg.get("name"):
tokens += len(encoding.encode(msg["name"])) + 1
return tokens
# system prompt始终保留(不计入对话历史预算)
system_msgs = [m for m in messages if m["role"] == "system"]
non_system = [m for m in messages if m["role"] != "system"]
# 计算可用预算(减去system prompt占用)
system_tokens = sum(count_tokens(m) for m in system_msgs)
remaining_budget = max_tokens - system_tokens
# 从最新的消息开始往回填充(优先保留最近的对话)
kept_messages = []
for msg in reversed(non_system):
msg_tokens = count_tokens(msg)
if remaining_budget - msg_tokens < 0:
break # 预算用完,停止
kept_messages.insert(0, msg)
remaining_budget -= msg_tokens
return system_msgs + kept_messages
# 使用示例
messages = [
{"role": "system", "content": "你是一个编程助手。"},
{"role": "user", "content": "帮我写一个很长的函数..." * 100}, # 很长的消息
{"role": "assistant", "content": "好的,这是实现..." * 200}, # 很长的回复
{"role": "user", "content": "简化一下"}, # 短消息
{"role": "assistant", "content": "好的,简化版..."}, # 短回复
{"role": "user", "content": "加个注释"}, # 当前消息
]
# 预算8000 tokens,从最新开始往回填
trimmed = token_budget_trim(messages, max_tokens=8000)
# 如果前两条消息太长超出预算,只会保留后面的短消息
进阶:带优先级的Token预算
python
def priority_token_budget(
messages: list,
max_tokens: int = 8000,
model: str = "gpt-4o-mini",
priority_keywords: list = None
) -> list:
"""
带优先级的Token预算管理:
不是简单地从最新往回填,而是优先保留"重要"的消息
重要性判断:
1. 最近的消息 > 早期的消息
2. 包含关键词的消息 > 普通消息
3. 用户消息 > 助手消息(用户的需求比AI的回复更重要)
"""
encoding = tiktoken.encoding_for_model(model)
priority_keywords = priority_keywords or ["需求", "决定", "确认", "重要", "必须"]
def count_tokens(msg):
return len(encoding.encode(msg.get("content", ""))) + 4
def calc_priority(msg, index, total):
"""计算消息的优先级分数(越高越重要)"""
score = 0
# 位置分: 越新的消息分数越高
score += (index / total) * 50
# 角色分: 用户消息更重要
if msg["role"] == "user":
score += 20
# 关键词分: 包含关键词的更重要
content = msg.get("content", "")
for kw in priority_keywords:
if kw in content:
score += 10
break
return score
system_msgs = [m for m in messages if m["role"] == "system"]
non_system = [m for m in messages if m["role"] != "system"]
system_tokens = sum(count_tokens(m) for m in system_msgs)
remaining_budget = max_tokens - system_tokens
# 计算每条消息的优先级
scored_messages = []
for i, msg in enumerate(non_system):
score = calc_priority(msg, i, len(non_system))
tokens = count_tokens(msg)
scored_messages.append((score, i, msg, tokens))
# 按优先级排序(高优先级先填入)
scored_messages.sort(key=lambda x: x[0], reverse=True)
# 按优先级填充预算
selected_indices = set()
for score, idx, msg, tokens in scored_messages:
if remaining_budget - tokens >= 0:
selected_indices.add(idx)
remaining_budget -= tokens
# 按原始顺序组装(保持对话的时间顺序)
kept = [msg for i, msg in enumerate(non_system) if i in selected_indices]
return system_msgs + kept
优缺点总结
优点:
✓ 精确控制Token使用和成本
✓ 不需要额外的LLM调用(纯计算,零额外成本)
✓ 可以与其他策略组合(如超出预算时触发摘要压缩)
✓ 带优先级版本可以保留重要信息
缺点:
✗ 实现比滑动窗口复杂(需要Token计数库)
✗ 基础版本仍然会丢失早期信息(和滑动窗口一样的问题)
✗ Token计数有一定开销(虽然比LLM调用小得多)
✗ 不同模型的tokenizer不同,需要适配
策略组合:生产环境最佳实践
实际生产中,单一策略往往不够用。推荐的组合方式:
┌─────────────────────────────────────────────────────────────────────┐
│ 生产环境推荐组合 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Token预算检查 │
│ ↓ 超出预算? │
│ Step 2: 摘要压缩(把早期对话压缩为摘要) │
│ ↓ 压缩后仍超出? │
│ Step 3: 滑动窗口(强制只保留最近N轮) │
│ ↓ │
│ 最终结果: 一定在Token预算内的messages │
│ │
└─────────────────────────────────────────────────────────────────────┘
python
def production_memory_manager(
messages: list,
token_budget: int = 8000,
summary_threshold: int = 10, # 超过10轮触发摘要
max_keep_rounds: int = 5, # 摘要时保留最近5轮
hard_limit_rounds: int = 20, # 硬上限: 无论如何不超过20轮
model: str = "gpt-4o-mini"
) -> list:
"""
生产环境的短期记忆管理器: 组合三种策略
执行顺序:
1. 硬上限检查(滑动窗口兜底)
2. 摘要压缩(超过阈值时压缩早期对话)
3. Token预算裁剪(确保不超出预算)
"""
# Step 1: 硬上限兜底(防止极端情况)
result = sliding_window(messages, max_rounds=hard_limit_rounds)
# Step 2: 摘要压缩(如果轮次超过阈值)
non_system = [m for m in result if m["role"] != "system"]
if len(non_system) > summary_threshold * 2:
result = summarize_and_compress(result, max_rounds_to_keep=max_keep_rounds)
# Step 3: Token预算裁剪(确保不超出预算)
result = token_budget_trim(result, max_tokens=token_budget, model=model)
return result
2.3 工作记忆(Working Memory)
是什么
定义: 当前任务执行过程中的中间状态、草稿和临时数据。
本质: Agent在执行一个多步骤任务时,需要"记住"中间结果。
比如第1步查到了订单号,第3步需要用这个订单号去退款。
类比: 你在做一道复杂数学题时,
在草稿纸上写下的中间计算结果 = 工作记忆
题目做完,草稿纸就可以扔了
与短期记忆的区别:
短期记忆 = 对话历史(用户说了什么、AI回了什么)
工作记忆 = 任务状态(当前走到哪一步、中间结果是什么)
短期记忆是"对话级"的,工作记忆是"任务级"的。
一次对话中可能有多个任务,每个任务有自己的工作记忆。
工作记忆包含什么
1. 当前任务的执行计划(如果有规划的话)
"Step 1: 查订单 ✅ → Step 2: 检查退款条件 → Step 3: 创建退款"
2. 中间结果
"订单号: ORD-12345, 金额: 299元, 状态: 已完成"
3. 当前步骤
"正在执行Step 2"
4. 草稿/思考过程
"用户说要退款,但订单已超过7天,需要确认是否符合特殊退款条件"
5. 错误记录
"第一次查询用了错误的表名,已修正"
实现代码
python
from typing import TypedDict, Annotated, Optional
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
# ═══════════════════════════════════════════
# 方式1: 在LangGraph State中定义工作记忆字段
# ═══════════════════════════════════════════
class AgentState(TypedDict):
# 短期记忆: 对话历史
messages: Annotated[list, add_messages]
# 工作记忆: 任务执行状态
plan: Optional[list[str]] # 执行计划
current_step: int # 当前步骤索引
intermediate_results: dict # 中间结果 {step_id: result}
scratchpad: str # 思考草稿
task_status: str # "planning" / "executing" / "reflecting" / "done"
error_log: list[str] # 错误记录
# ═══════════════════════════════════════════
# 方式2: 显式的工作记忆管理器
# ═══════════════════════════════════════════
class WorkingMemory:
"""工作记忆管理器"""
def __init__(self):
self.plan = [] # 执行计划
self.current_step = 0 # 当前步骤
self.results = {} # 中间结果
self.scratchpad = "" # 草稿
self.errors = [] # 错误记录
def set_plan(self, steps: list[str]):
"""设置执行计划"""
self.plan = steps
self.current_step = 0
self.results = {}
print(f"📋 计划设定: {len(steps)}步")
for i, step in enumerate(steps):
print(f" Step {i+1}: {step}")
def advance_step(self, result: str):
"""完成当前步骤,记录结果,前进到下一步"""
self.results[self.current_step] = result
self.current_step += 1
remaining = len(self.plan) - self.current_step
print(f" ✅ Step {self.current_step} 完成,剩余 {remaining} 步")
def record_error(self, error: str):
"""记录错误"""
self.errors.append(f"Step {self.current_step + 1}: {error}")
def get_context(self) -> str:
"""生成工作记忆的文本表示,用于注入到LLM上下文"""
context = "## 当前任务状态\n"
if self.plan:
context += f"计划: {len(self.plan)}步\n"
for i, step in enumerate(self.plan):
status = "✅" if i < self.current_step else ("▶️" if i == self.current_step else "⬜")
context += f" {status} Step {i+1}: {step}\n"
if i in self.results:
context += f" 结果: {self.results[i][:100]}\n"
if self.errors:
context += f"\n⚠️ 错误记录:\n"
for err in self.errors:
context += f" - {err}\n"
if self.scratchpad:
context += f"\n📝 思考草稿: {self.scratchpad}\n"
return context
def is_done(self) -> bool:
"""任务是否完成"""
return self.current_step >= len(self.plan)
def reset(self):
"""重置工作记忆(任务完成后)"""
self.__init__()
# 使用示例
wm = WorkingMemory()
wm.set_plan(["查询订单状态", "检查退款条件", "创建退款申请", "发送通知"])
wm.advance_step("订单ORD-12345, 金额299元, 状态:已完成")
wm.advance_step("符合7天无理由退款条件")
print(wm.get_context())
深度辨析:短期记忆 vs 工作记忆
很多人会混淆短期记忆和工作记忆,因为它们看起来都是"当前正在用的信息"。
但它们的本质区别在于: 谁在管理、管理什么、生命周期多长。
一句话区分:
短期记忆 = Agent与LLM的一次对话会话中产生的消息记录(messages数组)
工作记忆 = Agent自身维护的、跨越多次LLM调用的任务执行状态
┌─────────────────────────────────────────────────────────────────────────────┐
│ 短期记忆 vs 工作记忆 对比 │
├──────────────────┬──────────────────────────┬───────────────────────────────┤
│ 维度 │ 短期记忆 │ 工作记忆 │
├──────────────────┼──────────────────────────┼───────────────────────────────┤
│ 本质 │ 对话历史 │ 任务执行状态 │
│ 内容 │ user/assistant消息列表 │ 计划、中间结果、当前步骤 │
│ 管理者 │ LLM API(messages参数) │ Agent编排层(你的代码) │
│ 生命周期 │ 一次会话(session) │ 一次任务(task) │
│ 与LLM的关系 │ 直接传给LLM │ Agent决定何时、如何注入LLM │
│ 粒度 │ 一次完整的对话 │ 一个任务的多个步骤 │
│ 典型实现 │ messages数组 │ State对象 / 状态机 │
└──────────────────┴──────────────────────────┴───────────────────────────────┘
关键洞察: 一个任务可能需要多次LLM调用,每次调用都是独立的"短期记忆"
┌─────────────────────────────────────────────────────────────────────┐
│ 用户请求: "帮我退款" │
│ │
│ Agent编排层(工作记忆在这里) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 工作记忆 State: │ │
│ │ plan: ["查订单", "检查条件", "创建退款", "通知用户"] │ │
│ │ current_step: 0 │ │
│ │ results: {} │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Step 1 │ │ Step 2 │ │ Step 3 │ │ Step 4 │ │
│ │ 调用LLM │ → │ 调用LLM │ → │ 调用LLM │ → │ 调用LLM │ │
│ │ (独立的 │ │ (独立的 │ │ (独立的 │ │ (独立的 │ │
│ │ 短期记忆)│ │ 短期记忆)│ │ 短期记忆)│ │ 短期记忆)│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ result存入 result存入 result存入 任务完成 │
│ 工作记忆 工作记忆 工作记忆 │
└─────────────────────────────────────────────────────────────────────┘
每个Step与LLM的交互:
- 有自己独立的messages数组(短期记忆)
- Agent从工作记忆中取出需要的上下文,注入到这次调用的messages中
- LLM返回结果后,Agent把结果写回工作记忆
- 然后进入下一个Step,开始新的LLM调用(新的短期记忆)
以LangGraph为例,理解工作记忆的本质:
LangGraph的核心机制:
- State(状态)= 工作记忆
- Node(节点)= 每个步骤(每个节点可能调用一次LLM)
- Edge(边)= 步骤之间的流转逻辑
┌─────────────────────────────────────────────────────────────────┐
│ LangGraph 的 State 就是工作记忆 │
│ │
│ class AgentState(TypedDict): │
│ messages: list # ← 这是短期记忆(对话历史) │
│ plan: list[str] # ← 这是工作记忆(执行计划) │
│ current_step: int # ← 这是工作记忆(当前步骤) │
│ order_info: dict # ← 这是工作记忆(中间结果) │
│ task_status: str # ← 这是工作记忆(任务状态) │
│ │
│ 注意: messages字段虽然也在State里,但它的角色是"短期记忆"。 │
│ State中除了messages之外的字段,才是真正的"工作记忆"。 │
└─────────────────────────────────────────────────────────────────┘
LangGraph的节点编排流程:
START → [规划节点] → [执行节点1] → [执行节点2] → [反思节点] → END
│ │ │ │
▼ ▼ ▼ ▼
调用LLM 调用LLM 调用LLM 调用LLM
生成计划 查询订单 检查条件 总结结果
│ │ │ │
▼ ▼ ▼ ▼
写入State 写入State 写入State 写入State
(plan字段) (order_info) (check_result) (final_answer)
每个节点与LLM的交互都是独立的:
- 节点从State中读取需要的信息
- 构造自己的messages数组(可能只包含system + 当前任务描述 + 相关上下文)
- 调用LLM获取结果
- 把结果写回State
- LangGraph根据Edge决定下一个节点
为什么这个区分很重要?
1. 架构设计层面:
- 短期记忆: 你不需要自己管理,LLM API天然支持(传messages就行)
- 工作记忆: 你必须自己设计和管理(State结构、读写逻辑、生命周期)
2. 信息流向层面:
- 短期记忆: 线性累积(每轮对话追加到messages末尾)
- 工作记忆: 结构化读写(Agent决定在哪个步骤读取/写入哪些字段)
3. 与LLM的关系:
- 短期记忆: 完整传给LLM(LLM看到所有对话历史)
- 工作记忆: 选择性注入(Agent只把当前步骤需要的信息传给LLM)
4. 实际开发中的体现:
- 如果你只是做一个简单的聊天机器人 → 只需要短期记忆(messages管理)
- 如果你要做一个能执行多步骤任务的Agent → 必须设计工作记忆
常见误区:
✗ "把所有信息都塞进messages就行了"
→ 这样每个步骤的LLM调用都会看到所有历史,Token浪费严重
✓ "Agent编排层维护State,每次调用LLM时只注入必要的上下文"
→ 这才是工作记忆的正确用法
总结:
短期记忆 → "LLM在一次调用中能看到什么"
工作记忆 → "Agent在多次LLM调用之间记住什么"
短期记忆是LLM的能力边界(上下文窗口)
工作记忆是Agent的编排能力(你写的代码决定的)
没有工作记忆的Agent = 只能做单轮问答
有工作记忆的Agent = 能执行复杂的多步骤任务
2.4 长期记忆(Long-term Memory)
是什么
定义: 跨会话持久化存储的知识和信息,不会因为会话结束而消失。
本质: 存储在外部系统(向量数据库、KV存储等)中的信息,
Agent在需要时通过检索获取。
类比: 你大脑中的长期知识------
你知道Python怎么写、你记得同事的名字、你知道公司的业务规则。
这些知识不会因为你睡了一觉就消失。
与短期记忆的关键区别:
短期记忆: 存在messages数组中,会话结束就没了
长期记忆: 存在外部数据库中,永久保存,按需检索
长期记忆存什么
1. 用户偏好和个人信息
"用户叫张三,是Go开发者,喜欢简洁的代码风格"
"用户的项目用的是PostgreSQL数据库"
2. 领域知识和业务规则
"退款规则: 7天内无理由退款,7-30天需要审批"
"公司的代码规范: 函数名用驼峰命名"
3. 历史交互的关键信息
"2024-01-15: 用户反馈查询接口太慢,建议加索引"
"2024-02-01: 帮用户重构了订单模块"
4. 反思和经验教训(Reflexion模式)
"上次查用户表时用了user_name字段,但实际字段名是username"
"处理大文件时不要一次性读入内存,应该流式处理"
5. 实体和关系(知识图谱形式)
"张三 → 负责 → 订单系统"
"订单系统 → 依赖 → 支付服务"
存储方案概述
长期记忆需要持久化存储在外部系统中,常见的存储方案包括:
- 向量数据库(Chroma, Pinecone, Milvus)→ 语义检索,适合非结构化文本
- KV存储(Redis, MongoDB, SQLite)→ 精确查找,适合结构化事实
- 知识图谱(Neo4j, NebulaGraph)→ 关系查询,适合实体关系
- 文件系统(本地文件 + grep)→ 全文搜索,适合大文档/日志
最常用的方案: 向量数据库
原因: Agent的大部分记忆都是非结构化文本,语义检索最灵活
关于存储方案的详细对比、本地存储vs分布式存储的选型、
以及不同记忆类型的具体技术选型建议,详见 → 3.1 存储(Store)章节
实现代码
python
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
from datetime import datetime
import json
class LongTermMemory:
"""长期记忆管理器(基于向量数据库)"""
def __init__(self, persist_directory: str = "./agent_long_term_memory"):
self.embeddings = OpenAIEmbeddings()
self.vectorstore = Chroma(
embedding_function=self.embeddings,
persist_directory=persist_directory,
collection_name="agent_memory"
)
# ---- 存储 ----
def store(self, content: str, memory_type: str = "general",
importance: float = 0.5, metadata: dict = None) -> None:
"""存储一条记忆"""
meta = {
"type": memory_type, # "preference" / "fact" / "reflection" / "interaction"
"importance": importance, # 0.0 - 1.0
"timestamp": datetime.now().isoformat(),
**(metadata or {})
}
doc = Document(page_content=content, metadata=meta)
self.vectorstore.add_documents([doc])
def store_user_preference(self, preference: str) -> None:
"""存储用户偏好"""
self.store(preference, memory_type="preference", importance=0.8)
def store_fact(self, fact: str) -> None:
"""存储事实信息"""
self.store(fact, memory_type="fact", importance=0.6)
def store_reflection(self, task: str, reflection: str, success: bool) -> None:
"""存储反思(Reflexion模式)"""
content = f"任务: {task}\n反思: {reflection}\n结果: {'成功' if success else '失败'}"
self.store(content, memory_type="reflection", importance=0.9)
# ---- 检索 ----
def retrieve(self, query: str, top_k: int = 5,
memory_type: str = None) -> list[Document]:
"""检索相关记忆"""
# 如果指定了类型,用过滤条件
filter_dict = {"type": memory_type} if memory_type else None
results = self.vectorstore.similarity_search(
query, k=top_k, filter=filter_dict
)
return results
def retrieve_as_text(self, query: str, top_k: int = 5) -> str:
"""检索并格式化为文本(直接注入到Prompt中)"""
docs = self.retrieve(query, top_k)
if not docs:
return ""
lines = []
for doc in docs:
meta = doc.metadata
type_label = {"preference": "偏好", "fact": "事实",
"reflection": "经验", "interaction": "历史"}.get(meta.get("type", ""), "记忆")
lines.append(f"[{type_label}] {doc.page_content}")
return "\n".join(lines)
# ---- 统计 ----
def count(self) -> int:
"""记忆总数"""
return self.vectorstore._collection.count()
# 使用示例
memory = LongTermMemory()
# 存储
memory.store_user_preference("用户偏好Go语言,代码风格简洁")
memory.store_user_preference("用户使用PostgreSQL数据库")
memory.store_fact("用户的项目名叫order-service,是一个订单微服务")
memory.store_reflection(
task="查询用户表",
reflection="字段名是username而不是user_name,下次注意",
success=False
)
# 检索
results = memory.retrieve_as_text("帮我写一个数据库查询")
print(results)
# 输出:
# [偏好] 用户偏好Go语言,代码风格简洁
# [偏好] 用户使用PostgreSQL数据库
# [事实] 用户的项目名叫order-service,是一个订单微服务
# [经验] 任务: 查询用户表 反思: 字段名是username而不是user_name...
2.5 情景记忆(Episodic Memory)
是什么
定义: 对过往完整经历的记忆,包含时间、地点、过程和结果。
与长期记忆的区别:
长期记忆: 存储的是"知识点"(零散的事实和偏好)
情景记忆: 存储的是"完整故事"(一次完整的任务执行经历)
类比:
长期记忆: "我知道Python的list是可变的" ← 一个知识点
情景记忆: "上周三下午,我帮用户调试了一个list引用bug,
先查了代码,发现是浅拷贝问题,改成deepcopy后解决了" ← 一段完整经历
为什么需要情景记忆:
当Agent遇到类似的任务时,可以回忆起"上次是怎么做的",
直接复用成功的策略,或避免失败的路径。
来源论文:
"Generative Agents" (Park et al., 2023)
→ 每个Agent的记忆流(Memory Stream)就是情景记忆
→ 记录了Agent经历的每一个事件
→ 通过"重要性 + 时效性 + 相关性"综合打分来检索
情景记忆的结构
一条情景记忆包含:
{
"episode_id": "ep_001",
"timestamp": "2024-03-15T14:30:00",
"task": "帮用户排查订单服务延迟问题",
"context": "用户反馈下单接口响应时间从200ms升到2s",
"trajectory": [
{"step": 1, "action": "check_service_status('order-service')", "result": "degraded"},
{"step": 2, "action": "check_logs('order-service', 'ERROR')", "result": "ConnectionPool exhausted"},
{"step": 3, "action": "check_metrics('order-service', 'connections')", "result": "200/200"},
{"step": 4, "thought": "连接池满了,需要扩容或排查连接泄漏"}
],
"outcome": "success",
"resolution": "发现连接池配置过小,建议扩容到500",
"lessons_learned": "服务延迟问题优先检查连接池和资源指标"
}
实现代码
python
from dataclasses import dataclass, field, asdict
from datetime import datetime
import json
@dataclass
class Episode:
"""一条情景记忆"""
task: str # 任务描述
context: str # 任务背景
trajectory: list[dict] # 执行轨迹
outcome: str # "success" / "failure" / "partial"
resolution: str # 最终解决方案
lessons_learned: str # 经验教训
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
episode_id: str = field(default_factory=lambda: f"ep_{datetime.now().strftime('%Y%m%d%H%M%S')}")
class EpisodicMemory:
"""情景记忆管理器"""
def __init__(self, long_term_memory: LongTermMemory):
self.ltm = long_term_memory # 复用长期记忆的向量数据库
def record_episode(self, episode: Episode) -> None:
"""记录一次完整的任务经历"""
# 将情景记忆序列化为文本,存入向量数据库
content = f"""任务: {episode.task}
背景: {episode.context}
结果: {episode.outcome}
解决方案: {episode.resolution}
经验教训: {episode.lessons_learned}
执行步骤: {json.dumps(episode.trajectory, ensure_ascii=False)[:500]}"""
self.ltm.store(
content=content,
memory_type="episode",
importance=0.9 if episode.outcome == "failure" else 0.7,
metadata={
"episode_id": episode.episode_id,
"outcome": episode.outcome,
"task_type": self._classify_task(episode.task)
}
)
def recall_similar_episodes(self, current_task: str, top_k: int = 3) -> list[Document]:
"""回忆与当前任务类似的过往经历"""
return self.ltm.retrieve(
query=f"任务经历: {current_task}",
top_k=top_k,
memory_type="episode"
)
def recall_failures(self, current_task: str, top_k: int = 2) -> list[Document]:
"""专门回忆失败的经历(避免重蹈覆辙)"""
# 先检索相关的情景记忆
all_episodes = self.ltm.retrieve(
query=f"失败经历: {current_task}",
top_k=top_k * 2,
memory_type="episode"
)
# 过滤出失败的
failures = [doc for doc in all_episodes if doc.metadata.get("outcome") == "failure"]
return failures[:top_k]
def _classify_task(self, task: str) -> str:
"""简单的任务分类(生产环境可用LLM分类)"""
if "排查" in task or "故障" in task or "延迟" in task:
return "troubleshooting"
elif "查询" in task or "分析" in task:
return "data_analysis"
elif "写" in task or "生成" in task or "创建" in task:
return "generation"
return "general"
# 使用示例
ltm = LongTermMemory()
episodic = EpisodicMemory(ltm)
# 记录一次成功的排障经历
episode = Episode(
task="排查订单服务延迟问题",
context="用户反馈下单接口响应时间从200ms升到2s",
trajectory=[
{"step": 1, "action": "check_service_status", "result": "degraded"},
{"step": 2, "action": "check_logs", "result": "ConnectionPool exhausted"},
{"step": 3, "action": "check_metrics(connections)", "result": "200/200 满了"},
],
outcome="success",
resolution="连接池配置过小(200),扩容到500后恢复",
lessons_learned="服务延迟优先检查连接池;连接数满=连接池过小或有泄漏"
)
episodic.record_episode(episode)
# 下次遇到类似问题时回忆
similar = episodic.recall_similar_episodes("支付服务响应变慢")
# → 会检索到上面的排障经历,Agent可以参考"上次是连接池问题"
三、记忆的核心操作
记忆系统有四个核心操作:存储(Store)、检索(Retrieve)、压缩(Compress)、遗忘(Forget)。
┌─────────────────────────────────────────────────────────────────────────┐
│ 记忆的四大核心操作 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 存储 │ │ 检索 │ │ 压缩 │ │ 遗忘 │ │
│ │ Store │ │ Retrieve │ │ Compress │ │ Forget │ │
│ │ │ │ │ │ │ │ │ │
│ │ 什么时候 │ │ 什么时候 │ │ 什么时候 │ │ 什么时候 │ │
│ │ 存什么? │ │ 取什么? │ │ 怎么缩? │ │ 删什么? │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 类比: 图书馆 │
│ 存储 = 新书入库编目 │
│ 检索 = 根据需求找到相关书籍 │
│ 压缩 = 把多本书的内容整理成一份摘要 │
│ 遗忘 = 过时的书下架销毁 │
└─────────────────────────────────────────────────────────────────────────┘
3.1 存储(Store):什么时候存什么、存在哪里
核心问题: 不是所有信息都值得存入长期记忆。
存太多 → 检索时噪音大,干扰Agent输出
存太少 → 关键信息丢失,Agent"健忘"
判断标准: 这条信息在未来的对话中还会有用吗?
应该存的:
✅ 用户明确表达的偏好("我喜欢用Go")
✅ 重要的事实信息("项目用的是PostgreSQL 15")
✅ 任务执行的反思和教训("上次用错了表名")
✅ 用户纠正Agent的内容("不是user_name,是username")
✅ 关键决策和原因("选择方案A是因为性能更好")
不应该存的:
❌ 闲聊和寒暄("今天天气真好")
❌ 一次性的临时信息("帮我算一下3+5")
❌ 已经过时的信息("明天下午3点开会" → 会议已过)
❌ 过于细节的中间过程(每一步的工具返回原始数据)
自动提取关键信息
python
from openai import OpenAI
import json
client = OpenAI()
def extract_memories_from_conversation(conversation: str) -> list[dict]:
"""从对话中自动提取值得存储的记忆"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""从以下对话中提取值得长期记住的信息。
只提取以下类型的信息:
1. 用户偏好(语言、工具、风格等)
2. 重要事实(项目信息、技术栈、业务规则等)
3. 用户纠正的内容(Agent说错了,用户纠正的)
4. 关键决策(选择了什么方案、为什么)
对话:
{conversation}
输出JSON格式:
{{
"memories": [
{{"content": "记忆内容", "type": "preference/fact/correction/decision", "importance": 0.0-1.0}}
]
}}
如果没有值得记住的信息,返回空列表。"""
}],
response_format={"type": "json_object"},
temperature=0.0
)
result = json.loads(response.choices[0].message.content)
return result.get("memories", [])
# 使用示例
conversation = """
用户: 帮我写一个HTTP服务
AI: 好的,你想用什么语言?
用户: Go,我们项目统一用Go。数据库是PostgreSQL 15。
AI: 好的,我用net/http还是gin框架?
用户: 用gin吧,我们团队习惯用gin。
"""
memories = extract_memories_from_conversation(conversation)
# 输出:
# [
# {"content": "用户团队统一使用Go语言", "type": "preference", "importance": 0.9},
# {"content": "数据库使用PostgreSQL 15", "type": "fact", "importance": 0.8},
# {"content": "团队习惯使用gin框架", "type": "preference", "importance": 0.8}
# ]
# 存入长期记忆
for mem in memories:
memory.store(mem["content"], memory_type=mem["type"], importance=mem["importance"])
存储方案对比
确定了"存什么"之后,下一个问题是"存在哪里"。
不同类型的记忆适合不同的存储方案:
┌──────────────┬────────────────────┬──────────────────────┬──────────────────┐
│ 存储方案 │ 适合存什么 │ 检索方式 │ 代表工具 │
├──────────────┼────────────────────┼──────────────────────┼──────────────────┤
│ 向量数据库 │ 非结构化文本 │ 语义相似度检索 │ Chroma, Pinecone │
│ │ (对话、文档、反思) │ "意思相近"就能找到 │ Weaviate, Milvus │
├──────────────┼────────────────────┼──────────────────────┼──────────────────┤
│ KV存储 │ 结构化事实 │ 精确键值查找 │ Redis, MongoDB │
│ │ (用户名、偏好、配置) │ 知道key就能找到 │ │
├──────────────┼────────────────────┼──────────────────────┼──────────────────┤
│ 知识图谱 │ 实体关系 │ 图遍历/关系查询 │ Neo4j, NebulaGraph│
│ │ (人物关系、系统依赖) │ "A和B什么关系" │ │
├──────────────┼────────────────────┼──────────────────────┼──────────────────┤
│ 文件系统 │ 大文档/日志 │ 全文搜索 │ 本地文件 + grep │
│ │ (会议记录、代码) │ │ │
└──────────────┴────────────────────┴──────────────────────┴──────────────────┘
最常用的方案: 向量数据库
原因: Agent的大部分记忆都是非结构化文本,语义检索最灵活
部署架构选择:本地存储 vs 分布式存储
上面的表格回答了"用什么类型的存储",但还有一个关键问题:
"存储部署在哪里?是本地还是远程?单机还是集群?"
这个选择直接影响: 性能、成本、可扩展性、数据安全性。
本地存储
定义: 记忆数据存储在Agent运行的同一台机器上(本地文件系统或嵌入式数据库)。
典型方案:
┌──────────────────────────────────────────────────────────────────────────┐
│ 方案 │ 技术选型 │ 适合场景 │
├──────────────────────────────────────────────────────────────────────────┤
│ 本地向量数据库 │ Chroma (SQLite后端) │ 个人Agent、原型开发 │
│ │ FAISS (Meta开源) │ 高性能本地检索 │
│ │ LanceDB │ 嵌入式向量DB,零依赖 │
├──────────────────────────────────────────────────────────────────────────┤
│ 本地KV存储 │ SQLite │ 结构化数据、用户偏好 │
│ │ LevelDB / RocksDB │ 高性能键值存储 │
│ │ JSON文件 │ 最简单,适合原型 │
├──────────────────────────────────────────────────────────────────────────┤
│ 本地文件系统 │ Markdown文件 │ 人类可读的记忆存储 │
│ │ JSONL文件 │ 追加写入的日志型存储 │
└──────────────────────────────────────────────────────────────────────────┘
优点:
✓ 零网络延迟(读写都在本地,微秒级)
✓ 零运维成本(不需要部署和维护服务器)
✓ 数据隐私(数据不出本机,适合敏感场景)
✓ 离线可用(不依赖网络)
✓ 启动简单(pip install 就能用)
缺点:
✗ 不支持多Agent共享记忆(数据锁在一台机器上)
✗ 容量受限于本地磁盘
✗ 无高可用(机器挂了,记忆就丢了,除非有备份)
✗ 不支持水平扩展
适合场景:
- 个人开发者的本地Agent(如Cursor、Windsurf等IDE Agent)
- 原型开发和PoC验证
- 对隐私要求极高的场景(如医疗、法律)
- 单用户单Agent的桌面应用
知识扩展
1.JSONL:每行都是一个独立的、完整的JSON对象,专为可扩展的流式处理而生
2.嵌入式数据库(Embedded Database)
"嵌入式"在数据库领域是一个专门术语,指数据库引擎直接嵌入到应用程序进程中运行,
而不是作为独立的服务进程存在。
两种数据库部署模式对比:
┌─────────────────────────────────────────────────────────────────────┐
│ 客户端-服务器模式(Client-Server) │
│ │
│ 你的Agent程序 ──── 网络连接 ────→ 数据库服务器(独立进程) │
│ (客户端) TCP/HTTP (需要单独启动和维护) │
│ │
│ 代表: MySQL, PostgreSQL, Redis, Milvus, Qdrant(服务模式) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 嵌入式模式(Embedded) │
│ │
│ 你的Agent程序 │
│ ┌──────────────────────────────────────┐ │
│ │ 应用代码 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ 数据库引擎(库/Library) │ │ ← 同一个进程内 │
│ │ │ 直接读写本地文件 │ │ │
│ │ └──────────────────────────────┘ │ │
│ └──────────────────────────────────────┘ │
│ ↕ │
│ 本地磁盘文件 │
│ │
│ 代表: SQLite, Chroma, LanceDB, FAISS, DuckDB │
└─────────────────────────────────────────────────────────────────────┘
简单类比:
客户端-服务器 = 去餐厅吃饭(需要一个独立运行的餐厅)
嵌入式 = 自己家厨房做饭(厨房就在你家里,不需要额外的服务)
嵌入式数据库能否做远程/分布式?
简短回答: 原生不支持,但有些提供了额外的服务模式。
┌────────────┬──────────────┬──────────────────────┬──────────────────────────┐
│ 数据库 │ 嵌入式模式 │ 是否有远程/服务模式 │ 说明 │
├────────────┼──────────────┼──────────────────────┼──────────────────────────┤
│ SQLite │ ✅ 纯嵌入式 │ ❌ 原生不支持 │ 设计哲学就是单机单进程 │
│ FAISS │ ✅ 纯嵌入式 │ ❌ 原生不支持 │ 只是内存中的索引库 │
│ Chroma │ ✅ 默认嵌入式 │ ⚠️ 有服务模式 │ 可启动为独立服务,分布式弱 │
│ LanceDB │ ✅ 默认嵌入式 │ ⚠️ 有云版本 │ LanceDB Cloud提供远程访问 │
│ Qdrant │ ❌ 默认服务 │ ✅ 原生分布式 │ 天生客户端-服务器架构 │
│ Milvus │ ❌ 默认服务 │ ✅ 原生分布式 │ 专为大规模分布式设计 │
└────────────┴──────────────┴──────────────────────┴──────────────────────────┘
为什么嵌入式数据库天然不适合分布式?
核心原因: 嵌入式数据库直接操作本地文件,没有网络层。
分布式需要解决的问题:
1. 网络通信协议 → 嵌入式没有网络层
2. 数据分片(Sharding)→ 嵌入式数据都在一个文件里
3. 副本同步(Replication)→ 嵌入式没有同步机制
4. 并发控制 → 嵌入式通常只支持单写者
5. 故障转移(Failover)→ 嵌入式没有集群管理
结论: 嵌入式 ≠ 不能远程,而是"原生设计不考虑远程"。
要做远程/分布式,需要在上面包一层网络服务,但那就不再是"嵌入式"了。
嵌入式向量数据库的核心价值:
✓ 零部署: pip install chromadb,一行代码就能用
✓ 零运维: 没有服务要启动、监控、升级
✓ 零延迟: 函数调用级别,没有网络开销
✓ 零成本: 不需要服务器、不需要云服务费用
✓ 可移植: 整个数据库就是一个文件夹,拷贝走就能用
适合的场景(占Agent应用的大多数):
- 个人Agent(你自己用的AI助手)
- 开发调试阶段
- 数据量 < 100万条
- 单用户、单实例
- 对延迟敏感(本地调用 < 1ms,远程调用 > 10ms)
分布式存储
定义: 记忆数据存储在远程服务器或集群中,Agent通过网络访问。
典型方案:
┌──────────────────────────────────────────────────────────────────────────┐
│ 方案 │ 技术选型 │ 适合场景 │
├──────────────────────────────────────────────────────────────────────────┤
│ 云向量数据库 │ Pinecone (全托管) │ 不想运维,快速上线 │
│ │ Weaviate Cloud │ 支持混合检索 │
│ │ Qdrant Cloud │ 高性能,支持过滤 │
│ │ Zilliz Cloud (Milvus)│ 大规模向量检索 │
├──────────────────────────────────────────────────────────────────────────┤
│ 自建向量数据库 │ Milvus (集群版) │ 亿级向量,高并发 │
│ │ Weaviate (自部署) │ 需要完全控制数据 │
│ │ Qdrant (自部署) │ Rust实现,性能优秀 │
│ │ pgvector (PostgreSQL)│ 已有PG,不想引入新组件 │
├──────────────────────────────────────────────────────────────────────────┤
│ 分布式KV/文档存储 │ Redis Cluster │ 高频读写、缓存热点记忆 │
│ │ MongoDB Atlas │ 灵活Schema、文档型记忆 │
│ │ DynamoDB │ AWS生态、自动扩缩容 │
├──────────────────────────────────────────────────────────────────────────┤
│ 分布式知识图谱 │ Neo4j Aura (云) │ 复杂关系查询 │
│ │ NebulaGraph (集群) │ 大规模图数据 │
│ │ Amazon Neptune │ AWS托管图数据库 │
└──────────────────────────────────────────────────────────────────────────┘
优点:
✓ 多Agent共享记忆(多个Agent实例访问同一份数据)
✓ 高可用(数据有副本,单节点故障不影响服务)
✓ 水平扩展(数据量增长时可以加节点)
✓ 多用户支持(SaaS产品必须用分布式)
✓ 专业的备份和恢复机制
缺点:
✗ 网络延迟(每次读写都要走网络,毫秒级)
✗ 运维成本(需要部署、监控、升级、备份)
✗ 费用(云服务按量计费,自建需要服务器)
✗ 复杂度高(需要处理网络异常、一致性等问题)
适合场景:
- 多用户SaaS产品(如ChatGPT、Claude等)
- 多Agent协作系统(Agent之间需要共享记忆)
- 生产环境的企业级应用
- 数据量大(百万级以上的记忆条目)
- 需要高可用和灾备的场景
如何选择:决策流程
按照以下决策树来选择:
Q1: 你的Agent是单用户还是多用户?
├── 单用户(个人工具/桌面应用)
│ └── Q2: 数据量大吗?(>100万条记忆)
│ ├── 不大 → 本地存储(Chroma/SQLite/FAISS)
│ └── 很大 → 本地FAISS + 定期归档到云端
│
└── 多用户(SaaS/企业应用)
└── Q3: 你有运维能力吗?
├── 没有(小团队/个人)→ 云托管方案(Pinecone/Weaviate Cloud)
└── 有(有DevOps团队)
└── Q4: 数据规模?
├── 中等(<1亿向量)→ 单节点自建(Qdrant/Weaviate)
└── 大规模(>1亿向量)→ 集群部署(Milvus/Qdrant集群)
实际生产中的混合架构(推荐):
┌─────────────────────────────────────────────────────────────────────┐
│ 混合存储架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 热数据层(本地/Redis) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 最近24小时的记忆、高频访问的用户偏好 │ │
│ │ 技术: Redis / 本地缓存 │ │
│ │ 特点: 极低延迟(<1ms),容量小 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ↕ 定期同步 │
│ 温数据层(向量数据库) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 所有活跃记忆(最近30天) │ │
│ │ 技术: Qdrant / Milvus / Pinecone │ │
│ │ 特点: 语义检索(<50ms),容量中等 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ↕ 定期归档 │
│ 冷数据层(对象存储/数据仓库) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 历史记忆归档(30天以前) │ │
│ │ 技术: S3 / COS / PostgreSQL │ │
│ │ 特点: 成本极低,检索慢(需要时再加载到温数据层) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
不同记忆类型的存储选型建议
不同类型的长期记忆,适合不同的存储方案:
┌──────────────────────────────────────────────────────────────────────────────┐
│ 记忆类型 │ 推荐本地方案 │ 推荐分布式方案 │ 原因 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 用户偏好 │ SQLite / JSON文件 │ Redis / MongoDB │ 结构化数据 │
│ (语言、风格) │ │ │ 精确查找 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 对话摘要 │ Chroma / LanceDB │ Pinecone / Qdrant │ 非结构化文本 │
│ (历史总结) │ │ │ 语义检索 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 事实知识 │ SQLite (精确查找) │ PostgreSQL + pgvector │ 混合查询 │
│ (项目信息) │ + Chroma (语义检索) │ (精确+语义都支持) │ 精确+模糊 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 反思/经验 │ Chroma / FAISS │ Weaviate / Milvus │ 语义检索 │
│ (成功/失败经验) │ │ │ 相似场景匹配 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 实体关系 │ SQLite (简单关系) │ Neo4j / NebulaGraph │ 图遍历 │
│ (人物、系统) │ NetworkX (内存图) │ │ 关系推理 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 操作日志 │ JSONL文件 / SQLite │ Elasticsearch / CK │ 时序数据 │
│ (行为记录) │ │ ClickHouse │ 聚合分析 │
└──────────────────────────────────────────────────────────────────────────────┘
特别说明 --- pgvector 的崛起:
如果你的项目已经在用 PostgreSQL,强烈推荐 pgvector 扩展:
- 一个数据库同时支持: 关系查询 + 向量检索 + 全文搜索
- 不需要引入额外的向量数据库组件
- 适合中等规模(<1000万向量)的场景
- 2024-2025年已成为很多团队的首选方案
选型代码示例
python
"""
根据不同场景选择存储方案的示例
"""
# ═══════════════════════════════════════════
# 方案A: 本地存储(个人Agent / 原型开发)
# ═══════════════════════════════════════════
# 向量检索: Chroma(零配置,pip install即可)
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
local_vectorstore = Chroma(
embedding_function=OpenAIEmbeddings(),
persist_directory="./my_agent_memory", # 数据存在本地目录
collection_name="memories"
)
# KV存储: SQLite(Python内置,无需安装)
import sqlite3
conn = sqlite3.connect("./my_agent_memory/preferences.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS user_prefs (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT
)
""")
# ═══════════════════════════════════════════
# 方案B: 分布式存储(生产环境 / 多用户)
# ═══════════════════════════════════════════
# 向量检索: Qdrant Cloud(高性能,支持过滤)
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams
qdrant = QdrantClient(
url="https://your-cluster.qdrant.io",
api_key="your-api-key"
)
# 创建集合(如果不存在)
qdrant.recreate_collection(
collection_name="agent_memories",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE)
)
# KV存储: Redis(高频读写、多Agent共享)
import redis
r = redis.Redis(host="your-redis-host", port=6379, decode_responses=True)
r.hset("user:zhangsan:prefs", mapping={
"language": "zh",
"code_style": "concise",
"framework": "Go + Gin"
})
# ═══════════════════════════════════════════
# 方案C: 混合方案(PostgreSQL + pgvector)
# ═══════════════════════════════════════════
# 一个数据库搞定: 关系数据 + 向量检索
import psycopg2
conn = psycopg2.connect("postgresql://user:pass@host:5432/agent_db")
cur = conn.cursor()
# 创建带向量字段的表
cur.execute("""
CREATE TABLE IF NOT EXISTS memories (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
memory_type VARCHAR(50),
importance FLOAT DEFAULT 0.5,
user_id VARCHAR(100),
embedding vector(1536), -- pgvector扩展提供的向量类型
created_at TIMESTAMP DEFAULT NOW(),
metadata JSONB
);
-- 创建向量索引(HNSW算法,检索速度快)
CREATE INDEX IF NOT EXISTS memories_embedding_idx
ON memories USING hnsw (embedding vector_cosine_ops);
""")
# 存储记忆(同时存结构化数据和向量)
cur.execute("""
INSERT INTO memories (content, memory_type, importance, user_id, embedding, metadata)
VALUES (%s, %s, %s, %s, %s, %s)
""", (
"用户偏好Go语言,使用Gin框架",
"preference",
0.8,
"zhangsan",
embedding_vector, # 1536维向量
'{"source": "conversation", "session_id": "abc123"}'
))
# 语义检索(向量相似度)
cur.execute("""
SELECT content, memory_type, importance,
1 - (embedding <=> %s) as similarity
FROM memories
WHERE user_id = %s AND importance > 0.5
ORDER BY embedding <=> %s
LIMIT 5
""", (query_vector, "zhangsan", query_vector))
# 精确查询(传统SQL)
cur.execute("""
SELECT content FROM memories
WHERE user_id = %s AND memory_type = 'preference'
ORDER BY importance DESC
""", ("zhangsan",))
3.2 检索(Retrieve):什么时候取什么、怎么取
核心问题: 长期记忆中可能有成百上千条信息,
每次对话不可能全部取出来塞进上下文。
需要"按需检索"------只取与当前问题最相关的记忆。
检索时机:
1. 每次用户发送新消息时 → 检索与用户问题相关的记忆
2. Agent准备执行工具前 → 检索与该工具/任务相关的经验
3. Agent遇到错误时 → 检索类似错误的历史反思
4. Agent生成最终回复前 → 检索用户偏好和约束
前置认知:检索方式是存储时就决定的
⚠️ 重要认知(在了解具体检索方式之前,必须先理解这一点):
检索方式不是"事后选择",而是"存储时就决定了"。
你选了什么存储方案,就决定了你能用什么检索方式。
如果存储时没有建好对应的索引,检索时就无法使用该方式。
以全文检索为例:
┌─────────────────────────────────────────────────────────────────┐
│ 存储阶段(必须提前做好): │
│ 原始文本 → 分词器(Tokenizer) → 倒排索引(Inverted Index) │
│ │
│ "Redis连接池耗尽" → ["Redis", "连接池", "耗尽"] │
│ ↓ │
│ 倒排索引: │
│ "Redis" → [doc_3, doc_7, doc_15] │
│ "连接池" → [doc_3, doc_22] │
│ "耗尽" → [doc_3, doc_45] │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 检索阶段: │
│ 查询 "Redis连接池" → 分词 → ["Redis", "连接池"] │
│ ↓ │
│ 查倒排索引 → "Redis"出现在doc_3,7,15 │
│ → "连接池"出现在doc_3,22 │
│ → 交集: doc_3(两个词都有,BM25分数最高) │
└─────────────────────────────────────────────────────────────────┘
各检索方式对存储的前置要求:
┌──────────────┬──────────────────────────────────────────────────────────┐
│ 检索方式 │ 存储时必须做的事 │
├──────────────┼──────────────────────────────────────────────────────────┤
│ 语义检索 │ 必须调用Embedding模型,把文本转为向量一起存入 │
│ 全文检索 │ 必须选支持FTS的数据库,存储时自动分词+建倒排索引 │
│ 精确检索 │ 设计好key/字段 + 建索引即可(最简单) │
│ 图检索 │ 必须提取实体和关系,构建图结构 │
└──────────────┴──────────────────────────────────────────────────────────┘
设计记忆系统时,正确的思考顺序是:
① 先想清楚"将来要怎么检索"(语义?关键词?精确?)
② 再选择对应的存储方案(向量DB?FTS?KV?)
③ 存储时就把索引建好(Embedding / 分词 / 倒排索引)
如果你一开始只用了向量数据库,后来想加全文检索,
就需要补建FTS索引或把数据迁移到支持FTS的存储中。
这也是为什么推荐混合存储架构------提前把多种检索能力都准备好。
四种检索方式详解
不同的存储方案对应不同的检索方式,选错检索方式会导致"明明存了但找不到":
┌──────────────────────────────────────────────────────────────────────────────┐
│ 检索方式全景图 │
├──────────────┬──────────────────┬──────────────────┬─────────────────────────┤
│ 检索方式 │ 原理 │ 对应存储 │ 适合检索什么 │
├──────────────┼──────────────────┼──────────────────┼─────────────────────────┤
│ 语义检索 │ Embedding向量 │ 向量数据库 │ "意思相近"的非结构化文本 │
│ (Semantic) │ 余弦相似度 │ Chroma/Pinecone │ 对话摘要、反思、知识 │
├──────────────┼──────────────────┼──────────────────┼─────────────────────────┤
│ 精确检索 │ 键值/条件匹配 │ KV存储/SQL │ 结构化事实、用户偏好 │
│ (Exact) │ WHERE条件 │ Redis/SQLite/PG │ "用户的语言是什么" │
├──────────────┼──────────────────┼──────────────────┼─────────────────────────┤
│ 全文检索 │ 关键词倒排索引 │ ES/文件系统 │ 包含特定关键词的文档 │
│ (Full-text) │ BM25算法 │ Elasticsearch │ "哪条记忆提到了Redis" │
├──────────────┼──────────────────┼──────────────────┼─────────────────────────┤
│ 图检索 │ 关系遍历 │ 知识图谱 │ 实体之间的关系 │
│ (Graph) │ Cypher查询 │ Neo4j/Nebula │ "张三负责哪些服务" │
├──────────────┼──────────────────┼──────────────────┼─────────────────────────┤
│ 混合检索 │ 多种方式组合 │ 多存储联合 │ 复杂场景,需要综合 │
│ (Hybrid) │ 加权融合 │ pgvector等 │ "语义相关+时间最近" │
└──────────────┴──────────────────┴──────────────────┴─────────────────────────┘
方式1: 语义检索(Semantic Search)
原理:
1. 把记忆文本通过Embedding模型转为向量(如1536维浮点数组)
2. 把用户查询也转为向量
3. 计算两个向量的余弦相似度(cosine similarity)
4. 返回相似度最高的Top-K条记忆
核心优势: 不需要精确匹配关键词,"意思相近"就能找到
查询: "帮我写个接口"
能找到: "用户偏好用Go语言开发HTTP服务" ← 没有"接口"这个词,但语义相关
适合检索:
✅ 对话摘要、历史交互记录
✅ 反思和经验教训
✅ 非结构化的领域知识
✅ 任何"不确定用什么关键词搜"的场景
不适合:
❌ 精确的事实查询("用户的数据库版本是多少")
❌ 需要精确匹配的场景("找到key=user_language的记录")
关键参数:
- top_k: 返回多少条结果(通常3-10)
- similarity_threshold: 相似度阈值(低于此值的不返回,通常0.5-0.7)
- embedding_model: 向量化模型(text-embedding-3-small/large)
python
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
class SemanticRetriever:
"""语义检索器: 基于向量相似度"""
def __init__(self, persist_dir: str = "./memory_db"):
self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
self.vectorstore = Chroma(
embedding_function=self.embeddings,
persist_directory=persist_dir,
collection_name="memories"
)
def search(self, query: str, top_k: int = 5,
threshold: float = 0.6,
filter_type: str = None) -> list[dict]:
"""
语义检索
Args:
query: 用户的问题或当前上下文
top_k: 返回条数
threshold: 相似度阈值(0-1,越高越严格)
filter_type: 可选,只检索特定类型的记忆
"""
# 构建过滤条件
filter_dict = {"type": filter_type} if filter_type else None
# 带分数的相似度搜索
results = self.vectorstore.similarity_search_with_relevance_scores(
query, k=top_k, filter=filter_dict
)
# 过滤低于阈值的结果
filtered = []
for doc, score in results:
if score >= threshold:
filtered.append({
"content": doc.page_content,
"score": score,
"metadata": doc.metadata
})
return filtered
# 使用示例
retriever = SemanticRetriever()
# 场景1: 用户问编程问题,检索相关偏好和经验
results = retriever.search("帮我写一个数据库查询接口")
# → 找到: "用户偏好Go语言" (0.82), "使用PostgreSQL" (0.78), "上次查询用错了字段名" (0.71)
# 场景2: 只检索反思类记忆
results = retriever.search("数据库操作", filter_type="reflection")
# → 找到: "字段名是username不是user_name" (0.85)
方式2: 精确检索(Exact Match / Key-Value Lookup)
原理:
通过精确的键、条件或SQL WHERE子句查找记录。
不涉及"相似度",要么完全匹配,要么不匹配。
核心优势: 确定性高,速度快,不会有"似是而非"的结果
查询: user_id="zhangsan" AND type="preference"
结果: 精确返回张三的所有偏好设置
适合检索:
✅ 用户偏好和配置(语言、框架、风格)
✅ 确定性事实(项目名、数据库版本、团队成员)
✅ 状态信息(当前任务进度、上次操作结果)
✅ 需要精确答案的场景
不适合:
❌ 不确定用什么key查的场景
❌ 需要"模糊匹配"的场景
❌ 非结构化的自由文本
python
import sqlite3
import json
import redis
class ExactRetriever:
"""精确检索器: 基于键值/条件匹配"""
def __init__(self, db_path: str = "./memory.db"):
self.conn = sqlite3.connect(db_path)
self._init_db()
def _init_db(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
memory_type TEXT DEFAULT 'general',
importance FLOAT DEFAULT 0.5,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
self.conn.execute("""
CREATE INDEX IF NOT EXISTS idx_user_type
ON memories(user_id, memory_type)
""")
self.conn.commit()
def get_by_key(self, user_id: str, key: str) -> str | None:
"""精确键值查找"""
cur = self.conn.execute(
"SELECT value FROM memories WHERE user_id = ? AND key = ?",
(user_id, key)
)
row = cur.fetchone()
return row[0] if row else None
def get_by_type(self, user_id: str, memory_type: str,
limit: int = 10) -> list[dict]:
"""按类型查找所有记忆"""
cur = self.conn.execute("""
SELECT key, value, importance, created_at
FROM memories
WHERE user_id = ? AND memory_type = ?
ORDER BY importance DESC, updated_at DESC
LIMIT ?
""", (user_id, memory_type, limit))
return [{"key": r[0], "value": r[1], "importance": r[2],
"created_at": r[3]} for r in cur.fetchall()]
def get_all_preferences(self, user_id: str) -> dict:
"""获取用户的所有偏好(常用于每次对话开始时)"""
cur = self.conn.execute("""
SELECT key, value FROM memories
WHERE user_id = ? AND memory_type = 'preference'
ORDER BY importance DESC
""", (user_id,))
return {r[0]: r[1] for r in cur.fetchall()}
# 使用示例
retriever = ExactRetriever()
# 场景1: 获取用户的编程语言偏好
lang = retriever.get_by_key("zhangsan", "preferred_language")
# → "Go"
# 场景2: 获取所有偏好,注入到system prompt
prefs = retriever.get_all_preferences("zhangsan")
# → {"preferred_language": "Go", "framework": "Gin", "db": "PostgreSQL 15", "style": "简洁"}
# 场景3: 获取所有事实类记忆
facts = retriever.get_by_type("zhangsan", "fact")
# → [{"key": "project_name", "value": "order-service", ...}, ...]
方式3: 全文检索(Full-text Search)
原理:
基于关键词的倒排索引检索。把文本分词后建立索引,
查询时通过关键词匹配找到包含这些词的文档。
常用BM25算法计算相关性分数。
核心优势: 精确的关键词匹配,不会"理解错意思"
查询: "Redis连接池"
结果: 精确找到包含"Redis"和"连接池"的记忆
对比语义检索:
语义检索查"Redis连接池" → 可能返回"数据库连接管理"(语义相关但不精确)
全文检索查"Redis连接池" → 只返回确实包含这两个词的记忆
适合检索:
✅ 用户提到了具体的技术名词、错误信息、函数名
✅ 需要精确关键词匹配的场景
✅ 搜索日志、代码片段、错误堆栈
✅ 当语义检索"太模糊"时的补充
不适合:
❌ 用户用不同的词描述同一件事("接口"vs"API"vs"端点")
❌ 需要理解语义的场景
python
# 方案A: 使用SQLite FTS5(轻量级,适合本地Agent)
import sqlite3
class FullTextRetriever:
"""全文检索器: 基于SQLite FTS5"""
def __init__(self, db_path: str = "./memory_fts.db"):
self.conn = sqlite3.connect(db_path)
self._init_fts()
def _init_fts(self):
"""创建FTS5全文索引表"""
self.conn.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts
USING fts5(
content, -- 记忆正文
memory_type, -- 类型标签
tags, -- 关键词标签
tokenize='unicode61' -- 支持中文分词
)
""")
self.conn.commit()
def search(self, keyword: str, limit: int = 10) -> list[dict]:
"""关键词全文搜索"""
cur = self.conn.execute("""
SELECT content, memory_type, rank
FROM memory_fts
WHERE memory_fts MATCH ?
ORDER BY rank
LIMIT ?
""", (keyword, limit))
return [{"content": r[0], "type": r[1], "rank": r[2]}
for r in cur.fetchall()]
def search_with_highlight(self, keyword: str, limit: int = 10) -> list[dict]:
"""搜索并高亮匹配的关键词"""
cur = self.conn.execute("""
SELECT highlight(memory_fts, 0, '<b>', '</b>'), memory_type, rank
FROM memory_fts
WHERE memory_fts MATCH ?
ORDER BY rank
LIMIT ?
""", (keyword, limit))
return [{"content": r[0], "type": r[1], "rank": r[2]}
for r in cur.fetchall()]
# 使用示例
fts = FullTextRetriever()
# 场景: 用户遇到了具体的错误信息
results = fts.search("connection refused Redis")
# → 找到: "2024-03-15: Redis连接池耗尽,connection refused,原因是max_connections设置太小"
# 场景: 搜索包含特定函数名的记忆
results = fts.search("getUserById")
# → 找到: "getUserById函数的字段名是username不是user_name"
方式4: 图检索(Graph Traversal)
原理:
在知识图谱中,通过关系遍历找到相关实体和信息。
从一个节点出发,沿着边(关系)找到关联的节点。
核心优势: 能回答"关系型"问题
查询: "张三负责的服务有哪些?"
→ 从"张三"节点出发,沿"负责"边,找到所有服务节点
查询: "order-service的上下游依赖是什么?"
→ 从"order-service"出发,沿"依赖"和"被依赖"边遍历
适合检索:
✅ 人物关系(谁负责什么、谁汇报给谁)
✅ 系统依赖(A服务依赖B服务)
✅ 因果链(问题A导致了问题B)
✅ 多跳推理(A认识B,B认识C,所以A可以通过B联系C)
不适合:
❌ 非结构化文本检索
❌ 没有明确实体关系的场景
python
# 方案A: 使用NetworkX(轻量级,适合本地Agent)
import networkx as nx
class GraphRetriever:
"""图检索器: 基于知识图谱的关系查询"""
def __init__(self):
self.graph = nx.DiGraph() # 有向图
def add_relation(self, subject: str, relation: str, obj: str,
metadata: dict = None):
"""添加一条关系"""
self.graph.add_edge(subject, obj, relation=relation, **(metadata or {}))
def get_relations(self, entity: str, relation: str = None,
direction: str = "out") -> list[dict]:
"""查询某个实体的关系"""
results = []
if direction in ("out", "both"):
for _, target, data in self.graph.out_edges(entity, data=True):
if relation is None or data.get("relation") == relation:
results.append({
"subject": entity,
"relation": data["relation"],
"object": target
})
if direction in ("in", "both"):
for source, _, data in self.graph.in_edges(entity, data=True):
if relation is None or data.get("relation") == relation:
results.append({
"subject": source,
"relation": data["relation"],
"object": entity
})
return results
def find_path(self, start: str, end: str) -> list[str] | None:
"""查找两个实体之间的关系路径"""
try:
path = nx.shortest_path(self.graph, start, end)
return path
except nx.NetworkXNoPath:
return None
def get_neighbors(self, entity: str, depth: int = 1) -> dict:
"""获取实体的N跳邻居(用于上下文扩展)"""
subgraph = nx.ego_graph(self.graph, entity, radius=depth)
return {
"nodes": list(subgraph.nodes()),
"edges": [(u, v, d["relation"]) for u, v, d in subgraph.edges(data=True)]
}
# 使用示例
graph = GraphRetriever()
# 构建知识图谱
graph.add_relation("张三", "负责", "order-service")
graph.add_relation("张三", "负责", "payment-service")
graph.add_relation("order-service", "依赖", "payment-service")
graph.add_relation("order-service", "依赖", "user-service")
graph.add_relation("payment-service", "依赖", "redis")
# 场景1: "张三负责哪些服务?"
results = graph.get_relations("张三", relation="负责")
# → [{"subject": "张三", "relation": "负责", "object": "order-service"},
# {"subject": "张三", "relation": "负责", "object": "payment-service"}]
# 场景2: "order-service依赖什么?"
results = graph.get_relations("order-service", relation="依赖")
# → [{"subject": "order-service", "relation": "依赖", "object": "payment-service"},
# {"subject": "order-service", "relation": "依赖", "object": "user-service"}]
# 场景3: "张三和redis有什么关系?"(多跳推理)
path = graph.find_path("张三", "redis")
# → ["张三", "payment-service", "redis"]
# 解读: 张三 →负责→ payment-service →依赖→ redis
混合检索(Hybrid Search)
为什么需要混合检索?
单一检索方式都有盲区:
- 语义检索: 可能"理解错意思",返回语义相关但实际无关的结果
- 精确检索: 只能找到完全匹配的,换个说法就找不到
- 全文检索: 只看关键词,不理解语义
- 图检索: 只能查关系,不能查自由文本
混合检索 = 多种方式组合,取长补短
最常见的混合方式:
1. 语义检索 + 全文检索(BM25)→ 互补性最强
2. 语义检索 + 精确检索 → 先查偏好,再查相关知识
3. 语义检索 + 图检索 → 先找相关实体,再查实体关系
python
from dataclasses import dataclass
from typing import Optional
@dataclass
class RetrievalResult:
content: str
score: float
source: str # "semantic" / "keyword" / "exact" / "graph"
metadata: dict = None
class HybridRetriever:
"""混合检索器: 组合多种检索方式"""
def __init__(self, semantic: SemanticRetriever,
exact: ExactRetriever,
fulltext: FullTextRetriever = None,
graph: GraphRetriever = None):
self.semantic = semantic
self.exact = exact
self.fulltext = fulltext
self.graph = graph
def retrieve(self, query: str, user_id: str,
semantic_weight: float = 0.5,
keyword_weight: float = 0.3,
exact_weight: float = 0.2,
top_k: int = 8) -> list[RetrievalResult]:
"""
混合检索: 综合多种方式的结果
策略:
1. 先用精确检索获取用户偏好(总是需要的)
2. 用语义检索获取相关记忆
3. 用全文检索补充关键词匹配的结果
4. 合并去重,加权排序
"""
all_results = []
# Step 1: 精确检索 --- 获取用户偏好(这些总是有用的)
preferences = self.exact.get_all_preferences(user_id)
for key, value in preferences.items():
all_results.append(RetrievalResult(
content=f"[偏好] {key}: {value}",
score=exact_weight * 0.9, # 偏好总是高分
source="exact",
metadata={"key": key}
))
# Step 2: 语义检索 --- 找语义相关的记忆
semantic_results = self.semantic.search(query, top_k=top_k)
for r in semantic_results:
all_results.append(RetrievalResult(
content=r["content"],
score=semantic_weight * r["score"],
source="semantic",
metadata=r["metadata"]
))
# Step 3: 全文检索 --- 补充关键词匹配(如果有的话)
if self.fulltext:
keyword_results = self.fulltext.search(query, limit=top_k)
for r in keyword_results:
# BM25的rank是负数,越小越相关,归一化到0-1
normalized_score = 1.0 / (1.0 + abs(r.get("rank", 0)))
all_results.append(RetrievalResult(
content=r["content"],
score=keyword_weight * normalized_score,
source="keyword",
metadata={"type": r.get("type")}
))
# Step 4: 去重 + 排序
seen_contents = set()
unique_results = []
for r in all_results:
content_key = r.content[:100] # 用前100字符去重
if content_key not in seen_contents:
seen_contents.add(content_key)
unique_results.append(r)
unique_results.sort(key=lambda x: x.score, reverse=True)
return unique_results[:top_k]
def retrieve_formatted(self, query: str, user_id: str,
top_k: int = 8) -> str:
"""检索并格式化为可注入Prompt的文本"""
results = self.retrieve(query, user_id, top_k=top_k)
if not results:
return ""
lines = ["## 相关记忆(按相关性排序)"]
for r in results:
source_label = {
"semantic": "📝", "keyword": "🔍",
"exact": "📌", "graph": "🔗"
}.get(r.source, "💡")
lines.append(f" {source_label} [{r.score:.2f}] {r.content}")
return "\n".join(lines)
检索结果的综合排序
当多种检索方式返回了结果后,如何决定最终排序?
综合打分公式:
final_score = w1 * relevance + w2 * recency + w3 * importance + w4 * access_count
各因子含义:
relevance: 与当前查询的相关性(语义相似度 / BM25分数)
recency: 时效性(越新的记忆分数越高,指数衰减)
importance: 重要性(存储时标记的,0-1)
access_count: 访问频率(被检索到的次数越多,说明越有用)
推荐权重:
通用场景: w1=0.5, w2=0.2, w3=0.2, w4=0.1
时效敏感: w1=0.3, w2=0.4, w3=0.2, w4=0.1 (如新闻、实时数据)
知识密集: w1=0.6, w2=0.1, w3=0.2, w4=0.1 (如技术文档、规则)
python
from datetime import datetime
import math
class ScoredRetriever:
"""带综合打分的检索器"""
def __init__(self, vectorstore,
decay_rate: float = 0.995,
weights: dict = None):
self.vectorstore = vectorstore
self.decay_rate = decay_rate
self.weights = weights or {
"relevance": 0.5,
"recency": 0.2,
"importance": 0.2,
"access_count": 0.1
}
def retrieve(self, query: str, top_k: int = 5) -> list[dict]:
"""综合打分检索"""
# 粗筛: 多取一些候选
raw_results = self.vectorstore.similarity_search_with_relevance_scores(
query, k=top_k * 3
)
now = datetime.now()
scored = []
for doc, similarity in raw_results:
if similarity < 0.5: # 相关性太低的直接丢弃
continue
# 时间衰减: 每天衰减0.5%
timestamp_str = doc.metadata.get("timestamp", "")
recency = 1.0
if timestamp_str:
try:
age_days = (now - datetime.fromisoformat(timestamp_str)).days
recency = self.decay_rate ** age_days
except:
recency = 0.5
# 重要性
importance = doc.metadata.get("importance", 0.5)
# 访问频率(归一化到0-1)
access_count = doc.metadata.get("access_count", 0)
access_score = 1 - math.exp(-0.1 * access_count) # 对数增长
# 综合打分
final_score = (
self.weights["relevance"] * similarity +
self.weights["recency"] * recency +
self.weights["importance"] * importance +
self.weights["access_count"] * access_score
)
scored.append({
"content": doc.page_content,
"final_score": final_score,
"relevance": similarity,
"recency": recency,
"importance": importance,
"metadata": doc.metadata
})
scored.sort(key=lambda x: x["final_score"], reverse=True)
return scored[:top_k]
MMR多样性检索
问题: 纯相似度检索可能返回很多"意思差不多"的记忆
比如检索"Go语言",可能返回5条都是"用户喜欢Go"的变体
MMR (Maximal Marginal Relevance) 的解决方案:
在相关性和多样性之间取平衡
既要和查询相关,又要彼此之间不太重复
lambda_mult参数:
= 1.0 → 纯相关性(可能有重复)
= 0.0 → 纯多样性(可能不相关)
= 0.5-0.7 → 推荐值(兼顾相关性和多样性)
python
def retrieve_with_diversity(vectorstore, query: str, top_k: int = 5,
lambda_mult: float = 0.7) -> list:
"""
MMR多样性检索
适合场景: 需要从多个角度获取记忆
比如用户问"帮我优化这个服务",你希望同时获取:
- 用户的技术偏好
- 该服务的历史问题
- 类似优化的经验
而不是5条都是"用户喜欢Go"
"""
results = vectorstore.max_marginal_relevance_search(
query, k=top_k, lambda_mult=lambda_mult
)
return results
检索方式选型指南
根据你的场景选择检索方式:
┌──────────────────────────────────────────────────────────────────────────────┐
│ 你的场景 │ 推荐检索方式 │ 原因 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 个人Agent,记忆<1000条 │ 语义检索 │ 简单够用 │
│ 个人Agent,需要精确偏好 │ 语义 + 精确 │ 偏好用KV,其他用向量│
├──────────────────────────────────────────────────────────────────────────────┤
│ 企业Agent,记忆>10000条 │ 混合检索 │ 单一方式有盲区 │
│ 企业Agent,有组织架构/系统依赖 │ 混合 + 图检索 │ 关系查询必须用图 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 代码Agent,需要搜索错误信息 │ 语义 + 全文 │ 错误信息需要精确匹配│
│ 客服Agent,FAQ检索 │ 语义检索 + MMR │ 需要多样性避免重复 │
├──────────────────────────────────────────────────────────────────────────────┤
│ 已有PostgreSQL │ pgvector混合检索 │ 一个DB搞定所有 │
│ 需要最简方案 │ Chroma语义检索 │ pip install即可 │
└──────────────────────────────────────────────────────────────────────────────┘
检索结果注入LLM上下文
检索到记忆后,最后一步是把它注入到LLM的输入中。
注入位置和格式直接影响LLM对记忆的利用效果。
注入位置(按推荐程度排序):
1. System Prompt末尾(推荐)→ LLM会把它当作"背景知识"
2. 用户消息之前(可选)→ 作为"上下文补充"
3. 单独的system消息(可选)→ 明确标记为"记忆"
注入格式示例:
python
def build_messages_with_memory(system_prompt: str, user_message: str,
retriever: HybridRetriever,
user_id: str) -> list[dict]:
"""构建带记忆的LLM请求消息"""
# 检索相关记忆
memory_text = retriever.retrieve_formatted(user_message, user_id, top_k=5)
# 注入到system prompt末尾
enhanced_system = system_prompt
if memory_text:
enhanced_system += f"""
## 关于这位用户的记忆(请参考但不要逐条复述)
{memory_text}
注意: 以上记忆仅供参考,如果与用户当前的明确要求冲突,以用户当前要求为准。
"""
messages = [
{"role": "system", "content": enhanced_system},
{"role": "user", "content": user_message}
]
return messages
# 实际使用
messages = build_messages_with_memory(
system_prompt="你是一个编程助手。",
user_message="帮我写一个HTTP接口",
retriever=hybrid_retriever,
user_id="zhangsan"
)
# messages[0] 的 system content 会包含:
# "你是一个编程助手。
# ## 关于这位用户的记忆
# 📌 [0.90] [偏好] preferred_language: Go
# 📌 [0.85] [偏好] framework: Gin
# 📝 [0.78] 用户的项目名叫order-service,是一个订单微服务
# 📝 [0.71] 上次写接口时字段名用错了,username不是user_name
# ..."
3.3 压缩(Compress):记忆太多怎么办
问题: 随着使用时间增长,长期记忆会越来越多。
1000条记忆 → 检索时噪音增大
10000条记忆 → 检索速度变慢,相关性下降
压缩策略:
策略1: 摘要合并
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
把多条相似的记忆合并为一条摘要
原始:
- "用户喜欢Go语言"
- "用户的项目用Go写的"
- "用户说团队统一用Go"
压缩后:
- "用户及其团队统一使用Go语言进行开发"
策略2: 分层压缩(Hierarchical Compression)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
最近的记忆保持详细,越早的记忆越粗略
最近7天: 保留原始记忆(详细)
7-30天: 按天压缩为摘要
30天以上: 按周压缩为摘要
类比: 你能清楚记得昨天做了什么,
大概记得上周做了什么,
只记得上个月的几件大事。
策略3: 关键信息提取
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
从冗长的记忆中只保留关键实体和关系
原始: "2024年3月15日下午,用户张三说他们的order-service项目
最近遇到了性能问题,数据库是PostgreSQL 15,部署在
阿里云的ECS上,用的是4核8G的配置..."
提取后:
- 用户: 张三
- 项目: order-service
- 问题: 性能问题
- 数据库: PostgreSQL 15
- 部署: 阿里云ECS, 4核8G
实现代码
python
from openai import OpenAI
import json
client = OpenAI()
def merge_similar_memories(memories: list[str], threshold: float = 0.85) -> list[str]:
"""合并相似的记忆"""
if len(memories) <= 1:
return memories
# 用LLM判断哪些记忆可以合并
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""以下是多条记忆,请将意思相近的合并为一条,保留所有关键信息。
不相近的保持原样。
记忆列表:
{json.dumps(memories, ensure_ascii=False, indent=2)}
输出JSON格式:
{{"merged_memories": ["合并后的记忆1", "合并后的记忆2", ...]}}"""
}],
response_format={"type": "json_object"},
temperature=0.0
)
result = json.loads(response.choices[0].message.content)
return result.get("merged_memories", memories)
def hierarchical_compress(memory_store: LongTermMemory,
days_threshold: int = 30) -> None:
"""分层压缩: 将超过阈值天数的记忆压缩为摘要"""
from datetime import timedelta
cutoff_date = datetime.now() - timedelta(days=days_threshold)
# 获取所有旧记忆(这里简化,实际需要按时间过滤)
old_memories = memory_store.retrieve(
query="", # 空查询获取所有
top_k=100
)
# 按周分组
weekly_groups = {}
for doc in old_memories:
timestamp_str = doc.metadata.get("timestamp", "")
if timestamp_str:
try:
ts = datetime.fromisoformat(timestamp_str)
if ts < cutoff_date:
week_key = ts.strftime("%Y-W%W")
weekly_groups.setdefault(week_key, []).append(doc.page_content)
except:
pass
# 每周的记忆压缩为一条摘要
for week, memories in weekly_groups.items():
if len(memories) > 3: # 超过3条才压缩
merged = merge_similar_memories(memories)
# 删除旧记忆,存入压缩后的版本
for m in merged:
memory_store.store(
content=f"[{week}周摘要] {m}",
memory_type="compressed",
importance=0.6
)
print(f"压缩完成: {len(weekly_groups)}周的记忆已压缩")
3.4 遗忘(Forget):什么时候该删除记忆
为什么需要遗忘:
1. 过时的信息会误导Agent
"用户的数据库是MySQL 5.7" → 但用户已经迁移到PostgreSQL了
2. 错误的记忆需要清除
"用户喜欢verbose的代码风格" → 但这是Agent理解错了
3. 存储空间和检索效率
记忆太多 → 检索变慢,噪音增大
遗忘策略:
1. 显式遗忘: 用户主动要求"忘掉这个"
2. 矛盾替换: 新信息与旧记忆矛盾时,用新的替换旧的
3. 时间衰减: 长期未被检索的记忆自动降低优先级
4. 容量淘汰: 达到容量上限时,淘汰最不重要/最旧的记忆
实现代码
python
class MemoryWithForgetting(LongTermMemory):
"""支持遗忘机制的记忆管理器"""
def forget_by_content(self, content_keyword: str) -> int:
"""根据内容关键词删除记忆"""
# 检索包含关键词的记忆
results = self.retrieve(content_keyword, top_k=10)
deleted = 0
for doc in results:
if content_keyword.lower() in doc.page_content.lower():
# Chroma支持按ID删除
self.vectorstore.delete(ids=[doc.metadata.get("id")])
deleted += 1
return deleted
def resolve_contradiction(self, new_fact: str, old_query: str) -> None:
"""矛盾解决: 新事实替换旧记忆"""
# 1. 找到可能矛盾的旧记忆
old_memories = self.retrieve(old_query, top_k=5)
# 2. 用LLM判断是否矛盾
for doc in old_memories:
is_contradicted = self._check_contradiction(new_fact, doc.page_content)
if is_contradicted:
# 删除旧记忆
self.vectorstore.delete(ids=[doc.metadata.get("id")])
print(f"🗑️ 删除矛盾记忆: {doc.page_content[:50]}...")
# 3. 存入新事实
self.store(new_fact, memory_type="fact", importance=0.8)
print(f"✅ 存入新记忆: {new_fact}")
def _check_contradiction(self, new_info: str, old_info: str) -> bool:
"""用LLM判断两条信息是否矛盾"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""判断以下两条信息是否矛盾(即不能同时为真):
信息A(新): {new_info}
信息B(旧): {old_info}
只回答"是"或"否"。"""
}],
temperature=0.0
)
return "是" in response.choices[0].message.content
def decay_and_cleanup(self, max_memories: int = 1000,
min_importance: float = 0.2) -> int:
"""定期清理: 删除过旧且不重要的记忆"""
# 获取所有记忆的元数据
collection = self.vectorstore._collection
all_data = collection.get(include=["metadatas"])
if not all_data["metadatas"]:
return 0
# 计算每条记忆的"保留分数"
now = datetime.now()
to_delete = []
for i, meta in enumerate(all_data["metadatas"]):
importance = meta.get("importance", 0.5)
timestamp_str = meta.get("timestamp", "")
# 时间衰减
time_score = 1.0
if timestamp_str:
try:
age_days = (now - datetime.fromisoformat(timestamp_str)).days
time_score = 0.995 ** age_days
except:
pass
retain_score = importance * 0.6 + time_score * 0.4
if retain_score < min_importance:
to_delete.append(all_data["ids"][i])
# 如果超过容量上限,额外删除低分记忆
if len(all_data["ids"]) - len(to_delete) > max_memories:
# 按retain_score排序,删除多余的
pass # 简化处理
if to_delete:
collection.delete(ids=to_delete)
return len(to_delete)
四、记忆检索的高级策略
4.1 Generative Agents的检索算法
来源: "Generative Agents: Interactive Simulacra of Human Behavior" (Park et al., 2023)
这篇论文提出了Agent记忆检索的经典算法,被后续大量工作引用。
核心思想: 检索分数 = 时效性 × 重要性 × 相关性
retrieval_score = α × recency + β × importance + γ × relevance
recency (时效性): 越新的记忆分数越高
公式: recency = decay_factor ^ hours_since_last_access
注意: 是"上次被访问"的时间,不是"创建"时间
→ 经常被用到的记忆不会衰减
importance (重要性): 记忆本身的重要程度
由LLM在存储时打分 (1-10)
"用户吃了早餐" → 1分
"用户决定辞职" → 9分
relevance (相关性): 与当前查询的语义相似度
用Embedding计算余弦相似度
为什么这个算法好:
- 纯相关性: 可能检索到很旧的、已经过时的记忆
- 纯时效性: 可能检索到最近但不相关的记忆
- 纯重要性: 可能检索到重要但和当前问题无关的记忆
- 三者结合: 找到"最近的、重要的、且相关的"记忆
4.2 MemGPT的分层记忆管理
来源: "MemGPT: Towards LLMs as Operating Systems" (Packer et al., 2023)
核心类比: 把LLM的记忆管理类比为操作系统的内存管理
操作系统:
CPU寄存器 (极快, 极小) → L1/L2缓存 → 内存(RAM) → 硬盘(SSD/HDD)
MemGPT:
上下文窗口 (极快, 有限) → 工作记忆 → 长期记忆(向量DB)
关键创新: Agent自己决定什么时候"换入/换出"记忆
- 当上下文快满时,Agent主动把不重要的信息"换出"到长期记忆
- 当需要某个信息时,Agent主动从长期记忆"换入"到上下文
这就像操作系统的虚拟内存:
物理内存不够 → 把不常用的页面换出到硬盘
需要某个页面 → 从硬盘换入到物理内存
实现方式:
给Agent提供两个特殊工具:
- memory_save(content): 把信息存入长期记忆
- memory_search(query): 从长期记忆中检索信息
Agent在对话过程中,自己判断什么时候该存、什么时候该取。
MemGPT风格的实现
python
from openai import OpenAI
import json
client = OpenAI()
class MemGPTStyleAgent:
"""MemGPT风格: Agent自己管理记忆的存取"""
def __init__(self):
self.long_term_memory = LongTermMemory()
self.context_budget = 8000 # tokens
def get_tools(self):
"""给Agent提供记忆管理工具"""
return [
{
"type": "function",
"function": {
"name": "memory_save",
"description": "将重要信息保存到长期记忆中。当你发现用户说了重要的偏好、事实或你学到了新的经验时,调用此工具保存。",
"parameters": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "要保存的信息内容"
},
"importance": {
"type": "number",
"description": "重要程度 0.0-1.0"
}
},
"required": ["content"],
"additionalProperties": False
}
}
},
{
"type": "function",
"function": {
"name": "memory_search",
"description": "从长期记忆中搜索相关信息。当你需要回忆用户的偏好、历史信息或过往经验时,调用此工具。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索查询"
}
},
"required": ["query"],
"additionalProperties": False
}
}
}
]
def run(self, messages: list) -> str:
"""运行Agent(Agent自己决定何时存取记忆)"""
system_prompt = """你是一个有长期记忆的智能助手。
你有两个记忆管理工具:
- memory_save: 保存重要信息到长期记忆
- memory_search: 从长期记忆中搜索信息
使用规则:
1. 当用户提到偏好、个人信息、项目信息时 → 主动保存
2. 当你需要回忆用户的历史信息时 → 主动搜索
3. 当你犯了错被纠正时 → 保存纠正信息作为经验
4. 不要保存临时性的、一次性的信息"""
full_messages = [{"role": "system", "content": system_prompt}] + messages
tools = self.get_tools()
# Agent循环(可能多次调用记忆工具)
for _ in range(5):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=full_messages,
tools=tools,
temperature=0.0
)
msg = response.choices[0].message
# 最终回答
if msg.content and not msg.tool_calls:
return msg.content
# 工具调用
if msg.tool_calls:
full_messages.append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
if tc.function.name == "memory_save":
self.long_term_memory.store(
args["content"],
importance=args.get("importance", 0.7)
)
result = "已保存到长期记忆"
elif tc.function.name == "memory_search":
docs = self.long_term_memory.retrieve(args["query"], top_k=3)
result = "\n".join([doc.page_content for doc in docs]) or "未找到相关记忆"
full_messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
return "处理完成"
# 使用示例
agent = MemGPTStyleAgent()
# 第一次对话: Agent会自动保存用户偏好
result = agent.run([{"role": "user", "content": "我叫张三,我是Go开发者,项目用PostgreSQL"}])
# Agent内部会调用 memory_save("用户叫张三,是Go开发者,使用PostgreSQL")
# 第二次对话(新会话): Agent会自动搜索记忆
result = agent.run([{"role": "user", "content": "帮我写一个数据库连接池"}])
# Agent内部会调用 memory_search("用户 编程语言 数据库")
# → 检索到"Go开发者,PostgreSQL"
# → 直接用Go写PostgreSQL的连接池
五、记忆与规划的协同
规划(Planning)在第14课ReAct框架中已经介绍了概念。这里聚焦于:记忆系统如何让规划变得更好。
5.1 回顾:什么是规划
规划 = Agent在执行任务前,先生成一个完整的执行计划
ReAct(无规划):
想一步 → 做一步 → 想一步 → 做一步...
问题: 没有全局视角,容易走弯路
Plan-and-Execute(有规划):
先规划全部步骤 → 再逐步执行
优势: 有全局视角,步骤之间有依赖关系
5.2 记忆如何增强规划
没有记忆的规划:
每次都从零开始规划,不知道之前类似任务是怎么做的
→ 可能规划出低效的方案
→ 可能重复犯之前的规划错误
有记忆的规划:
规划前先检索"类似任务的历史规划"和"历史反思"
→ 参考成功的规划方案
→ 避免失败的规划路径
→ 规划质量随使用次数提升
类比:
没有记忆: 每次做项目都像第一次做,不参考任何历史经验
有记忆: 做项目前先看看之前类似项目的计划和复盘,站在经验上规划
实现:记忆增强的Plan-and-Execute
python
from openai import OpenAI
import json
client = OpenAI()
class MemoryEnhancedPlanner:
"""记忆增强的规划器"""
def __init__(self, long_term_memory: LongTermMemory, episodic_memory: EpisodicMemory):
self.ltm = long_term_memory
self.episodic = episodic_memory
def plan(self, goal: str, available_tools: list[str]) -> list[dict]:
"""生成执行计划(参考历史记忆)"""
# 1. 检索相关的历史经验
similar_episodes = self.episodic.recall_similar_episodes(goal, top_k=2)
past_failures = self.episodic.recall_failures(goal, top_k=2)
user_preferences = self.ltm.retrieve(goal, top_k=3)
# 2. 构建规划Prompt(注入记忆)
memory_context = ""
if similar_episodes:
memory_context += "\n## 类似任务的历史经验\n"
for doc in similar_episodes:
memory_context += f"- {doc.page_content[:200]}\n"
if past_failures:
memory_context += "\n## ⚠️ 历史失败教训(务必避免)\n"
for doc in past_failures:
memory_context += f"- {doc.page_content[:200]}\n"
if user_preferences:
memory_context += "\n## 用户偏好\n"
for doc in user_preferences:
memory_context += f"- {doc.page_content[:100]}\n"
# 3. 让LLM生成计划
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": f"""你是一个任务规划专家。根据目标生成执行计划。
{memory_context}
可用工具: {', '.join(available_tools)}
请参考历史经验来优化计划,避免历史失败中提到的问题。
输出JSON格式:
{{
"steps": [
{{"id": 1, "description": "步骤描述", "tool": "工具名", "depends_on": [], "rationale": "为什么这样做"}}
]
}}"""
}, {
"role": "user",
"content": f"目标: {goal}"
}],
response_format={"type": "json_object"},
temperature=0.0
)
plan = json.loads(response.choices[0].message.content)
return plan.get("steps", [])
def replan(self, goal: str, original_plan: list,
completed_steps: dict, failure_reason: str) -> list[dict]:
"""重规划: 执行中遇到问题时调整计划"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""原始计划执行中遇到问题,需要重新规划。
目标: {goal}
原始计划: {json.dumps(original_plan, ensure_ascii=False)}
已完成步骤: {json.dumps(completed_steps, ensure_ascii=False)}
失败原因: {failure_reason}
请生成新的计划(从失败点开始),避免同样的问题。
输出JSON格式: {{"steps": [...]}}"""
}],
response_format={"type": "json_object"},
temperature=0.0
)
new_plan = json.loads(response.choices[0].message.content)
return new_plan.get("steps", [])
六、记忆与反思的协同
反思(Reflection/Reflexion)在第14课也已介绍概念。这里聚焦于:记忆系统如何让反思真正发挥作用。
6.1 回顾:什么是反思
反思 = Agent执行任务后,评估结果,总结经验教训
没有反思的Agent:
失败了 → 简单重试 → 可能犯同样的错
有反思的Agent:
失败了 → 分析为什么失败 → 总结教训 → 带着教训重试 → 避免同样的错
关键: 反思本身不难,难的是"反思的结果存在哪里、下次怎么用"
→ 答案就是记忆系统!
6.2 反思 + 记忆 = 持续学习
Reflexion的完整流程:
┌──────────────────────────────────────────────────────────┐
│ │
│ 1. 执行任务 │
│ ↓ │
│ 2. 评估结果(成功/失败) │
│ ↓ │
│ 3. 如果失败 → 生成反思 │
│ "为什么失败了?下次怎么避免?" │
│ ↓ │
│ 4. 把反思存入长期记忆 ← 关键!没有这步,反思就白做了 │
│ ↓ │
│ 5. 重试任务(或下次遇到类似任务时) │
│ ↓ │
│ 6. 从记忆中检索相关反思 ← 关键!检索到才能避免重复犯错 │
│ ↓ │
│ 7. 带着反思信息执行 → 避免同样的错误 │
│ │
└──────────────────────────────────────────────────────────┘
没有记忆的反思:
反思完就忘了,下次还是犯同样的错
→ 反思只在当前会话内有效
有记忆的反思:
反思存入长期记忆,跨会话持久生效
→ Agent真正在"学习",越用越聪明
6.3 完整实现:Reflexion + 记忆
python
from openai import OpenAI
import json
client = OpenAI()
class ReflexionAgentWithMemory:
"""带记忆的Reflexion Agent: 能从失败中学习,跨会话积累经验"""
def __init__(self, tools: list, tool_map: dict, max_retries: int = 3):
self.tools = tools
self.tool_map = tool_map
self.max_retries = max_retries
self.memory = LongTermMemory(persist_directory="./reflexion_memory")
def execute(self, task: str) -> str:
"""执行任务(带反思和记忆)"""
for attempt in range(self.max_retries):
print(f"\n{'='*60}")
print(f"🔄 尝试 {attempt + 1}/{self.max_retries}")
# 1. 从记忆中检索相关反思
past_reflections = self.memory.retrieve(
query=f"反思 经验 {task}",
top_k=3
)
# 2. 构建System Prompt(注入历史反思)
system = "你是一个智能Agent,请完成用户的任务。\n"
if past_reflections:
system += "\n## ⚠️ 历史经验教训(务必参考,避免重复犯错)\n"
for doc in past_reflections:
system += f"- {doc.page_content}\n"
system += "\n请特别注意以上经验,避免犯同样的错误。\n"
# 3. 执行任务
messages = [
{"role": "system", "content": system},
{"role": "user", "content": task}
]
trajectory = [] # 记录执行轨迹
result = None
for step in range(7):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=self.tools,
temperature=0.0
)
msg = response.choices[0].message
if msg.content and not msg.tool_calls:
result = msg.content
trajectory.append(f"最终回答: {result[:200]}")
break
if msg.tool_calls:
messages.append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
trajectory.append(f"调用: {tc.function.name}({args})")
tool_result = self.tool_map[tc.function.name](**args)
result_str = json.dumps(tool_result, ensure_ascii=False)
trajectory.append(f"结果: {result_str[:100]}")
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result_str
})
# 4. 评估结果
is_success = self._evaluate(task, result, trajectory)
if is_success:
print(f"✅ 任务成功!")
# 成功也存储经验(正面经验)
self.memory.store_reflection(
task=task,
reflection=f"成功完成。关键步骤: {'; '.join(trajectory[:3])}",
success=True
)
return result
else:
# 5. 生成反思
reflection = self._reflect(task, trajectory, result)
print(f"🤔 反思: {reflection}")
# 6. 存入记忆(关键!)
self.memory.store_reflection(
task=task,
reflection=reflection,
success=False
)
return f"经过{self.max_retries}次尝试仍未成功。最后的反思已存入记忆,下次会做得更好。"
def _evaluate(self, task: str, result: str, trajectory: list) -> bool:
"""评估任务是否成功(用LLM-as-Judge)"""
if not result:
return False
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""评估以下任务执行结果是否成功:
任务: {task}
执行结果: {result[:500]}
判断标准:
1. 是否完整回答了任务要求
2. 是否有明显错误
3. 是否包含"无法完成"、"失败"等消极表述
只回答"成功"或"失败"。"""
}],
temperature=0.0
)
return "成功" in response.choices[0].message.content
def _reflect(self, task: str, trajectory: list, result: str) -> str:
"""生成反思"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""你刚刚执行了一个任务但结果不理想,请进行反思:
任务: {task}
执行轨迹: {chr(10).join(trajectory[-6:])}
最终结果: {result[:200] if result else '无结果'}
请简洁回答(2-3句话):
1. 哪里出了问题?
2. 下次应该怎么做才能避免?"""
}],
temperature=0.3
)
return response.choices[0].message.content
6.4 反思的质量很重要
好的反思 vs 坏的反思:
❌ 坏的反思(没有信息量):
"我需要做得更好"
"下次要更仔细"
"应该换一种方法"
✅ 好的反思(具体、可操作):
"查询user表时用了user_name字段,但实际字段名是username(无下划线)"
"排查延迟问题时应该先看连接池指标,而不是先看CPU"
"用户要的是Go代码,不是Python,下次先确认语言偏好"
如何引导LLM生成高质量反思:
1. 提供具体的执行轨迹(不是只说"失败了")
2. 要求回答"具体哪一步出了问题"
3. 要求回答"具体应该怎么改"(而不是泛泛而谈)
4. 限制反思长度(2-3句话,避免废话)
七、完整实战:从零构建一个有记忆的Agent
本节将所有知识整合,构建一个完整的、有记忆的Agent系统。
7.1 系统架构
┌─────────────────────────────────────────────────────────────────────────┐
│ 完整的记忆增强Agent架构 │
└─────────────────────────────────────────────────────────────────────────┘
用户输入
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Agent编排层 │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ 1.记忆检索 │ → │ 2.LLM推理 │ → │ 3.工具执行 │ │
│ │ │ │ │ │ │ │
│ │ 从长期记忆 │ │ 带记忆上下文│ │ 执行工具 │ │
│ │ 检索相关信息│ │ 的推理决策 │ │ 获取结果 │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ ↑ │ │
│ │ ▼ │
│ ┌────────────┐ ┌────────────┐ │
│ │ 6.记忆存储 │ ← ┌────────────┐ ← │ 4.结果评估 │ │
│ │ │ │ 5.反思生成 │ │ │ │
│ │ 存储重要信息│ │ (如果失败) │ │ 成功/失败? │ │
│ │ 和反思教训 │ └────────────┘ └────────────┘ │
│ └────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 长期记忆存储 │ │ 工具层 │
│ (Chroma向量DB) │ │ (MCP/本地工具) │
└──────────────────┘ └──────────────────┘
7.2 完整代码实现
python
"""
完整的记忆增强Agent实现
依赖: pip install openai chromadb langchain langchain-openai tiktoken
"""
from openai import OpenAI
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
from datetime import datetime
from dataclasses import dataclass, field
import json
import tiktoken
client = OpenAI()
# ═══════════════════════════════════════════════════════════════════
# Part 1: 记忆存储层
# ═══════════════════════════════════════════════════════════════════
class MemoryStore:
"""统一的记忆存储(向量数据库)"""
def __init__(self, persist_dir: str = "./agent_memory_db"):
self.embeddings = OpenAIEmbeddings()
self.vectorstore = Chroma(
embedding_function=self.embeddings,
persist_directory=persist_dir,
collection_name="agent_memories"
)
def add(self, content: str, metadata: dict) -> None:
"""添加一条记忆"""
metadata["timestamp"] = datetime.now().isoformat()
doc = Document(page_content=content, metadata=metadata)
self.vectorstore.add_documents([doc])
def search(self, query: str, top_k: int = 5,
filter_dict: dict = None) -> list[Document]:
"""语义检索"""
kwargs = {"k": top_k}
if filter_dict:
kwargs["filter"] = filter_dict
return self.vectorstore.similarity_search(query, **kwargs)
def count(self) -> int:
return self.vectorstore._collection.count()
# ═══════════════════════════════════════════════════════════════════
# Part 2: 记忆管理器(核心)
# ═══════════════════════════════════════════════════════════════════
class MemoryManager:
"""Agent记忆管理器: 统一管理短期/长期/情景记忆"""
def __init__(self, persist_dir: str = "./agent_memory_db"):
self.store = MemoryStore(persist_dir)
self.short_term: list[dict] = [] # 当前会话的对话历史
self.working: dict = {} # 当前任务的工作记忆
# ---- 短期记忆管理 ----
def add_message(self, role: str, content: str):
"""添加一条对话消息到短期记忆"""
self.short_term.append({"role": role, "content": content})
def get_messages(self, max_tokens: int = 6000) -> list[dict]:
"""获取短期记忆(带Token预算控制)"""
encoding = tiktoken.encoding_for_model("gpt-4o-mini")
# 从最新的开始往回取
result = []
total_tokens = 0
for msg in reversed(self.short_term):
msg_tokens = len(encoding.encode(msg.get("content", ""))) + 4
if total_tokens + msg_tokens > max_tokens:
break
result.insert(0, msg)
total_tokens += msg_tokens
return result
# ---- 长期记忆管理 ----
def remember(self, content: str, memory_type: str = "general",
importance: float = 0.5):
"""存入长期记忆"""
self.store.add(content, {
"type": memory_type,
"importance": importance
})
def recall(self, query: str, top_k: int = 5,
memory_type: str = None) -> list[str]:
"""从长期记忆中检索"""
filter_dict = {"type": memory_type} if memory_type else None
docs = self.store.search(query, top_k=top_k, filter_dict=filter_dict)
return [doc.page_content for doc in docs]
# ---- 情景记忆 ----
def record_episode(self, task: str, trajectory: list,
outcome: str, lessons: str):
"""记录一次完整的任务经历"""
content = f"任务: {task}\n结果: {outcome}\n经验: {lessons}"
self.store.add(content, {
"type": "episode",
"outcome": outcome,
"importance": 0.9 if outcome == "failure" else 0.7
})
# ---- 反思记忆 ----
def store_reflection(self, task: str, reflection: str):
"""存储反思"""
content = f"[反思] 任务: {task} | 教训: {reflection}"
self.store.add(content, {
"type": "reflection",
"importance": 0.9
})
def recall_reflections(self, task: str, top_k: int = 3) -> list[str]:
"""检索相关反思"""
return self.recall(f"反思 教训 {task}", top_k=top_k, memory_type="reflection")
# ---- 自动提取记忆 ----
def auto_extract_and_store(self, user_msg: str, assistant_msg: str):
"""从对话中自动提取值得记住的信息"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""从以下对话中提取值得长期记住的信息(用户偏好、重要事实、纠正内容)。
如果没有值得记住的,返回空列表。
用户: {user_msg}
助手: {assistant_msg}
输出JSON: {{"memories": [{{"content": "...", "type": "preference/fact/correction/decision", "importance": 0.0-1.0}}]}}"""
}],
response_format={"type": "json_object"},
temperature=0.0
)
try:
result = json.loads(response.choices[0].message.content)
for mem in result.get("memories", []):
self.remember(mem["content"], mem["type"], mem.get("importance", 0.6))
except:
pass # 提取失败不影响主流程
# ---- 工作记忆 ----
def set_working(self, key: str, value):
"""设置工作记忆"""
self.working[key] = value
def get_working(self, key: str, default=None):
"""获取工作记忆"""
return self.working.get(key, default)
def clear_working(self):
"""清除工作记忆(任务完成后)"""
self.working = {}
# ---- 生成记忆上下文(注入到Prompt中)----
def build_memory_context(self, current_query: str) -> str:
"""构建记忆上下文文本,用于注入到System Prompt"""
context_parts = []
# 长期记忆
relevant_memories = self.recall(current_query, top_k=5)
if relevant_memories:
context_parts.append("## 相关记忆")
for mem in relevant_memories:
context_parts.append(f"- {mem}")
# 反思记忆
reflections = self.recall_reflections(current_query, top_k=2)
if reflections:
context_parts.append("\n## ⚠️ 历史经验教训")
for ref in reflections:
context_parts.append(f"- {ref}")
# 工作记忆
if self.working:
context_parts.append("\n## 当前任务状态")
for k, v in self.working.items():
context_parts.append(f"- {k}: {v}")
return "\n".join(context_parts)
# ═══════════════════════════════════════════════════════════════════
# Part 3: 完整的记忆增强Agent
# ═══════════════════════════════════════════════════════════════════
class MemoryAgent:
"""完整的记忆增强Agent"""
def __init__(self, tools: list = None, tool_map: dict = None,
system_prompt: str = "你是一个有记忆的智能助手。",
persist_dir: str = "./agent_memory_db"):
self.tools = tools or []
self.tool_map = tool_map or {}
self.base_system_prompt = system_prompt
self.memory = MemoryManager(persist_dir)
self.max_steps = 7
def chat(self, user_input: str) -> str:
"""主对话入口"""
# 1. 记录用户消息到短期记忆
self.memory.add_message("user", user_input)
# 2. 构建记忆上下文
memory_context = self.memory.build_memory_context(user_input)
# 3. 组装完整的System Prompt
full_system = self.base_system_prompt
if memory_context:
full_system += f"\n\n{memory_context}\n\n请参考以上记忆信息来回答。"
# 4. 构建messages
messages = [{"role": "system", "content": full_system}]
messages += self.memory.get_messages()
# 5. 调用LLM(可能有工具调用循环)
result = self._llm_loop(messages)
# 6. 记录助手回复到短期记忆
self.memory.add_message("assistant", result)
# 7. 自动提取并存储长期记忆
self.memory.auto_extract_and_store(user_input, result)
return result
def _llm_loop(self, messages: list) -> str:
"""LLM调用循环(处理工具调用)"""
for step in range(self.max_steps):
kwargs = {
"model": "gpt-4o-mini",
"messages": messages,
"temperature": 0.0
}
if self.tools:
kwargs["tools"] = self.tools
response = client.chat.completions.create(**kwargs)
msg = response.choices[0].message
# 最终回答
if msg.content and not msg.tool_calls:
return msg.content
# 工具调用
if msg.tool_calls:
messages.append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
if tc.function.name in self.tool_map:
result = self.tool_map[tc.function.name](**args)
else:
result = {"error": f"未知工具: {tc.function.name}"}
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False)
})
return "达到最大步数限制。"
def reflect_and_learn(self, task: str, success: bool, details: str = ""):
"""手动触发反思(用于任务完成后的复盘)"""
if not success:
# 生成反思
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"""任务执行失败,请生成简洁的反思(2-3句话):
任务: {task}
详情: {details}
回答: 1.哪里出了问题 2.下次怎么避免"""
}],
temperature=0.3
)
reflection = response.choices[0].message.content
self.memory.store_reflection(task, reflection)
print(f"🤔 反思已存储: {reflection}")
else:
self.memory.record_episode(task, [], "success", details)
print(f"✅ 成功经验已存储")
# ═══════════════════════════════════════════════════════════════════
# Part 4: 使用示例
# ═══════════════════════════════════════════════════════════════════
if __name__ == "__main__":
# 创建Agent
agent = MemoryAgent(
system_prompt="你是一个全栈开发助手,帮助用户解决编程问题。"
)
# --- 第一次会话 ---
print("=== 会话1 ===")
# Agent会自动提取并记住用户偏好
r1 = agent.chat("你好,我叫张三,我是Go开发者,项目用PostgreSQL数据库")
print(f"Agent: {r1}\n")
r2 = agent.chat("帮我写一个数据库连接的工具函数")
print(f"Agent: {r2}\n")
# → Agent会用Go + PostgreSQL来写(因为记住了偏好)
# --- 模拟新会话(短期记忆清空,但长期记忆保留)---
print("\n=== 会话2(新会话)===")
agent.memory.short_term = [] # 清空短期记忆,模拟新会话
r3 = agent.chat("帮我写一个HTTP接口")
print(f"Agent: {r3}\n")
# → Agent从长期记忆中检索到"Go开发者",直接用Go写
# → 不需要用户再次说明语言偏好!
# --- 反思学习 ---
agent.reflect_and_learn(
task="写数据库连接函数",
success=False,
details="用了database/sql的Query方法但忘记关闭rows,导致连接泄漏"
)
# → 下次写数据库相关代码时,Agent会检索到这个反思
# → 会记得"要关闭rows"
7.3 运行效果演示
=== 会话1 ===
Agent: 你好张三!很高兴认识你。Go是一门很棒的语言,PostgreSQL也是非常可靠的数据库选择。有什么我可以帮你的吗?
[内部] 自动提取记忆:
- "用户叫张三" (type=fact, importance=0.7)
- "用户是Go开发者" (type=preference, importance=0.9)
- "项目使用PostgreSQL数据库" (type=fact, importance=0.8)
Agent: 好的,这是一个Go的PostgreSQL连接工具函数:
go
package db
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
func NewConnection(host, port, user, password, dbname string) (*sql.DB, error) {
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
return sql.Open("postgres", connStr)
}
=== 会话2(新会话)===
[内部] 检索长期记忆:
- "用户是Go开发者" (相关性: 0.85)
- "项目使用PostgreSQL数据库" (相关性: 0.72)
Agent: 好的,这是一个Go的HTTP接口示例(使用标准库net/http):
go
package main
import (
"encoding/json"
"net/http"
)
...
→ 注意: Agent直接用Go写了,没有问"你要用什么语言"!
→ 这就是长期记忆的价值。
=== 反思学习 ===
🤔 反思已存储: "写数据库查询时忘记关闭rows导致连接泄漏。
下次使用Query方法后必须defer rows.Close(),
或者使用QueryRow来避免手动关闭。"
→ 下次Agent写数据库代码时,会自动检索到这个反思
→ 会记得加 defer rows.Close()
八、工程最佳实践
8.1 记忆系统设计原则
原则1: 分层存储,按需检索
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 短期记忆放messages,工作记忆放State,长期记忆放向量DB
❌ 把所有东西都往上下文窗口里塞
原因: 上下文窗口是"寸土寸金"的,只放最相关的信息
原则2: 存储要精选,不是什么都存
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 只存"未来还会有用"的信息
❌ 把每一轮对话都存入长期记忆
原因: 存太多 → 检索时噪音大 → 干扰Agent输出
原则3: 检索要相关,不是越多越好
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 每次检索3-5条最相关的记忆
❌ 每次检索20条记忆全部塞进Prompt
原因: 无关记忆会干扰LLM的判断,还浪费Token
原则4: 反思优先级最高
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 检索记忆时,反思类记忆优先展示
❌ 反思和普通记忆混在一起
原因: 反思包含"哪里做错了"的关键信息,对避免重复犯错最有价值
原则5: 定期压缩和清理
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 定期合并相似记忆、清理过时信息
❌ 记忆只增不减,越来越臃肿
原因: 记忆质量比数量重要
原则6: 矛盾检测和更新
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 新信息与旧记忆矛盾时,更新旧记忆
❌ 矛盾的信息同时存在
原因: 矛盾信息会让Agent输出不一致
例: 旧记忆"用户用MySQL" + 新信息"用户迁移到PostgreSQL了"
→ 应该删除旧记忆,存入新信息
8.2 常见陷阱与解决方案
陷阱1: 记忆污染
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
问题: Agent错误理解了用户意图,把错误信息存入了长期记忆
后续所有对话都被这条错误记忆影响
例: 用户说"这个项目用Java"(指的是项目A)
Agent记住了"用户用Java"
后来用户问项目B的问题,Agent也用Java回答
解决:
- 存储时标注上下文("项目A用Java"而不是"用户用Java")
- 提供"纠正记忆"的机制(用户可以说"忘掉这个")
- 存储前让LLM确认理解是否正确
陷阱2: 检索噪音
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
问题: 检索到的记忆和当前问题不太相关,但被注入到了Prompt中
LLM被这些无关信息干扰,输出质量下降
例: 用户问"今天天气怎么样"
检索到"用户喜欢Go语言"(语义上有一点点相关性)
Agent回答时莫名其妙提到Go
解决:
- 提高相关性阈值(0.7以上才注入)
- 不是每次都检索(简单问题不需要记忆)
- 用LLM二次过滤(检索后让LLM判断哪些真的相关)
陷阱3: 上下文膨胀
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
问题: 记忆上下文 + 对话历史 + 工具定义 + 工具返回值
全部加起来超过了上下文窗口
解决:
- 设定Token预算: 记忆最多占2K tokens
- 动态调整: 工具多时减少记忆,记忆多时减少历史
- 分优先级: system > 工具定义 > 最近对话 > 记忆 > 早期对话
陷阱4: 隐私泄露
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
问题: 长期记忆中可能存储了用户的敏感信息
如果记忆被其他用户/会话访问到,就是隐私泄露
解决:
- 记忆按用户隔离(每个用户独立的记忆空间)
- 敏感信息脱敏后再存储
- 提供"清除所有记忆"的功能
- 定期审计记忆内容
8.3 生产环境的记忆系统选型
┌──────────────────┬────────────────────────────────────────────────────┐
│ 场景 │ 推荐方案 │
├──────────────────┼────────────────────────────────────────────────────┤
│ 个人项目/Demo │ Chroma (本地向量DB) + 文件持久化 │
│ │ 简单、免费、够用 │
├──────────────────┼────────────────────────────────────────────────────┤
│ 小团队产品 │ Chroma/Weaviate + Redis(KV) + PostgreSQL(元数据) │
│ │ 向量检索 + 精确查找 + 结构化存储 │
├──────────────────┼────────────────────────────────────────────────────┤
│ 大规模生产 │ Pinecone/Milvus + Redis Cluster + Neo4j(可选) │
│ │ 高可用、可扩展、支持百万级记忆 │
├──────────────────┼────────────────────────────────────────────────────┤
│ 框架集成 │ LangGraph + LangChain Memory │
│ │ 或 LlamaIndex Memory Module │
│ │ 框架内置的记忆管理,开箱即用 │
└──────────────────┴────────────────────────────────────────────────────┘
LangGraph内置记忆方案:
- MemorySaver: 基于checkpointer的短期记忆
- 自定义State: 工作记忆
- 外部向量DB: 长期记忆(需要自己集成)
8.4 记忆系统的评估指标
如何判断你的记忆系统是否工作良好?
1. 检索准确率 (Retrieval Precision)
检索到的记忆中,有多少是真正相关的?
目标: > 80%
2. 检索召回率 (Retrieval Recall)
所有相关的记忆中,有多少被检索到了?
目标: > 70%
3. 记忆利用率 (Memory Utilization)
注入到Prompt中的记忆,有多少真正影响了Agent的输出?
目标: > 60%
4. 反思有效率 (Reflection Effectiveness)
存储的反思中,有多少在后续任务中被成功利用?
目标: > 50%
5. 用户满意度
有记忆 vs 无记忆的Agent,用户满意度对比
目标: 有记忆的满意度显著更高
评估方法:
- A/B测试: 同一个Agent,开/关记忆系统,对比效果
- 人工评估: 抽样检查记忆检索的相关性
- 自动评估: 用LLM-as-Judge评估记忆是否被有效利用
九、总结与知识图谱
9.1 本课核心知识点
┌─────────────────────────────────────────────────────────────────────────┐
│ Agent记忆系统 - 知识图谱 │
└─────────────────────────────────────────────────────────────────────────┘
Agent记忆系统
│
┌───────────────┼───────────────┐
│ │ │
为什么需要 记忆类型 核心操作
│ │ │
┌───────┼───────┐ ┌──┼──┐ ┌──┼──┐
│ │ │ │ │ │ │ │ │
LLM无 上下文 无法 短期 工作 长期 存储 检索 压缩 遗忘
状态 窗口有限 跨会话 记忆 记忆 记忆
│ │ │
messages State 向量DB
滑动窗口 草稿 语义检索
摘要压缩 状态 时间衰减
重要性加权
┌───────────────┼───────────────┐
│ │ │
与规划协同 与反思协同 工程实践
│ │ │
记忆增强的 Reflexion+记忆 分层存储
Plan-and-Execute = 持续学习 按需检索
参考历史经验 跨会话积累经验 定期压缩
避免重复犯错 隐私保护
9.2 与前后课程的关系
第14课 ReAct推理行动框架
→ 提出了"纯ReAct缺乏记忆"的问题
→ 本课给出了完整的解决方案
第15课 MCP协议
→ 解决了"工具从哪来"的问题
→ 本课解决了"经验从哪来"的问题
第16课 Skill系统
→ Skill中可以包含"领域知识"(一种静态记忆)
→ 本课的记忆系统是动态的、可学习的
第18课 多智能体系统(下一课)
→ 多个Agent之间如何共享记忆?
→ 记忆系统是多Agent协作的基础设施之一
📝 作业
作业1:实现一个带记忆的对话Agent
基于本课的代码,实现一个能记住用户偏好的对话Agent。要求:
- 第一次对话时,用户告诉Agent自己的偏好
- 关闭会话后重新开始(清空短期记忆)
- 第二次对话时,Agent能从长期记忆中检索到偏好并使用
验证方法:第二次对话时不提任何偏好信息,看Agent是否能正确使用之前记住的偏好。
作业2:实现Reflexion学习循环
基于本课的ReflexionAgentWithMemory,实现一个能从失败中学习的Agent:
- 给Agent一个会失败的任务(如查询一个不存在的表)
- 第一次失败后,Agent生成反思并存入记忆
- 第二次执行同样的任务,Agent应该能避免同样的错误
验证方法:对比第一次和第二次的执行轨迹,确认Agent确实参考了反思。
作业3(进阶):实现记忆压缩
当长期记忆超过50条时,自动触发压缩:
- 合并相似的记忆
- 对超过30天的记忆进行摘要压缩
- 删除重要性低于0.3的记忆
下一篇文章见:AI系列文章导航目录-持续更新中