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

StateGraph

StateGraph 类是使用的主要图类。它由用户定义的 State 对象参数化。

编译你的图

要构建你的图,你首先定义状态,然后添加节点,接着再编译它。编译你的图到底意味着什么,为什么需要这样做? 编译是一个相当简单的步骤。它会对你的图结构进行一些基本的检查(例如没有孤立节点等)。在这里你也可以指定运行时参数,如检查点断点。你只需调用 .compile 方法来编译你的图:
graph = graph_builder.compile(...)
必须先编译图,然后才能使用它。

状态

当定义图时,你首先要做的是定义图的 StateState图的模式reducer 函数组成,后者指定了如何对状态应用更新。State 的模式将是图中所有 NodesEdges 的输入模式,并且可以是 TypedDictPydantic 模型。所有 Nodes 都将发出对 State 的更新,这些更新随后会使用指定的 reducer 函数进行应用。

模式

指定图模式的主要文档化方法是使用 TypedDict。如果你想在状态中提供默认值,可以使用 dataclass。如果你需要对图状态进行递归数据验证,我们也支持使用 Pydantic BaseModel(但请注意 Pydantic 的性能不如 TypedDictdataclass)。 默认情况下,图将具有相同的输入和输出模式。如果你想改变这一点,也可以直接指定显式的输入和输出模式。当你有很多键,并且其中一些明确用于输入、另一些用于输出时,这很有用。更多信息请参阅指南
langchain 中更高级别的 create_agent 工厂不支持 Pydantic 状态模式。

多种模式

通常,所有图节点都使用单一模式进行通信。这意味着它们将读写到相同的状态通道。但是,在某些情况下,我们希望对此有更多控制:
  • 内部节点可以传递图输入/输出中不需要的信息。
  • 我们可能还想为图使用不同的输入/输出模式。例如,输出可能只包含单个相关的输出键。
可以让节点在图中写入私有状态通道,用于内部节点通信。我们可以简单地定义一个私有模式 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:
    # 写入 OverallState
    return {"foo": state["user_input"] + " name"}

def node_2(state: OverallState) -> PrivateState:
    # 从 OverallState 读取,写入 PrivateState
    return {"bar": state["foo"] + " is"}

def node_3(state: PrivateState) -> OutputState:
    # 从 PrivateState 读取,写入 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'}
这里有两个微妙而重要的点需要注意:
  1. 我们将 state: InputState 作为输入模式传递给 node_1。但我们写入了 foo,这是 OverallState 中的一个通道。我们如何能写入一个未包含在输入模式中的状态通道?这是因为一个节点可以写入图状态中的任何状态通道。图状态是初始化时定义的状态通道的并集,这包括了 OverallState 以及过滤器 InputStateOutputState
  2. 我们用以下方式初始化图:
    StateGraph(
        OverallState,
        input_schema=InputState,
        output_schema=OutputState
    )
    
    我们如何在 node_2 中写入 PrivateState?如果 StateGraph 初始化时没有传入这个模式,图又是如何获得对它的访问权限的? 我们能够这样做,是因为节点只要存在状态模式定义,就可以声明额外的状态通道。在这个例子中,由于 PrivateState 模式已定义,我们可以在图中添加 bar 作为新的状态通道并向其写入。
私有通道在流式传输时不会被编辑(redacted)。输入、输出和私有模式限制了每个节点读取的内容(其输入模式)以及 invoke 返回的内容(输出模式)。它们不会stream 隐藏通道。当你使用 stream_mode="values" 进行流式传输时,图默认会发出所有状态通道——包括私有通道——因为 values 流默认使用完整的通道集合,而不是输出模式。这就是为什么像 bar 这样的私有通道会被 invoke 隐藏,但在流式传输时可见:
for chunk in graph.stream({"user_input": "My"}, stream_mode="values"):
    print(chunk)
