目录
[二、为什么会有 Reducer?](#二、为什么会有 Reducer?)
[(二)后端开发视角理解 Reducer](#(二)后端开发视角理解 Reducer)
[1. SQL 类比](#1. SQL 类比)
[2. Java 类比](#2. Java 类比)
[三、Reducer 解决的核心问题](#三、Reducer 解决的核心问题)
[(一)Channel 抽象](#(一)Channel 抽象)
[1. LastValue](#1. LastValue)
[2. BinaryOperatorAggregate](#2. BinaryOperatorAggregate)
[(二)使用 Annotated 声明 Reducer](#(二)使用 Annotated 声明 Reducer)
[(三)自定义 Reducer](#(三)自定义 Reducer)
[(三)BinaryOperatorAggregate:Reducer 模式](#(三)BinaryOperatorAggregate:Reducer 模式)
[1. 场景一:默认覆盖模式](#1. 场景一:默认覆盖模式)
[2. 场景二:自动累加模式](#2. 场景二:自动累加模式)
[3. 场景三:自定义 Reduce合并 Dict](#3. 场景三:自定义 Reduce合并 Dict)
[1. 场景一:覆盖](#1. 场景一:覆盖)
[2. 场景二:累加](#2. 场景二:累加)
[3. 场景三:自定义 Reducer](#3. 场景三:自定义 Reducer)
[(一)坑 1:忘记添加 Reducer](#(一)坑 1:忘记添加 Reducer)
[(二)坑 2:Reducer 字段未初始化](#(二)坑 2:Reducer 字段未初始化)
[(三)坑 3:Reducer 有副作用](#(三)坑 3:Reducer 有副作用)
[1. 历史记录无限增长](#1. 历史记录无限增长)
[2. 初始化统一管理](#2. 初始化统一管理)
[3. Reducer 必须保持纯函数](#3. Reducer 必须保持纯函数)
[4. 调试难度提升](#4. 调试难度提升)
[1. 统一初始化](#1. 统一初始化)
[2. 给历史字段加上限](#2. 给历史字段加上限)
[3. 对话场景优先使用 add_messages](#3. 对话场景优先使用 add_messages)
干货分享,感谢您的阅读!
本文是「LangGraph 实战」系列第 2 篇。上一篇我们已经跑通了最小直线图,并留下了一个关键问题:
节点返回的 dict,究竟是如何合并进全局 State 的?
这一篇我们就来揭开 Channel 与 Reducer 的面纱------这是从 Demo 迈向真实 Agent 的关键一步。
一、一个非常容易踩的坑
我们照着上一篇的套路,兴冲冲地写了一个对话 Agent。
每个节点都会向 messages 列表追加一条消息,最后希望把完整对话返回出来。结果一运行,却发现:
明明经过了 3 个节点,最终
messages里却只剩下最后一个节点写入的那条消息,前面的内容全都消失了。
开始排查:
- Node 逻辑没问题
- Edge 连接也没问题
- State 定义看起来也正常
最后才发现,问题出在一个几乎没人第一次会注意到的地方:
LangGraph 默认的 State 合并策略是"覆盖(Overwrite)",而不是"追加(Append)"。
后一个节点返回的值,会直接替换前一个节点写入的值。这也是 LangGraph 中最常见、最隐蔽的坑之一。而解决它的关键,就是理解今天的主角:Reducer
二、为什么会有 Reducer?
(一)Reducer策略
在 Demo 01 中,我们已经掌握了 StateGraph 的基本结构。但当开始构建真实 Agent 时,很快就会遇到这样的问题:
多个节点都在更新同一个字段时,到底应该覆盖,还是累加?
例如:
messages
这个字段显然希望保留整个对话历史。
而像:
current_step
status
这类字段通常只关心最新状态。
因此:
- 有些字段应该覆盖
- 有些字段应该累加
LangGraph 并没有替你做统一决定,而是把这个权力交给开发者:
每个 State 字段都可以拥有独立的合并策略。
而这个策略,就是 Reducer。
(二)后端开发视角理解 Reducer
如果你有后端开发经验,可以这样理解:
1. SQL 类比
普通字段:
sql
UPDATE task SET status = 'done'
覆盖旧值。
而消息历史更像:
sql
INSERT INTO messages (...)
不断累积。
2. Java 类比
覆盖:
java
map.put(key, value)
自定义合并:
java
map.merge(
key,
value,
(oldValue, newValue) -> oldValue + newValue
)
这里的:
(oldValue, newValue) -> ...
本质上就是 Reducer。
三、Reducer 解决的核心问题
两种策略的区别如下:
| 策略 | 行为 | 适用场景 | 类比 |
|---|---|---|---|
| 覆盖(默认) | 新值直接替换旧值 | 当前状态、步骤标记 | map.put() / SQL UPDATE |
| 累加(Reducer) | 新旧值通过函数合并 | 消息历史、执行记录 | map.merge() / SQL INSERT |
判断标准其实非常简单:
这个字段表示"当前值",还是"历史记录"?
如果是:
当前值
只关心最新状态:
status
current_step
current_node
使用默认覆盖即可。
历史记录
希望保留全部过程:
messages
steps
tool_calls
使用 Reducer。
四、核心原理
(一)Channel 抽象
LangGraph 的状态管理底层建立在 Channel 机制之上。每一个 State 字段,都会对应一个 Channel。Channel 负责决定:
这个字段应该如何接收和更新数据。
主要有两种类型:
1. LastValue
只保留最新值。
旧值 -> 新值
直接覆盖。
2. BinaryOperatorAggregate
通过 Reducer 合并:
旧值 + 新值 -> 合并结果
(二)使用 Annotated 声明 Reducer
定义方式如下:
python
from typing import Annotated, TypedDict
import operator
class MyState(TypedDict):
current_step: str # → LastValue(覆盖)
messages: Annotated[list[str], operator.add] # → 累加器(追加)
其中:
python
Annotated[类型, Reducer]
表示:
- 第一个参数:字段类型
- 第二个参数:Reducer 函数
(三)自定义 Reducer
除了:
python
operator.add
你还可以使用任意函数。
要求签名统一为:
python
(old, new) -> merged
例如:
python
def merge_dicts(old: dict, new: dict) -> dict:
return {
**old,
**new
}
class State(TypedDict):
metadata: Annotated[
dict,
merge_dicts
]
这样多个节点写入的 dict 就会自动合并。
五、源码分析
(一)_get_channels(schema)
当创建 StateGraph 时:
python
StateGraph(...)
内部会调用:
python
_get_channels()
解析 TypedDict 上的类型注解。
简化后的逻辑如下:
python
# langgraph/graph/state.py(简化)
def _get_channels(schema):
channels = {}
for field_name, field_type in get_type_hints(schema, include_extras=True).items():
if hasattr(field_type, '__metadata__'):
# 有 Annotated → 取 metadata[0] 作为 Reducer
reducer = field_type.__metadata__[0]
channels[field_name] = BinaryOperatorAggregate(field_type.__origin__, reducer)
else:
# 无 Annotated → 默认 LastValue(覆盖)
channels[field_name] = LastValue(field_type)
return channels
逻辑非常直接:
有 Annotated
↓
BinaryOperatorAggregate
没有 Annotated
↓
LastValue
(二)LastValue:覆盖模式
核心代码几乎只有一行:
python
class LastValue(BaseChannel):
def update(self, values):
if values:
self.value = values[-1] # 直接覆盖
只取最后一个值。
(三)BinaryOperatorAggregate:Reducer 模式
python
class BinaryOperatorAggregate(BaseChannel):
def update(self, values):
for value in values:
if self.value is not None:
self.value = self.operator(self.value, value) # old, new → merged
else:
self.value = value
本质就是不断执行:
merged = reducer(old, new)
(四)完整更新流程
假设节点返回:
{
"messages": ["新消息"],
"status": "done"
}
更新过程如下:
Node 返回结果
↓
messages
↓
BinaryOperatorAggregate
↓
operator.add(
old,
["新消息"]
)
↓
旧列表 + 新列表
status
↓
LastValue
↓
直接覆盖
最终 State 更新完成
六、实战代码
(一)全量代码展示
下面通过三个场景演示不同的 State 合并策略。原始代码全量展示如下:
python
"""Demo 02: State Management --- 状态定义与 Reducer 机制。
演示 LangGraph 的状态管理核心:
1. TypedDict 定义类型安全的 State
2. Reducer(Annotated)实现自动累加 vs 覆盖
3. 自定义 Reducer 函数
4. State 在节点间的传递和更新
运行方式:
python stages/stage1_fundamentals/02_state_management/main.py
"""
from __future__ import annotations
import operator
import sys
from pathlib import Path
from typing import Annotated, TypedDict
from langgraph.graph import END, START, StateGraph
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent.parent))
from shared import get_logger, log_step
logger = get_logger("demo.02_state_mgmt")
# ============================================================
# 场景一:覆盖模式 vs 累加模式对比
# ============================================================
class OverwriteState(TypedDict):
"""无 Reducer 的 State --- 所有字段都是覆盖模式。"""
value: str
counter: int
class AccumulateState(TypedDict):
"""带 Reducer 的 State --- 列表自动累加,计数覆盖。"""
messages: Annotated[list[str], operator.add]
steps: Annotated[list[str], operator.add]
current_node: str
def demo_overwrite():
"""演示覆盖模式:每个 Node 的返回值直接替换旧值。"""
print("\n--- 场景一:覆盖模式(无 Reducer)---")
def node_a(state: OverwriteState) -> dict:
log_step(logger, "node_a", f"接收到 value='{state['value']}', counter={state['counter']}")
return {"value": "来自 node_a", "counter": 10}
def node_b(state: OverwriteState) -> dict:
log_step(logger, "node_b", f"接收到 value='{state['value']}', counter={state['counter']}")
return {"value": "来自 node_b", "counter": 20}
graph = StateGraph(OverwriteState)
graph.add_node("a", node_a)
graph.add_node("b", node_b)
graph.add_edge(START, "a")
graph.add_edge("a", "b")
graph.add_edge("b", END)
app = graph.compile()
result = app.invoke({"value": "初始值", "counter": 0})
print(f" 最终 value = '{result['value']}' ← node_b 的值覆盖了 node_a")
print(f" 最终 counter = {result['counter']} ← node_b 的值覆盖了 node_a")
return result
def demo_accumulate():
"""演示累加模式:带 Reducer 的字段自动合并。"""
print("\n--- 场景二:累加模式(Annotated + operator.add)---")
def collector_a(state: AccumulateState) -> dict:
log_step(logger, "collector_a", "添加消息和步骤记录")
return {
"messages": ["collector_a: 第一条消息"],
"steps": ["collector_a"],
"current_node": "collector_a",
}
def collector_b(state: AccumulateState) -> dict:
log_step(logger, "collector_b", f"当前已有 {len(state['messages'])} 条消息")
return {
"messages": ["collector_b: 第二条消息", "collector_b: 第三条消息"],
"steps": ["collector_b"],
"current_node": "collector_b",
}
def summarizer(state: AccumulateState) -> dict:
log_step(logger, "summarizer", f"汇总 {len(state['messages'])} 条消息")
summary = f"共收集 {len(state['messages'])} 条消息,经过节点: {' → '.join(state['steps'])}"
return {
"messages": [f"总结: {summary}"],
"steps": ["summarizer"],
"current_node": "summarizer",
}
graph = StateGraph(AccumulateState)
graph.add_node("collector_a", collector_a)
graph.add_node("collector_b", collector_b)
graph.add_node("summarizer", summarizer)
graph.add_edge(START, "collector_a")
graph.add_edge("collector_a", "collector_b")
graph.add_edge("collector_b", "summarizer")
graph.add_edge("summarizer", END)
app = graph.compile()
result = app.invoke({
"messages": [],
"steps": [],
"current_node": "",
})
print(f" messages 数量 = {len(result['messages'])}(自动累加,不是覆盖!)")
for i, msg in enumerate(result["messages"]):
print(f" [{i+1}] {msg}")
print(f" steps = {' → '.join(result['steps'])}")
print(f" current_node = '{result['current_node']}'(无 Reducer,被最后一个节点覆盖)")
return result
# ============================================================
# 场景三:自定义 Reducer 函数
# ============================================================
def merge_dicts(left: dict, right: dict) -> dict:
"""自定义 Reducer: 合并字典,保留所有 key,新值覆盖旧值。"""
return {**left, **right}
def dedup_list(left: list, right: list) -> list:
"""自定义 Reducer: 列表合并并去重。"""
seen = set()
result = []
for item in left + right:
if item not in seen:
seen.add(item)
result.append(item)
return result
class CustomReducerState(TypedDict):
"""使用自定义 Reducer 的 State。"""
metadata: Annotated[dict, merge_dicts]
tags: Annotated[list[str], dedup_list]
status: str
def demo_custom_reducer():
"""演示自定义 Reducer 函数。"""
print("\n--- 场景三:自定义 Reducer ---")
def tagger_a(state: CustomReducerState) -> dict:
log_step(logger, "tagger_a", "添加标签和元数据")
return {
"metadata": {"source": "tagger_a", "priority": "high"},
"tags": ["python", "langgraph", "ai"],
"status": "tagging",
}
def tagger_b(state: CustomReducerState) -> dict:
log_step(logger, "tagger_b", "添加更多标签和元数据")
return {
"metadata": {"author": "demo", "priority": "low"},
"tags": ["langgraph", "agent", "demo"],
"status": "done",
}
graph = StateGraph(CustomReducerState)
graph.add_node("tagger_a", tagger_a)
graph.add_node("tagger_b", tagger_b)
graph.add_edge(START, "tagger_a")
graph.add_edge("tagger_a", "tagger_b")
graph.add_edge("tagger_b", END)
app = graph.compile()
result = app.invoke({"metadata": {}, "tags": [], "status": ""})
print(f" metadata = {result['metadata']}")
print(" ↑ merge_dicts: source 来自 tagger_a, author 来自 tagger_b, priority 被 tagger_b 覆盖")
print(f" tags = {result['tags']}")
print(" ↑ dedup_list: 'langgraph' 只出现一次(去重)")
print(f" status = '{result['status']}'(无 Reducer,直接覆盖)")
return result
def run_demo() -> dict:
"""运行 State Management Demo。"""
print("=" * 60)
print(" Demo 02: State Management --- 状态定义与 Reducer 机制")
print("=" * 60)
result1 = demo_overwrite()
result2 = demo_accumulate()
result3 = demo_custom_reducer()
print()
print("=" * 60)
print(" 关键概念回顾")
print("=" * 60)
print(" 1. 无 Reducer : 字段直接覆盖(LastValue 策略)")
print(" 2. operator.add : 列表自动拼接、数字自动相加")
print(" 3. 自定义 Reducer : 任意合并逻辑(dict 合并、列表去重等)")
print(" 4. Annotated : 使用 typing.Annotated 声明 Reducer")
print(" 5. Channel : LangGraph 内部的数据管道抽象")
print()
return {"overwrite": result1, "accumulate": result2, "custom": result3}
if __name__ == "__main__":
run_demo()
(二)重点代码说明
1. 场景一:默认覆盖模式
python
# 场景一:覆盖模式(无 Reducer)
class OverwriteState(TypedDict):
value: str # 每个节点写它都会覆盖
counter: int
没有 Reducer。所有更新都会覆盖旧值。
2. 场景二:自动累加模式
python
# 场景二:累加模式(operator.add)
class AccumulateState(TypedDict):
messages: Annotated[list[str], operator.add] # 自动追加
steps: Annotated[list[str], operator.add]
current_node: str # 无 Reducer → 仍覆盖
其中:
messages
steps
自动追加。
而:
current_node
仍然保持覆盖模式。
3. 场景三:自定义 Reduce合并 Dict
python
# 场景三:自定义 Reducer
def merge_dicts(left: dict, right: dict) -> dict:
return {**left, **right}
def dedup_list(left: list, right: list) -> list:
seen, result = set(), []
for item in left + right:
if item not in seen:
seen.add(item)
result.append(item)
return result
class CustomReducerState(TypedDict):
metadata: Annotated[dict, merge_dicts] # dict 合并
tags: Annotated[list[str], dedup_list] # 追加并去重
status: str
(三)运行结果
实际执行输出如下:
python
Connected to server 127.0.0.1:50252
============================================================
Demo 02: State Management --- 状态定义与 Reducer 机制
============================================================
--- 场景一:覆盖模式(无 Reducer)---
最终 value = '来自 node_b' ← node_b 的值覆盖了 node_a
最终 counter = 20 ← node_b 的值覆盖了 node_a
--- 场景二:累加模式(Annotated + operator.add)---
messages 数量 = 4(自动累加,不是覆盖!)
[1] collector_a: 第一条消息
[2] collector_b: 第二条消息
[3] collector_b: 第三条消息
[4] 总结: 共收集 3 条消息,经过节点: collector_a → collector_b
steps = collector_a → collector_b → summarizer
current_node = 'summarizer'(无 Reducer,被最后一个节点覆盖)
--- 场景三:自定义 Reducer ---
metadata = {'source': 'tagger_a', 'priority': 'low', 'author': 'demo'}
↑ merge_dicts: source 来自 tagger_a, author 来自 tagger_b, priority 被 tagger_b 覆盖
tags = ['python', 'langgraph', 'ai', 'agent', 'demo']
↑ dedup_list: 'langgraph' 只出现一次(去重)
status = 'done'(无 Reducer,直接覆盖)
============================================================
关键概念回顾
============================================================
1. 无 Reducer : 字段直接覆盖(LastValue 策略)
2. operator.add : 列表自动拼接、数字自动相加
3. 自定义 Reducer : 任意合并逻辑(dict 合并、列表去重等)
4. Annotated : 使用 typing.Annotated 声明 Reducer
5. Channel : LangGraph 内部的数据管道抽象
2026-06-04 23:12:06 INFO demo.02_state_mgmt | 📌 接收到 value='初始值',
counter=0
INFO demo.02_state_mgmt | 📌 接收到 value='来自
node_a', counter=10
INFO demo.02_state_mgmt | 📌 添加消息和步骤记录
INFO demo.02_state_mgmt | 📌 当前已有 1 条消息
INFO demo.02_state_mgmt | 📌 汇总 3 条消息
INFO demo.02_state_mgmt | 📌 添加标签和元数据
INFO demo.02_state_mgmt | 📌 添加更多标签和元数据
Process finished with exit code 0
如何理解结果?
1. 场景一:覆盖
value
counter
都只保留了最后一个节点的写入结果。这正是默认行为。
2. 场景二:累加
messages
经过多个节点后保留了全部内容。
而:
current_node
因为没有 Reducer,
最终只剩:
summarizer
这说明:
同一个 State 内,不同字段可以采用完全不同的更新策略。
3. 场景三:自定义 Reducer
merge_dicts
实现:
metadata
自动合并。
dedup_list
实现:
tags
自动去重。
例如:
langgraph
只会保留一份。
七、常见坑与排查
(一)坑 1:忘记添加 Reducer
最终只剩最后一条消息。
错误写法
python
class State(TypedDict):
messages: list[str]
正确写法
python
class State(TypedDict):
messages: Annotated[
list[str],
operator.add
]
(二)坑 2:Reducer 字段未初始化
直接报错信息
python
app.invoke({})
直接报错:
KeyError
TypeError
Reducer 需要:
old + new
但:
None + [...]
这里基本就是没有初始化导致显然不成立。
正确做法
python
app.invoke({
"messages": [],
"steps": [],
"current_node": ""
})
(三)坑 3:Reducer 有副作用
错误示例
python
def bad(a, b):
a.extend(b)
return a
直接修改了旧对象。
正确示例
python
def good(a, b):
return a + b
返回全新对象。
💡 踩坑笔记
我在一开始学习 LangGraph 过程中真实遇到的问题例如:
"我曾经盯着 messages 为什么只剩一条消息排查了五分钟,最后发现只是少写了一个 Annotated。"
这种真实经历往往比知识点本身更有价值,大家快动手操作一下吧。
八、工程化思考与生产实践
(一)工程化思考简要
1. 历史记录无限增长
Reducer 只负责追加,不会自动清理。
因此:
messages
会越来越长。最终导致:
- Token 爆炸
- 内存上涨
需要定期裁剪。
2. 初始化统一管理
建议不要在每次:
invoke()
时手动传空列表。
统一使用工厂函数:
def make_initial_state():
...
3. Reducer 必须保持纯函数
不要:
- 写日志
- 写文件
- 修改入参
把它当成数学函数即可。
4. 调试难度提升
State 字段越来越多后,很难定位是谁修改了什么。建议结合:
stream()
观察每个节点产生的增量更新。
(二)生产级实践建议
1. 统一初始化
def make_initial_state():
return {
"messages": [],
"steps": [],
"current_node": ""
}
2. 给历史字段加上限
def add_capped(
old,
new,
cap=100
):
return (
old + new
)[-cap:]
只保留最近 100 条。
3. 对话场景优先使用 add_messages
from langgraph.graph.message import add_messages
class ChatState(TypedDict):
messages: Annotated[
list,
add_messages
]
相比:
operator.add
它支持:
- 追加
- 去重
- 更新
- 删除
是生产环境中的首选方案。
九、总结
State 管理是 LangGraph 中最容易被忽视、却又最重要的机制之一。
Node 和 Edge 决定:
Graph 怎么走。
而 Reducer 决定:
数据怎么流。
记住一句最重要的话:
每定义一个 State 字段,都先问自己:它是"当前值",还是"历史记录"?
- 当前值 → 覆盖
- 历史记录 → Reducer
掌握这一点后,你就真正拥有了构建复杂 Agent 状态系统的能力。
下一篇预告
掌握了状态管理之后,Graph 已经能够稳定传递数据。
但现实中的 Agent 不会永远沿着一条直线执行:
- 用户问题不同
- 工具调用结果不同
- 模型决策不同
Graph 必须学会"分叉"。
下一篇:
《让 Graph 学会"分叉":Conditional Edge 与路由函数实战》
我们将正式进入 LangGraph 的分支控制世界,学习如何根据状态动态决定下一步执行路径。
而在后续第 4 篇中,我们还会专门深入讲解:
MessagesState 与 add_messages ------ LangGraph 对话场景中的智能 Reducer。