第 10 章

记忆管理

短期记忆 + 长期记忆——让智能体真正"记住"用户

线程内对话不丢失,跨会话偏好永久保存

每次都陌生

智能体和用户聊了 20 分钟,关掉重开就全忘了——每次都像第一次见面。

对话断了

用户说"我喜欢深色模式"。下一轮对话时,智能体推荐了亮色主题。短期记忆丢失——同一线程内的上下文没能保留。

换线程就忘

用户在对话 A 中说了"我对花生过敏"。开了新对话 B,智能体推荐了花生酱。长期记忆缺失——跨线程的信息没有保存。

上下文爆炸

聊了 100 轮后,把全部消息历史丢给模型——超出 token 限制。记忆需要管理——不是越多越好。

LangGraph 的记忆方案:短期记忆(MemorySaver + thread_id)解决对话内记忆,长期记忆(InMemoryStore + 语义搜索)解决跨会话记忆。两者组合,智能体才能真正"认识"用户。

开始之前

前置知识

  • 持久化 — checkpointer 和线程
  • 快速上手 — 完整的智能体代码
  • 了解 MessagesValue 的消息追加机制

学完后你应该能

  • 区分短期记忆和长期记忆
  • 用 MemorySaver 实现多轮对话
  • 用 InMemoryStore 实现跨线程记忆
  • 配置生产环境的 PostgresSaver 和 PostgresStore
  • 用嵌入模型实现语义搜索并集成到图节点
  • 管理消息历史(裁剪、删除、总结)

两种记忆

短期记忆管对话,长期记忆管用户

图:短期记忆 vs 长期记忆
短期记忆长期记忆
范围线程内(单次对话)跨线程(跨所有对话)
实现Checkpointer(MemorySaver)Store(InMemoryStore)
访问方式自动(通过 state.messages)手动(通过 runtime.store)
典型数据对话历史用户偏好、知识、摘要

短期记忆

MemorySaver + thread_id = 多轮对话

思考题:你跟 AI 说"我叫小明",下一轮它就忘了。每次都像新认识的人一样,用户体验极差。怎么让 AI 在同一个对话中记住之前说过的话?
import { MemorySaver, StateGraph } from "@langchain/langgraph";

const checkpointer = new MemorySaver();
const graph = builder.compile({ checkpointer });

// 第一轮
await graph.invoke(
  { messages: [{ role: "user", content: "hi! i am Bob" }] },
  { configurable: { thread_id: "1" } }
);

// 第二轮 — 同一个 thread_id,保留上一轮的上下文
await graph.invoke(
  { messages: [{ role: "user", content: "what is my name?" }] },
  { configurable: { thread_id: "1" } }
);
// 模型知道你是 Bob!

📄 完整代码:10_short_term_memory.ts

关键:同一个 thread_id 下,每轮对话都会保留之前的消息历史。MessagesValue 的 reducer 自动把新消息拼到已有消息后面。

生产环境

内存存储重启就丢。生产环境用数据库后端。

思考题:开发时用 MemorySaver(内存),进程一重启记忆全丢了。上线后用户每次刷新页面就失忆,这不行。怎么把记忆持久化到数据库?迁移成本高吗?
// 短期记忆:PostgresSaver
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";

const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const checkpointer = PostgresSaver.fromConnString(DB_URI);
// await checkpointer.setup(); // 首次使用需要初始化表结构

// 长期记忆:PostgresStore
import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres/store";

const store = PostgresStore.fromConnString(DB_URI);
// await store.setup(); // 首次使用需要初始化表结构

const graph = builder.compile({ checkpointer, store });

📄 完整代码:10_production_backends.ts

子图中使用

只需在编译父图时提供 checkpointer,LangGraph 会自动传播给子图:

const subgraph = subgraphBuilder.compile(); // 不需要单独指定
const graph = builder.compile({ checkpointer }); // 父图指定即可

首次使用必须 setup()

checkpointer.setup() 和 store.setup() 会创建必要的数据库表和索引。只需调用一次,后续运行不需要再调用。

长期记忆

InMemoryStore + namespace = 跨线程的用户数据

