LangGraph 条件边与路由

QuibblerAgentQuibblerAgent 2026-11-04 约 22 分钟 3 次阅读

第 3 篇的多轮翻译实战收尾时,route 函数已经亮过一次相:审校节点跑完,它读一眼黑板上的 done,决定把流程送回 draft 回炉,还是直达 END。当时一句带过,本篇把它背后的机制正面讲透——条件边就是图上的 if,是 LangGraph 全部控制流的灵魂,分支、回环、提前收束全由它承载。

版本口径与全系列一致:langgraph 1.2.x,PyPI 独立发布,示例只依赖 langgraph 一个包,条件边的规则以Graph API 文档的 Edges 一节为准。全文按"固定边的局限 → 基本用法 → 路径映射 → 多路分发 → 设计准则 → 与 reducer 联动 → 客服分流器实战 → 总结"展开。


1、固定边的局限

第 2 篇的五步搭图里,流程全靠 add_edge 串起:从 START 到写提纲,再到写成稿,一条边一个脚印。这种普通边是固定转移——下一站在建图时就已写死,源节点跑完必去,不管黑板上写的是什么。流水线业务这样最省事;流程一旦需要"看情况",固定边立刻露怯。

三类典型诉求:

  • 分支:稿件达标走发布,不达标回炉重写
  • 回环:信息不够,退回去补一轮检索再来
  • 提前收束:前置校验失败,直接带着错误出图

用固定边硬拗这些形状只有两条歪路。要么把 if 塞进节点函数体里手工调用下一步,控制流又缩回代码里,draw_mermaid 打印的拓扑与真实路径对不上;要么为每种组合各建中转节点,节点数随分支数爆炸。

LangGraph 给的是第三条路:条件边。给源节点挂一个路由函数,框架在该节点跑完、增量合入黑板之后调用它,按当前 State 现场决定下一站。两种边对比如下:

维度 普通边 条件边
声明方式 add_edge add_conditional_edges
转移时机 源节点跑完立即去目标 源节点跑完先调路由函数
去向 建图期写死 运行期按 State 决定
典型用途 固定流水线 分支、回环、提前收束

一句话立住定位:普通边表达"接下来一定是它",条件边表达"接下来看情况"。

2、第一个条件边

沿用写作流水线的语境,把流程升级成"写作—审校—按判决处置":初稿交审校,通过则直达终点,不通过回炉修订后再审。State、两个节点与路由函数如下:

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

class State(TypedDict):
    topic: str
    article: str
    passed: bool

def write(state: State) -> dict:
    if state.get("article"):                      # 回炉:在原稿上修订
        return {"article": state["article"] + "(按意见修订)"}
    return {"article": f"围绕[{state['topic']}]的初稿"}

def review(state: State) -> dict:
    return {"passed": "修订" in state["article"]}  # 演示口径:修订过才放行

def after_review(state: State) -> str:
    return END if state["passed"] else "write"    # 通过收工,否则回炉

解读:after_review 就是路由函数——入参是当前 State,返回值是下一站的节点名。它不干活、不返回增量,只回答"去哪";通过时返回 END 常量(值为字符串 "__end__",与 START 同在 langgraph.graph 导入),图到此收束。建图只比第 2 篇多一行:

builder = StateGraph(State)
builder.add_node("write", write)
builder.add_node("review", review)
builder.add_edge(START, "write")
builder.add_edge("write", "review")                    # 固定边:写完必审
builder.add_conditional_edges("review", after_review)  # 条件边:审完看判决
print(builder.compile().invoke({"topic": "条件边"})["article"])

输出如下:

围绕[条件边]的初稿(按意见修订)

走读执行轨迹:write 产出初稿,review 发现稿中没有"修订"字样判不通过,after_review 返回 write,流程沿回边回到写作节点;write 在原稿上补修订标记,二审放行,after_review 返回 END。首次上手记三件事:去向不必逐条 add_edge 声明,路由返回什么框架就找同名节点;返回未注册的名字会在运行期报错;条件写反造成死循环时,第 2 篇的 GraphRecursionError 会兜底。第 1 篇 Agent 循环里判断"还有没有 tool call"的 route,用的正是同一机制。

3、显式路径映射

第 2 节的路由函数直接返回节点名,函数与图拓扑焊死在一起——节点一改名,路由得跟着改。更讲究的写法是让它返回业务标签,翻译交给 add_conditional_edges 的第三个参数 path_map:一个字典,键是路由返回值,值是真正的目标节点。以工单分级为例,进件节点写好等级后分流:

def by_level(state: State) -> str:
    return "vip" if state["level"] >= 5 else "normal"

