LangGraph1.0智能体开发:Graph API概念与设计

一、图(Graphs)

从核心来看,LangGraph将智能体(agent)的工作流程建模为 "图"。你可以通过以下三个关键组件来定义智能体的行为:

  • 状态(State):一种共享数据结构,用于表示应用程序当前的快照。它可以是任何数据类型,但通常会通过共享状态模式(shared state schema)来定义。
  • 节点(Nodes):承载智能体逻辑的函数。节点接收当前状态作为输入,执行某些计算或副作用操作,然后返回更新后的状态。
  • 边(Edges):根据当前状态决定下一个执行节点的函数。边可以是条件分支(conditional branches),也可以是固定转移(fixed transitions)。

通过组合节点和边,你可以创建复杂的、带循环的工作流程,这些流程会随着时间推移不断更新状态。不过,LangGraph的真正优势在于其对状态的管理方式。

需要强调的是:节点和边本质上只是函数 ------ 它们既可以包含大语言模型(LLM),也可以只是常规的代码。 简而言之:节点负责执行任务,边负责决定下一步执行什么。

LangGraph的底层图算法采用 "消息传递(message passing)" 机制来定义通用程序。当一个节点完成操作后,它会通过一条或多条边向其他节点发送消息。这些接收消息的节点随后执行自身的函数,并将生成的消息传递给下一组节点,此过程不断循环。该程序的设计灵感源自谷歌的Pregel系统,以离散的 "超步(super-steps)" 方式运行。

一个 "超步" 可视为对图中节点的一次迭代。并行运行的节点属于同一个超步,而顺序运行的节点则分属不同的超步。在图执行开始时,所有节点均处于未激活状态。当某个节点通过其任意一条入边(或 "通道")接收到新消息(状态)时,该节点会被激活。激活后的节点将执行自身函数,并返回更新结果。在每个超步结束时,没有接收到入边消息的节点会通过将自身标记为未激活来 "投票" 停止。当所有节点均处于未激活状态,且没有正在传递的消息时,图的执行过程便会终止。

1.1 状态图(StateGraph)

StateGraph类是核心的图操作类,其参数由用户自定义的 "状态(State)" 对象指定。

1.2 编译图

构建图的流程如下:首先定义状态,接着添加节点和边,最后对图进行编译。那么 "编译图" 具体指什么?为何需要这一步骤? 编译是一个非常简洁的步骤,主要作用包括:对图的结构进行基础校验(例如检查是否存在孤立节点等);同时,你也可以在此步骤中指定运行时参数(如检查点工具checkpointers、断点工具 breakpoints 等)。 只需调用 .compile 方法,即可完成图的编译:

python 复制代码
graph = graph_builder.compile(...)

二、状态(State)

定义图时,首要任务是确定图的 "状态(State)"。状态由图的模式(Schema) 和归约函数(reducer functions) 组成:模式规定了状态的数据结构,归约函数则定义了如何将更新应用到状态中。状态的模式将作为图中所有节点(Nodes)和边(Edges)的输入模式,其类型可以是 TypedDict 或 Pydantic 模型。所有节点都会输出对状态的更新,这些更新将通过指定的归约函数应用到状态上。

2.1 模式(Schema)

官方推荐的图模式定义方式是使用TypedDict。如果需要为状态设置默认值,可使用数据类(dataclass)。若需实现递归数据校验,也支持使用Pydantic的BaseModel作为图状态(注意:Pydantic的性能低于TypedDict或数据类)。 默认情况下,图的输入模式和输出模式是一致的。若需修改这一默认行为,也可直接指定显式的输入模式和输出模式 ------ 当状态包含多个键(keys),且部分键仅用于输入、部分仅用于输出时,这种方式非常实用。

2.2 多模式(Multiple schemas)