# {'user_input': 'My'}
# {'user_input': 'My', 'foo': 'My name'}
# {'user_input': 'My', 'foo': 'My name', 'bar': 'My name is'}        # <-- 私有通道
# {'user_input': 'My', 'foo': 'My name', 'bar': 'My name is', 'graph_output': 'My name is Lance'}
要将流式传输的值限制到特定的通道集合(例如,仅输出模式),可以传入 output_keys
for chunk in graph.stream(
    {"user_input": "My"},
    stream_mode="values",
    output_keys=["graph_output"],
):
    print(chunk)
# {'graph_output': 'My name is Lance'}
如果你只需要每个步骤中节点实际生成的通道(而不是完整的累积状态),可以改用 stream_mode="updates"

Reducers

Reducers 是理解节点更新如何应用到 State 的关键。State 中的每个键都有其独立的 reducer 函数。如果没有明确指定 reducer 函数,则假定对该键的所有更新都应覆盖其原有值。有几种不同类型的 reducer,从默认类型的 reducer 开始:

默认 reducer

这两个例子展示了如何使用默认 reducer:
Example 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"]}
Example 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 类型为第二个键(bar)指定了一个 reducer 函数(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 键是通过将两个列表相加来更新的。

Overwrite

在某些情况下,你可能想绕过 reducer 直接覆盖状态值。LangGraph 为此提供了 Overwrite 类型。在此了解如何使用 Overwrite

在图状态中处理消息

为什么要使用消息?

大多数现代 LLM 提供商都有一个接受消息列表作为输入的聊天模型接口。特别是 LangChain 的 聊天模型接口 接受一个消息对象列表作为输入。这些消息有多种形式,例如 HumanMessage(用户输入)或 AIMessage(LLM 响应)。 要阅读更多关于消息对象是什么的内容,请参考 消息概念指南

在你的图中使用消息

在许多情况下,将先前的对话历史作为消息列表存储在图状态中会很有帮助。为此,我们可以在图状态中添加一个键(通道),用于存储 Message 对象的列表,并用一个 reducer 函数(参见下面示例中的 messages 键)对其进行注解。这个 reducer 函数对于告诉图如何通过每次状态更新(例如,当节点发送更新时)来更新状态中的 Message 对象列表至关重要。如果你不指定 reducer,每次状态更新都将用最新提供的值覆盖整个消息列表。如果你只想将消息追加到现有列表中,可以使用 operator.add 作为 reducer。 但是,你可能还想手动更新图状态中的消息(例如,人机协同)。如果你使用 operator.add,你发送给图的手动状态更新将被追加到现有消息列表中,而不是更新现有的消息。为了避免这种情况,你需要一个能够跟踪消息 ID 并在更新时覆盖现有消息的 reducer。为了实现这一点,你可以使用预构建的 add_messages 函数。对于全新的消息,它会直接追加到现有列表中,同时它也会正确处理现有消息的更新。

序列化

除了跟踪消息 ID 外,每当 messages 通道上接收到状态更新时,add_messages 函数还会尝试将消息反序列化为 LangChain Message 对象。 更多信息请参见LangChain 序列化/反序列化。这允许以以下格式发送图输入/状态更新:
# 这是支持的
{"messages": [HumanMessage(content="message")]}

# 这也是支持的
{"messages": [{"type": "human", "content": "message"}]}
由于在使用 add_messages 时,状态更新总是被反序列化为 LangChain Messages,你应该使用点号表示法来访问消息属性,例如 state["messages"][-1].content 下面是一个使用 add_messages 作为其 reducer 函数的图示例。
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 的预构建状态,它使消息的使用变得容易。MessagesState 定义了一个单一的 messages 键,该键是 AnyMessage 对象的列表,并使用 add_messages reducer。通常,除了消息之外还有更多状态需要跟踪,所以我们看到人们会子类化这个状态并添加更多字段,例如:
from langgraph.graph import MessagesState

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

节点

在 LangGraph 中,节点是 Python 函数(同步或异步),它们接受以下参数:
  1. state—图的状态
  2. config—一个 RunnableConfig 对象,包含像 thread_id 这样的配置信息以及像 tags 这样的追踪信息
  3. runtime—一个 Runtime 对象,包含运行时 context和其他信息,如 storestream_writerexecution_infoserver_infoheartbeat(用于空闲超时刷新)和 control(用于优雅关闭
NetworkX 类似,你可以使用 add_node 方法将这些节点添加到图中:
from dataclasses import dataclass
from typing_extensions import TypedDict

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_execution_info(state: State, runtime: Runtime):
    print("In node with thread_id: ", runtime.execution_info.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_execution_info", node_with_execution_info)
...
在幕后,函数被转换成 RunnableLambda,这为你的函数增加了批处理和异步支持,以及原生的追踪和调试功能。 如果你在添加节点到图时没有指定名字,它将被赋予一个等价于函数名的默认名字。
builder.add_node(my_node)
# 之后你可以通过引用 `"my_node"` 来创建到/从这个节点的边

重新执行和幂等性

当你使用检查点编译时,LangGraph 会在超级步骤边界保存检查点,而不是在节点内部的函数中间。如果执行暂停并在之后恢复(例如,在中断重试之后),受影响的节点会从其函数的开头重新运行。暂停前的代码和副作用都会再次运行。 幂等性。 设计节点逻辑时要确保重新执行不会破坏状态。如果一个节点插入了一行数据库记录,那么运行两次不应该创建重复的行,除非这是有意为之。使用幂等键、upsert 或写前读检查。对于 interrupt() 周围的副作用,请参阅 interrupt 之前调用的副作用必须是幂等的 图变更。 关于代码更改的确定性规则不适用于图结构。你可以在不破坏现有线程恢复能力的情况下添加或移除节点和边。恢复的运行使用已保存的状态,并执行你现在编译的任何图。 节点内的任务和中断。 如果一个节点调用了任务interrupt,那么在恢复时适用更严格的确定性规则。LangGraph 从检查点恢复已完成的任务结果,但如果在恢复点之前更改代码中的任务interrupt 顺序,可能会导致缓存值不匹配。功能化 API入口点会编译成一个单独的节点,该节点以这种方式运行整个入口点方法。请参阅确定性幂等性在节点中使用任务

在节点中使用任务

如果一个节点包含多个操作,你可能会发现将每个操作实现为一个任务比将逻辑分散到多个节点更容易。当图使用检查点时,任务结果会被设置检查点,因此恢复一个线程可以跳过节点内已完成的任务工作。
from typing import NotRequired

import requests
from langchain_core.utils.uuid import uuid7
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict


class State(TypedDict):
    url: str
    result: NotRequired[str]


def call_api(state: State):
    """Example node that makes an API request."""
    result = requests.get(state["url"]).text[:100]
    return {"result": result}


builder = StateGraph(State)
builder.add_node("call_api", call_api)
builder.add_edge(START, "call_api")
builder.add_edge("call_api", END)

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

thread_id = str(uuid7())
config = {"configurable": {"thread_id": thread_id}}

graph.invoke({"url": "https://www.example.com"}, config)

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,用于根据节点输入生成缓存键,默认使用 pickle 对输入进行 hash
    • 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]:
    # 昂贵的计算
    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}}]
