以编程方式使用文档

存储允许代理在线程之间持久化信息,包括用户偏好、积累的知识以及应该超越单个对话留存的事实。与 检查点不同,检查点保存的是限定在单个线程范围内的完整图状态,而存储则保存可从任何线程访问的任意键值数据。

!共享状态模型

基本用法

以下代码片段单独展示 InMemoryStore 的用法,未使用 LangGraph:

const memoryStore = new MemoryStore();

记忆按 tuple进行命名空间划分,在以下示例中为 (<user_id>, "memories") 。命名空间可以是任意长度,代表任意内容,不一定是用户特定的。

const userId = "1";
const namespaceForMemory = [userId, "memories"];

使用 store.put 方法将记忆保存到存储中的命名空间。指定上述定义的命名空间,以及记忆的键值对:键是记忆的唯一标识符(memory_id),值(字典)本身就是记忆。

const memoryId = crypto.randomUUID();
const memory = { food_preference: "I like pizza" };
await memoryStore.put(namespaceForMemory, memoryId, memory);

使用 store.search 方法从命名空间读取记忆,对于给定用户返回记忆列表,最多 limit 条(默认 10)。使用 InMemoryStore时,项按插入顺序返回,因此最近的记忆位于列表末尾;其他后端可能以不同方式排序记忆(参见 列出命名空间中的项).

const memories = await memoryStore.search(namespaceForMemory);
memories[memories.length - 1];

// {
//   value: { food_preference: 'I like pizza' },
//   key: '07e0caf4-1631-47b7-b15f-65515d4c1843',
//   namespace: ['1', 'memories'],
//   createdAt: '2024-10-02T17:22:31.590602+00:00',
//   updatedAt: '2024-10-02T17:22:31.590605+00:00'
// }

它具有的属性有:

  • * value:此记忆的值
  • * key:此记忆在此命名空间中的唯一键
  • * namespace:字符串元组,此记忆类型的命名空间

  • * createdAt:此记忆创建时的时间戳
  • * updatedAt:此记忆更新时的时间戳

列出命名空间中的项目

调用 store.search ,不指定 query 且不指定 filter 将返回存储在命名空间前缀下、最多 limit个项目。当您不需要语义排序时,使用此方法枚举命名空间中的所有内容。

请牢记三个行为:

  • - **namespace_prefix 按前缀匹配,而非精确匹配。** ("alice",) 也会返回 ("alice", "memories"), ("alice", "preferences")下的项目,依此类推。要限制为单个级别,请传递完整的命名空间或在客户端按 item.namespace.
  • - **超出 limit 的结果将被静默截断。** 没有溢出信号——将 limit 设置为预期的上限,或使用 offset.
  • 默认排序取决于存储后端。 PostgresStoreAsyncPostgresStoreupdated_at 降序返回结果(最近更新的排在最前面)。 InMemoryStore 按插入顺序返回结果(最近插入的排在最后)。请勿依赖不同实现之间的特定顺序;如需排序,请在客户端按 item.updated_at 进行排序(如果顺序重要)。

要分页浏览大型命名空间:

要发现存在的命名空间(例如,在列出用户记忆前迭代每个用户),请使用 store.listNamespaces:

语义搜索

除了简单的检索,存储还支持语义搜索,允许您根据含义而非精确匹配来查找记忆。要启用此功能,请使用嵌入模型配置存储:

const store = new InMemoryStore({
  index: {
    embeddings: new OpenAIEmbeddings({ model: "text-embedding-3-small" }),
    dims: 1536,
    fields: ["food_preference", "$"], // Fields to embed
  },
});

现在搜索时,您可以使用自然语言查询来查找相关记忆:

// Find memories about food preferences
// (This can be done after putting memories into the store)
const memories = await store.search(namespaceForMemory, {
  query: "What does the user like to eat?",
  limit: 3, // Return top 3 matches
});

您可以通过配置 fields 参数或通过在存储记忆时指定 index 参数来控制记忆的哪些部分被嵌入:

