第 9 章 记忆系统

0 阅读8分钟

第 9 章 记忆系统

本章要解决的问题

Agent 需要记住用户偏好、历史操作和长期知识,但记太多会污染上下文——记忆该怎么设计?

章节大纲

  • 9.1 记忆分类:短期(上下文)/ 长期(向量库)/ 工作记忆
  • 9.2 记忆写入、检索与遗忘策略
  • 9.3 记忆与 RAG 的分工与配合
  • 🛠 解决方案:记忆污染/过期/混淆问题的解决模板

9.1 记忆分类:Agent 的"三层记忆"

9.1.1 人类记忆的启发

人类记忆分三层:工作记忆(眼前的事)、短期记忆(最近的事)、长期记忆(重要的事)。Agent 的记忆也照此设计——不是"要不要记忆"的问题,而是"什么记忆放哪一层"的问题。

9.1.2 三层记忆架构

┌────────────────────────────────────────────┐
│ 短期记忆(上下文窗口)                       │
│ 本次会话的消息/工具结果,会话结束即清空       │
│ 载体:messages 数组(第3章上下文管理)       │
├────────────────────────────────────────────┤
│ 工作记忆(任务期间)                         │
│ 当前任务的中间状态/局部变量,任务结束即清理    │
│ 载体:内存 / Redis(TTL)                   │
├────────────────────────────────────────────┤
│ 长期记忆(跨会话)                           │
│ 用户偏好/历史结论/沉淀知识,可跨会话保留       │
│ 载体:数据库 / 向量库(第8章技术)            │
└────────────────────────────────────────────┘

图 1:记忆三层架构

图 1:记忆三层架构

存活期容量载体读写频率呼应章节
短期记忆单次会话小(窗口内)messages每次请求第 3 章
工作记忆单次任务内存/Redis每步第 6 章
长期记忆跨会话DB/向量库会话起止本章

9.1.3 记忆设计的核心矛盾

记太多 → 上下文污染(记忆噪音干扰当前判断);记太少 → Agent"失忆"(答非所问、重复问)。

这个矛盾贯穿整个记忆设计。解法不是"多记"或"少记",而是分层 + 按需注入:默认只带少量核心记忆进上下文,大量记忆"存在外面、用时才取"(9.2 的检索式记忆)。

9.2 记忆写入、检索与遗忘

9.2.1 记忆的生命周期:写 → 存 → 取 → 忘

[写入] 从对话中提炼值得记住的信息(用户偏好/事实/结论)
   ↓
[存储] 按类型存:结构化事实→DB;非结构化内容→向量库
   ↓
[检索] 新会话开始时,按当前话题检索相关记忆注入上下文
   ↓
[遗忘] 过期/低价值记忆降权或删除(防止记忆污染)

图 2:记忆生命周期

图 2:记忆生命周期

9.2.2 写入:什么值得记住

不是所有对话都值得记忆。写入前先判断价值(用规则或一次模型判断):

MEMORY_RULES = [
    "用户明确表达的偏好(我喜欢/我习惯/请以后……)",
    "与用户身份相关的稳定事实(职业/城市/家庭)",
    "跨会话需要的承诺与约定(下周给你方案)",
    "长期结论(上次已确认的决策)",
]

def should_remember(utterance, client):
    """判断是否值得写入长期记忆"""
    # 规则初筛(零成本)
    if any(k in utterance for k in ["我喜欢", "我习惯", "请以后", "记住了", "记住"]):
        return True
    # 模型判断(长尾)—— 呼应"规则优先、模型兜底"心法
    resp = client.chat.completions.create(
        model="deepseek-chat",
        messages=[{"role": "user", "content":
            f"判断以下用户发言是否包含值得长期记住的用户信息。"
            f"只输出 JSON:{{\"worth_memory\": true/false, \"confidence\": 0.0~1.0}}\n发言:{utterance}"}],
        temperature=0.1,
        response_format={"type": "json_object"},
    )
    import json
    try:
        result = json.loads(resp.choices[0].message.content)
    except json.JSONDecodeError:
        return False  # 解析失败时不写入记忆(宁缺毋滥)
    return result.get("worth_memory", False) and result.get("confidence", 0) > 0.7

写入原则:宁缺毋滥。 记忆是稀缺资源(要占上下文),写错/写废的记忆比没有记忆更糟(污染)。

生产加固:用户发言(utterance)在写入记忆前应做脱敏处理(手机号、邮箱等 PII 正则替换,呼应第 22 章);记忆写入操作应加权限校验,确保只有授权来源才能写入;长期记忆应设过期和容量上限,避免无限增长。

9.2.3 存储:结构化 vs 向量化

记忆类型例子存储检索方式
结构化事实偏好/身份/约定关系型/键值库精确查询(按用户 ID)
非结构化内容历史讨论/笔记/结论向量库语义检索(按话题)
# 结构化记忆:按用户 ID 存取(Redis/DB)
def save_fact(user_id, key, value):
    db.set(f"mem:{user_id}:fact:{key}", value)

def load_facts(user_id):
    return db.mget(f"mem:{user_id}:fact:*")

# 非结构化记忆:向量检索(第8章技术)
def save_note(user_id, text):
    vec_db.add(text, metadata={"user_id": user_id})

def search_notes(user_id, topic, top_k=3):
    return vec_db.search(topic, filter={"user_id": user_id}, top_k=top_k)

多租户隔离:所有记忆都必须带 user_id,检索时强制过滤(呼应第 8 章元数据过滤、第 24 章 A5)。