builder.add_conditional_edges(
    "intake", by_level,
    {"vip": "expert_reply", "normal": "auto_reply"},  # 标签到节点的翻译表
)

解读:路由函数只认业务语义,布线全部集中在 path_map——节点改名、调整去向动一行字典即可,路由一行不改;布尔路由同样常见,官方文档示例正是把 True 与 False 分别映射到两个节点。它还送一份防御红利:路由返回的标签若不在映射表里,运行期立刻报错,配置遗漏当场暴露,而不是静默走错路。完整签名为 add_conditional_edges(source, path, path_map, then),逐参数说明见 add_conditional_edges API 参考;then 参数可为这批分支统一指定跑完之后的下一站,是扇入的速记写法。

4、多路分发与汇入

路由函数不必只返回一个名字——返回节点名列表,一次判断能同时点亮多条支路。官方规则说得很直白:列表里的目标节点将作为下一个超步并行执行。以风控审核为例,高风险账户双线处置,人工介入与自动冻结同时进行;低风险单线归档:

def after_check(state: State) -> list[str]:
    if state["risk"] == "high":
        return ["alert_human", "freeze_account"]   # 高风险:双线并行
    return ["auto_note"]                           # 低风险:单线归档

builder.add_conditional_edges("check", after_check)
for name in ("alert_human", "freeze_account", "auto_note"):
    builder.add_edge(name, END)                    # 三条支路全部汇入终点

拓扑画出来是这样:

flowchart LR S([START]) --> A[风控检查] A -->|高风险| B[通知人工] A -->|高风险| C[冻结账号] A -->|低风险| D[自动归档] B --> E([END]) C --> E D --> E

解读:一次判断、两条箭头同时激活,alert_human 与 freeze_account 在同一超步并行执行,两条都结束图才继续,最后全部汇入 END——这是扇出扇入的最小完整形态。分支节点本身依旧是普通节点,还可以继续挂自己的条件边,嵌套分支不过是条件边套条件边,结构在图上始终一目了然。两个衔接点。其一,并行分支写同一个没有 reducer 的字段会撞上第 3 篇的 InvalidUpdateError,各写各的键或挂合并函数即可化解。其二,条件边也能挂在 START 上做条件入口,图一开始就按输入分路;至于运行前不知开几路、每路还要各带一份 State 的 map-reduce 场景,路由函数可返回 Send 对象,第 10 篇多 Agent 组网再展开。

5、路由函数设计准则

条件边好不好用,九成看路由函数干不干净。四条准则,按踩坑频率排。

其一,只读不写,保持纯函数。路由函数读 State、返回去向,此外什么都不做:不改字段、不发请求、不写日志。苛刻的原因是两个收益都值钱。可恢复性:第 7 篇的持久化会在超步边界打快照、必要时重放,带副作用的路由会让重放不可复现。可测试性:纯函数一行就能断言,不需要建图:

assert after_review({"topic": "t", "article": "初稿",
                     "passed": False}) == "write"
assert after_review({"topic": "t", "article": "初稿(修订)",
                     "passed": True}) == END

解读:两行断言就是路由逻辑的全部测试,图与函数各自独立验证。真需要"边改状态边路由",官方另备 Command 原语——一个返回值同时携带增量与去向,后续实战篇用到再展开。

其二,返回值必须是注册过的节点名或 END。返回不存在的名字运行期直接报错,这是特性不是缺陷:拼错的节点名当场暴露。

其三,同一节点的出边别混用。官方明确警告:固定边与条件边挂在同一节点上时,两条路径都会执行,图的行为将难以推理。要么固定、要么动态,二选一。

其四,判断放对位置。"去哪"的判断属于路由函数,"干什么"的判断属于节点;把业务逻辑堆进路由,它就成了图上看不见的隐形节点。

节点回答"做什么",路由回答"去哪"。两件事分开,图才看得懂、测得动。

6、条件分支与 reducer 联动

第 3 篇讲并发写冲突时留了两条路:给字段挂 reducer 汇合,或让各分支写不同的键。后者与条件边天然般配——分支各写各的字段互不干扰,下游节点按需取用。看一张修稿分诊小图:先给稿件打分(演示口径按长度),过线走轻润色,不过线走重写,最后统一装订:

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

class State(TypedDict):
    text: str
    score: int
    light: str        # 轻润色分支专属
    heavy: str        # 重写分支专属
    final: str

def grade(state: State) -> dict:
    return {"score": len(state["text"])}           # 演示口径:按长度打分

def route(state: State) -> str:
    return "light_polish" if state["score"] >= 10 else "heavy_rewrite"

def light_polish(state: State) -> dict:
    return {"light": state["text"] + "(已润色)"}

