Agent开发 6.1 - 认知记忆体系分层设计:工作记忆、情节记忆与语义记忆的读写机制

2026-10-29 | 80分钟 | yrobot | Agent开发, 记忆系统, 工作记忆, 情节记忆, 语义记忆, MemGPT, 混合检索, 生命周期

在构建长周期运行的 Agent 应用(如个人编程助手、长期留学规划顾问或企业级技术客服)时,系统不仅要处理用户当前的一两句提问,更要面对数周甚至数月内持续累积的海量历史交互。

如果用户在三个月前提到过“我本地主力语言是 TypeScript,开发环境为 macOS”,一个月前排查过具体的 Docker 端口占用问题,而今天发来一句话:“帮我写个启动脚本,按我之前的环境来配置”。大模型如果无法获取过去的背景信息,就只能给出通用的模板代码;但若简单粗暴地把过去数百轮的全部历史消息塞进上下文窗口,单次调用的 Token 成本和响应时延将大幅上升,模型还会陷入注意力被稀释的“迷失在中间(Lost in the Middle)”困境,甚至直接超出上下文长度限制而崩溃。

解决这一矛盾的核心,在于为 Agent 建立分层清晰的认知记忆体系。


从玩具教材到生产现实:简单存储的三个断层

在许多开源入门教程中,对“记忆”的实现往往非常简化:要么直接在内存数组中追加 messages 列表,要么在数据库中建立一张聊天记录表,在每次用户发问时,直接将整张表或近期文本无差别计算向量并存入向量数据库,再通过相似度搜索(Cosine Similarity)将排名前几的记录拼入 Prompt。

在真实的商业业务中,这种无差别的存储与检索方式会迅速暴露三个断层:

  1. 注意力被历史噪声污染:用户的日常问候、随口吐槽以及排查故障时产生的大段报错日志,如果都被无差别切块并存入向量库,在后续检索时就极易因为微弱的字词重叠而被召回,向当前上下文注入大量与任务无关的垃圾信息。
  2. 纯语义检索的符号盲区:向量模型擅长捕捉自然语言的“主题和意图”,但不擅长精确匹配具体的数字、代码符号或临时配置。例如用户询问“上次在 Docker 里跑 Node 脚本映射的端口号是多少”,纯向量检索计算的是“容器”、“端口”等高维语义,往往会把 MySQL 的 3306 端口、Nginx 的 443 端口混在一起召回,反而很难精准命中具体的端口数值。
  3. 缺乏主动提炼与事实更新:原始对话是琐碎且口语化的。当用户表达“把我的主力语言改成 Rust 吧”时,系统需要做的是更新底层结构化的事实属性,而不是在历史消息流末尾简单追加一段聊天文本,否则大模型在面对相互冲突的历史记录时很容易产生混淆。

为了解决这些现实问题,工业界演化出了成熟的架构参考:MemGPT(现更名为 Letta) 借用操作系统的多级存储模型,将上下文划分为常驻内存与外部换出存储;Mem0 与 Zep 则在后台提供自动化的实体识别与事实演化图谱。


一、 三层认知记忆的代码物理本质

认知科学将人类记忆划分为感觉记忆、工作记忆、情景记忆与语义记忆。映射到软件工程中,这些概念对应着完全确定的数据结构与存储载体:

