在大模型应用向多智能体协作、复杂任务拆解演进的当下,LangGraph作为专注于构建状态ful、可交互智能体系统的框架,其并行节点执行机制成为突破性能瓶颈的关键。多智能体系统中,大量独立任务(如多源数据检索、并行分析处理)若按线性顺序执行,会因等待单个任务完成而浪费资源,显著降低响应速度。而LangGraph的并行化能力,通过Fan-out(多输出,又称为扇出)与Fan-in(多输入,又称为扇入)的核心模式,让多个节点在同一时间步(step)同步运行,再将结果高效汇聚,从根本上提升了复杂任务的执行效率。
本节将聚焦这一核心技术,从基础概念到实战落地,层层拆解并行执行的实现逻辑与关键细节。首先通过线性执行与并行执行的对比示例,直观呈现并行化的优势与潜在问题------当多个节点同时更新同一状态时,易出现状态冲突报错。随后深入解析解决方案:利用Python的Annotated类型提示搭配Reducer函数,定义状态合并规则,解决并行更新的冲突问题。
4.2.1 LangGraph中的并行化实战
LangGraph中的并行执行是指多个节点在同一时间步内同时运行,而非遵循先后顺序依次执行,其核心依托 Fan-out与Fan-in两种模式实现。
- Fan-out指一个节点的输出会同步分发至多个并行节点,让这些节点各自独立开展任务。
- Fan-in则是待多个并行节点执行完成后,将它们的输出集中汇聚到同一个节点,实现结果的整合与后续处理。
1. 一个简单的顺序执行示例
下面是我们实现的一个简单的顺序执行示例:
#顺序执行代码
from pydantic import BaseModel
class AgentState(BaseModel):
node_state:str
class ReturnNodeValue:
def init(self, node_secret: str):
self._value = node_secret
def call(self, state: AgentState):
print(f"Adding {self._value} to {state.node_state}")
return {"node_state": self._value}
from langgraph.graph import StateGraph,MessagesState,START,END,add_messages
graph_builder = StateGraph(AgentState)
graph_builder.add_node("a", ReturnNodeValue("I'm A"))
graph_builder.add_node("b", ReturnNodeValue("I'm B"))
graph_builder.add_node("c", ReturnNodeValue("I'm C"))
graph_builder.add_node("d", ReturnNodeValue("I'm D"))
graph_builder.set_entry_point("a")
graph_builder.add_edge("a", "b")
graph_builder.add_edge("b", "c")
graph_builder.add_edge("c", "d")
graph_builder.add_edge("d", END)
graph = graph_builder.compile()
if name == 'main':
replys = graph.stream(input={"node_state":""})
for reply in replys:
print(reply)
print("----------------------")
以上代码中,我们顺序对实例化节点进行了处理,这段代码展示了LangGraph中顺序执行(Sequential Execution)的基础实现,核心是构建一个按固定顺序依次执行的节点流程。
其中,ReturnNodeValue类是一个可调用类(通过实现__call__方法,让类的实例可以像函数一样被调用),用于定义节点的执行逻辑。
- 每个节点实例化时传入一个字符串(如"I'm A"),作为该节点的"标识"。
- call 方法接收当前状态state,执行两个核心操作:(1)打印日志:显示当前节点正在将自身标识添加到现有状态中。(2)返回状态更新:通过字典{"state": self._value}告诉LangGraph,需要将AgentState中的node_state字段更新为当前节点的标识。
输出结果如下:
Adding I'm A to
{'a': None}
Adding I'm B to
{'b': None}
Adding I'm C to
{'c': None}
Adding I'm D to
{'d': None}
可以看到,上面代码的流程严格按a→b→c→d的顺序执行,每个节点必须等待前一个节点完成后才会启动。每个节点执行时,node_state会被更新为当前节点的标识(如a执行后node_state变为"I'm A",b 执行后变为"I'm B"等),体现了"顺序覆盖"的特点。
也就是说,这段代码的核心是展示LangGraph中顺序执行的基础模式:通过定义状态结构、节点逻辑,再用边连接节点形成固定顺序的流程,最终实现节点按先后顺序依次执行,前一个节点的输出(状态更新)会作为后一个节点的输入。这种模式适合任务之间有依赖关系、需要严格按步骤执行的场景。
2. 并行执行的代码
对以上代码进行修正,将顺序执行改为并行执行,代码如下:
from pydantic import BaseModel
class AgentState(BaseModel):
node_state:str
class ReturnNodeValue:
def init(self, node_secret: str):
self._value = node_secret
def call(self, state: AgentState):
print(f"Adding {self._value} to {state.node_state}")
return {"node_state": self._value}
from langgraph.graph import StateGraph,START,END
graph_builder = StateGraph(AgentState)
graph_builder.add_node("a", ReturnNodeValue("I'm A"))
graph_builder.add_node("b", ReturnNodeValue("I'm B"))
graph_builder.add_node("c", ReturnNodeValue("I'm C"))
graph_builder.add_node("d", ReturnNodeValue("I'm D"))
并行流程:a 输出到 b和c,然后输入 d
graph_builder.add_edge(START, "a")
graph_builder.add_edge("a", "b") # a → b
graph_builder.add_edge("a", "c") # a → c(并行)
graph_builder.add_edge("b", "d") # b → d
graph_builder.add_edge("c", "d") # c → d
graph_builder.add_edge("d", END)
graph = graph_builder.compile()
if name == 'main':
replys = graph.stream(input={"node_state":""})
for reply in replys:
print(reply)
print("---------")
我们先从执行的构建图来看:
START → a → b → d → END
↓
c → d
从设计上看,节点a执行完成后,同时触发b和c两个节点(而非按顺序执行),这两个节点在同一时间步并行运行。而节点b和c都执行完成后,才会共同触发节点d(d需等待b和c全部结束),最终d执行完成后到达END。
输出结果如下:
Adding I'm A to
{'a': {'node_state': "I'm A"}}
Adding I'm B to I'm A
Adding I'm C to I'm A
{'b': {'node_state': "I'm B"}}
{'c': {'node_state': "I'm C"}}
langgraph.errors.InvalidUpdateError: At key 'node_state': Can receive only one value per step. Use an Annotated key to handle multiple values.
For troubleshooting, visit: https://docs.langchain.com/oss/python/langgraph /errors/INVALID_CONCURRENT_GRAPH_UPDATE
此时结果报错,这是由于同一个时间步(step)内,b和c都试图更新state这个键。LangGraph不知道如何合并这两个更新,所以抛出InvalidUpdateError错误。
4.2.2 并行化实战中Reduce的妙用
前面尝试通过LangGraph实现并行执行时,最终因状态更新冲突导致执行失败。其核心原因在于:并行节点(如b和c)处于同一时间步,且同时对AgentState中的同一个状态字段node_state发起更新,LangGraph无法自动判断如何合并这些并发更新,进而引发冲突。
要解决这一问题,关键在于为状态字段指定Reducer机制------通过定义明确的状态合并规则,让框架知道如何处理多个并行节点的更新请求。前文讲解状态更新时提到,add_messages本质是一种列表拼接式的Reducer工具,可将多个并行更新的结果有序拼接成列表,恰好适配当前场景。
我们重新定义AgentState,为node_state字段绑定add_messages作为Reducer,具体代码如下:
from langgraph.graph import add_messages
from pydantic import BaseModel
from typing import Annotated,List
class AgentState(BaseModel):
node_state:Annotated[List,add_messages]
这里我们将node_state字段指定为Annotated[List, add_messages]类型,其中add_messages作为Reducer工具,能自动将多个并行节点的更新结果有序拼接成列表,让框架清晰知晓如何合并并发更新,从而顺利规避冲突,保障并行执行的正常推进。完整的并行代码如下:
from langgraph.graph import add_messages
from pydantic import BaseModel
from typing import Annotated,List
class AgentState(BaseModel):
node_state:Annotated[List,add_messages]
class ReturnNodeValue:
def init(self, node_secret: str):
self._value = node_secret
def call(self, state: AgentState):
print(f"Adding {self._value} to {state.node_state}")
return {"node_state": self._value}
from langgraph.graph import StateGraph,START,END
graph_builder = StateGraph(AgentState)
graph_builder.add_node("a", ReturnNodeValue("I'm A"))
graph_builder.add_node("b", ReturnNodeValue("I'm B"))
graph_builder.add_node("c", ReturnNodeValue("I'm C"))
graph_builder.add_node("d", ReturnNodeValue("I'm D"))
并行流程:a输出到b和c,然后输入d
graph_builder.add_edge(START, "a")
graph_builder.add_edge("a", "b") # a → b
graph_builder.add_edge("a", "c") # a → c(并行)
graph_builder.add_edge("b", "d") # b → d
graph_builder.add_edge("c", "d") # c → d
graph_builder.add_edge("d", END)
graph = graph_builder.compile()
if name == 'main':
replys = graph.stream(input={"node_state":""})
for reply in replys:
print("---------")
此时输出如下:
Adding I'm A to [HumanMessage(content='', additional_kwargs={}, response_metadata={}, id='bc75b85f-ea1b-40ef-aa5d-3c32ba9173fc')]
Adding I'm B to [HumanMessage(content='', additional_kwargs={}, response_metadata={}, id='bc75b85f-ea1b-40ef-aa5d-3c32ba9173fc'), HumanMessage(content="I'm A", additional_kwargs={}, response_metadata={}, id='583f933a-e985-45b4-8a6c-9de9710ba188')]
Adding I'm C to [HumanMessage(content='', additional_kwargs={}, response_metadata={}, id='bc75b85f-ea1b-40ef-aa5d-3c32ba9173fc'), HumanMessage(content="I'm A", additional_kwargs={}, response_metadata={}, id='583f933a-e985-45b4-8a6c-9de9710ba188')]---------
Adding I'm D to [HumanMessage(content='', additional_kwargs={}, response_metadata={}, id='bc75b85f-ea1b-40ef-aa5d-3c32ba9173fc'), HumanMessage(content="I'm A", additional_kwargs={}, response_metadata={}, id='583f933a-e985-45b4-8a6c-9de9710ba188'), HumanMessage(content="I'm B", additional_kwargs={}, response_metadata={}, id='98879729-67c9-4d1a-b048-6223f5dcdd4e'), HumanMessage(content="I'm C", additional_kwargs={}, response_metadata={}, id='22cbc05d-7732-46a7-951a-7668aa44ddad')]
可以看到,这里我们使用并行执行代码成功规避了状态更新冲突,输出结果清晰呈现了预期效果:节点a先执行,其结果被添加到初始状态的node_state列表中;随后节点b和c在同一时间步并行运行,二者读取到的是相同的a执行后的状态,且各自的输出通过add_messages Reducer有序拼接至node_state;待b和c均执行完成后,节点d启动,读取到包含初始状态、a、b、c所有结果的完整列表并完成自身输出的拼接。整个过程中,node_state以HumanMessage列表形式存储所有并行节点的更新结果,add_messages有效实现了并发更新的有序合并,彻底解决了之前的冲突问题,确保了"a扇出至b、c并行执行,再共同扇入至d"的并行流程顺畅推进。
