LangGraph == 图的状态(State)管理 (中)

3.3. 节点中访问State

3.3.1. 图节点中读取State

在 LangGraph 中,节点本质上是一个可调用对象,通常定义为普通 Python 函数。节点函数被执行时,LangGraph 会自动将当前图运行到该节点时的 State 传入节点函数。

节点函数的第一个参数通常是当前运行图的状态对象,也就是 State。

复制代码
def node(state: StateSchema):

其中,state 表示当前节点执行时可以访问到的全局状态快照。节点可以通过读取 state 中的字段获取上游节点写入的数据,并基于这些数据完成当前节点的业务逻辑。

示例代码如下:

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

# 定义状态
class OverAllState(TypedDict):
    logs:  Annotated[list[str], add]
    current_id: str

# 定义节点
def node_1(state: OverAllState)->dict:
 for k, v in state.items():
     print(f"k={k}: v={v}")
 return  {
     # 保留当前状态的logs add
     "logs": ["node_1运行"],
     #没有配置归于 直接覆盖当前状态
     "current_id": "node_1"
 }

# 定义状态图
builder = StateGraph(state_schema=OverAllState)
# 添加节点到状态图
builder.add_node("node_1", node_1)
# 添加边到状态图
builder.add_edge(START, "node_1")
builder.add_edge("node_1", END)
# 编译状态图
graph = builder.compile()
# 执行状态图
result = graph.invoke({"logs": ["START"], "current_id": "start"})


# 打印执行结果
print(result)

输出如下

复制代码
k=logs: v=['START']
k=current_id: v=start
{'logs': ['START', 'node_1运行'], 'current_id': 'node_1'}

3.3.2. 图节点更新State

在 LangGraph 中,节点函数通常不需要返回更新后的完整状态,只需要返回本节点对状态的局部更新。

也就是说,节点的返回值可以只包含需要修改的状态字段。

  • 对于节点没有返回的字段,LangGraph 会保留其原有状态值;

  • 对于节点返回的字段,LangGraph 会根据该字段是否配置了 Reducer 来决定如何合并更新值。

    • 如果字段配置了 Reducer,则使用对应的 Reducer 函数将旧值和新值合并;
    • 如果字段没有配置 Reducer,则按照默认规则使用节点返回的新值覆盖原值。

LangGraph运行时会按照状态字段的Reducer函数将其与当前的最新状态合并。

示例代码如下:

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

class OverAllState(TypedDict):
    logs: Annotated[list[str], add]
    id: str

def node_a(state: OverAllState):
    for k, v in state.items():
        print(f"k: {k}, v: {v}")
    return {
        "logs": ["node_a 更新状态"]
    }

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

graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})
print('=' * 30, '-> result <-', '=' * 30)
print(result)

输出如下

复制代码
k: logs, v: ['START']
k: id, v: start
============================== -> result <- ==============================
{'logs': ['START', 'node_a 更新状态'], 'id': 'start'}

在上述示例中:

  • logs 字段通过 Annotated[list[str], add] 绑定了 Reducer 函数 operator.add,因此 LangGraph 会将原有的 logs 值和 node_a 返回的新 logs 值进行列表拼接:

    ["START"] + ["node_a 更新状态"]

最终得到:

复制代码
["START", "node_a 更新状态"]
  • id 字段没有出现在 node_a 的返回值中,因此该字段不会被更新。图运行结束后,输出状态中的 id 仍然保持输入时的值:

    "id": "start"

因此,LangGraph 节点更新 State 的核心规则可以概括为:

节点只返回需要更新的字段;未返回的字段保持不变;返回的字段根据是否配置 Reducer 决定是合并还是覆盖。

3.3.3. Overwrite绕过Reducer

在前面的示例中,如果某个状态字段定义了 Reducer,那么节点返回该字段的更新值时,LangGraph 默认会通过对应的 Reducer 将新值与已有状态值进行合并。

在某些场景下,我们可能并不希望继续执行 Reducer 的聚合逻辑,而是希望本次更新直接覆盖旧值。这时可以使用 Overwrite。

Overwrite 的作用是:告诉 LangGraph 本次状态更新不走该字段原本定义的 Reducer,而是直接用新值覆盖状态中的旧值。

需要注意的是,Overwrite 只影响当前这一次更新,并不会修改状态字段本身的 Reducer 定义。后续节点如果继续正常返回该字段的更新值,仍然会按照原来的 Reducer 逻辑进行合并。

示例代码如下:

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

class OverAllState(TypedDict):
    logs: Annotated[list[str], add]
    id: str

def node_a(state: OverAllState):
    return {
        "logs": ["node_a"],
        "id": "node_a"
    }

def node_b(state: OverAllState):
    return {
        "logs": Overwrite(["node_b"]),
        "id": "node_b"
    }

