第2章 状态 State 详解

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_resulta_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()

解析producer1producer2 并行执行,都写了 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 = 18operator.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_messagesfrom 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 把状态存起来,实现会话记忆与故障恢复。

相关推荐
派大_星1 小时前
基于MediaPipe和传统机器学习的手势识别
python
ZDN_is_beauty1 小时前
綦江烟草部署(在wsl2里部署)
人工智能·python
小小龙学IT1 小时前
astAPI 异步 Web 框架深度解析
python
PiaoKe___3 小时前
云手机原理与 Python 自动化实战:ADB 批量控制、任务调度与落地建议
服务器·arm开发·python·自动化
Gigavision9 小时前
基于BUAA-MIHR数据集的噪声解耦对比学习算法
人工智能·python·深度学习·算法
IPdodo_10 小时前
跨境 API 调用不稳定怎么办:出口、超时重试与链路监控的实践
网络·python·网络协议
Madison-No710 小时前
搭建项目测试环境
linux·运维·服务器·python
计算机编程-吉哥10 小时前
YOLO26 vs YOLO11 vs YOLOv8:深度学习咖啡果实成熟度分割系统【计算机毕业设计选题推荐】
人工智能·python·深度学习·yolo·django·毕业设计
半个落月11 小时前
LangChain.js Agent Memory 实战(下):用 Milvus 构建可检索的长期记忆
javascript·langchain