Skip to content

基础入门

基本概念

图的基本要素

(1)State(状态):LangGraph 运行过程中的共享数据结构,用于表示应用在某一时刻的状态快照。它承载了图运行所需的上下文信息、中间结果和后续节点需要读取的数据,是节点之间传递信息的核心载体。它与我们在学习 LangChain Agent 时使用的 State 是同一概念

(2)Node(节点):LangGraph 中的具体执行单元,通常实现为一个函数。节点会读取当前 State,执行相应的业务逻辑,并返回对 State 的局部更新。节点本身并不直接修改全局状态,状态的合并与提交由运行时统一完成

(3)Edges(边):用于定义节点之间的流转关系,决定一个节点执行完成后下一步应该进入哪个节点。Edge 可以是固定流转,也可以根据当前 State 进行条件判断,从而实现分支、循环等复杂控制流程

图运行过程

(1)计划 / 路由阶段(Plan / Routing):根据当前的 State(状态) 和 Edge(边) 的逻辑,确定本轮超步中应该被执行的节点

(2)执行阶段(Execution):运行本轮被选中的节点。如果本轮有多个节点同时被触发,它们会并行执行。每个节点都会基于本轮开始时的状态快照进行计算,并输出各自对状态的局部更新。在本阶段中,一个节点产生的更新不会立即被其他节点读取到

(3)状态更新/提交阶段(Update / Commit):当本轮所有节点都执行完成后,LangGraph 会将它们的输出统一合并到 State 中,生成新的状态快照。这个新状态会作为下一轮 Superstep 的输入

API 类型

(1)Graph API

Graph API 采用声明式方式构建工作流。开发者需要显式定义 State、Node 和 Edge,将业务流程组织成一个可视化的图结构

当流程中存在较复杂的分支、多个节点之间共享状态、并行执行、结果汇聚,或者需要通过图结构帮助调试和团队协作时,更适合使用 Graph API。官方文档也明确建议,在需要复杂流程可视化、显式状态管理、多条件分支、并行路径以及团队协作时,优先选择 Graph API

(2)Functional API

Functional API 采用命令式方式构建工作流,更接近普通 Python 函数调用。开发者可以使用 @entrypoint 定义工作流入口,使用 @task 定义可被检查点记录的任务,然后在函数内部使用普通的 if/else、循环和函数调用来组织流程。官方文档指出,当已有过程式代码需要最小改造、流程主要是线性的、分支逻辑较简单、希望快速原型验证时,更适合使用 Functional API

对比项Graph APIFunctional API
编程风格声明式图结构命令式函数流程
核心抽象State、Node、Edgeentrypoint、task
状态管理显式定义全局 State更多依赖函数参数和返回值
流程表达通过节点和边表达通过普通 Python 控制流表达
可视化能力强,天然适合画图和调试弱,更像普通代码流程
适合场景复杂工作流、多分支、多节点协作简单流程、快速原型、已有代码改造
学习成本相对更高相对更低

基本使用

状态类型

全局状态 / 内部状态:图内部主要使用的状态,创建 StateGraph 时传递给 state_schema 参数。它通常包含图运行过程中需要读写的大部分字段

输入状态:图对外接收输入时使用的状态,创建 StateGraph 时传递给 input_schema 参数。它用于约束调用图时允许传入哪些字段

输出状态:图最终对外返回结果时使用的状态,创建 StateGraph 时传递给 output_schema 参数。它用于约束图运行结束后只返回哪些字段

私有状态:图内部节点之间传递的临时状态,通常不作为图的输入,也不作为图的最终输出。它可以通过节点函数的入参类型注解声明,并在节点返回值中写入

示例代码

基本步骤:定义状态、定义节点、创建图,添加节点、添加边、获取图、运行图

使用 addEdge()方法添加边,表示节点 A 指向节点 B 的边关系

调用图时,输入会按照 input_schema 进行约束,如果创建图时声明了 input_schema,那么外部输入会按照 input_schema 进行约束,如果没有声明 input_schema,则通常按照 state_schema 作为图的输入 Schema

状态定义有三种类型,分别是 TypedDict、Pydantic、dataclass,官方推荐使用 TypedDict

相比普通 dictTypedDict 可以提供更明确的字段约束和类型提示;相比 dataclassTypedDict 更贴近 LangGraph 中状态的更新方式,因为节点通常返回的是表示“部分状态更新”的字典,而不是完整对象;相比 Pydantic BaseModel,它又更加轻量,不会引入额外的数据校验开销。因此,在没有复杂校验需求的情况下,TypedDict 是定义 LangGraph State Schema 的首选方式

START 不能省略。因为 START 不只是一个语义上的起点标记,它还用于告诉 LangGraph:图运行时应当从哪些节点开始执行,即下面这条边是启动图执行的关键

END 可以省略,END 并不是运行阶段真正执行的节点,即指向 END 的边不会像指向普通节点的边那样触发一个真实的节点任务,它更多用于表达图结构中的终止语义:当前路径执行到这里即可结束

python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class InputState(TypedDict):
    username: str

class OutputState(TypedDict):
    graph_output: str

class OverAllState(TypedDict):
    nickname: str
    username: str
    graph_output: str

class PrivateState(TypedDict):
    greeting: str

def node_1(state: InputState) -> OverAllState:
    # 向全局状态写入数据
    return {
        "nickname": "Dear " + state["username"]
    }

def node_2(state: OverAllState) -> PrivateState:
    # 从全局状态读取数据,写入私有状态
    return {
        "greeting": state["nickname"] + ", 早上好~"
    }

def node_3(state: PrivateState) -> OutputState:
    # 从私有状态读取数据,写入输出状态
    return {
        "graph_output": state["greeting"] + " 很高兴认识你!"
    }

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()
print(graph.invoke({"username":"小黄"}))

from IPython.display import display

display(graph)

"""
运行结果如下
{'graph_output': 'Dear 小黄, 早上好~ 很高兴认识你!'}
"""

"""
图结构如下

┌───────────┐
│ __start__ │
└─────┬─────┘


┌───────────┐
│  node_1   │
└─────┬─────┘


┌───────────┐
│  node_2   │
└─────┬─────┘


┌───────────┐
│  node_3   │
└─────┬─────┘


┌───────────┐
│  __end__  │
└───────────┘
"""

代码执行过程解析

(1)调用图时,InputState 限制外部只需传入 username

(2)node_1 读取 username,生成 nickname 并写入全局状态

(3)node_2 读取 nickname,生成临时字段 greeting,通过 PrivateState 传递给下一个节点

(4)node_3 读取 greeting,生成最终的 graph_output

(5)图执行结束后,根据 OutputState 对结果进行裁剪,只返回 graph_output

输入 username

node_1 生成 nickname

node_2 生成临时 greeting

node_3 生成 graph_output

输出 graph_output

图结构可视化

(1)方式一:绘制 mermaid 并获取源码

python
raw_mermaid = graph.get_graph().draw_mermaid()
print(raw_mermaid)

(2)方式二:绘制 mermaid 并转换为 png

python
from IPython.display import display, Image

png_bytes = graph.get_graph().draw_mermaid_png()
png = Image(png_bytes)
display(png)

# 保存为图片文件
png_filename = 'first_demo_graph.png'
with open(png_filename, "wb") as f:
    f.write(png_bytes)

(3)方式三:Jupyter 中的快捷用法

python
from IPython.display import display

display(graph)

状态设计规范

(1)输入状态和输出状态通常应是全局状态的子集

(2)私有状态和全局状态应尽量避免字段重名

(3)节点函数应明确声明入参状态类型和返回状态类型

(4)节点函数中不应该访问入参状态类型中不存在的字段

(5)节点函数返回的字典应尽量和返回类型注解保持一致

状态记录约束

全局状态、输入状态、输出状态通常在创建 StateGraph 时被记录

私有状态通常在调用 add_node() 添加节点时,根据节点入参类型注解被记录

被记录后的状态字段,底层会成为图运行时可以读写的状态字段

状态访问约束

(1)调用图时,输入会按照 input_schema 进行约束,如果创建图时声明了 input_schema,那么外部输入会按照 input_schema 进行约束,如果没有声明 input_schema,则通常按照 state_schema 作为图的输入 Schema,因此,input_schema 的作用不是“只让第一个节点可见”,而是约束图的外部输入结构,此处的约束是指:按照 schema 裁剪输入,只保留 schema 中出现的状态字段

(2)节点接收到的状态会按照节点入参类型进行裁剪,每个节点能读取哪些字段,主要取决于该节点第一个参数的类型注解

(3)节点函数返回的是状态更新,即本节点想要更新的字段,不需要返回完整状态

(4)节点返回值的应用主要由字段名称和图中已记录的状态字段决定,节点返回的字典会根据字段名称写入对应状态字段,并按照该字段的 Reducer 规则进行合并,需要注意的是,函数返回类型注解主要用于表达代码意图,不是严格的运行时写入边界,也就是说,如果某个字段已经被图记录为可用状态字段,那么节点即使没有在返回类型注解中声明该字段,也可能仍然可以返回并更新它,不过,为了代码清晰,仍然推荐让节点的返回值和返回类型注解保持一致

(5)最终输出会按照 output_schema 进行裁剪,图运行完成后,最终返回给外部调用方的结果会按照 output_schema 进行裁剪,因此,output_schema 的作用不是“只让最后一个节点可见”,而是约束图最终对外暴露哪些字段

MessageState

LangGraph 构建的计算图通常会和 LLM 结合使用,而 LLM 在运行过程中通常需要维护一组消息列表

为了提升开发效率,LangGraph 官方提供了一个预定义状态类型 MessageState,只有一个字段 messages,开发者可以直接继承该状态类型,并在其基础上扩展自定义状态字段。

python
from langchain_deepseek import ChatDeepSeek
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState
from langchain.messages import HumanMessage

from dotenv import load_dotenv
load_dotenv(override=True)

model = ChatDeepSeek(
    model='deepseek-v4-flash',
    extra_body={
        "thinking": {
            "type": "disabled"
        }
    }
)

class OverAllState(MessagesState):
    username: str
    output: str

def node_a(state: OverAllState) -> OverAllState:
    return {
        "messages": [HumanMessage("你好,我是 " + state["username"])]
    }

def llm_node(state: OverAllState) -> OverAllState:
    res = model.invoke(state["messages"])
    return {
        "messages": [res],
        "output": res.content
    }

builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("llm_node", llm_node)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "llm_node")
builder.add_edge("llm_node", END)

graph = builder.compile()
response = graph.invoke({"username": "小黄"})
print(response)

State Reducer

基本介绍

State Reducer 是 LangGraph 中用于合并状态更新的核心机制。在 LangGraph 的 StateGraph 中,每个节点可以读取和写入共享状态,而 Reducer 定义了如何将多个节点对同一状态键的更新合并

Reducer 的核心特征如下

(1)函数签名:(Value, Value) -> Value,接收当前值和更新值,返回合并后的新值

(2)注解定义:通过 Annotated[Type, reducer_function] 为状态键指定 Reducer

(3)默认行为:未指定 Reducer 的状态键使用覆盖策略(Last-Write-Wins)

函数定义

Reducer 本质上是一个二元合并函数,用于定义当同一个字段产生多个更新值时,LangGraph 应该如何将这些值合并为一个最终结果

该 Reducer 的作用是:当某个状态字段存在多次列表更新时,将这些列表内容追加合并,而不是直接覆盖原值

python
# left: 从最开始的位置合并到当前节点的位置的值
# right: 当前节点的值
# 返回值: 合并后的值
def my_reducer(left: list[str],right: list[str]) -> list[str]:
    return left + right
# 1. node1 运行之后的值
left = ["start","node_1 运行完毕"]
right = ["node_2 运行完毕"]
# 2. 合并
merged = my_reducer(left,right)
print(merged)

# 输出:['start', 'node_1 运行完毕', 'node_2 运行完毕']

内置函数

add 函数,用于将两个列表合并为一个列表,可在状态定义时将 Reducer 和状态字段关联

add_messages(left:Messages, right:Messages) 函数,在合并 left 与 right 时,不是简单地执行列表拼接,而是依据消息的 id 进行合并

right 中的某条消息的 idleft不存在,则将该消息追加到结果列表末尾

right 中的某条消息的 idleft 中已有消息的 id 相同,则使用 right 中的新消息替换 left 中的旧消息

python
# reducer 关联状态字段
from typing import TypedDict, Annotated

class OverAllState(TypedDict):
    logs: Annotated[list[str], my_reducer]
    cur_id: str

# add 函数
from operator import add

print(f"{add(1,2) = }")
print(f"{add([1,2], [3,4]) = }")
print(f"{add(['a','b'], ['c']) = }")

""""
输出内容如下

add(1,2) = 3
add([1,2], [3,4]) = [1, 2, 3, 4]
add(['a','b'], ['c']) = ['a', 'b', 'c']
""""

# add_messages 函数
from langgraph.graph.message import add_messages
from langchain.messages import HumanMessage, AIMessage, SystemMessage

left = [
    SystemMessage(content="你是个善解人意的助手", id='1'),
    HumanMessage(content="你好", id='2'),
    AIMessage(content="你好~", id='3'),
]

right = [
    HumanMessage(content="我是老王,你是小王", id='2'),
    AIMessage(content="好的,我记住啦", id='3'),
    HumanMessage(content="你是谁?", id='4'),
    AIMessage(content="我是小王", id='5'),
]

