LangGraph ~ 为图添加跨线程持久化

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);
}

注意

  1. 多并发 graph run(多个请求)可以读写同一个 InMemoryStore,因为所有节点拿到的是同一个内存对象引用,实现跨线程共享记忆
  2. 但是 JS 的 InMemoryStore 不是强并发安全,JS 是单线程事件循环,不会有真正 OS 线程竞争;但同一个用户并发多次写入,会出现竞态(last write wins,覆盖)。生产环境不要用 InMemoryStore,换成 RedisStore / PostgresStore
  3. 进程重启,InMemoryStore 所有数据全部丢失,只适合本地调试
相关推荐
xianrenli382 小时前
水肺潜水学习
学习
凌云若寒3 小时前
BarTender提示#807错误:无法在拥有其他许可证的Licensing Service上激活节点锁定的 Professional 版许可证 的解决办法
运维·服务器·前端·学习·软件需求
爱吃苹果的日记本4 小时前
离散数学第七课
学习·离散数学
迪丽热爱4 小时前
多媒体应用31-914(计算机硬件与体系结构)
学习
dadaobusi5 小时前
学习:BPU
学习
ab1237685 小时前
OpenCV 学习笔记
笔记·opencv·学习
磁场转动100万匹6 小时前
Hive 学习第一天:搞懂它是什么,并搭好可用的环境
hive·hadoop·学习
dadaobusi6 小时前
RDMA最新进展
学习
我命由我123456 小时前
金融租赁极简理解
经验分享·学习·职场和发展·金融·求职招聘·职场发展·学习方法