LangGraph 系列 · 第 5 讲:条件边与动态路由(让智能体“会决策”)

41 阅读18分钟

📖 1. 导读

💡本文是 LangGraph 系列的第 5 讲。如果说第 4 讲让智能体“会行动”(能调用工具),那么第 5 讲要解决的是:让智能体能根据输入与状态,选择不同的执行路径——也就是“会决策”。

这一讲重点讲清两个核心能力:

  • 条件边(Conditional Edges):根据条件函数的返回值,动态选择下一步去哪个节点
  • 动态路由(Routing):把“选择哪条路径”的逻辑做成一个 Router(规则/模型/混合),实现可扩展的多分支工作流

1.1 本讲你将学会

  • ✅ 什么时候该用条件边,什么时候该用多个图/多个子流程
  • ✅ 3 种常用路由策略:
  • ✅ 如何用add_conditional_edges()写出可维护的多分支路由
  • Prompt Chaining + Routing:先分类再分步处理的链式工作流
  • Parallelization + Routing:同一意图下并行多个子任务,再聚合输出
  • ✅ 如何把路由做成一个“智能客服分流”式的综合案例

1.2 前置要求

  • 🛠️ Python 3.10+
  • 推荐安装:pip install -U langgraph langchain-core
  • LLM(示例沿用 DeepSeek)

🧠 2. 为什么需要“路由”?

当你的智能体开始变复杂时,你会自然遇到这些问题:

  • 用户问题类型很多:价格/退款/技术/知识库/闲聊
  • 不同类型的请求,需要不同的提示词、不同的工具、不同的权限
  • 你不希望“一条链”里塞所有逻辑,否则会:

**路由(Routing)**的价值:先识别“该走哪条路”,再进入“专用流程”。

官方文档把 Routing 作为一种核心工作流模式:先处理输入,再将其导向上下文相关的任务流(比如把“退款问题”导向退款流程)。

🧱 3. 条件边(Conditional Edges)怎么理解?

在 LangGraph 里,边分两类:

  • 普通边add_edge("A", "B"),固定从 A 到 B
  • 条件边add_conditional_edges("A", route_fn, mapping),从 A 出发,由route_fn(state)决定下一步去哪里

条件边的关键点是:

  1. route_fn 必须是纯函数风格:只读 state,返回一个“路由标签/节点名”
  2. mapping 是约束:把“允许的返回值”映射到“实际节点”
  3. 可扩展:你可以从 2 分支扩到 10 分支,但不要在route_fn里写成一坨 if-else(后面会给更好的组织方式)

🧪 4. 实战一:规则路由(最稳定、最推荐做兜底)

适合场景:

  • 需求简单、分类明确(关键词/正则/结构化字段)
  • 追求稳定性(不希望模型“路由错”)

下面示例:把用户输入分到 3 个分支:math/search/chat

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    user_input: str
    route: str
    output: str

def router_rule(state: State) -> dict:
    text = state["user_input"].lower()
    if any(k in text for k in ["计算", "+", "-", "*", "/", "多少"]):
        route = "math"
    elif any(k in text for k in ["搜索", "查一下", "资料", "wiki"]):
        route = "search"
    else:
        route = "chat"
    return {"route": route}

def node_math(state: State) -> dict:
    return {"output": "这里执行计算逻辑(示例略)"}

def node_search(state: State) -> dict:
    return {"output": "这里执行搜索逻辑(示例略)"}

def node_chat(state: State) -> dict:
    return {"output": "这里执行闲聊逻辑(示例略)"}

def route_decision(state: State) -> str:
    # 只返回一个“标签”,让 mapping 决定下一跳
    return state["route"]

builder = StateGraph(State)
builder.add_node("router", router_rule)
builder.add_node("math", node_math)
builder.add_node("search", node_search)
builder.add_node("chat", node_chat)

builder.add_edge(START, "router")
builder.add_conditional_edges(
    "router",
    route_decision,
    {
        "math": "math",
        "search": "search",
        "chat": "chat",
    },
)
builder.add_edge("math", END)
builder.add_edge("search", END)
builder.add_edge("chat", END)

graph = builder.compile()
print(graph.invoke({"user_input": "帮我计算 12 * 37 + 9", "route": "", "output": ""})["output"])

