LangGraph.js 也允许跨多个线程持久化数据
例如,可以将有关用户的信息(他们的姓名或偏好)存储在共享内存中,并在新的对话线程中重复使用它们
示例
创建一个图,它能够检索有关用户偏好的信息。
通过定义一个 InMemoryStore 来实现这一点------一个可以在内存中存储数据并查询数据的对象。
在编译图时传入存储对象。这允许图中的每个节点访问该存储:当定义节点函数时,可以定义 store 关键字参数,LangGraph 将自动传入编译图时使用的存储对象
当使用 Store 接口存储对象时,需要定义两件事
- 对象的命名空间,一个元组(类似于目录)
- 对象键(类似于文件名)
在示例中使用 ("memories", ) 作为命名空间,并使用随机 UUID 作为每个新记忆的键
首先定义一个 InMemoryStore,它已经填充了一些关于用户的记忆
js
import { InMemoryStore } from "@langchain/langgraph";
const inMemoryStore = new InMemoryStore();
js
import { v4 as uuidv4 } from "uuid";
import { ChatAnthropic } from "@langchain/anthropic";
import { BaseMessage } from "@langchain/core/messages";
import {
Annotation,
StateGraph,
START,
MemorySaver,
LangGraphRunnableConfig,
messagesStateReducer,
} from "@langchain/langgraph";
const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
});
const model = new ChatAnthropic({ modelName: "claude-3-5-sonnet-20240620" });
// NOTE: we're passing the Store param to the node -- this is the Store we compile the graph with
const callModel = async (
state: typeof StateAnnotation.State,
config: LangGraphRunnableConfig
): Promise<{ messages: any }> => {
const store = config.store;
if (!store) {
throw new Error("store is required when compiling the graph");
}
if (!config.configurable?.userId) {
throw new Error("userId is required in the config");
}
const namespace = ["memories", config.configurable?.userId];
const memories = await store.search(namespace);
// 返回该 namespace 下所有记忆条目数组
const info = memories.map((d) => d.value.data).join("\n");
const systemMsg = `You are a helpful assistant talking to the user. User info: ${info}`;
// Store new memories if the user asks the model to remember
const lastMessage = state.messages[state.messages.length - 1];
// 当用户的消息中有 remember 时,将其存储到 store 中
if (typeof lastMessage.content === "string" && lastMessage.content.toLowerCase().includes("remember")) {
await store.put(namespace, uuidv4(), { data: lastMessage.content });
}
const response = await model.invoke([
// 系统提示词
{ type: "system", content: systemMsg },
// 状态 state 中的历史消息列表
...state.messages,
]);
return { messages: response };
};
const builder = new StateGraph(StateAnnotation)
.addNode("call_model", callModel)
.addEdge(START, "call_model");
// NOTE: we're passing the store object here when compiling the graph
const graph = builder.compile({
checkpointer: new MemorySaver(),
store: inMemoryStore,
});
// If you're using LangGraph Cloud or LangGraph Studio, you don't need to pass the store or checkpointer when compiling the graph, since it's done automatically.
// 如果部署到 LangGraph Cloud / Studio,底层平台已经内置了持久化的 store + checkpointer
// 不需要你手动实例化 InMemoryStore / MemorySaver,也不用在 compile 传,平台自动注入
// 本地开发才需要手动创建并传入

const inMemoryStore = new InMemoryStore() 传给 graph.compile({store:inMemoryStore}),所有 graph 运行实例共享同一个内存存储对象,实现跨 thread_id(对话线程)读取 / 写入同一份用户记忆
compile 传入 store,LangGraph 会把这个 inMemoryStore 对象引用注入到每个 node 的 config.store 参数,所以在 callModel 节点里 const store = config.store,拿到的就是同一个全局对象
namespace = ["memories", config.configurable?.userId]:用 namespace 做数据隔离,A 用户的记忆不会跑到 B 用户store.put(namespace, uuidv4(), { data: lastMessage.content }):只要用户消息带remember,就把记忆写入这个用户的 namespace- 下次同一个 userId,任意 thread_id 跑这个图,
store.search都能把之前存的记忆查出来,拼进 system prompt
用户维度,有 n 多条记忆,每个记忆有自己的 uuid
跨线程写入
第一次
js
let config = { configurable: { thread_id: "1", userId: "1" } };
let inputMessage = { type: "user", content: "Hi! Remember: my name is Bob" };
for await (const chunk of await graph.stream(
{ messages: [inputMessage] },
{ ...config, streamMode: "values" }
)) {
console.log(chunk.messages[chunk.messages.length - 1]);
}
json
HumanMessage {
"id": "ef28a40a-fd75-4478-929a-5413f2a6b044",
"content": "Hi! Remember: my name is Bob",
"additional_kwargs": {},
"response_metadata": {}
}
AIMessage {
"id": "msg_01UcHJnSAuVDFuDmqaYkxWAf",
"content": "Hello Bob! It's nice to meet you. I'll remember that your name is Bob. How can I assist you today?",
"additional_kwargs": {
"id": "msg_01UcHJnSAuVDFuDmqaYkxWAf",
"type": "message",
"role": "assistant",
"model": "claude-3-5-sonnet-20240620",
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 28,
"output_tokens": 29
}
},
"response_metadata": {
"id": "msg_01UcHJnSAuVDFuDmqaYkxWAf",
"model": "claude-3-5-sonnet-20240620",
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 28,
"output_tokens": 29
},
"type": "message",
"role": "assistant"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 28,
"output_tokens": 29,
"total_tokens": 57
}
}
第二次
js
config = { configurable: { thread_id: "2", userId: "1" } };
inputMessage = { type: "user", content: "what is my name?" };
for await (const chunk of await graph.stream(
{ messages: [inputMessage] },
{ ...config, streamMode: "values" }
)) {
console.log(chunk.messages[chunk.messages.length - 1]);
}
json
HumanMessage {
"id": "eaaa4e1c-1560-4b0a-9c2d-396313cb000c",
"content": "what is my name?",
"additional_kwargs": {},
"response_metadata": {}
}
AIMessage {
"id": "msg_01VfqUerYCND1JuWGvbnAacP",
"content": "Your name is Bob. It's nice to meet you, Bob!",
"additional_kwargs": {
"id": "msg_01VfqUerYCND1JuWGvbnAacP",
"type": "message",
"role": "assistant",
"model": "claude-3-5-sonnet-20240620",
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 33,
"output_tokens": 17
}
},
"response_metadata": {
"id": "msg_01VfqUerYCND1JuWGvbnAacP",
"model": "claude-3-5-sonnet-20240620",
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 33,
"output_tokens": 17
},
"type": "message",
"role": "assistant"
},
"tool_calls": [],
"invalid_tool_calls": [],
"usage_metadata": {
"input_tokens": 33,
"output_tokens": 17,
"total_tokens": 50
}
}
检索
js
const memories = await inMemoryStore.search(["memories", "1"]);
for (const memory of memories) {
console.log(await memory.value);
}
注意
- 多并发 graph run(多个请求)可以读写同一个 InMemoryStore,因为所有节点拿到的是同一个内存对象引用,实现跨线程共享记忆
- 但是 JS 的 InMemoryStore 不是强并发安全,JS 是单线程事件循环,不会有真正 OS 线程竞争;但同一个用户并发多次写入,会出现竞态(last write wins,覆盖)。生产环境不要用
InMemoryStore,换成 RedisStore / PostgresStore - 进程重启,InMemoryStore 所有数据全部丢失,只适合本地调试