langgraph分支之 - 动态分支(Dynamic Branch)
文章目录动态分支Dynamic Branch并行节点条件分支Command介绍用Command实现条件分支小结动态分支Dynamic Branch定义节点的后续执行路径在运行时才确定可以根据当前状态、输入数据或中间结果动态决定要触发哪些下游任务或跳转到哪个下游节点。特点下游执行目标可以在运行时选择下游任务数量可以在运行时决定可以为同一个下游节点动态创建多个执行任务适合实现 Map-Reduce 式动态扇出、多任务并行处理、运行时条件跳转等场景。需要注意的是所谓“动态”通常不是指运行时临时创建新的节点定义。节点本身一般仍需要在图编译前注册。动态性主要体现在运行时决定触发哪些节点以及为这些节点创建多少个执行任务。典型用法Send动态扇出任务/数据Command(goto...)运行时跳转到下游节点⚡ 核心判断运行时决定后续执行目标或任务数量 → 动态分支并行节点Send结合add_conditional_edges()使用可以用于动态扇出任务所谓扇出Fan-out是指一个上游节点像扇子一样向外分发出多个下游任务。例如根据一个主题同时生成诗、词、笑话三个任务又如根据一个列表为列表中的每个元素动态启动一个处理任务。具体来说路由函数可以返回一个Send实例序列。每个Send实例都描述了一次独立的任务分发分发到哪个下游节点给这个下游节点传入什么私有状态。运行时LangGraph会根据返回的每个Send实例创建对应的任务。这些任务通常会在同一个超步中并行执行。Send类构造器源码如下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.nodenode self.argarg该构造器接收两个参数并记录为实例属性node待启动的下游节点名称必须是已在状态图中注册的合法名称arg传递给下游节点的信息仅对**node**指向的节点可见通常应是私有状态。每个Send实例接收到的arg是独立的互不相干。实例如下fromtypingimportTypedDict,Literalfromcollections.abcimportSequencefromlanggraph.graphimportStateGraph,START,ENDfromlanggraph.typesimportSendfromlangchain.messagesimportHumanMessagefromlangchain_deepseekimportChatDeepSeekfromdotenvimportload_dotenv load_dotenv(overrideTrue)CONTENT_TYPES[poem,ci_poem,joke]modelChatDeepSeek(modeldeepseek-v4-flash,extra_body{thinking:{type:disabled}})classOverAllState(TypedDict):topic:strpoem:strci_poem:strjoke:strclassWorkerState(TypedDict): 私有状态只对 Worker 节点可用 content_type:Literal[poem,ci_poem,joke]prompt:strclassInputState(TypedDict):topic:strclassOutputState(TypedDict):poem:strci_poem:strjoke:strdefworker_node(state:WorkerState)-OutputState:content_typestate[content_type]promptstate[prompt]contentmodel.invoke([HumanMessage(prompt)]).contentreturn{content_type:content}defrouter(state:InputState)-Sequence[Send]:prompt请生成关于 {} 的 {}english2chinese{poem:一首诗,ci_poem:一首词,joke:一个笑话}topicstate[topic]return[Send(worker_node,{content_type:content_type,prompt:prompt.format(topic,english2chinese[content_type]),})forcontent_typeinCONTENT_TYPES]builderStateGraph(state_schemaOverAllState,input_schemaInputState,output_schemaOutputState)builder.add_node(worker_node,worker_node)builder.add_conditional_edges(START,router,path_map[worker_node])builder.add_edge(worker_node,END)graphbuilder.compile()resgraph.invoke({topic:布偶狗})print(res)fromIPython.displayimportdisplay display(graph)在这个案例中router()并没有直接返回某一个固定的下游节点名称而是返回了多个Send实例[Send(worker_node,{content_type:poem,...}),Send(worker_node,{content_type:ci_poem,...}),Send(worker_node,{content_type:joke,...}),]因此运行时会动态创建三个指向worker_node的任务。三个任务执行的是同一个节点函数但它们接收到的私有状态不同所以可以分别生成诗、词和笑话。输出如下{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中显式声明了可能被路由到的下游节点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更适合“状态更新”和“控制流跳转”需要放在同一个节点返回值中的场景。例如一个节点既要更新状态又要根据当前状态决定下一步跳转目标就可以返回returnCommand(update{foo:bar},gotonext_node)示例如下fromtypingimportTypedDict,Literalfromlanggraph.typesimportCommandfromlanggraph.graphimportStateGraph,START,ENDfromlangchain.messagesimportHumanMessagefromlangchain_deepseekimportChatDeepSeekfromdotenvimportload_dotenv load_dotenv(overrideTrue)modelChatDeepSeek(modeldeepseek-v4-flash,extra_body{thinking:{type:disabled}})classOverAllState(TypedDict):topic:strcontent_type:Literal[poem,joke]poem:strjoke:strdefrouter(state:OverAllState)-Command[Literal[poem_node,joke_node,END]]:content_typestate[content_type]ifcontent_typepoem:returnCommand(gotopoem_node)elifcontent_typejoke:returnCommand(gotojoke_node)returnCommand(gotoEND)defpoem_node(state:OverAllState)-OverAllState:topicstate[topic]promptf请生成一首关于{topic}的七言绝句poemmodel.invoke([HumanMessage(prompt)]).contentreturn{poem:poem}defjoke_node(state:OverAllState)-OverAllState:topicstate[topic]promptf请生成一个关于{topic}的冷笑话jokemodel.invoke([HumanMessage(prompt)]).contentreturn{joke:joke}builderStateGraph(state_schemaOverAllState)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)graphbuilder.compile()poem_resgraph.invoke({topic:布偶猫,content_type:poem})joke_resgraph.invoke({topic:布偶猫,content_type:joke})# 这里故意传入一个不符合 Literal[poem, joke] 的值# 用来观察运行时兜底分支。# 注意Literal 主要服务于静态类型检查# 默认不会在 Python 运行时阻止非法值传入。illegal_resgraph.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)fromIPython.displayimportdisplay display(graph)上述代码中router()节点根据content_type的值决定下一步跳转目标当content_type poem时跳转到poem_node当content_type joke时跳转到joke_node当传入其他非法值时跳转到END图直接结束。结果如下-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}这里使用Command[Literal[poem_node,joke_node,END]]作为router()的返回值类型注解。注解不能限制goto在运行时只能写这些值而是为了让 LangGraph 和类型检查工具知道这个节点可能跳转到哪些目标。对于图结构渲染来说这个注解非常重要。如果不声明渲染出来的图可能无法正确表达router节点的潜在跳转关系。需要注意的是如果某个节点使用Command(goto...)控制后续跳转一般不要再给这个节点额外添加普通下游边。否则普通边和Command指定的跳转都可能生效导致多个下游节点被同时触发。因此本例中只添加builder.add_edge(START,router)而不添加类似下面这样的边builder.add_edge(router,poem_node)builder.add_edge(router,joke_node)因为router的后续跳转已经由Command(goto...)决定。小结动态分支有两类典型应用场景场景典型 API说明动态扇出Sendadd_conditional_edges()运行时创建多个任务常用于Map-Reduce动态跳转Command(goto...)节点内部根据状态决定跳转目标二者的区别如下Send更强调“一个节点动态分发多个任务实例”Command(goto...)更强调“当前节点执行完后动态跳转到哪个节点”。Send解决的是运行时要启动多少个任务实例。Command(goto...)解决的是当前节点执行完后要去哪里。不过无论是Send还是Command(goto...)目标节点通常都需要提前注册到图中。所谓“动态”主要是运行时动态决定任务数量、任务输入或跳转路径而不是运行时临时创建新的节点。