规则路由的小建议

  • 把“规则”集中管理:用一个表/字典存关键词,而不是散落在 if-else 里
  • ✅ 先做一个规则兜底(比如把高风险/高确定性的分流先锁死)
  • ⚠️ 不要用规则承载“语义理解”,那是模型的强项(下一节)

🤖 5. 实战二:LLM 路由(结构化输出 + 条件边)

适合场景:

  • 分类靠语义理解(“这个问题更像退款还是技术故障?”)
  • 类别会扩展(未来会加更多分支)

核心做法:

  1. 用一个router_llm节点:让模型输出一个结构化字段(比如decision
  2. 用条件边:根据decision跳到对应节点

下面示例参考官方 Routing 思路(用结构化输出做路由逻辑),改成中文“故事/段子/诗歌”三选一。

from typing import TypedDict
from typing_extensions import Literal
from pydantic import BaseModel, Field

from langgraph.graph import StateGraph, START, END
from langchain_core.messages import SystemMessage, HumanMessage
from langchain.chat_models import init_chat_model

llm = init_chat_model(model="deepseek-chat", model_provider="deepseek")

class Route(BaseModel):
    step: Literal["poem", "story", "joke"] = Field(
        description="下一步走 poem/story/joke 中的一个"
    )

router = llm.with_structured_output(Route)

class State(TypedDict):
    input: str
    decision: str
    output: str

def llm_call_router(state: State) -> dict:
    decision = router.invoke(
        [
            SystemMessage(content="根据用户请求,选择输出 poem / story / joke 中最合适的一种。只返回你的选择。"),
            HumanMessage(content=state["input"]),
        ]
    )
    return {"decision": decision.step}

def write_story(state: State) -> dict:
    msg = llm.invoke(f"写一个关于『{state['input']}』的短故事,100字以内。")
    return {"output": msg.content}

def write_joke(state: State) -> dict:
    msg = llm.invoke(f"写一个关于『{state['input']}』的段子,尽量好笑。")
    return {"output": msg.content}

def write_poem(state: State) -> dict:
    msg = llm.invoke(f"写一首关于『{state['input']}』的小诗,4~8行。")
    return {"output": msg.content}

def route_decision(state: State) -> str:
    return state["decision"]

builder = StateGraph(State)
builder.add_node("router", llm_call_router)
builder.add_node("story", write_story)
builder.add_node("joke", write_joke)
builder.add_node("poem", write_poem)

builder.add_edge(START, "router")
builder.add_conditional_edges(
    "router",
    route_decision,
    {
        "story": "story",
        "joke": "joke",
        "poem": "poem",
    },
)
builder.add_edge("story", END)
builder.add_edge("joke", END)
builder.add_edge("poem", END)

graph = builder.compile()
print(graph.invoke({"input": "猫咪", "decision": "", "output": ""})["output"])

LLM 路由的工程注意事项(很重要)

  • 一定要结构化输出:不要让模型随便输出文本再做字符串包含判断(不稳定)
  • 最好有兜底分支:当模型输出意外值时,统一 fallback 到安全路径
  • 把路由粒度控制住:路由只负责“选路”,不要在路由节点里做重活(否则 debug 很痛苦)

🧩 6. 实战三:综合案例——“智能客服”动态分流

这是最常见、最实用的路由案例之一:先识别用户问题属于哪一类,再进入专用流程。

我们把问题分成四类:

  • pricing:价格/套餐/费用
  • refund:退款/退订/取消
  • tech:技术故障/报错/登录失败
  • other:其他/闲聊

6.1 设计要点

  • 路由层:只产出intent
  • 业务层:每个分支用不同 system prompt(甚至不同工具/权限)
  • 可扩展:未来新增deliveryinvoice只需要加一个节点 + 更新 mapping
from typing import TypedDict
from typing_extensions import Literal
from pydantic import BaseModel, Field

from langgraph.graph import StateGraph, START, END
from langchain.chat_models import init_chat_model
from langchain_core.messages import SystemMessage, HumanMessage

llm = init_chat_model(model="deepseek-chat", model_provider="deepseek")

class Intent(BaseModel):
    intent: Literal["pricing", "refund", "tech", "other"] = Field(
        description="用户意图分类:pricing/refund/tech/other"
    )

intent_router = llm.with_structured_output(Intent)

class State(TypedDict):
    user_input: str
    intent: str
    reply: str

def classify_intent(state: State) -> dict:
    result = intent_router.invoke(
        [
            SystemMessage(
                content=(
                    "你是客服分流路由器。只做分类,不要回答用户问题。\n"
                    "请把用户输入分类到 pricing/refund/tech/other 四类之一。"
                )
            ),
            HumanMessage(content=state["user_input"]),
        ]
    )
    return {"intent": result.intent}

def handle_pricing(state: State) -> dict:
    msg = llm.invoke(
        [
            SystemMessage(content="你是价格与套餐客服,回答要清晰、结构化,必要时用表格。"),
            HumanMessage(content=state["user_input"]),
        ]
    )
    return {"reply": msg.content}

def handle_refund(state: State) -> dict:
    msg = llm.invoke(
        [
            SystemMessage(content="你是退款与退订客服,先给流程步骤,再给注意事项。"),
            HumanMessage(content=state["user_input"]),
        ]
    )
    return {"reply": msg.content}

def handle_tech(state: State) -> dict:
    msg = llm.invoke(
        [
            SystemMessage(content="你是技术支持,优先问关键排查信息,并给出可执行的排障步骤。"),
            HumanMessage(content=state["user_input"]),
        ]
    )
    return {"reply": msg.content}

def handle_other(state: State) -> dict:
    msg = llm.invoke(
        [
            SystemMessage(content="你是通用客服,保持友好、简洁。"),
            HumanMessage(content=state["user_input"]),
        ]
    )
    return {"reply": msg.content}

def route_intent(state: State) -> str:
    return state["intent"]

builder = StateGraph(State)
builder.add_node("router", classify_intent)
builder.add_node("pricing", handle_pricing)
builder.add_node("refund", handle_refund)
builder.add_node("tech", handle_tech)
builder.add_node("other", handle_other)

builder.add_edge(START, "router")
builder.add_conditional_edges(
    "router",
    route_intent,
    {
        "pricing": "pricing",
        "refund": "refund",
        "tech": "tech",
        "other": "other",
    },
)
builder.add_edge("pricing", END)
builder.add_edge("refund", END)
builder.add_edge("tech", END)
builder.add_edge("other", END)

graph = builder.compile()
print(graph.invoke({"user_input": "我想取消订阅,怎么退款?", "intent": "", "reply": ""})["reply"])

🔗 7. 实战四:Prompt Chaining + Routing(先分类再分步处理)

这是生产环境中最常见的组合模式之一:先路由到不同分支,然后在每个分支内进行多步骤的链式处理(每个 LLM 调用处理前一个的输出)。

7.1 适用场景

  • 文档翻译:先识别目标语言,再执行“翻译 → 校对 → 润色”的链式流程
  • 内容生成:先分类(技术文档/营销文案),再执行“大纲 → 初稿 → 优化”的链式流程
  • 质量检查:先分类(代码审查/文档审查),再执行“检查 → 修复 → 验证”的链式流程

7.2 核心设计思路

  1. 第一层:路由层(识别类型/语言/领域)
  2. 第二层:链式处理层(每个分支内,多个节点顺序执行,后一个处理前一个的输出)

下面示例:多语言文档翻译系统,先路由到目标语言,然后在每个语言分支内执行“翻译 → 校对 → 润色”三步链式处理。

from typing import TypedDict
from typing_extensions import Literal
from pydantic import BaseModel, Field

from langgraph.graph import StateGraph, START, END
from langchain.chat_models import init_chat_model
from langchain_core.messages import SystemMessage, HumanMessage

llm = init_chat_model(model="deepseek-chat", model_provider="deepseek")

class LanguageRoute(BaseModel):
    target_lang: Literal["en", "ja", "ko"] = Field(
        description="目标语言:en(英语)/ja(日语)/ko(韩语)"
    )

lang_router = llm.with_structured_output(LanguageRoute)

class State(TypedDict):
    source_text: str
    target_lang: str
    translated: str
    proofread: str
    polished: str

def route_language(state: State) -> dict:
    """路由层:识别目标语言"""
    result = lang_router.invoke(
        [
            SystemMessage(
                content=(
                    "你是语言路由助手。根据用户输入,识别目标翻译语言。\n"
                    "如果用户明确提到语言(如'翻译成英语'),选择对应语言;\n"
                    "否则根据上下文推断,默认选择 en。"
                )
            ),
            HumanMessage(content=f"用户输入:{state['source_text']}"),
        ]
    )
    return {"target_lang": result.target_lang}

def translate_to_en(state: State) -> dict:
    """英语分支:第一步 - 翻译"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是专业的中英翻译,准确传达原文意思。"),
            HumanMessage(content=f"请将以下中文翻译成英语:\n{state['source_text']}"),
        ]
    )
    return {"translated": msg.content}

def proofread_en(state: State) -> dict:
    """英语分支:第二步 - 校对"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是英语校对专家,检查语法、用词和流畅度。"),
            HumanMessage(
                content=f"请校对以下英语翻译,修正错误并提升表达:\n{state['translated']}"
            ),
        ]
    )
    return {"proofread": msg.content}