merged = add_messages(left, right)

for msg in merged:
    print(msg)

""""
输出内容如下

content='你是个善解人意的助手' additional_kwargs={} response_metadata={} id='1'
content='我是老王,你是小王' additional_kwargs={} response_metadata={} id='2'
content='好的,我记住啦' additional_kwargs={} response_metadata={} id='3' tool_calls=[] invalid_tool_calls=[]
content='你是谁?' additional_kwargs={} response_metadata={} id='4'
content='我是小王' additional_kwargs={} response_metadata={} id='5' tool_calls=[] invalid_tool_calls=[]
""""

默认行为

如果某个 State 字段没有显式定义 Reducer,LangGraph 会使用默认的状态更新行为后一次更新值会覆盖该字段原有的状态值

换句话说,当节点返回的更新结果中包含某个字段时,如果该字段没有配置 Reducer,LangGraph 不会对新旧值进行追加、合并或累加,而是直接使用本次返回的新值替换原来的旧值

节点中访问 State

图节点中读取 State

state 表示当前节点执行时可以访问到的全局状态快照。节点可以通过读取 state 中的字段获取上游节点写入的数据,并基于这些数据完成当前节点的业务逻辑

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

class OverAllState(TypedDict):
    logs: Annotated[list[str], add]
    id: str

def node_a(state: OverAllState):
    for k, v in state.items():
        print(f"k: {k}, v: {v}")

builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", END)

graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})

""""
输出内容如下

k: logs, v: ['START']
k: id, v: start
""""

图节点更新 state

对于节点没有返回的字段,LangGraph 会保留其原有状态值

对于节点返回的字段,LangGraph 会根据该字段是否配置了 Reducer 来决定如何合并更新值

(1)如果字段配置了 Reducer,则使用对应的 Reducer 函数将旧值和新值合并;

(2)如果该字段没有配置 Reducer,则按照默认规则使用节点返回的新值覆盖原值。

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

class OverAllState(TypedDict):
    # 使用 add 作为 Reducer 函数,将新旧 logs 列表合并
    logs: Annotated[list[str], add]
    id: str

def node_a(state: OverAllState):
    for k, v in state.items():
        print(f"k: {k}, v: {v}")
    return {
        "logs": ["node_a 更新状态"]
    }

builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", END)

graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})
print('=' * 30, '-> result <-', '=' * 30)
print(result)

""""
输出内容如下

k: logs, v: ['START']
k: id, v: start
============================== -> result <- ==============================
{'logs': ['START', 'node_a 更新状态'], 'id': 'start'}
""""

Overwrite 覆盖

在某些场景下,我们可能并不希望继续执行 Reducer 的聚合逻辑,而是希望本次更新直接覆盖旧值,这时可以使用 Overwrite

Overwrite 的作用是:告诉 LangGraph 本次状态更新不走该字段原本定义的 Reducer,而是直接用新值覆盖状态中的旧值

需要注意的是,Overwrite 只影响当前这一次更新,并不会修改状态字段本身的 Reducer 定义。后续节点如果继续正常返回该字段的更新值,仍然会按照原来的 Reducer 逻辑进行合并

python
from langgraph.graph import StateGraph, START, END
from langgraph.types import Overwrite
from typing import TypedDict, Annotated
from operator import add

class OverAllState(TypedDict):
    logs: Annotated[list[str], add]
    id: str

def node_a(state: OverAllState):
    return {
        "logs": ["node_a"],
        "id": "node_a"
    }

def node_b(state: OverAllState):
    return {
        "logs": Overwrite(["node_b"]),
        "id": "node_b"
    }

def node_c(state: OverAllState):
    return {
        "logs": ["node_c"],
        "id": "node_c"
    }

builder = StateGraph(state_schema=OverAllState)
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_edge("node_a", "node_b")
builder.add_edge("node_b", "node_c")
builder.add_edge("node_c", END)

graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})
print('=' * 30, '-> result <-', '=' * 30)
print(result)

"""
输入内容如下

============================== -> result <- ==============================
{'logs': ['node_b', 'node_c'], 'id': 'node_c'}
"""

执行结果解析

(1)node_b 返回更新时,用 Overwrite 包裹了 logs 字段的值,那么当前状态的 logs 会被 ["node_b"] 覆盖,因此最终输出的 logs 字段值变成了 ['node_b', 'node_c']

(2)id 字段按照默认行为,保留最后一次更新的值