set_entry_point(node) 定义了图将执行的第一个节点。 它等价于 builder.add_edge(START, node)set_finish_point(node) 定义了图中的最后一个节点。 它等价于 builder.add_edge(node, END)两种方法都有效,但 add_edge(START, ...)add_edge(..., END) 是推荐的现代语法。
  1. 第一次运行代码时,由于模拟了昂贵的计算,运行需要两秒钟。
  2. 第二次运行利用了缓存,并很快返回。

边定义了逻辑如何路由以及图如何决定停止。这是你的智能体如何工作以及不同节点如何相互通信的重要组成部分。有几种关键类型的边:
  • 普通边:直接从一个节点转到下一个节点。
  • 条件边:调用一个函数来确定接下来去哪个(些)节点。
  • 入口点:用户输入到达时首先调用哪个节点。
  • 条件入口点:调用一个函数来确定用户输入到达时首先调用哪个(些)节点。
一个节点可以有多个传出边。如果一个节点有多个传出边,所有那些目标节点将作为下一个超级步骤的一部分并行执行。
对于每个节点,只选择一种路由机制:使用普通边进行静态路由,或使用条件边 / Command 进行动态路由。不要从同一个节点混合使用普通边和动态路由,因为两种路径都可能执行,会使图的行为更难推理。