def polish_en(state: State) -> dict:
    """英语分支:第三步 - 润色"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是英语润色专家,让文本更自然、地道。"),
            HumanMessage(
                content=f"请润色以下英语文本,使其更符合母语者习惯:\n{state['proofread']}"
            ),
        ]
    )
    return {"polished": msg.content}

def translate_to_ja(state: State) -> dict:
    """日语分支:第一步 - 翻译"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是专业的中日翻译,准确传达原文意思。"),
            HumanMessage(content=f"请将以下中文翻译成日语:\n{state['source_text']}"),
        ]
    )
    return {"translated": msg.content}

def proofread_ja(state: State) -> dict:
    """日语分支:第二步 - 校对"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是日语校对专家,检查语法、用词和敬语使用。"),
            HumanMessage(
                content=f"请校对以下日语翻译,修正错误并提升表达:\n{state['translated']}"
            ),
        ]
    )
    return {"proofread": msg.content}

def polish_ja(state: State) -> dict:
    """日语分支:第三步 - 润色"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是日语润色专家,让文本更自然、地道。"),
            HumanMessage(
                content=f"请润色以下日语文本,使其更符合母语者习惯:\n{state['proofread']}"
            ),
        ]
    )
    return {"polished": msg.content}

def translate_to_ko(state: State) -> dict:
    """韩语分支:第一步 - 翻译"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是专业的中韩翻译,准确传达原文意思。"),
            HumanMessage(content=f"请将以下中文翻译成韩语:\n{state['source_text']}"),
        ]
    )
    return {"translated": msg.content}

def proofread_ko(state: State) -> dict:
    """韩语分支:第二步 - 校对"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是韩语校对专家,检查语法、用词和敬语使用。"),
            HumanMessage(
                content=f"请校对以下韩语翻译,修正错误并提升表达:\n{state['translated']}"
            ),
        ]
    )
    return {"proofread": msg.content}

