LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 2 · State & Memory
1 2-1 State Schema
2 2-2 Reducers
3 2-3 Multi Schemas
4 2-4 Trim & Filter
5 2-5 Chatbot Summary
6 2-6 Ext Memory
SUM Module Summary
HomeLangGraphModule 2 · State & Memory2-1 State Schema
📓 Notebook 📖 Explained
LANGGRAPH MODULE 2 · LESSON 1

深入理解 LangGraph
State Schema 状态模式定义

TypedDict、Dataclass、Pydantic——三种方式定义图的"数据契约"。
从类型提示到运行时校验,选择合适的状态模式是构建健壮 Agent 的第一步。

1 Module 2 的目标:State 与 Memory 的深度理解

在 Module 1 中,你已经学会了 LangGraph 的核心骨架:State(状态)、Node(节点)、Edge(边)、StateGraph(图)。你知道了如何用 TypedDict 定义一个简单的 State,并让节点之间通过它传递数据。

但 Module 1 只是冰山一角。现实世界里的 Agent 需要处理更复杂的状态结构:包含消息历史的对话上下文、需要校验的业务字段、带有默认值的可选属性……这些都需要你对 State Schema 有更深的理解。

Module 2 的两条主线

State(状态定义):如何用不同的 Python 数据类型定义图的状态模式,包括 TypedDict、Dataclass、Pydantic;以及如何用 Reducer 控制状态更新行为。

Memory(记忆):如何让 Agent 记住跨轮次的对话内容,包括 In-memory checkpointer 和外部 Memory Store。

本讲解文档聚焦于 Lesson 1:State Schema,也就是 Module 2 的起点——搞清楚"状态"究竟可以用几种方式定义,各自有什么优缺点,实际项目里该怎么选。

为什么要专门讲 Schema 定义?

State Schema 是图的"数据契约"(Data Contract)。整张图里所有的节点都依赖这个契约来读写数据。选错了 Schema 方式会带来两类典型问题:

理解三种 Schema 定义方式,才能在合适的场景选用合适的工具,写出可维护、易调试的 LangGraph 代码。

2 什么是 State Schema,为什么需要它

Schema 是图的"数据合同"

当你创建一个 StateGraph 时,第一步就是传入一个 State Schema 类

# 这里的 MyState 就是 Schema——它定义了这张图能存放什么数据
builder = StateGraph(MyState)

这个 Schema 做了三件事:

核心洞察:State 是图的"共享内存"

LangGraph 的每个 Node 是相互独立的函数,它们之间不能直接传参、也不能共享变量。State 就是它们唯一的通信介质。Node A 把计算结果写入 State,Node B 从 State 里读出来继续处理。Schema 则是这块共享内存的"格式说明书"。

LangGraph 支持哪些 Schema 定义方式?

LangGraph 在这里非常灵活——它不强制你用某一种特定的 Python 数据类型。只要是 Python 能识别的结构化类型,LangGraph 基本都能接受:

TypedDict
  • Python 标准库
  • 字典风格访问
  • 仅类型提示,不校验
  • 最轻量、最常用
Dataclass
  • Python 标准库
  • 属性风格访问
  • 仅类型提示,不校验
  • 支持默认值
Pydantic BaseModel
  • 第三方库
  • 属性风格访问
  • 运行时强校验
  • 最安全、最严格

接下来我们逐一深入讲解这三种方式。

3 TypedDict:最常用的状态定义方式

TypedDict 是什么?

TypedDict 来自 Python 的 typing(或 typing_extensions)模块。它让你可以定义一个"有类型约束的字典"——本质上还是字典,但每个键都标注了期望的值类型。

from typing_extensions import TypedDict

# 定义一个最简单的 TypedDict State
class TypedDictState(TypedDict):
    foo: str   # 字段 foo,期望类型是字符串
    bar: str   # 字段 bar,期望类型是字符串
重要:类型提示 ≠ 类型强制