普通边

如果你总是想从节点 A 转到节点 B,可以直接使用 add_edge 方法。
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 而不是条件边。

入口点

入口点是在图启动时运行的第一个节点。你可以使用 add_edge 方法,从虚拟的 START 节点连接到要执行的第一个节点,以指定从何处进入图。
from langgraph.graph import START

graph.add_edge(START, "node_a")

条件入口点

条件入口点允许你根据自定义逻辑从不同的节点开始。你可以使用 add_conditional_edges 从虚拟的 START 节点来实现这一点。
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 设计模式。在这种设计模式中,第一个节点可能会生成一个对象列表,你可能想对所有这些对象应用一些其他节点。对象的数量可能事先未知(意味着边的数量可能未知),并且传递给下游 Node 的输入 State 应该是不同的(每个生成的对象一个)。 为了支持这种设计模式,LangGraph 支持从条件边返回 Send 对象。Send 接受两个参数:第一个是节点名称,第二个是传递给该节点的状态。
from langgraph.types import 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

Command 是一个用于控制图执行的多功能原语。它接受四个参数:
  • update:应用状态更新(类似于从节点返回更新)。
  • goto:导航到特定节点(类似于条件边)。
  • graph:当从子图导航时,指定目标父图。
  • resume:提供值以在中断后恢复执行。
Command 在三种上下文中使用:

从节点返回

updategoto

