跳转至

4.4 记忆系统设计

一、核心概念

如果你用过 ChatGPT,一定遇到过这种情况:上一个对话窗口里你跟它聊了半天你的项目背景,新开一个窗口,它什么都不记得了。这不是 Bug,是 LLM 的本质特性——模型本身是无状态的,每次调用都是一次"失忆"。

Agent 系统里,记忆问题被放大十倍。一个需要执行 20 步任务的 Agent,必须记住:用户的意图是什么、第 3 步的工具调用结果是什么、用户之前说过"不要修改生产数据库"这条约束。如果做不到,Agent 要么反复追问用户,要么犯下灾难性的"遗忘错误"。

工程上,记忆系统面临三个核心矛盾:上下文窗口有限(贵且有上限)、信息重要性不均等(并非所有内容都值得记)、访问时机不一致(有些信息要在几秒内访问,有些要在几周后召回)。针对这三个矛盾,记忆系统被自然地分成三层:短期记忆(对话上下文管理)、长期记忆(向量存储的语义召回)、外部记忆(结构化数据库的精确查询)。三层各司其职,组合使用才能支撑复杂 Agent 场景。


二、原理深讲

短期记忆:对话上下文管理

短期记忆解决的是"这次对话里发生了什么"的问题。LLM 的输入就是它的工作记忆,所有上下文都以 Token 的形式存活在 messages 列表里。

问题在于 Token 有上限,而且费钱。一次多轮对话,每轮都要把历史全部重传,成本随对话长度线性增长。当历史超过窗口上限时,朴素地截断会直接丢失关键信息。三种主流策略:

滑动窗口裁剪:保留最近 N 轮对话,丢弃最早的消息。实现最简单,但会丢失早期建立的重要上下文(如用户在第 1 轮说的"我是法律从业者")。适合任务相对短平快、上下文依赖局部性强的场景。

摘要压缩:当历史超过阈值时,用 LLM 对早期历史生成摘要,用摘要替换原始消息。保留语义,大幅压缩 Token。代价是引入额外的 LLM 调用延迟和成本,摘要质量也依赖 LLM 能力。适合长对话、信息密度高的客服/助手场景。

重要性保留:给每条消息打重要性分数,优先保留高分消息。分数来源可以是:用户明确指令("记住这个")、包含约束/偏好的消息、最近一轮的消息。这是最灵活也最复杂的方案。

# 摘要压缩伪代码
def compress_history(messages, max_tokens=2000):
    if token_count(messages) < max_tokens:
        return messages

    # 将早期历史交给 LLM 摘要
    early_messages = messages[:-6]  # 保留最近 3 轮
    summary = llm.invoke(
        f"请将以下对话历史压缩为简洁摘要,保留关键信息:\n{early_messages}"
    )

    # 用摘要替换早期历史
    return [{"role": "system", "content": f"对话背景摘要:{summary}"}] + messages[-6:]

多轮对话状态追踪与实体提取:光管理消息还不够,Agent 还需要主动追踪对话中的实体状态。比如用户先说"帮我分析 NVDA",后说"再看看它的竞争对手",Agent 必须知道"它"指的是 NVDA。LangGraph 的 State 机制天然适合做这件事——把当前追踪的实体、任务状态、用户偏好存入 State,每轮更新。

策略 Token 节省 信息保真度 实现复杂度 适用场景
滑动窗口 低(丢早期上下文) 短对话、本地依赖强
摘要压缩 中(摘要有损) 长对话客服/助手
重要性保留 复杂任务 Agent

长期记忆:向量存储的语义召回

短期记忆解决"当前对话",长期记忆解决"跨会话"问题。用户上周提到他不喜欢冗长的回答,下周再开新对话时,Agent 应该记得。这类信息不适合放 Context(每次都带太贵),适合存向量数据库,按需检索。

记忆写入时机:何时值得记?

这是长期记忆设计中最容易被忽视的问题。如果什么都写,向量库很快变成垃圾场,检索时噪声极高。判断是否值得写入的经验规则:

  • 用户显式偏好:用户说"我喜欢/不喜欢 X"——值得记
  • 重要约束:用户说"不要使用外部 API"——必须记
  • 任务完成结果:Agent 完成了某个子任务的关键输出——值得记
  • 普通事实性对话:用户问今天天气——不值得记
  • 中间过程:工具调用的中间结果——通常不值得记(除非是关键决策点)

写入粒度也很重要:不要把整段对话写入,而是提炼成原子化的"记忆条目",每条包含:内容、来源时间戳、关联实体、重要性分数。

# 记忆条目结构
memory_entry = {
    "content": "用户偏好使用 DeepSeek 模型以控制成本",
    "entity": "user_preference",
    "created_at": "2025-04-01T10:00:00Z",
    "importance": 0.9,
    "source_session_id": "session_abc123"
}

