跳至内容

低级概念指南

LangGraph的核心是将代理工作流建模为图。你可以使用三个关键组件来定义代理的行为

  1. State: 表示应用程序当前快照的共享数据结构。它由一个Annotation 对象表示。

  2. Nodes: 编码代理逻辑的 JavaScript/TypeScript 函数。它们接收当前的State 作为输入,执行一些计算或副作用,并返回更新后的State

  3. Edges: JavaScript/TypeScript 函数,根据当前State 确定要执行的下一个Node。它们可以是条件分支或固定转换。

通过组合NodesEdges,你可以创建随着时间的推移而演化State 的复杂循环工作流。然而,真正的力量来自 LangGraph 如何管理该State。强调一下:NodesEdges 仅仅是 JavaScript/TypeScript 函数 - 它们可以包含 LLM 或只是普通的 JavaScript/TypeScript 代码。

简而言之:节点执行工作。边告诉下一步该做什么

LangGraph 的底层图算法使用消息传递 来定义一个通用程序。当一个 Node 完成其操作时,它会沿着一条或多条边向其他节点(s) 发送消息。这些接收节点随后执行它们的函数,将结果消息传递给下一组节点,该过程继续进行。受 Google 的Pregel 系统的启发,程序在离散的“超步”中进行。

超步可以被认为是对图节点的一次迭代。在同一超步中并行运行的节点属于同一超步,而在不同超步中顺序运行的节点则属于不同的超步。在图执行开始时,所有节点都处于inactive 状态。当节点在任何传入边(或“通道”)上接收到新消息(状态)时,它会变为active 状态。然后,活动节点运行其函数并以更新做出响应。在每个超步结束时,没有传入消息的节点会通过将自身标记为inactive 来投票halt。当所有节点都处于inactive 状态且没有消息正在传输时,图执行终止。

状态图

StateGraph 类是使用的主要图类。它由用户定义的State 对象参数化。(使用Annotation 对象定义,并作为第一个参数传递)

消息图 (已弃用)

MessageGraph 类是一种特殊的图类型。MessageGraphState 仅仅是消息数组。除了聊天机器人以外,很少使用此类,因为大多数应用程序都需要State 比消息数组更复杂。

编译你的图

要构建你的图,首先定义状态,然后添加节点,然后进行编译。编译你的图究竟是什么,为什么要编译?

编译是一个非常简单的步骤。它对图的结构进行了一些基本的检查(没有孤立的节点等)。这也是你可以指定运行时参数的地方,例如检查点断点。你可以通过调用.compile 方法来编译你的图

const graph = graphBuilder.compile(...);

必须在使用你的图之前对其进行编译。

状态

定义图时,首先要做的就是定义图的StateState 包括有关图结构的信息,以及reducer 函数,这些函数指定如何将更新应用于状态。State 的模式将是图中所有NodesEdges 的输入模式,应该使用一个Annotation 对象来定义。所有Nodes 将会发出对State 的更新,然后使用指定的reducer 函数来应用这些更新。

注释

指定图模式的方法是定义一个根Annotation 对象,其中每个键都是状态中的一个项目。

Reducers

Reducers 是理解节点发出的更新如何应用于State 的关键。State 中的每个键都有其独立的 reducer 函数。如果没有显式指定 reducer 函数,则假定所有对该键的更新都应该覆盖它。让我们看几个例子来更好地理解它们。

示例 A

import { StateGraph, Annotation } from "@langchain/langgraph";

const State = Annotation.Root({
  foo: Annotation<number>,
  bar: Annotation<string[]>,
})

const graphBuilder = new StateGraph(State);

在这个例子中,没有为任何键指定 reducer 函数。假设图的输入是{ foo: 1, bar: ["hi"] }。然后假设第一个Node 返回{ foo: 2 }。这被视为对状态的更新。请注意,Node 不需要返回整个State 模式 - 只需要返回更新即可。应用此更新后,State 将变为{ foo: 2, bar: ["hi"] }。如果第二个节点返回{ bar: ["bye"] },则State 将变为{ foo: 2, bar: ["bye"] }

示例 B

import { StateGraph, Annotation } from "@langchain/langgraph";

const State = Annotation.Root({
  foo: Annotation<number>,
  bar: Annotation<string[]>({
    reducer: (state: string[], update: string[]) => state.concat(update),
    default: () => [],
  }),
})

const graphBuilder = new StateGraph(State);

在这个例子中,我们已经更新了bar 字段,使其成为包含reducer 函数的对象。此函数始终接受两个位置参数:stateupdate,其中state 代表当前状态值,update 代表从Node 返回的更新。请注意,第一个键保持不变。假设图的输入是{ foo: 1, bar: ["hi"] }。然后假设第一个Node 返回{ foo: 2 }。这被视为对状态的更新。请注意,Node 不需要返回整个State 模式 - 只需要返回更新即可。应用此更新后,State 将变为{ foo: 2, bar: ["hi"] }。如果第二个节点返回{ bar: ["bye"] },则State 将变为{ foo: 2, bar: ["hi", "bye"] }。请注意,这里bar 键通过将两个数组连接在一起进行更新。