TypedDict 的类型标注只是提示(hint),不是强制(enforce)。静态检查工具(如 mypy、Pyright)或 IDE 会利用这些提示来提前发现潜在错误,但 Python 解释器在运行时不会校验这些类型。你完全可以给 foo 赋一个整数,Python 不会报错。

使用 Literal 约束枚举值

对于只能取固定几个值的字段(比如状态、情绪、模式),可以用 Literal 类型提示来做约束:

from typing import Literal
from typing_extensions import TypedDict

class TypedDictState(TypedDict):
    name: str                      # 任意字符串
    mood: Literal["happy", "sad"]  # 只能是 "happy" 或 "sad"

这里 Literal["happy", "sad"] 告诉类型检查器:mood 字段的值只应该是这两个字符串之一。如果你写了 mood: "angry",mypy 会报错——但运行时 Python 不管。

在 LangGraph 中使用 TypedDict

把 TypedDict 类传给 StateGraph,然后定义节点函数,访问方式与普通字典完全一样——用 state["字段名"]

import random
from typing import Literal
from langgraph.graph import StateGraph, START, END

# ─── 节点函数:用字典下标访问 state ───
def node_1(state):
    print("---Node 1---")
    # state["name"] 读取当前 name 字段的值
    return {"name": state["name"] + " is ... "}

def node_2(state):
    print("---Node 2---")
    return {"mood": "happy"}

def node_3(state):
    print("---Node 3---")
    return {"mood": "sad"}

def decide_mood(state) -> Literal["node_2", "node_3"]:
    if random.random() < 0.5:
        return "node_2"
    return "node_3"

# ─── 构建图 ───
builder = StateGraph(TypedDictState)
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_conditional_edges("node_1", decide_mood)
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
graph = builder.compile()

# ─── 调用:传入初始 State 字典 ───
result = graph.invoke({"name": "Lance"})
# 输出可能是:{'name': 'Lance is ... ', 'mood': 'happy'}
为什么 LangGraph 文档和例子默认用 TypedDict?

TypedDict 是 Python 标准库的一部分,无需额外安装。它访问方式直观(字典下标),和大多数 Python 开发者的习惯一致。对于简单到中等复杂度的 State,TypedDict 是最低摩擦的选择——这就是为什么你在 LangGraph 官方文档和几乎所有教程里都会看到它。

TypedDict 的图结构可视化

TypedDictState 示例图结构

__start__
State: {name: "Lance"}
node_1
写入: name = "Lance is ... "
decide_mood() 路由
node_2
写入: mood = "happy"
node_3
写入: mood = "sad"
__end__
State: {name: "Lance is ... ", mood: "happy/sad"}

4 Dataclass:更 Pythonic 的结构化数据

Python Dataclass 简介

Python 的 dataclasses 模块(Python 3.7+ 内置)提供了另一种定义结构化数据的方式。它用 @dataclass 装饰器自动生成 __init____repr__ 等魔术方法,让你可以用更简洁的语法创建"主要用于存储数据的类":

from dataclasses import dataclass
from typing import Literal

@dataclass
class DataclassState:
    name: str
    mood: Literal["happy", "sad"]

和 TypedDict 比,Dataclass 有两个显著不同:

Dataclass 版本的节点函数

注意 node_1 里访问 State 的方式改变了——从字典下标变成了属性访问:

# TypedDict 版本(字典下标访问)
def node_1_typed_dict(state):
    return {"name": state["name"] + " is ... "}  # state["name"]

# Dataclass 版本(属性访问)
def node_1_dataclass(state):
    return {"name": state.name + " is ... "}     # state.name
有趣的细节:节点仍然返回字典

即使 State 是 Dataclass,节点返回的更新值仍然是字典,不是一个新的 Dataclass 实例。这是因为 LangGraph 内部会把节点返回的字典里的每个键单独对应到 State 的各个字段上去合并更新。只要字典的键名和 Dataclass 的属性名一致,LangGraph 就能正确处理。

