前端转型 Agent 开发 06 之 Agent Memory 记忆系统(让 Agent 更智能,更懂你)

前述:因为模型本身无状态信息存储的,虽然 Checkpointer 只保存单个会话信息,而 Memory 则能让你跨会话记住用户信息。

一、基础知识

1.1、为啥需要 Memory

LLM 是无状态 的------每次调用都是独立的,模型完全不记得"上一轮你跟它说了什么"。Checkpointer 解决了单次会话内的记忆(靠把 messages 数组存档),但它有两个硬伤:

  1. 会话结束就失效------新开一个会话(thread_id 变了),Checkpointer 里上一个会话的历史就读不到了;
  2. 粒度太粗 ------Checkpointer 存的是完整的对话流水,你想要"用户偏好辣的""用户叫张三"这种提炼后的长期事实,它给不了。

Memory 解决的就是跨会话的、提炼后的长期记忆------让 Agent 在任何一次新对话里,都能先"想起"这个用户是谁、喜欢什么、之前聊过什么重要决策。

维度 无 Memory 有 Memory
新会话开场 Agent 不认识用户,每次从零开始 Agent 知道"这是张三,喜欢简洁回答,上次在搞 React 项目"
个性化 千人一面,同一套 system prompt 按用户偏好动态调整 prompt
连续性 用户每次都要重新交代背景 Agent 主动回忆相关历史,减少用户重复输入
合规 无法按用户删除数据 按 namespace 删除某用户全部记忆(GDPR)

1.2、三层记忆系统架构

一个完整的 Agent 记忆系统通常是三层结构,各司其职:

层 名称 存什么 生命周期 对应实现
1 工作记忆(Working) 当前这一轮推理的临时变量 单次 invoke LangGraph State
2 短期记忆(Short-term) 当前会话的对话历史(会压缩) 单个会话 摘要压缩器(监控 token 用量,触发压缩)
3 长期记忆(Long-term) 跨会话的用户偏好/事实/决策 永久 BaseStore(向量/KV)

可以类比人脑:工作记忆 = 你现在脑子里想的事;短期记忆 = 今天发生过的事(睡一觉可能忘);长期记忆 = 你的名字、技能、人生经历(几乎不会忘)。Agent 要像人一样智能,三层缺一不可。

1.3、长期记忆与短期记忆

长期记忆

长期记忆存的是跨会话、提炼后的结构化信息------不是原始对话流水,而是从中抽取的"事实"。常见类型:

  • 用户偏好:喜欢简洁回答、偏好 TypeScript、习惯用 pnpm
  • 客观事实:用户叫张三、是前端工程师、在用 React 19
  • 长期目标:正在做一个叫 Apollo 的项目、目标是做 AI Agent
  • 历史决策:上周决定用 SQLite 而不是 Postgres、理由是嵌入式部署

长期记忆需要主动写入------每次对话结束后,用一个"记忆提取"步骤,让模型从对话中识别值得长期记住的信息,写入 store。

短期记忆

短期记忆存的是当前会话的对话历史 ,它的核心挑战是上下文窗口有限。一个聊了 100 轮的会话,如果全量保留历史,token 早就爆了。

生产环境中常见的做法(封装为一个短期记忆管理器):

  1. 实时监控 token 占用(用 usage_metadata);
  2. 当占用达到 75% 阈值时,触发摘要压缩------把最早的历史消息让模型总结成一段 summary;
  3. 压缩后,原始的 N 条历史消息被替换成 1 条 summary 消息,token 占用骤降;
  4. 当占用达到 90% 时,更激进地丢弃最早的非关键消息。

这样会话能持续很久,而早期信息以"压缩摘要"的形式保留,不会完全丢失。

1.4、命名空间区分

长期存储中用于隔离不同用户或不同类型记忆的键空间。

为什么需要 Namespace?

  • 多用户隔离:Alice 的偏好不能影响 Bob;
  • 多类型隔离:偏好(preferences)和事实(facts)分开存储,便于按类型检索;
  • 隐私边界:可以按 namespace 删除/导出用户数据(GDPR 合规)。

常见的 namespace 模式:

  • (userId, preferences) ------ 用户偏好
  • (userId, facts) ------ 关于用户的客观事实
  • (userId, goals) ------ 长期目标
  • (teamId, decisions) ------ 团队决策历史

二、Memory 的使用

2.1、基础主动使用

Write(写入)

从对话中提取值得记住的信息,主动写入 store;