消息注释

MessagesAnnotation 是 LangGraph 中为数不多的几个有见地的组件之一。MessagesAnnotation 是一种特殊的状态注释,旨在简化使用消息数组作为状态中的一个键。具体来说,像这样导入和使用预构建的MessagesAnnotation

import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";

const graph = new StateGraph(MessagesAnnotation)
  .addNode(...)
  ...

等效于手动初始化你的状态,如下所示

import { BaseMessage } from "@langchain/core/messages";
import { Annotation, StateGraph, messagesStateReducer } from "@langchain/langgraph";

export const StateAnnotation = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: messagesStateReducer,
    default: () => [],
  }),
});

const graph = new StateGraph(StateAnnotation)
  .addNode(...)
  ...

MessagesAnnotation 的状态只有一个名为messages 的键。这是一个BaseMessage 数组,带有messagesStateReducer 作为 reducer。messagesStateReducer 基本上将消息添加到现有列表中(它还做了一些不错的额外操作,例如从 OpenAI 消息格式转换为标准 LangChain 消息格式,根据消息 ID 处理更新等等)。

我们经常看到消息数组是状态的一个关键组件,因此这个预构建的状态旨在简化消息的使用。通常,除了消息之外,还需要跟踪更多状态,因此我们看到人们扩展此状态并添加更多字段,例如

import { Annotation, MessagesAnnotation } from "@langchain/langgraph";

const StateWithDocuments = Annotation.Root({
  ...MessagesAnnotation.spec, // Spread in the messages state
  documents: Annotation<string[]>,
})

节点

在 LangGraph 中,节点通常是 JavaScript/TypeScript 函数(同步或async),其中第一个位置参数是状态,(可选)第二个位置参数是“配置”,包含可选的可配置参数(例如thread_id)。

类似于NetworkX,你可以使用addNode 方法将这些节点添加到图中

import { RunnableConfig } from "@langchain/core/runnables";
import { StateGraph, Annotation } from "@langchain/langgraph";

const GraphAnnotation = Annotation.Root({
  input: Annotation<string>,
  results: Annotation<string>,
})

// The state type can be extracted using `typeof <annotation variable name>.State`
const myNode = (state: typeof GraphAnnotation.State, config?: RunnableConfig) => {
  console.log("In node: ", config.configurable?.user_id);
  return {
    results: `Hello, ${state.input}!`
  }  
}

// The second argument is optional
const myOtherNode = (state: typeof GraphAnnotation.State) => {
  return state
}

const builder = new StateGraph(GraphAnnotation)
  .addNode("myNode", myNode)
  .addNode("myOtherNode", myOtherNode)
  ...

在幕后,函数被转换为RunnableLambda's,它为你的函数添加了批处理和流式处理支持,以及本地跟踪和调试。

START 节点

START 节点是一个特殊节点,表示向图发送用户输入的节点。引用此节点的主要目的是确定首先应调用哪些节点。

import { START } from "@langchain/langgraph";

graph.addEdge(START, "nodeA");

END 节点

END 节点是一个特殊节点,表示终端节点。当要表示哪些边在完成后没有动作时,会引用此节点。

import { END } from "@langchain/langgraph";

graph.addEdge("nodeA", END);

边定义了逻辑的路由方式以及图如何决定停止。这是代理运作方式以及不同节点如何相互通信的重要组成部分。有一些关键类型的边

  • 普通边:直接从一个节点到下一个节点。
  • 条件边:调用函数来确定下一步要转到的节点(s)。
  • 入口点:当用户输入到达时要调用的第一个节点。
  • 条件入口点:调用函数来确定当用户输入到达时要调用的第一个节点(s)。

一个节点可以有多个输出边。如果一个节点有多个输出边,则所有这些目标节点都将在下一个超步中并行执行。

普通边

如果你始终想要从节点 A 转到节点 B,可以直接使用addEdge 方法。

graph.addEdge("nodeA", "nodeB");

条件边

如果你想要可选地路由到 1 个或多个边(或可选地终止),你可以使用addConditionalEdges 方法。此方法接受一个节点的名称和一个“路由函数”,在该节点执行后调用该函数

graph.addConditionalEdges("nodeA", routingFunction);

类似于节点,routingFunction 接受图的当前state 并返回一个值。

默认情况下,返回值 routingFunction 用作要将状态发送到下一个节点的节点(或节点数组)的名称。所有这些节点都将在下一个超级步长中并行运行。

您可以选择提供一个对象,将 routingFunction 的输出映射到下一个节点的名称。

graph.addConditionalEdges("nodeA", routingFunction, {
  true: "nodeB",
  false: "nodeC"
});

入口点

入口点是图开始时运行的第一个节点(或节点集)。您可以使用虚拟 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