调用 Dataclass 图

# 构建图(和 TypedDict 版一样,只是 Schema 类不同)
builder = StateGraph(DataclassState)
builder.add_node("node_1", node_1_dataclass)
builder.add_node("node_2", node_2)  # node_2/node_3 返回字典,通用
builder.add_node("node_3", node_3)
builder.add_edge(START, "node_1")
builder.add_conditional_edges("node_1", decide_mood)
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
graph = builder.compile()

# 调用:传入 Dataclass 实例而不是字典
result = graph.invoke(DataclassState(name="Lance", mood="sad"))
# 输出(LangGraph 返回字典格式):
# ---Node 1---
# ---Node 3---
# {'name': 'Lance is ... ', 'mood': 'sad'}

Dataclass 的优势:支持默认值

Dataclass 的一个实用优势是可以直接在字段定义上指定默认值,这对于有些字段"可以不传"的场景非常方便:

from dataclasses import dataclass, field

@dataclass
class AgentDataclassState:
    # 必填字段,无默认值
    user_input: str

    # 有默认值的字段——如果初始化时没传,就用默认值
    messages: list = field(default_factory=list)  # 默认空列表
    iteration: int = 0                             # 默认 0
    is_done: bool = False                          # 默认 False
为什么列表默认值要用 field(default_factory=list)

这是 Python 的一个经典陷阱:不能用 messages: list = [],因为这个空列表是所有实例共享的同一个对象,会导致意外的状态污染。field(default_factory=list) 保证每个实例创建时都得到一个全新的独立列表。

TypedDict 本身没有内置的默认值机制(你需要在初始化时手动提供所有字段,或者配合 total=False 使字段变为可选)。因此,如果你的 State 有许多可选字段或带默认值的字段,Dataclass 比 TypedDict 更合适。

5 Pydantic BaseModel:运行时强校验

TypedDict/Dataclass 的根本缺陷

TypedDict 和 Dataclass 都只提供类型提示,运行时完全不做检查。这意味着即使你写了 mood: Literal["happy", "sad"],下面这段代码也不会报错:

# Dataclass:即使 mood 不在 Literal 允许范围内,Python 也不会报错!
bad_instance = DataclassState(name="Lance", mood="mad")
print(bad_instance.mood)  # 输出: mad — 没有任何错误

这在生产环境是个隐患:数据验证的失败会被悄悄忽略,错误的数据进入图的执行流程,最终在某个意想不到的地方引发难以追踪的 bug。

Pydantic:运行时校验的解决方案

Pydantic 是 Python 生态里最流行的数据验证库,LangChain 和 LangGraph 的内部大量使用它。它通过继承 BaseModel 来定义数据模型,并在实例化时立即执行类型校验:

from pydantic import BaseModel, field_validator, ValidationError

class PydanticState(BaseModel):
    name: str
    mood: str  # "happy" 或 "sad"

    # 自定义校验器:在实例化时自动执行
    @field_validator("mood")
    @classmethod
    def validate_mood(cls, value):
        if value not in ["happy", "sad"]:
            raise ValueError("Each mood must be either 'happy' or 'sad'")
        return value

当你尝试用无效值创建实例时,Pydantic 会立刻抛出 ValidationError

try:
    # mood="mad" 不在允许值范围内,立刻报错!
    bad_state = PydanticState(name="John Doe", mood="mad")
except ValidationError as e:
    print("Validation Error:", e)

# 输出:
# Validation Error: 1 validation error for PydanticState
# mood
#   Input should be 'happy' or 'sad' [type=literal_error, input_value='mad', ...]

@field_validator 的运作原理

@field_validator 装饰器注册了一个类方法,在 Pydantic 构建实例时自动调用:

在 LangGraph 中无缝使用 Pydantic State