javascript 复制代码
import { InMemoryStore } from '@langchain/langgraph';

// ① 创建 store(生产环境用持久化后端)
const store = new InMemoryStore();

// ② 从对话中提取事实,写入长期记忆
//    namespace 格式:(userId, 类型)
await store.put(
  ['user_001', 'preferences'], // namespace
  'response_style',            // key
  { value: '简洁,不要寒暄' },  // value
);

await store.put(
  ['user_001', 'facts'],
  'name',
  { value: '张三' },
);

// 也可以用向量化的方式写入(便于语义检索)
await store.put(
  ['user_001', 'decisions'],
  '2026-08-11-db-choice',
  {
    value: '决定用 SQLite 而非 Postgres,理由是嵌入式部署无需独立数据库服务',
    // 可选:写入时会自动生成 embedding,支持相似度搜索
  },
);

Retrieve(读取)

查询前先读取该用户的偏好;

csharp 复制代码
// ① 精确读取:按 namespace + key
const pref = await store.get(['user_001', 'preferences'], 'response_style');
console.log(pref?.value); // "简洁,不要寒暄"

// ② 批量读取:拿某个 namespace 下全部记忆
const allFacts = await store.search(['user_001', 'facts']);
// [{ key: 'name', value: { value: '张三' } }, ...]

// ③ 语义检索:根据当前问题,找最相关的历史记忆
const relevant = await store.search(
  ['user_001', 'decisions'],
  { query: '数据库选型', limit: 3 }, // 会做向量相似度排序
);
// 返回与"数据库选型"语义最相近的 3 条决策记录

Inject(注入)

把偏好拼到 system prompt;

javascript 复制代码
// 在每次会话开始、模型推理前,把长期记忆注入 system prompt
const userPrefs = await store.search(['user_001', 'preferences']);
const userFacts = await store.search(['user_001', 'facts']);

const systemPrompt = [
  '你是一个私人助手。',
  '',
  '## 关于这个用户(来自长期记忆)',
  userFacts.items.map(f => `- ${f.key}: ${f.value.value}`).join('\n'),
  '',
  '## 用户偏好',
  userPrefs.items.map(p => `- ${p.value.value}`).join('\n'),
].join('\n');

// 注入到 Agent 的 system message
const agent = createAgent({
  llm: model,
  prompt: systemPrompt, // 模型现在"知道"这是张三、喜欢简洁
});

一种常见的工程化做法是把 Write/Retrieve/Inject 三步封装成一个记忆协调器(Memory Coordinator)------自动在合适的时机触发,业务代码无需关心记忆系统的存在。

2.2、自动记录偏好记忆

让用户手动管理记忆太麻烦。更好的做法是让 Agent 自己从对话中识别并记住偏好 。LangGraph 的 createAgent + store 能力支持自动记忆:

javascript 复制代码
import { createAgent } from '@langchain/langgraph';
import { Annotation } from '@langchain/langgraph';

const agent = createAgent({
  llm: new ChatAnthropic({ model: 'claude-sonnet-4-20250514' }),
  tools: [/* ... */],
  store, // ← 传入 store,开启自动记忆能力

  // preModelHook:每轮推理前,自动检索并注入相关记忆
  preModelHook: async (state, config) => {
    const userId = config.configurable.user_id;
    // 语义检索:根据最近几条消息,找相关的长期记忆
    const recentText = state.messages.slice(-3).map(m => m.content).join(' ');
    const memories = await store.search(
      ['memories', userId],
      { query: recentText, limit: 5 },
    );

    if (memories.items.length > 0) {
      const memoryText = memories.items
        .map(m => `- ${m.value.text}`)
        .join('\n');
      return {
        messages: [{
          role: 'system',
          content: `[回忆起关于该用户的记忆]\n${memoryText}`,
        }],
      };
    }
  },

  // store + 默认的 store schema 会自动触发"记忆提取"
  // LangGraph 内置机制:每轮结束后,模型可以调用 put_memory 工具写入 store
});

这样,当用户说"我最近在用 React 19"时,Agent 会自动识别这是值得记住的事实,写入 ['memories', userId];下次新会话用户问 React 问题,Agent 会自动检索到这条记忆,知道用户在用 React 19。

2.3、记忆纠错

