TypedDict、Dataclass、Pydantic——三种方式定义图的"数据契约"。
从类型提示到运行时校验,选择合适的状态模式是构建健壮 Agent 的第一步。
在 Module 1 中,你已经学会了 LangGraph 的核心骨架:State(状态)、Node(节点)、Edge(边)、StateGraph(图)。你知道了如何用 TypedDict 定义一个简单的 State,并让节点之间通过它传递数据。
但 Module 1 只是冰山一角。现实世界里的 Agent 需要处理更复杂的状态结构:包含消息历史的对话上下文、需要校验的业务字段、带有默认值的可选属性……这些都需要你对 State Schema 有更深的理解。
State(状态定义):如何用不同的 Python 数据类型定义图的状态模式,包括 TypedDict、Dataclass、Pydantic;以及如何用 Reducer 控制状态更新行为。
Memory(记忆):如何让 Agent 记住跨轮次的对话内容,包括 In-memory checkpointer 和外部 Memory Store。
本讲解文档聚焦于 Lesson 1:State Schema,也就是 Module 2 的起点——搞清楚"状态"究竟可以用几种方式定义,各自有什么优缺点,实际项目里该怎么选。
State Schema 是图的"数据契约"(Data Contract)。整张图里所有的节点都依赖这个契约来读写数据。选错了 Schema 方式会带来两类典型问题:
mood 字段设成了 "angry",但类型提示写的是只能 "happy" 或 "sad",如果没有运行时校验,这个 bug 会悄悄地带入生产环境。理解三种 Schema 定义方式,才能在合适的场景选用合适的工具,写出可维护、易调试的 LangGraph 代码。
当你创建一个 StateGraph 时,第一步就是传入一个 State Schema 类:
# 这里的 MyState 就是 Schema——它定义了这张图能存放什么数据
builder = StateGraph(MyState)
这个 Schema 做了三件事:
str、int、list……)LangGraph 的每个 Node 是相互独立的函数,它们之间不能直接传参、也不能共享变量。State 就是它们唯一的通信介质。Node A 把计算结果写入 State,Node B 从 State 里读出来继续处理。Schema 则是这块共享内存的"格式说明书"。
LangGraph 在这里非常灵活——它不强制你用某一种特定的 Python 数据类型。只要是 Python 能识别的结构化类型,LangGraph 基本都能接受:
接下来我们逐一深入讲解这三种方式。
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 类型提示来做约束:
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 不管。
把 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'}
TypedDict 是 Python 标准库的一部分,无需额外安装。它访问方式直观(字典下标),和大多数 Python 开发者的习惯一致。对于简单到中等复杂度的 State,TypedDict 是最低摩擦的选择——这就是为什么你在 LangGraph 官方文档和几乎所有教程里都会看到它。
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 有两个显著不同:
state["name"],Dataclass 用属性访问 state.name{"name": "Lance"},Dataclass 用构造函数 DataclassState(name="Lance", mood="sad")注意 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 就能正确处理。
# 构建图(和 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 的一个实用优势是可以直接在字段定义上指定默认值,这对于有些字段"可以不传"的场景非常方便:
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
这是 Python 的一个经典陷阱:不能用 messages: list = [],因为这个空列表是所有实例共享的同一个对象,会导致意外的状态污染。field(default_factory=list) 保证每个实例创建时都得到一个全新的独立列表。
TypedDict 本身没有内置的默认值机制(你需要在初始化时手动提供所有字段,或者配合 total=False 使字段变为可选)。因此,如果你的 State 有许多可选字段或带默认值的字段,Dataclass 比 TypedDict 更合适。
TypedDict 和 Dataclass 都只提供类型提示,运行时完全不做检查。这意味着即使你写了 mood: Literal["happy", "sad"],下面这段代码也不会报错:
# Dataclass:即使 mood 不在 Literal 允许范围内,Python 也不会报错!
bad_instance = DataclassState(name="Lance", mood="mad")
print(bad_instance.mood) # 输出: mad — 没有任何错误
这在生产环境是个隐患:数据验证的失败会被悄悄忽略,错误的数据进入图的执行流程,最终在某个意想不到的地方引发难以追踪的 bug。
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 装饰器注册了一个类方法,在 Pydantic 构建实例时自动调用:
@field_validator("mood"):指定这个校验函数作用于 mood 字段@classmethod:Pydantic 要求 field_validator 是类方法(cls 是类本身,value 是字段的值)value(你也可以在这里做数据清洗,比如 return value.strip())ValueError,Pydantic 会将其包装成 ValidationError把 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 不止于基本类型校验。你还可以用它实现:
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)
LangChain 本身大量使用 Pydantic:BaseChatModel、BaseMessage、BaseTool 等核心类都继承自 Pydantic BaseModel。如果你的 State 里需要存放 LangChain 的消息对象(HumanMessage、AIMessage),用 Pydantic State 可以获得更好的类型安全性和 IDE 智能提示。
| 特性 | 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、需要数据校验 |
选 TypedDict。零安装成本,字典风格直观,LangGraph 官方示例都用它。
选 Dataclass。field(default_factory=...) 比 TypedDict 的 total=False 更优雅,属性访问也更符合 OOP 风格。
选 Pydantic BaseModel。实例化时立刻校验,不合法数据不会进入图的执行流程,减少"数据污染"导致的难查 bug。
三种方式都可以,但 Pydantic 与 LangChain 的集成最自然,因为 LangChain 的消息类本身就是 Pydantic 模型。
不要因为"Pydantic 有校验所以一定最好"就在所有场景都用 Pydantic。在教学代码和快速迭代阶段,TypedDict 的轻量性是真实优势——更少的代码噪音,更快的原型验证。选择合适的工具,而不是最复杂的工具。
LangGraph 有一个非常重要的内部机制:State 的每个字段是独立存储的,而不是整个 State 对象作为一个整体被替换。
这解释了一个看起来奇怪的现象:即使 State 是 Dataclass 或 Pydantic 模型,节点返回的仍然是字典——
# State 是 DataclassState,但节点依然返回字典
def node_2(state):
return {"mood": "happy"} # 只返回要改变的字段
# 不需要返回完整的 DataclassState 对象
LangGraph 收到这个字典 {"mood": "happy"} 后,会把 mood 字段单独更新到当前 State 里,其他字段(比如 name)保持不变。
默认情况下,当节点返回一个字段的新值时,这个新值会直接覆盖旧值:
对于 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
add_messages 是 LangGraph 内置的 Reducer,专门用于消息列表的累积。它还会智能处理消息 ID 去重。Reducer 的完整机制(包括如何自定义 Reducer)会在 Module 2 的 Lesson 4:State Reducers 中详细讲解。这里先建立基本印象即可。
| 阶段 | 发生了什么 | 开发者需要做什么 |
|---|---|---|
| Schema 定义 | 声明图里有哪些字段、类型、更新策略 | 定义 TypedDict / Dataclass / Pydantic 类 |
| 图初始化 | LangGraph 读取 Schema,了解 State 的结构 | 把 Schema 类传给 StateGraph(Schema) |
| invoke() 调用 | 传入初始 State,开始执行 | 传入字典或 Schema 实例 |
| 节点执行 | 节点函数接收当前 State,返回更新字典 | 写节点函数,读 state、返回变更字典 |
| State 合并 | LangGraph 把节点返回的字典合并进当前 State(默认覆盖,或用 Reducer) | 了解覆盖 vs 追加的区别 |
| 到达 END | invoke() 返回最终 State(字典格式) | 从返回的字典里取需要的字段 |
state["field"] 访问{...}state.field 访问MyState(...)field(default_factory=...)state.field 访问@field_validator 支持自定义校验Annotated[list, add_messages]在定义 Schema 前,先列一张表:哪个节点会读哪些字段?哪个节点会写哪些字段?这张表就是你的 Schema 的雏形。
用 user_question 而不是 q;用 search_results 而不是 results。State 是所有节点的共享契约,模糊的命名会让每个节点的开发者都感到困惑。
原型阶段用 TypedDict 快速迭代,Schema 结构稳定后再迁移到 Pydantic 增加运行时保障。两者的迁移成本不高——字段定义基本可以复用,主要是访问方式从字典下标改为属性。
State 是"节点间共享的数据",不是"所有数据"。如果某个数据只在某个节点内部使用,就只是局部变量,不需要进 State。保持 State 精简,只存放跨节点通信必要的字段。
任何需要"累积"而不是"覆盖"的字段都要用 Annotated[..., add_messages] 或自定义 Reducer。最典型的就是对话的 messages 列表——每轮对话要追加,不能覆盖。
State Schema 不只是"数据结构定义",它是整个图的数据契约。TypedDict 给你最低摩擦的开发体验,Dataclass 给你更好的默认值管理,Pydantic 给你生产级的数据安全保障。理解三者的差异,你就能根据场景做出正确的选型决策——这是构建可靠 LangGraph Agent 的第一步,也是 Module 2 的基础。