[LangGraph] 长期记忆
长期记忆是需要长久保持的,长期记忆通常用于存储:
- 用户画像
- 偏好
- 历史习惯
- 长期事实(不会频繁变)
例如:
- 用户是前端工程师
- 用户更喜欢中文回答
- 用户不需要基础解释
- 用户常问某一类问题
凡是“不应该被时间旅行影响的事实”,就应该放进长期记忆,而非短期记忆。下面是短期记忆和长期记忆两者之间的对比:
| 维度 | 短期记忆(Short-term) | 长期记忆(Long-term) |
|---|---|---|
| 关注点 | 当前任务 / 当前流程 | 用户 / 世界的长期事实 |
| 生命周期 | 仅在当前线程或会话中有效 | 跨 Graph / 跨会话和线程 |
| 是否分支 | ✅ 会(时间旅行) | ❌ 不应该 |
| 是否可回溯 | ✅ checkpoint | 一般不需要 |
| 修改频率 | 高频 | 低频 |
| 典型内容 | 当前会话中的消息、对话历史 | 用户的长期信息、偏好、背景 |
| 设计目标 | 可纠错、可回放 | 可积累、可复用 |
一句话总结:短期记忆是可以频繁修改的,而长期记忆一般是不会去修改的。
添加长期记忆
和短期记忆一样,langgraph 中提供了内存和数据库的形式。
这里选择 InMemoryStore 内存的形式来做介绍。
InMemoryStore 是一个轻量级的存储方式,下面是一个快速上手的代码:
import { InMemoryStore } from "@langchain/langgraph";
// 实例化
const store = new InMemoryStore();
const userId = "user-1";
// 长期记忆的命名空间
// 1. 长期记忆的类别
// 2. 区分不同的用户
const namespace = ["user_profile", userId];
// 这是一个包含长期事实的信息
const userInfo = "User name is Bob, works as a developer.";
// 将信息存储到store里面
// 1.命名空间 2. key 3. 具体的数据
await store.put(namespace, "memory-key", { data: userInfo });
// 从store里面搜索信息
const memories = await store.search(namespace, { query: "Bob" });
console.log(memories);
在 langgraph 中,长期记忆的内容是存储在 store 中的,可以通过 userId 来区分不同用户的记忆。
长期记忆是跨会话的,和当前对话的上下文无关,因此可以在不同的 thread_id(对话 ID)中共享。
-
读取记忆:使用
store.search方法,可以读取已经存储的长期记忆。例如,系统可以在每次对话开始时加载用户的记忆,进行个性化回应。const memories = await store.search(namespace, { query: "Bob" }); const memoryText = memories.map((m: any) => m.value.data).join("\n") || ""; -
写入记忆:当用户提供新的信息,或者 AI 收到一些关键的指示(如“记住我叫什么名字”),你可以通过
store.put方法将这些信息写入长期记忆。const memory = "User name is Alice"; await store.put(namespace, "memory-key", { data: memory });
如何检查,提取需要被长期存储的信息
1. “工具调用”模式(最推荐,最省钱 )
不设置专门的检查节点,而是给 AI 一个工具(例如 save_user_preference)。
做法: 只有当 AI 觉得用户说的话里有“金子”(比如“我不吃花生”或“我偏好 Python”)时,它才会主动调用这个工具。
优点: 如果用户只是说“你好”或“谢谢”,AI 就不会触发存储逻辑,节省 Token。
缺点: 需要模型足够聪明,能自主判断哪些信息值得记录。
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const saveUserFact = tool(
async ({ fact }, { configurable }) => {
const { userId, store } = configurable;
// Save to cross-thread memory
await store.put([userId, "memories"], "facts", { fact });
return `Memory updated: ${fact}`;
},
{
name: "save_user_fact",
description: "Saves a persistent fact about the user for future sessions.",
schema: z.object({ fact: z.string() }),
}
);
// Bind to your model
const modelWithTools = model.bindTools([saveUserFact]);
2. “会话结束”总结模式(最完整)
等一轮对话结束(或达到一定对话轮数)后,再运行一个总结节点。
做法: 该节点回顾过去 10-20 条消息,提取核心事实并存入 Store。
优点: 上下文更完整,AI 能看清整件事的来龙去脉再做决定。
缺点: 记忆有延迟,通常要到下一次开启新对话时,AI 才会“想起”这些事。
const memoryNode = async (state: typeof MessagesState.State, config: RunnableConfig) => {
const { store, userId } = config.configurable;
// 1. Ask LLM to summarize the interaction
const response = await model.invoke([
["system", "Extract key user preferences or facts from this chat."],
...state.messages
]);
// 2. Persist to long-term Store
await store.put([userId, "profile"], "latest_summary", { content: response.content });
return {}; // No change to the chat messages
};
3. “异步反思”模式(体验最好)
在 LangGraph 中,你可以让回复用户和更新记忆并行执行。
做法: 立即给用户返回答案(保证不卡顿),同时在后台启动一个“反思节点”去分析输入并更新长期存储。
优点: 用户感知不到延迟。
const loadMemoryNode = async (state: typeof MessagesState.State, config: RunnableConfig) => {
const { store, userId } = config.configurable;
// Get the saved data
const savedData = await store.get([userId, "profile"], "latest_summary");
if (savedData) {
// Inject memory into the system prompt
const memoryPrompt = `Remember: ${savedData.value.content}`;
return { messages: [new SystemMessage(memoryPrompt)] };
}
return {};
};
Code:
import "dotenv/config";
import readline from "readline-sync";
import { ChatOpenAI } from "@langchain/openai";
import {
StateGraph,
START,
END,
InMemoryStore,
MemorySaver,
} from "@langchain/langgraph";
import { BaseMessage, HumanMessage } from "@langchain/core/messages";
import { z } from "zod";
// 图的状态结构,里面只有一项,消息
const StateSchema = z.object({
messages: z.array(z.custom<BaseMessage>()),
});
// 根据Schema生成对应的ts类型
type TState = z.infer<typeof StateSchema>;
// 模型
const model = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0.5,
});
const store = new InMemoryStore(); // 做长期记忆
const checkpointer = new MemorySaver(); // 做短期记忆
// 节点函数 - 聊天
async function chatNode(state: TState, config): Promise<Partial<TState>> {
// 1. 从长期记忆里面获取信息
// 2. 和大模型进行交流 - 会把长期记忆的信息带过去
// 3. 判断新的这一轮会话,有没有信息需要存入到长期记忆里面
const userId = config.configurable.userId;
const namespace = ["user_profile", userId];
// 取出长期记忆
// 记忆里面如果有东西:
/**
* [
{
key: "xxxx",
value: {
data: "我叫张三。" // 记忆内容
}
},
{
key: "xxxx",
value: {
data: "我喜欢编程。" // 另一条记忆内容
}
}
]
*
*/
const memories = await store.search(namespace, {});
// console.log("memories>>>", memories);
// 取出记忆的内容,组装成一个字符串
// "我叫张三。\n 我喜欢编程。"
const memoryText = memories?.map((m) => m.value.data).join("\n") || "";
// 提示词
const systemPrompt = `
你是一个持续与用户聊天的助手。
以下是你已知的用户长期信息(如果有):
${memoryText || "(暂无)"}
`;
// 和大模型进行交流
const response = await model.invoke([
{ role: "system", content: systemPrompt }, // 系统设定
...state.messages,
]);
// 现在我们其实已经有了模型的回复
// 先不着急推入到 messages 里面
// 先把用户发的消息取出来,判断用户发的消息是否需要存储到长期记忆里面
const lastMsg = state.messages.at(-1); // 取出最后一条内容(用户发的)
const lastUserMsg =
typeof lastMsg?.content === "string" ? lastMsg.content : "";
// 需要判断是否有存储到长期记忆里面的必要
if (shouldSaveToMemory(lastUserMsg)) {
// 如果进入此分支,说明要做长期记忆
// 需要做一个信息的提取
const memory = extractMemory(lastUserMsg);
if (memory) {
// 提取到信息了,存
await store.put(namespace, crypto.randomUUID(), {
data: memory,
});
console.log("🧠 [长期记忆已保存]:", memory);
}
}
return { messages: [...state.messages, response] };
}
function shouldSaveToMemory(text: string): boolean {
// 定义一组关键字,如果用户说的话包含这些关键词,就认为是个人信息
const keywords = ["我是", "我叫", "我在", "我的工作", "我喜欢", "记住"];
return keywords.some((k) => text.includes(k));
}
function extractMemory(text: string): string | null {
if (text.includes("我叫")) {
return text;
}
if (text.includes("我是")) {
return text;
}
if (text.includes("我喜欢")) {
return text;
}
if (text.includes("我的工作是")) {
return text;
}
return null;
}
const graph = new StateGraph(StateSchema)
.addNode("chatNode", chatNode)
.addEdge(START, "chatNode")
.addEdge("chatNode", END)
.compile({
checkpointer, // 注入短期记忆检查点
store, // 注入长期记忆存储
});
async function main() {
const config = {
configurable: {
userId: "bill",
thread_id: "bill-chat",
},
};
let state: TState = {
messages: [],
};
console.log("🤖 聊天开始(Ctrl+C 退出)");
while (true) {
// 获取用户在终端的输入
const input = readline.question("\n你:");
// 将用户的输入封装成 HumanMessage 对象,加入到本地状态
state.messages.push(new HumanMessage(input));
const result = await graph.invoke(state, config);
// console.log("result>>>", result);
const aiMsg = result.messages.at(-1);
console.log("\nAI:", aiMsg?.content);
state = result; // 更新本地状态
}
}
main();

浙公网安备 33010602011771号