跳到主要内容

LangGraph 的核心是将代理工作流建模为图。你使用三个关键组件来定义代理的行为
  1. State:一个共享数据结构,表示应用程序的当前快照。它可以是任何数据类型,但通常使用共享状态模式定义。
  2. Nodes:编码代理逻辑的函数。它们接收当前状态作为输入,执行一些计算或副作用,并返回更新后的状态。
  3. Edges:根据当前状态决定接下来执行哪个 Node 的函数。它们可以是条件分支或固定转换。
通过组合 NodesEdges,你可以创建复杂的、循环的工作流,这些工作流会随着时间的推移而演变状态。然而,真正的力量来自于 LangGraph 管理状态的方式。需要强调的是:NodesEdges 只是函数 - 它们可以包含 LLM 或仅仅是普通的旧代码。 简而言之:节点完成工作,边指示下一步做什么 LangGraph 底层的图算法使用消息传递来定义一个通用程序。当一个节点完成其操作时,它会通过一条或多条边向其他节点发送消息。这些接收节点然后执行其函数,将结果消息传递给下一组节点,并继续该过程。受 Google Pregel 系统的启发,该程序以离散的“超级步骤”进行。 一个超级步骤可以被认为是图节点的一次迭代。并行运行的节点属于同一个超级步骤,而顺序运行的节点属于不同的超级步骤。在图执行开始时,所有节点都处于 inactive 状态。当一个节点在其任何传入边(或“通道”)上接收到新消息(状态)时,它会变为 active。活动节点然后运行其函数并返回更新。在每个超级步骤结束时,没有传入消息的节点会通过将自己标记为 inactive 来投票 halt。当所有节点都处于 inactive 状态且没有消息正在传输时,图执行终止。

状态图

StateGraph 类是主要的图类。它由用户定义的 State 对象参数化。

编译你的图

要构建你的图,你首先定义状态,然后添加节点,然后编译它。编译你的图到底是什么意思,为什么需要它? 编译是一个非常简单的步骤。它对你的图的结构进行一些基本检查(没有孤立节点等)。你还可以在其中指定运行时参数,例如检查点和断点。你只需调用 .compile 方法即可编译你的图:
const graph = new StateGraph(StateAnnotation)
  .addNode("nodeA", nodeA)
  .addEdge(START, "nodeA")
  .addEdge("nodeA", END)
  .compile();
必须在使用图之前编译它。

状态

当你定义一个图时,你做的第一件事就是定义图的 StateState 包含图的模式以及reducer 函数,这些函数指定如何将更新应用于状态。State 的模式将是图中所有 NodesEdges 的输入模式,并且可以是 Zod 模式或使用 Annotation.Root 构建的模式。所有 Nodes 都将发出对 State 的更新,然后使用指定的 reducer 函数应用这些更新。

Schema

指定图模式的主要文档方式是使用 Zod 模式。但是,我们也支持使用 Annotation API 来定义图的模式。 默认情况下,图将具有相同的输入和输出模式。如果你想改变这一点,你也可以直接指定明确的输入和输出模式。当你有很多键,并且有些键专门用于输入,有些键专门用于输出时,这会很有用。

多个模式

通常,所有图节点都使用单个模式进行通信。这意味着它们将读取和写入相同的状态通道。但是,在某些情况下,我们希望对此有更多的控制。
  • 内部节点可以传递图中输入/输出不需要的信息。
  • 我们可能还希望为图使用不同的输入/输出模式。例如,输出可能只包含一个相关的输出键。
可以将节点写入图内的私有状态通道以进行内部节点通信。我们可以简单地定义一个私有模式 PrivateState 还可以为图定义显式输入和输出模式。在这些情况下,我们定义一个包含所有与图操作相关的键的“内部”模式。但是,我们也定义了作为“内部”模式子集的 inputoutput 模式,以约束图的输入和输出。有关更多详细信息,请参阅此指南 让我们看一个示例:
const InputState = z.object({
  userInput: z.string(),
});

const OutputState = z.object({
  graphOutput: z.string(),
});

const OverallState = z.object({
  foo: z.string(),
  userInput: z.string(),
  graphOutput: z.string(),
});

const PrivateState = z.object({
  bar: z.string(),
});

