记忆管理
短期记忆 + 长期记忆——让智能体真正"记住"用户
线程内对话不丢失,跨会话偏好永久保存每次都陌生
智能体和用户聊了 20 分钟,关掉重开就全忘了——每次都像第一次见面。
对话断了
用户说"我喜欢深色模式"。下一轮对话时,智能体推荐了亮色主题。短期记忆丢失——同一线程内的上下文没能保留。
换线程就忘
用户在对话 A 中说了"我对花生过敏"。开了新对话 B,智能体推荐了花生酱。长期记忆缺失——跨线程的信息没有保存。
上下文爆炸
聊了 100 轮后,把全部消息历史丢给模型——超出 token 限制。记忆需要管理——不是越多越好。
两种记忆
短期记忆管对话,长期记忆管用户

图:短期记忆 vs 长期记忆
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:左右两部分,用虚线分隔 - 左侧标题"短期记忆"(蓝色),包含: - 一个"Thread 线程"方框,内部有多个消息气泡(用户→AI→用户→AI) - 标注:"MemorySaver + thread_id" - 底部标注:"对话结束 → 记忆保留在线程内" - 右侧标题"长期记忆"(绿色),包含: - 一个"Store 存储"圆柱体,内部有多条记忆条目 - 标注:"InMemoryStore + namespace" - 三个线程(Thread A/B/C)都有箭头指向 Store - 底部标注:"任何线程都能访问" - 中间虚线标注:"线程内 vs 跨线程" - 各元素关系:短期记忆绑定到单个线程(一对一),线程内消息按时间顺序排列;长期记忆是独立存储,多个线程共享访问(多对一) - 关键标注:左侧标注"MemorySaver + thread_id",右侧标注"InMemoryStore + namespace",中间虚线标注"线程内 vs 跨线程",三个线程箭头标注"均可读写" - 配色建议:浅灰背景,短期蓝色,长期绿色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
| 短期记忆 | 长期记忆 | |
|---|---|---|
| 范围 | 线程内(单次对话) | 跨线程(跨所有对话) |
| 实现 | Checkpointer(MemorySaver) | Store(InMemoryStore) |
| 访问方式 | 自动(通过 state.messages) | 手动(通过 runtime.store) |
| 典型数据 | 对话历史 | 用户偏好、知识、摘要 |
短期记忆
MemorySaver + thread_id = 多轮对话
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 自动把新消息拼到已有消息后面。
生产环境
内存存储重启就丢。生产环境用数据库后端。
// 短期记忆: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 = 跨线程的用户数据
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" });
// 使用记忆增强模型调用...
};
await graph.invoke({ messages: [...] }, { configurable: { thread_id: "1" }, context: { userId: "1" } })。节点通过 runtime.context 获取。
语义搜索
用嵌入模型实现"模糊匹配"记忆
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,
});
语义搜索实战
在图节点中用 runtime.store 实现记忆增强的对话
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 });
runtime.store.search() 按语义检索记忆 → 注入 system prompt。搜索由 store 的嵌入索引自动处理,节点代码无需关心嵌入细节。
管理短期记忆
消息越来越多时要控制——裁剪、删除、总结
裁剪消息
import { trimMessages } from "@langchain/core/messages";
const messages = trimMessages(state.messages, {
tokenCounter: model, // 必填:用模型的 token 计数器(也可传自定义函数)
strategy: "last",
maxTokens: 128,
startOn: "human",
endOn: ["human", "tool"],
});
删除消息
import { RemoveMessage } from "@langchain/core/messages";
if (messages.length > 2) {
return {
messages: messages
.slice(0, 2)
.map((m) => new RemoveMessage({ id: m.id })),
};
}
总结消息
把旧消息压缩成摘要,删除原始消息。保留最近几条 + 摘要作为上下文。
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");
排错指南
第二轮对话忘了上一轮
症状:换了 thread_id,之前的对话内容全没了。
修复:确保多轮对话使用同一个 thread_id。thread_id 不同 = 不同对话。
Store 搜索没有结果
症状:store.search() 返回空数组。
修复:检查 namespace 是否一致。put 和 search 必须用相同的 namespace 路径。
token 超限
症状:对话很长后报 context_length_exceeded。
修复:用 trimMessages 或 RemoveMessage 裁剪历史消息。别把所有消息都丢给模型。
动手练习
构建一个有记忆的聊天机器人
- 加上 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 });
// ... 使用偏好增强模型调用
};