五要素讲到第四个——Reducer。这是最容易被忽略、却最容易踩坑的一个。

很多人学 LangGraph,state、node、edge 都搞懂了,图也能跑,但一到多 Agent 协作,就冒出诡异的 bug:「消息莫名丢了」「上一步的结果被覆盖了」。十有八九,问题出在 reducer 没搞清楚。这一篇就把它讲透。

问题:多个节点改同一个字段,怎么合

回忆一下:节点执行后,返回的是「更新」(要改的字段)。但一个 state 字段,可能在一次运行里被多个节点更新。这时问题来了——后一个节点的更新,怎么和前一个节点的更新合

举个具体的:messages 这个字段,节点 A 往里加了一条消息,节点 B 又往里加了一条。最终 state 里的 messages 应该是:

  • 覆盖:只有 B 那条(A 的丢了)
  • 追加:A 和 B 两条都在

这两种结果天差地别。Reducer 就是定义「这个字段用哪种方式合」的规则。

覆盖 vs 追加:两种合并结果

默认行为:覆盖

如果不对一个字段指定 reducer,它的默认行为是覆盖——后写的直接顶掉前写的。

这对简单字段是合理的:比如 current_task 这种「当前值」,新的覆盖旧的没问题。但如果对 messages 也用覆盖,就会出事——第二个节点一更新,前面所有对话历史全没了,模型再被调用时啥也不记得。

消息列表:必须用追加

消息列表几乎总是要用追加 reducer:每个节点往里加消息,累加进去,谁都不覆盖谁。这样对话历史才能完整保留。

LangGraph 对 messages 字段提供了现成的追加 reducer(通常叫 add_messages),你声明 state 时指定它,messages 就自动是追加语义。

messages 用追加 reducer

这就是为什么第 9 篇说「消息列表默认是追加」——不是巧合,是必须如此,否则记忆就断了。

自定义 Reducer:覆盖之外的可能

追加和覆盖是最常见的两种,但 reducer 可以自定义。一些场景:

  • 取最大值:多个节点都更新一个「置信度」字段,取最高的
  • 合并字典:多个节点都往一个 dict 字段塞键值,合并而非覆盖
  • 去重追加:追加但去掉重复项

自定义 reducer 就是一个函数:def reducer(old, new): return 合并结果。你按业务需要写合并逻辑,声明给对应字段。

自定义 reducer 的几种场景

为什么这是并发协作的关键

reducer 之所以重要,是因为它直接决定多节点(尤其并发节点)写同一字段时的一致性

LangGraph 的 super-step 里,多个节点可能并发执行(如果它们之间没有依赖)。这些并发节点如果都写同一个字段,没有清晰的 reducer,结果就是不确定的——谁先合谁后合,state 最终是什么,全凭运气。

有了 reducer,合并规则是确定的:追加就累加,覆盖就后写为准,自定义就按你的逻辑。不确定性被消除,并发协作才可靠。这是第 17 篇多 Agent 协作能成立的基础。

Reducer 消除并发写的不确定性

一个排查心法

遇到「状态莫名不对」时,按这个顺序排查:

  1. 这个字段指定了 reducer 吗?
  2. 它的 reducer 是覆盖还是追加?
  3. 多个节点写它时,合并结果符合预期吗?

大部分「状态被意外覆盖」的 bug,都卡在第 1、2 步——字段没指定 reducer(默认覆盖),但实际需要追加。

收束:Reducer 是协作的一致性保障

这一篇讲了 Reducer:

  • 它定义 state 字段的合并规则:覆盖、追加、或自定义
  • 默认是覆盖;消息列表必须用追加
  • 自定义 reducer 可实现取最大、合并字典等
  • 它消除多节点并发写的不确定性,是多 Agent 协作的基础
  • 排查「状态丢失」先查 reducer

下一篇讲五要素的最后一个——Super-step 与 Checkpoint,把「图怎么一步步推进」和「怎么存档恢复」连起来讲,这是长流程 Agent 的根基。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。我相信 AI 是程序员的最佳搭档。

如果你想跟完这套「图解 LangChain」,欢迎关注公众号 「十三Tech」。全系列 42 篇,会按认识基础、LangGraph 状态机、Agent 与 middleware、RAG 检索、Tools/MCP/记忆、生产化收束这条线更新。

十三Tech公众号二维码