def polish_ko(state: State) -> dict:
    """韩语分支:第三步 - 润色"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是韩语润色专家,让文本更自然、地道。"),
            HumanMessage(
                content=f"请润色以下韩语文本,使其更符合母语者习惯:\n{state['proofread']}"
            ),
        ]
    )
    return {"polished": msg.content}

def route_by_lang(state: State) -> str:
    """路由函数:根据目标语言选择分支"""
    return state["target_lang"]

# 构建图
builder = StateGraph(State)

# 路由节点
builder.add_node("router", route_language)

# 英语分支节点(链式)
builder.add_node("translate_en", translate_to_en)
builder.add_node("proofread_en", proofread_en)
builder.add_node("polish_en", polish_en)

# 日语分支节点(链式)
builder.add_node("translate_ja", translate_to_ja)
builder.add_node("proofread_ja", proofread_ja)
builder.add_node("polish_ja", polish_ja)

# 韩语分支节点(链式)
builder.add_node("translate_ko", translate_to_ko)
builder.add_node("proofread_ko", proofread_ko)
builder.add_node("polish_ko", polish_ko)

# 连接:START → router
builder.add_edge(START, "router")

# 路由:router → 各语言分支
builder.add_conditional_edges(
    "router",
    route_by_lang,
    {
        "en": "translate_en",
        "ja": "translate_ja",
        "ko": "translate_ko",
    },
)

# 英语分支链式连接
builder.add_edge("translate_en", "proofread_en")
builder.add_edge("proofread_en", "polish_en")
builder.add_edge("polish_en", END)

# 日语分支链式连接
builder.add_edge("translate_ja", "proofread_ja")
builder.add_edge("proofread_ja", "polish_ja")
builder.add_edge("polish_ja", END)

# 韩语分支链式连接
builder.add_edge("translate_ko", "proofread_ko")
builder.add_edge("proofread_ko", "polish_ko")
builder.add_edge("polish_ko", END)

graph = builder.compile()

# 测试
result = graph.invoke({
    "source_text": "人工智能正在改变我们的生活方式。",
    "target_lang": "",
    "translated": "",
    "proofread": "",
    "polished": "",
})
print(f"最终润色结果:{result['polished']}")

7.3 Prompt Chaining + Routing 的关键要点

  • 路由层与处理层分离:路由只负责分类,不处理业务逻辑
  • 每个分支独立链式处理:不同分支可以有不同的处理步骤数
  • 状态传递清晰:链式节点通过 state 字段传递中间结果(如translated → proofread → polished
  • 可扩展性强:新增语言只需添加一组节点和一条路由映射

⚡ 8. 实战五:Parallelization + Routing(同一意图下并行多个子任务)

这是提升效率的经典组合:先路由到不同分支,然后在某个分支内并行执行多个独立子任务,最后聚合结果。

8.1 适用场景

  • 内容生成:先分类(技术文档/营销文案),然后并行生成“标题/正文/摘要/关键词”
  • 数据分析:先分类(销售数据/用户行为),然后并行执行“统计/可视化/报告生成”
  • 多维度评估:先分类(代码审查/文档审查),然后并行检查“性能/安全性/可读性”

8.2 核心设计思路

  1. 第一层:路由层(识别任务类型)
  2. 第二层:并行处理层(多个独立节点同时执行,都从 START 或同一节点出发)
  3. 第三层:聚合层(等待所有并行任务完成,合并结果)

下面示例:智能内容生成系统,先路由到不同类型(技术文档/营销文案),然后在每个类型内并行生成“标题/正文/摘要/关键词”,最后聚合输出。

from typing import TypedDict
from typing_extensions import Literal
from pydantic import BaseModel, Field

from langgraph.graph import StateGraph, START, END
from langchain.chat_models import init_chat_model
from langchain_core.messages import SystemMessage, HumanMessage

llm = init_chat_model(model="deepseek-chat", model_provider="deepseek")

class ContentTypeRoute(BaseModel):
    content_type: Literal["tech", "marketing"] = Field(
        description="内容类型:tech(技术文档)/marketing(营销文案)"
    )

type_router = llm.with_structured_output(ContentTypeRoute)

class State(TypedDict):
    topic: str
    content_type: str
    title: str
    body: str
    summary: str
    keywords: str
    final_output: str

def route_content_type(state: State) -> dict:
    """路由层:识别内容类型"""
    result = type_router.invoke(
        [
            SystemMessage(
                content=(
                    "你是内容类型路由器。根据用户主题,判断应该生成技术文档还是营销文案。\n"
                    "- 技术类主题(如'API使用'、'系统架构')→ tech\n"
                    "- 营销类主题(如'产品推广'、'品牌宣传')→ marketing"
                )
            ),
            HumanMessage(content=f"主题:{state['topic']}"),
        ]
    )
    return {"content_type": result.content_type}

# ========== 技术文档分支:并行生成节点 ==========
def generate_tech_title(state: State) -> dict:
    """技术文档:生成标题"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是技术文档专家,生成清晰、专业的标题。"),
            HumanMessage(content=f"为主题『{state['topic']}』生成一个技术文档标题。"),
        ]
    )
    return {"title": msg.content}