PydanticState 传给 StateGraph,其余代码和 TypedDict/Dataclass 版本完全相同:

# 图的构建代码和之前完全一样,只是 Schema 换成了 PydanticState
builder = StateGraph(PydanticState)
builder.add_node("node_1", node_1_dataclass)  # 属性访问方式,同 Dataclass
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_edge(START, "node_1")
builder.add_conditional_edges("node_1", decide_mood)
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
graph = builder.compile()

# 调用:传入 PydanticState 实例
result = graph.invoke(PydanticState(name="Lance", mood="sad"))
# 输出:{'name': 'Lance is ... ', 'mood': 'sad'}

Pydantic 更多强大特性

Pydantic 不止于基本类型校验。你还可以用它实现:

from pydantic import BaseModel, Field, validator
from typing import Optional, List

class RobustAgentState(BaseModel):
    # 带默认值的字段
    user_input: str
    messages: List[str] = []  # Pydantic 对列表默认值做了安全处理

    # 可选字段(None 是合法值)
    search_result: Optional[str] = None

    # 用 Field 设置取值范围约束
    max_iterations: int = Field(default=5, ge=1, le=20)  # 1 <= value <= 20

    # 字符串长度约束
    session_id: str = Field(min_length=1, max_length=64)
Pydantic 与 LangChain 的深度集成

LangChain 本身大量使用 Pydantic:BaseChatModelBaseMessageBaseTool 等核心类都继承自 Pydantic BaseModel。如果你的 State 里需要存放 LangChain 的消息对象(HumanMessageAIMessage),用 Pydantic State 可以获得更好的类型安全性和 IDE 智能提示。

6 三种方式横向对比与选型建议

功能对比表

特性 TypedDict Dataclass Pydantic BaseModel
所属库 Python 标准库(typing) Python 标准库(dataclasses) 第三方库(pydantic)
字段访问方式 state["name"] state.name state.name
初始化方式 {"name": "Lance"} MyState(name="Lance") MyState(name="Lance")
类型提示支持 有(仅静态检查) 有(仅静态检查) 有(静态 + 运行时)
运行时类型校验 有(实例化时校验)
默认值支持 有限(需 total=False) 原生支持 原生支持
自定义校验逻辑 手动实现 __post_init__ @field_validator 原生支持
序列化支持 就是字典,天然可序列化 需要 dataclasses.asdict() .model_dump() / .model_json()
与 LangChain 集成 最好(LangChain 本身用 Pydantic)
学习成本 最低 中(需学 Pydantic API)
适用场景 原型、教学、简单 Agent 有默认值、属性风格偏好 生产 Agent、需要数据校验

如何选择?决策树

最常见的误区

不要因为"Pydantic 有校验所以一定最好"就在所有场景都用 Pydantic。在教学代码和快速迭代阶段,TypedDict 的轻量性是真实优势——更少的代码噪音,更快的原型验证。选择合适的工具,而不是最复杂的工具。

7 State 在图中的流转机制详解

字段是独立存储的

LangGraph 有一个非常重要的内部机制:State 的每个字段是独立存储的,而不是整个 State 对象作为一个整体被替换。

这解释了一个看起来奇怪的现象:即使 State 是 Dataclass 或 Pydantic 模型,节点返回的仍然是字典——

# State 是 DataclassState,但节点依然返回字典
def node_2(state):
    return {"mood": "happy"}  # 只返回要改变的字段
    # 不需要返回完整的 DataclassState 对象

LangGraph 收到这个字典 {"mood": "happy"} 后,会把 mood 字段单独更新到当前 State 里,其他字段(比如 name)保持不变

默认更新行为:覆盖(Overwrite)

默认情况下,当节点返回一个字段的新值时,这个新值会直接覆盖旧值:

name: "Lance"
mood: "sad"
node_1 执行前
name: "Lance is ... "
mood: "sad"
node_1 执行后(name 被覆盖,mood 不变)
name: "Lance is ... "
mood: "sad"
node_3 执行前
name: "Lance is ... "
mood: "sad"
node_3 执行后(mood 被覆盖但值相同,name 不变)

