跳至内容

持久化

LangGraph 具有内置的持久化层,通过检查点实现。当您使用检查点编译图时,检查点会在每个超级步保存图状态的检查点。这些检查点被保存到一个线程中,可以在图执行后访问。由于线程允许在执行后访问图的状态,因此可以实现多种强大的功能,包括人机交互、内存、时间旅行和容错。请参阅此操作指南,了解有关如何在您的图中添加和使用检查点的端到端示例。下面,我们将更详细地讨论每个概念。

Checkpoints

线程

线程是由检查点保存的每个检查点分配的唯一 ID 或线程标识符。当使用检查点调用图时,您**必须**在配置的可配置部分指定thread_id

{"configurable": {"thread_id": "1"}}

检查点

检查点是在每个超级步保存的图状态的快照,由具有以下关键属性的StateSnapshot对象表示

  • config:与该检查点关联的配置。
  • metadata:与该检查点关联的元数据。
  • values:此时状态通道的值。
  • next:图中接下来要执行的节点名称的元组。
  • tasks:包含有关接下来要执行的任务的信息的PregelTask对象的元组。如果该步骤之前已尝试过,则将包含错误信息。如果图从节点内部动态中断,则任务将包含与中断相关的其他数据。

让我们看看当一个简单的图按如下方式调用时保存了哪些检查点

import { StateGraph, START, END } from "langgraph";
import { MemorySaver } from "langgraph/checkpoint";
import { Annotation } from "@langchain/core/utils/types";

const GraphAnnotation = Annotation.Object({
  foo: Annotation<string>
  bar: Annotation<string[]>({
    reducer: (a, b) => [...a, ...b],
    default: () => [],
  })
});

function nodeA(state: typeof GraphAnnotation.State) {
  return { foo: "a", bar: ["a"] };
}

function nodeB(state: typeof GraphAnnotation.State) {
  return { foo: "b", bar: ["b"] };
}

const workflow = new StateGraph(GraphAnnotation);
workflow.addNode("nodeA", nodeA);
workflow.addNode("nodeB", nodeB);
workflow.addEdge(START, "nodeA");
workflow.addEdge("nodeA", "nodeB");
workflow.addEdge("nodeB", END);

const checkpointer = new MemorySaver();
const graph = workflow.compile({ checkpointer });

const config = { configurable: { thread_id: "1" } };
await graph.invoke({ foo: "" }, config);

运行图后,我们预计会看到正好4个检查点

  • 空检查点,其中START是接下来要执行的节点
  • 具有用户输入{foo: '', bar: []}nodeA作为接下来要执行的节点的检查点
  • 具有nodeA输出{foo: 'a', bar: ['a']}nodeB作为接下来要执行的节点的检查点
  • 具有nodeB输出{foo: 'b', bar: ['a', 'b']}并且没有接下来要执行的节点的检查点

请注意,由于我们为bar通道有一个归约器,因此bar通道值包含来自两个节点的输出。

获取状态

与保存的图状态交互时,您**必须**指定线程标识符。您可以通过调用await graph.getState(config)来查看图的最新状态。这将返回一个StateSnapshot对象,该对象对应于与配置中提供的线程 ID 关联的最新检查点,或者如果提供了检查点 ID,则对应于该线程的检查点关联的检查点。

// Get the latest state snapshot
const config = { configurable: { thread_id: "1" } };
const state = await graph.getState(config);

// Get a state snapshot for a specific checkpoint_id
const configWithCheckpoint = { configurable: { thread_id: "1", checkpoint_id: "1ef663ba-28fe-6528-8002-5a559208592c" } };
const stateWithCheckpoint = await graph.getState(configWithCheckpoint);

在我们的示例中,getState的输出如下所示

{
  values: { foo: 'b', bar: ['a', 'b'] },
  next: [],
  config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1ef663ba-28fe-6528-8002-5a559208592c' } },
  metadata: { source: 'loop', writes: { nodeB: { foo: 'b', bar: ['b'] } }, step: 2 },
  created_at: '2024-08-29T19:19:38.821749+00:00',
  parent_config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' } },
  tasks: []
}

获取状态历史记录

您可以通过调用await graph.getStateHistory(config)来获取给定线程的图执行的完整历史记录。这将返回与配置中提供的线程 ID 关联的StateSnapshot对象的列表。重要的是,检查点将按时间顺序排序,最新的检查点/StateSnapshot位于列表的第一个。

const config = { configurable: { thread_id: "1" } };
const history = await graph.getStateHistory(config);

在我们的示例中,getStateHistory的输出如下所示

[
  {
    values: { foo: 'b', bar: ['a', 'b'] },
    next: [],
    config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1ef663ba-28fe-6528-8002-5a559208592c' } },
    metadata: { source: 'loop', writes: { nodeB: { foo: 'b', bar: ['b'] } }, step: 2 },
    created_at: '2024-08-29T19:19:38.821749+00:00',
    parent_config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' } },
    tasks: [],
  },
  {
    values: { foo: 'a', bar: ['a'] },
    next: ['nodeB'],
    config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1ef663ba-28f9-6ec4-8001-31981c2c39f8' } },
    metadata: { source: 'loop', writes: { nodeA: { foo: 'a', bar: ['a'] } }, step: 1 },
    created_at: '2024-08-29T19:19:38.819946+00:00',
    parent_config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1ef663ba-28f4-6b4a-8000-ca575a13d36a' } },
    tasks: [{ id: '6fb7314f-f114-5413-a1f3-d37dfe98ff44', name: 'nodeB', error: null, interrupts: [] }],
  },
  // ... (other checkpoints)
]

State

回放

还可以回放先前的图执行。如果我们使用thread_idcheckpoint_id调用图,那么我们将从对应于checkpoint_id的检查点重新播放该图。

  • thread_id只是线程的 ID。这始终是必需的。
  • checkpoint_id此标识符引用线程内的特定检查点。

在调用图时,您必须将这些作为配置的可配置部分传递

// { configurable: { thread_id: "1" } }  // valid config
// { configurable: { thread_id: "1", checkpoint_id: "0c62ca34-ac19-445d-bbb0-5b4984975b2a" } }  // also valid config

const config = { configurable: { thread_id: "1" } };
await graph.invoke(inputs, config);

重要的是,LangGraph 知道特定的检查点是否之前已执行过。如果是,LangGraph 只需重新播放图中的那个特定步骤,而不重新执行该步骤。请参阅此关于时间旅行的操作指南,以了解有关回放的更多信息。

Replay

更新状态

除了从特定的检查点重新播放图之外,我们还可以编辑图的状态。我们使用graph.updateState()执行此操作。此方法有三个不同的参数

配置

配置应包含thread_id,指定要更新哪个线程。当仅传递thread_id时,我们将更新(或派生)当前状态。或者,如果我们包含checkpoint_id字段,那么我们将派生该选定的检查点。

这些是将用于更新状态的值。请注意,此更新与节点的任何更新的处理方式完全相同。这意味着这些值将传递给归约器函数(如果为图状态中的某些通道定义了这些函数)。这意味着updateState不会自动覆盖每个通道的状态通道值,而仅覆盖没有归约器的通道。让我们来看一个例子。

假设您已使用以下模式定义了图的状态(请参阅上面的完整示例)

import { Annotation } from "@langchain/core/utils/types";

const GraphAnnotation = Annotation.Object({
  foo: Annotation<string>
  bar: Annotation<string[]>({
    reducer: (a, b) => [...a, ...b],
    default: () => [],
  })
});

现在假设图的当前状态为

{ foo: "1", bar: ["a"] }

如果您按如下方式更新状态

await graph.updateState(config, { foo: "2", bar: ["b"] });

那么图的新状态将为

{ foo: "2", bar: ["a", "b"] }

foo键(通道)已完全更改(因为没有为该通道指定归约器,因此updateState会覆盖它)。但是,为bar键指定了一个归约器,因此它会将"b"追加到bar的状态。

作为节点

调用updateState时,您还可以选择指定的最后一个参数是第三个位置参数asNode。如果您提供了它,则更新将被应用,就好像它来自节点asNode一样。如果未提供asNode,则将其设置为更新状态的最后一个节点(如果不明确)。这样做的原因是接下来要执行的步骤取决于最后一个给出更新的节点,因此这可以用来控制接下来执行哪个节点。请参阅此关于时间旅行的操作指南,以了解有关派生状态的更多信息。

Update

检查点库

在后台,检查点由符合BaseCheckpointSaver接口的检查点对象提供支持。LangGraph 提供了几个检查点实现,所有这些实现都是通过独立的可安装库实现的

  • @langchain/langgraph-checkpoint:检查点保存器(BaseCheckpointSaver)和序列化/反序列化接口(SerializerProtocol)的基本接口。包括用于实验的内存中检查点实现(MemorySaver)。LangGraph 附带了@langchain/langgraph-checkpoint
  • @langchain/langgraph-checkpoint-sqlite:使用 SQLite 数据库的 LangGraph 检查点实现(SqliteSaver)。非常适合实验和本地工作流程。需要单独安装。

检查点接口

每个检查点都符合BaseCheckpointSaver接口并实现以下方法

  • .put - 使用其配置和元数据存储检查点。
  • .putWrites - 存储链接到检查点的中间写入(即待处理写入)。
  • .getTuple - 使用给定的配置(thread_idcheckpoint_id)获取检查点元组。这用于在graph.getState()中填充StateSnapshot
  • .list - 列出与给定配置和筛选条件匹配的检查点。这用于在graph.getStateHistory()中填充状态历史记录

功能

人机交互

首先,检查点通过允许人工检查、中断和批准图步骤来促进人机交互工作流工作流。这些工作流需要检查点,因为人工需要能够在任何时间点查看图的状态,并且图必须能够在人工对状态进行任何更新后恢复执行。请参阅这些操作指南以了解具体示例。

内存

其次,检查点允许在交互之间进行"内存"。在重复的人工交互(如对话)的情况下,任何后续消息都可以发送到该线程,该线程将保留其对先前消息的记忆。请参阅此操作指南,了解有关如何使用检查点添加和管理对话内存的端到端示例。

时间旅行

第三,检查点允许进行"时间旅行",允许用户回放先前的图执行以审查和/或调试特定的图步骤。此外,检查点可以使图状态在任意检查点处派生,以探索替代轨迹。

容错

最后,检查点还提供容错和错误恢复:如果一个或多个节点在给定的超级步失败,您可以从最后成功的步骤重新启动图。此外,当图节点在给定的超级步执行过程中失败时,LangGraph 会存储来自在该超级步成功完成的任何其他节点的挂起的检查点写入,以便无论何时我们从该超级步恢复图执行,我们都不会重新运行成功的节点。

待处理写入

此外,当图节点在给定的超级步执行过程中失败时,LangGraph 会存储来自在该超级步成功完成的任何其他节点的挂起的检查点写入,以便无论何时我们从该超级步恢复图执行,我们都不会重新运行成功的节点。