图
在其核心,LangGraph 将智能体工作流程建模为图。您可以使用三个关键组件来定义您智能体的行为:-
State:表示您应用程序当前快照的共享数据结构。它可以表示任何数据类型,但通常使用共享状态模式定义。 -
Nodes:编码您智能体逻辑的函数。它们接收当前状态作为输入,执行一些计算或副作用,并返回更新后的状态。 -
Edges:根据当前状态确定要执行下一个Node的函数。它们可以是条件分支或固定转换。
Nodes 和 Edges,您可以创建复杂、循环的工作流程,这些工作流程会随着时间的推移而演变状态。然而,真正的力量来自于 LangGraph 如何管理这种状态。强调一下:Nodes 和 Edges 仅仅是一些函数——它们可以包含一个大型语言模型(LLM)或者仅仅是老式的代码。
简而言之:节点执行工作,边指示下一步做什么。
LangGraph的底层图算法使用消息传递来定义一个通用程序。当一个节点完成其操作时,它会通过一条或多条边向其他节点发送消息。这些接收节点随后执行它们的函数,将产生的消息传递给下一组节点,然后这个过程继续进行。受到Google的Pregel系统的启发,该程序以离散的“超级步骤”进行。
超级步骤可以被视为对图节点的单次迭代。并行运行的节点属于同一个超级步骤,而顺序运行的节点属于不同的超级步骤。在图执行开始时,所有节点都处于一个 inactive 状态。当一个节点在其任何入边(或“通道”)上接收到新的消息(状态)时,它变为 active。然后,活动节点运行其函数并响应更新。在每个超级步骤结束时,没有入消息的节点通过将自己标记为 inactive 来投票 halt。当所有节点都 inactive 并且没有消息在传输时,图执行终止。
状态图
TheStateGraph 类是主要使用的图类。这是通过一个用户定义的 State 对象进行参数化的。
编译您的图
为了构建您的图,您首先定义状态,然后添加节点和边,最后进行编译。编译您的图究竟是什么,为什么需要它呢? 编译是一个相对简单的步骤。它对您图的结构进行一些基本的检查(例如,没有孤立节点等)。这里您还可以指定运行时参数,如checkpointers和断点。您只需调用.compile方法即可编译您的图:
状态
当您定义一个图时,首先要定义图的State。State 包括 图的模式 以及 reducer 函数,这些函数指定了如何应用更新到状态。State 的模式将是图中所有 Nodes 和 Edges 的输入模式,可以是 Zod 模式或使用 Annotation.Root 构建的模式。所有 Nodes 都会向 State 发出更新,然后使用指定的 reducer 函数应用这些更新。
架构
主要记录的指定图模式的方法是通过使用 Zod 模式。然而,我们也支持使用Annotation API 来定义图的模式。
默认情况下,图将具有相同的输入和输出模式。如果您想更改此设置,也可以直接指定显式的输入和输出模式。这在您有很多键,其中一些明确用于输入,而另一些用于输出时非常有用。
多个模式
通常,所有图节点都使用单个模式进行通信。这意味着它们将读取和写入相同的状态通道。但是,有些情况下我们希望对此有更多的控制:- 内部节点可以传递图中输入/输出不需要的信息。
- 我们还可能希望为图使用不同的输入/输出模式。例如,输出可能只包含一个相关的输出键。
PrivateState。
图也可以定义显式的输入和输出模式。在这些情况下,我们定义一个包含与图操作相关的所有键的“内部”模式。但是,我们也定义了 input 和 output 模式,这些模式是“内部”模式的子集,以约束图的输入和输出。有关更多详细信息,请参阅本指南。
让我们来看一个例子:
state 作为输入模式到 node1。但是,我们将输出写入到 foo,这是 OverallState 中的一个通道。我们如何将输出写入到不在输入模式中包含的状态通道呢?这是因为节点 可以写入图状态中的任何状态通道。图状态是初始化时定义的状态通道的并集,包括 OverallState 以及过滤器 InputState 和 OutputState。
- 我们使用
StateGraph({ state: OverallState, input: InputState, output: OutputState })初始化图。那么,我们如何在PrivateState中写入node2呢?如果它在StateGraph初始化时没有被传递,图是如何获取这个模式的呢?我们可以这样做,因为 节点也可以声明额外的状态通道,只要存在状态模式定义。在这种情况下,PrivateState模式已被定义,因此我们可以在图中添加bar作为新的状态通道并对其进行写入。
约简器
还原器是理解节点更新如何应用于State 的关键。每个在 State 中的键都有自己的独立还原函数。如果没有明确指定还原函数,则假定对该键的所有更新都应该覆盖它。存在几种不同的还原器类型,首先是默认的还原器类型:
默认Reducer
这两个示例展示了如何使用默认的reducer: 示例 A:{ foo: 1, bar: ["hi"] }. 然后假设第一个 Node 返回 { foo: 2 }。这被视为对状态的更新。请注意,Node 不需要返回整个 State 架构 - 只需一个更新。应用此更新后,State 将变为 { foo: 2, bar: ["hi"] }。如果第二个节点返回 { bar: ["bye"] },那么 State 将变为 { foo: 2, bar: ["bye"] }。
示例 B:
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 对象。这允许以以下格式发送图输入/状态更新:
MessagesZodMeta 时,状态更新始终反序列化为 LangChain Messages,因此您应该使用点符号来访问消息属性,例如 state.messages[state.messages.length - 1].content。以下是一个使用 MessagesZodMeta 的图例:
MessagesZodState 使用单个 messages 键定义,该键是一个包含 @[BaseMessage] 对象的列表,并使用适当的reducer。通常,需要跟踪的状态不仅仅是消息,因此我们看到人们扩展这个状态并添加更多字段,例如:
节点
在LangGraph中,节点通常是接受以下参数的函数(同步或异步):state:图的 状态config:一个RunnableConfig对象,其中包含如thread_id和如tags的配置信息
addNode 方法向图中添加节点。
START 节点
START 节点是一个特殊节点,表示将用户输入发送到图中的节点。引用此节点的主要目的是确定哪些节点应该首先调用。
END 节点
END 节点是一个特殊节点,表示一个终端节点。当您想要表示哪些边在执行完毕后没有动作时,会引用此节点。
节点缓存
LangGraph支持根据节点输入缓存任务/节点。要使用缓存:- 在编译图(或指定入口点)时指定缓存
- 为节点指定缓存策略。每个缓存策略支持:
keyFunc,用于根据节点的输入生成缓存键。ttl,缓存的有效期(以秒为单位)。如果未指定,缓存将永远不会过期。
边
边定义了逻辑的路径以及图如何决定停止。这是智能体工作方式以及不同节点之间如何相互通信的重要组成部分。存在几种关键的边类型:- 正常边:直接从一个节点跳转到下一个节点。
- 条件边:调用一个函数以确定下一个要跳转到的节点。
- 入口点:当用户输入到达时,首先调用哪个节点。
- 条件入口点:当用户输入到达时,调用一个函数以确定首先调用哪个节点。
正常边
如果您始终想从节点A到节点B,可以直接使用addEdge方法。
条件边
如果您想可选地路由到一个或多个边(或可选地终止),可以使用addConditionalEdges方法。此方法接受一个节点的名称以及在该节点执行后要调用的“路由函数”:
routingFunction 接受图中当前的 state 并返回一个值。
默认情况下,使用 routingFunction 返回值作为发送状态到下一个节点的节点(或节点列表)的名称。所有这些节点都将作为下一个超级步骤的一部分并行运行。
您可以可选地提供一个对象,将 routingFunction 的输出映射到下一个节点的名称。
入口点
入口点是图开始运行时首先运行的第一个(些)节点。您可以使用从虚拟START节点到第一个执行节点的addEdge方法来指定进入图的位置。
条件入口点
一个条件入口点允许您根据自定义逻辑从不同的节点开始。您可以使用来自虚拟START节点的addConditionalEdges来实现这一点。
routingFunction 的输出映射到下一个节点的名称。
Send
默认情况下,Nodes 和 Edges 在事先定义,并在相同的共享状态上运行。然而,可能存在某些情况下,确切的边在事先未知,或者您可能希望同时存在不同版本的 State。一个常见的例子是与 map-reduce 设计模式相关。在这个设计模式中,一个节点可能生成一个对象列表,您可能希望将其他节点应用于所有这些对象。对象的数量可能在事先未知(意味着边的数量可能未知),并且输入到下游 Node 的 State 应该不同(每个生成的对象一个)。
为了支持这种设计模式,LangGraph 支持从条件边返回 Send 对象。Send 接受两个参数:第一个是节点的名称,第二个是要传递给该节点的状态。
Command
可以结合控制流(边)和状态更新(节点)来使用,这可能会很有用。例如,你可能希望在同一个节点中同时执行状态更新并决定下一个要访问的节点。LangGraph通过从节点函数返回一个Command对象来实现这一点:
Command,您还可以实现动态控制流行为(与条件边相同):
Command时,您必须在添加节点时添加ends参数,以指定它可以路由到的节点:
当在您的节点函数中返回
Command 时,您必须添加带有节点路由到的节点名称列表的返回类型注解,例如 Command[Literal["my_other_node"]]。这对于图形渲染是必要的,并告知 LangGraph my_node 可以导航到 my_other_node。Command的端到端示例。
应该在什么情况下使用命令而不是条件边?
导航到父图中节点
如果您正在使用子图,您可能希望从一个子图中的节点导航到不同的子图(即在父图中的不同节点)。为此,您可以在Command中指定graph: Command.PARENT:
Command中指定graph: Command.PARENT:
在内部工具中使用
一个常见的用例是从工具内部更新图状态。例如,在一个客户支持应用程序中,您可能希望在对话开始时根据客户的账户号码或ID查找客户信息。 请参阅本指南获取详细信息。人工参与循环
Command 是人机交互工作流程的重要组成部分:当使用 interrupt() 收集用户输入时,随后使用 Command 提供输入并通过 new Command({ resume: "User input" }) 恢复执行。有关更多信息,请参阅 人机交互概念指南。
图迁移
LangGraph可以轻松处理图定义(节点、边和状态)的迁移,即使在使用检查点来跟踪状态的情况下也是如此。- 对于图末端的线程(即未中断的线程),您可以更改整个图的拓扑结构(即所有节点和边,删除、添加、重命名等)
- 对于当前中断的线程,我们支持所有拓扑更改,除了重命名/删除节点(因为该线程现在可能即将进入一个不再存在的节点)— 如果这是阻碍,请与我们联系,我们可以优先解决。
- 对于修改状态,我们在添加和删除键方面具有完全的前向和后向兼容性
- 重命名的状态键将失去现有线程中保存的状态
- 类型发生不兼容变化的州键可能会在更改之前具有状态线程中引起问题 — 如果这是阻碍,请与我们联系,我们可以优先解决。
运行时上下文
在创建图时,您可以指定一个用于传递给节点的contextSchema 运行时上下文。这对于传递不属于图状态的节点信息非常有用。例如,您可能希望传递模型名称或数据库连接等依赖项。
context 属性将此配置传递到图中。
递归限制
递归限制设置图在单次执行期间可以执行的超级步骤的最大数量。一旦达到限制,LangGraph将引发GraphRecursionError。默认情况下,此值设置为25步。递归限制可以在运行时设置在任何图上,并通过配置对象传递给invoke/stream。重要的是,recursionLimit是一个独立的config键,不应作为所有其他用户定义配置传递到configurable键中。以下是一个示例: