在 LangGraph 中,StateGraph 以 State 贯穿整个 Graph 的执行过程。每个 Node 读取当前 State 完成自己的任务后,通常只返回本次执行产生的部分更新,而不是重新构造完整 State。
LangGraph 会根据这些更新修改当前 State,再将新的 State 传递给后续 Node,推动 Graph 继续执行。
对于普通字段,这种更新方式很好理解。但如果 State 中保存的是消息、日志、搜索结果这类列表数据,新的值应该直接覆盖原来的内容,还是追加到已有数据之后?如果多个 Node 都更新同一个字段,LangGraph 又该如何合并这些结果?
这些问题,本质上都指向同一个机制:State 的更新与合并规则。而在 LangGraph 中,负责定义这套规则的核心机制就是 Reducer。
一、State 的更新规则
先看一个简单的 State:
from typing_extensions import TypedDict
class TaskState(TypedDict):
topic: str
status: str
logs: list[str]
一个 Node 可以只返回自己修改的字段:
def start_task(state: TaskState):
return {
"status": "running"
}
假设执行前的 State 是:
{
"topic": "LangGraph",
"status": "pending",
"logs": ["task created"]
}
执行以后得到:
{
"topic": "LangGraph",
"status": "running",
"logs": ["task created"]
}
Node 返回的并不是新的完整 State,而是一次 Partial State Update。
对于 status,新值直接替换旧值即可:
pending
↓
running
但如果 Node 返回:
{
"logs": ["task started"]
}
就会出现两种可能:
["task created"]
↓
["task started"]
或者:
["task created"]
+
["task started"]
↓
["task created", "task started"]
字段类型是 list[str],并不能决定采用哪一种更新方式。
LangGraph 需要知道:这个字段收到新值时,旧值和新值应该怎样合并。
这就是 Reducer。
二、Reducer 的合并机制
如果 State 字段没有配置 Reducer,LangGraph 默认采用覆盖方式更新,正如刚才我们已经看到的。
对于消息、日志、搜索结果集合等需要持续累积的数据,则往往需要显式定义 Reducer。
Reducer 可以理解为字段级别的合并函数。
一次 State Update 到来时,它接收:
当前值
+
本次更新值
并计算:
下一步的值
例如希望日志不断追加,可以这样定义:
import operator
from typing import Annotated
from typing_extensions import TypedDict
class TaskState(TypedDict):
logs: Annotated[list[str], operator.add]
其中 Annotated[list[str], operator.add],它告诉 LangGraph:
logs 的类型是 list[str]
logs 收到更新时
使用 operator.add 合并
因此:
["task created"]
+
["step a"]
得到:
["task created", "step a"]
把前面的 Graph 改成 Reducer 版本:
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
class TaskState(TypedDict):
logs: Annotated[list[str], operator.add]
def step_a(state: TaskState):
return {
"logs": ["step a"]
}
def step_b(state: TaskState):
return {
"logs": ["step b"]
}
builder = StateGraph(TaskState)
builder.add_node("step_a", step_a)
builder.add_node("step_b", step_b)
builder.add_edge(START, "step_a")
builder.add_edge("step_a", "step_b")
builder.add_edge("step_b", END)
graph = builder.compile()
result = graph.invoke(
{
"logs": ["task created"]
}
)
print(result)
其运行结果如下:
{'logs': ['task created', 'step a', 'step b']}
Node 本身仍然只返回:
{
"logs": ["step a"]
}
Node 并不知道旧值是什么,也没有自己执行:
state["logs"] + ["step a"]
合并逻辑由 Reducer 负责。
因此 Node 和 Reducer 职责分别如下:
Node:产生 State Update
Reducer:决定 Update 怎样合并进 State
四、自定义 Reducer
Reducer 不一定使用 operator.add。
只要能够接收当前值和新值,并返回合并后的结果,就可以作为 Reducer。
例如一个研究 Agent 会从多个步骤产生标签:
analyze_topic
↓
["agent", "langgraph"]
analyze_content
↓
["langgraph", "state"]
如果直接追加,会得到:
["agent", "langgraph", "langgraph", "state"]
如果希望自动去重,可以定义自己的 Reducer:
from typing import Annotated
from typing_extensions import TypedDict
def merge_tags(
current: list[str],
new: list[str]
) -> list[str]:
return list(dict.fromkeys(current + new))
class ResearchState(TypedDict):
tags: Annotated[list[str], merge_tags]
在 Graph 中使用:
from langgraph.graph import StateGraph, START, END
def analyze_topic(state: ResearchState):
return {
"tags": ["agent", "langgraph"]
}
def analyze_content(state: ResearchState):
return {
"tags": ["langgraph", "state"]
}
builder = StateGraph(ResearchState)
builder.add_node("analyze_topic", analyze_topic)
builder.add_node("analyze_content", analyze_content)
builder.add_edge(START, "analyze_topic")
builder.add_edge("analyze_topic", "analyze_content")
builder.add_edge("analyze_content", END)
graph = builder.compile()
result = graph.invoke(
{
"tags": []
}
)
print(result)
Reducer 对两个列表进行合并和去重,运行结果如下:
{'tags': ['agent', 'langgraph', 'state']}
四、MessagesState 的消息更新
Agent 中最常见的累积型 State 就是消息历史。
例如:
HumanMessage
AIMessage
ToolMessage
HumanMessage
AIMessage
直觉上,可以使用:
messages: Annotated[
list,
operator.add
]
这样每次返回新的消息:
return {
"messages": [new_message]
}
都可以追加到旧列表后面。
但消息和普通日志列表存在一个区别:已有消息有时需要被修改。
假设 State 中已经存在:
id = msg-1
content = "helo"
后来需要改成:
id = msg-1
content = "hello"
如果只是列表追加,最终会同时存在:
msg-1 helo
msg-1 hello
这显然不适合消息状态。
因此 LangGraph 提供了专门的 Reducer:
from langgraph.graph.message import add_messages
可以这样定义:
from typing import Annotated
from typing_extensions import TypedDict
from langchain.messages import AnyMessage
from langgraph.graph.message import add_messages
class ChatState(TypedDict):
messages: Annotated[
list[AnyMessage],
add_messages
]
它和普通 Reducer 的使用方式完全一样,只是合并规则针对 Message 做了专门处理。
add_messages 会根据消息 ID 合并消息:
新的 ID
→ 追加
已有 ID
→ 更新对应消息
因此,它比:
operator.add
更适合 Agent 的消息历史。
六、MessagesState 的使用
由于消息 State 非常常见,LangGraph 已经提供了预定义的:
from langgraph.graph import MessagesState
可以直接用来创建 Graph:
builder = StateGraph(MessagesState)
可以把它理解成已经预先配置好了:
messages: Annotated[
list[AnyMessage],
add_messages
]
来看一个简单例子:
from langchain.messages import AIMessage
from langgraph.graph import (
MessagesState,
StateGraph,
START,
END,
)
def reply(state: MessagesState):
last_message = state["messages"][-1]
return {
"messages": [
AIMessage(
content=f"收到:{last_message.content}"
)
]
}
builder = StateGraph(MessagesState)
builder.add_node("reply", reply)
builder.add_edge(START, "reply")
builder.add_edge("reply", END)
graph = builder.compile()
result = graph.invoke(
{
"messages": [
{
"role": "user",
"content": "介绍一下 Reducer",
}
]
}
)
for message in result["messages"]:
print(
type(message).__name__,
message.content
)
运行结果如下:
HumanMessage 介绍一下 Reducer
AIMessage 收到:介绍一下 Reducer
reply Node 只返回了一条新的 AIMessage,最终 State 中却保留了用户消息和 AI 消息。
这就是 add_messages 在处理消息 State。
实际 Agent 往往还需要其他字段。
可以直接扩展 MessagesState:
from langgraph.graph import MessagesState
class AgentState(MessagesState):
topic: str
retry_count: int
如果其他字段也需要累积,可以继续单独配置 Reducer。
同一个 State 中,不同字段可以使用完全不同的更新规则。
七、State Schema 的边界
理解 Reducer 以后,设计 State 时就不能只考虑:
需要哪些字段
还应该同时考虑:
这个字段由谁更新
是否会多次更新
是否会被并行 Node 更新
新值应该覆盖还是合并
如果合并应该采用什么规则
例如:
class AgentState(MessagesState):
query: str
documents: list[str]
summary: str
retry_count: int
几个字段的更新语义可能完全不同。
query 通常代表当前查询,新值可以直接覆盖。
summary 通常只需要保存最新结果,也适合默认覆盖。
retry_count 如果 Node 每次返回完整计数,也可以继续覆盖。
documents 则取决于 Graph 结构。
如果只有一个搜索 Node:
search
↓
documents
覆盖可能就足够。
如果多个搜索 Node 会同时产生文档:
search_web
search_database
search_files
那么 documents 很可能需要 Reducer。
messages 则通常使用 add_messages。
可以用一个简单的判断方式:
新值代表最新状态
↓
默认覆盖
新值需要和旧值同时保留
↓
使用 Reducer
存在特殊合并规则
↓
自定义 Reducer
State 本身也不应该包含所有中间变量。
只有需要跨 Node 使用、参与路由、持久化或者任务恢复的数据,才更适合进入 State。
Node 内部的一次性临时变量,继续留在函数内部即可。
这样 State schema 会更清楚,后面的 Persistence、Interrupt 和 Subgraph 设计也会更容易。
总结
本文介绍了 LangGraph 的 State 更新机制。Node 返回的是部分状态更新,字段默认采用覆盖方式;需要累积、去重或合并数据时,可以通过 Reducer 定义更新规则。
add_messages 专门处理消息状态,MessagesState 则提供了常用封装。设计 State 时,还应提前考虑并发更新以及不同字段的合并语义。
社区讨论
参与讨论
有问题或想法?欢迎继续讨论。