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方法即可编译您的图:
graph = graph_builder.compile(...)
必须在可以使用之前编译您的图。

状态

当您定义一个图时,首先要定义图的 StateState 包括 图的架构 以及 reducer 函数,这些函数指定了如何应用更新到状态。State 的架构将是图中所有 NodesEdges 的输入架构,可以是 TypedDictPydantic 模型。所有 Nodes 都会向 State 发出更新,然后使用指定的 reducer 函数应用这些更新。

架构

图的主要文档化方式是通过使用TypedDict来指定其模式。如果您想在您的状态中提供默认值,请使用dataclass。我们还支持使用Pydantic BaseModel作为您的图状态,如果您想要递归数据验证(尽管请注意,Pydantic的性能不如TypedDictdataclass)。 默认情况下,图将具有相同的输入和输出模式。如果您想更改此设置,也可以直接指定显式的输入和输出模式。这在您有很多键,其中一些明确用于输入,而另一些用于输出时非常有用。有关如何使用的说明,请参阅此处指南

多个模式

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

class OutputState(TypedDict):
    graph_output: str

class OverallState(TypedDict):
    foo: str
    user_input: str
    graph_output: str

class PrivateState(TypedDict):
    bar: str

def node_1(state: InputState) -> OverallState:
    # Write to OverallState
    return {"foo": state["user_input"] + " name"}

def node_2(state: OverallState) -> PrivateState:
    # Read from OverallState, write to PrivateState
    return {"bar": state["foo"] + " is"}

def node_3(state: PrivateState) -> OutputState:
    # Read from PrivateState, write to OutputState
    return {"graph_output": state["bar"] + " Lance"}

builder = StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", "node_3")
builder.add_edge("node_3", END)

graph = builder.compile()
graph.invoke({"user_input":"My"})
# {'graph_output': 'My name is Lance'}
这里有两个微妙且重要的要点需要注意: 我们传入 state: InputState 作为输入模式到 node_1。但是,我们将输出写入到 foo,这是 OverallState 中的一个通道。我们如何将输出写入到不在输入模式中包含的状态通道呢?这是因为节点 可以写入图状态中的任何状态通道。图状态是初始化时定义的状态通道的并集,包括 OverallState 以及过滤器 InputStateOutputState
  1. 我们使用 StateGraph(OverallState,input_schema=InputState,output_schema=OutputState) 初始化图。那么,我们如何在 PrivateState 中写入 node_2 呢?如果它没有被传递到 StateGraph 初始化中,图是如何获取这个模式的呢?我们可以这样做,因为 节点也可以声明额外的状态通道,只要存在状态模式定义。在这种情况下,PrivateState 模式已定义,因此我们可以在图中添加 bar 作为新的状态通道并写入它。

约简器

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

默认Reducer

这两个示例展示了如何使用默认的reducer: 示例 A:
from typing_extensions import TypedDict

class State(TypedDict):
    foo: int
    bar: list[str]
在这个示例中,没有为任何键指定reducer函数。让我们假设图输入为: {"foo": 1, "bar": ["hi"]}. 然后假设第一个 Node 返回 {"foo": 2}。这被视为对状态的更新。请注意,Node 不需要返回整个 State 架构 - 只需一个更新。应用此更新后,State 将变为 {"foo": 2, "bar": ["hi"]}。如果第二个节点返回 {"bar": ["bye"]},那么 State 将变为 {"foo": 2, "bar": ["bye"]} 示例 B:
from typing import Annotated
from typing_extensions import TypedDict
from operator import add

class State(TypedDict):
    foo: int
    bar: Annotated[list[str], add]
在这个示例中,我们使用了 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 序列化/反序列化的更多信息,请参阅此处。这允许以以下格式发送图输入/状态更新:
# this is supported
{"messages": [HumanMessage(content="message")]}

# and this is also supported
{"messages": [{"type": "human", "content": "message"}]}
由于在使用 add_messages 时,状态更新始终被反序列化为 LangChain Messages,因此您应该使用点符号来访问消息属性,例如 state["messages"][-1].content。以下是一个使用 add_messages 作为其还原函数的图的示例。
from langchain.messages import AnyMessage
from langgraph.graph.message import add_messages
from typing import Annotated
from typing_extensions import TypedDict

class GraphState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

消息状态

由于在状态中拥有消息列表非常常见,因此存在一个预构建的状态,称为 MessagesState,这使得使用消息变得容易。MessagesState 使用单个 messages 键定义,该键是一个 AnyMessage 对象的列表,并使用 add_messages 红ucer。通常,除了消息之外,还有更多需要跟踪的状态,因此我们看到人们继承这个状态并添加更多字段,例如:
from langgraph.graph import MessagesState

