langgraph分支之 - 动态分支(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 声明候选下游节点后,渲染图才能正确展示 STARTworker_node 的条件边关系。

补充说明:如果多个并行任务写入同一个状态字段,通常需要为该字段定义 reducer,用于合并多个任务的输出。本例中三个任务分别写入 poemci_poemjoke 三个不同字段,因此不会发生同一字段的并发合并问题。


条件分支
Command介绍

CommandLangGraph 中用于控制图执行的多功能原语,它的构造器可以接受四个参数,并记录在同名类属性中:

  • 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=...),目标节点通常都需要提前注册到图中。所谓"动态",主要是运行时动态决定任务数量、任务输入或跳转路径,而不是运行时临时创建新的节点。

相关推荐
这张生成的图像能检测吗1 小时前
(论文速读)SCNN:用于交通场景理解的空间CNN
人工智能·深度学习·目标检测·计算机视觉·道路线检测
@insist1231 小时前
系统规划与管理师-流程评价与持续改进核心知识-终章
大数据·人工智能·软考·系统规划与管理师·软件水平考试·系统规划与管理工程师
meilindehuzi_a1 小时前
Workflow 与 Agent 有什么区别:从 LangChain 流水线到智能体决策
前端·人工智能
SLD_Allen1 小时前
大规模分布式AI训练基础设施
人工智能·分布式·模型训练
这是个栗子1 小时前
前端开发中的常用工具函数(九)
开发语言·javascript·ecmascript·at
码农学院1 小时前
AI优化AIO技术演进史:从模板生成到多智能体协同创作
人工智能
科技发布2 小时前
拓氪科技 AI 内容引擎软文投放效果详解
人工智能·科技
nanawinona2 小时前
2026年下半年量化学习,不同基础要查不同缺口
人工智能·python
唐青枫2 小时前
Java Spring Security 实战详解:从登录认证到 JWT 权限控制
java