const graph = new StateGraph({
  state: OverallState,
  input: InputState,
  output: OutputState,
})
  .addNode("node1", (state) => {
    // Write to OverallState
    return { foo: state.userInput + " name" };
  })
  .addNode("node2", (state) => {
    // Read from OverallState, write to PrivateState
    return { bar: state.foo + " is" };
  })
  .addNode(
    "node3",
    (state) => {
      // Read from PrivateState, write to OutputState
      return { graphOutput: state.bar + " Lance" };
    },
    { input: PrivateState }
  )
  .addEdge(START, "node1")
  .addEdge("node1", "node2")
  .addEdge("node2", "node3")
  .addEdge("node3", END)
  .compile();

await graph.invoke({ userInput: "My" });
// { graphOutput: 'My name is Lance' }
这里有两个微妙且重要的注意事项
  1. 我们将 state 作为输入模式传递给 node1。但是,我们写入 foo,这是 OverallState 中的一个通道。我们如何写入未包含在输入模式中的状态通道?这是因为节点可以写入图状态中的任何状态通道。图状态是初始化时定义的状态通道的并集,其中包括 OverallState 以及过滤器 InputStateOutputState
  2. 我们使用 StateGraph({ state: OverallState, input: InputState, output: OutputState }) 初始化图。那么,我们如何在 node2 中写入 PrivateState 呢?如果 PrivateState 未在 StateGraph 初始化中传递,图如何访问此模式?我们可以这样做,因为节点还可以声明额外的状态通道,只要状态模式定义存在即可。在这种情况下,PrivateState 模式已定义,因此我们可以在图中添加 bar 作为新的状态通道并写入它。

Reducer

Reducers 是理解节点更新如何应用于 State 的关键。State 中的每个键都有其自己的独立 reducer 函数。如果未明确指定 reducer 函数,则假定对该键的所有更新都应该覆盖它。有几种不同类型的 reducer,从默认类型的 reducer 开始

默认 Reducer

这两个例子展示了如何使用默认的 reducer: 例子 A:
const State = z.object({
  foo: z.number(),
  bar: z.array(z.string()),
});
在此示例中,未为任何键指定 reducer 函数。我们假设图的输入为: { foo: 1, bar: ["hi"] }。然后我们假设第一个 Node 返回 { foo: 2 }。这被视为对状态的更新。请注意,Node 不需要返回整个 State 模式 - 只需返回一个更新。应用此更新后,State 将变为 { foo: 2, bar: ["hi"] }。如果第二个节点返回 { bar: ["bye"] },则 State 将变为 { foo: 2, bar: ["bye"] } 例子 B:
import * as z from "zod";
import { registry } from "@langchain/langgraph/zod";

const State = z.object({
  foo: z.number(),
  bar: z.array(z.string()).register(registry, {
    reducer: {
      fn: (x, y) => x.concat(y),
    },
    default: () => [] as string[],
  }),
});
在此示例中,我们使用Zod 4 注册表为第二个键(bar)指定了一个 reducer 函数。请注意,第一个键保持不变。我们假设图的输入是 { foo: 1, bar: ["hi"] }。然后我们假设第一个 Node 返回 { foo: 2 }。这被视为对状态的更新。请注意,Node 不需要返回整个 State 模式 - 只需返回一个更新。应用此更新后,State 将变为 { foo: 2, bar: ["hi"] }。如果第二个节点返回 { bar: ["bye"] },则 State 将变为 { foo: 2, bar: ["hi", "bye"] }。请注意,这里 bar 键是通过将两个数组相加来更新的。

在图状态中使用消息

为什么要使用消息?

大多数现代 LLM 提供商都有一个聊天模型接口,它接受消息列表作为输入。LangChain 的ChatModel 特别接受 Message 对象列表作为输入。这些消息以多种形式出现,例如 @[HumanMessage](用户输入)或 AIMessage(LLM 响应)。要了解有关消息对象的更多信息,请参阅概念指南。

在你的图中使用消息

在许多情况下,将之前的对话历史记录作为消息列表存储在图状态中会很有帮助。为此,我们可以在图状态中添加一个键(通道)来存储 Message 对象列表,并用 reducer 函数对其进行注释(参见下面示例中的 messages 键)。reducer 函数对于告诉图如何用每次状态更新(例如,当节点发送更新时)更新状态中的 Message 对象列表至关重要。如果你不指定 reducer,每次状态更新都会用最新提供的值覆盖消息列表。如果你想简单地将消息附加到现有列表,你可以使用一个连接数组的函数作为 reducer。 然而,你可能还想手动更新图状态中的消息(例如,人工干预)。如果你使用简单的连接函数,你发送给图的手动状态更新将被附加到现有消息列表,而不是更新现有消息。为了避免这种情况,你需要一个能够跟踪消息 ID 并在更新时覆盖现有消息的 reducer。要实现这一点,你可以使用预构建的 messagesStateReducer 函数或在状态模式用 Zod 定义时使用 MessagesZodMeta。对于全新的消息,它将简单地附加到现有列表,但它也将正确处理现有消息的更新。

序列化

除了跟踪消息 ID 之外,MessagesZodMeta 还会在 messages 通道收到状态更新时尝试将消息反序列化为 LangChain Message 对象。这允许以以下格式发送图输入/状态更新
// this is supported
{
  messages: [new HumanMessage("message")];
}

// and this is also supported
{
  messages: [{ role: "human", content: "message" }];
}
由于在使用 MessagesZodMeta 时状态更新总是反序列化为 LangChain Messages,因此你应该使用点符号访问消息属性,例如 state.messages[state.messages.length - 1].content。下面是一个使用 MessagesZodMeta 的图示例
import { StateGraph, MessagesZodMeta } from "@langchain/langgraph";
import { registry } from "@langchain/langgraph/zod";
import * as z from "zod";

const MessagesZodState = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
});

const graph = new StateGraph(MessagesZodState)
  ...
MessagesZodState 定义了一个单独的 messages 键,它是一个 @[BaseMessage] 对象列表,并使用适当的 reducer。通常,除了消息之外还需要跟踪更多状态,因此我们看到人们扩展此状态并添加更多字段,例如
const State = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
  documents: z.array(z.string()),
});

节点

在 LangGraph 中,节点通常是接受以下参数的函数(同步或异步)
  1. state:图的状态
  2. config:一个RunnableConfig 对象,包含配置信息,如 thread_id 和跟踪信息,如 tags
你可以使用 addNode 方法将节点添加到图中。
import { StateGraph } from "@langchain/langgraph";
import { RunnableConfig } from "@langchain/core/runnables";
import * as z from "zod";

const State = z.object({
  input: z.string(),
  results: z.string(),
});

const builder = new StateGraph(State);
  .addNode("myNode", (state, config) => {
    console.log("In node: ", config?.configurable?.user_id);
    return { results: `Hello, ${state.input}!` };
  })
  addNode("otherNode", (state) => {
    return state;
  })
  ...
在幕后,函数被转换为RunnableLambda,它为你的函数添加了批量和异步支持,以及原生跟踪和调试。 如果你在图中添加节点时未指定名称,它将获得一个与函数名称相同的默认名称。
builder.addNode(myNode);
// You can then create edges to/from this node by referencing it as `"myNode"`

START 节点

START 节点是一个特殊节点,表示将用户输入发送到图的节点。引用此节点的主要目的是确定应首先调用哪些节点。
import { START } from "@langchain/langgraph";

graph.addEdge(START, "nodeA");

END 节点

END 节点是一个特殊的节点,表示一个终止节点。当你希望表示哪些边在完成后没有后续操作时,会引用此节点。
import { END } from "@langchain/langgraph";

graph.addEdge("nodeA", END);

节点缓存

LangGraph 支持基于节点输入进行任务/节点缓存。要使用缓存:
  • 在编译图(或指定入口点)时指定一个缓存
  • 为节点指定缓存策略。每个缓存策略支持:
    • keyFunc,用于根据节点输入生成缓存键。
    • ttl,缓存的生存时间(秒)。如果未指定,缓存将永不过期。
import { StateGraph, MessagesZodMeta } from "@langchain/langgraph";
import { registry } from "@langchain/langgraph/zod";
import * as z from "zod";
import { InMemoryCache } from "@langchain/langgraph-checkpoint";

const MessagesZodState = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
});

const graph = new StateGraph(MessagesZodState)
  .addNode(
    "expensive_node",
    async () => {
      // Simulate an expensive operation
      await new Promise((resolve) => setTimeout(resolve, 3000));
      return { result: 10 };
    },
    { cachePolicy: { ttl: 3 } }
  )
  .addEdge(START, "expensive_node")
  .compile({ cache: new InMemoryCache() });

await graph.invoke({ x: 5 }, { streamMode: "updates" });   
// [{"expensive_node": {"result": 10}}]
await graph.invoke({ x: 5 }, { streamMode: "updates" });   
// [{"expensive_node": {"result": 10}, "__metadata__": {"cached": true}}]

边定义了逻辑的路由方式以及图如何决定停止。这是代理工作方式以及不同节点之间如何通信的重要组成部分。边有几种主要类型
  • 普通边:直接从一个节点到下一个节点。
  • 条件边:调用一个函数来决定接下来要去哪个节点(或哪些节点)。
  • 入口点:当用户输入到达时,首先调用哪个节点。
  • 条件入口点:当用户输入到达时,调用一个函数来决定首先调用哪个节点(或哪些节点)。