def node_c(state: OverAllState):
    return {
        "logs": ["node_c"],
        "id": "node_c"
    }

builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", "node_c")
builder.add_edge("node_c", END)

graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})
print('=' * 30, '-> result <-', '=' * 30)
print(result)

输出如下

复制代码
============================== -> result <- ==============================
{'logs': ['node_b', 'node_c'], 'id': 'node_c'}
  • logs 字段绑定了 operator.add() 函数
    • 如果没有 Overwrite,则最终输出的 logs 的值应为 ['START', 'node_a', 'node_b', 'node_c']
    • node_b 返回更新时,用 Overwrite 包裹了 logs 字段的值,那么当前状态的 logs 会被 ["node_b"] 覆盖,因此最终输出的 logs 字段值变成了 ['node_b', 'node_c']
  • id 字段按照默认行为,保留最后一次更新的值。

3.4. Multi Schema用法

3.4.1. 状态类型

LangGraph 支持在一个图中使用多个状态 Schema,用于区分图的外部输入、外部输出、内部共享状态以及节点间的临时状态。

常见状态类型可以分为以下几类:

  • 全局状态 / 内部状态:图内部主要使用的状态,创建 StateGraph 时传递给 state_schema 参数。它通常包含图运行过程中需要读写的大部分字段。
  • 输入状态:图对外接收输入时使用的状态,创建 StateGraph 时传递给 input_schema 参数。它用于约束调用图时允许传入哪些字段。
  • 输出状态:图最终对外返回结果时使用的状态,创建 StateGraph 时传递给 output_schema 参数。它用于约束图运行结束后只返回哪些字段。
  • 私有状态:图内部节点之间传递的临时状态,通常不作为图的输入,也不作为图的最终输出。它可以通过节点函数的入参类型注解声明,并在节点返回值中写入。

需要注意,输入状态和输出状态主要面向图的边界,即"图如何接收外部输入"和"图如何返回外部结果";而全局状态和私有状态主要面向图内部节点之间的数据传递。

3.4.2. 状态之间的关系

3.4.2.1. 设计规范

本节主要说明 LangGraph 状态设计中的规范。以下规则属于工程上的最佳实践,违反这些规范未必一定导致程序报错,但容易降低代码的可读性和可维护性。

输入状态和输出状态通常应是全局状态的子集 输入状态描述图对外需要接收的数据,输出状态描述图最终需要返回的数据。通常情况下,它们都应该是全局状态的一部分。 如:

复制代码
class InputState(TypedDict):
    username: str

class OutputState(TypedDict):
    graph_output: str

class OverAllState(TypedDict):
    username: str
    nickname: str
    graph_output: str

其中,InputState 和 OutputState 的所有字段均存在于 OverAllState 中。

私有状态和全局状态应尽量避免字段重名 私有状态的定位是图内部某些节点之间传递的临时字段。如果私有状态字段和全局状态字段重名,虽然某些情况下程序仍然可以运行,但容易让人误以为该字段是全局共享字段,从而造成理解混乱。 因此,推荐让私有状态字段和全局状态字段保持清晰边界。

节点函数应明确声明入参状态类型和返回状态类型 节点函数的第一个参数通常是当前节点可读取的状态。通过类型注解声明该参数,可以明确表达该节点需要读取哪些字段。 同时,给节点函数声明返回状态类型,也可以帮助阅读者理解该节点会更新哪些字段。 例如:

复制代码
def node_1(state: InputState) -> OverAllState:
    return {
        "nickname": "Dear " + state["username"]
    }

节点函数中不应该访问入参状态类型中不存在的字段 节点实际接收到的状态会按照其入参类型进行裁剪。因此,如果节点入参声明为 InputState,就不应该在节点内部访问 InputState 中不存在的字段。 例如:

复制代码
def node_1(state: InputState) -> OverAllState:
    return {
        "nickname": state["username"]
    }

如果在该函数中访问:

复制代码
state["nickname"]

而 nickname 不属于 InputState,运行时就可能抛出 KeyError。

节点函数返回的字典应尽量和返回类型注解保持一致 从 Python 类型注解的角度看,函数返回类型只是静态提示,运行时不会自动强制校验。 从 LangGraph 的运行机制看,节点返回的是对状态的部分更新,不是完整状态。只要返回字段已经被图记录为可用状态字段,LangGraph 就可以将其作为状态更新处理。 不过,从工程规范上讲,节点返回字典中的字段最好和函数返回类型注解保持一致,这样更利于阅读、调试和维护。

3.4.2.2. 源码层面的约束

本节从底层机制角度说明 LangGraph 如何记录、裁剪和更新状态。

3.4.2.2.1. 状态的记录