通常,图中所有节点通过单一模式通信,即它们读取和写入同一个状态通道。但在某些场景下,我们需要更精细的控制: 内部节点可传递无需包含在图输入 / 输出中的信息; 可为图设置不同的输入 / 输出模式(例如,输出仅包含一个核心结果键)。 实现方式:

  • 私有状态通道:节点可写入图内部的私有状态通道,用于内部节点间的通信。只需定义一个私有模式(如 PrivateState)即可;
  • 显式输入/输出模式:可定义图的显式输入/输出模式。此时需先定义一个 "内部模式",包含图操作所需的所有键;再定义输入模式和输出模式(二者均为内部模式的子集),以约束图的输入和输出范围。更多细节可参考该指南。 示例如下:
python 复制代码
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,但却向 OverallState 中的通道 foo 写入数据。为何能向输入模式中未包含的状态通道写入数据?这是因为节点可以写入图状态中的任意状态通道------ 图状态是初始化时定义的所有状态通道的集合,其中包含 OverallState 以及过滤后的 InputState 和 OutputState。

我们通过以下方式初始化图:

python 复制代码
StateGraph(
    OverallState,
    input_schema=InputState,
    output_schema=OutputState
)

那么,如何在 node_2 中向 PrivateState 写入数据?如果 PrivateState 未在 StateGraph 初始化时传入,图如何获取该模式的访问权限? 这一操作之所以可行,是因为只要状态模式已定义,节点也可以声明额外的状态通道。本示例中,PrivateState 模式已提前定义,因此我们可以在图中新增 bar 这一状态通道,并向其写入数据。

2.3 归约函数(Reducers)

归约函数是理解节点更新如何应用到状态(State)的关键。状态中的每个键(key)都有其独立的归约函数。如果未显式指定归约函数,则默认所有对该键的更新都会覆盖原有值。归约函数包含多种类型,首先介绍默认类型: 默认归约函数(Default Reducer) 以下两个示例展示了默认归约函数的使用方式: 示例A:

python 复制代码
from typing_extensions import TypedDict

class State(TypedDict):
    foo: int
    bar: list[str]

本示例中,未为任何键指定归约函数。假设图的输入为:{"foo": 1, "bar": ["hi"]}。 若第一个节点返回 {"foo": 2}(节点无需返回完整状态模式,仅需返回待更新的内容),应用该更新后,状态将变为 {"foo": 2, "bar": ["hi"]}; 若第二个节点返回 {"bar": ["bye"]},状态将进一步更新为 {"foo": 2, "bar": ["bye"]}(bar键的原有值被新列表覆盖)。 示例B:

python 复制代码
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)指定了归约函数 operator.add(列表拼接函数),第一个键(foo)仍使用默认归约函数。假设图的输入为 {"foo": 1, "bar": ["hi"]}: 若第一个节点返回 {"foo": 2},应用更新后状态为 {"foo": 2, "bar": ["hi"]}(foo 键被覆盖,bar 键保持不变); 若第二个节点返回 {"bar": ["bye"]},状态将变为 {"foo": 2, "bar": ["hi", "bye"]}(bar 键通过 add 函数将新旧列表拼接,而非覆盖)。

强制覆盖(Overwrite): 在某些场景下,你可能希望绕过归约函数(reducer),直接覆盖某个状态值。LangGraph为此提供了Overwrite类型来实现这一需求。

2.4 在图状态中处理消息

为何使用消息?

大多数主流大语言模型(LLM)提供商都设有聊天模型接口,该接口接收消息列表作为输入。其中,LangChain的聊天模型接口尤其会接收消息对象列表作为输入。这些消息包含多种类型,例如 HumanMessage(用户输入)或 AIMessage(LLM 响应)。

在图中使用消息

在许多场景下,将历史对话记录以消息列表的形式存储在图状态中会非常实用。要实现这一操作,需在图状态中添加一个键(即 "通道"),用于存储消息对象列表,并为该键标注一个归约函数。 归约函数的核心作用是:告知图在每次状态更新时(例如某个节点发送更新时),如何对状态中的消息对象列表进行更新。具体规则如下:

  • 若未指定归约函数,每次状态更新都会用最新提供的值覆盖原有的消息列表;
  • 若仅需将新消息追加到现有列表中,可使用 operator.add 作为归约函数。

