LangGraph实战教程:一文搞懂图的状态(State)管理

前情回顾

  1. LangGraph 入门基础全解析
  2. LangGraph中的Reducer是什么
  3. LangGraph 核心概念详解:从编译到可视化

在构建LangGraph计算图时,状态(State)是贯穿整个图执行流程的核心概念。每个节点都可以读取和写入状态,从而实现数据的流转和共享。本文将系统讲解LangGraph中三种状态Schema的定义方式,并通过代码示例帮助你快速掌握。

一、什么是状态(State)?

在LangGraph中,状态本质上是一个共享的数据容器,它承载着图在执行过程中需要传递和维护的所有信息。每个节点都可以从状态中读取数据,也可以向状态中写入更新。

状态的定义实际上是在声明状态的Schema ------即状态字段的完整描述。官方推荐了三种定义Schema的方式:TypedDictdataclassPydantic

二、三种状态Schema定义方式

2.1 TypedDict ------ 最推荐的方式

TypedDict是Python标准库 typing模块提供的一种类型注解工具。它允许你像定义普通字典一样定义状态的结构,同时为每个字段指定类型。

py 复制代码
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
from operator import add

class OverAllState(TypedDict):
    logs: Annotated[list[str], add]  # 使用add归约器实现日志追加
    cur_id: str                       # 当前ID字符串

def node_1(state: OverAllState) -> OverAllState:
    pre_id = state["cur_id"]
    return {
        "logs": ["node_1 运行完毕"],
        "cur_id": pre_id + ", node_1"
    }

def node_2(state: OverAllState) -> OverAllState:
    pre_id = state["cur_id"]
    return {
        "logs": ["node_2 运行完毕"],
        "cur_id": pre_id + ", node_2"
    }

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

graph = builder.compile()
print(graph.invoke({"cur_id": "start"}))

输出结果:

arduino 复制代码
{'logs': ['node_1 运行完毕', 'node_2 运行完毕'], 'cur_id': 'start, node_1, node_2'}

关键点解析:

  • 使用 state["字段名"]方式访问状态字段
  • 节点函数返回的是字典,表示对状态的部分更新
  • Annotated[list[str], add]中的 add是一个归约器(Reducer),表示多个节点返回的 logs列表会自动合并,而不是覆盖

2.2 dataclass ------ 面向对象风格

如果你更喜欢面向对象的编程风格,可以使用 dataclass。它的最大特点是字段访问方式由 ['字段名']变为 .字段名

py 复制代码
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from dataclasses import dataclass
from operator import add

@dataclass
class OverAllState:
    logs: Annotated[list[str], add]
    cur_id: str

def node_1(state: OverAllState) -> OverAllState:
    pre_id = state.cur_id  # 注意:使用 . 访问
    return {
        "logs": ["node_1 运行完毕"],
        "cur_id": pre_id + ", node_1"
    }

def node_2(state: OverAllState) -> OverAllState:
    pre_id = state.cur_id
    return {
        "logs": ["node_2 运行完毕"],
        "cur_id": pre_id + ", node_2"
    }

# 后续构建流程与TypedDict完全相同
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)

graph = builder.compile()
print(graph.invoke({"cur_id": "start"}))

输出结果与TypedDict版本完全一致。

注意: ​ 虽然访问方式变了,但节点函数的返回值仍然是字典 ,而不是 OverAllState对象。这是LangGraph的设计约定------节点返回的是"状态更新",而非完整状态。

2.3 Pydantic ------ 严格的校验派

Pydantic 是最强大的数据验证库,它在定义状态Schema的同时还能提供数据校验能力。字段访问方式与 dataclass相同,使用 .字段名

py 复制代码
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from pydantic import BaseModel
from operator import add

class OverAllState(BaseModel):
    logs: Annotated[list[str], add]
    cur_id: str

def node_1(state: OverAllState) -> OverAllState:
    pre_id = state.cur_id
    return {
        "logs": ["node_1 运行完毕"],
        "cur_id": pre_id + ", node_1"
    }

def node_2(state: OverAllState) -> OverAllState:
    pre_id = state.cur_id
    return {
        "logs": ["node_2 运行完毕"],
        "cur_id": pre_id + ", node_2"
    }

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

graph = builder.compile()
print(graph.invoke({"cur_id": "start"}))

输出结果同样一致。

三、三种方式的校验行为对比

在实际开发中,字段不匹配的情况时有发生。三种方式在处理这类异常时的行为有所不同。

3.1 输入字段不匹配

当我们调用 graph.invoke()传入初始状态时,如果传入的字段名与Schema定义不一致:

方式 异常类型 说明
TypedDict KeyError 将输入视为字典,找不到键时报错
dataclass TypeError 将输入映射到类属性,参数不匹配报类型错误
Pydantic ValidationError 最严格的校验,会给出详细的错误信息

3.2 节点返回字段不匹配

当节点返回的字典中包含Schema中没有定义的字段时,三种方式的行为是统一 的:该字段会被静默忽略,不会报错。

这意味着你需要小心拼写错误,否则某个节点的更新可能永远不会生效。

四、最佳实践:为什么推荐TypedDict?