记忆检索:时间衰减 + 相关性混合排序

检索时不能只用语义相似度——一条"用户三年前说过他在上海"的记忆,相关性可能很高,但时效性已经很低了。工程上,综合评分常用:

final_score = α * semantic_similarity + β * recency_score + γ * importance_score

recency_score = exp(-λ * days_elapsed)  # 指数衰减,λ 控制衰减速度

实践中 α=0.5、β=0.3、γ=0.2 是常见起点,根据业务场景调整。mem0 是目前做这块做得比较完整的开源库,直接封装了上述逻辑,生产项目可以直接用。


外部记忆:结构化数据库

向量检索擅长语义相似,但不擅长精确查询。"用户的订阅计划是什么"——这是一个精确查找,不是语义匹配。外部记忆用关系型数据库或图数据库存储结构化信息。

结构化记忆:用户画像 / 任务状态存 PostgreSQL

用户画像(姓名、偏好、历史行为)、Agent 任务状态(当前执行到第几步、子任务完成情况)都属于结构化信息,天然适合关系型数据库。设计要点:

  • 用户画像表需要版本管理,历史偏好变更要可追溯
  • 任务状态表需要幂等设计,Agent 崩溃后重启不能重复执行已完成步骤
  • 与 LangGraph Checkpoint 机制配合使用效果最好——Checkpoint 存 Agent 执行状态,PostgreSQL 存业务语义数据

图记忆:知识图谱存 Neo4j

当实体之间关系复杂时,关系型数据库力不从心。比如"张三 → 负责 → 项目 A → 依赖 → 系统 B → 由 → 李四维护"这类多跳关系,用 SQL 写起来痛苦,用 Cypher 查询则非常自然:

MATCH (user:Person {name: "张三"})-[:MANAGES]->(project:Project)
      -[:DEPENDS_ON]->(system:System)<-[:MAINTAINS]-(owner:Person)
RETURN project.name, system.name, owner.name

Graph RAG(Microsoft 2024)验证了图结构对多跳推理的增强效果——对于需要跨实体关系推理的问答场景,图记忆比纯向量检索准确率高出显著差距。

三层记忆系统整体架构

graph TD
    U[用户输入] --> A[Agent]
    A --> STM[短期记忆\n对话 Context]
    A --> |检索| LTM[长期记忆\n向量数据库]
    A --> |精确查询| EXT[外部记忆\nPostgreSQL / Neo4j]

    STM --> |超出阈值| COMP[摘要压缩]
    COMP --> STM

    A --> |写入| WRITE{值得记忆?}
    WRITE --> |是| LTM
    WRITE --> |结构化| EXT
    WRITE --> |否| DISCARD[丢弃]

    LTM --> |混合排序\n时间+相关性| A
    EXT --> A

三、工程视角:常见误区与最佳实践

误区一:把所有历史消息都塞进 Context正确做法:设定 Token 预算,超出阈值时主动触发压缩或裁剪。Token 预算建议控制在模型上下文窗口的 50-60%,留出足够空间给系统 Prompt、工具定义和输出。

误区二:长期记忆写入不加过滤,什么都存正确做法:设计记忆写入的"过滤层"——在每轮对话结束后,让 LLM 判断本轮是否产生了值得长期记忆的信息,并抽取成原子化条目再写入。宁愿漏记,也不要把向量库变成噪声库。

误区三:检索时只用余弦相似度,不考虑时效性正确做法:对于用户偏好、环境信息类记忆,必须引入时间衰减权重。三年前的"用户住在北京",不能和昨天的偏好同等对待。

误区四:用向量检索做精确查询(如"用户的 VIP 等级是多少")正确做法:精确查询走数据库,语义查询走向量库,两者不能互相替代。设计记忆系统时,明确区分信息的查询模式:需要精确匹配的存 PostgreSQL,需要语义召回的存向量库。

误区五:忽略记忆的隐私与安全边界正确做法:多用户系统中,记忆必须严格按用户 ID 隔离。向量库中每条记忆条目必须带 user_id 字段,检索时强制过滤。曾有案例因为没有做用户隔离,导致 Agent 把用户 A 的偏好"泄漏"给用户 B 的对话——这是严重的数据安全问题。


四、延伸思考

🤔 思考题:随着模型上下文窗口越来越大(已有模型支持 100 万 Token),长期记忆系统是否会变得多余?把所有历史对话直接塞进 Context 是否会成为更简单有效的方案?

这个问题没有标准答案,但值得认真思考两点反驳:(1)成本问题——100 万 Token 的输入在当前定价下仍然昂贵,压缩和选择性召回在经济上仍然有必要;(2)"Lost in the Middle"现象——研究表明 LLM 对超长 Context 中间部分的利用率显著下降,简单堆砌历史并不等于有效利用记忆。记忆系统的价值不只是"存",更是"选择性地召回"。