记忆不是写一次就永远对的------用户会改主意,早期的事实会过期。所以记忆系统需要纠错机制:

  1. 冲突检测:写入新记忆前,先检索同 namespace 是否有语义冲突的旧记忆;
  2. 覆盖/合并:有冲突时,让模型判断是新信息覆盖旧信息,还是合并;
  3. 过期标记 :给记忆加 updatedAt 和 confidence 字段,低置信度或过期的记忆优先被淘汰。
typescript 复制代码
async function writeMemoryWithDedup(
  store: any,
  namespace: string[],
  newFact: string,
) {
  // 检索是否已有相似记忆
  const existing = await store.search(namespace, {
    query: newFact,
    limit: 3,
  });

  // 如果有语义高度相似的旧记忆,让模型判断是否要更新
  const similar = existing.items.filter(i => i.score > 0.85);
  if (similar.length > 0) {
    // 简单策略:直接用新记忆覆盖最相似的那条
    const old = similar[0];
    await store.put(namespace, old.key, {
      text: newFact,
      updatedAt: Date.now(),
      // 标记这是修正后的版本
      revisedFrom: old.value.text,
    });
    console.log(`[纠错] 更新了旧记忆: ${old.value.text} → ${newFact}`);
  } else {
    // 无冲突,新增
    await store.put(namespace, `fact_${Date.now()}`, {
      text: newFact,
      createdAt: Date.now(),
    });
  }
}

一个完整的纠错链路通常这样工作:当用户说"其实我不喜欢 TypeScript,我喜欢 Python"时,旧的"喜欢 TypeScript"记忆会被覆盖更新而不是并存------避免 Agent 同时持有两条互相矛盾的记忆造成困惑。


三、Memory 进阶话题

3.1、Vector-based Memory 检索流程

前面 store.search 默认走的是精确 key 匹配 。但很多场景需要"语义检索"------用户问"数据库选型",Agent 应自动联想到"我们之前决定用 SQLite"的记忆。这是通过 Embedding + 向量相似度实现的:

javascript 复制代码
import { InMemoryStore } from '@langchain/langgraph';
import { OpenAIEmbeddings } from '@langchain/openai';

// ① 创建带 embedding 的 store
const store = new InMemoryStore({
  index: {
    embeddings: new OpenAIEmbeddings({ model: 'text-embedding-3-small' }),
    dims: 1536, // embedding 维度
  },
});

// ② 写入记忆(自动计算 embedding)
await store.put(
  ['user_001', 'decisions'],
  'db-choice',
  { text: '决定用 SQLite 而非 Postgres,理由是嵌入式部署无需独立数据库服务' },
);

// ③ 语义检索:根据 query 找最相关的记忆
const results = await store.search(
  ['user_001', 'decisions'],
  {
    query: '数据库选型',         // 自然语言查询
    limit: 3,                   // 返回 top 3
    // score 字段:相似度分数(0~1,越高越相关)
  },
);

results.items.forEach(item => {
  console.log(`[score=${item.score.toFixed(2)}] ${item.value.text}`);
});
// [score=0.89] 决定用 SQLite 而非 Postgres,理由是嵌入式部署...
// [score=0.42] 上次讨论过 MongoDB,但没采用...

完整数据流:

scss 复制代码
写入(put):
   text → embedding model → vec[1536] → 存到 store
   (同时存原文本,供检索时返回)

检索(search with query):
   query → embedding model → queryVec[1536]
   → 计算 queryVec 与所有记忆 vec 的 cosine 相似度
   → 按分数排序 → 取 top K → 返回 text

3.2、Memory TTL / 过期机制

记忆不是"写一次就永久存在"------某些信息会过期(如"上周的项目"在 3 个月后没意义),低置信度的记忆应该被淘汰。给每条记忆加 expiresAt 字段:

typescript 复制代码
async function putWithExpiry(
  store: any,
  namespace: string[],
  key: string,
  value: any,
  ttlMs: number,
) {
  await store.put(namespace, key, {
    ...value,
    expiresAt: Date.now() + ttlMs, // 过期时间戳
    createdAt: Date.now(),
    confidence: 1.0,              // 初始置信度
  });
}

// 写入:30 天后过期
await putWithExpiry(store, ['user_001', 'goals'], 'current-project',
  { text: '正在做 Apollo 项目' },
  30 * 24 * 60 * 60 * 1000,
);

// 检索时自动过滤过期
async function searchActive(store: any, namespace: string[], query: string) {
  const results = await store.search(namespace, { query, limit: 10 });
  const now = Date.now();
  return results.items.filter(item => {
    const expired = item.value.expiresAt && item.value.expiresAt < now;
    if (expired) {
      // 异步删除过期记忆
      store.delete(namespace, item.key);
    }
    return !expired;
  });
}

