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: strdef node_3(state: PrivateState) -> OutputState:
return {
"graph_output": state["greeting"]
}
-
当
node_3被添加到图中时,PrivateState中的greeting字段会被记录到图中,从而成为图内部可以传递的状态字段。 -
总结
- 全局状态、输入状态、输出状态通常在创建
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 进行裁剪。