一个节点可以有多个出边。如果一个节点有多个出边,则所有这些目标节点都将在下一个超级步骤中并行执行。

普通边

如果你总是想从节点 A 到节点 B,你可以直接使用 @[addEdge] 方法。
graph.addEdge("nodeA", "nodeB");

条件边

如果你想有条件地路由到一个或多个边缘(或有条件地终止),你可以使用 @[addConditionalEdges] 方法。此方法接受一个节点名称和一个在该节点执行后调用的“路由函数”
graph.addConditionalEdges("nodeA", routingFunction);
与节点类似,routingFunction 接受图的当前 state 并返回一个值。 默认情况下,routingFunction 的返回值用作下一个要发送状态的节点名称(或节点列表)。所有这些节点将在下一个超级步骤中并行运行。 你可以选择提供一个对象,将 routingFunction 的输出映射到下一个节点的名称。
graph.addConditionalEdges("nodeA", routingFunction, {
  true: "nodeB",
  false: "nodeC",
});
如果你想在一个函数中结合状态更新和路由,请使用Command而不是条件边。

入口点

入口点是图启动时运行的第一个节点。你可以使用虚拟 START 节点的 @[addEdge] 方法到第一个要执行的节点来指定进入图的位置。
import { START } from "@langchain/langgraph";

graph.addEdge(START, "nodeA");

条件入口点

条件入口点允许你根据自定义逻辑从不同的节点开始。你可以使用虚拟 START 节点的 @[addConditionalEdges] 来实现此目的。
import { START } from "@langchain/langgraph";

graph.addConditionalEdges(START, routingFunction);
你可以选择提供一个对象,将 routingFunction 的输出映射到下一个节点的名称。
graph.addConditionalEdges(START, routingFunction, {
  true: "nodeB",
  false: "nodeC",
});

发送

默认情况下,NodesEdges 是预先定义的,并作用于相同的共享状态。然而,在某些情况下,确切的边可能无法预先知道,并且/或者你可能希望同时存在不同版本的 State。一个常见的例子是 map-reduce 设计模式。在此设计模式中,第一个节点可能生成一个对象列表,并且你可能希望将其他节点应用于所有这些对象。对象的数量可能无法预先知道(意味着边的数量可能无法知道),并且下游 Node 的输入 State 应该不同(每个生成的对象一个)。 为了支持这种设计模式,LangGraph 支持从条件边返回 Send 对象。Send 接受两个参数:第一个是节点名称,第二个是传递给该节点的状态。
import { Send } from "@langchain/langgraph";

graph.addConditionalEdges("nodeA", (state) => {
  return state.subjects.map((subject) => new Send("generateJoke", { subject }));
});

命令

将控制流(边)和状态更新(节点)结合起来会很有用。例如,你可能希望在同一个节点中同时执行状态更新和决定接下来要前往哪个节点。LangGraph 提供了一种通过从节点函数返回 Command 对象来实现此目的的方法
import { Command } from "@langchain/langgraph";

graph.addNode("myNode", (state) => {
  return new Command({
    update: { foo: "bar" },
    goto: "myOtherNode",
  });
});
使用 Command 你还可以实现动态控制流行为(与条件边相同)
import { Command } from "@langchain/langgraph";

graph.addNode("myNode", (state) => {
  if (state.foo === "bar") {
    return new Command({
      update: { foo: "baz" },
      goto: "myOtherNode",
    });
  }
});
在节点函数中使用 Command 时,你必须在添加节点时添加 ends 参数,以指定它可以路由到哪些节点
builder.addNode("myNode", myNode, {
  ends: ["myOtherNode", END],
});
当在节点函数中返回 Command 时,你必须添加带有节点名称列表的返回类型注解,例如 Command[Literal["my_other_node"]]。这对于图渲染是必要的,并告诉 LangGraph my_node 可以导航到 my_other_node
请查阅这篇操作指南,获取如何使用 Command 的端到端示例。

什么时候应该使用命令而不是条件边?

  • 当你需要同时更新图状态路由到不同的节点时,请使用 Command。例如,在实现多代理交接时,路由到不同的代理并向该代理传递一些信息很重要。
  • 使用条件边在节点之间有条件地路由,而无需更新状态。
如果你正在使用子图,你可能希望从子图中的一个节点导航到另一个子图(即父图中的不同节点)。为此,你可以在 Command 中指定 graph: Command.PARENT
import { Command } from "@langchain/langgraph";