class State(MessagesState):
    documents: list[str]

节点

在LangGraph中,节点是接受以下参数的Python函数(同步或异步):
  1. state:图的状态
  2. config:一个包含如thread_id和如tags等配置信息以及跟踪信息的RunnableConfig对象
  3. runtime:一个包含运行时 context和其他如storestream_writer等信息的一个Runtime对象
NetworkX 类似,您可以使用 add_node 方法将这些节点添加到图中:
from dataclasses import dataclass
from typing_extensions import TypedDict

from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph
from langgraph.runtime import Runtime

class State(TypedDict):
    input: str
    results: str

@dataclass
class Context:
    user_id: str

builder = StateGraph(State)

def plain_node(state: State):
    return state

def node_with_runtime(state: State, runtime: Runtime[Context]):
    print("In node: ", runtime.context.user_id)
    return {"results": f"Hello, {state['input']}!"}

def node_with_config(state: State, config: RunnableConfig):
    print("In node with thread_id: ", config["configurable"]["thread_id"])
    return {"results": f"Hello, {state['input']}!"}


builder.add_node("plain_node", plain_node)
builder.add_node("node_with_runtime", node_with_runtime)
builder.add_node("node_with_config", node_with_config)
...
幕后,函数被转换为 RunnableLambda,这为您的函数添加了批处理和异步支持,同时还提供了原生的跟踪和调试功能。 如果您在图中添加一个节点而没有指定名称,它将被赋予一个默认名称,该名称与函数名称等效。
builder.add_node(my_node)
# You can then create edges to/from this node by referencing it as `"my_node"`

START 节点

START 节点是一个特殊节点,表示将用户输入发送到图中的节点。引用此节点的主要目的是确定哪些节点应该首先调用。
from langgraph.graph import START

graph.add_edge(START, "node_a")

END 节点

END 节点是一个特殊节点,表示一个终端节点。当您想要表示哪些边在执行完毕后没有动作时,会引用此节点。
from langgraph.graph import END

graph.add_edge("node_a", END)

节点缓存

LangGraph支持根据节点输入缓存任务/节点。要使用缓存:
  • 在编译图(或指定入口点)时指定缓存
  • 为节点指定缓存策略。每个缓存策略支持:
    • key_func,用于根据节点的输入生成缓存键,默认为输入的 hash(使用pickle序列化)
    • ttl,缓存在秒中的存活时间。如果没有指定,缓存将永远不会过期。
例如:
import time
from typing_extensions import TypedDict
from langgraph.graph import StateGraph
from langgraph.cache.memory import InMemoryCache
from langgraph.types import CachePolicy


class State(TypedDict):
    x: int
    result: int


builder = StateGraph(State)


def expensive_node(state: State) -> dict[str, int]:
    # expensive computation
    time.sleep(2)
    return {"result": state["x"] * 2}


builder.add_node("expensive_node", expensive_node, cache_policy=CachePolicy(ttl=3))
builder.set_entry_point("expensive_node")
builder.set_finish_point("expensive_node")

graph = builder.compile(cache=InMemoryCache())

print(graph.invoke({"x": 5}, stream_mode='updates'))    
# [{'expensive_node': {'result': 10}}]
print(graph.invoke({"x": 5}, stream_mode='updates'))    
# [{'expensive_node': {'result': 10}, '__metadata__': {'cached': True}}]
  1. 首次运行需要两秒钟(由于模拟了昂贵的计算)。
  2. 第二次运行利用缓存并快速返回。

边定义了逻辑的路径以及图如何决定停止。这是智能体工作方式以及不同节点之间如何相互通信的重要组成部分。存在几种关键的边类型:
  • 正常边:直接从一个节点跳转到下一个节点。
  • 条件边:调用一个函数以确定下一个要跳转到的节点。
  • 入口点:当用户输入到达时,首先调用哪个节点。
  • 条件入口点:当用户输入到达时,调用一个函数以确定首先调用哪个节点。
一个节点可以有多个出边。如果一个节点有多个出边,所有这些目标节点将作为下一个超级步骤的一部分并行执行。

正常边

如果您总是想从节点A到节点B进行转换,可以直接使用添加边方法。
graph.add_edge("node_a", "node_b")

条件边