过期策略的常见模式:

类型 TTL 例子
临时事实 7-30 天 "这周的项目"、"最近的会议"
稳定偏好 永久 "用户喜欢简洁回答"
技能记忆 永久(可手动遗忘) "用户习惯用 pnpm"
敏感记忆 永久 + 加密 "用户的身份证号"(需用户主动清除)

3.3、Memory 导入导出(GDPR)

欧盟 GDPR 等法规要求用户有权导出和删除自己的数据。Memory 系统必须支持这两项操作:

typescript 复制代码
// ① 导出:把某用户全部记忆打包成 JSON
async function exportUserMemories(store: any, userId: string) {
  const allMemories: any[] = [];

  // 遍历该用户所有 namespace
  const namespaces = ['preferences', 'facts', 'goals', 'decisions'];
  for (const ns of namespaces) {
    const items = await store.search([userId, ns]);
    allMemories.push(...items.items.map(item => ({
      namespace: [userId, ns],
      key: item.key,
      value: item.value,
    })));
  }

  return {
    userId,
    exportedAt: new Date().toISOString(),
    version: 1,
    memories: allMemories,
  };
}

// ② 导入:把 JSON 写回 store(迁移设备 / 备份恢复)
async function importUserMemories(store: any, exportData: any) {
  for (const mem of exportData.memories) {
    await store.put(mem.namespace, mem.key, mem.value);
  }
}

// ③ 硬删除:用户行使"被遗忘权"
async function forgetUser(store: any, userId: string) {
  const namespaces = ['preferences', 'facts', 'goals', 'decisions'];
  for (const ns of namespaces) {
    const items = await store.search([userId, ns]);
    for (const item of items.items) {
      await store.delete([userId, ns], item.key);
    }
  }
  console.log(`已删除用户 ${userId} 的全部记忆`);
}

在合规要求高的产品里,UI 上通常会有"导出我的数据"和"删除我的账号"两个按钮,分别调用 exportUserMemories 和 forgetUser。

3.4、Memory 可视化

让用户能看到 Agent"记得什么",并手动编辑/删除------这能极大提升信任感(用户不再觉得 Agent 是黑盒):

scss 复制代码
┌─────────────────────────────────────────────────────┐
│ Agent 记得关于你的事                  [导出] [清空] │
├─────────────────────────────────────────────────────┤
│ 📌 偏好 (3 条)                                      │
│   • 喜欢简洁回答,不要寒暄             [编辑] [×]  │
│   • 习惯用 pnpm 管理依赖                [编辑] [×]  │
│   • 喜欢 TypeScript > JavaScript        [编辑] [×]  │
├─────────────────────────────────────────────────────┤
│ 📋 事实 (5 条)                                      │
│   • 名字:张三                          [编辑] [×]  │
│   • 职业:前端工程师                    [编辑] [×]  │
│   • 在用 React 19 + Vite                [编辑] [×]  │
│   • 项目名:Apollo                     [编辑] [×]  │
│   • 所在地:上海                        [编辑] [×]  │
├─────────────────────────────────────────────────────┤
│ 🎯 目标 (2 条)                                      │
│   • 做 AI Agent 产品                    [编辑] [×]  │
│   • 月底前发布 v0.1                     [编辑] [×]  │
└─────────────────────────────────────────────────────┘

实现思路:后端暴露 GET /memory/:userId 接口 → 前端按 namespace 分组渲染 → 用户点 × 调 store.delete。

3.5、共享记忆(团队 / 家庭)

namespace 的设计天然支持多层级 ------除了 (userId, type),还可以用 (teamId, type) 或 (familyId, type) 做共享空间:

csharp 复制代码
// 团队共享决策
await store.put(
  ['team_frontend', 'decisions'],
  'styling-2026',
  { text: '决定组件库用 shadcn/ui 而非 Material UI' },
);

// 家庭共享偏好
await store.put(
  ['family_smith', 'preferences'],
  'dinner-time',
  { text: '家里 18:30 吃晚饭' },
);

// 检索时 Agent 能"想起"团队/家庭共识
const teamDecisions = await store.search(
  ['team_frontend', 'decisions'],
  { query: '组件库选型' },
);

共享 vs 私有的隔离原则:

层级 namespace 模式 谁能读写 典型场景
个人私有 (userId, type) 只有该用户 个人偏好、个人事实
团队共享 (teamId, type) 团队成员 团队决策、团队规范
家庭共享 (familyId, type) 家庭成员 家庭日程、家庭偏好
全局公共 ('global', type) 所有人 公司级规范、产品手册

权限控制:生产环境的 store 必须在写入/读取时校验 namespace 是否属于当前用户------基于用户身份的 namespace 权限校验几乎是标配,避免越权读取其他用户的记忆。

3.6、记忆类型扩展

除了"事实"和"偏好",还可以让 Agent 记住更丰富的记忆类型:

类型 例子 用途
事实型(Facts) "用户叫张三" 客观信息
偏好型(Preferences) "喜欢简洁回答" 主观偏好
目标型(Goals) "在做 Apollo 项目" 长期意图
流程型(Procedures) "部署流程:build → test → push → ssh deploy" 操作步骤
情绪型(Emotion) "用户最近工作压力大,措辞偏急躁" 情感状态
技能型(Skills) "用户常用快捷键 Cmd+Shift+P" 操作习惯
关系型(Relationships) "李四是用户的技术 leader" 人际关系

实现上,就是在 namespace 里多一层 type 区分:

php 复制代码
// 流程型记忆
await store.put(
  ['user_001', 'procedures'],
  'deploy-vanguard',
  { text: '部署 Apollo:1) pnpm build 2) pnpm test 3) scp dist/ server' },
);

// 情绪型记忆
await store.put(
  ['user_001', 'emotion'],
  '2026-08-stress',
  { text: '用户在赶 v0.1 发布,沟通时偏急躁,回复宜简洁直入主题' },
);

// 检索时按类型过滤
const procedures = await store.search(['user_001', 'procedures'], {
  query: '部署',
  limit: 3,
});

四、Memory / RAG / Checkpoint 三者对比

概念 数据来源 作用域 是否区分用户
Checkpoint 图运行时 state 单 Thread 否(按 thread_id)
Memory 用户行为 / 偏好 跨 Thread 共享 是(按 namespace)
RAG 外部文档 全局知识库 否(公共知识)

记忆这三者的最简单方法:

  • Checkpoint = "这个会话进行到哪了"
  • Memory = "这个用户喜欢什么"
  • RAG = "世界上有什么知识"

RAG 是另外一个比较重要的课题,后面有机会看看能不能结合着实战来进行另外的专题来分享如何使用 RAG 系统来搭建一个系统的客服 Agent。


参考资料:

Agent 开发系列文章

前端转型 Agent 开发 01 之 Agent API 调用(和 Agent 的基础对话):juejin.cn/post/767744...

前端转型 Agent 开发 02 之 Provider 与 Structured Output(规范化模型输入输出):juejin.cn/post/767745...

前端转型 Agent 开发 03 之 Agent Tools(给 Agent 装上手脚):juejin.cn/post/768007...

前端转型 Agent 开发 04 之 MCP 与 Skill(赋予 Agent 更广工作能力):juejin.cn/post/768563...

前端转型 Agent 开发 05 之 Agent Hooks 与 Checkpointer(让 Agent 从全自动转变人为可掌控):juejin.cn/spost/76878...

相关推荐
zhangzeyuaaa2 小时前
Ruby 多线程、GVL(GIL)与 Mutex 完全指南
开发语言·前端·ruby
默_笙2 小时前
🌊 向量库和 ES 都查不出"关系",我只好给奶茶建了一张人脉网
前端·javascript
优选资讯2 小时前
标签打印软件怎么对接 Excel 数据
前端·excel
孟健3 小时前
Qwen3.8-27B 本地推理实测:从 14 tok/s 到 159 tok/s 的投机解码调优
人工智能·llm·ai编程
一个风轻云淡3 小时前
GCC 和 GDB命令简单解读
java·linux·前端
YYYing.4 小时前
【Agent系列 (二) 】大语言模型基础
人工智能·语言模型·自然语言处理·agent
u0111026754 小时前
图片处理接口如何统一错误信息 前端提示与后端错误码设计
前端·状态模式
福兮说4 小时前
浏览器里把 iPhone 的 HEIC 转成 JPG:Chrome 解不开、转完大了七成、拍摄时间全丢,六个坑实测
前端·javascript·图像处理·chrome·ios·iphone·heic
libokaifa5 小时前
把语音模型塞进手机:sherpa-onnx 在 Android 上的 ASR / TTS
前端