def generate_tech_body(state: State) -> dict:
    """技术文档:生成正文"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是技术文档专家,生成详细、结构化的正文内容。"),
            HumanMessage(content=f"为主题『{state['topic']}』生成技术文档正文(500-800字)。"),
        ]
    )
    return {"body": msg.content}

def generate_tech_summary(state: State) -> dict:
    """技术文档:生成摘要"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是技术文档专家,生成简洁、准确的摘要。"),
            HumanMessage(content=f"为主题『{state['topic']}』生成技术文档摘要(100-150字)。"),
        ]
    )
    return {"summary": msg.content}

def generate_tech_keywords(state: State) -> dict:
    """技术文档:生成关键词"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是技术文档专家,提取关键术语和标签。"),
            HumanMessage(content=f"为主题『{state['topic']}』提取5-8个技术关键词。"),
        ]
    )
    return {"keywords": msg.content}

# ========== 营销文案分支:并行生成节点 ==========
def generate_marketing_title(state: State) -> dict:
    """营销文案:生成标题"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是营销文案专家,生成吸引眼球、有感染力的标题。"),
            HumanMessage(content=f"为主题『{state['topic']}』生成一个营销文案标题。"),
        ]
    )
    return {"title": msg.content}

def generate_marketing_body(state: State) -> dict:
    """营销文案:生成正文"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是营销文案专家,生成有说服力、情感化的正文内容。"),
            HumanMessage(content=f"为主题『{state['topic']}』生成营销文案正文(300-500字)。"),
        ]
    )
    return {"body": msg.content}

def generate_marketing_summary(state: State) -> dict:
    """营销文案:生成摘要"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是营销文案专家,生成简洁、有吸引力的摘要。"),
            HumanMessage(content=f"为主题『{state['topic']}』生成营销文案摘要(50-100字)。"),
        ]
    )
    return {"summary": msg.content}