从节点函数返回 Command 以在单个步骤中更新状态并路由到下一个节点:
def my_node(state: State) -> Command[Literal["my_other_node"]]:
    return Command(
        # 状态更新
        update={"foo": "bar"},
        # 控制流
        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 时,你必须添加返回类型注解,指明该节点路由到的节点名称列表,例如 Command[Literal["my_other_node"]]。这对于图渲染是必要的,它告诉 LangGraph my_node 可以导航到 my_other_node
Command 仅添加动态边——用 add_edge / addEdge 定义的静态边仍然会执行。例如,如果 node_a 返回 Command(goto="my_other_node"),而你还有 graph.add_edge("node_a", "node_b"),那么 node_bmy_other_node 都会运行。对于每个节点,使用 Command 或静态边来路由到下一个节点,不要两者都用。
查看此操作方法指南,了解如何端到端使用 Command 的示例。

graph

如果你正在使用子图,可以通过在 Command 中指定 graph=Command.PARENT,从子图内的节点导航到父图中的不同节点:
def my_node(state: State) -> Command[Literal["other_subgraph"]]:
    return Command(
        update={"foo": "bar"},
        goto="other_subgraph",  # 其中 `other_subgraph` 是父图中的一个节点
        graph=Command.PARENT
    )
graph 设置为 Command.PARENT 将导航到最近的父图。当你从子图节点向父图节点发送更新,且更新的键是父图和子图状态模式共享的键时,你必须在父图状态中为你正在更新的键定义一个 reducer。参见此示例
这在实现多智能体交接时特别有用。详情请参阅导航到父图中的节点

作为 invokestream 的输入

Command(resume=...)唯一旨在作为 invoke()/stream() 输入的 Command 模式。不要使用 Command(update=...) 作为输入来继续多轮对话——因为传入任何 Command 作为输入都会从最新的检查点恢复(即最后运行的步骤,而不是 __start__),如果图已经完成,它会看起来卡住了。要在线程上继续对话,传入一个普通的输入字典:
# 错误 - 图从最新检查点恢复
# (最后运行的步骤),看起来卡住了
graph.invoke(Command(update={
    "messages": [{"role": "user", "content": "follow up"}]
}), config)

# 正确 - 普通字典从 __start__ 重新开始
graph.invoke( {
    "messages": [{"role": "user", "content": "follow up"}]
}, config)

resume

使用 Command(resume=...) 来提供一个值,并在中断后恢复图执行。传递给 resume 的值会成为暂停节点内 interrupt() 调用的返回值:
from typing import TypedDict

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt


class State(TypedDict):
    messages: list[dict]


def human_review(state: State):
    # Pauses the graph and waits for a value
    answer = interrupt("Do you approve?")
    return {"messages": [{"role": "user", "content": answer}]}


graph = (
    StateGraph(State)
    .add_node("human_review", human_review)
    .add_edge(START, "human_review")
    .add_edge("human_review", END)
    .compile(checkpointer=InMemorySaver())
)

config = {"configurable": {"thread_id": "graph-api-resume"}}

# First run - hits the interrupt and pauses
stream = graph.stream_events({"messages": []}, config, version="v3")
_ = stream.output  # drive the stream to completion
print(stream.interrupts)

# Resume with a value - the interrupt() call returns "yes"
resumed = graph.stream_events(Command(resume="yes"), config, version="v3")
final = resumed.output
查看中断概念指南以了解有关中断模式的完整细节,包括多重中断和验证循环。

从工具返回

你可以从工具返回 Command 以更新图状态和控制流。使用 update 修改状态(例如,保存在对话期间查找到的客户信息),使用 goto 在工具完成后路由到特定节点。
在工具内部使用时,goto 会添加一条动态边——在调用该工具的节点上已定义的任何静态边仍然会执行。对于每个节点,使用工具驱动的动态路由或静态边来路由到下一个节点,不要两者都用。
请参考在工具内部使用以了解详情。

图迁移

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。从 1.0.6 版本开始,默认递归限制设置为 1000 步。递归限制可以在任何图上于运行时设置,并通过配置字典传递给 invoke/stream。重要的是,recursion_limit 是一个独立的 config 键,不应像所有其他用户定义的配置那样放在 configurable 键内。参见下面的示例:
graph.invoke(inputs, config={"recursion_limit": 5}, context={"llm": "anthropic"})
阅读递归限制以了解有关递归限制如何工作的更多信息。

访问和处理递归计数器

当前的步骤计数器可在任何节点的 config["metadata"]["langgraph_step"] 中访问,允许在达到递归限制之前进行主动的递归处理。这使你能够在图逻辑中实现优雅的降级策略。

它是如何工作的

步骤计数器存储在 config["metadata"]["langgraph_step"] 中。LangGraph 在图执行时递增此计数器,并在超过配置的 recursion_limit 时引发 GraphRecursionError

访问当前步骤计数器

你可以在任何节点内访问当前步骤计数器,以监控执行进度。
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph

def my_node(state: dict, config: RunnableConfig) -> dict:
    current_step = config["metadata"]["langgraph_step"]
    print(f"Currently on step: {current_step}")
    return state

主动递归处理

LangGraph 提供了一个 RemainingSteps 托管值,用于跟踪在达到递归限制之前还剩余多少步。这允许在图内实现优雅降级。
from typing import Annotated, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.managed import RemainingSteps

class State(TypedDict):
    messages: Annotated[list, lambda x, y: x + y]
    remaining_steps: RemainingSteps  # 托管值 - 跟踪达到限制前的剩余步骤

def reasoning_node(state: State) -> dict:
    # RemainingSteps 由 LangGraph 自动填充
    remaining = state["remaining_steps"]

    # 检查是否快要用完步骤
    if remaining <= 2:
        return {"messages": ["即将达到限制,正在收尾..."]}

    # 正常处理
    return {"messages": ["思考中..."]}

def route_decision(state: State) -> Literal["reasoning_node", "fallback_node"]:
    """根据剩余步骤进行路由"""
    if state["remaining_steps"] <= 2:
        return "fallback_node"
    return "reasoning_node"

def fallback_node(state: State) -> dict:
    """处理接近递归限制的情况"""
    return {"messages": ["已达到复杂度限制,提供尽力而为的答案"]}

# 构建图
builder = StateGraph(State)
builder.add_node("reasoning_node", reasoning_node)
builder.add_node("fallback_node", fallback_node)
builder.add_edge(START, "reasoning_node")
builder.add_conditional_edges("reasoning_node", route_decision)
builder.add_edge("fallback_node", END)

graph = builder.compile()

# RemainingSteps 适用于任何 recursion_limit
result = graph.invoke({"messages": []}, {"recursion_limit": 10})

主动式 vs. 响应式方法

处理递归限制主要有两种方法:主动式(在图内监控)和响应式(在外部捕获错误)。
from typing import Annotated, Literal, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.managed import RemainingSteps
from langgraph.errors import GraphRecursionError

class State(TypedDict):
    messages: Annotated[list, lambda x, y: x + y]
    remaining_steps: RemainingSteps

# 主动式方法(推荐) - 使用 RemainingSteps
def agent_with_monitoring(state: State) -> dict:
    """在图内主动监控和处理递归"""
    remaining = state["remaining_steps"]

    # 早期检测 - 路由到内部处理
    if remaining <= 2:
        return {
            "messages": ["即将达到限制,返回部分结果"]
        }

    # 正常处理
    return {"messages": [f"处理中... (剩余 {remaining} 步)"]}

def route_decision(state: State) -> Literal["agent", END]:
    if state["remaining_steps"] <= 2:
        return END
    return "agent"

# 构建图
builder = StateGraph(State)
builder.add_node("agent", agent_with_monitoring)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", route_decision)
graph = builder.compile()

# 主动式:图优雅完成
result = graph.invoke({"messages": []}, {"recursion_limit": 10})

# 响应式方法(后备) - 在外部捕获错误
try:
    result = graph.invoke({"messages": []}, {"recursion_limit": 10})
except GraphRecursionError as e:
    # 图执行失败后在外部处理
    result = {"messages": ["后备方案:超出递归限制"]}
这两种方法之间的主要区别是:
方法检测时机处理方式控制流
主动式(使用 RemainingSteps达到限制之前在图内通过条件路由图继续运行至完成节点
响应式(捕获 GraphRecursionError超过限制之后在图外的 try/catch 中图执行终止
主动式的优势:
  • 在图内实现优雅降级
  • 可以在检查点中保存中间状态
  • 通过部分结果获得更好的用户体验
  • 图正常完成(没有异常)
响应式的优势:
  • 实现更简单
  • 无需修改图逻辑
  • 集中式错误处理

其他可用的元数据

除了 langgraph_stepconfig["metadata"] 中还提供了以下元数据:
def inspect_metadata(state: dict, config: RunnableConfig) -> dict:
    metadata = config["metadata"]

    print(f"步骤: {metadata['langgraph_step']}")
    print(f"节点: {metadata['langgraph_node']}")
    print(f"触发器: {metadata['langgraph_triggers']}")
    print(f"路径: {metadata['langgraph_path']}")
    print(f"检查点命名空间: {metadata['langgraph_checkpoint_ns']}")

    return state

可视化

能够可视化图通常是很好的,尤其是当它们变得更复杂时。LangGraph 附带了多种内置的可视化图的方法。更多信息请参阅可视化你的图

可观察性与追踪

要追踪、调试和评估你的智能体,请使用 LangSmith

了解更多