4.2.1. 静态分支(Static Branch)
- 定义 :节点的下游候选节点 在图编译阶段就完全确定,只是运行时根据条件选择哪条边执行。
- 特点:
- 下游节点集合固定,数量、目标在编译时确定
- 运行时可选择一个或多个下游目标
- 可以用来做条件分支,但不生成新的节点
⚡ 核心判断:
编译期知道下游集合 → 静态分支
4.2.1.1. 并行节点
并行节点是最简单的静态分支形式。
当多个节点都从同一个上游节点触发时,它们会在同一个超步被激活。典型写法如下:
builder.add_edge(START, "node_a")
builder.add_edge(START, "node_b")
示例:两个节点并行执行
from dotenv import load_dotenv
from langchain_deepseek import ChatDeepSeek
from typing import TypedDict
from typing import NotRequired
from langgraph.graph import StateGraph, START, END
from IPython.display import display
load_dotenv(verbose=True)
model=ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={"thinking": {"type": "enabled"}},
)
#1 定义状态
class OverAllState(TypedDict):
topic: NotRequired[str]
poem: NotRequired[str]
joke: NotRequired[str]
# 2 定义节点
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke('写一首关于' + state["topic"] + '的诗')
return {
"poem": poem.content,
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke('写一个关于' + state["topic"] + '的笑话')
return {
"joke": joke.content,
}
# 3 定义边
builder=StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
# 4 定义边 并行
builder.add_edge(START, "node_a")
builder.add_edge(START, "node_b")
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
# 5 编译图
graph = builder.compile()
result = graph.invoke({"topic": "猫咪"})
# 6 打印结果
print(result)
display(graph)
上述案例中,node_a 和 node_b 都由 START 触发。图运行时,二者会在同一个超步中被调度,它们各自读取当前状态并独立执行。
需要注意的是:
- 这里的"并行"主要指调度语义上的并行;两个节点之间没有先后依赖;
- 它们的输出会在当前超步执行完成后统一合并到状态中;
- 如果两个节点写入同一个状态字段,则该字段通常需要配置合适的
Reducer,否则可能出现状态更新冲突,抛出InvalidUpdateError异常
输出如下
{
"topic": "猫咪",
"poem": "《猫咪》\n夜巡檐角步轻悄,昼卧花阴晒暖毛。\n偶逐流莺穿柳过,忽惊蝴蝶上眉梢。\n\n赏析:这首作品以猫咪日常为切入点,通过"夜巡檐角"、"昼卧花阴"展现其昼伏夜出的习性,"逐流莺"、"惊蝴蝶"则捕捉其灵动瞬间。全诗未著"猫"字,却以"轻悄步"、"晒暖毛"等细节勾勒出猫咪的形神特征,尾句"上眉梢"更将好奇神态融入蝴蝶意象,物我交融,余韵悠长。",
"joke": "有一只猫走进了一家披萨店,店员问:"您好,请问您要什么?"\n猫淡定地说:"喵~(我要一个12寸的海鲜披萨,加双倍芝士。)"\n店员愣了愣,说:"猫先生,您确定?"\n猫点点头,店员只好照做,然后猫掏出钱包付了钱,趴在小桌子上优雅地吃了起来。\n\n这时,旁边桌的客人看呆了,忍不住问店员:"它怎么能像人一样点餐、付钱、吃披萨?"\n店员耸耸肩,小声说:"我也不知道,但自从它上次用我的电脑打了一局《猫里奥》通关后,我就没敢拒绝它的任何要求。""
}

4.2.1.2. 条件分支
StateGraph 提供了 add_conditional_edges 方法,用于从某个上游节点出发,根据运行时状态选择下游节点。
def add_conditional_edges(
self,
source: str,
path: Callable[..., Hashable | Sequence[Hashable]]
| Callable[..., Awaitable[Hashable | Sequence[Hashable]]]
| Runnable[Any, Hashable | Sequence[Hashable]],
path_map: dict[Hashable, str] | list[str] | None = None,
) -> Self:
不考虑 self,核心参数有三个:
source:条件分支的起始节点;path:路由规则,是一个可执行对象,通常是函数path_map:路由规则的返回值到真实节点名之间的映射关系。
其中,path 的返回值表示跳转的目标节点,可以是:
- 字符串或特殊对象
END表示的单个目标; - 字符串或特殊对象
END表示的多个目标组成的序列;
path_map
-
可以省略,即取默认值
None,此时path返回值中出现的字符串必须是合法的节点名称。 -
可以是字典,维护
path返回值和真实节点的映射。 -
也可以是列表,如下
path_map=["node_a", "node_b", "node_c"]
相当于
path_map={
"node_a": "node_a",
"node_b": "node_b",
"node_c": "node_c",
}
- 此时也要求
path返回值中出现的字符串必须是合法的节点名称。
4.2.1.2.1. 不使用 path_map
如果不传 path_map,那么路由函数的返回值通常应当直接是图中的节点名称。如下
def router(state: OverAllState) -> Literal["node_a", "node_b"]:
if "诗" in state["content_type"]:
return "node_a"
return "node_b"
此时,router 返回的 "node_a" 和 "node_b" 必须能够直接对应图中已经注册的节点名。
完整案例如下
from dotenv import load_dotenv
from langchain_deepseek import ChatDeepSeek
from typing import TypedDict
from typing import NotRequired,Literal
from langgraph.graph import StateGraph, START, END
from IPython.display import display
load_dotenv(verbose=True)
model=ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={"thinking": {"type": "enabled"}},
)
#1 定义状态
class OverAllState(TypedDict):
topic: NotRequired[str]
poem: NotRequired[str]
joke: NotRequired[str]
content_type:NotRequired[str]
# 2 定义节点
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke('写一首关于' + state["topic"] + '的诗')
return {
"poem": poem.content,
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke('写一个关于' + state["topic"] + '的笑话')
return {
"joke": joke.content,
}
def my_route (state: OverAllState) -> Literal['node_a', 'node_b']:
if "诗" in state["content_type"]:
return "node_a"
return "node_b"
# 3 定义边
builder=StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
# 4 定义边 条件执行
builder.add_conditional_edges(START,my_route)
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
# 5 编译图
graph = builder.compile()
result = graph.invoke({"topic": "猫咪","content_type":'笑话'})
# 6 打印结果
print(result)
display(graph)
输出如下
{'topic': '猫咪', 'joke': '我家猫特别爱监督我工作。\n\n昨天我写代码写到半夜,它跳上键盘踩了一串乱码,然后抬头看我。\n\n我说:"你删我代码干嘛?"\n\n它喵了一声。\n\n我翻译了一下,大概意思是:"不是删,是帮你加个注释:此处有猫,勿扰。"', 'content_type': '笑话'}

观察图结构可以发现,__start__指向node_a和node_b的线是虚线,这表示节点的跳转是有条件的,不是固定的。
4.2.1.2.2. 使用 path_map
如果不希望路由函数直接返回节点名,而是返回业务语义更强的标识,可以使用 path_map 进行映射。如下:
def router(state: OverAllState) -> Literal["a", "b"]:
if "诗" in state["content_type"]:
return "a"
return "b"
builder.add_conditional_edges(
START,
router,
path_map={
"a": "node_a",
"b": "node_b",
}
)
此时返回的 "a" 和 "b" 可以不是图中已注册的节点名称,但要通过 path_map 映射到正确的节点
这种写法的好处是:
- 路由函数可以返回业务含义更清晰的标签;
- 图节点名称可以保持工程化命名;
- 渲染图结构时,边上可以显示路由标签,使图更容易理解。
示例如下
from dotenv import load_dotenv
from langchain_deepseek import ChatDeepSeek
from typing import TypedDict
from typing import NotRequired,Literal
from langgraph.graph import StateGraph, START, END
from IPython.display import display
load_dotenv(verbose=True)
model=ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={"thinking": {"type": "enabled"}},
)
#1 定义状态
class OverAllState(TypedDict):
topic: NotRequired[str]
poem: NotRequired[str]
joke: NotRequired[str]
content_type:NotRequired[str]
# 2 定义节点
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke('写一首关于' + state["topic"] + '的诗')
return {
"poem": poem.content,
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke('写一个关于' + state["topic"] + '的笑话')
return {
"joke": joke.content,
}
def my_route (state: OverAllState) -> Literal['joke', 'poem']:
if "诗" in state["content_type"]:
return "poem"
return "joke"
# 3 定义边
builder=StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
# 4 定义边 条件执行
builder.add_conditional_edges(START,my_route,path_map={
"poem":"node_a",
"joke":"node_b",
})
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
# 5 编译图
graph = builder.compile()
result = graph.invoke({"topic": "猫咪","content_type":'笑话'})
# 6 打印结果
print(result)
display(graph)
输出如下
{'topic': '猫咪', 'joke': '我家猫特别聪明,会自己开门。有天半夜它把我卧室门打开,我以为它想我了。结果它站在门口看了我一眼,确认我还活着、还能上班买猫粮,然后转身走了。', 'content_type': '笑话'}

观察图结构可以发现,__start__指向node_a和node_b的虚线上出现了文本a和b,它们是路由函数返回的名称,通过path_map映射到具体的下游节点。
4.2.1.2.3. 同时路由至多个节点
add_conditional_edges 也支持一次路由到多个下游节点。
1. 不用path_map
from dotenv import load_dotenv
from langchain_deepseek import ChatDeepSeek
from typing import TypedDict
from typing import NotRequired,Literal,Sequence
from langgraph.graph import StateGraph, START, END
from IPython.display import display
load_dotenv(verbose=True)
model=ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={"thinking": {"type": "enabled"}},
)
#1 定义状态
class OverAllState(TypedDict):
topic: NotRequired[str]
poem: NotRequired[str]
joke: NotRequired[str]
ci_poem: NotRequired[str]
content_type:NotRequired[str]
# 2 定义节点
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke('写一首关于' + state["topic"] + '的诗')
return {
"poem": poem.content,
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke('写一个关于' + state["topic"] + '的笑话')
return {
"joke": joke.content,
}
def node_c(state: OverAllState) -> OverAllState:
ci_poem = model.invoke("写一个关于" + state["topic"] + "的词")
return {
"ci_poem": ci_poem.content,
}
def my_route(
state: OverAllState,
) -> Sequence[Literal["node_a", "node_b", "node_c"]]:
if "诗" in state["content_type"]:
return ["node_a", "node_c"]
return ["node_b", "node_c"]
# 3 定义边
builder=StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
# 4 定义边 条件执行
builder.add_conditional_edges(START,my_route)
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
builder.add_edge("node_c", END)
# 5 编译图
graph = builder.compile()
result = graph.invoke({"topic": "猫咪","content_type":'笑话'})
# 6 打印结果
print(result)
display(graph)
在这个例子中:
- 当
content_type包含"诗"时,同时触发node_a和node_c; - 否则同时触发
node_b和node_c。
输出如下
{'topic': '猫咪', 'joke': '猫咪去面试程序员。\n\n面试官问:"你最大的优点是什么?"\n\n猫说:"我特别擅长 Ctrl+C。"\n\n面试官又问:"那缺点呢?"\n\n猫看了看桌上的小鱼干:"我会顺手把鱼也 Ctrl+C......然后 Ctrl+X。"\n\n面试官:"所以你是复制还是吃完?"\n\n猫舔舔爪子:"吃完叫剪切,懂吧?"', 'ci_poem': '喵星人', 'content_type': '笑话'}

观察图结构可以发现,node_a、node_b和node_c独立于图结构之外。
和上一节案例相比,router函数返回的是序列而非单个节点,渲染器无法推断节点间的映射关系。
2. 添加映射
通过 path_map 显示声明映射关系,明确下游节点集合,在提升代码可读性的同时,也有助于渲染器正确展示条件边。
当前场景下 path 返回值中的字符串就是合法的节点名称,path_map 可以是字典
path_map={
"node_a": "node_a",
"node_b": "node_b",
"node_c": "node_c",
}
也可以是列表
path_map=["node_a", "node_b", "node_c"]
示例如下
from dotenv import load_dotenv
from langchain_deepseek import ChatDeepSeek
from typing import TypedDict
from typing import NotRequired,Literal,Sequence
from langgraph.graph import StateGraph, START, END
from IPython.display import display
load_dotenv(verbose=True)
model=ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={"thinking": {"type": "enabled"}},
)
#1 定义状态
class OverAllState(TypedDict):
topic: NotRequired[str]
poem: NotRequired[str]
joke: NotRequired[str]
ci_poem: NotRequired[str]
content_type:NotRequired[str]
# 2 定义节点
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke('写一首关于' + state["topic"] + '的诗')
return {
"poem": poem.content,
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke('写一个关于' + state["topic"] + '的笑话')
return {
"joke": joke.content,
}
def node_c(state: OverAllState) -> OverAllState:
ci_poem = model.invoke("写一个关于" + state["topic"] + "的词")
return {
"ci_poem": ci_poem.content,
}
def my_route(
state: OverAllState,
) -> Sequence[Literal["poem", "joke", "ci_poem"]]:
if "诗" in state["content_type"]:
return ["poem", "ci_poem"]
return ["joke", "ci_poem"]
# 3 定义边
builder=StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
# 4 定义边 条件执行
builder.add_conditional_edges(START,my_route,path_map={
"poem":"node_a",
"joke":"node_b",
"ci_poem":"node_c",
})
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
builder.add_edge("node_c", END)
# 5 编译图
graph = builder.compile()
result = graph.invoke({"topic": "猫咪","content_type":'笑话'})
# 6 打印结果
print(result)
display(graph)
输出如下
{'topic': '猫咪', 'joke': '我家猫半夜在客厅疯狂跑酷,我质问它:"你到底在抓什么?" \n它停下来看了我一眼,说:"抓你明天要用的精神。"', 'ci_poem': '喵星人', 'content_type': '笑话'}

如图所示,图结构被正确渲染。
4.2.1.3. defer node execution
某些情况下,我们希望在所有常规任务节点执行完毕后,再进行日志、审计等收尾工作
此时可以在添加节点时设置defer=True,如下:
builder.add_node("audit_node", audit_node, defer=True)
defer=True 的含义是:
当前节点不会在其被触发后立即执行,而是被延迟到常规图运行流程结束后,再在额外的超步中触发执行。
这类节点适合用于:
- 日志记录;
- 审计检查;
- 结果汇总;
- 收尾清理;
- 统一校验前面节点是否已完成。
4.2.1.3.1. 底层实现机制

1. 编译阶段:使用特殊 Channel
-
LangGraph在编译状态图时,会为边创建对应的Channel。 -
此时会根据节点的
defer属性创建不同类型的Channel,如下。self.channels[branch_channel] = (
LastValueAfterFinish(Any)
if node.defer
else EphemeralValue(Any, guard=False)
) -
defer默认值为False对于普通节点,边对应的通道类型是
EphemeralValue,可以理解为普通临时通道;而对于defer=True的节点,边对应的通道类型是特殊的LastValueAfterFinish。
2. 常规运行阶段-写入但不触发
- 在图运行过程中,每个节点执行完成后,会向其下游边对应的
Channel写入数据。 - 常规的
Channel在运行开始后处于可用状态,被写入后记录在updated_channels列表中,从而在下一个超步中触发下游节点的执行。 - 但是,
LastValueAfterFinish类型的通道起初是不可用的,首次被写入时不会添加到updated_channels列表中,下游节点自然不会被触发。
3. 常规流程结束后:调用finish()唤醒延迟节点
LangGraph底层用trigger_to_nodes维护了 边的Channel-> 节点 的映射,是一个字典。- 在每个超步结束后,运行时会根据
updated_channels判断是否还有新的节点需要被触发。 - 如果
updated_channels和trigger_to_nodes的 key 没有交集,说明当前没有新的普通节点需要继续执行,常规运行流程已结束。 - 此时,
LangGraph运行时会调用所有Channel的finish()方法。 - 对于普通
Channel,finish()通常不会产生新的触发效果;但对于LastValueAfterFinish类型通道,首次调用finish()时,会将内部的finished标记设置为True,并返回True。 - 一旦
finished=True,该通道的is_available()就会变为True。 - 于是,原本被延迟的通道会被加入
updated_channels,从而在额外的超步中触发对应的defer节点。
4. 总结
因此,defer=True 的运行机制可以概括为:
触发边的
Channel为特殊类型,首次写入不触发;常规流程结束后,特殊通道finish();通道变为可用;从而触发延迟节点,后者在额外超步中执行。
4.2.1.3.2. 案例
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from loguru import logger
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
poem: str
joke: str
def node_a(state: OverAllState) -> OverAllState:
poem = model.invoke([HumanMessage(f"写一首关于 {state['topic']} 的七言绝句")]).content
return {
"poem": poem
}
def node_b(state: OverAllState) -> OverAllState:
joke = model.invoke([HumanMessage(f"写一个关于 {state['topic']} 的笑话")]).content
return {
"joke": joke
}
def audit_node(state: OverAllState) -> OverAllState:
logger.info(f"任务节点全部执行完毕,诗 {'已生成' if state['poem'] else '未生成'},笑话 {'已生成' if state['joke'] else '未生成'}")
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("audit_node", audit_node, defer=True)
builder.add_edge(START, "node_a")
builder.add_edge(START, "node_b")
builder.add_edge(START, "audit_node")
builder.add_edge("node_a", END)
builder.add_edge("node_b", END)
builder.add_edge("audit_node", END)
graph = builder.compile()
res = graph.invoke({"topic": "布偶狗"})
print(res)
from IPython.display import display
display(graph)
在这个图中:
node_a和node_b是普通节点,在常规流程中被触发;audit_node虽然也由START触发,但由于设置了defer=True,不会立即执行;node_a和node_b都执行完成后,常规运行流程结束,audit_node才会在额外的超步中执行;- 因此,
audit_node中可以读取到node_a和node_b已经写入并提交后的状态。
输出如下
2026-05-28 17:07:09.618 | INFO | __main__:audit_node:40 - 任务节点全部执行完毕,诗 已生成,笑话 已生成
{
'topic': '布偶狗',
'poem': '《咏布偶狗》\n'
'绸身棉腑自憨娇,静卧无眠守寂寥。\n'
'不吠不嗔街巷事,偎人一晌解无聊。\n'
'\n'
'注:我的创作思路是通过拟人化手法,赋予布偶狗以温柔静默的品格。'
'首句以"绸身棉腑"点明材质,次句"守寂寥"暗合宠物陪伴的特质。'
'后两句通过"不吠不嗔"与"解无聊"的对比,凸显布偶狗无声胜有声的治愈力量,'
'在机械玩物中注入人文温度。',
'joke': '这是一个关于布偶狗的笑话:\n'
'\n'
'有一只布偶狗,它什么都好,毛茸茸的,蓝眼睛,性格温顺,'
'唯一的缺点就是------它是个布偶,不会动。\n'
'\n'
'它的主人每天都把它放在沙发上,假装它在看电视、在思考狗生。\n'
'\n'
'有一天,主人带了一只真的活狗回家,想让布偶狗有个伴。'
'活狗非常活泼,对着布偶狗又叫又摇尾巴,但布偶狗一动不动。\n'
'\n'
'活狗很奇怪,绕着布偶狗转了三圈,然后凑上去闻了闻。\n'
'\n'
'最后,活狗叹了口气,对主人说:\n'
'\n'
'"主人,这只狗......是不是在用'静默模式'跟我交流?'
'它的蓝牙是不是断了?"'
}

4.2.1.3.3. 小结
defer=True 适合"最后执行"的收尾节点。
它的本质是通过特殊的 Channel 控制触发时机:
- 编译阶段为延迟节点创建
LastValueAfterFinish类型通道; - 常规运行阶段该通道可以被写入,但不可用,不会触发下游节点;
- 常规流程结束后调用通道的
finish()方法; finish()首次生效后将通道标记为可用;- 延迟节点在额外超步中被触发执行。
因此,
defer=True将节点的触发信号延迟释放,使该节点在常规流程结束后执行。