9.2.4 检索:按需注入,不是全量倾倒

新会话开始时,只注入与当前话题相关的记忆,而不是把用户所有历史都塞进上下文:

图 3:记忆按需注入

图 3:记忆按需注入

def build_user_context(user_id, current_query):
    context = []
    # 1. 核心偏好(少量,总是带)
    context.append(load_facts(user_id, key="core_prefs"))
    # 2. 相关记忆(按当前话题检索)
    relevant = search_notes(user_id, current_query, top_k=3)
    context.extend(relevant)
    return context  # 注入 system 或独立 message

按需注入 = 记忆版的"按需投喂"(呼应第 3 章上下文编排、第 16 章上下文隔离)——同一哲学:默认只带必要的。

9.2.5 遗忘:记忆防污染的关键

记忆不清洗就会积累噪音甚至错误信息。遗忘策略:

策略做法适用
时间衰减超期记忆降权/删除偏好可能变化的记忆
冲突覆盖新信息与旧记忆冲突 → 更新旧记忆用户改了偏好
价值清理长期未命中/低分记忆归档控制存储膨胀
用户可控提供"忘记我"功能合规要求

记忆过期是常态:用户去年"喜欢咖啡",今年可能改喝茶——记忆必须有更新机制(呼应第 18 章学习漂移与回退保护)。

记忆更新机制:同一条事实变化时,应按 user_id + fact_key 做 upsert(覆盖旧值),而不是重复写入。示例:

def upsert_fact(user_id, key, value):
    """更新或插入记忆(覆盖旧值,而非追加)"""
    db.set(f"mem:{user_id}:fact:{key}", json.dumps({"value": value, "updated_at": time.time()}))

用户同意与合规:记忆写入前最好有显式或隐式同意,并提供"忘记我"功能(可撤销、可导出、可删除)。尤其涉及用户偏好、身份信息、跨会话记忆时,需遵守相关数据保护法规。

9.3 记忆与 RAG 的分工与配合

9.3.1 两者到底有什么区别

这是最常被混淆的问题。一句话区分:

图 4:记忆 vs RAG 分工

图 4:记忆 vs RAG 分工

RAG 回答"知识性问题"(客观资料),记忆回答"个人化问题"(用户相关)。

维度RAG(第 8 章)记忆(本章)
存什么知识库文档(公有的)用户记忆(私有的)
服务对象所有用户共享每个用户独立
更新频率低频(文档变更)高频(每次对话)
检索依据问题语义用户身份 + 话题
典型问题"报销标准是多少""我上次说到哪了"

9.3.2 技术同源,场景不同

两者技术上高度同源(都用向量库 + 检索),区别在数据源和检索入口:

RAG:  query → 向量检索(全库文档)→ 注入 → 回答知识问题
记忆:  query + user_id → 向量检索(该用户记忆)→ 注入 → 回答个性化问题

工程落地:可以用同一个向量库,用 metadata.scope 区分(scope=kb 知识库 / scope=user:{id} 用户记忆),检索时按 scope 过滤。

9.3.3 配合使用的场景

一个成熟的 Agent 常常两者并用:

def build_enhanced_context(user_id, question):
    # 1. 记忆:用户的个性化上下文
    user_ctx = load_user_context(user_id, question)
    # 2. RAG:问题的知识资料
    kb_ctx = rag.retrieve(question, top_k=3)
    # 3. 合并注入(记忆在前,知识在后)
    return f"""【用户背景】{user_ctx}
【参考资料】{kb_ctx}
请结合用户背景,基于参考资料回答。"""

典型例子:用户问"适合我的报销方案"——记忆提供"该用户是销售岗"(个性化),RAG 提供"公司报销制度"(知识),两者合成才是好答案。记忆管"你是谁",RAG 管"世界是什么"。

🛠 解决方案:记忆污染/过期/混淆问题的解决模板

常见问题

  1. "记忆把上下文撑爆了":全量倾倒记忆。对策:按需注入(9.2.4)+ 核心偏好限量(最多几条)。
  2. "记忆过时,答错信息":记忆未更新。对策:冲突覆盖(用户新说法覆盖旧记忆)+ 时间衰减(9.2.5)。
  3. "A 用户的记忆串到 B 用户":缺 user_id 隔离。对策:所有记忆带 user_id + 检索强制过滤(9.2.3)。
  4. "记忆里有错误信息,越用越错":写入时未校验。对策:写入前判断价值 + 低置信度不写(9.2.2);错误记忆可被用户纠正覆盖。
  5. "不知道怎么区分记忆和 RAG":知识问题走 RAG,个人问题走记忆(9.3.1);拿不准就用"合并注入"方案(9.3.3)。

解决方案速查表

现象根因解决方案
上下文爆全量倾倒按需注入 + 限量
记忆过时无更新机制冲突覆盖 + 时间衰减
记忆串号无隔离user_id 过滤
记忆污染写入不校验价值判断 + 低置信不写
记忆/RAG 混淆场景不清知识→RAG、个人→记忆

实战提示

  1. 记忆设计三问:存什么(写入判断)、怎么取(按需检索)、何时忘(遗忘策略)——缺一个都不完整。
  2. 宁缺毋滥:写错记忆比没有记忆更糟(污染上下文)。
  3. 隔离是底线:多用户场景,user_id 过滤必须强制,否则就是数据事故。
  4. 记忆要有生命周期:写 → 存 → 取 → 忘,遗忘和写入一样重要。
  5. 记忆是第 18 章学习的载体:学习到的用户偏好最终沉淀为记忆——两章天然衔接。