文章目录
-
-
- [动态分支(Dynamic Branch)](#动态分支(Dynamic Branch))
-
动态分支(Dynamic Branch)
-
定义 :节点的后续执行路径在 运行时 才确定,可以根据当前状态、输入数据或中间结果,动态决定要触发哪些下游任务或跳转到哪个下游节点。
-
特点:
- 下游执行目标可以在运行时选择;
- 下游任务数量可以在运行时决定;
- 可以为同一个下游节点动态创建多个执行任务;
- 适合实现 Map-Reduce 式动态扇出、多任务并行处理、运行时条件跳转等场景。
需要注意的是,所谓"动态"通常不是指运行时临时创建新的节点定义。节点本身一般仍需要在图编译前注册。动态性主要体现在:运行时决定触发哪些节点,以及为这些节点创建多少个执行任务。
-
典型用法:
Send(动态扇出任务/数据)Command(goto=...)(运行时跳转到下游节点)
⚡ 核心判断:
运行时决定后续执行目标或任务数量 → 动态分支
并行节点
Send 结合 add_conditional_edges() 使用,可以用于动态扇出任务
所谓扇出(Fan-out),是指一个上游节点像扇子一样,向外分发出多个下游任务。例如,根据一个主题,同时生成诗、词、笑话三个任务;又如,根据一个列表,为列表中的每个元素动态启动一个处理任务。
具体来说,路由函数可以返回一个 Send 实例序列。每个 Send 实例都描述了一次独立的任务分发:
- 分发到哪个下游节点;
- 给这个下游节点传入什么私有状态。
运行时,LangGraph 会根据返回的每个 Send 实例创建对应的任务。这些任务通常会在同一个超步中并行执行。
Send 类构造器源码如下
python
def __init__(self, /, node: str, arg: Any) -> None:
"""
Initialize a new instance of the `Send` class.
Args:
node: The name of the target node to send the message to.
arg: The state or message to send to the target node.
"""
self.node = node
self.arg = arg
该构造器接收两个参数,并记录为实例属性:
- node:待启动的下游节点名称,必须是已在状态图中注册的合法名称
- arg:传递给下游节点的信息,仅对**
node**指向的节点可见,通常应是私有状态。每个Send实例接收到的arg是独立的,互不相干。
实例如下
python
from typing import TypedDict, Literal
from collections.abc import Sequence
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
from langchain.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
CONTENT_TYPES = ["poem", "ci_poem", "joke"]
model = ChatDeepSeek(
model = "deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
poem: str
ci_poem: str
joke: str
class WorkerState(TypedDict):
"""
私有状态,只对 Worker 节点可用
"""
content_type: Literal["poem", "ci_poem", "joke"]
prompt: str
class InputState(TypedDict):
topic: str
class OutputState(TypedDict):
poem: str
ci_poem: str
joke: str
def worker_node(state: WorkerState) -> OutputState:
content_type = state["content_type"]
prompt = state["prompt"]
content = model.invoke([HumanMessage(prompt)]).content
return {
content_type: content
}
def router(state: InputState) -> Sequence[Send]:
prompt = "请生成关于 {} 的 {}"
english2chinese = {
"poem": "一首诗",
"ci_poem": "一首词",
"joke": "一个笑话"
}
topic = state["topic"]
return [Send(
"worker_node",
{
"content_type": content_type,
"prompt": prompt.format(topic, english2chinese[content_type]),
}
) for content_type in CONTENT_TYPES
]
builder = StateGraph(state_schema=OverAllState, input_schema=InputState, output_schema=OutputState)
builder.add_node("worker_node", worker_node)
builder.add_conditional_edges(
START,
router,
path_map=["worker_node"]
)
builder.add_edge("worker_node", END)
graph = builder.compile()
res = graph.invoke({"topic": "布偶狗"})
print(res)
from IPython.display import display
display(graph)
在这个案例中,router() 并没有直接返回某一个固定的下游节点名称,而是返回了多个 Send 实例:
python
[
Send("worker_node", {"content_type": "poem", ...}),
Send("worker_node", {"content_type": "ci_poem", ...}),
Send("worker_node", {"content_type": "joke", ...}),
]
因此,运行时会动态创建三个指向 worker_node 的任务。三个任务执行的是同一个节点函数,但它们接收到的私有状态不同,所以可以分别生成诗、词和笑话。
输出如下
json
{
"poem": "## 《布偶狗》\n\n它曾蜷在藤椅里数谷粒,\n一只耳朵垂向磨损的地板。\n现在檐角的水滴是它的心跳,\n牵动穿珠眼珠转动。\n\n穿碎花裙的小女孩,\n用膝盖丈量过它的静默。\n她松开橡皮筋的午后,\n线缝里渗出稚嫩的气流。\n\n有人将米尺抖成琴弦,\n折叠的假寐里,\n它代替远方的稻草人,\n在积雨的布纹中扎根。\n\n田野在练习告别,\n而它守着发芽的梯田。\n当月光填满每道接缝,\n皮毛间涌出成群的往事。",
"ci_poem": "《临江仙·布偶狗》\n绒作皮毛棉作骨,琉璃眸底春凝。\n也无吠影也无惊。倚床垂耳听,抱主索糖迎。\n\n不向残羹争冷炙,旧衾犹带余馨。\n学人解语却无声。偶沾星与月,懒问雨和晴。\n\n注:我以布偶狗为题,通过"绒作皮毛棉作骨"等句,以拟人手法赋予玩偶灵性。下阕"不向残羹争冷炙"暗喻玩偶超然物外的品性,末句"偶沾星与月"更添空灵意境,将玩偶与自然意象相融,表达出静默相伴的永恒温馨。",
"joke": "# 布偶狗的笑话\n\n有一天,一只布偶狗走进一家宠物店,对店主说:\n\n"老板,我想买一只真正的狗朋友。"\n\n店主看了看它,好奇地问:"你不是狗吗?"\n\n布偶狗叹了口气,说:"唉,我是布偶做的,只能陪主人睡觉和卖萌,但我不会汪汪叫,也不会摇尾巴,更不会追球。每次主人想带我出去散步,我只能在包里安静地坐着,感觉自己特别没用。"\n\n店主笑了笑,说:"那你想要什么样的狗朋友?"\n\n布偶狗眼睛一亮:"我想要一只真正的狗,可以带着我去跑、去叫、去玩!我要学习怎么当一只好狗!"\n\n店主想了想,指着一只活泼的小金毛说:"那这只怎么样?它精力旺盛,可以教你很多。"\n\n布偶狗激动地冲过去,结果因为自己是布做的,腿太软,直接摔了个跟头,四脚朝天躺在地上。\n\n小金毛跑过来,闻了闻它,然后叼起它的棉花尾巴,一路拖到了狗窝里。\n\n布偶狗挣扎着喊:"等等!我还没学会怎么站起来!"\n\n小金毛歪了歪头,汪汪了两声,像是说:"没事,我先教你躺平。"\n\n从此以后,布偶狗成了小金毛最好的"垫子"------每天被压在身下睡觉,还觉得特别温暖。\n\n布偶狗心想:"原来,当一只合格的布偶狗朋友,最重要的技能是------当沙发。" 🛋️🐾"
}

此处图结构能成功渲染的关键,是在 path_map 中显式声明了可能被路由到的下游节点:
python
path_map=["worker_node"]
因为 Send 的目标和数量可以运行时动态决定,图渲染器无法仅靠运行时返回值提前知道图结构。通过 path_map 声明候选下游节点后,渲染图才能正确展示 START 到 worker_node 的条件边关系。
补充说明:如果多个并行任务写入同一个状态字段,通常需要为该字段定义 reducer,用于合并多个任务的输出。本例中三个任务分别写入 poem 、ci_poem 、joke 三个不同字段,因此不会发生同一字段的并发合并问题。
条件分支
Command介绍
Command 是 LangGraph 中用于控制图执行的多功能原语,它的构造器可以接受四个参数,并记录在同名类属性中:
update:更新图状态,效果等同于节点直接返回状态更新字典;goto:指定节点执行完成后的跳转目标,可用于运行时条件分支。当需要同时更新状态并控制跳转时,比单独使用条件边更合适;graph:存在子图时,用于指定跳转发生在哪一层图中,例如从子图跳转到父图;resume:用于恢复被中断的图执行,常见于human-in-the-loop场景。
用Command实现条件分支
可以通过 Command(goto=...) 在节点内部实现条件分支。
与 add_conditional_edges() 相比,Command 更适合"状态更新"和"控制流跳转"需要放在同一个节点返回值中的场景。
例如,一个节点既要更新状态,又要根据当前状态决定下一步跳转目标,就可以返回:
python
return Command(
update={"foo": "bar"},
goto="next_node"
)
示例如下:
python
from typing import TypedDict, Literal
from langgraph.types import Command
from langgraph.graph import StateGraph, START, END
from langchain.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from dotenv import load_dotenv
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
class OverAllState(TypedDict):
topic: str
content_type: Literal["poem", "joke"]
poem: str
joke: str
def router(state: OverAllState) -> Command[Literal["poem_node", "joke_node", END]]:
content_type = state["content_type"]
if content_type == "poem":
return Command (
goto="poem_node"
)
elif content_type == "joke":
return Command (
goto="joke_node"
)
return Command (
goto=END
)
def poem_node(state: OverAllState) -> OverAllState:
topic = state["topic"]
prompt = f"请生成一首关于 {topic} 的七言绝句"
poem = model.invoke([HumanMessage(prompt)]).content
return {
"poem": poem
}
def joke_node(state: OverAllState) -> OverAllState:
topic = state["topic"]
prompt = f"请生成一个关于 {topic} 的冷笑话"
joke = model.invoke([HumanMessage(prompt)]).content
return {
"joke": joke
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("router", router)
builder.add_node("poem_node", poem_node)
builder.add_node("joke_node", joke_node)
builder.add_edge(START, "router")
builder.add_edge("poem_node", END)
builder.add_edge("joke_node", END)
graph = builder.compile()
poem_res = graph.invoke({"topic": "布偶猫", "content_type": "poem"})
joke_res = graph.invoke({"topic": "布偶猫", "content_type": "joke"})
# 这里故意传入一个不符合 Literal["poem", "joke"] 的值,
# 用来观察运行时兜底分支。
# 注意:Literal 主要服务于静态类型检查,
# 默认不会在 Python 运行时阻止非法值传入。
illegal_res = graph.invoke({
"topic": "布偶猫",
"content_type": "illegal"
})
print('=' * 30, '-> poem <-', '=' * 30)
print(poem_res)
print('=' * 30, '-> joke <-', '=' * 30)
print(joke_res)
print('=' * 30, '-> illegal <-', '=' * 30)
print(illegal_res)
from IPython.display import display
display(graph)
上述代码中,router() 节点根据 content_type 的值决定下一步跳转目标:
- 当
content_type == "poem"时,跳转到poem_node; - 当
content_type == "joke"时,跳转到joke_node; - 当传入其他非法值时,跳转到
END,图直接结束。
结果如下
python
============================== -> poem <- ==============================
{
'topic': '布偶猫',
'content_type': 'poem',
'poem': '《布偶猫》\n雪砌绒身碧眼秋,一帘幽梦卧云舟。\n忽听银铃摇碎月,轻衔星子入西楼。\n\n(注:此诗通过"雪砌绒身"、"碧眼秋"等意象勾勒布偶猫雪白柔顺的毛发与澄澈眼眸;"卧云舟"、"衔星子"等超现实笔法,赋予其精灵般的气质;末句"入西楼"暗合其优雅步态与神秘秉性,整体形成月光浸染的梦幻意境。)'
}
============================== -> joke <- ==============================
{
'topic': '布偶猫',
'content_type': 'joke',
'joke': '给你分享一个关于布偶猫的冷笑话:\n\n---\n\n有一天,一只布偶猫跑去宠物店里应聘。 \n店员问:"你有什么特长呢?" \n布偶猫懒洋洋地回答:"我特别软。" \n店员说:"还有呢?" \n猫翻个身,露出肚皮:"你看,我连摸都不需要自己动------你只要一伸手,我自己就'布偶'过去了。"\n\n---\n\n希望能让你会心一笑(或者冷到打个哆嗦)😺❄️'
}
============================== -> illegal <- ==============================
{
'topic': '布偶猫',
'content_type': 'illegal'
}

这里使用:
python
Command[Literal["poem_node", "joke_node", END]]
作为 router() 的返回值类型注解。
注解不能限制 goto 在运行时只能写这些值,而是为了让 LangGraph 和类型检查工具知道:这个节点可能跳转到哪些目标。对于图结构渲染来说,这个注解非常重要。如果不声明,渲染出来的图可能无法正确表达 router 节点的潜在跳转关系。
需要注意的是,如果某个节点使用 Command(goto=...) 控制后续跳转,一般不要再给这个节点额外添加普通下游边。否则,普通边和 Command 指定的跳转都可能生效,导致多个下游节点被同时触发。
因此,本例中只添加:
python
builder.add_edge(START, "router")
而不添加类似下面这样的边:
python
builder.add_edge("router", "poem_node")
builder.add_edge("router", "joke_node")
因为 router 的后续跳转已经由 Command(goto=...) 决定。
小结
动态分支有两类典型应用场景:
| 场景 | 典型 API | 说明 |
|---|---|---|
| 动态扇出 | Send + add_conditional_edges() |
运行时创建多个任务,常用于 Map-Reduce |
| 动态跳转 | Command(goto=...) |
节点内部根据状态决定跳转目标 |
二者的区别如下:
Send更强调"一个节点动态分发多个任务实例";Command(goto=...)更强调"当前节点执行完后动态跳转到哪个节点"。
Send解决的是:运行时要启动多少个任务实例。
Command(goto=...)解决的是:当前节点执行完后要去哪里。
不过,无论是 Send 还是 Command(goto=...),目标节点通常都需要提前注册到图中。所谓"动态",主要是运行时动态决定任务数量、任务输入或跳转路径,而不是运行时临时创建新的节点。