flowchart TD
    subgraph LLM_LAYER ["大模型推理层"]
        LLM["大模型 (LLM 推理核心)"]
    end

    subgraph WM_LAYER ["1. 工作记忆 (Working Memory)"]
        WM["即时 Prompt 上下文 (毫秒级访问)
- 系统基础设定 (System Prompt)
- 核心活跃画像 (Active Profile)
- 刚刚召回的相关事件 (Retrieved Context)
- 最近几轮对话记录 (Recent Turns)"] end subgraph EM_LAYER ["2. 情节记忆 (Episodic Memory)"] EM["事件与轨迹流数据库 (带时间戳的时序日志)
- 存储形态: 关系型数据库 + 向量索引
- 检索方式: 时间过滤 + BM25 关键词 + 向量相似度
- 作用: 复盘排查、历史事件回溯"] end subgraph SM_LAYER ["3. 语义记忆 (Semantic Memory)"] SM["结构化事实与实体画像 (脱离时间流的知识库)
- 存储形态: Key-Value 字典 / 文档 / 知识图谱
- 检索方式: 键名精确读取 O(1)
- 作用: 用户偏好、系统规则、确定性决策依据"] end LLM <-->|"读写当前上下文"| WM LLM -.->|"工具调用 / 后台调度"| EM LLM -.->|"事实提炼 / 确定性注入"| SM

1. 工作记忆(Working Memory)

  • 代码本质:单次 API 请求直接送入大模型的上下文窗口数组(Context Window)。
  • 存储载体:Node.js 或服务端运行时的临时内存变量。
  • 内容构成:系统提示词、从语义记忆抽取的当前活跃画像、刚刚检索出的少量上下文、以及最近 3~5 轮的对话消息。
  • 物理特性:访问延迟为零,但容量严格受限,会话结束或单步完成后即可重置。

2. 情节记忆(Episodic Memory)

  • 代码本质:带时间戳与事件因果链的结构化日志流(Timeline Log Stream)。
  • 存储载体:PostgreSQL / SQLite 结合向量插件(如 pgvector)。
  • 内容构成:每次交互的具体事件、使用的工具、返回的客观结果以及当时的执行上下文。
  • 物理特性:保留了事件发生的来龙去脉,按时间序列持久化归档,主要用于历史复盘、故障排查与经验比对。

3. 语义记忆(Semantic Memory)

  • 代码本质:从历史经历中脱水提炼出的结构化事实字典(Fact Profile / Key-Value Store)。
  • 存储载体:Redis 或关系型数据库的单行 JSON 字段。
  • 内容构成:独立于特定对话时间的客观事实(例如:{ primary_language: "Rust", target_os: "macOS" })。
  • 物理特性:脱离了口语化的上下文,具备版本管理与状态覆盖能力,读取时可以直接通过键名进行 O(1) 的确定性匹配。

二、 检索机制剖析:为什么必须采用混合检索?

要理解情节记忆的读取,必须先厘清向量相似度搜索的物理运作机理。

1. 向量化与余弦相似度的物理本质

Embedding 模型是一个高维语义映射器,它将任意长度的文本转换为固定维度的浮点数数组(如 1536 维)。这串数组是该文本在多维语义坐标系中的位置坐标。

余弦相似度(Cosine Similarity)通过计算两个向量在多维空间中的夹角余弦值来衡量相关性:夹角越小,余弦值越接近 1,代表两句话在语义主题上越接近。

2. 向量检索的局限与翻车点

纯向量检索的强项在于“模糊语义理解”(例如理解“电脑开不了机”和“PC 无法启动”在讨论同一件事),但其弱项恰恰是“精确符号匹配”:

假设情节数据库中存储了以下三条历史记录:

  • 记录 A:在 Docker 容器中启动 Node 服务,端口映射为 8080
  • 记录 B:配置 Nginx 反向代理,监听 443 端口
  • 记录 C:MySQL 容器连接超时,排查发现宿主机 3306 端口被占用

当查询输入为 “上个月 Docker 里跑 Node 脚本的端口号是多少” 时,这三条记录因为都高频出现“容器”、“端口”、“服务”、“脚本”等词汇,其向量在高维空间中的夹角非常小。由于具体的端口数字(8080、443、3306)在 1536 个浮点数中权重被极度平摊,余弦相似度无法保证记录 A 一定排在第一位。

3. 工业界的标准解法:混合检索(Hybrid Search)

在生产级工程中,不能单纯依赖向量检索,必须组合三种检索能力:

  1. 关键词精确检索(如 BM25 算法):通过词频与逆文档频率,对罕见的特定标识符(如 Node、Docker、特定错误码)给予高权重,确保字面精确命中;
  2. 稠密向量检索(Dense Vector Search):负责跨词汇的同义意图理解,兜底自然语言表达差异;
  3. 元数据硬过滤(Metadata Filtering):在搜索计算前,先通过数据库 SQL 语句排除超出时间范围或不同会话的脏数据(例如:WHERE timestamp >= '2026-09-01')。

三、 记忆的主动生命周期:Recall、Retain 与 Reflect

记忆系统不是静态的数据库读写,它拥有完整的生命周期循环:

sequenceDiagram
    autonumber
    actor User as 用户
    participant Gateway as 运行时调度器 (Harness)
    participant WM as 工作记忆 (Context)
    participant LLM as 大模型 (推理决策)
    participant SM as 语义记忆 (Profile Store)
    participant EM as 情节记忆 (Event Store)

    User->>Gateway: 输入请求 ("改用 Rust 开发,并启动之前的 Node 脚本")
    
    rect rgb(240, 248, 255)
        note over Gateway,WM: 1. Recall 检索阶段
        Gateway->>SM: 读取当前有效画像 (primary_language, os)
        Gateway->>EM: 混合检索匹配历史情节 ("Node 脚本启动")
        Gateway->>WM: 组装 Prompt (工作记忆注满)
    end

    WM->>LLM: 触发单步推演
    LLM->>Gateway: 输出决策行动与最终回答
    Gateway->>User: 响应结果

    rect rgb(255, 250, 240)
        note over Gateway,EM: 2. Retain 沉淀阶段
        Gateway-->>EM: 异步追加事件日志流水 (原始请求、动作、工具输出)
    end

    rect rgb(245, 255, 245)
        note over Gateway,SM: 3. Reflect 反思阶段
        Gateway-->>SM: 异步提炼事实变更 (识别到语言变更,将 primary_language 写入 Rust)
    end
  1. Recall(检索召回): 在向大模型发起调用之前,根据用户当前的输入意图,从语义记忆读取当前生效的静态偏好,并从情节记忆中检索出相关性最高的 1~2 条历史经验,注入工作记忆。
  2. Retain(保留沉淀): 在单次交互闭环后,将本次交互的原始用户输入、模型决策、工具调用参数及执行结果,打上时间戳完整写入情节记忆流。
  3. Reflect(反思提炼): 在后台异步执行事实提炼。如果交互中包含了长效规则或偏好变更(例如用户声明了技术栈变化),提取器将其结构化为 Key-Value 事实更新至语义记忆,并记录变更时间与历史版本,实现知识的脱水与演进。

四、 控制流权衡:MemGPT 显式工具调用 vs 隐式前置预加载

在调度时机上,业界存在两种不同的控制流流派:

方案维度 方案 A:MemGPT 显式自主调用流 方案 B:隐式前置切面流 (Pre-Hook) 方案 C:双轨组合架构 (推荐实践)
工作机制 将记忆封装为工具(recall_memory, update_profile),由大模型在 ReAct 循环中自主决定是否调用 在进入大模型前,通过代码拦截器自动执行意图提取与检索,直接塞入 Prompt 前置切面快速预加载基础画像;主模型上下文仍保留显式检索工具作为兜底
首字时延 较高。每次检索都需要多消耗一轮大模型往返(通常增加 1~2 秒) 极低。仅增加毫秒级数据库查询,大模型单次往返直接产出内容 极低。常态下单次往返,复杂回溯时按需二次触发
模型依赖度 极高。小参数模型或复杂提示下容易出现漏调、假装记得或调用参数错误 极低。由确定性代码逻辑驱动,不存在模型漏调的问题 适中。基础事实由确定性代码保证,深层回溯依赖模型推理
复杂查询适应 极佳。模型推演到第 3 步发现缺少依赖时,可中途灵活发起针对性查询 较弱。若用户输入存在语义重心偏移,刚进门时的整句检索容易召回无关记录 极佳。快模型改写查询保障前置准确率,自主工具处理复杂推演

隐式预加载中的“语义重心偏移”故障

如果采用简单的方案 B,直接把用户的整句话送入检索器,经常会遇到语义偏移:

  • 用户输入:“根据我上上周确定的那个数据库选型,帮我写对应的迁移脚本”
  • 问题:句子末尾的“写迁移脚本”是当前的动作指令,占据了句子的大部分字词;而真正需要检索的记忆实体是前半句的“数据库选型”。直接用整句做向量搜索,检索器往往会去抓取历史项目中关于“迁移脚本”的记录,反而漏掉了数据库选型结论。

工业级双轨解法:快模型查询改写(Query Rewriting)+ 自主兜底

解决这一问题的标准手段是:在进入前置检索切面时,先调用极小参数量的快速模型(如 50ms 响应的轻量模型),将用户的原始输入分解为两部分:

  1. 当前动作指令:写迁移脚本;
  2. 记忆检索意图:查找关于“数据库选型”的决策结论。

拿着提炼后的纯净意图进行混合检索,既能消除语义偏移,又能避免主模型多耗费一轮昂贵的工具往返。


五、 TypeScript 最小高信噪比微内核实现

以下 TypeScript 代码演示了三层认知记忆的数据模型、快模型意图提炼、混合过滤与 3R 生命周期的调度机制:

// ==========================================
// 1. 三层记忆的数据模型定义
// ==========================================

/** 语义记忆:长期稳定的用户画像与事实字典 */
export interface SemanticProfile {
  [key: string]: {
    value: string;         // 当前生效值,如 "database": "PostgreSQL"
    updatedAt: string;     // 更新时间
    history: string[];     // 变更审计轨迹,避免旧事实直接丢失
  };
}

/** 情节记忆:带时间戳与上下文因果链的事件日志 */
export interface EpisodicRecord {
  id: string;
  timestamp: string;
  topic: string;           // 主题,如 "技术选型决策"
  event: string;           // 具体事件事实
  keywords: string[];      // 显式提取的关键词,用于 BM25 精确命中
  embedding?: number[];    // 语义向量,用于同义意图模糊匹配
}

/** 工作记忆:当前请求直接输入大模型的即时上下文窗口 */
export interface WorkingMemory {
  systemPrompt: string;
  activeProfile: Record<string, string>; // 从语义记忆抽取的当前生效画像
  recalledEvents: string[];              // 刚刚召回的 1~2 条相关情节
  messages: Array<{ role: "user" | "assistant"; content: string }>;
}

// ==========================================
// 2. 认知记忆调度器实现
// ==========================================

export class CognitiveMemoryHarness {
  private semanticStore: SemanticProfile = {};
  private episodicStore: EpisodicRecord[] = [];

  /**
   * [Recall - 检索召回]
   * 采用查询改写与双模过滤,避免整句语义重心偏移
   */
  async recall(rawUserQuery: string): Promise<{ profile: Record<string, string>; relevantEvents: string[] }> {
    // 步骤 1:轻量提取真实检索实体与关键词(消除当前动作指令的噪音干扰)
    const intent = await this.fastExtractIntent(rawUserQuery);

    // 步骤 2:直接从语义字典提取当前确切事实,O(1) 复杂度,零幻觉风险
    const activeFacts: Record<string, string> = {};
    for (const key of intent.profileKeys) {
      if (this.semanticStore[key]) {
        activeFacts[key] = this.semanticStore[key].value;
      }
    }

    // 步骤 3:情节库双模匹配(关键词精确覆盖 + 向量语义兜底)
    const matchedEvents = this.episodicStore
      .filter((record) => {
        // 核心关键词命中判断(保证特定数字、专有名词不丢失)
        return record.keywords.some((kw) => intent.keywords.includes(kw));
      })
      .slice(0, 2)
      .map((e) => `[${e.timestamp}] ${e.event}`);

    return { profile: activeFacts, relevantEvents: matchedEvents };
  }

  /**
   * [Retain - 保留沉淀]
   * 会话结束后,原汁原味记入事件流水
   */
  async retain(event: string, topic: string, keywords: string[]): Promise<void> {
    this.episodicStore.push({
      id: `evt_${Date.now()}`,
      timestamp: new Date().toISOString(),
      topic,
      event,
      keywords,
    });
  }

  /**
   * [Reflect - 反思提炼]
   * 异步任务:将自然语言口语提炼为结构化事实并维护版本
   */
  async reflect(updates: Array<{ key: string; value: string }>): Promise<void> {
    for (const { key, value } of updates) {
      const current = this.semanticStore[key];
      const history = current ? [...current.history, current.value] : [];
      
      this.semanticStore[key] = {
        value,
        updatedAt: new Date().toISOString(),
        history,
      };
    }
  }

  /** 模拟轻量模型在 50ms 内完成的意图与关键词抽取 */
  private async fastExtractIntent(query: string): Promise<{ profileKeys: string[]; keywords: string[] }> {
    if (query.includes("数据库")) {
      return { profileKeys: ["database"], keywords: ["技术选型", "数据库", "PostgreSQL"] };
    }
    return { profileKeys: [], keywords: [] };
  }
}

六、 故障实测注入:无差别向量库 vs 分层认知治理

为了验证分层记忆架构在消除噪声干扰上的有效性,我们在测试套件中注入一组典型的冲突对话:

注入背景数据

  1. 30 天前(历史废话与报错记录):
    "以前老系统的 Python 项目跑迁移脚本真慢,SQLite 本地单文件性能太差了"
  2. 14 天前(正式架构决策):
    "团队开会决议:新系统全线采用 PostgreSQL 作为主数据库"
  3. 今天用户发问:
    "帮我写对应的迁移脚本,连接到我们定下来的数据库"

运行结果对比

[模式 A: 玩具教程的无差别全量向量检索]
1. 用户输入 "帮我写对应的迁移脚本..." 传入 Embedding 算余弦相似度
2. 检索器评分结果:
   - 匹配度 0.88: "以前老系统的 Python 项目跑迁移脚本真慢,SQLite 本地单文件性能太差了" (由于'迁移脚本'多次重合被高分命中)
   - 匹配度 0.71: "团队开会决议:新系统全线采用 PostgreSQL 作为主数据库"
3. 上下文组装: 将包含 SQLite 的废话记录排在第一名塞给大模型
4. 最终输出: 大模型参考第一条记录,写出了 SQLite 的迁移连接代码!(业务发生严重偏差)

--------------------------------------------------------------------------------

[模式 B: 生产级分层认知记忆治理]
1. 快模型提取检索意图: { profileKeys: ["database"], keywords: ["数据库"] }
2. 语义记忆 (Semantic Store) O(1) 精准命中: { database: "PostgreSQL" }
3. 情节记忆检索: 排除无关的 SQLite 废话,锁定 14 天前的主题决策
4. 最终输出: 无论历史上有多少关于迁移脚本的随口吐槽,大模型依据语义记忆 100% 确认主数据库为 PostgreSQL,准确生成目标脚本。

总结

Agent 记忆系统的设计,本质上是在有限的上下文窗口、不可预测的历史对话长度与精确的业务事实需求之间寻找最优平衡:

  1. 破除单一载体迷思:不要把所有信息都一股脑推给向量数据库。工作记忆负责即时推演,情节记忆负责时间线追踪与经验比对,语义记忆负责保存高度提炼的确定性事实。
  2. 落实 3R 生命周期:检索召回(Recall)要结合查询意图改写以防御语义重心偏移;事件日志(Retain)保持原始客观;事实提炼(Reflect)异步提炼并保留版本,确保事实演进不失真。
  3. 拥抱双轨工程实践:以低延迟的前置隐式检索作为常规垫底,以大模型自主的显式工具调用作为长程深度推演的兜底。