LangGraph 的状态并不是简单保存在一个普通字典中,而是会被拆分成多个可读写的状态字段。每个状态字段在底层通常对应一个 Channel。

这些状态字段会在不同阶段被记录到状态图中。

  • StateGraph 记录状态字段的核心方法是 _add_schema() _add_schema() 会解析传入的状态 Schema,并将其中声明的字段记录到图中,使这些字段成为图运行时可以读写的状态字段。

  • 创建 StateGraph 时,会记录 state_schema、input_schema 和 output_schema 中的字段 当创建状态图时:

    builder = StateGraph(
    OverAllState,
    input_schema=InputState,
    output_schema=OutputState
    )

LangGraph 会解析这些 Schema,并将其中涉及的字段加入图的状态管理体系。

  • 调用 add_node() 添加节点时,也可能记录节点入参声明的状态 Schema 当添加节点时,LangGraph 会根据节点函数第一个参数的类型注解推断该节点的输入状态类型。 如果这个输入状态类型之前没有被图记录过,LangGraph 也会通过 _add_schema() 将其加入图中。 这也是私有状态能够生效的原因。 例如:

    class PrivateState(TypedDict):
    greeting: str

    def node_3(state: PrivateState) -> OutputState:
    return {
    "graph_output": state["greeting"]
    }

  1. 当 node_3 被添加到图中时,PrivateState 中的 greeting 字段会被记录到图中,从而成为图内部可以传递的状态字段。

  2. 总结

    • 全局状态、输入状态、输出状态通常在创建 StateGraph 时被记录。
    • 私有状态通常在调用 add_node() 添加节点时,根据节点入参类型注解被记录。
    • 被记录后的状态字段,底层会成为图运行时可以读写的状态字段。
3.4.2.2.2. 状态的访问

调用图时,输入会按照 input_schema 进行约束 当调用图时:

复制代码
graph.invoke({"username": "小黄"})

如果创建图时声明了 input_schema,那么外部输入会按照 input_schema 进行约束。 如果没有声明 input_schema,则通常按照 state_schema 作为图的输入 Schema。 因此,input_schema 的作用不是"只让第一个节点可见",而是约束图的外部输入结构。 此处的约束是指:按照 schema 裁剪输入,只保留 schema 中出现的状态字段

节点接收到的状态会按照节点入参类型进行裁剪 每个节点能读取哪些字段,主要取决于该节点第一个参数的类型注解。 例如:

复制代码
def node_1(state: InputState) -> OverAllState:

此时,node_1 接收到的 state 会按照 InputState 进行裁剪。即使图的全局状态中还有其他字段,node_1 也不应该访问不属于 InputState 的字段。 如果访问了入参状态中不存在的字段,例如:

复制代码
state["nickname"]

就可能抛出:

复制代码
KeyError

节点返回的是状态更新,而不是完整状态 节点函数不需要返回完整状态,只需要返回本节点想要更新的字段。 例如:

复制代码
def node_1(state: InputState) -> OverAllState:
    return {
        "nickname": "Dear " + state["username"]
    }

这里虽然返回类型注解是 OverAllState,但函数实际只返回了 nickname 一个字段。这是允许的,因为 LangGraph 会把节点返回值视为对状态的部分更新。

节点返回值的应用主要由字段名称和图中已记录的状态字段决定 节点返回的字典会根据字段名称写入对应状态字段,并按照该字段的 Reducer 规则进行合并。 需要注意的是,函数返回类型注解主要用于表达代码意图,不是严格的运行时写入边界。 也就是说,如果某个字段已经被图记录为可用状态字段,那么节点即使没有在返回类型注解中声明该字段,也可能仍然可以返回并更新它。 不过,为了代码清晰,仍然推荐让节点的返回值和返回类型注解保持一致。

最终输出会按照 output_schema 进行裁剪 图运行完成后,最终返回给外部调用方的结果会按照 output_schema 进行裁剪。 因此,output_schema 的作用不是"只让最后一个节点可见",而是约束图最终对外暴露哪些字段。 例如,图内部状态中可能同时存在:

复制代码
username
nickname
greeting
graph_output

但如果 output_schema 只包含:

复制代码
graph_output

那么最终 graph.invoke() 的返回结果就只会包含 graph_output。

3.4.3. 案例

下面通过一个简单案例说明四类状态的定义和使用。

复制代码
from  typing import TypedDict

from langgraph.graph import StateGraph, START, END



#  输入状态
class InputState(TypedDict):
      username: str

#  输出状态
class OutputState(TypedDict):
      graph_output: str

#  全局状态
class OverAllState(TypedDict):
      username: str
      graph_output: str
      nickname: str

#  私有状态
class PrivateState(TypedDict):
      greeting: str