如果您想可选地路由到一个或多个边(或可选地终止),可以使用add_conditional_edges方法。此方法接受一个节点的名称以及在该节点执行后要调用的“路由函数”:
graph.add_conditional_edges("node_a", routing_function)
与节点类似,routing_function 接受图中当前的 state 并返回一个值。 默认情况下,使用 routing_function 返回值作为发送状态到下一个节点的节点(或节点列表)的名称。所有这些节点都将作为下一个超级步骤的一部分并行运行。 您可以可选地提供一个将 routing_function 的输出映射到下一个节点名称的字典。
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
如果您想在单个函数中结合状态更新和路由,请使用Command代替条件边。

入口点

入口点是图开始运行时首先运行的第一个(些)节点。您可以使用从虚拟START节点到第一个执行节点的add_edge方法来指定进入图的位置。
from langgraph.graph import START

graph.add_edge(START, "node_a")

条件入口点

一个条件入口点允许您根据自定义逻辑从不同的节点开始。您可以使用来自虚拟START节点的add_conditional_edges来完成此操作。
from langgraph.graph import START

graph.add_conditional_edges(START, routing_function)
您可以可选地提供一个将 routing_function 的输出映射到下一个节点名称的字典。
graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "node_c"})

Send

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

graph.add_conditional_edges("node_a", continue_to_jokes)

Command

可以结合控制流(边)和状态更新(节点)来使用,这可能会很有用。例如,你可能希望在同一个节点中同时执行状态更新并决定下一个要访问的节点。LangGraph通过从节点函数返回一个Command对象来实现这一点:
def my_node(state: State) -> Command[Literal["my_other_node"]]:
    return Command(
        # state update
        update={"foo": "bar"},
        # control flow
        goto="my_other_node"
    )
使用 Command,您还可以实现动态控制流行为(与 条件边 相同):
def my_node(state: State) -> Command[Literal["my_other_node"]]:
    if state["foo"] == "bar":
        return Command(update={"foo": "baz"}, goto="my_other_node")
当在您的节点函数中返回 Command 时,您必须添加带有节点路由到的节点名称列表的返回类型注解,例如 Command[Literal["my_other_node"]]。这对于图形渲染是必要的,并告知 LangGraph my_node 可以导航到 my_other_node
查看这个使用指南,了解如何端到端地使用Command

应该在什么情况下使用命令而不是条件边?

  • 当您需要同时更新图状态并路由到不同的节点时,请使用Command。例如,在实现多智能体交接时,此时路由到不同的智能体并传递一些信息给该智能体非常重要。
  • 使用条件边在节点之间有条件地路由,而不更新状态。

导航到父图中节点

如果您正在使用子图,您可能希望从一个子图中的节点导航到不同的子图(即父图中的不同节点)。为此,您可以在Command中指定 graph=Command.PARENT
def my_node(state: State) -> Command[Literal["other_subgraph"]]:
    return Command(
        update={"foo": "bar"},
        goto="other_subgraph",  # where `other_subgraph` is a node in the parent graph
        graph=Command.PARENT
    )
graph 设置为 Command.PARENT 将导航到最近的父图。当您从子图节点向父图节点发送由父图和子图状态模式共享的键的更新时,您必须在父图状态中为要更新的键定义一个reducer。请参阅此示例
这在实现多智能体交接时尤其有用。 查看此指南获取详细信息。

在内部工具中使用

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

人工增强循环

Command 是人机交互工作流程的重要组成部分:当使用 interrupt() 收集用户输入时,随后使用 Command 提供输入并通过 Command(resume="User input") 恢复执行。查看 此概念指南 获取更多信息。

图迁移

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

运行时上下文

在创建图时,您可以指定一个 context_schema 用于传递给节点的运行时上下文。这对于传递不属于图状态的节点信息非常有用。例如,您可能希望传递模型名称或数据库连接等依赖项。
@dataclass
class ContextSchema:
    llm_provider: str = "openai"

graph = StateGraph(State, context_schema=ContextSchema)
您可以将此上下文传递给图,使用 invoke 方法的 context 参数。
graph.invoke(inputs, context={"llm_provider": "anthropic"})
您可以在节点或条件边内部访问和使用此上下文:
from langgraph.runtime import Runtime

def node_a(state: State, runtime: Runtime[ContextSchema]):
    llm = get_llm(runtime.context.llm_provider)
    # ...
查看本指南以获取完整的配置说明。

递归限制

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

可视化

通常能够可视化图形是非常方便的,尤其是当图形变得更加复杂时。LangGraph提供了几种内置的图形可视化方式。有关更多信息,请参阅本操作指南