def heavy_rewrite(state: State) -> dict:
    return {"heavy": state["text"] + "(已重写)"}

def assemble(state: State) -> dict:
    return {"final": state.get("light") or state.get("heavy")}

建图连边与运行如下:

builder = StateGraph(State)
builder.add_node("grade", grade)
builder.add_node("light_polish", light_polish)
builder.add_node("heavy_rewrite", heavy_rewrite)
builder.add_node("assemble", assemble)
builder.add_edge(START, "grade")
builder.add_conditional_edges("grade", route)    # 打完分当场分流
builder.add_edge("light_polish", "assemble")     # 两分支殊途同归
builder.add_edge("heavy_rewrite", "assemble")
builder.add_edge("assemble", END)
print(builder.compile().invoke({"text": "短句"})["final"])

输出为 短句(已重写)。三个要点:路由读的 score 由上游 grade 写入,"上游产判断依据、路由做裁判"是条件边最常见的配套;两分支各写各的键,assemble 用 get 判空、各取所需,黑板上还能追溯走了哪条路;本例分支互斥,其实写同一个键也不会撞——写冲突只发生在并行扇出,互斥分流与并行汇合的合并策略要分开设计。这是第 3 篇与本篇的合流点:并行汇合交给 reducer,互斥分流用不同的键。

7、客服分流器实战

把前几节合起来做一个够用的真东西:客服分流器。进线先分类,退款类走退款专席,技术类走技术支持,其余走通用答复;分类用关键词规则演示,骨架与真模型完全同构。State 与全部节点如下:

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

class State(TypedDict):
    question: str
    category: str
    answer: str

KEYWORDS = {"退款": "refund", "账号": "tech", "报错": "tech"}

def classify(state: State) -> dict:
    for word, label in KEYWORDS.items():
        if word in state["question"]:
            return {"category": label}
    return {"category": "general"}                # 兜底:认不出的走通用

def refund_desk(state: State) -> dict:
    return {"answer": f"退款已受理:{state['question']}"}

def tech_support(state: State) -> dict:
    return {"answer": f"技术排查中:{state['question']}"}

def general_desk(state: State) -> dict:
    return {"answer": f"常见问题答复:{state['question']}"}

路由与建图如下,path_map 让布线一目了然:

def route(state: State) -> str:
    return state["category"]                      # 路由只认业务标签

builder = StateGraph(State)
builder.add_node("classify", classify)
for name, fn in [("refund_desk", refund_desk),
                 ("tech_support", tech_support),
                 ("general_desk", general_desk)]:
    builder.add_node(name, fn)
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route, {
    "refund": "refund_desk", "tech": "tech_support",
    "general": "general_desk"})                   # 标签到节点的翻译表
for name in ("refund_desk", "tech_support", "general_desk"):
    builder.add_edge(name, END)                   # 三席全部汇入终点
graph = builder.compile()

for q in ("我要退款", "登录报错怎么办", "你们几点上班"):
    print(graph.invoke({"question": q})["answer"])

输出如下:

退款已受理:我要退款
技术排查中:登录报错怎么办
常见问题答复:你们几点上班

解读:classify 产标签、route 传标签、path_map 翻译标签,三段各司其职;三个处理节点都写 answer 键,而每轮 invoke 只有一个分支执行,互斥路径写同键天然安全,印证第 6 节的分界。想确认某条进线实际走了哪条路,用第 2 篇的 stream 配 updates 模式回放,每个片段的键就是实际执行的节点名。升级成真分流器只动一处:把 classify 换成一次模型调用,让它结构化输出 refund、tech、general 三选一,图结构一字不改——条件边把"怎么分流"与"谁来分流"彻底解耦。

8、总结

条件边给图装上了 if:add_conditional_edges(源, 路由函数) 两参数起步,路由读 State 返回节点名,返回 END 收束、返回列表扇出并行;path_map 把业务标签翻译成节点名,拓扑集中一处可维护;纪律三条——路由函数只读不写的纯函数、返回值必须是注册名或 END、同一节点固定边与动态边不混用;合并策略分辖区,互斥分流各写各的键,并行汇合交给 reducer。

练习一道:给客服分流器加第四条"投诉"路径——KEYWORDS 加一个关键词映射到 complaint,path_map 加一条指向新建的 complaint_desk,三处改动跑通新链路。进一步,让投诉命中时返回 ["complaint_desk", "refund_desk"] 双线并行,注意让两个节点写不同的键或给 answer 挂 reducer,正好检验第 3 篇与第 4 节的功课。对于准备继续深入的读者,建议下一站进第 5 篇——create_agent 把本篇的路由机制预制成官方 Agent 图,模型节点与工具循环即将登场。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

75