State 是 LangGraph 中最重要的概念:它代表整个图的结果目标,以及图当中节点需要修改的数据目标。用 LangGraph 构建应用时,第一步就是定义 State。
本章讲清四件事:State 的三种定义方式、输入/输出/内部三层状态的隔离、节点间的私有状态、以及决定「多次写入如何合并」的 Reducer 机制。
2.1 状态的定义
State Schema(状态模式)可以通过三种方式定义:TypedDict、Pydantic BaseModel 和 Dataclass。
方式一:TypedDict(推荐)
TypedDict 是 Python 提供的一种类型提示工具,用于为字典(Dict)的键和值指定精确的类型信息。状态类继承 TypedDict,定义键的类型和 reducer 函数即可:
python
from typing import TypedDict
class MyStateFull(TypedDict):
rag_result: str
web_search_result: str
final_answer: str
query: str
a_new_key: str
解析 :TypedDict 定义的并不是一个真正的类,而是一个「带类型注解的字典模板」。运行时状态就是一个普通 dict,只是静态类型检查器(mypy、IDE)能据此提示你键名和类型。它的优势是零运行时开销、与 LangGraph 的状态合并机制(本质是 dict 操作)天然契合,所以是官方推荐的首选。
方式二:Pydantic BaseModel
python
from pydantic import BaseModel
class MyStateFull(BaseModel):
rag_result: str
web_search_result: str
final_answer: str
query: str
a_new_key: str
解析 :Pydantic 提供运行时数据校验 ------invoke 时传入了类型不匹配的字段(比如该传 str 却传了 int),会在图的入口直接抛出 ValidationError,而不是等某个节点深处报出难查的 AttributeError;同时它也支持静态类型检查工具进行类型推导。代价是每次状态更新都有一次校验开销。经验法则:入口数据来源不可控(用户输入、外部 API)时用 Pydantic 把关,内部可控流程用 TypedDict 提速。
方式三:Dataclass
python
from dataclasses import dataclass
@dataclass
class MyStateFullTwo():
rag_result: str
web_search_result: str
final_answer: str
query: str
a_new_key: str
解析 :@dataclass 是 Python 标准库装饰器,自动生成 __init__、__repr__、__eq__ 等特殊方法,适合主要用作数据容器的类。它介于前两者之间:有真正的类结构,但无运行时校验。
原稿勘误 :原 Word 文档中该示例的最后一行
a_new_key:str缩进错误(顶到了行首),实际运行会抛IndentationError或被解析成模块级语句,上文已修正。
三种方式怎么选
| 方式 | 运行时校验 | 性能开销 | 适用 |
|---|---|---|---|
| TypedDict | 无(仅静态提示) | 最低 | 绝大多数场景的默认选择 |
| Pydantic BaseModel | 有 | 中 | 入口数据需要严格把关 |
| Dataclass | 无 | 低 | 偏好类语法的场景 |
2.2 输入输出数据隔离
在 LangGraph 当中,可以精细管理输入到图中的状态键有哪些,以及输出的状态键有哪些。这是通过初始化 StateGraph 时,分别指定三个参数实现的:
python
StateGraph(
state_schema=..., # 图的完整内部状态
input_schema=..., # 图接受什么输入
output_schema=..., # 图返回什么输出
)
三者关系如下:
input_schema state_schema output_schema
用户输入 ──────────────> ┌─────────────────────────────┐ ──────────────> 返回结果
(只允许这些键) │ 图的完整内部状态空间 │ (只返回这些键)
│ 所有节点可读写的全部字段 │
└─────────────────────────────┘
state_schema
这是图的完整内部状态 ,包含了所有节点可能读写的字段,必须指定,不能为空。特点:
- 是图的「全局状态空间」;
- 所有节点都可以访问和写入这个 schema 中的任何字段。
input_schema
定义图接受什么输入,是 state_schema 的子集。特点:
- 可选参数,如果不指定,默认等于 state_schema;
- 限制图的输入接口,只能传入这些字段;
- 必须是 state_schema 的子集或相等。
output_schema
定义图返回什么输出,同样是 state_schema 的子集。特点:
- 可选参数,不指定时默认等于 state_schema;
- 限制图的输出接口,只返回这些字段。
为什么要隔离:图的内部状态往往包含大量中间产物(检索原文、重写后的 query、调试标记......),这些不应暴露给调用方。隔离后,图对外呈现一个干净的小接口,内部实现可以随意重构------这就是「接口与实现分离」在 Agent 编排里的落地。
代码示例
python
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
# ★ 关键:三个 schema 同时指定
graph = StateGraph(
state_schema=MyStateFull,
input_schema=InputSchema,
output_schema=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"})
print('最终结果为', res)
逐段解析:
InputSchema只声明了query一个字段。调用invoke({"query": ...})时,任何试图传入rag_result等其他字段的输入都会被拒绝(抛出校验错误);- 节点函数的参数类型仍然标注为
MyStateFull------节点内部读写的是完整内部状态,不受 input/output 隔离影响; final_answer_node里的print(state)会看到包含所有内部字段的完整状态;- 最终
invoke的返回值只剩{"final_answer": ...},rag_result、a_new_key等中间字段被 output_schema 过滤掉了。
节点间数据隔离(私有状态)
LangGraph 当中节点所接收的状态,还可以是一个非全局状态的私有状态:
python
from typing import TypedDict
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}
# ★ final_answer_node 的入参类型是 SearchState,而非全局 MyState
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)
解析 :final_answer_node 声明入参为 SearchState,LangGraph 会据此把传给该节点的状态投影 成只含 rag_result / web_search_result 的子集。它的实际价值在于多团队协作与防御式编程:某节点声明了自己关心的键,别的节点新增字段不会意外影响它;也方便在 IDE 里获得精确的类型提示。注意私有状态只是「视图裁剪」,节点返回的增量仍然合并进全局状态。
2.3 Reducer 函数
Reducer 用于进行当前增量状态(节点输出的状态)和全局状态的合并。State 中的每个键都有其独立的 reducer 函数:每个 node 返回值中的每个 key,与全局 state_schema 中对应的 key 进行合并更新,具体更新逻辑取决于该 key 指定的 reducer 函数。
一句话:reducer 决定了「同一个键被多个节点写入时,谁说了算」。
Reducer 常用的有以下几种:
- 默认行为:未指定 Reducer 时,使用覆盖更新(后者直接覆盖前者);
- 内置 reducer 函数 :例如
langgraph.graph.message当中的add_messages函数; - 自定义 Reducer:支持用户自定义合并逻辑。
默认 Reducer:覆盖更新
python
"""
LangGraph Reducer 演示 - 默认 Reducer(覆盖更新)
"""
from typing import List
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
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()
解析 :初始状态 {"foo": 1, "bar": ["hi"]},node1 返回 {"foo": 2} 后状态变为 {"foo": 2, "bar": ["hi"]}------只覆盖被写的键,其他键原样保留。这就是「增量更新」的默认语义:返回什么,对应键就被替换成什么。本例两个节点各写各的键,互不干扰,所以覆盖语义工作正常。
内置 Reducer:operator.add
Annotated[类型, reducer] 是给键挂 reducer 的标准语法:Annotated 把「值类型」和「合并函数」一起塞进类型注解里,LangGraph 在编译期读取注解,为该键注册指定的合并函数。
列表追加
python
"""
LangGraph Reducer 演示 - operator.add Reducer(列表追加)
"""
import operator
from typing import Annotated, List
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class ListAddState(TypedDict):
# ★ 给 data 键挂上 operator.add 作为 reducer
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()
解析 :producer1 和 producer2 并行执行,都写了 data 键 。如果用默认覆盖语义,后完成者会抹掉先完成者的结果(并行场景下结果还不确定)。挂上 operator.add 后,合并逻辑变成 旧值 + 增量,对列表而言就是拼接,最终结果为 [0, 1, 2, 3, 4]。这是并行节点写同一个键时的标准解法。
字符串连接
python
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!"}
初始 text="Say: ",两个并行节点各返回一段字符串,合并后为 "Say: Hello World!"。operator.add 对字符串就是 + 拼接。
数值累加
python
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}
初始 count=10,两个并行节点分别返回 5 和 3,最终 count = 10 + 5 + 3 = 18。operator.add 对数值就是算术加法------适合「计次」「累计分数」这类场景。
三个例子共用同一个 reducer(operator.add),行为差异完全来自操作数类型(list + list / str + str / int + int),这正是复用 Python 内置运算符的巧妙之处。
自定义 Reducer
自定义 Reducer 函数接收两个参数(旧值、增量),将其合并后返回一个结果:
python
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):
# 给 messages 键挂上自定义 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="你好")]})
逐段解析:
add_message(left, right):left是全局状态里该键的当前值 ,right是本节点返回的增量 。函数里打印两侧内容,运行时你能直观看到每次合并发生在哪个节点之后;最终返回left + right,即「历史消息 + 新消息」;messages: Annotated[List[BaseMessage], add_message]:把自定义函数注册为messages键的合并策略;- 执行流程:初始
[HumanMessage("你好")]→tool_node触发一次合并得到[Human, ToolMessage]→llm_node再触发一次得到[Human, Tool, AIMessage]。消息历史像滚雪球一样累积,这正是对话记忆的底层原理; - 实战中通常直接用官方内置的
add_messages(from langgraph.graph.message import add_messages),它除了拼接还处理消息 ID 去重与同 ID 覆盖,本例手写一个简化版是为了看清机制。
Reducer 速查
| 需求 | 写法 | 合并语义 |
|---|---|---|
| 每个键只被一个节点写 | 不指定 | 覆盖 |
| 并行节点结果都要保留(列表) | Annotated[List[X], operator.add] |
列表拼接 |
| 文本逐步累积 | Annotated[str, operator.add] |
字符串拼接 |
| 计数 / 累加 | Annotated[int, operator.add] |
数值相加 |
| 对话记忆 | Annotated[List[BaseMessage], add_messages] |
消息拼接 + 去重 |
| 特殊合并规则 | 自定义 (old, new) -> merged |
你说了算 |
2.4 本章小结
- State 定义三选一:TypedDict(默认推荐)、Pydantic(要校验)、Dataclass(爱类语法);
state_schema是全局状态空间,input_schema/output_schema收窄对外的输入输出接口,节点还可以用私有状态进一步裁剪自己看到的视图;- 节点返回增量,由各键的 reducer 与全局状态合并;
- 默认 reducer 是覆盖;并行节点写同一个键时,用
Annotated[类型, operator.add]或自定义 reducer 收集全部结果; add_messages是构建对话记忆的标准 reducer。
下一章讲状态的「时间维度」:如何用 Checkpointer 把状态存起来,实现会话记忆与故障恢复。