Skip to main content

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

状态图

The StateGraph 类是主要使用的图类。这是通过一个用户定义的 State 对象进行参数化的。

编译您的图

为了构建您的图,您首先定义状态,然后添加节点,最后进行编译。编译您的图究竟是什么,为什么需要它呢? 编译是一个相对简单的步骤。它对您图的结构进行一些基本的检查(例如,没有孤立节点等)。这里您还可以指定运行时参数,如checkpointers和断点。您只需调用.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 函数应用这些更新。

架构

主要记录的指定图模式的方法是通过使用 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' }
这里有两个微妙且重要的要点需要注意: 我们传入 state 作为输入模式到 node1。但是,我们将输出写入到 foo,这是 OverallState 中的一个通道。我们如何将输出写入到不在输入模式中包含的状态通道呢?这是因为节点 可以写入图状态中的任何状态通道。图状态是初始化时定义的状态通道的并集,包括 OverallState 以及过滤器 InputStateOutputState
  1. 我们使用 StateGraph({ state: OverallState, input: InputState, output: OutputState }) 初始化图。那么,我们如何在 PrivateState 中写入 node2 呢?如果它在 StateGraph 初始化时没有被传递,图是如何获取这个模式的呢?我们可以这样做,因为 节点也可以声明额外的状态通道,只要存在状态模式定义。在这种情况下,PrivateState 模式已被定义,因此我们可以在图中添加 bar 作为新的状态通道并对其进行写入。

约简器

还原器是理解节点更新如何应用于 State 的关键。每个在 State 中的键都有自己的独立还原函数。如果没有明确指定还原函数,则假定对该键的所有更新都应该覆盖它。存在几种不同的还原器类型,首先是默认的还原器类型:

默认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 注册表来指定第二个键的reducer函数(bar)。请注意,第一个键保持不变。假设图输入为{ 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 对象的列表,并用一个还原函数(如下例中的 messages 键)对其进行注释。还原函数对于告诉图如何在每个状态更新时更新状态中 Message 对象的列表至关重要(例如,当一个节点发送更新时)。如果您没有指定还原函数,每次状态更新都会用最近提供的值覆盖消息列表。如果您只想将消息追加到现有列表中,可以使用一个将数组连接起来的函数作为还原函数。 然而,您可能还需要手动更新图状态中的消息(例如,人工干预)。如果您使用简单的连接函数,您发送给图的手动状态更新将被追加到现有的消息列表中,而不是更新现有消息。为了避免这种情况,您需要一个可以跟踪消息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",
});

Send

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

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

Command

可以结合控制流(边)和状态更新(节点)来使用,这可能会很有用。例如,你可能希望在同一个节点中同时执行状态更新并决定下一个要访问的节点。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 将导航到最近的父图。当你从子图节点向父图节点发送更新,且该键由父图和子图状态模式共享时,你必须为父图状态中你正在更新的键定义一个还原器
如果您正在使用子图,您可能希望从一个子图中的节点导航到不同的子图(即父图中的不同节点)。为此,您可以在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 将导航到最近的父图。当你从子图节点向父图节点发送由父图和子图状态模式共享的键的更新时,你必须为父图状态中你正在更新的键定义一个还原器
这在实现多智能体交接时尤其有用。 查看此指南获取详细信息。

在内部工具中使用

一个常见的用例是从工具内部更新图状态。例如,在一个客户支持应用程序中,您可能希望在对话开始时根据客户的账户号码或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提供了几种内置的图形可视化方法。有关更多信息,请参阅本操作指南