如果你用 LangGraph 构建过多节点的 Agent,一定接触过这样的状态定义:
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]
current_tool: str
step_count: int
Annotated[list, add_messages] 这个写法乍看有点奇怪——为什么不直接用 list[AnyMessage]?背后的 add_messages 是什么意思?这篇文章就来拆解 LangGraph 状态系统的核心设计:Channel。
最直觉的做法是给每个图维护一个全局字典,节点执行完后把输出写进去。这在单节点顺序执行时没问题,但 LangGraph 的核心价值之一是并发——多个节点可以在同一个超步(superstep)内并行运行。一旦并发,共享可变字典至少带来三个问题。
竞态问题。假设 tool_a 和 tool_b 并行执行,都往 state["result"] 写入,最后谁写谁赢,取决于线程调度顺序,结果不可预期。你可以加锁,但这等于串行化了并发,得不偿失。
序列化问题。LangGraph 的核心特性之一是 checkpoint——每个超步执行完后将状态持久化到存储,以便恢复和重放。如果状态是一个普通 dict,checkpoint 层只能存一个"最终值的快照"。当两个节点并发写入同一个 key 时,checkpoint 不知道这两次写入该如何合并——是取最后一个?还是拼接?还是求和?这个语义必须由某处来定义。
调度问题。Pregel 式执行引擎在每个超步结束后需要决定下一轮触发哪些节点:只有"在本超步有新写入"的 channel 才会触发订阅了它的节点。普通 dict 没有版本信息,引擎无法判断某个 key 在这一轮是否真的被更新了。
这三个问题加在一起,指向同一个结论:状态中的每个字段不能只是一个裸值,它需要封装自己的合并逻辑、版本追踪和序列化/反序列化能力。这就是引入 Channel 的原因。
BaseChannel 定义在 langgraph/channels/base.py,是一个泛型抽象基类,三个类型参数分别是 Value(读取时的类型)、Update(写入时的类型)和 Checkpoint(持久化时的类型)。接口中最关键的两个方法是 update() 和 checkpoint()。
update() 的签名是 update(self, values: Sequence[Update]) -> bool。注意它接收的是一个列表,而不是单个值。这是设计上最重要的一个决策:同一超步内可能有多个节点同时写入同一个 channel,框架在超步结束时收集所有写入,一次性调用 update(),由 channel 自身决定如何合并这批写入。返回值 bool 表示本次调用是否真正改变了 channel 的内部状态——引擎靠这个决定是否触发下游节点。
checkpoint() 返回一个可序列化的表示,from_checkpoint() 从这个表示重建 channel 实例。两者合起来构成了 channel 的持久化协议。
版本追踪隐含在 update() 返回的布尔值里:每次 update() 返回 True,引擎就认为这个 channel 的版本发生了变化,会触发订阅它的节点在下一超步执行。版本不比较值本身,只看"有没有被更新",这让不同类型的 channel 可以自由定义"更新"的语义。
下图展示了六种 Channel 的继承关系和各自的核心语义:
classDiagram
class BaseChannel {
+update(values: list) bool
+get() Value
+checkpoint() Any
+from_checkpoint(data)
+version : int|str|float
}
class LastValue {
单写覆盖语义
多写直接抛异常
}
class BinaryOperatorAggregate {
reducer(a, b) → a
多写折叠合并
e.g. add_messages
}
class DeltaChannel {
增量存储 diff
父链重建全量值
快照截断父链
}
class NamedBarrierValue {
等待全部指定写入
fan-in 同步原语
}
class EphemeralValue {
超步后自动清空
不写入 checkpoint
}
class UntrackedValue {
写入不递增版本
不触发节点调度
}
BaseChannel <|-- LastValue
BaseChannel <|-- BinaryOperatorAggregate
BaseChannel <|-- DeltaChannel
BaseChannel <|-- NamedBarrierValue
BaseChannel <|-- EphemeralValue
BaseChannel <|-- UntrackedValue
LastValue 是默认 channel 类型,语义直白:每次写入覆盖旧值,最多只接受一次写入。
# last_value.py 核心逻辑
def update(self, values: Sequence[Value]) -> bool:
if len(values) == 0:
return False
if len(values) != 1:
raise InvalidUpdateError(
f"At key '{self.key}': Can receive only one value per step. "
"Use an Annotated key to handle multiple values."
)
self.value = values[-1]
return True