思考题:用户在对话A中说"我是前端工程师",在对话B中问"我是做什么的",AI 答不上来——因为 thread_id 不同,短期记忆互不相通。怎么让记忆跨对话持久保存?
import { InMemoryStore, StateGraph } from "@langchain/langgraph";

const store = new InMemoryStore();
const graph = builder.compile({ store });
import { v4 as uuidv4 } from "uuid";

// 在节点中访问 store
const callModel: GraphNode<typeof State> = async (state, runtime) => {
  const userId = runtime.context?.userId;
  const namespace = [userId, "memories"];

  // 搜索相关记忆
  const memories = await runtime.store?.search(namespace, {
    query: state.messages.at(-1)?.content,
    limit: 3,
  });
  const info = memories?.map((d) => d.value.data).join("\n") || "";

  // 存储新记忆
  await runtime.store?.put(namespace, uuidv4(), { data: "User prefers dark mode" });

  // 使用记忆增强模型调用...
};

📄 完整代码:10_long_term_memory.ts

调用时传入 context:await graph.invoke({ messages: [...] }, { configurable: { thread_id: "1" }, context: { userId: "1" } })。节点通过 runtime.context 获取。

语义搜索

用嵌入模型实现"模糊匹配"记忆

思考题:用户存了一条"学习 LangGraph 的工作流模式",后来说"怎么保存状态"——关键词完全不同,Store 怎么找到相关的记忆?按关键词匹配肯定搜不到。
import { OpenAIEmbeddings } from "@langchain/openai";

const store = new InMemoryStore({
  index: {
    embeddings: new OpenAIEmbeddings({ model: "text-embedding-3-small" }),
    dims: 1536,
  },
});

// 存入记忆
await store.put(["user_123", "memories"], "1", { text: "I love pizza" });
await store.put(["user_123", "memories"], "2", { text: "I am a plumber" });

// 语义搜索——"我饿了" 匹配到 "I love pizza"
const items = await store.search(["user_123", "memories"], {
  query: "I'm hungry",
  limit: 1,
});

📄 完整代码:10_semantic_memory.ts

语义搜索 vs 精确匹配:没有嵌入模型时,search 只能精确匹配。加上嵌入模型后,"我饿了"也能找到"我爱吃披萨"——因为它们的语义相近。

语义搜索实战

在图节点中用 runtime.store 实现记忆增强的对话

思考题:怎么在图节点中通过 runtime.store 搜索记忆,并把找到的记忆注入到 LLM 的系统提示中?
const chat: GraphNode<typeof State> = async (state, runtime) => {
  // 根据用户最后一条消息搜索相关记忆
  const items = await runtime.store.search(
    ["user_123", "memories"],
    { query: state.messages.at(-1)?.content, limit: 2 }
  );
  const memories = items.map((item) => item.value.text).join("\n");
  const memoriesText = memories
    ? `## Memories of user\n${memories}` : "";

  // 将记忆注入 system prompt
  const response = await model.invoke([
    { role: "system", content: `You are a helpful assistant.\n${memoriesText}` },
    ...state.messages,
  ]);
  return { messages: [response] };
};

const builder = new StateGraph(State)
  .addNode("chat", chat)
  .addEdge(START, "chat");
const graph = builder.compile({ store });

📄 完整代码:10_semantic_memory.ts

模式:编译图时传入带嵌入模型的 store → 节点中通过 runtime.store.search() 按语义检索记忆 → 注入 system prompt。搜索由 store 的嵌入索引自动处理,节点代码无需关心嵌入细节。

管理短期记忆

消息越来越多时要控制——裁剪、删除、总结

裁剪消息

思考题:对话越来越长,消息堆积到上百条,发给 LLM 的 token 数暴增。怎么自动裁剪旧消息,只保留最近的上下文?
import { trimMessages } from "@langchain/core/messages";

const messages = trimMessages(state.messages, {
  tokenCounter: model,  // 必填:用模型的 token 计数器(也可传自定义函数)
  strategy: "last",
  maxTokens: 128,
  startOn: "human",
  endOn: ["human", "tool"],
});

