langgraph教程系列-02-让流程分叉-条件边与路由

本文是「LangGraph 教程系列」第 2 篇。写作时基于 langgraph 1.2.10、langchain 1.3.14、langchain-deepseek 1.1.0、Python 3.12+。配套代码仓库 https://github.com/wxj006007/deep-research-assistant ,本篇对应 tag v0.1

上一篇我们用 State / Node / Edge 三件套把 v0 跑了起来,还立了个 flag,链和循环都是图的特例,Agent 需要的判断和分叉只有图能表达。

这一篇就来兑现「分叉」。

一、v0 撞的墙,一条道走到黑

给 v0 提两个问题

LangGraph 是哪家公司开源的?
为什么 Agent 框架纷纷从链式结构转向图结构?

第一个是事实型问题,有客观标准答案,好的回答应该准确、简短。LangChain Inc.,完事。第二个是分析型问题,没有标准答案,好的回答应该多角度展开、给出推理过程。

但 v0 只有一个 answer 节点、一套系统提示词。结果就是两头不讨好,事实型问题它答出一篇小作文,分析型问题它草率地丢一个结论。你想想看,「简洁准确」和「充分展开」这两个目标本身就是矛盾的,一套提示词没法同时优化。

解法很自然,先判断问题类型,再走不同的分支。这在链里做不到,管道在写代码时就焊死了。在图里,这就是一条条件边(conditional edge)。

二、v0.1 的图结构

#mermaid-svg-ceQs6yqWXUNegEIx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ceQs6yqWXUNegEIx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ceQs6yqWXUNegEIx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ceQs6yqWXUNegEIx .error-icon{fill:#552222;}#mermaid-svg-ceQs6yqWXUNegEIx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ceQs6yqWXUNegEIx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ceQs6yqWXUNegEIx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ceQs6yqWXUNegEIx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ceQs6yqWXUNegEIx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ceQs6yqWXUNegEIx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ceQs6yqWXUNegEIx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ceQs6yqWXUNegEIx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ceQs6yqWXUNegEIx .marker.cross{stroke:#333333;}#mermaid-svg-ceQs6yqWXUNegEIx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ceQs6yqWXUNegEIx p{margin:0;}#mermaid-svg-ceQs6yqWXUNegEIx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ceQs6yqWXUNegEIx .cluster-label text{fill:#333;}#mermaid-svg-ceQs6yqWXUNegEIx .cluster-label span{color:#333;}#mermaid-svg-ceQs6yqWXUNegEIx .cluster-label span p{background-color:transparent;}#mermaid-svg-ceQs6yqWXUNegEIx .label text,#mermaid-svg-ceQs6yqWXUNegEIx span{fill:#333;color:#333;}#mermaid-svg-ceQs6yqWXUNegEIx .node rect,#mermaid-svg-ceQs6yqWXUNegEIx .node circle,#mermaid-svg-ceQs6yqWXUNegEIx .node ellipse,#mermaid-svg-ceQs6yqWXUNegEIx .node polygon,#mermaid-svg-ceQs6yqWXUNegEIx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ceQs6yqWXUNegEIx .rough-node .label text,#mermaid-svg-ceQs6yqWXUNegEIx .node .label text,#mermaid-svg-ceQs6yqWXUNegEIx .image-shape .label,#mermaid-svg-ceQs6yqWXUNegEIx .icon-shape .label{text-anchor:middle;}#mermaid-svg-ceQs6yqWXUNegEIx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ceQs6yqWXUNegEIx .rough-node .label,#mermaid-svg-ceQs6yqWXUNegEIx .node .label,#mermaid-svg-ceQs6yqWXUNegEIx .image-shape .label,#mermaid-svg-ceQs6yqWXUNegEIx .icon-shape .label{text-align:center;}#mermaid-svg-ceQs6yqWXUNegEIx .node.clickable{cursor:pointer;}#mermaid-svg-ceQs6yqWXUNegEIx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ceQs6yqWXUNegEIx .arrowheadPath{fill:#333333;}#mermaid-svg-ceQs6yqWXUNegEIx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ceQs6yqWXUNegEIx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ceQs6yqWXUNegEIx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ceQs6yqWXUNegEIx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ceQs6yqWXUNegEIx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ceQs6yqWXUNegEIx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ceQs6yqWXUNegEIx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ceQs6yqWXUNegEIx .cluster text{fill:#333;}#mermaid-svg-ceQs6yqWXUNegEIx .cluster span{color:#333;}#mermaid-svg-ceQs6yqWXUNegEIx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ceQs6yqWXUNegEIx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ceQs6yqWXUNegEIx rect.text{fill:none;stroke-width:0;}#mermaid-svg-ceQs6yqWXUNegEIx .icon-shape,#mermaid-svg-ceQs6yqWXUNegEIx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ceQs6yqWXUNegEIx .icon-shape p,#mermaid-svg-ceQs6yqWXUNegEIx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ceQs6yqWXUNegEIx .icon-shape .label rect,#mermaid-svg-ceQs6yqWXUNegEIx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ceQs6yqWXUNegEIx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ceQs6yqWXUNegEIx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ceQs6yqWXUNegEIx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} factual
analytical
START
classify