不过,你可能还需要手动更新图状态中的消息(例如 "人机协同(human-in-the-loop)" 场景)。若此时使用 operator.add 作为归约函数,发送给图的手动状态更新会被追加到现有消息列表中,而非更新已有的消息。 为避免这种情况,需要一个能跟踪消息ID的归约函数:当消息被更新时,该函数会覆盖原有消息。LangGraph 提供的预构建函数add_messages可实现此需求 ------ 对于全新消息,它会直接追加到列表;对于已有消息的更新,它也能正确处理。

序列化(Serialization)

除了跟踪消息ID,add_messages函数还有一个作用:当messages通道接收到状态更新时,会尝试将消息反序列化为LangChain消息对象。 这一特性支持以以下格式发送图的输入/状态更新:

python 复制代码
# 支持这种格式
{"messages": [HumanMessage(content="message")]}
# 也支持这种格式
{"messages": [{"type": "human", "content": "message"}]}

由于使用add_messages时,状态更新总会被反序列化为LangChain消息对象,因此需通过点语法访问消息属性,例如 state["messages"][-1].content(获取最后一条消息的内容)。 以下是一个使用 add_messages 作为归约函数的图示例:

python 复制代码
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)

由于在状态中存储消息列表是极为常见的需求,LangGraph提供了预构建的MessagesState状态类,简化消息的使用流程。 MessagesState的定义包含一个messages键:该键对应AnyMessage对象列表,并使用add_messages作为归约函数。 通常,图需要跟踪的状态不仅限于消息,因此用户常通过继承MessagesState并添加更多字段来扩展状态,示例如下:

python 复制代码
from langgraph.graph import MessagesState

class State(MessagesState):
    documents: list[str]  # 新增"文档列表"字段

三、节点(Nodes)

在LangGraph中,节点是Python函数(支持同步或异步),需接收以下参数:

  • state------ 图的当前状态
  • config------RunnableConfig 对象,包含线程 ID(thread_id)等配置信息,以及标签(tags)等追踪信息
  • runtime------Runtime 对象,包含运行时上下文及存储(store)、流写入器(stream_writer)等其他信息 与NetworkX类似,你可以通过add_node方法将这些节点添加到图中:
python 复制代码
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------ 该转换为函数添加了批量处理和异步支持,同时原生集成了追踪和调试功能。 若添加节点时未指定名称,LangGraph会自动使用函数名作为节点的默认名称:

python 复制代码
builder.add_node(my_node)
# 之后可通过名称 "my_node" 为该节点创建入边/出边

起始节点(START Node)

START 是一个特殊节点,用于表示接收用户输入并启动图执行的节点。引用该节点的核心目的是指定图的首个执行节点:

python 复制代码
from langgraph.graph import START

graph.add_edge(START, "node_a")  # 从起始节点指向 "node_a",即 "node_a" 为首个执行节点

终止节点(END Node)

END 是一个特殊节点,代表图的终止节点。当某条边指向该节点时,意味着这条路径执行完毕后不再有后续操作:

python 复制代码
from langgraph.graph import END

graph.add_edge("node_a", END)  # "node_a" 执行完毕后,图终止运行

节点缓存(Node Caching)

LangGraph 支持基于节点输入对任务 / 节点结果进行缓存。使用缓存需执行以下两步: 编译图(或指定入口点)时配置缓存(cache) 为节点指定缓存策略(cache_policy),该策略支持两个配置项: key_func:基于节点输入生成缓存键的函数,默认使用 pickle 对输入进行哈希运算 ttl:缓存有效期(单位:秒),未指定时缓存永久有效 示例如下:

python 复制代码
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}

# 为节点指定缓存策略:有效期 3 秒
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())

# 首次调用:执行耗时计算,需 2 秒
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}}]

说明: 首次调用因模拟耗时计算,需2秒完成; 第二次调用时,输入与缓存键匹配,直接返回缓存结果,执行速度极快。

四、边(Edges)

边定义了逻辑的路由方式以及图如何决定终止,这是智能体实现工作流程、不同节点间实现通信的核心环节。边主要包含以下几种关键类型:

  • 普通边(Normal Edges):直接从一个节点指向另一个节点。
  • 条件边(Conditional Edges):通过调用函数来决定下一步指向哪个(或哪些)节点。
  • 入口点(Entry Point):用户输入到达时,首个执行的节点。
  • 条件入口点(Conditional Entry Point):通过调用函数来决定用户输入到达时,首个执行的节点(或多个节点)。

一个节点可以有多个出边。若某个节点存在多条出边,那么所有出边指向的目标节点会在下一个超步(superstep)中并行执行。

普通边(Normal Edges)

若希望始终从节点 A 指向节点 B,可直接使用 add_edge 方法:

python 复制代码
graph.add_edge("node_a", "node_b")

条件边(Conditional Edges)

若需根据条件选择路由到一条或多条边(或选择终止),可使用add_conditional_edges 方法。该方法需传入源节点名称,以及在该节点执行后调用的 "路由函数(routing_function)":

python 复制代码
graph.add_edge("node_a", routing_function)

与节点类似,路由函数会接收图的当前状态作为输入,并返回一个值。默认情况下,路由函数的返回值会作为下一步要传递状态的节点名称(或节点名称列表),这些节点将在下一个超步中并行执行。 你也可以选择性地传入一个字典,将路由函数的输出与下一个节点的名称进行映射:

python 复制代码
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})

若你希望在单个函数中同时实现状态更新与路由逻辑,建议使用 Command(而非条件边)。

入口点(Entry Point)

入口点是图启动时首个执行的节点(或多个节点)。你可以通过从虚拟节点 START 向首个执行节点添加边,来指定图的入口:

python 复制代码
from langgraph.graph import START

graph.add_edge(START, "node_a")

条件入口点(Conditional Entry Point)

条件入口点允许根据自定义逻辑,从不同节点启动图的执行。你可以通过从虚拟节点 START 添加条件边来实现这一功能:

python 复制代码
from langgraph.graph import START

graph.add_conditional_edges(START, routing_function)

你也可以选择性地传入一个字典,将路由函数的输出与下一个节点的名称进行映射:

python 复制代码
graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "node_c"})

五、发送(Send)

默认情况下,节点(Nodes)和边(Edges)是提前定义好的,且基于同一个共享状态运行。但在某些场景下,可能无法提前确定具体的边,或者需要同时存在多个不同版本的状态(State)。map-reduce设计模式就是一个典型例子:在该模式中,第一个节点可能会生成一个对象列表,而你需要将另一个节点应用于这个列表中的所有对象。由于对象的数量可能无法提前预知(意味着边的数量也不确定),且下游节点的输入状态应当各不相同(每个生成的对象对应一个独立状态)。 为支持这种设计模式,LangGraph允许从条件边(conditional edges)中返回Send对象。Send接收两个参数:第一个是目标节点的名称,第二个是要传递给该节点的状态。

python 复制代码
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对象来实现这一需求:

python 复制代码
def my_node(state: State) -> Command[Literal["my_other_node"]]:
    return Command(
        # 状态更新
        update={"foo": "bar"},
        # 控制流(跳转目标)
        goto="my_other_node"
    )

通过 Command 还能实现动态控制流行为(与条件边功能一致):

python 复制代码
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。例如,在实现多智能体交接(multi-agent handoffs)时,不仅需要跳转到其他智能体,还需向该智能体传递信息,此时 Command 是最佳选择。 当你只需基于条件在节点间路由,无需更新状态时,使用条件边。

跳转到父图中的节点

若你在使用子图(subgraphs),可能需要从子图内的某个节点跳转到另一个子图(即父图中的某个节点)。此时,可在 Command 中指定 graph=Command.PARENT 来实现:

python 复制代码
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 后,会跳转到最近的父图。 注意:若子图节点向父图节点发送状态更新,且更新的键(key)在父图和子图的状态模式中均存在,则必须在父图状态中为该键定义归约函数。 这一特性在实现多智能体交接时尤为实用。

在工具中使用 Command

一个常见场景是从工具内部更新图状态。例如,在客户支持应用中,你可能需要在对话初期根据客户的账号或ID查询客户信息。

人机协同(Human-in-the-loop)

Command 是人机协同工作流的重要组成部分:当使用 interrupt() 收集用户输入后,可通过 Command(resume="用户输入内容") 提交输入并恢复图的执行。

七、图迁移(Graph Migrations)

即便使用检查点工具(checkpointer)跟踪状态,LangGraph也能轻松处理图定义(节点、边和状态)的迁移。

  • 对于处于图执行末尾的线程(即未被中断的线程),你可以修改图的完整拓扑结构(例如添加、删除、重命名所有节点和边等)。
  • 对于当前处于中断状态的线程,我们支持除 "重命名 / 删除节点" 之外的所有拓扑结构修改(因为该线程接下来可能要进入一个已不存在的节点)。
  • 在状态修改方面,新增或删除状态键(key)时,我们提供完全的向前兼容和向后兼容性。
  • 被重命名的状态键会丢失其在现有线程中已保存的状态。
  • 若状态键的类型发生不兼容的变更,可能会导致线程中存储的 "变更前状态" 出现问题。

八、运行时上下文(Runtime Context)

创建图时,你可以为传递给节点的运行时上下文指定 context_schema(上下文模式)。这一功能适用于传递不属于图状态的信息 ------ 例如,你可能需要传递模型名称、数据库连接等依赖项。

python 复制代码
from dataclasses import dataclass

@dataclass
class ContextSchema:
    llm_provider: str = "openai"  # 大语言模型提供商,默认值为 "openai"

graph = StateGraph(State, context_schema=ContextSchema)

之后,可通过 invoke 方法的 context 参数将该上下文传入图中:

python 复制代码
graph.invoke(inputs, context={"llm_provider": "anthropic"})

在节点或条件边中,你可以访问并使用该上下文:

python 复制代码
from langgraph.runtime import Runtime

def node_a(state: State, runtime: Runtime[ContextSchema]):
    llm = get_llm(runtime.context.llm_provider)  # 获取对应提供商的 LLM 实例
    # ... 后续业务逻辑

递归限制(Recursion Limit)

递归限制用于设置图在单次执行过程中最多可运行的 "超步(super-step)" 数量。一旦达到该限制,LangGraph 会抛出 GraphRecursionError 异常。默认情况下,递归限制值为 25 步。 你可以在运行时为任意图设置递归限制,通过 config 字典将其传入 invokestream 方法。 注意recursion_limit 是独立的配置键,无需像其他用户自定义配置那样放在 configurable 键内。

示例如下:

python 复制代码
graph.invoke(inputs, config={"recursion_limit": 5}, context={"llm": "anthropic"})

访问和处理递归计数器

任意节点内都可通过 config["metadata"]["langgraph_step"] 获取当前执行步数,从而在达到递归限制前主动处理递归逻辑 ------ 这能让你在图的逻辑中实现 "优雅降级" 策略。

工作原理

步数计数器存储在 config["metadata"]["langgraph_step"] 中。递归限制的校验逻辑为:step > stop(其中 stop = step + recursion_limit + 1)。当超出限制时,LangGraph 会抛出 GraphRecursionError

访问当前步数计数器

你可以在任意节点内访问当前步数,以监控执行进度:

python 复制代码
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"当前步数:{current_step}")
    return state

主动递归处理

你可以检查步数计数器,并在达到限制前主动路由到其他节点,实现图的优雅降级:

python 复制代码
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, END

def reasoning_node(state: dict, config: RunnableConfig) -> dict:
    current_step = config["metadata"]["langgraph_step"]
    recursion_limit = config["recursion_limit"]  # 始终存在,默认值为 25

    # 检查是否接近限制(例如,达到限制的 80% 阈值)
    if current_step >= recursion_limit * 0.8:
        return {
            **state,
            "route_to": "fallback",  # 标记路由目标为降级节点
            "reason": "Approaching recursion limit"  # 原因:接近递归限制
        }

    # 正常业务处理
    return {"messages": state["messages"] + ["thinking..."]}  # 追加思考过程

def fallback_node(state: dict, config: RunnableConfig) -> dict:
    """处理接近递归限制的场景"""
    return {
        **state,
        "messages": state["messages"] + ["已达到复杂度限制,提供最佳努力答案"]
    }

def route_based_on_state(state: dict) -> str:
    if state.get("route_to") == "fallback":
        return "fallback"  # 路由到降级节点
    elif state.get("done"):
        return END  # 执行完成,路由到终止节点
    return "reasoning"  # 继续路由到推理节点

# 构建图
graph = StateGraph(dict)
graph.add_node("reasoning", reasoning_node)  # 推理节点
graph.add_node("fallback", fallback_node)    # 降级节点
graph.add_conditional_edges("reasoning", route_based_on_state)  # 推理节点的条件边
graph.add_edge("fallback", END)  # 降级节点执行后终止
graph.set_entry_point("reasoning")  # 入口节点为推理节点

app = graph.compile()

主动式 vs 响应式处理方案

处理递归限制主要有两种方案:主动式(在图内监控)和响应式(在外部捕获错误)。

python 复制代码
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, END
from langgraph.errors import GraphRecursionError

# 主动式方案(推荐)
def agent_with_monitoring(state: dict, config: RunnableConfig) -> dict:
    """在图内主动监控并处理递归"""
    current_step = config["metadata"]["langgraph_step"]
    recursion_limit = config["recursion_limit"]

    # 提前检测------路由到内部处理逻辑(距离限制还有 2 步时)
    if current_step >= recursion_limit - 2:
        return {
            **state,
            "status": "recursion_limit_approaching",
            "final_answer": "已达到迭代限制,返回部分结果"
        }

    # 正常业务处理
    return {"messages": state["messages"] + [f"第 {current_step} 步"]}

# 响应式方案(降级方案)
try:
    result = graph.invoke(initial_state, {"recursion_limit": 10})
except GraphRecursionError as e:
    # 图执行失败后,在外部处理
    result = fallback_handler(initial_state)

两种方案的核心差异如下:

方案 检测时机 处理位置 控制流
主动式(使用 langgraph_step) 达到限制前 图内部(通过条件路由) 图继续执行至完成节点
响应式(捕获 GraphRecursionError) 超出限制后 图外部(try/catch 块) 图执行终止

主动式方案优势:

  • 图内实现优雅降级
  • 可在检查点中保存中间状态
  • 返回部分结果,提升用户体验
  • 图正常完成执行(无异常抛出)

响应式方案优势:

  • 实现更简洁
  • 无需修改图逻辑
  • 集中式错误处理

其他可用元数据

langgraph_step 外,config["metadata"] 中还包含以下元数据:

python 复制代码
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
相关推荐
test管家2 小时前
如何在Python中使用SQLite数据库进行增删改查操作?
python
AI-Frontiers2 小时前
谷歌重磅出品!揭秘21种Agentic设计模式,AI从业者必备
agent
yangmf20403 小时前
APM(三):监控 Python 服务链
大数据·运维·开发语言·python·elk·elasticsearch·搜索引擎
yangmf20404 小时前
APM(二):监控 Python 服务
大数据·python·elasticsearch·搜索引擎
CoderJia程序员甲4 小时前
GitHub 热榜项目 - 日榜(2025-11-23)
python·开源·github·mcp
字节数据平台4 小时前
火山引擎Data Agent赋能金融行业,打造智能投顾与精准营销新范式
agent
AI爱好者4 小时前
WordPress保卫战:用Python分析日志并封禁恶意爬虫
python
nvd114 小时前
Gidgethub 使用指南
开发语言·python
___波子 Pro Max.4 小时前
Python模块导入详解与最佳实践
python