17-大模型智能体开发工程师:深入学习Agent记忆系统

系列文章导航: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。要求:

  1. 第一次对话时,用户告诉Agent自己的偏好
  2. 关闭会话后重新开始(清空短期记忆)
  3. 第二次对话时,Agent能从长期记忆中检索到偏好并使用

验证方法:第二次对话时不提任何偏好信息,看Agent是否能正确使用之前记住的偏好。

作业2:实现Reflexion学习循环

基于本课的ReflexionAgentWithMemory,实现一个能从失败中学习的Agent:

  1. 给Agent一个会失败的任务(如查询一个不存在的表)
  2. 第一次失败后,Agent生成反思并存入记忆
  3. 第二次执行同样的任务,Agent应该能避免同样的错误

验证方法:对比第一次和第二次的执行轨迹,确认Agent确实参考了反思。

作业3(进阶):实现记忆压缩

当长期记忆超过50条时,自动触发压缩:

  1. 合并相似的记忆
  2. 对超过30天的记忆进行摘要压缩
  3. 删除重要性低于0.3的记忆

下一篇文章见:AI系列文章导航目录-持续更新中

相关推荐
数据仓库搬砖人1 小时前
LangGraph 原理深度解析:为什么它是目前最适合构建 Agent 的框架
人工智能
孟陬1 小时前
国外技术周刊 #139:LLM 正在杀死程序员的「懒惰美德」
前端·人工智能·后端
Peter·Pan爱编程2 小时前
23. 算法库:用算法代替手写循环
c++·人工智能·算法
Nile2 小时前
Claude Code-Dynamic Workflows:1.为什么用工作流?
人工智能·ai·ai编程·ai-native
狂炫冰美式2 小时前
AI 生成 Draw.io,导入飞书/Lark 画板后可编辑
前端·人工智能·后端
战族狼魂2 小时前
从零构建企业级Hermes-Agent:复杂任务拆解、工具协同与安全落地实践
开发语言·人工智能·python
o561-6o623o7鹿2 小时前
陈,生理实验系统虚实结合型 生理学实验系统 生理学实验系统软件
人工智能
继续商行2 小时前
Go 并发原语深度剖析:Channel 与 Mutex 的性能博弈
人工智能
yaoxiaoganggang2 小时前
克隆 Superpowers 的规则库到你的本地(或者直接作为 Git Submodule)
人工智能·经验分享·git·ai编程