graph.addNode("myNode", (state) => {
  return new Command({
    update: { foo: "bar" },
    goto: "otherSubgraph", // where `otherSubgraph` is a node in the parent graph
    graph: Command.PARENT,
  });
});
graph 设置为 Command.PARENT 将导航到最近的父图。当你从子图节点向父图节点发送更新,用于父图和子图状态模式共享的键时,你必须在父图状态中为你正在更新的键定义一个reducer
如果你正在使用子图,你可能希望从子图中的一个节点导航到另一个子图(即父图中的不同节点)。为此,你可以在 Command 中指定 graph: Command.PARENT
import { Command } from "@langchain/langgraph";

graph.addNode("myNode", (state) => {
  return new Command({
    update: { foo: "bar" },
    goto: "otherSubgraph", // where `otherSubgraph` is a node in the parent graph
    graph: Command.PARENT,
  });
});
graph 设置为 Command.PARENT 将导航到最近的父图。当你从子图节点向父图节点发送更新,用于父图和子图状态模式共享的键时,你必须在父图状态中为你正在更新的键定义一个reducer
这在实现多代理交接时特别有用。 请查阅此指南了解详细信息。

在工具中使用

一个常见的用例是从工具内部更新图状态。例如,在客户支持应用程序中,你可能希望在对话开始时根据客户的帐号或 ID 查找客户信息。 有关详细信息,请参阅此指南

人工干预

Command 是人机交互工作流的重要组成部分:当使用 interrupt() 收集用户输入时,然后使用 Command 提供输入并通过 new Command({ resume: "User input" }) 恢复执行。有关更多信息,请参阅人机交互概念指南

图迁移

LangGraph 可以轻松处理图定义(节点、边和状态)的迁移,即使在使用检查点来跟踪状态时也是如此。
  • 对于图末尾(即未中断)的线程,你可以更改图的整个拓扑结构(即所有节点和边,移除、添加、重命名等)
  • 对于当前中断的线程,我们支持除重命名/删除节点之外的所有拓扑更改(因为该线程现在可能即将进入一个不再存在的节点)——如果这是一个障碍,请联系我们,我们可以优先解决。
  • 对于修改状态,我们完全向后兼容和向前兼容添加和删除键
  • 重命名的状态键会丢失其在现有线程中保存的状态
  • 如果状态键的类型以不兼容的方式发生变化,目前可能会导致在更改之前具有状态的线程中出现问题——如果这是一个障碍,请联系我们,我们可以优先解决。

运行时上下文

创建图时,你可以为传递给节点的运行时上下文指定 contextSchema。这对于将不属于图状态的信息传递给节点很有用。例如,你可能希望传递依赖项,例如模型名称或数据库连接。
import * as z from "zod";

const ContextSchema = z.object({
  llm: z.union([z.literal("openai"), z.literal("anthropic")]),
});

const graph = new StateGraph(State, ContextSchema);
然后你可以使用 context 属性将此配置传递给图。
const config = { context: { llm: "anthropic" } };

await graph.invoke(inputs, config);
然后你可以在节点或条件边内部访问和使用此上下文
import { Runtime } from "@langchain/langgraph";
import * as z from "zod";

const nodeA = (
  state: z.infer<typeof State>,
  runtime: Runtime<z.infer<typeof ContextSchema>>,
) => {
  const llm = getLLM(runtime.context?.llm);
  // ...
};
请参阅此指南,了解有关配置的完整说明。
graph.addNode("myNode", (state, runtime) => {
  const llmType = runtime.context?.llm || "openai";
  const llm = getLLM(llmType);
  return { results: `Hello, ${state.input}!` };
});

递归限制

递归限制设置了图在单次执行期间可以执行的超级步骤的最大数量。一旦达到此限制,LangGraph 将引发 GraphRecursionError。默认情况下,此值设置为 25 步。递归限制可以在运行时在任何图上设置,并通过配置对象传递给 invoke/stream。重要的是,recursionLimit 是一个独立的 config 键,不应像所有其他用户定义的配置一样传递到 configurable 键中。请参阅以下示例
await graph.invoke(inputs, {
  recursionLimit: 5,
  context: { llm: "anthropic" },
});

可视化

可视化图通常很有用,尤其当它们变得复杂时。LangGraph 提供了几种内置的图可视化方法。有关更多信息,请参阅此操作指南
以编程方式连接这些文档到 Claude、VSCode 等,通过 MCP 获取实时答案。
© . This site is unofficial and not affiliated with LangChain, Inc.