📄 完整代码:10_trim_messages.ts

删除消息

思考题:用户说"把我刚才发的消息删掉,里面有我的密码"。对话历史中的敏感信息必须彻底移除。怎么精确删除某条消息,而不是清空整个对话?
import { RemoveMessage } from "@langchain/core/messages";

if (messages.length > 2) {
  return {
    messages: messages
      .slice(0, 2)
      .map((m) => new RemoveMessage({ id: m.id })),
  };
}

📄 完整代码:10_remove_message.ts

总结消息

思考题:对话有 50 条消息,trimMessages 太粗暴——直接丢弃旧消息可能丢失关键信息。能不能把旧消息"浓缩"成一段摘要,既节省 token 又保留关键信息?

把旧消息压缩成摘要,删除原始消息。保留最近几条 + 摘要作为上下文。

📄 完整代码:10_memory_summarization.ts

删除消息时注意:确保删除后的消息历史仍然合法——某些 provider 期望历史以 user 消息开头;带 tool calls 的 assistant 消息后必须有对应的 tool 结果消息。删除不当会导致 LLM 调用报错。

管理检查点

查看、遍历、清理线程数据

// 查看线程当前状态
await graph.getState({ configurable: { thread_id: "1" } });

// 遍历线程历史
for await (const state of graph.getStateHistory({ configurable: { thread_id: "1" } })) {
  console.log(state);
}

// 删除线程的所有检查点
await checkpointer.deleteThread("1");
注意:MemorySaver 的数据存在内存中。进程重启后所有检查点都会丢失。生产环境务必使用 PostgresSaver 等持久化后端。

排错指南

第二轮对话忘了上一轮

症状:换了 thread_id,之前的对话内容全没了。

修复:确保多轮对话使用同一个 thread_id。thread_id 不同 = 不同对话。

Store 搜索没有结果

症状:store.search() 返回空数组。

修复:检查 namespace 是否一致。put 和 search 必须用相同的 namespace 路径。

token 超限

症状:对话很长后报 context_length_exceeded。

修复:用 trimMessages 或 RemoveMessage 裁剪历史消息。别把所有消息都丢给模型。

动手练习

构建一个有记忆的聊天机器人

任务:基于 Ch04 的计算器智能体,添加记忆功能。
  • 加上 MemorySaver,让多轮计算能保持上下文
  • 加上 InMemoryStore,在节点中存储用户的计算偏好(比如"总是返回整数")
  • 用 trimMessages 限制最多保留最近 10 条消息

参考答案

// 1. 添加 MemorySaver
const checkpointer = new MemorySaver();

// 2. 添加 InMemoryStore
const store = new InMemoryStore();

// 3. 编译时同时传入
const graph = builder.compile({ checkpointer, store });

// 4. 在 llmCall 节点中管理记忆
const llmCall: GraphNode<typeof State> = async (state, runtime) => {
  // 裁剪消息
  const messages = trimMessages(state.messages, {
    tokenCounter: model,  // 必填:token 计数器
    strategy: "last",
    maxTokens: 2000,
    startOn: "human",
  });

  // 读取用户偏好
  const userId = runtime.context?.userId;
  const prefs = await runtime.store?.search([userId, "prefs"], { limit: 3 });

  // ... 使用偏好增强模型调用
};
模式:checkpointer 管线程内记忆(对话历史),store 管跨线程记忆(用户偏好)。两者互不干扰,组合使用。

本章回顾

到这里,你已经能

  • 区分短期记忆和长期记忆
  • 用 MemorySaver 实现多轮对话
  • 用 InMemoryStore 存储跨线程数据
  • 配置生产环境的 PostgresSaver 和 PostgresStore
  • 用嵌入模型实现语义搜索并集成到图节点
  • 管理消息历史(裁剪/删除/总结)

它和前后章节的关系

核心结论:短期记忆让智能体记住当前对话,长期记忆让智能体认识用户。两者配合,才能构建真正有用的 AI 助手。别忘了管理记忆大小——不是越多越好。

延伸阅读

继续本课程

相关章节