需要"追加"而不是"覆盖"时:Reducer

对于 messages(消息历史)这样的字段,你希望每次节点执行时追加新消息到列表里,而不是覆盖掉整个列表。这就需要用到 Reducer——通过 Annotated 语法附加到字段上的函数:

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages

class ChatState(TypedDict):
    # Annotated[list, add_messages] 表示:
    # - 这个字段是 list 类型
    # - 更新策略是 add_messages(追加而非覆盖)
    messages: Annotated[list, add_messages]

    # 普通字段,默认覆盖
    current_step: str
messages: [HumanMsg("你好")]
LLM 节点执行前
messages: [HumanMsg("你好"),
AIMsg("你好!")]
LLM 节点执行后(追加,不覆盖)
Reducer 在 Module 2 后续课程中会深入讲解

add_messages 是 LangGraph 内置的 Reducer,专门用于消息列表的累积。它还会智能处理消息 ID 去重。Reducer 的完整机制(包括如何自定义 Reducer)会在 Module 2 的 Lesson 4:State Reducers 中详细讲解。这里先建立基本印象即可。

State 的完整生命周期

阶段发生了什么开发者需要做什么
Schema 定义 声明图里有哪些字段、类型、更新策略 定义 TypedDict / Dataclass / Pydantic 类
图初始化 LangGraph 读取 Schema,了解 State 的结构 把 Schema 类传给 StateGraph(Schema)
invoke() 调用 传入初始 State,开始执行 传入字典或 Schema 实例
节点执行 节点函数接收当前 State,返回更新字典 写节点函数,读 state、返回变更字典
State 合并 LangGraph 把节点返回的字典合并进当前 State(默认覆盖,或用 Reducer) 了解覆盖 vs 追加的区别
到达 END invoke() 返回最终 State(字典格式) 从返回的字典里取需要的字段

8 总结:State Schema 设计的最佳实践

关键结论一览

TypedDict 要点
  • • 用字典下标 state["field"] 访问
  • • 类型标注是提示,运行时不校验
  • • invoke() 时传字典 {...}
  • • 最轻量,最常用于教学/原型
Dataclass 要点
  • • 用属性 state.field 访问
  • • 类型标注是提示,运行时不校验
  • • invoke() 时传实例 MyState(...)
  • • 原生支持默认值 field(default_factory=...)
Pydantic 要点
  • • 用属性 state.field 访问
  • • 实例化时强校验,不合法立刻报错
  • @field_validator 支持自定义校验
  • • 生产 Agent 首选,与 LangChain 深度兼容
共同规则
  • • 节点始终返回字典,不管 Schema 是什么类型
  • • 字段默认更新策略是覆盖
  • • 追加用 Annotated[list, add_messages]
  • • State 每个字段独立存储和更新

设计 State Schema 的实用建议

这一课在 Module 2 中的位置

Lesson 1(本课) State Schema 的三种定义方式(TypedDict / Dataclass / Pydantic)
Lesson 2 State Schema 进阶:多 Schema 和私有状态(Multiple Schemas)
Lesson 3 过滤与裁剪消息(Trim / Filter Messages)
Lesson 4 State Reducers:控制字段的更新策略(覆盖 vs 追加 vs 自定义)
Lesson 5 聊天机器人:对话摘要(Chatbot Summarization)
Lesson 6 外部记忆:Chatbot with External Memory
最终结论

State Schema 不只是"数据结构定义",它是整个图的数据契约。TypedDict 给你最低摩擦的开发体验,Dataclass 给你更好的默认值管理,Pydantic 给你生产级的数据安全保障。理解三者的差异,你就能根据场景做出正确的选型决策——这是构建可靠 LangGraph Agent 的第一步,也是 Module 2 的基础。