在实际项目中,官方和社区普遍推荐使用 TypedDict​ 来定义状态Schema。原因如下:

  1. 语法简洁:无需导入额外装饰器,直接定义字典结构即可
  2. 类型提示友好:IDE可以正确推断字段类型,提供自动补全
  3. 贴近更新逻辑:节点返回的是字典形式的"部分更新",与TypedDict天然契合
  4. 轻量无负担:不像Pydantic那样有校验开销,性能更好
  5. 官方首选:绝大多数LangGraph官方示例都使用TypedDict

什么时候用其他方式?

  • 如果你习惯面向对象风格,且团队统一使用 dataclass,可以继续沿用
  • 如果你需要对输入数据进行严格的类型校验(例如来自外部API的数据),选择Pydantic

五、总结

维度 TypedDict dataclass Pydantic
字段访问 ["key"] .attr .attr
输入校验 KeyError TypeError ValidationError
返回多余字段 静默忽略 静默忽略 静默忽略
推荐度 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
适用场景 通用首选 OOP风格团队 需要严格校验

掌握了状态管理,你就掌握了LangGraph的核心。接下来,你可以在此基础上构建更复杂的图结构,比如添加条件分支、循环等高级特性。


六、相关面试题

面试题1:LangGraph中为什么要使用 Annotated[list[str], add]这样的写法?不加 add会怎样?

参考答案:

Annotated[list[str], add]中的 add是一个归约器(Reducer) 。它定义了当多个节点都对同一个字段进行更新时,如何处理这些更新的策略。add表示将多个节点返回的列表进行拼接(+操作)。

如果不加 add,默认行为是覆盖 。也就是说,后执行的节点返回的 logs会完全替换前面节点设置的 logs,导致历史日志丢失。这在很多场景下是不符合预期的。

面试题2:节点函数的返回值必须是完整的State对象吗?能否只返回部分字段?

参考答案:

不需要返回完整的State对象。节点函数返回的是一个字典 ,表示对状态的部分更新。LangGraph会自动将这个字典与当前状态进行合并。如果返回的字典中只包含部分字段,那么其他字段保持不变。

这也是为什么推荐使用 TypedDict的原因之一------节点返回字典,状态也是字典,二者天然匹配。

面试题3:如果我在节点中不小心返回了一个状态Schema中没有定义的字段,会发生什么?

参考答案:

这个多余的字段会被静默忽略 。无论是使用 TypedDictdataclass还是 Pydantic,LangGraph都不会因为节点返回了未定义的字段而抛出异常。这可能会导致一些难以调试的问题------比如你期望更新某个字段,但因为拼写错误,更新始终没有生效。

建议在开发阶段仔细检查节点返回的字段名是否与Schema定义一致,或者编写单元测试来验证状态更新的正确性。

面试题4:Pydantic方式定义状态有什么独特的优势?

参考答案:

Pydantic的主要优势在于数据校验 。当你从外部来源(如用户输入、API响应、数据库查询)获取数据并注入状态时,Pydantic会自动校验数据类型是否符合Schema定义。如果类型不匹配,会抛出 ValidationError并提供详细的错误信息。

此外,Pydantic还支持:

  • 字段默认值
  • 自定义验证器
  • JSON Schema导出
  • 序列化/反序列化

如果你的应用需要处理不可信的输入数据,Pydantic是一个很好的选择。

面试题5:如何在一个图中使用多种不同的状态Schema?

参考答案:

通常情况下,一个 StateGraph只能绑定一个 state_schema。如果你需要在不同阶段使用不同的数据结构,有以下几种方案:

  1. 状态嵌套 :在状态中定义一个嵌套字段,例如 sub_state: dict,然后在不同节点中操作这个子字典
  2. 多个子图:将不同的逻辑拆分成独立的子图(Subgraph),每个子图有自己的状态Schema,然后通过父图协调它们
  3. 动态字段 :使用 TypedDicttotal=False选项定义可选字段,或者使用 dict作为兜底字段类型

推荐优先考虑第一种方案,因为它保持了图的整体性和可追踪性。


希望这篇教程能帮助你彻底掌握LangGraph的状态管理!如果你有任何疑问或想深入了解某个知识点,欢迎留言讨论。

相关推荐
数智化管理手记1 小时前
财务大数据怎么管住资金风险?财务大数据和财务数智化到底怎么结合?
大数据·网络·数据库·人工智能·数据挖掘
whyfail1 小时前
前端学 Spring Boot(5):从“你是谁”到“服务真的上线了”
前端·spring boot·后端
AI导出鸭1 小时前
如何让deepseek生成word文档 ?「AI 导出鸭」苹果版:从API流式解析到Pages级渲染,硬核攻克公式裂变与表格回流的终极方案。
人工智能·chatgpt·word·cocoa·ai导出鸭
2601_953824612 小时前
【计算机毕业设计】基于Spring Boot的画师接稿平台设计与实现
java·spring boot·后端
涛声依旧god2 小时前
如何打造一个 AI Agent 自动写作并一键发布技术文章的自动化系统
人工智能·ai·自动化·ai编程
AI_AGENT_DEV_AI2 小时前
AI 自动化阅卷系统的开发
人工智能
2zcode2 小时前
基于MATLAB车辆拥堵密度依赖的自适应交通信号控制系统
人工智能·计算机视觉·matlab
程序员黑豆2 小时前
鸿蒙开发:RelativeContainer相对布局组件实战——实现5个文本组件对齐
前端·华为·harmonyos
环境栈笔记2 小时前
高性价比指纹浏览器推荐与选型:如何对照价格和实际可用功能筛选候选
前端·人工智能·后端·自动化