判断问题类型
factual_answer

简洁准确地回答
analytical_answer

多角度展开分析
END

实线是普通边(必然走),虚线是条件边(运行时二选一)。整个 v0.1 相比 v0 的变化,answer 节点一分为二,前面加了一个 classify 节点,中间用条件边接起来。

三、动手实现

3.1 State,加一个字段

python 复制代码
class ResearchState(TypedDict):
    question: str
    question_type: str  # "factual" | "analytical"
    answer: str

相比 v0 只多了 question_type。分类结果要写进 state,因为路由的依据必须存在于 state 里。这是 LangGraph 的基本纪律,节点之间不直接传参,一切经过 state。

3.2 分类节点,让 LLM 当裁判

python 复制代码
def classify_node(state: ResearchState) -> dict:
    llm = get_llm()
    response = llm.invoke([
        ("system",
         "你是一个问题分类器。判断用户问题属于哪一类:\n"
         "- factual:有客观标准答案的事实型问题(是什么、何时、多少)\n"
         "- analytical:需要推理和多角度讨论的分析型问题(为什么、如何评价、利弊)\n"
         "只输出 factual 或 analytical 这一个单词,不要输出其他内容。"),
        ("human", state["question"]),
    ])
    label = response.content.strip().lower()
    # 兜底:分类器输出意外内容时按分析型处理
    if label not in ("factual", "analytical"):
        label = "analytical"
    return {"question_type": label}

这里有两个工程细节。一个是强约束输出,提示词里写死「只输出一个单词」。分类节点的输出要喂给程序逻辑,不是给人看的,越死板越好。另一个是必须有兜底,LLM 是概率模型,总有一天它会给你输出 "Factual." 或者一段解释。这个事儿我也踩过坑,strip().lower() 处理大小写和空白,白名单校验兜住其余所有意外。宁可答得啰嗦(走分析型),不可让图在运行时崩掉。

3.3 两个回答节点,各自优化各自的目标

python 复制代码
def factual_answer_node(state: ResearchState) -> dict:
    llm = get_llm()  # temperature=0,要稳
    response = llm.invoke([
        ("system", "你是一个研究助手。这是一个事实型问题,"
                   "请给出准确、简洁的答案,不要展开议论。"),
        ("human", state["question"]),
    ])
    return {"answer": response.content}

def analytical_answer_node(state: ResearchState) -> dict:
    llm = get_llm(temperature=0.7)  # 分析型允许更发散
    response = llm.invoke([
        ("system", "你是一个研究助手。这是一个分析型问题,"
                   "请从多个角度展开分析,给出推理过程,最后总结你的观点。"),
        ("human", state["question"]),
    ])
    return {"answer": response.content}

分支的价值在这里显形。两个节点不只提示词不同,连 temperature 都不同,事实型要 0(稳定可复现),分析型给 0.7(允许发散)。如果没有分叉,这种「按题施策」根本无从谈起。

3.4 路由函数,条件边的扳道工

python 复制代码
def route_by_type(state: ResearchState) -> Literal["factual", "analytical"]:
    return state["question_type"]

就这三行?就这三行。

路由函数的职责被刻意压到最小,读 state,返回一个字符串,别的什么都不干。

新手最容易犯的错是把分类逻辑写进路由函数,在里面调 LLM、做判断。不是说这样跑不起来,而是说它违背了 LangGraph 的设计意图,节点干活,边选路。节点(classify)可以调用模型、可以有副作用、结果写进 state,可以被 checkpoint 记录(第 6 篇会讲为什么这很重要)。路由函数应该是纯函数,只读 state 做决定,快进快出。把它们分开,图的每一步都有据可查。混在一起,路由就成了监控和调试的盲区。

3.5 组装,add_conditional_edges

