骑手送餐学 LangGraph:一文彻底看懂“边”的所有玩法(从直路到绕路再到回头路)

0 阅读13分钟

你是个外卖站长,手下有一群骑手。每位骑手就是一个节点(Node),他们需要依次或按规则完成任务。
而你给骑手下达的“下一步去哪儿”的指令,就是边(Edge)。
搞懂了边,你就掌握了 LangGraph 工作流的全部交通规则。
本文不讲高深数学,只用骑手送餐的故事,把普通边、条件边、入口点、循环控制讲得明明白白,每个概念都配有完整可运行的代码和逐行注释。


一、先认识“状态(State)”——骑手们共享的小白板

在 LangGraph 中,所有骑手(节点)共用一个“小白板”,叫做 State
它就像一个订单信息栏:谁取了餐、金额多少、送到哪一步了,所有人都能看、能改。

我们的所有代码都会使用下面这个简单的小白板:

from typing_extensions import TypedDict
 
# 定义状态(小白板)
class GraphState(TypedDict):
    value: int      # 比如订单金额、剩余距离
    step: str       # 当前步骤说明(如“取餐中”“送餐中”)

为什么需要 State?
如果没有 State,每个骑手干活时都不知道之前发生了什么。有了 State,A 骑手把金额加 1 元,B 骑手就能看到新金额并继续处理。State 是图执行过程中传递数据的唯一通道。


二、普通边(Normal Edge)——一条路走到黑,最听话的骑手

2.1 故事版

普通边就是固定不变的单行道
站长说:“小王,你先去 A 店取餐,然后必须去 B 店换电瓶,最后去 C 小区送餐,送完回家。”
骑手不会问为什么,也不会看路上堵不堵,就按这个顺序执行。
这就是普通边:从 A → B → C → END(结束)。

2.2 什么时候用普通边?

  • 工作流是直线流水线:数据清洗 → 特征提取 → 模型预测

  • 没有分支,不需要决策

  • 你想强制按顺序执行

2.3 代码实战(完整 + 保姆级注释)

# 导入必要模块
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
 
# ---------- 1. 定义小白板 ----------
class GraphState(TypedDict):
    value: int
    step: str
 
# ---------- 2. 定义节点函数(骑手)----------
def node_a(state: GraphState) -> dict:
    """骑手 A:把金额加 1 元,并更新步骤说明"""
    print(f"骑手 A 开始工作,当前金额: {state['value']}")
    new_value = state["value"] + 1
    print(f"骑手 A 完成,金额变为: {new_value}")
    return {"value": new_value, "step": "A 执行完毕"}
 
def node_b(state: GraphState) -> dict:
    """骑手 B:把金额翻倍"""
    print(f"骑手 B 开始工作,当前金额: {state['value']}")
    new_value = state["value"] * 2
    print(f"骑手 B 完成,金额变为: {new_value}")
    return {"value": new_value, "step": "B 执行完毕"}
 
def node_c(state: GraphState) -> dict:
    """骑手 C:金额减 1 元(比如优惠)"""
    print(f"骑手 C 开始工作,当前金额: {state['value']}")
    new_value = state["value"] - 1
    print(f"骑手 C 完成,金额变为: {new_value}")
    return {"value": new_value, "step": "C 执行完毕"}
 
# ---------- 3. 创建图构建器 ----------
# StateGraph 需要一个状态类型参数,告诉框架小白板长什么样
builder = StateGraph(GraphState)
 
# ---------- 4. 添加节点(把骑手注册到地图上)----------
builder.add_node("node_a", node_a)  # 第一个参数是节点名(自己起),第二个是实际函数
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
 
# ---------- 5. 添加普通边(固定路线)----------
# START 是 LangGraph 内置的特殊节点,代表“用户输入进入的点”
builder.add_edge(START, "node_a")   # 从 START 到 node_a
builder.add_edge("node_a", "node_b") # 从 node_a 到 node_b
builder.add_edge("node_b", "node_c") # 从 node_b 到 node_c
builder.add_edge("node_c", END)      # 从 node_c 到 END(结束)
 
# ---------- 6. 编译图 ----------
# 编译后得到一个可运行的“应用”
graph = builder.compile()
 
# ---------- 7. 执行图 ----------
print("=== 普通边演示:骑手 A → B → C ===")
# invoke 传入初始状态(必须与 GraphState 定义匹配)
result = graph.invoke({"value": 1, "step": ""})
print(f"最终结果: {result}\n")

运行结果:

=== 普通边演示:骑手 A → B → C ===
骑手 A 开始工作,当前金额: 1
骑手 A 完成,金额变为: 2
骑手 B 开始工作,当前金额: 2
骑手 B 完成,金额变为: 4
骑手 C 开始工作,当前金额: 4
骑手 C 完成,金额变为: 3
最终结果: {'value': 3, 'step': 'C 执行完毕'}

关键点解析:

  • 每个节点函数必须接收一个 state 参数,并返回一个 字典(要更新的状态字段)。

  • 不需要返回全部 State,只返回改变的部分即可,框架会自动合并。

  • add_edge 的两个参数都是节点名(包括 START 和 END 这两个特殊名字)。


三、条件边(Conditional Edge)——会看情况拐弯的聪明骑手

3.1 故事版

条件边就是骑手到达一个点后,要看看小白板上当前的信息,再决定下一步去哪
例如:骑手 A 取完餐后,看一眼订单金额:

  • 如果是偶数,走 B 路线(送高档小区,价格翻倍)

  • 如果是奇数,走 C 路线(送普通小区,价格减 1 元)

这个“看金额决定走哪条路”的函数,叫做路由函数(Routing Function)

3.2 路由函数怎么写?

路由函数接收当前 state,返回一个字符串(目标节点名),或者返回多个字符串(并行执行,后面会讲)。
LangGraph 会根据返回值去查找路由映射表,决定实际跳转到哪个节点。

3.3 完整代码(从 A 分支到 B 或 C)

from typing import Literal   # 用于限制返回值只能是固定的几个字符串
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
 
class GraphState(TypedDict):
    value: int
    step: str
 
def node_a(state: GraphState) -> dict:
    print(f"骑手 A:取餐,当前金额 {state['value']},加 1 元")
    new_value = state["value"] + 1
    return {"value": new_value, "step": "A 执行完毕"}
 
def node_b(state: GraphState) -> dict:
    print(f"骑手 B:高档小区,金额翻倍,当前 {state['value']} -> {state['value']*2}")
    return {"value": state["value"] * 2, "step": "B 执行完毕"}
 
def node_c(state: GraphState) -> dict:
    print(f"骑手 C:普通小区,减 1 元,当前 {state['value']} -> {state['value']-1}")
    return {"value": state["value"] - 1, "step": "C 执行完毕"}
 
# ---------- 路由函数 ----------
def route_condition(state: GraphState) -> Literal["node_b", "node_c"]:
    """根据 state['value'] 决定去 node_b 还是 node_c"""
    # 注意:此时 state 是经过 node_a 更新后的值
    current_val = state["value"]
    if current_val % 2 == 0:
        print(f"路由决策:{current_val} 是偶数,去 node_b")
        return "node_b"
    else:
        print(f"路由决策:{current_val} 是奇数,去 node_c")
        return "node_c"
 