#  节点1 对用Start ==> InputState
#  修改的内容在全局状态中 ==> OverAllState
def node_1(state: InputState) -> OverAllState:
    # 向全局状态添加用户名
    return {
        "nickname" : "Dear "+ state["username"],
        "graph_output": "",
        "username": ""
    }

#  节点2 对接节点1 ==> OverAllState「因为node1返回的OverAllState」
#  使用的参数在全局状态中, 修改的内容在私有状态中 ==> PrivateState
def node_2(state: OverAllState) -> PrivateState:
     # 向私有状态添加问候语
     return {
        "greeting": "Hello, "+ state["nickname"]
     }

#  节点3 对接节点2 ==> PrivateState「因为node2返回的PrivateState」
#  推荐让节点返回值和返回类型注解一致
def node_3(state: PrivateState) -> OutputState:
    return {
        "graph_output": state["greeting"]+'很高兴见到你'
    }

#  构建状态图 「在定义图的时候加载 全局状态, 输入状态, 输出状态」
builder = StateGraph(state_schema=OverAllState, input_schema=InputState, output_schema=OutputState)

# 添加节点「加载私有状态」
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_edge('node_1', 'node_2')
builder.add_edge('node_2', 'node_3')
builder.add_edge('node_3', END)

# 编译状态图
graph = builder.compile()

# 执行状态图
result = graph.invoke({"username": "张三"})

print(result)

输出如下

复制代码
{'graph_output': 'Hello, Dear 张三很高兴见到你'}

3.4.4. 案例执行过程分析

上述案例中,各类状态的作用如下。

1. 输入状态
复制代码
#  输入状态
class InputState(TypedDict):
      username: str

InputState 用于约束图的外部输入。

因此调用图时,只需要传入:

复制代码
# 执行状态图
result = graph.invoke({"username": "张三"})
2. 全局状态
复制代码
#  全局状态
class OverAllState(TypedDict):
      username: str
      graph_output: str
      nickname: str

OverAllState 是图内部主要使用的状态 Schema。

其中:

  • username 来自图的输入;
  • nickname 由 node_1 写入;
  • graph_output 由 node_3 写入,并最终作为图的输出返回。
3. 私有状态
复制代码
#  私有状态
class PrivateState(TypedDict):
      greeting: str

PrivateState 用于节点之间传递临时数据。

在本例中

复制代码
#  节点2 对接节点1 ==> OverAllState「因为node1返回的OverAllState」
#  使用的参数在全局状态中, 修改的内容在私有状态中 ==> PrivateState
def node_2(state: OverAllState) -> PrivateState:
     # 向私有状态添加问候语
     return {
        "greeting": "Hello, "+ state["nickname"]
     }

node_2 写入了 greeting 字段。

随后:

复制代码
#  节点3 对接节点2 ==> PrivateState「因为node2返回的PrivateState」
#  推荐让节点返回值和返回类型注解一致
def node_3(state: PrivateState) -> OutputState:
    return {
        "graph_output": state["greeting"]+'很高兴见到你'
    }

node_3 通过 PrivateState 读取 greeting 字段,并生成最终输出。

4. 输出状态
复制代码
#  输出状态
class OutputState(TypedDict):
      graph_output: str

OutputState 用于约束图最终返回给外部调用方的数据。

虽然图内部运行过程中还存在 username、nickname、greeting 等字段,但最终结果只返回:

复制代码
{'graph_output': 'Hello, Dear 张三很高兴见到你'}

这是因为图创建时声明了:

复制代码
output_schema=OutputState

所以最终输出会按照 OutputState 进行裁剪。

相关推荐
一条小小yu1 小时前
Text-to-SQL Agent
数据库·sql
李福春1 小时前
斗破Google ADK Java苍穹-第2境斗者-凝旋筑基-Java环境
java·开发语言·gemini·llmagent·深圳同盟·腾讯云架构师技术同盟·adk java
the局外人1 小时前
FastAPI中jwt的基本使用与实践
后端·python·fastapi
Dovis(誓平步青云)2 小时前
现场问题先关闭还是先验收,建筑任务怎样处理依赖
前端·javascript·数据库·mysql·音视频
老王爱玩车2 小时前
预处理详解
c语言·开发语言·学习
dishugj2 小时前
HANA监控、巡检与切换演练
数据库·oracle
mklpo1472 小时前
为不支持sse4.2命令集的老旧设备制作Win11 26H2系统镜像,2026-10-11。
运维·windows·python
GoldenSpider.AI2 小时前
用 AI 改流程之前先做「四桶」体检:9 次返工里 8 次靠规则就能消掉(附 Python 发布前校验器)
人工智能·python·自动化·ai智能体
小白快快跑哦2 小时前
python-字符串全解(六):正则表达式-修饰符
python·正则表达式·修饰符