python 复制代码
builder = StateGraph(ResearchState)
builder.add_node("classify", classify_node)
builder.add_node("factual_answer", factual_answer_node)
builder.add_node("analytical_answer", analytical_answer_node)

builder.add_edge(START, "classify")
builder.add_conditional_edges(
    "classify",          # 从哪个节点出发
    route_by_type,       # 用哪个函数选路
    {                    # 返回值 -> 目标节点 的映射
        "factual": "factual_answer",
        "analytical": "analytical_answer",
    },
)
builder.add_edge("factual_answer", END)
builder.add_edge("analytical_answer", END)
graph = builder.compile()

add_conditional_edges 三个参数,出发节点、路由函数、映射表。映射表把路由函数的返回值翻译成目标节点名。这里两个键恰好和节点名部分重合,但它们是两个命名空间,路由函数返回的是「路名」,映射表才决定「路通向哪」。

第三个参数可以省略,此时路由函数直接返回节点名。但我自己的感受是,显式写映射表更划算,LangGraph 能据此画出准确的图结构,而且路由逻辑和图拓扑解耦,改节点名不用改路由函数。

3.6 跑起来

python 复制代码
questions = [
    "LangGraph 是哪家公司开源的?",
    "为什么 Agent 框架纷纷从链式结构转向图结构?",
]
for question in questions:
    result = graph.invoke({"question": question})
    print(f"分类:{result['question_type']}")
    print(f"回答:{result['answer']}\n")

第一个问题被分到 factual,回答一两句话。第二个被分到 analytical,回答分点展开。同一个图,两条执行路径,这就是条件边。

四、几个值得记住的点

顺着上面的再聊聊,有三个点值得单独拎出来。

条件边是「运行时」的决策。普通边在你写代码时就定了,条件边到 invoke 那一刻才知道走哪条。这是图对链最根上的超越,流程结构可以响应数据。

路由依据一定要落在 state 里。你可能想,分类和路由合成一步多省事。这个想法不是没道理,代码确实能少几行。但 state 里留下 question_type,好处是调试时能看到分类结果,下游节点能引用它,将来做 checkpoint 回放时它是历史的一部分。state 是图的记忆,别绕过它抄近道。

分支可以不止两条。映射表想加几个键就加几个键。将来你的助手可能要区分闲聊、事实、分析、需要检索四种类型,条件边的写法不变,只是映射表变长。

五、本篇小结

回到「分叉」这块,v0 撞的墙是一套提示词无法同时服务事实型和分析型问题,解法是 classify 节点判断类型,再用 add_conditional_edges 按类型选路。过程中我们顺手立了三条纪律,节点干活边选路,路由函数保持纯函数。路由依据写进 state,不走私下传参。LLM 分类必须有兜底,白名单校验防止图崩溃。

v0.1 会分叉了,但它还有个隐患没暴露。目前每个字段都是「整存整取」,新值直接覆盖旧值。等到下一步我们让助手做多轮检索,问题就来了,第二次检索的结果会把第一次的覆盖掉。检索结果应该是累积的,不是覆盖的。

状态如何累加而不是覆盖?下一篇,reducer 登场。

赞或收藏 关注 我们下次再见

相关推荐
_itgo10 小时前
LangGraph 主要3 种核心模式
ai·langchain·langgraph
糖果店的幽灵11 小时前
人已经用 WorkBuddy 找工作拿了面试,你还在一份份手工改简历
人工智能·面试·职场和发展·langgraph
Esaka_Forever21 小时前
为什么要学习 LangGraph,LangChain 与 LangGraph 的适用边界
langgraph
人间凡尔赛2 天前
2026多智能体系统深度解析:从GPT-5.6 Ultra到开源框架,构建你的Agent军团
ai·agent·多智能体·langgraph·crewai·gpt-5.6
Esaka_Forever2 天前
StateGraph —— LangGraph 的核心基石
langgraph
uncle_ll3 天前
LangGraph 深度解析:用图结构构建下一代智能代理与多智能体系统
langchain·llm·agent·graph·langgraph
糖果店的幽灵3 天前
大模型测评DeepEval快速入门-RAG指标详解
数据库·人工智能·langgraph·大模型测评·deepeval
其实防守也摸鱼3 天前
镜像校验完成iso完整操作流程(Windows 环境,适配 Ubuntu 22.04 / 24.04)
linux·服务器·windows·学习·ubuntu·教程
糖果店的幽灵4 天前
2026 年最强Obsidian保姆级教程,10分钟打造你的第二大脑
人工智能·langgraph