// Store with specific fields to embed
await store.put(
  namespaceForMemory,
  crypto.randomUUID(),
  {
    food_preference: "I love Italian cuisine",
    context: "Discussing dinner plans",
  },
  { index: ["food_preference"] } // Only embed "food_preferences" field
);

// Store without embedding (still retrievable, but not searchable)
await store.put(
  namespaceForMemory,
  crypto.randomUUID(),
  { system_info: "Last updated: 2024-01-01" },
  { index: false }
);

在 LangGraph 中使用

memoryStore 与检查点配合使用:检查点将状态保存到线程(如上所述),而 memoryStore 允许您存储任意信息以供访问 _跨_ 个线程访问。将检查点和 memoryStore 一起编译图,如下所示。

// We need this because we want to enable threads (conversations)
const checkpointer = new MemorySaver();

// ... Define the graph ...

// Compile the graph with the checkpointer and store
const graph = workflow.compile({ checkpointer, store: memoryStore });

然后使用 thread_id,与之前相同,并且也带有 user_id,它作为该特定用户的记忆命名空间,与之前相同。

// Invoke the graph
const userId = "1";
const config = { configurable: { thread_id: "1" }, context: { userId } };

// First let's just say hi to the AI
for await (const update of await graph.stream(
  { messages: [{ role: "user", content: "hi" }] },
  { ...config, streamMode: "updates" }
)) {
  console.log(update);
}

您可以从任何节点访问 store 和 userId 从 _任何节点_ 通过 runtime 参数。您可以使用它来保存记忆:

const MessagesState = new StateSchema({
  messages: MessagesValue,
});

const updateMemory: GraphNode<typeof MessagesState> = async (state, runtime) => {
  // Get the user id from the config
  const userId = runtime.context?.user_id;
  if (!userId) throw new Error("User ID is required");

  // Namespace the memory
  const namespace = [userId, "memories"];

  // ... Analyze conversation and create a new memory
  const memory = "Some memory content";

  // Create a new memory ID
  const memoryId = crypto.randomUUID();

  // We create a new memory
  await runtime.store?.put(namespace, memoryId, { memory });
};

您还可以从任何节点访问 store,并使用 store.search 方法获取记忆。记忆作为对象列表返回,可以转换为字典。

memories[memories.length - 1];
// {
//   value: { food_preference: 'I like pizza' },
//   key: '07e0caf4-1631-47b7-b15f-65515d4c1843',
//   namespace: ['1', 'memories'],
//   createdAt: '2024-10-02T17:22:31.590602+00:00',
//   updatedAt: '2024-10-02T17:22:31.590605+00:00'
// }

您访问记忆并在模型调用中使用它们。

const callModel: GraphNode<typeof MessagesState> = async (state, runtime) => {
  // Get the user id from the config
  const userId = runtime.context?.user_id;

  // Namespace the memory
  const namespace = [userId, "memories"];

  // Search based on the most recent message
  const memories = await runtime.store?.search(namespace, {
    query: state.messages[state.messages.length - 1].content,
    limit: 3,
  });
  const info = memories.map((d) => d.value.memory).join("\n");

  // ... Use memories in the model call
};

如果您创建新线程,只要 user_id 相同,您仍然可以访问相同的记忆。

// Invoke the graph
const config = { configurable: { thread_id: "2" }, context: { userId: "1" } };

// Let's say hi again
for await (const update of await graph.stream(
  { messages: [{ role: "user", content: "hi, tell me about my memories" }] },
  { ...config, streamMode: "updates" }
)) {
  console.log(update);
}

当您在本地使用 LangSmith 时(例如在 Studio) or 托管环境中),基础 store 可默认使用,在编译图时无需指定。但是,要启用语义搜索,您 **do** 需要在您的 langgraph.json 文件中配置索引设置。例如:

{
    ...
    "store": {
        "index": {
            "embed": "openai:text-embeddings-3-small",
            "dims": 1536,
            "fields": ["$"]
        }
    }
}

请参阅 部署指南 了解更多详情和配置选项。

后续步骤