图
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 的输入架构,可以是 TypedDict 或 Pydantic 模型。所有 Nodes 都会向 State 发出更新,然后使用指定的 reducer 函数应用这些更新。
架构
图的主要文档化方式是通过使用TypedDict来指定其模式。如果您想在您的状态中提供默认值,请使用dataclass。我们还支持使用Pydantic BaseModel作为您的图状态,如果您想要递归数据验证(尽管请注意,Pydantic的性能不如TypedDict或dataclass)。
默认情况下,图将具有相同的输入和输出模式。如果您想更改此设置,也可以直接指定显式的输入和输出模式。这在您有很多键,其中一些明确用于输入,而另一些用于输出时非常有用。有关如何使用的说明,请参阅此处指南。
多个模式
通常,所有图节点都使用单个模式进行通信。这意味着它们将读取和写入相同的状态通道。但是,有些情况下我们希望对此有更多的控制:- 内部节点可以传递图中输入/输出不需要的信息。
- 我们还可能希望为图使用不同的输入/输出模式。例如,输出可能只包含一个相关的输出键。
PrivateState。
图也可以定义显式的输入和输出模式。在这些情况下,我们定义一个包含与图操作相关的所有键的“内部”模式。但是,我们也定义了 input 和 output 模式,这些模式是“内部”模式的子集,以约束图的输入和输出。有关更多详细信息,请参阅本指南。
让我们来看一个例子:
state: InputState 作为输入模式到 node_1。但是,我们将输出写入到 foo,这是 OverallState 中的一个通道。我们如何将输出写入到不在输入模式中包含的状态通道呢?这是因为节点 可以写入图状态中的任何状态通道。图状态是初始化时定义的状态通道的并集,包括 OverallState 以及过滤器 InputState 和 OutputState。
- 我们使用
StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)初始化图。那么,我们如何在PrivateState中写入node_2呢?如果它没有被传递到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:
Annotated 类型来指定第二个键的还原函数 (operator.add)。请注意,第一个键保持不变。假设图输入为 {"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 对象的列表至关重要(例如,当一个节点发送更新时)。如果您没有指定还原函数,每次状态更新都会用最近提供的值覆盖消息列表。如果您只想简单地将消息追加到现有列表中,可以使用 operator.add 作为还原函数。
然而,您可能还需要手动更新图状态中的消息(例如,人工干预)。如果您使用 operator.add,您发送给图的手动状态更新将被追加到现有的消息列表中,而不是更新现有消息。为了避免这种情况,您需要一个可以跟踪消息 ID 并覆盖现有消息的 reducer。为了实现这一点,您可以使用预构建的 add_messages 函数。对于全新的消息,它将简单地追加到现有列表中,但也会正确处理现有消息的更新。
序列化
除了跟踪消息ID之外,add_messages 函数还会在接收到 messages 通道上的状态更新时尝试将消息反序列化为 LangChain Message 对象。有关 LangChain 序列化/反序列化的更多信息,请参阅此处。这允许以以下格式发送图输入/状态更新:
add_messages 时,状态更新始终被反序列化为 LangChain Messages,因此您应该使用点符号来访问消息属性,例如 state["messages"][-1].content。以下是一个使用 add_messages 作为其还原函数的图的示例。
消息状态
由于在状态中拥有消息列表非常常见,因此存在一个预构建的状态,称为MessagesState,这使得使用消息变得容易。MessagesState 使用单个 messages 键定义,该键是一个 AnyMessage 对象的列表,并使用 add_messages 红ucer。通常,除了消息之外,还有更多需要跟踪的状态,因此我们看到人们继承这个状态并添加更多字段,例如:
节点
在LangGraph中,节点是接受以下参数的Python函数(同步或异步):state:图的状态config:一个包含如thread_id和如tags等配置信息以及跟踪信息的RunnableConfig对象runtime:一个包含运行时context和其他如store和stream_writer等信息的一个Runtime对象
NetworkX 类似,您可以使用 add_node 方法将这些节点添加到图中:
START 节点
START 节点是一个特殊节点,表示将用户输入发送到图中的节点。引用此节点的主要目的是确定哪些节点应该首先调用。
END 节点
END 节点是一个特殊节点,表示一个终端节点。当您想要表示哪些边在执行完毕后没有动作时,会引用此节点。
节点缓存
LangGraph支持根据节点输入缓存任务/节点。要使用缓存:- 在编译图(或指定入口点)时指定缓存
- 为节点指定缓存策略。每个缓存策略支持:
key_func,用于根据节点的输入生成缓存键,默认为输入的hash(使用pickle序列化)ttl,缓存在秒中的存活时间。如果没有指定,缓存将永远不会过期。
- 首次运行需要两秒钟(由于模拟了昂贵的计算)。
- 第二次运行利用缓存并快速返回。
边
边定义了逻辑的路径以及图如何决定停止。这是智能体工作方式以及不同节点之间如何相互通信的重要组成部分。存在几种关键的边类型:- 正常边:直接从一个节点跳转到下一个节点。
- 条件边:调用一个函数以确定下一个要跳转到的节点。
- 入口点:当用户输入到达时,首先调用哪个节点。
- 条件入口点:当用户输入到达时,调用一个函数以确定首先调用哪个节点。
正常边
如果您总是想从节点A到节点B进行转换,可以直接使用添加边方法。条件边
如果您想可选地路由到一个或多个边(或可选地终止),可以使用add_conditional_edges方法。此方法接受一个节点的名称以及在该节点执行后要调用的“路由函数”:routing_function 接受图中当前的 state 并返回一个值。
默认情况下,使用 routing_function 返回值作为发送状态到下一个节点的节点(或节点列表)的名称。所有这些节点都将作为下一个超级步骤的一部分并行运行。
您可以可选地提供一个将 routing_function 的输出映射到下一个节点名称的字典。
入口点
入口点是图开始运行时首先运行的第一个(些)节点。您可以使用从虚拟START节点到第一个执行节点的add_edge方法来指定进入图的位置。
条件入口点
一个条件入口点允许您根据自定义逻辑从不同的节点开始。您可以使用来自虚拟START节点的add_conditional_edges来完成此操作。
routing_function 的输出映射到下一个节点名称的字典。
Send
默认情况下,Nodes 和 Edges 在事先定义,并在相同的共享状态下运行。然而,可能存在某些情况下,确切的边在事先未知,或者您可能希望同时存在不同版本的 State。一个常见的例子是与 map-reduce 设计模式。在这个设计模式中,一个节点可能生成一个对象列表,您可能希望将其他节点应用于所有这些对象。对象的数量可能在事先未知(意味着边的数量可能未知),并且输入到下游 Node 的 State 应该不同(每个生成的对象一个)。
为了支持这种设计模式,LangGraph 支持从条件边返回 Send 对象。Send 接受两个参数:第一个是节点的名称,第二个是要传递给该节点的状态。
Command
可以结合控制流(边)和状态更新(节点)来使用,这可能会很有用。例如,你可能希望在同一个节点中同时执行状态更新并决定下一个要访问的节点。LangGraph通过从节点函数返回一个Command对象来实现这一点:
Command,您还可以实现动态控制流行为(与 条件边 相同):
当在您的节点函数中返回
Command 时,您必须添加带有节点路由到的节点名称列表的返回类型注解,例如 Command[Literal["my_other_node"]]。这对于图形渲染是必要的,并告知 LangGraph my_node 可以导航到 my_other_node。Command。
应该在什么情况下使用命令而不是条件边?
- 当您需要同时更新图状态并路由到不同的节点时,请使用
Command。例如,在实现多智能体交接时,此时路由到不同的智能体并传递一些信息给该智能体非常重要。 - 使用条件边在节点之间有条件地路由,而不更新状态。
导航到父图中节点
如果您正在使用子图,您可能希望从一个子图中的节点导航到不同的子图(即父图中的不同节点)。为此,您可以在Command中指定 graph=Command.PARENT:
这在实现多智能体交接时尤其有用。
查看此指南获取详细信息。
在内部工具中使用
一个常见的用例是从工具内部更新图状态。例如,在一个客户支持应用程序中,您可能希望在对话开始时根据客户的账户号码或ID查找客户信息。 请参阅本指南获取详细信息。人工增强循环
Command 是人机交互工作流程的重要组成部分:当使用 interrupt() 收集用户输入时,随后使用 Command 提供输入并通过 Command(resume="User input") 恢复执行。查看 此概念指南 获取更多信息。
图迁移
LangGraph可以轻松处理图定义(节点、边和状态)的迁移,即使在使用检查点来跟踪状态的情况下也是如此。- 对于图末端的线程(即未中断的线程),您可以更改整个图的拓扑结构(即所有节点和边,删除、添加、重命名等)
- 对于当前中断的线程,我们支持所有拓扑更改,除了重命名/删除节点(因为该线程现在可能即将进入一个不再存在的节点)— 如果这是阻碍,请与我们联系,我们可以优先解决此问题。
- 对于修改状态,我们在添加和删除键方面具有完全的前向和后向兼容性
- 重命名的状态键将失去现有线程中保存的状态
- 类型发生不兼容更改的状态键可能会在更改之前具有状态线程中引起问题 — 如果这是阻碍,请与我们联系,我们可以优先解决此问题。
运行时上下文
在创建图时,您可以指定一个context_schema 用于传递给节点的运行时上下文。这对于传递不属于图状态的节点信息非常有用。例如,您可能希望传递模型名称或数据库连接等依赖项。
invoke 方法的 context 参数。
递归限制
递归限制设置图在单次执行期间可以执行的最大超级步骤。一旦达到限制,LangGraph将引发GraphRecursionError。默认情况下,此值设置为25步。递归限制可以在运行时设置在任何图上,并通过配置字典传递给invoke/stream。重要的是,recursion_limit是一个独立的config键,不应作为所有其他用户定义配置传递到configurable键中。以下是一个示例: