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 时,必须添加返回类型注解,明确该节点可路由到的所有节点名称(例如 CommandLiteral\["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
相关推荐
步行cgn4 分钟前
Spring 注入 Map 集合详解
数据库·python·spring
今儿敲了吗8 分钟前
03停用词过滤
笔记·python
嘟嘟嘟952727 分钟前
AI Agent 的边缘困境
人工智能·架构·agent
HYDtomako27 分钟前
Agent checkpoint设计
ai·agent·checkpoint·claude code
李可以量化1 小时前
Tornado 部署公域网络安全与防护(上)
python
梦在远山后1 小时前
从需求到架构:我的企业研发 Agent 整体设计
langchain·agent
风跟我说过她1 小时前
SQL 一键转经典 Chen 风格 ER 图:开源 CLI + 在线工具 + Agent Skill
数据库·python·sql·开源·开源软件
DigitalOcean1 小时前
Kimi K3 + Claude:AI Agent 多模型路由实战
llm·agent
Ulyanov1 小时前
AudioVision Pro:基于 PySide6 + sounddevice 的实时音频可视化播放器设计
python·算法·音视频
ID34610744201 小时前
【课程设计】基于Spring Boot+Vue的校园共享无人机服务系统设计与实现-计算机毕设 附源码44219
javascript·vue.js·spring boot·python·node.js·php·课程设计