LangGraphV1.1.0

文章目录

    LangGraph

    版本:V1.1.0

    第 1 章 LangGraph 概述 1.1 什么是 LangGraph LangGraph 是一个低级编排框架和运行时环境,用于构建、管理和部署长期运行的有状 态智能体(agents)。核心理念是将 Agent 工作流建模为图(Graph),其中: ➢ 节点(Nodes):代表计算单元,可以是 LLM 调用、工具执行或任何自定义逻辑 ➢ 边(Edges):定义节点之间的转换逻辑,决定执行流程 ➢ 状态(State):在整个图执行过程中共享和传递的数据

    LangGraph 提供了构建生产级智能体应用的核心能力: ➢ 持久化执行:构建能够从故障中恢复并长时间运行的智能体 ➢ 人机协作:在任何时刻检查和修改智能体状态 ➢ 记忆管理:支持短期工作记忆和跨会话的长期记忆 ➢ 流式处理:专为流式工作流设计 ➢ 生产级部署:为有状态、长期运行的工作流提供可扩展的基础设施 LangGraph 官方源码地址及官网地址如下: ➢ GitHub 地址:https://github.com/langchain-ai/langgraph ➢ 官方文档:https://docs.langchain.com/oss/python/langgraph/overview 1.2 与 LangChain 区别

    LangGraph 与 LangChain 的高级抽象不同,它提供了更细粒度的控制,让开发者能够精 确控制智能体的执行流程,适合需要定制化编排的复杂应用场景。 通过一句话概述 LangChain 和 LangGraph 的区别:当 LLM 应用需要「有状态、可循 环、可分支的多步骤控制流(像一个可回溯的流程图/状态机)」时,LangChain 很难优雅完 成,而必须用 LangGraph。 以下表格以更细粒度的方式展示了两者的区别: 特性 LangGraph LangChain 抽象级别 低级,提供细粒度控制 高级,开箱即用 状态管理 内置状态机和检查点 需要自行管理状态 执行模型 基于图的并行执行 线性链式执行 持久化 原生支持 需要额外实现 适用场景 复杂、有状态的智能体应用开发 简单的链式调用 正是因为 LangGraph 和 LangChain 上述的区别,使得 LangGraph 在以下场景下,有了 更加广泛的应用: ➢ 复杂的多智能体系统 ➢ 需要长期记忆的应用 ➢ 需要人工审核的工作流 ➢ 后台处理任务和实时交互 ➢ 需要精细控制的定制化智能体编排 下面通过快速入门示例来具体讲解如何通过 LangGraph 构建一个应用。 第 2 章 快速入门 2.1 环境安装 1)创建环境 conda create –-name=langgraph_venv 3.12 conda activate langgraph_venv pip install langgraph==1.0.5 2)确认成功 pip show langgraph 2.2 简单示例 使用 LangGraph 构建一个应用,可以分为如下步骤:

    (1)定义状态; (2)定义节点函数; (3)通过状态创建 builder 实例,并添加节点和边; (4)对 builder 进行编译,得到可调用的 graph 对象; (5)调用 graph。 接下来的例子,以一个基于 RAG 的问答系统为例,来做一个快速入门展示。 该问答系统需要实现的功能如下:当接收用户问题之后,先分别进行联网搜索和基于知 识库的检索,得到结果之后,使用大语言模型进行总结回答。 具体代码如下所示: from typing import TypedDict from pydantic import BaseModel from langgraph.constants import START, END from langgraph.graph import StateGraph from langgraph.types import Send,Command class MyState(TypedDict): rag_result:str web_search_result:str query:str final_answer:str

    graph = StateGraph(state_schema=MyState)

    def rag_search_node(state:MyState): query = state["query"] rag_result = f"关于{query}的 rag_result" return {"rag_result":rag_result}

    def web_search_node(state:MyState): query = state["query"] web_search_result = f"关于{query}的 web_search_result" return {"web_search_result":web_search_result}

    def final_answer_node(state:MyState): rag_result = state["rag_result"] web_search_result = state["web_search_result"] final_answer = f"LLM基于{rag_result}和{web_search_result}的最终回复" return {"final_answer":final_answer}

    graph.add_node(rag_search_node) graph.add_node(web_search_node)

    graph.add_node(final_answer_node) graph.add_edge(START, "rag_search_node") graph.add_edge(START,"web_search_node") graph.add_edge("rag_search_node", "final_answer_node") graph.add_edge("web_search_node","final_answer_node") graph.add_edge("final_answer_node", END) compiled_graph = graph.compile() res = compiled_graph.invoke({"query":"如何使用 LangGraph"}) print(res) 编译好之后,可以通过 mermaid 查看图的结构。

    pip install grandalf

    graph_structure = compiled_graph.get_graph() res = graph_structure.draw_ascii() print(res) 通过该例子,可以快速对 LangGraph 编程流程有一个概念。 第 3 章 状态 需要通过 LangGraph 构建一个应用程序时,第一步就是定义 State,因为 State 代表了整 个图的结果目标,和图当中节点需要修改的数据目标。 State 作为 LangGraph 当中最重要的概念,有其专门的定义方式,和运行时的行为特点。 3.1 状态的定义 3.1.1 定义状态类 State Schema 可以通过三种方式定义: ➢ TypedDict(推荐方式): TypedDict 是 Python 提供的一种类型提示工具,用于为 字典(Dict)的键和值指定精确的类型信息。状态类继承 TypedDict,定义键的类型和 reducer 函数(具体参看 3.1.4 reducer 章节内容)即可; 代码示例如下: from typing import TypedDict class MyStateFull(TypedDict): rag_result:str web_search_result:str final_answer:str query:str a_new_key:str ➢ Pydantic BaseModel:Pydantic 提供运行时数据校验,并支持静态类型检查工具 进行类型推导。状态类通过继承 Pydantic 的 BaseModel,定义键的类型和 reducer 函数;

    代码示例如下: from pydantic import BaseModel class MyStateFull(BaseModel): rag_result:str web_search_result:str final_answer:str query:str a_new_key:str ➢ Dataclass: dataclass 是 Python 标准库中的一个装饰器,用于自动生成常见特殊方 法(如__init__、repr、__eq__等),从而简化主要用作数据容器的类的定义。状态 类通过 dataclass 装饰器装饰后,定义键的类型和 reducer 函数即可。 代码示例如下: from dataclasses import dataclass @dataclass class MyStateFullTwo(): rag_result:str web_search_result:str final_answer:str query:str a_new_key:str 3.1.2 输入输出数据隔离 在 LangGraph 当中,可以精细管理输入到图中的状态键有哪些,以及输出的状态键有 哪些。这是通过初始化 StateGraph 时,分别指定三个参数:state_schema、input_schema 和 output_schema 来实现的。 其具体作用,可以参考如下图示:

    1)state_schema 这是图的完整内部状态,包含了所有节点可能读写的字段,必须指定,不能为空。 特点: ➢ 是图的"全局状态空间"; ➢ 所有节点都可以访问和写入这个 schema 中的任何字段。 2)input_schema 定义图接受什么输入,是 state_schema 的子集 特点: ➢ 可选参数,如果不指定,默认等于 state_schema; ➢ 限制图的输入接口,只能传入这些字段; ➢ 是 state_schema 的子集或相等。 3)output_schema 定义图返回什么输出,是 state_schema 的子集 特点: ➢ 可选参数,如果不指定,默认等于 state_schema; ➢ 限制图的输出接口,只返回这些字段; ➢ 是 state_schema 的子集或相等。 代码如下所示: from typing import TypedDict

    from langgraph.constants import START, END from langgraph.graph import StateGraph class MyStateFull(TypedDict): rag_result:str web_search_result:str final_answer:str query:str a_new_key:str

    class InputSchema(TypedDict): query:str

    class OutputSchema(TypedDict): final_answer:str

    graph = StateGraph(state_schema=MyStateFull,input_schema=InputSchema,output_sch ema=OutputSchema)

    def rag_search_node(state:MyStateFull): print(state) query = state["query"] rag_result = f"关于{query}的 rag_result" return {"rag_result":rag_result,"a_new_key":"a_new_key_value"}

    def web_search_node(state:MyStateFull): print(state) query = state["query"] web_search_result = f"关于{query}的 web_search_result" return {"web_search_result":web_search_result}

    def final_answer_node(state:MyStateFull): print('在 final_answer_node当中的 state',state) rag_result = state["rag_result"] web_search_result = state["web_search_result"] final_answer = f"LLM基于{rag_result}和{web_search_result}的最终回复" return {"final_answer":final_answer}

    graph.add_node(rag_search_node) graph.add_node(web_search_node) graph.add_node(final_answer_node) graph.add_edge(START, "rag_search_node") graph.add_edge(START,"web_search_node") graph.add_edge("rag_search_node", "final_answer_node")

    graph.add_edge("web_search_node","final_answer_node") graph.add_edge("final_answer_node", END) compiled_graph = graph.compile() res = compiled_graph.invoke({"query":"如何使用 LangGraph","a_new_key":"a_new_key_value"}) print('最终结果为',res) 3.1.3 节点间数据隔离 LangGraph 当中节点所接收的状态,还可以为一个非全局状态的私有状态。
    示例代码如下: from typing import TypedDict from pydantic import BaseModel from langgraph.constants import START, END from langgraph.graph import StateGraph class MyState(TypedDict): query:str final_answer:str

    class SearchState(TypedDict): rag_result:str web_search_result:str

    class InputSchema(TypedDict): query:str

    class OutputSchema(TypedDict): final_answer:str

    graph = StateGraph(state_schema=MyState)

    def rag_search_node(state:MyState): print(state) query = state["query"] rag_result = f"关于{query}的 rag_result" return {"rag_result":rag_result,"a_new_key":"a_new_key_value"}

    def web_search_node(state:MyState): query = state["query"] web_search_result = f"关于{query}的 web_search_result" return {"web_search_result":web_search_result}

    def final_answer_node(state:SearchState): print('在 final_answer_node当中的 state',state)

    rag_result = state["rag_result"] 
    web_search_result = state["web_search_result"] 
    final_answer = f"LLM基于{rag_result},和{web_search_result}的最终回复
    

    " return {"final_answer":final_answer}

    graph.add_node(rag_search_node) graph.add_node(web_search_node) graph.add_node(final_answer_node) graph.add_edge(START, "rag_search_node") graph.add_edge(START,"web_search_node") graph.add_edge("rag_search_node", "final_answer_node") graph.add_edge("web_search_node","final_answer_node") graph.add_edge("final_answer_node", END) compiled_graph = graph.compile() res = compiled_graph.invoke({"query":"如何使用 LangGraph"}) print('最终结果为',res) 3.1.4 Reducer 函数 Reducer 用于进行当前增量状态(节点输出的状态)和全局状态的合并。State 中的每个 键都有其独立的 reducer 函数。每个 node 的返回值中的每个 key 与全局 state_schema 中对应 的 key 进行合并更新,具体更新逻辑取决于每个 key 指定的 reducer 函数。 Reducer 常用函数有以下几种: ➢ 默认行为:未指定 Reducer 时使用覆盖更新 ➢ 内置 reducer 函数:例如 langgraph.graph.messages 当中的 add_messages 函数 ➢ 自定义 Reducer:支持用户自定义合并逻辑 如果未明确指定 reducer 函数,则默认对该键的更新是覆盖行为。以下为默认覆盖行为: """ LangGraph Reducer函数演示 - 默认 Reducer(覆盖更新) """

    from typing import List from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END

    1. 默认 Reducer(覆盖更新)

    class DefaultReducerState(TypedDict): foo: int bar: List[str]

    def node_default_1(state: DefaultReducerState) -> dict: return {"foo": 2} def node_default_2(state: DefaultReducerState) -> dict: return {"bar": ["bye"]} def run_demo(): print("1. 默认 Reducer(覆盖更新)演示:") builder = StateGraph(DefaultReducerState) builder.add_node("node1", node_default_1) builder.add_node("node2", node_default_2) builder.add_edge(START, "node1") builder.add_edge("node1", "node2") builder.add_edge("node2", END) graph = builder.compile() result = graph.invoke({"foo": 1, "bar": ["hi"]}) print(f"初始状态: {{'foo': 1, 'bar': ['hi']}}") print(f"执行结果: {result}\n")

    if name == "main": run_demo() 内置函数:operator.add:用于列表追加或数值累加 1)列表追加 """ LangGraph Reducer函数演示 - operator.add Reducer(列表追加) """

    import operator from typing import Annotated, List from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END

    3. operator.add Reducer(列表追加)

    class ListAddState(TypedDict): data: Annotated[List[int], operator.add]

    def producer_1(state: ListAddState) -> dict: return {"data": [1, 2]}

    def producer_2(state: ListAddState) -> dict: return {"data": [3, 4]}

    def run_demo():

    print("operator.add Reducer(列表追加)演示:") 
    builder = StateGraph(ListAddState) 
    builder.add_node("producer1", producer_1) 
    builder.add_node("producer2", producer_2) 
    builder.add_edge(START, "producer1") 
    builder.add_edge(START, "producer2")  # 并行执行 
    builder.add_edge("producer1", END) 
    builder.add_edge("producer2", END) 
    graph = builder.compile() 
     
    result = graph.invoke({"data": [0]}) 
    print(f"初始状态: {{'data': [0]}}") 
    print(f"执行结果: {result}\n") 
    

    if name == "main": run_demo() 2)字符串连接 """ LangGraph Reducer函数演示 - 字符串连接 Reducer """

    import operator from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END

    6. 字符串连接 Reducer

    class StringConcatState(TypedDict): text: Annotated[str, operator.add]

    def add_text_1(state: StringConcatState) -> dict: return {"text": "Hello "}

    def add_text_2(state: StringConcatState) -> dict: return {"text": "World!"}

    def run_demo(): print(" 字符串连接 Reducer演示:") builder = StateGraph(StringConcatState) builder.add_node("add_text_1", add_text_1) builder.add_node("add_text_2", add_text_2)

    builder.add_edge(START, "add_text_1") 
    builder.add_edge(START, "add_text_2")  # 并行执行 
    builder.add_edge("add_text_1", END) 
    builder.add_edge("add_text_2", END) 
    graph = builder.compile() 
     
    result = graph.invoke({"text": "Say: "}) 
    print(f"初始状态: {{'text': 'Say: '}}") 
    print(f"执行结果: {result}\n") 
    

    if name == "main": run_demo() 3)数值累加

    """ LangGraph Reducer函数演示 - 数值累加 Reducer """

    import operator from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END

    7. 数值累加 Reducer

    class NumberAddState(TypedDict): count: Annotated[int, operator.add]

    def increment_1(state: NumberAddState) -> dict: return {"count": 5}

    def increment_2(state: NumberAddState) -> dict: return {"count": 3}

    def run_demo(): print(" 数值累加 Reducer演示:") builder = StateGraph(NumberAddState) builder.add_node("increment_1", increment_1) builder.add_node("increment_2", increment_2) builder.add_edge(START, "increment_1") builder.add_edge(START, "increment_2") # 并行执行

    builder.add_edge("increment_1", END) 
    builder.add_edge("increment_2", END) 
    graph = builder.compile() 
     
    result = graph.invoke({"count": 10}) 
    print(f"初始状态: {{'count': 10}}") 
    print(f"执行结果: {result}\n") 
    

    if name == "main": run_demo() 可以自定义 Reducer 函数,用于合并操作。自定义 Reducer 函数接收两个参数,用以将 其合并,最终返回一个结果。 例如此处定义一个 add_message 函数,用于将全局状态当中的 message和节点的 message 进行合并操作: from langgraph.graph import StateGraph from langchain_core.messages import ToolMessage,HumanMessage,AIMessage,BaseMessage from typing import Annotated, TypedDict,List from langgraph.constants import START

    自定义 Reducer

    def add_message(message_list_left:list,message_list_right:list): print("="*20) print("正在执行 add_message") print('左边的',message_list_left) print('右边的',message_list_right) print("="*20)

    return message_list_left + message_list_right 
    

    class MyAgent(TypedDict):

    # 在初始使用自定义的 reducer 
    messages:Annotated[List[BaseMessage],add_message] 
    

    def tool_node(state:MyAgent): return {"messages":[ToolMessage(content="来自 tool_node的内容 ",tool_call_id="xxx")]}

    def llm_node(state:MyAgent): return {"messages":[AIMessage(content="来自 llm_node的内容")]}

    builder = StateGraph(state_schema=MyAgent)

    builder.add_node(tool_node) builder.add_node(llm_node) builder.add_edge(START,"tool_node") builder.add_edge("tool_node","llm_node")

    graph = builder.compile() graph.invoke({"messages":[HumanMessage(content="你好")]}) 3.2 状态的存储 3.2.1 实际场景当中的问题 在前面的例子当中,我们看到,每次用户 invoke 时,LangGraph 都会初始化一个空状 态,然后将用户传入的初始状态合并进来,再继续往下执行。 这在一些一次性、简单任务过程中,没有什么问题。但是对于一些复杂任务,就会出现 一些问题,考虑以下两个场景: 1)场景一:在 agent 的一个会话里,需要在多次调用当中保持上下文

    在这个场景下,我们定义的状态为 messages 列表,第一次调用过程中,图会在 messages 列表当中写入值。而第二次调用,又会初始化一个新的 messages 列表,导致缺失了第一次 调用的上下文。 2)场景二:图执行过程当中报错,不想重复已执行完节点,想要实现断点续传

    在这个场景下,第一次调用过程中,图会在 Node2 节点因某些原因报错,但是我们想要 拿到 Node1 节点所对应的值。并在修复完第二个节点之后,继续往下执行。 3.2.2 状态的存储 前面所涉及到的问题,解决方案都是将第一次调用的结果,存储起来,然后在第二次调 用时,从存储的结果当中取出状态,而不是让 LangGraph 去初始化空状态。 由于同一个 agent 会存在多用户或者单用户多会话等场景,要实现状态存储,并且后续 能够找到正确的状态,就需要在状态当中做一个 id 映射,写入时指定一个 id,读取时读取 指定 id 的状态。 首先需要将状态存储起来,LangGraph为我们提供了一个存储状态的方式:Checkpointer。 Checkpointer 也即检查点, 它可以存储图执行过程当中所产生的状态,具体存储的位置,有 内存、数据库等。Checkpointer 所有相关的实例化类都来自于 langgraph.checkpointer 包。 LangGraph 同样做了前述的 id 隔离,这个 id 在 langgraph 当中就称之为 thread_id。需 要注意:Thread_id 和实际编码当中的线程没有联系,只是一种隔离状态空间的方式。 实际编码进行状态隔离的步骤,如下所示: (1)创建 checkpointer 实例

    (2)图编译时,传入 checkpointer 实例 (3)图调用时,传递 thread_id:保证上下文空间,就传入相同的 thread_id,需要开辟 新的上下文空间,就传入新的 thread_id。 对于前面所讲到的场景一,要想在一个会话中保持上下文,只需要在多次调用时,传入 同一个 thread_id即可;要想启动一个新会话,就传入一个新的 thread_id。这也就是所谓“Agent 短期记忆”。 示例代码如下所示,重点关注内容参看带序号注释: def scenario_1_demo(): # 1、create_agent底层就是通过 LangGraph构建的图结构,此处直接通过 create_agent来举一个例子 from langchain.agents import create_agent from langchain.tools import tool from langchain_openai import ChatOpenAI

    # 1、引入 checkpointer 
    from langgraph.checkpoint.memory import InMemorySaver 
    
    llm = ChatOpenAI(model="gpt-4o-mini") 
    
    # 2、定义 checkpointer实例 
    checkpointer = InMemorySaver() 
    @tool 
    def weather_tool(city:str,date:str)->str: 
        """查天气的工具""" 
        return f'{city}在{date}的天气是晴朗的' 
     
    # 3、构建 Agent时引入 checkpointer 
    agent = create_agent( 
        model=llm, 
        tools=[weather_tool], 
        checkpointer=checkpointer, 
    ) 
    
    # 4、用户第一次调用 
    user1_res = agent.invoke({"messages":"北京 2026-01-14天气怎么样
    

    "},config={"configurable":{"thread_id":"user_1_session_1"}}) print('第一次调用',user1_res['messages'][- 1],end="\n\n==============\n\n")

    # 5、用户在同一个会话当中,第二次调用 
    
    user1_res_2 = agent.invoke({"messages":"适合出去玩吗
    

    "},config={"configurable":{"thread_id":"user_1_session_1"}}) print('第二次调用',user1_res_2['messages'][- 1],end="\n\n==============\n\n") 3.2.3 从状态中恢复执行 上一小节当中所讲到的例子,checkpointer 是通过 InMemorySaver 进行实例化的,这个 实例会将状态全部存储到内存当中,这仍然不能解决场景二的问题,场景二涉及的问题不再 是多次调用之间,保持上下文不中断,而是如何进行故障恢复。 在因为故障导致进程退出后,内存状态空间当中的内容,将全部丢失。在实际生产环境 下,需要使用数据库做持久化状态存储。 下面代码当中,Node2 节点表示在实际执行过程中,因为某些原因导致的错误。如果将 相关的状态存储到数据库中,在第一次执行时,node2 前面节点的状态,会写入到数据库中, 示例代码如下(关键点参见带序号注释):

    pip install langgraph-checkpoint-sqlite

    def scenario_2_demo(): """ 场景二:如何从 checkpointer中恢复状态 """ from langgraph.graph import StateGraph from typing import TypedDict from langgraph.graph import START # 1、引入 sqlite3包和 SqliteSaver类 import sqlite3 from langgraph.checkpoint.sqlite import SqliteSaver # 2、构建 Connection对象 conn = sqlite3.connect(database="./sqlite_data/langgraph_sqlite", check_same_thread=False) # 3、通过 connection对象构建 checkpointer实例 memory = SqliteSaver(conn) class MyState(TypedDict): key_1: str key_2: str key_3: str

    def node_1(state: MyState) -> MyState: 
        # 4、打印 node_1初始状态 
        print('node_1初始状态为',state) 
        return {"key_1":"value_1"} 
     
    def node_2(state: MyState) -> MyState: 
    
        # 5、此处模拟生产环境下代码的 bug或者其他不可预见情况,导致的报错 
        raise Exception("node_2节点执行报错") 
        return {"key_2":"value_2"} 
     
    def node_3(state: MyState) -> MyState: 
        return {"key_3":"value_3"} 
    
    # 添加节点,添加边 
    builder = StateGraph(MyState) 
    builder.add_node(node_1) 
    builder.add_node(node_2) 
    builder.add_node(node_3) 
    builder.add_edge(START, "node_1") 
    builder.add_edge("node_1", "node_2") 
    builder.add_edge("node_1", "node_3") 
    config = { 
        "configurable":{ 
            "thread_id":"123" 
        } 
    } 
    # 6、compile时传入 checkpointer实例 
    graph =builder.compile(checkpointer=memory) 
    # 7、调用时传入 config 
    graph.invoke({},config=config) 
    

    在 node2 节点问题处理完成后,要想从故障中恢复,需要以下两点: (1)invoke 时传递 None 作为初始参数; (2)传入相同的 thread_id。 满足以上两点,即可让图从断点开始,以第一次调用的历史状态,继续往下执行,示例 代码如下(关键点参见带序号注释): def scenario_2_demo(): """ 场景一:如何从 checkpointer中恢复状态 """ from langgraph.graph import StateGraph from typing import TypedDict from langgraph.graph import START

    import sqlite3 
    from langgraph.checkpoint.sqlite import SqliteSaver 
     
    conn = sqlite3.connect(database="./sqlite_data/langgraph_sqlite", 
    

    check_same_thread=False)

    memory = SqliteSaver(conn) 
    class MyState(TypedDict): 
        key_1: str 
        key_2: str 
        key_3: str 
    
    def node_1(state: MyState) -> MyState: 
         
        print('node_1初始状态为',state) 
        return {"key_1":"value_1"} 
     
    def node_2(state: MyState) -> MyState: 
        # 1、修复错误,注释掉报错代码 
        # raise Exception("node_2节点执行报错") 
        return {"key_2":"value_2"} 
     
    def node_3(state: MyState) -> MyState: 
        return {"key_3":"value_3"} 
    
    # 添加节点,添加边 
    builder = StateGraph(MyState) 
    builder.add_node(node_1) 
    builder.add_node(node_2) 
    builder.add_node(node_3) 
    builder.add_edge(START, "node_1") 
    builder.add_edge("node_1", "node_2") 
    builder.add_edge("node_1", "node_3") 
    config = { 
        "configurable":{ 
            "thread_id":"123" 
        } 
    } 
     
    graph = builder.compile(checkpointer=memory) 
    # 2、调用时传入相同的 config,注意,从故障中恢复,此处传递 None即可 
    graph.invoke(None,config=config) 
    

    3.3 获取历史状态 前面我们了解到,构造 checkpointer 之后,langgraph 可以保证在多次调用之间,保证状 态的连续,或者是通过状态进行恢复。不过,要想再深入了解状态,就需要知道状态的保存 时机和如何获取到所有的历史状态。

    3.3.1 LangGraph 底层运行算法 在了解如何获取历史状态之前,首先我们需要了解一下 LangGraph 底层算法—Pregel。 Pregel 是 LangGraph 底层的一个类,管理 LangGraph 应用程序的运行时(runtime)行 为。也就是说,整个图结构,从开始到结束的迭代执行过程,是由 Pregel 控制管理的。 在这个类当中,有两大主要组件: 1)Actors (节点) Actor 即为在前面介绍的 Node,对应在 LangGraph 当中的类为 PregelNode,其订阅某些 通道(channels),从通道当中读取数据,或者写入数据。PregelNode 也实现了 LangChain 的 Runnable 接口。 2)Channels(通道) Channels 用于 actors 之间通信,每个通道都有一个值类型、一个更新类型和一个更新函 数 —— 该函数接收一系列更新并修改存储的值。 正是因为这种设计,在 LangGraph 中通过边和节点构建的图,不是一个传统的有向无环 图(DAG)。在传统的有向无环图下,节点的执行顺序,完全边的连接,某个节点只有在和 它连接的所有的上游节点都执行完成了后,才会执行。 而在 LangGraph 图当中,节点的执行来自于一个 Pregel 的超步(SuperStep)。而一个 超步的执行逻辑,分为如下三个过程: (1)Plan 阶段:确定在此步骤中要执行哪些 Actors。例如,在第一步中,选择订阅特 殊输入通道的 Actors;在后续步骤中,选择订阅上一步骤中更新的通道的参与者 (2)Execute 阶段:并行执行所有选定的 Actors,直到所有参与者完成、其中一个失败 或达到超时时间 (3)Update 阶段:用本步骤中 Actors 所写的值更新 channels。 由以上三个步骤组合而成的超步会重复执行,直到没有 Actors 被选中执行,或者达到 最大步骤数为止。 以如下的图为例:

    图中展示了 builder.compile()内部逻辑:将逻辑图,转换成 Pregel 实例,而节点和边,分 别转换成了 Actors 和 Channels 实例。 针对于 Actors 实例,每个 Actor 实例都有其订阅和写入的 Channels,如下图所示:

    整个图的执行流程,可以分为如下几个 Step,Step 索引从 0 开始:

    整个执行流程如下: 首先在 step 为-1 时,__START__节点执行(图中未标注),将相关状态写入到 branch:to:a 当中;在 Step0 当中,节点 a 执行,将状态写入到 branch:to:b 和 branch:to:c;在 Step1 当中, 节点 b 和 c 执行,将状态写入到 branch:to:b_2 和 branch:to:d 当中;在 Step2,节点 b_2 和节 点 d 执行,b_2 节点再次将状态写入到 branch:to:d 当中。在 Step3 当中,节点 d 再次执行, 由于没有新的状态更新,图执行过程结束。 示例代码如下所示: import operator from typing import Annotated, Any from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END

    class State(TypedDict): """ 状态类型定义 """ aggregate: Annotated[list, operator.add]

    def a(state: State,config): print(f'Adding "A" to {state["aggregate"]}') return {"aggregate": ["A"]}

    def b(state: State,config): print(f'Adding "B" to {state["aggregate"]}') return {"aggregate": ["B"]}

    def b_2(state: State,config): print(f'Adding "B_2" to {state["aggregate"]}') return {"aggregate": ["B_2"]}

    def c(state: State,config): print(f'Adding "C" to {state["aggregate"]}') return {"aggregate": ["C"]}

    def d(state: State,config): print(f'Adding "D" to {state["aggregate"]}') return {"aggregate": ["D"]}

    builder = StateGraph(State)

    builder.add_node("a", a) builder.add_node("b", b) builder.add_node("b_2", b_2) builder.add_node("c", c) builder.add_node("d", d)

    builder.add_edge(START, "a") builder.add_edge("a", "b") builder.add_edge("a", "c") builder.add_edge("b", "b_2") builder.add_edge("b_2", "d") builder.add_edge("c", "d") builder.add_edge("d", END)

    graph = builder.compile()

    1、查看当前图的 channels

    print('当前图的 channels为',graph.channels,end="\n\n")

    2、查看当前图的节点

    print('当前图的节点为',graph.nodes,end="\n\n")

    3、查看 a节点的 trigger(当前节点的订阅)和 writers

    print('当前图的节点 a的 triggers为',graph.nodes['a'].triggers,end="\n\n") print('当前图的节点 a的 writers为',graph.nodes['a'].writers,end="\n\n")

    4、查看 d节点的 trigger和 writers

    print('当前图的节点 d的 triggers为',graph.nodes['d'].triggers,end="\n\n") print('当前图的节点 d的 writers为',graph.nodes['d'].writers,end="\n\n")

    5、执行图,查看执行结果

    output_state = graph.invoke({"aggregate": []})
    print('执行图后的状态为',output_state,end="\n\n")

    3.3.2 获取图执行的历史状态 理解了 Pregel 和 Step 的概念之后,现在可以来学习如何获取图执行的历史状态,以及 如何看懂历史状态当中所存储的内容。 构建好的 graph 实例,可以通过 get_state() / get_state_history()方法,传入想要获取的历 史状态的 thread_id,即可拿到图执行过程中的历史状态,get_state()方法获取到的是最近一 个时间步的状态,get_state_history()获取到的是图执行当中所有时间步的历史状态,其输出 值为一个迭代器,按照时间步倒序排列。 历史状态由 StateSnapShot 实例表示,也即状态快照,其中包含的信息如下:
    名称 类型 描述 values dict[str, Any] | Any 当前状态信息 next tuple[str, ...] 本超步中每个任务要执行的 节点名称 config RunnableConfig 用于获取此快照的配置。 metadata CheckpointMetadata 与此快照相关联的元数据 parent_config RunnableConfig 用于获取父快照(如果有的 话)的配置。 interrupts tuple[Interrupt, ...] 此超步中发生且有待解决的 中断。 我们重点需要关注的是,在状态快照中,除了有当前状态值以外,还包含接下来需要执 行的节点名称,这也为从故障中恢复提供了有用的信息,我们可以通过状态快照直接知道, 从哪一个节点开始继续往下执行。 示例代码如下所示: import operator from typing import Annotated, Any from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.sqlite import SqliteSaver import sqlite3 connection = sqlite3.connect('checkpointer.db',check_same_thread=False) checkpointer = SqliteSaver(connection) class State(TypedDict): """ 状态类型定义

    """ 
    aggregate: Annotated[list, operator.add] 
    

    def a(state: State,config): print(f'Adding "A" to {state["aggregate"]}') return {"aggregate": ["A"]}

    def b(state: State,config): print(f'Adding "B" to {state["aggregate"]}') return {"aggregate": ["B"]}

    def b_2(state: State,config): print(f'Adding "B_2" to {state["aggregate"]}') return {"aggregate": ["B_2"]}

    def c(state: State,config): print(f'Adding "C" to {state["aggregate"]}') return {"aggregate": ["C"]}

    def d(state: State,config): print(f'Adding "D" to {state["aggregate"]}') return {"aggregate": ["D"]}

    builder = StateGraph(State)

    builder.add_node("a", a) builder.add_node("b", b) builder.add_node("b_2", b_2) builder.add_node("c", c) builder.add_node("d", d)

    builder.add_edge(START, "a") builder.add_edge("a", "b") builder.add_edge("a", "c") builder.add_edge("b", "b_2") builder.add_edge("b_2", "d") builder.add_edge("c", "d") builder.add_edge("d", END)

    graph = builder.compile(checkpointer=checkpointer) res = graph.invoke({},config={'configurable':{'thread_id':'1'}})

    1、最终状态

    print(res)

    2、查看历史所有状态

    all_states = graph.get_state_history(config={'configurable':{'thread_id':'1'}}) all_states_list = list(all_states) print("历史所有状态如下:\n") for state in all_states_list: print(state,end="\n"+"="*30+"\n")

    print("\n\n最近一次状态如下:\n") last_state = graph.get_state(config={'configurable':{'thread_id':'1'}}) print(last_state) 第 4 章 节点 4.1 节点的输入输出 4.1.1 节点输入 在 LangGraph 中,一般来讲,节点都是 Python 函数(可以是同步的,也可以是异步的), 它们接受以下参数: ➢ state:图的状态,代表了具体的业务数据; ➢ config:一个 RunnableConfig 对象,包含诸如 thread_id 之类的配置信息,在调用 图时,也可以传递用户定义的其他配置; ➢ runtime:一个 Runtime 对象,包含运行时 context(可自定 context,在调用图时 传入即可)以及其他信息,如 store 和 stream_writer 等;2 以上参数,会在运行过程中,自动被 LangGraph 运行时注入。 节点定义 config 和 runtime 入参如下所示,在运行时,这两个参数 LangGraph 名称会通 过关键字传参方式进行传参,因此两个参数位置可以调换。 具体示例如下: import time from typing import TypedDict, Any, List from langchain_core.runnables import RunnableConfig from langgraph.graph import StateGraph, START, END from langgraph.runtime import Runtime

    class MockLLM: def invoke(self, prompt: str): return f"AI Generated: Answer for '{prompt}'"

    class MockDatabase: def get_user_info(self, user_id: str): return {"id": user_id, "role": "vip" if "vip" in user_id else "standard"}

    class CustomerSupportState(TypedDict): query: str # 用户问题 response: str # 客服回复 log: List[str] # 处理日志

    def node_customer_service(state: CustomerSupportState, config: RunnableConfig, runtime: Runtime) -> dict: """ 客服节点:展示如何从 config 中获取注入的依赖 (LLM, DB) 并使用 runtime 推 送进度。 """

    user_query = state["query"] 
     
    # 1、从 runtime当中获取 context对象 
    llm = runtime.context['llm'] 
    db = runtime.context['db'] 
     
    
    configurable = config.get("configurable", {}) 
    user_id = configurable.get("user_id", "guest") 
     
    print(f"\n[Node] 开始处理,User ID: {user_id}") 
     
    # 2、验证依赖是否存在 
    if not llm or not db: 
        return {"response": "System Error: Dependencies not injected!", 
    

    "log": ["Error"]}

    # 3、使用 db对象查看用户角色 
    user_info = db.get_user_info(user_id) 
    user_tier = user_info.get("role") 
    print(f"[Node] 从 DB 获取用户角色: {user_tier}") 
    
    # 4、使用 runtime.writer 向图外输出自定义数据 --- 
    writer = runtime.stream_writer 
    writer({"status": "thinking", "message": f"正在调用 LLM 为 
    

    {user_tier} 用户生成回复..."})

    time.sleep(0.5) 
    
    # 5、根据用户角色构建不同的 Prompt,并模拟 LLM 调用 
    prompt = f"User({user_tier}) asks: {user_query}" 
    llm_response = llm.invoke(prompt) 
     
    return { 
        "response": llm_response, 
        "log": [f"Processed by {llm.__class__.__name__}"] 
    } 
    
    
    return None 
    

    def run_demo(): builder = StateGraph(CustomerSupportState) builder.add_node("customer_service", node_customer_service) builder.add_edge(START, "customer_service") builder.add_edge("customer_service", END) graph = builder.compile()

    # 初始化外部依赖 
    my_llm = MockLLM() 
    my_db = MockDatabase() 
     
    print("==================================================") 
    print("场景: 依赖注入演示") 
    print("演示目标: 将 LLM 和 DB 对象通过 runtime.context 注入到节点中") 
    print("==================================================") 
     
    # 准备状态 
    initial_state = {"query": "如何升级会员?"} 
     
    # user_id为一个配置信息,将其放入到 config当中 
    config = { 
        "configurable": { 
            "user_id": "vip_user_999", 
        } 
    } 
     
    print("[System] 开始运行图,并注入依赖对象...") 
     
    context = { 
        "llm": my_llm, 
        "db": my_db 
    
    } 
    result = graph.invoke(initial_state,config=config,context=context) 
    print(result) 
    

    if name == "main": run_demo() 4.1.2 节点输出 节点的输出为当前节点对状态的增量更新,而不能将接收到的整个状态实例返回出去。 LangGraph 底层会将所有节点输出的状态,都作为增量状态,并尝试和当前的全局状态 做一次合并操作。如果将整个状态都输出,对于没有配置任何 reducer 的状态键,langgraph 底层无法完成合并,会抛出异常;对于配置了 reducer 的状态键,如果节点输出了不属于该 节点更新的状态,也会导致数据产生问题。 以下演示错误返回了整个状态的问题: from typing import TypedDict from langgraph.constants import START,END from langgraph.graph import StateGraph

    class MyState(TypedDict): query:str file_result:str web_result:str final_answer:str

    def query_web(state:MyState)->dict: """ 做网络搜索,返回搜索结果 :return: """ # 1、错误演示:直接 return整个 state,而非当前节点增量修改的状态 query = state['query'] state['web_result'] = f'{query}的网络搜索结果' return state

    def query_file(state:MyState)->dict: """ 做文件搜索,返回搜索结果 :return: """ query = state['query']

    state['file_result'] = f'{query}的文件搜索结果' 
    return state 
    

    def answer(state:MyState)->dict: """ 返回最终的答案 :return: """ web_result = state['web_result'] file_result = state['file_result'] final_answer = f'LLM基于{web_result},{file_result} 的最终结果' state['final_answer'] = final_answer return state

    graph = StateGraph(state_schema=MyState) graph.add_node(answer) graph.add_node(query_web) graph.add_node(query_file) graph.add_edge(START,'query_web') graph.add_edge(START,'query_file') graph.add_edge('query_web','answer') graph.add_edge('query_file','answer') compiled_graph = graph.compile() res = compiled_graph.invoke({"query":"什么是 Langgraph"}) print(res['final_answer']) 4.2 特殊节点 START 和 END 节点都是 LangGraph 当中的特殊节点。START 它代表着将用户输入发 送到图中的节点。引用此节点的主要目的是确定应首先调用哪些节点。END 节点是一个特 殊节点,代表终止节点。当想表示哪些边在完成后没有动作时,会引用这个节点。 START 和 END 节点本质上是一个字符串,如下所示: END = sys.intern("end") """The last (maybe virtual) node in graph-style Pregel.""" START = sys.intern("start") """The first (maybe virtual) node in graph-style Pregel.""" START 作为一个特殊节点,在图执行完 START 节点之后,也会创建一个 checkpoint, 参考 3.3.2 节。 4.3 节点缓存

    LangGraph 支持基于节点输入对节点进行缓存。对于配置了缓存的节点,且缓存结果没 有过期,以相同的输入再次调用节点时,可直接从缓存当中读取结果,不需要再进行节点计 算。 使用缓存的方法如下: (1)编译图时指定缓存存储后端:langgraph.cache 包当中提供了内存级别的缓存后端, Redis 缓存后端,和 Sqlite 缓存后端的具体实现,需要构造相关实例,并在图编译时传 入。 (2)为节点指定缓存策略。每个缓存策略需配置: ➢ 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 from langgraph.constants import START from langgraph.checkpoint.memory import InMemorySaver

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

    builder = StateGraph(State) checkpointer = InMemorySaver()

    1、定义节点:模拟一些有耗时计算的节点

    def expensive_node(state: State) -> dict[str, int]: # expensive computation print(f"expensive_node 被调用") time.sleep(5) print(f"expensive_node 计算完成") return {"result": state["x"] * 2}

    2、添加节点时,为节点配置缓存策略,这里设置为 10秒缓存,缓存键的配置同样使用

    默认的方式,可以在 CachePolicy当中传入 key_func配置不同的缓存键生成策略 builder.add_node("expensive_node", expensive_node, cache_policy=CachePolicy(ttl=10))

    builder.add_edge(START, "expensive_node")

    3、图编译时,传入缓存器,此处使用 InMemoryCache,也可以使用 RedisCache等

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

    4、第一次调用,传入的状态为 5

    print(graph.invoke({"x": 5},config={"configurable":{"thread_id":"1"}}))

    5、第二次调用,传入相同的状态,由于缓存策略,会直接从缓存中返回结果,而不会

    重新调用节点 print(graph.invoke({"x": 5},config={"configurable":{"thread_id":"2"}})) 4.4 节点重试 在很多使用场景中,有些节点由于客观原因限制,导致其执行过程是不稳定的。因此, 我们可能希望节点拥有自定义的重试策略,例如在调用 API、查询数据库或调用大语言模型 (LLM)等情况下。 为节点添加重试策略,需要在 add_node 中设置 retry_policy 参数。retry_policy 参数接受 一个 RetryPolicy 命名元组对象。RetryPolicy 对象有两个属性:max_attempts 和 retry_on 参 数,前者定义了总共重试次数,后者定义了对于哪些异常类型进行重试。 示例代码如下: import random from typing import Dict, Any from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.types import RetryPolicy

    定义状态

    class State(TypedDict): result: str

    模拟不稳定的 API调用,使用全局变量跟踪尝试次数

    attempt_counter = 0

    def unstable_api_call(state: State) -> Dict[str, Any]: """ 模拟一个不稳定的 API调用,有一定概率失败 """ global attempt_counter attempt_counter += 1 print(f"尝试调用 API,这是第 {attempt_counter} 次尝试")

    # 模拟前几次尝试失败,最后一次成功 
    if attempt_counter < 3: 
        raise Exception(f"模拟 API调用失败 (尝试 {attempt_counter})") 
    else: 
        # 第三次尝试成功 
        return { 
            "result": f"API调用成功,经过 {attempt_counter} 次尝试" 
        } 
    

    模拟抛出 ValueError 的节点

    def value_error_call(state: State) -> Dict[str, Any]: """ 模拟抛出 ValueError 的节点(不会被默认重试策略重试) """ print("调用会抛出 ValueError 的节点") raise ValueError("模拟 ValueError 异常")

    def run_demo(): print("=== LangGraph 节点重试策略演示 ===\n")

    # 重置全局计数器 
    global attempt_counter 
    attempt_counter = 0 
     
    # 演示 1: 使用默认重试策略 
    print("1. 使用默认重试策略:") 
    print("   默认策略会对除特定异常外的所有异常进行重试") 
    print("   不会重试的异常包括: ValueError, TypeError, ArithmeticError, 
    

    ImportError,") print(" LookupError, NameError, SyntaxError, RuntimeError,") print(" ReferenceError, StopIteration, StopAsyncIteration, OSError\n")

    builder1 = StateGraph(State) 
    

    添加节点,使用默认重试策略,

    builder1.add_node( 
        "unstable_call",  
        unstable_api_call,  
        retry_policy=RetryPolicy(max_attempts=5)  # 允许最多 5次尝试 
    ) 
     
    
    builder1.add_edge(START, "unstable_call") 
    builder1.add_edge("unstable_call", END) 
     
    graph1 = builder1.compile() 
     
    print("测试默认重试策略:") 
    try: 
        result = graph1.invoke({"result": ""}) 
        print(f"最终结果: {result}\n") 
    except Exception as e: 
        print(f"最终失败: {type(e).__name__}: {e}\n") 
     
    # 重置全局计数器 
    attempt_counter = 0 
    # 演示 2: 不会重试的异常类型 
    print("3. 测试不会重试的异常类型:") 
     
    builder3 = StateGraph(State) 
     
    # 添加节点,使用默认重试策略 
    builder3.add_node( 
        "value_error_call",  
        value_error_call,  
        retry_policy=RetryPolicy(max_attempts=3) 
    ) 
     
    builder3.add_edge(START, "value_error_call") 
    builder3.add_edge("value_error_call", END) 
     
    graph3 = builder3.compile() 
     
    print("测试 ValueError(默认策略不会重试):") 
    try: 
        result = graph3.invoke({"result": ""}) 
        print(f"最终结果: {result}\n") 
    except Exception as e: 
        print(f"最终失败: {type(e).__name__}: {e}\n") 
    

    if name == "main": run_demo() 4.5 图内外数据传递

    默认情况下,通过 graph.invoke 调用图时,仅在整个图的执行过程都结束之后,我们才 能够拿到最终的状态,那么如果想要在图的执行过程当中,想要获取到图内部所产生的数据, 应该如何实现? 考虑 Agent 的一个场景:首先,我们希望在 LLM 输出时,就能够拿到 LLM 所产生的 token,在前端进行展示;其次,如果 Agent 的流程较长,我们希望能够拿到,当前正在执行 的节点或者流程是什么。 由于 LangGraph 实现了 langchain_core 当中的 Runnable 接口,其为我们提供了 stream 和 astream 方法,也即流式输出方法,通过流式输出,就能解决前面所说到的问题。 Stream 方法提供了多种不同的模式,如下表所示: 模式 描述 values 每一个执行后,流式输出完整的状态 updates 图执行过程中,每一步执行后流式输出增量更新。如果在同一个步当中产生了多个 增量更新,这些增量更新会分别流式输出。 custom 流式输出节点内部的自定义数据。 messages 在任何调用了 LLM 的节点当中,流式输出两元组数据:(LLM Token,metadata) debug 流式输出所有能输出的信息 混合模式 流模式传入列表,在列表当中添加多种不同的模式,可以得到多种流式输出 具体代码如下所示: import time from typing import TypedDict, Annotated, List import operator from langchain_core.messages import BaseMessage, HumanMessage, AIMessage from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph, START, END from langgraph.types import StreamWriter from langgraph.runtime import Runtime llm = ChatOpenAI(model="gpt-4o-mini")

    --- 1. 定义状态 ---

    class State(TypedDict): messages: Annotated[List[BaseMessage], operator.add] current_step: str

    --- 2. 定义节点 ---

    def node_input(state: State): """模拟接收用户输入""" return { "messages": [HumanMessage(content="请帮我写一段 Python代码")], "current_step": "input_received" }

    def node_processing(state: State, runtime: Runtime): """ 模拟中间处理过程,并使用 writer 输出自定义流式数据 这对应 stream_mode="custom" """ steps = ["正在分析意图...", "正在检索知识库...", "正在构建 Prompt..."] writer = runtime.stream_writer for i, step in enumerate(steps): time.sleep(0.5) # 模拟耗时操作

        # 使用 writer 发送自定义数据 (不影响图的状态) 
        # 这些数据只能通过 stream_mode="custom" 接收到 
        writer({ 
            "step_index": i + 1, 
            "description": step, 
            "timestamp": time.time() 
        }) 
         
    return {"current_step": "processing_complete"} 
    

    def node_generation(state: State): """ 模拟 LLM 生成过程 LangGraph 的 stream_mode="messages" 会自动捕获 LLM 的流式输出 """

    # 调用 LLM 
    response = llm.invoke(state["messages"]) 
     
    return { 
        "messages": [response], 
        "current_step": "generation_complete" 
    } 
    

    --- 3. 构建图 ---

    builder = StateGraph(State)

    builder.add_node("input", node_input) builder.add_node("process", node_processing) builder.add_node("generate", node_generation)

    builder.add_edge(START, "input") builder.add_edge("input", "process") builder.add_edge("process", "generate") builder.add_edge("generate", END)

    graph = builder.compile()

    --- 4. 演示不同的流式输出模式 ---

    def run_demo(): initial_state = {"messages": [], "current_step": "start"}

    print(f"\n{'='*20} 1. Mode: values (输出完整状态) {'='*20}") 
    print("描述: 每执行完一个节点,输出当前的完整 State") 
    for event in graph.stream(initial_state, stream_mode="values"): 
        # event 就是当前的 State 字典 
        print(f"State: keys={list(event.keys())}, 
    

    step={event.get('current_step')}")

    print(f"\n{'='*20} 2. Mode: updates (输出增量更新) {'='*20}") 
    print("描述: 每执行完一个节点,输出该节点返回的增量数据") 
    for event in graph.stream(initial_state, stream_mode="updates"): 
        # event 是一个字典,key是节点名,value是该节点的输出 
        handle_event(event) 
        print(f"Update: {event}") 
    
    print(f"\n{'='*20} 3. Mode: custom (输出自定义数据) {'='*20}") 
    print("描述: 仅输出节点内部通过 writer() 发送的数据") 
    for event in graph.stream(initial_state, stream_mode="custom"): 
        # event 就是 writer() 中传入的对象 
        print(f"Custom Data: {event}") 
    
    print(f"\n{'='*20} 4. Mode: messages (输出 LLM Token) {'='*20}") 
    print("描述: 输出 LLM 生成的消息片段 (Token)") 
    # 注意:FakeListChatModel 默认是一次性返回,但在 messages 模式下会被包装
    

    成 chunk for chunk, metadata in graph.stream(initial_state, stream_mode="messages"): # chunk 是 BaseMessageChunk 对象 # metadata 包含 node 信息 node_name = metadata.get('langgraph_node', 'unknown')

        print(f"[{node_name}] Token: {chunk.content!r}") 
        # print(chunk.content,end="") 
        time.sleep(0.2) 
         
    
    print(f"\n{'='*20} 5. Mode: debug (调试模式) {'='*20}") 
    print("描述: 输出所有详细的执行信息,包括任务调度、输入输出等") 
    # debug 模式输出量很大,这里只打印前几条示意 
    count = 0 
    for event in graph.stream(initial_state, stream_mode="debug"): 
        if count < 3: 
            print(f"Debug Event: {event['type']} - {event.get('payload', 
    

    {}).get('name')}") count += 1 print("... (省略后续 debug 信息)")

    print(f"\n{'='*20} 6. Mixed Mode (混合模式) {'='*20}") 
    print("描述: 同时获取 updates 和 custom 数据") 
    # 返回的是 (mode, data) 元组 
    for mode, data in graph.stream(initial_state, 
    

    stream_mode=["updates", "custom"]): if mode == "updates": print(f"[Updates] 来自节点 {list(data.keys())[0]}") elif mode == "custom": print(f"[Custom] {data['description']}")

    if name == "main": run_demo() 4.6 人工审核节点 Agent 在完成用户设定的任务时,有时候我们希望用户参与当中部分重要决策过程。 LangGraph 为此提供了一个非常方便的原语:interrupt。其原理如下图所示:

    可以在节点内部任意位置引入 interrupt,当节点执行到 interrupt 所在位置时,就会停止 执行,并可以得到从图内传递出来的,需要用户审核的数据,在用户进行相关审核完成后, 又可通过 graph.invoke,让图继续往下执行。 具体示例如下: """ LangGraph interrupt 演示:让用户参与关键决策

    场景:准备执行一笔“转账”,在真正执行前让用户审核/修改转账信息。 """

    from future import annotations

    from typing import Any from typing_extensions import TypedDict

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

    class TransferState(TypedDict): recipient: str

    amount: int 
    memo: str 
    approved: bool 
    final_status: str 
    

    def review_transfer(state: TransferState) -> dict[str, Any]: print("\n[Node] review_transfer:生成待执行的转账请求") pending_transfer = { "recipient": state["recipient"], "amount": state["amount"], "memo": state["memo"], }

    user_review = interrupt( 
        { 
            "title": "转账审核", 
            "pending_transfer": pending_transfer, 
            "instruction": "请返回 bool(是否批准) 或 dict(可改 
    

    recipient/amount/memo,并带 approved 字段)。", } )

    approved = False 
    updated_transfer = dict(pending_transfer) 
    if isinstance(user_review, bool): 
        approved = user_review 
    elif isinstance(user_review, dict): 
        approved = bool(user_review.get("approved", True)) 
        for k in ("recipient", "amount", "memo"): 
            if k in user_review: 
                updated_transfer[k] = user_review[k] 
    
    print(f"[Node] review_transfer:用户审核结果 approved={approved},
    

    transfer={updated_transfer}") return { "approved": approved, "recipient": updated_transfer["recipient"], "amount": updated_transfer["amount"], "memo": updated_transfer["memo"], }

    def execute_transfer(state: TransferState) -> dict[str, str]: if not state["approved"]: print("\n[Node] execute_transfer:用户未批准,取消转账")

        return {"final_status": "已取消:用户未批准转账"} 
    
    print("\n[Node] execute_transfer:模拟执行转账") 
    recipient = state["recipient"] 
    amount = state["amount"] 
    memo = state["memo"] 
    return {"final_status": f"已转账:收款人={recipient},金额={amount},
    

    备注={memo}"}

    def build_graph(): builder = StateGraph(TransferState) builder.add_node("review_transfer", review_transfer) builder.add_node("execute_transfer", execute_transfer)

    builder.add_edge(START, "review_transfer") 
    builder.add_edge("review_transfer", "execute_transfer") 
    builder.add_edge("execute_transfer", END) 
    
    return builder.compile(checkpointer=InMemorySaver()) 
    

    def run_demo(): graph = build_graph() config = {"configurable": {"thread_id": "interrupt-demo-1"}}

    print("==================================================") 
    print("场景:转账前人工审核(interrupt)") 
    print("第一次 invoke 会停在 interrupt,并把审核数据带出来") 
    print("第二次 invoke 使用 Command(resume=...) 把用户决策传回去继续执行
    

    ") print("==================================================")

    initial_state: TransferState = { 
        "recipient": "Alice", 
        "amount": 100, 
        "memo": "午饭 AA", 
        "approved": False, 
        "final_status": "", 
    } 
    
    first = graph.invoke(initial_state, config=config) 
    interrupt_payload = first["__interrupt__"][0].value 
    print("\n[System] 图已暂停,等待用户审核。可给用户展示的数据如下:") 
    print(interrupt_payload) 
    
    
    user_resume_value = {"approved": True, "amount": 80, "memo": "改为 80
    

    元(实际应付)"} final = graph.invoke(Command(resume=user_resume_value), config=config) print("\n[System] 已恢复执行,最终结果:") print(final["final_status"])

    if name == "main": run_demo() 需要注意的是,使用 interrupt 时,在第二次调用 graph.invoke(command)继续执行时,有 interrupt 的函数,会从函数起点往下执行,如果在函数当中,有更新数据、调用 API 接口等 相关操作时,会造成多次重复执行的情况。因此,一定要保证这种类型操作的幂等性,或者 是将其封装到一个单独的节点当中,避免在恢复时再次执行。 第 5 章 边 5.1 边的本质 定义边,本质上是定义了,Pregel 当中节点订阅的状态通道和节点执行之后,需要更新 的状态通道。 边有几种关键类型: ➢ Normal Edges: 普通边。直接从一个节点连接到下一个节点。 ➢ Conditional Edges: 条件边。调用函数以确定接下来要前往哪个(哪些)节点。 5.2 条件边 条件边的本质是一个路由函数,其根据当前状态动态决定下一个要执行的节点。 from typing import Literal from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END

    定义状态

    class GraphState(TypedDict): value: int step: str

    定义节点函数

    def node_a(state: GraphState) -> dict: """节点 A""" print("执行节点 A") return {"value": state["value"], "step": "A执行完毕"}

    def node_b(state: GraphState) -> dict: """节点 B""" print("执行节点 B") return {"value": state["value"] * 2, "step": "B执行完毕"}

    def node_c(state: GraphState) -> dict: """节点 C""" print("执行节点 C") return {"value": state["value"] - 1, "step": "C执行完毕"}

    条件边的路由函数

    def route_condition(state: GraphState) -> Literal["node_b", "node_c"]: """根据 value值决定路由到哪个节点""" if state["value"] % 2 == 0: return "node_b_alias" # 偶数路由到节点 B else: return "node_c_alias" # 奇数路由到节点 C

    def main(): """演示条件边""" print("=== 条件边演示 ===")

    # 创建图 
    builder = StateGraph(GraphState) 
     
    # 添加节点 
    builder.add_node("node_a", node_a) 
    builder.add_node("node_b", node_b) 
    builder.add_node("node_c", node_c) 
     
    # 添加边 
    builder.add_edge(START, "node_a")  # 入口点 
     
    # 添加条件边 
    builder.add_conditional_edges( 
        "node_a",  # 源节点 
        route_condition,  # 路由函数 
        {  # 路由映射 
            "node_b_alias": "node_b", 
            "node_c_alias": "node_c" 
        } 
    ) 
     
    
    # 从 B和 C到结束 
    builder.add_edge("node_b", END) 
    builder.add_edge("node_c", END) 
     
    # 编译图 
    graph = builder.compile() 
     
    # 执行图 - 偶数情况 
    print("输入值为偶数:") 
    result = graph.invoke({"value": 2}) 
    print(f"执行结果: {result}") 
     
    # 执行图 - 奇数情况 
    print("\n输入值为奇数:") 
    result = graph.invoke({"value": 1}) 
    print(f"执行结果: {result}\n") 
    

    if name == "main": main() 5.3 可控循环 通过条件边,我们可以构建带有循环结构的图,例如在典型的 REACT 模式下,工具调 用和大模型总结生成结果形成了一个循环,如下图所示:

    需要注意的是,这种带循环的图结构,有一个隐藏的问题:图执行过程当中,可能因为 某些原因,导致一直在循环内循环往复执行,因此 LangGraph 也提供了一个强制使图的执行 终止的递归限制参数。 递归限制设定了图在抛出错误之前允许执行的超级步骤数量,默认值25,在graph.invoke 的 config 参数中指定。在经过指定数量的超级步骤后,图还没有自然停止执行时,LangGraph 会抛出异常 GraphRecursionError。 示例代码如下所示:

    from typing import Annotated, Dict, Literal from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.errors import GraphRecursionError

    class LoopState(TypedDict): count: int result: str max_count: int

    def node_a(state: LoopState) -> dict: """节点 a:处理逻辑并更新计数""" print(f"执行节点 a,当前计数: {state['count']}") return { 'count': state['count'] + 1, 'result': f"已处理{state['count']}次" }

    def node_b(state: LoopState) -> dict: """节点 b:辅助处理""" print(f"执行节点 b,当前计数: {state['count']}") return { 'result': f"已处理{state['count']}次 - 辅助处理" }

    def route(state: LoopState) -> Literal["b", END]: """条件路由函数:决定是继续循环还是终止""" # 终止条件:当计数达到最大值时终止 if state['count'] >= state['max_count']: print(f"满足终止条件,计数 {state['count']} >= {state['max_count']},返回 END") return END else: print(f"未满足终止条件,计数 {state['count']} < {state['max_count']},返回 b") return "b"

    创建图

    builder = StateGraph(LoopState)

    添加节点

    builder.add_node("a", node_a) builder.add_node("b", node_b)

    添加边

    builder.add_edge(START, "a") builder.add_conditional_edges("a", route) builder.add_edge("b", "a")

    编译图

    graph = builder.compile()

    执行图

    print("=== 开始执行工作流 ===") try: result = graph.invoke(input={ 'count': 0, 'result': '', 'max_count': 300 }, config={ 'recursion_limit': 6 # 设置递归限制 }) print("=== 执行结果 ===") print(result) except GraphRecursionError as e: print(f"递归错误: {e}")

    end