默认情况下,节点 是提前定义的,并且在同一个共享状态上运行。但是,在某些情况下,确切的边可能事先未知,或者您可能希望在同一时间存在不同版本的 状态。一个常见的示例是 map-reduce 设计模式。在此设计模式中,第一个节点可能会生成一个对象数组,并且您可能希望将另一个节点应用于所有这些对象。对象的数量事先可能未知(意味着边的数量可能未知),并且下游 节点 的输入 状态 应该不同(每个生成的物体一个)。

为了支持这种设计模式,LangGraph 支持从条件边返回 Send 对象。Send 接受两个参数:第一个是节点的名称,第二个是要传递给该节点的状态。

const continueToJokes = (state: { subjects: string[] }) => {
  return state.subjects.map((subject) => new Send("generate_joke", { subject }));
}

graph.addConditionalEdges("nodeA", continueToJokes);

持久性

LangGraph 具有内置的持久层,通过 检查点程序 实现。当您将检查点程序与图一起使用时,您可以在执行后与图的状态进行交互和管理。检查点程序在每个超级步长都会保存图状态的检查点(快照),从而启用多种强大的功能,包括人机交互、内存和容错。有关更多信息,请参阅此 概念指南

图迁移

LangGraph 可以轻松处理图定义(节点、边和状态)的迁移,即使使用检查点程序跟踪状态也是如此。

  • 对于图末端的线程(即未中断的线程),您可以更改整个图的拓扑结构(即所有节点和边,删除、添加、重命名等)。
  • 对于当前已中断的线程,我们支持除重命名/删除节点以外的所有拓扑更改(因为该线程现在可能即将进入一个不再存在的节点)——如果这是一个阻碍因素,请联系我们,我们可以优先考虑解决方案。
  • 对于修改状态,我们对添加和删除键具有完全的向前和向后兼容性。
  • 重命名的状态键将丢失现有线程中的保存状态。
  • 类型以不兼容的方式更改的状态键当前可能会导致更改前具有状态的线程出现问题——如果这是一个阻碍因素,请联系我们,我们可以优先考虑解决方案。

配置

在创建图时,您还可以标记图的某些部分是可配置的。这通常用于启用模型或系统提示之间的轻松切换。这允许您创建一个单一的“认知架构”(图),但具有多个不同的实例。

然后,您可以使用 configurable 配置字段将此配置传递到图中。

const config = { configurable: { llm: "anthropic" }};

await graph.invoke(inputs, config);

然后,您可以在节点内部访问和使用此配置。

const nodeA = (state, config) => {
  const llmType = config?.configurable?.llm;
  let llm: BaseChatModel;
  if (llmType) {
    const llm = getLlm(llmType);
  }
  ...
};

有关配置的完整细分,请参阅 此指南

断点

在某些节点执行之前或之后设置断点通常很有用。这可以用于在继续之前等待人工批准。这些可以在您 "编译" 图 时设置,或者使用称为 NodeInterrupt 的特殊错误动态抛出。您可以设置断点,节点执行之前(使用 interruptBefore)或节点执行之后(使用 interruptAfter)。

在使用断点时,您必须使用 检查点程序。这是因为您的图需要能够恢复执行。

为了恢复执行,您可以使用 null 作为输入和相同的 thread_id 调用您的图。

const config = { configurable: { thread_id: "foo" } };

// Initial run of graph
await graph.invoke(inputs, config);

// Let's assume it hit a breakpoint somewhere, you can then resume by passing in None
await graph.invoke(null, config);

有关如何添加断点的完整演练,请参阅 此指南

动态断点

根据某些条件,从给定节点内部动态中断图可能会有所帮助。在 LangGraph 中,您可以使用 NodeInterrupt 来实现这一点——这是一种特殊错误,可以从节点内部抛出。

function myNode(state: typeof GraphAnnotation.State): typeof GraphAnnotation.State {
  if (state.input.length > 5) {
    throw new NodeInterrupt(`Received input that is longer than 5 characters: ${state.input}`);
  }

  return state;
}

可视化

能够可视化图通常很有用,尤其是在它们变得越来越复杂时。LangGraph 带有一个内置的漂亮方式,可以将图渲染为 Mermaid 图。您可以像这样使用 getGraph() 方法。

const representation = graph.getGraph();
const image = await representation.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer);

您还可以查看 LangGraph Studio,这是一个包含强大可视化和调试功能的专用 IDE。

流式传输

LangGraph 是在对流式传输提供一流支持的基础上构建的。LangGraph 支持几种不同的流式传输模式。

  • "values": 这会在图的每个步骤后流式传输状态的完整值。
  • "updates: 这会在图的每个步骤后流式传输对状态的更新。如果在同一步骤中进行多个更新(例如,运行多个节点),则这些更新将分别流式传输。

此外,您可以使用 streamEvents 方法流式传输节点内部发生的事件。这对于 流式传输 LLM 调用的令牌 很有用。

LangGraph 是在对流式传输提供一流支持的基础上构建的,包括在执行期间从图节点流式传输更新,从 LLM 调用流式传输令牌等等。有关更多信息,请参阅此 概念指南