def generate_marketing_keywords(state: State) -> dict:
    """营销文案:生成关键词"""
    msg = llm.invoke(
        [
            SystemMessage(content="你是营销文案专家,提取营销关键词和标签。"),
            HumanMessage(content=f"为主题『{state['topic']}』提取5-8个营销关键词。"),
        ]
    )
    return {"keywords": msg.content}

# ========== 聚合节点 ==========
def aggregate_tech_content(state: State) -> dict:
    """技术文档:聚合所有并行结果"""
    output = f"# {state['title']}\n\n"
    output += f"## 摘要\n{state['summary']}\n\n"
    output += f"## 正文\n{state['body']}\n\n"
    output += f"## 关键词\n{state['keywords']}"
    return {"final_output": output}

def aggregate_marketing_content(state: State) -> dict:
    """营销文案:聚合所有并行结果"""
    output = f"【标题】{state['title']}\n\n"
    output += f"【摘要】{state['summary']}\n\n"
    output += f"【正文】\n{state['body']}\n\n"
    output += f"【关键词】{state['keywords']}"
    return {"final_output": output}


def route_by_type(state: State) -> str:
    """路由函数:根据内容类型选择分支"""
    return state["content_type"]

# 构建图
builder = StateGraph(State)

# 路由节点
builder.add_node("router", route_content_type)

# 技术文档分支:并行节点
builder.add_node("tech_title", generate_tech_title)
builder.add_node("tech_body", generate_tech_body)
builder.add_node("tech_summary", generate_tech_summary)
builder.add_node("tech_keywords", generate_tech_keywords)
builder.add_node("tech_aggregator", aggregate_tech_content)

# 营销文案分支:并行节点
builder.add_node("marketing_title", generate_marketing_title)
builder.add_node("marketing_body", generate_marketing_body)
builder.add_node("marketing_summary", generate_marketing_summary)
builder.add_node("marketing_keywords", generate_marketing_keywords)
builder.add_node("marketing_aggregator", aggregate_marketing_content)

# 连接:START → router
builder.add_edge(START, "router")

# 路由:router → 各内容类型分支的入口节点
builder.add_conditional_edges(
    "router",
    route_by_type,
    {
        "tech": "tech_parallel_entry",  # 技术文档分支入口
        "marketing": "marketing_parallel_entry",  # 营销文案分支入口
    },
)

# ========== 技术文档分支:真正的并行执行 ==========
def tech_parallel_entry(state: State) -> dict:
    """技术文档分支入口:触发并行任务"""
    # 这个节点不做实际工作,只是作为并行任务的起点
    # 实际工作中,可以在这里设置并行标志
    return {}

# 技术文档分支:4个并行节点都从入口节点出发
builder.add_node("tech_parallel_entry", tech_parallel_entry)
builder.add_edge("tech_parallel_entry", "tech_title")
builder.add_edge("tech_parallel_entry", "tech_body")
builder.add_edge("tech_parallel_entry", "tech_summary")
builder.add_edge("tech_parallel_entry", "tech_keywords")

# 所有并行节点完成后,都指向聚合节点
builder.add_edge("tech_title", "tech_aggregator")
builder.add_edge("tech_body", "tech_aggregator")
builder.add_edge("tech_summary", "tech_aggregator")
builder.add_edge("tech_keywords", "tech_aggregator")
builder.add_edge("tech_aggregator", END)

# ========== 营销文案分支:真正的并行执行 ==========
def marketing_parallel_entry(state: State) -> dict:
    """营销文案分支入口:触发并行任务"""
    return {}

# 营销文案分支:4个并行节点都从入口节点出发
builder.add_node("marketing_parallel_entry", marketing_parallel_entry)
builder.add_edge("marketing_parallel_entry", "marketing_title")
builder.add_edge("marketing_parallel_entry", "marketing_body")
builder.add_edge("marketing_parallel_entry", "marketing_summary")
builder.add_edge("marketing_parallel_entry", "marketing_keywords")

# 所有并行节点完成后,都指向聚合节点
builder.add_edge("marketing_title", "marketing_aggregator")
builder.add_edge("marketing_body", "marketing_aggregator")
builder.add_edge("marketing_summary", "marketing_aggregator")
builder.add_edge("marketing_keywords", "marketing_aggregator")
builder.add_edge("marketing_aggregator", END)

graph = builder.compile()

# 测试
result = graph.invoke({
    "topic": "如何使用 LangGraph 构建智能体",
    "content_type": "",
    "title": "",
    "body": "",
    "summary": "",
    "keywords": "",
    "final_output": "",
})
print(result["final_output"])

8.3 并行执行的工作原理

在上面的示例中,我们使用了入口节点tech_parallel_entry/marketing_parallel_entry)来触发并行执行:

  1. 路由层router根据内容类型,路由到对应的入口节点
  2. 并行层:入口节点同时连接到 4 个并行节点(title/body/summary/keywords)
  3. 聚合层:所有并行节点完成后,都指向聚合节点;LangGraph 会自动等待所有前置节点完成后再执行聚合节点

关键点

  • ✅ 多个节点从同一个节点出发 → 它们会并行执行
  • ✅ 多个节点都指向同一个聚合节点→ LangGraph 会等待所有节点完成后再执行聚合节点
  • ✅ 这种模式可以显著提升性能:4 个独立任务并行执行,总时间 ≈ 最慢的那个任务的时间

注意:如果并行节点之间有依赖关系,应该使用链式连接(Prompt Chaining),而不是并行连接。

8.4 Parallelization + Routing 的关键要点

  • 并行节点独立性:并行执行的节点之间不应该有依赖关系
  • 聚合节点等待:聚合节点需要等待所有并行任务完成(LangGraph 会自动处理)
  • 路由后并行:先路由到分支,再在分支内并行,避免跨分支的混乱
  • 性能提升明显:3-4 个并行任务可以显著缩短总执行时间

🔍 9. 路由设计的最佳实践(强烈建议看)

7.1 把“路由标签”与“节点名”分离

很多人会让route_fn直接返回节点名,这当然能跑,但长期维护会痛。

更推荐:

  • route_fn返回业务意图标签(pricing/refund/tech)
  • mapping 决定标签对应哪个节点(未来节点换名字不影响路由逻辑)

7.2 路由要“可观测”

建议把decision/intent放到 state 里(你已经在上面这么做了),这样:

  • debug 时能直接看到“路由怎么选的”
  • 可以统计每类请求占比(运营/产品很喜欢)

7.3 兜底策略(避免线上事故)

路由输出如果不在 mapping 里,建议统一 fallback:

  • 回到other(低风险回答)
  • 或走human_handoff(第 6 讲的人机协作会讲)

7.4 多分支不是越多越好

建议遵循:

  • 3~6 个一级分支最舒服
  • 更细的分流放在二级路由(路由节点之间也可以再路由)

📌 10. 小结

本讲你已经掌握了让智能体“会决策”的关键能力:

  • ✅ 条件边add_conditional_edges()的用法与心智模型
  • ✅ 规则路由(稳定兜底)与 LLM 路由(语义分流)
  • Prompt Chaining + Routing:先路由分类,再在每个分支内进行链式处理(翻译→校对→润色)
  • Parallelization + Routing:先路由分类,再在分支内并行执行多个独立任务,最后聚合结果
  • ✅ 综合案例:智能客服分流工作流

核心模式总结

模式适用场景关键特点
规则路由分类明确、追求稳定if/else 判断,最可靠
LLM 路由语义理解、类别扩展结构化输出,最灵活
Prompt Chaining + Routing多步骤处理、质量要求高先分类,再链式处理
Parallelization + Routing独立子任务、性能要求高先分类,再并行执行