# 创建图
builder = StateGraph(GraphState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_node("node_c", node_c)
 
# 固定入口:总是从 A 开始
builder.add_edge(START, "node_a")
 
# ---------- 添加条件边 ----------
# 从 node_a 出发,使用 route_condition 函数决定下一步
builder.add_conditional_edges(
    "node_a",              # 源节点
    route_condition,       # 路由函数
    {                      # 路由映射:函数返回值 -> 实际节点名
        "node_b": "node_b",
        "node_c": "node_c"
    }
)
 
# B 和 C 结束后都结束
builder.add_edge("node_b", END)
builder.add_edge("node_c", END)
 
graph = builder.compile()
 
# 测试偶数情况(初始 value=2)
print("=== 条件边测试:初始偶数 value=2 ===")
result1 = graph.invoke({"value": 2, "step": ""})
print(f"最终结果: {result1}\n")
 
# 测试奇数情况(初始 value=1)
print("=== 条件边测试:初始奇数 value=1 ===")
result2 = graph.invoke({"value": 1, "step": ""})
print(f"最终结果: {result2}\n")

运行结果分析:

  • 输入 value=2:
    A 执行后 value = 3(奇数) → 路由函数返回 "node_c" → 执行 C → value 变成 2 → 输出 {'value': 2, 'step': 'C 执行完毕'}

  • 输入 value=1:
    A 执行后 value = 2(偶数) → 路由函数返回 "node_b" → 执行 B → value 变成 4 → 输出 {'value': 4, 'step': 'B 执行完毕'}

3.4 多个出口:并行执行

如果一个路由函数返回多个节点名(比如返回一个列表 ["node_b", "node_c"]),那么这些节点会并行执行(在下一个超级步骤中同时运行)。这是 LangGraph 的一个重要特性,可以显著提高效率。

def route_to_multiple(state) -> list:
    if state["some_flag"]:
        return ["node_b", "node_c"]   # 两个节点并行执行
    return ["node_b"]

四、固定入口点(Entry Point)——老板指定第一个骑手

其实我们在前面的例子中已经用过了。
固定入口点就是通过 add_edge(START, "某个节点") 来设置的,它定义了工作流从哪个节点开始。
没有入口点的图是无法执行的,因为框架不知道第一个该调用谁。

4.1 代码回顾

builder.add_edge(START, "node_a")   # 固定从 node_a 开始

就这么简单。你也可以把 START 直接连到任何节点,不一定非叫“node_a”。


五、条件入口点(Conditional Entry Point)——根据订单选第一个骑手

5.1 故事版

有些订单是大额订单,需要先让“超级骑手 D”做预处理(比如加 10 元附加费);
有些是普通订单,直接让“普通骑手 A”开始即可。
老板站在 START 路口,看一眼订单金额:大于 5 元的派 D 先上,否则派 A 先上。
这就是条件入口点:START 后面接的不是固定边,而是一个条件边。

5.2 什么时候用?

  • 多租户系统:不同客户走不同的初始化流程

  • 根据输入参数选择不同的处理链

  • A/B 测试:随机决定从哪个分支开始

5.3 完整代码(条件入口点 + 后续合并)

from typing import Literal
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
 
class GraphState(TypedDict):
    value: int
    step: str
 
def node_a(state: GraphState) -> dict:
    """普通骑手 A:加 1 元"""
    print(f"普通骑手 A:金额 {state['value']} + 1 = {state['value']+1}")
    return {"value": state["value"] + 1, "step": "A 执行完毕"}
 
def node_d(state: GraphState) -> dict:
    """超级骑手 D:加 10 元(大单专属)"""
    print(f"超级骑手 D:金额 {state['value']} + 10 = {state['value']+10}")
    return {"value": state["value"] + 10, "step": "D 执行完毕"}
 
def node_b(state: GraphState) -> dict:
    """最终配送员 B:金额翻倍"""
    print(f"最终配送 B:金额 {state['value']} * 2 = {state['value']*2}")
    return {"value": state["value"] * 2, "step": "B 执行完毕"}
 
# 条件入口路由函数(注意:它接收的是初始 state,还没有经过任何节点)
def entry_condition(state: GraphState) -> Literal["node_a", "node_d"]:
    if state.get("value", 0) > 5:
        print(f"条件入口:订单金额 {state['value']} > 5,派超级骑手 D")
        return "node_d"
    else:
        print(f"条件入口:订单金额 {state['value']} <= 5,派普通骑手 A")
        return "node_a"
 
builder = StateGraph(GraphState)
builder.add_node("node_a", node_a)
builder.add_node("node_d", node_d)
builder.add_node("node_b", node_b)
 
# ----- 关键:条件入口点 -----
# 从 START 出发,使用 entry_condition 选择第一个节点
builder.add_conditional_edges(
    START,
    entry_condition,
    {
        "node_a": "node_a",
        "node_d": "node_d"
    }
)
 
# 无论从 A 还是 D 出来,都去 B,然后结束
builder.add_edge("node_a", "node_b")
builder.add_edge("node_d", "node_b")
builder.add_edge("node_b", END)
 
graph = builder.compile()
 
# 小订单测试
print("=== 条件入口点:小订单 value=3 ===")
r1 = graph.invoke({"value": 3, "step": ""})
print(f"结果: {r1}\n")
 
# 大订单测试
print("=== 条件入口点:大订单 value=10 ===")
r2 = graph.invoke({"value": 10, "step": ""})
print(f"结果: {r2}\n")

运行输出:

=== 条件入口点:小订单 value=3 ===
条件入口:订单金额 3 <= 5,派普通骑手 A
普通骑手 A:金额 3 + 1 = 4
最终配送 B:金额 4 * 2 = 8
结果: {'value': 8, 'step': 'B 执行完毕'}
 
=== 条件入口点:大订单 value=10 ===
条件入口:订单金额 10 > 5,派超级骑手 D
超级骑手 D:金额 10 + 10 = 20
最终配送 B:金额 20 * 2 = 40
结果: {'value': 40, 'step': 'B 执行完毕'}

注意:条件入口点与普通条件边的区别仅在于源节点是 START 而不是一个普通节点。


六、循环控制(Cycle)——让骑手来回跑,直到老板喊停

6.1 故事版

有些任务需要反复执行,比如“检查餐做好了没”:
骑手 A 去厨房看一眼,如果没做好,就绕一圈(经过 B 点)再回来重新看,直到做好为止。
这种 A → B → A 的回路就是循环
但循环必须要有终止条件,否则骑手会跑到天荒地老(死循环)。
我们通过一个条件边来判断:如果做好(达到最大次数),就去 END;否则继续循环。
此外还要设置递归限制(recursion_limit),相当于给骑手一个“最多跑几圈”的安全绳。

6.2 为什么需要循环?

  • 重试逻辑:调用外部 API 失败后重试

  • 迭代计算:牛顿法、梯度下降

  • 多轮对话:AI 需要反复思考直到得出最终答案

  • 状态机:等待某个外部条件满足

6.3 完整代码(计数器循环 + 递归限制)

from typing import Literal
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.errors import GraphRecursionError   # 用于捕获超限异常
 
# 定义循环专用的状态,增加 max_count 字段
class LoopState(TypedDict):
    count: int          # 当前循环次数
    result: str
    max_count: int      # 最大允许循环次数
 
def node_a(state: LoopState) -> dict:
    """骑手 A:执行一次核心任务,并将 count 加 1"""
    current_count = state['count']
    new_count = current_count + 1
    print(f"【循环 {new_count}】骑手 A 开始工作,当前计数: {current_count} -> {new_count}")
    return {
        "count": new_count,
        "result": f"已完成第 {new_count} 次处理"
    }
 
def node_b(state: LoopState) -> dict:
    """骑手 B:辅助处理(比如记录日志、延迟等),不改变 count"""
    print(f"【循环 {state['count']}】骑手 B 辅助记录: {state['result']}")
    # 注意:这里没有返回 count,所以 count 保持不变(框架会自动合并)
    return {
        "result": f"辅助后 -> {state['result']}"
    }
 
def route(state: LoopState) -> Literal["b", END]:
    """
    路由函数:判断是继续循环还是结束。
    当 count >= max_count 时结束,否则继续去 node_b(然后 node_b 会连回 node_a)
    """
    if state['count'] >= state['max_count']:
        print(f"✅ 终止条件满足:当前计数 {state['count']} >= 最大 {state['max_count']},结束循环")
        return END
    else:
        print(f" 继续循环:当前计数 {state['count']} < 最大 {state['max_count']}")
        return "b"
 
# 创建图
builder = StateGraph(LoopState)
builder.add_node("a", node_a)
builder.add_node("b", node_b)
 
# 入口:从 START 到 a
builder.add_edge(START, "a")
 
# 条件边:从 a 出发,根据 route 决定去 END 还是去 b
builder.add_conditional_edges("a", route)
 
# 关键回路:b 执行完后无条件回到 a(形成循环)
builder.add_edge("b", "a")
 
graph = builder.compile()
 
print("=== 循环控制演示:最多循环 3 次 ===")
try:
    result = graph.invoke(
        input={
            'count': 0,
            'result': '',
            'max_count': 3
        },
        config={
            'recursion_limit': 10   # 安全限制:最多执行 10 个超级步骤(每个节点调用算一步)
        }
    )
    print("\n最终结果:")
    print(result)
except GraphRecursionError as e:
    print(f"递归错误(超出限制): {e}")

运行结果解释:

  • 第 1 次:a (count=0→1) → route 判断 1<3 → 去 b → b → 回到 a

  • 第 2 次:a (1→2) → route 判断 2<3 → 去 b → b → 回到 a

  • 第 3 次:a (2→3) → route 判断 3>=3 → 去 END,结束。
    最终 count = 3。

关于递归限制:
recursion_limit 限制了图中节点被调用的总次数(包括重复调用)。如果循环次数超过这个值,框架会抛出 GraphRecursionError,防止程序卡死。
在上面的例子中最大循环 3 次,实际节点调用次数约为 3 次 a + 2 次 b = 5 次,远小于 10,所以安全。


七、总结:所有“边”的类型对比表

类型

决策依据

边数

常见应用场景

比喻

普通边

固定 1 条出边

线性 pipeline

直路,必须走

条件边

状态值

多条可选

分支逻辑、错误处理

看路牌拐弯

固定入口点

1 条出边(START → 节点)

单入口工作流

老板指定第一个人

条件入口点

初始状态

多条可选

多模式入口、A/B测

根据订单选第一个骑手

循环

状态值 + 回路

条件边 + 回路边

迭代、重试、轮询

绕圈直到满意

几个关键原则

  1. 状态是唯一通信方式

    :节点之间不要用全局变量,所有需要共享的数据都放在 State 里。

  2. 每个节点返回字典,只更新变化的字段

    :不要返回整个 State,框架会自动合并。

  3. 循环必须带终止条件

    :并且在 config 中设置合理的 recursion_limit。

  4. 善用条件入口点

    :可以让同一个图服务不同场景,减少重复代码。

  5. 并行执行

    :如果一个节点有多个出边(比如条件边返回列表),目标节点会并行执行,提高效率。