LangGraph 条件边:让 AI Agent 学会“做选择”

目录

一、为什么你又卡住了?

[二、为什么需要 Conditional Edge?](#二、为什么需要 Conditional Edge?)

(一)条件分支决策

(二)后端开发类比

[(三)Conditional Edge 解决什么问题?](#(三)Conditional Edge 解决什么问题?)

三、核心原理

[(一)Conditional Edge 的三要素](#(一)Conditional Edge 的三要素)

[1. Source Node](#1. Source Node)

[2. Router Function](#2. Router Function)

[3. Path Map](#3. Path Map)

(二)执行流程

四、源码分析

(一)add_conditional_edges()

[(二)_get_next_nodes(self, current_node, state)](#(二)_get_next_nodes(self, current_node, state))

(三)两个必须记住的细节

[1. 路由结果必须命中 path_map](#1. 路由结果必须命中 path_map)

[2. LangGraph 不会自动检测死循环](#2. LangGraph 不会自动检测死循环)

五、实战示例

(一)完整设计代码

(二)关键代码说明

[1. 场景一:简单二选一](#1. 场景一:简单二选一)

[2. 场景二:多路分支](#2. 场景二:多路分支)

[3. 场景三:循环重试](#3. 场景三:循环重试)

(三)运行效果

如何理解这三个场景?

场景一:运行时决策

场景二:入口即分流

场景三:重试机制

六、常见坑与排查

[(一)坑 1:返回值不在 path_map 中](#(一)坑 1:返回值不在 path_map 中)

现象

原因

解决

[(二)坑 2:循环没有退出条件](#(二)坑 2:循环没有退出条件)

现象

原因

解决

[(三)坑 3:在 Router 中修改 State](#(三)坑 3:在 Router 中修改 State)

错误示例

正确写法

[(四)坑 4:普通边与条件边混用](#(四)坑 4:普通边与条件边混用)

错误

正确

七、工程化实践与生产级建议

(一)工程化实践

[1. 防死循环](#1. 防死循环)

[2. 路由全覆盖](#2. 路由全覆盖)

[3. 增加可观测性](#3. 增加可观测性)

[4. 设计默认分支](#4. 设计默认分支)

(二)生产级方案

八、总结

下一篇预告


干货分享,感谢您的阅读!

本文是「LangGraph 实战」系列第 3 篇。前两篇我们已经掌握了:

  • 如何构建线性执行流程(StateGraph)
  • 如何利用 Reducer 合并状态(State)

但真正的 Agent 从来不会只走直线。它需要会分叉、会循环、会重试。这一篇,我们给图装上"大脑"------Conditional Edge(条件边)

一、为什么你又卡住了?

回到系列开头的内容审核需求。现在你已经能写出这样的工作流:

php 复制代码
提交文章 -> AI 评分 -> 发布

于是你信心满满地准备实现真正的业务逻辑:

评分 ≥ 60 分直接发布,否则退回修改;

调用外部 API 失败时自动重试,最多重试 5 次。

结果又发现一个问题:

python 复制代码
graph.add_edge("evaluate", "publish")

普通 Edge 在构建图时就已经确定了目标节点。

它无法表达:"运行时根据评分决定下一步去哪儿。" 更无法表达:"失败后返回上一步重新执行。"

而这恰恰是 Agent 最核心的能力。普通 Edge 是:

perl 复制代码
evaluate → publish

无论发生什么都往前走。

而你真正需要的是:

复制代码
evaluate
   ├── publish
   ├── revise
   └── retry

根据实际情况动态选择路径。这就是 Conditional Edge(条件边) 存在的意义。它让 LangGraph 从"脚本"进化成"Agent"。

二、为什么需要 Conditional Edge?

(一)条件分支决策

假设我们要构建一个内容审核 Agent:

复制代码
1. 接收用户提交内容
2. AI 评估内容质量
3. 高质量 → 发布
4. 质量不足 → 退回修改

步骤 3 和步骤 4 显然是一个条件分支。下一步执行什么,取决于运行时的评分结果,普通 Edge 做不到这一点,因为它在构建图时已经把执行路径固定死了。Conditional Edge 则提供了:

运行时决策能力(Runtime Decision Making)

这正是 Agent 与普通工作流最大的区别。

(二)后端开发类比

普通 Edge:

java 复制代码
methodA();
methodB();

执行顺序固定。

条件边:

java 复制代码
switch (router(state)) {
    case "approve":
        publish();
        break;

    case "reject":
        revise();
        break;
}

目标节点由运行时数据决定。

如果某个分支再指回自己:

java 复制代码
while (needRetry()) {
    callApi();
}

那就形成了循环。而循环、重试、轮询,本质上都是同一个模式。

(三)Conditional Edge 解决什么问题?

场景 普通 Edge Conditional Edge
固定流程
运行时分支
多路径选择
循环重试

记住一个判断标准:

只要下一步取决于运行时的数据,就应该使用 Conditional Edge。

三、核心原理

(一)Conditional Edge 的三要素

python 复制代码
graph.add_conditional_edges(
    "evaluate",          # 1. 从哪个节点出发
    review_router,       # 2. 路由函数:(State) -> str
    {                    # 3. 路由映射 path_map:路由值 → 目标节点
        "approve": "publish",
        "reject": "revise",
        "retry": "evaluate",  # 指回自己 → 形成循环
    }
)

它由三个部分组成:

1. Source Node

复制代码
"evaluate"

从哪个节点执行完后开始路由。

2. Router Function

复制代码
review_router(state)

签名:

复制代码
(State) -> str

根据当前 State 返回一个字符串。

例如:

python 复制代码
def review_router(state):
    if state["score"] >= 60:
        return "approve"
    return "reject"

3. Path Map

Dart 复制代码
{
    "approve": "publish",
    "reject": "revise",
}

负责把路由结果映射到目标节点。

(二)执行流程

html 复制代码
evaluate 执行完成
        ↓
State 更新
        ↓
review_router(state)
        ↓
返回字符串
        ↓
approve → publish
reject  → revise
retry   → evaluate

这里有一个非常关键的细节:

路由函数拿到的是节点执行完成并更新后的 State。

也就是说:

复制代码
evaluate()

刚刚计算出的结果,可以立即用于路由判断。

四、源码分析

(一)add_conditional_edges()

add_conditional_edges() 本质上会把路由信息封装成一个 Branch 对象保存起来。

简化后的源码:

python 复制代码
def add_conditional_edges(
    self,
    source,
    path,
    path_map=None,
    then=None
):
    if source not in self.nodes and source != START:
        raise ValueError(
            f"Source node '{source}' not found"
        )

    if path_map is None:
        path_map = {n: n for n in self.nodes}

    self.branches[source].append(
        Branch(
            path=path,
            ends=path_map,
            then=then
        )
    )

(二)_get_next_nodes(self, current_node, state)

执行阶段:

python 复制代码
# CompiledStateGraph 执行逻辑(简化)
def _get_next_nodes(self, current_node, state):
    branches = self.branches.get(current_node, [])
    if branches:
        for branch in branches:
            route_result = branch.path(state)          # 调用路由函数
            if route_result in branch.ends:
                return [branch.ends[route_result]]     # 命中映射 → 跳转
            raise ValueError(                          # ← 没命中就直接报错!
                f"Route returned '{route_result}', not in {list(branch.ends.keys())}"
            )
    return self._get_static_next(current_node)         # 没有条件边 → 走普通边

(三)两个必须记住的细节

1. 路由结果必须命中 path_map

否则直接抛异常:

复制代码
Route returned 'xxx'

不会静默忽略。这是新手最常见的问题之一。

2. LangGraph 不会自动检测死循环

它依赖:

复制代码
recursion_limit

作为兜底机制。默认值:25。超过后抛出:

复制代码
GraphRecursionError

因此循环逻辑必须自己维护计数器。

五、实战示例

(一)完整设计代码

python 复制代码
"""Demo 03: Conditional Edges --- 条件边与路由函数。

演示 LangGraph 的条件路由核心:
1. add_conditional_edges() 定义条件边
2. 路由函数根据 State 决定下一节点
3. 多分支选择(多路径路由)
4. 循环 + 条件边实现 Agent 循环

运行方式:
    python stages/stage1_fundamentals/03_conditional_edges/main.py
"""

from __future__ import annotations

import operator
import random
import sys
from pathlib import Path
from typing import Annotated, TypedDict

from langgraph.graph import END, START, StateGraph

sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent.parent))

from shared import get_logger, log_step, log_warning

logger = get_logger("demo.03_cond_edges")

random.seed(42)


# ============================================================
# 场景一:简单条件分支(通过 / 拒绝)
# ============================================================
class ReviewState(TypedDict):
    content: str
    score: int
    feedback: str
    path_taken: str


def evaluate_node(state: ReviewState) -> dict:
    """评估节点 --- 对内容打分。"""
    score = len(state["content"]) * 3 % 100
    log_step(logger, "evaluate", f"内容长度={len(state['content'])}, 评分={score}")
    return {"score": score}


def approve_node(state: ReviewState) -> dict:
    """审批通过节点。"""
    log_step(logger, "approve", f"内容通过审核!评分: {state['score']}")
    return {"feedback": f"✅ 审核通过,评分 {state['score']} 分", "path_taken": "approve"}


def reject_node(state: ReviewState) -> dict:
    """拒绝节点。"""
    log_step(logger, "reject", f"内容未通过审核,评分: {state['score']}")
    return {"feedback": f"❌ 审核未通过,评分 {state['score']} 分", "path_taken": "reject"}


def review_router(state: ReviewState) -> str:
    """路由函数:根据评分决定通过还是拒绝。"""
    if state["score"] >= 50:
        log_step(logger, "路由决策", f"评分 {state['score']} ≥ 50 → 走 approve 路径")
        return "approve"
    else:
        log_step(logger, "路由决策", f"评分 {state['score']} < 50 → 走 reject 路径")
        return "reject"


def demo_simple_branch():
    """演示简单的条件分支。"""
    print("\n--- 场景一:简单条件分支(通过/拒绝)---")

    graph = StateGraph(ReviewState)
    graph.add_node("evaluate", evaluate_node)
    graph.add_node("approve", approve_node)
    graph.add_node("reject", reject_node)

    graph.add_edge(START, "evaluate")
    graph.add_conditional_edges("evaluate", review_router, {
        "approve": "approve",
        "reject": "reject",
    })
    graph.add_edge("approve", END)
    graph.add_edge("reject", END)

    app = graph.compile()

    for content in ["LangGraph 入门教程,这是一篇高质量的内容", "短"]:
        print(f"\n  输入: '{content}'")
        result = app.invoke({
            "content": content,
            "score": 0,
            "feedback": "",
            "path_taken": "",
        })
        print(f"  结果: {result['feedback']}")
        print(f"  路径: evaluate → {result['path_taken']}")


# ============================================================
# 场景二:多分支路由(优秀 / 合格 / 不合格)
# ============================================================
class GradeState(TypedDict):
    student_name: str
    score: int
    grade: str
    message: str


def grade_router(state: GradeState) -> str:
    """多分支路由:根据分数划分等级。"""
    score = state["score"]
    if score >= 90:
        return "excellent"
    elif score >= 60:
        return "pass"
    else:
        return "fail"


def demo_multi_branch():
    """演示多分支路由。"""
    print("\n--- 场景二:多分支路由(优秀/合格/不合格)---")

    def excellent_node(state: GradeState) -> dict:
        return {"grade": "优秀", "message": f"🏆 {state['student_name']} 成绩优秀!"}

    def pass_node(state: GradeState) -> dict:
        return {"grade": "合格", "message": f"✅ {state['student_name']} 成绩合格"}

    def fail_node(state: GradeState) -> dict:
        return {"grade": "不合格", "message": f"⚠️ {state['student_name']} 需要补考"}

    graph = StateGraph(GradeState)
    graph.add_node("excellent", excellent_node)
    graph.add_node("pass", pass_node)
    graph.add_node("fail", fail_node)

    graph.add_conditional_edges(START, grade_router, {
        "excellent": "excellent",
        "pass": "pass",
        "fail": "fail",
    })
    graph.add_edge("excellent", END)
    graph.add_edge("pass", END)
    graph.add_edge("fail", END)

    app = graph.compile()

    students = [
        {"student_name": "小明", "score": 95},
        {"student_name": "小红", "score": 72},
        {"student_name": "小刚", "score": 45},
    ]

    for student in students:
        result = app.invoke({**student, "grade": "", "message": ""})
        print(f"  {result['message']} (分数: {student['score']}, 等级: {result['grade']})")


# ============================================================
# 场景三:循环 + 条件边(重试机制)
# ============================================================
class RetryState(TypedDict):
    task: str
    attempt: int
    max_attempts: int
    result: str
    log: Annotated[list[str], operator.add]


def attempt_node(state: RetryState) -> dict:
    """执行节点 --- 模拟一个可能失败的操作。"""
    attempt = state["attempt"] + 1
    success = random.random() > 0.88
    status = "成功" if success else "失败"
    log_step(logger, "attempt", f"第 {attempt} 次尝试... {status}")
    return {
        "attempt": attempt,
        "result": "success" if success else "fail",
        "log": [f"第 {attempt} 次尝试: {status}"],
    }


def retry_router(state: RetryState) -> str:
    """路由函数:判断是否需要重试。"""
    if state["result"] == "success":
        log_step(logger, "路由决策", "任务成功 → 完成")
        return "done"
    elif state["attempt"] >= state["max_attempts"]:
        log_warning(logger, f"已达最大尝试次数 {state['max_attempts']} → 强制结束")
        return "give_up"
    else:
        log_step(logger, "路由决策", f"任务失败,第 {state['attempt']}/{state['max_attempts']} 次 → 重试")
        return "retry"


def success_node(state: RetryState) -> dict:
    return {"log": [f"🎉 任务在第 {state['attempt']} 次尝试后成功完成!"]}


def give_up_node(state: RetryState) -> dict:
    return {"log": [f"😞 经过 {state['attempt']} 次尝试后放弃"]}


def demo_retry_loop():
    """演示循环 + 条件边实现重试机制。"""
    print("\n--- 场景三:循环 + 条件边(重试机制)---")

    graph = StateGraph(RetryState)
    graph.add_node("attempt", attempt_node)
    graph.add_node("success", success_node)
    graph.add_node("give_up", give_up_node)

    graph.add_edge(START, "attempt")
    graph.add_conditional_edges("attempt", retry_router, {
        "retry": "attempt",
        "done": "success",
        "give_up": "give_up",
    })
    graph.add_edge("success", END)
    graph.add_edge("give_up", END)

    app = graph.compile()

    result = app.invoke({
        "task": "调用外部 API",
        "attempt": 0,
        "max_attempts": 5,
        "result": "",
        "log": ["开始执行任务: 调用外部 API"],
    })

    print("  执行日志:")
    for entry in result["log"]:
        print(f"    {entry}")


def run_demo() -> dict:
    """运行 Conditional Edges Demo。"""
    print("=" * 60)
    print("  Demo 03: Conditional Edges --- 条件边与路由函数")
    print("=" * 60)

    demo_simple_branch()
    demo_multi_branch()
    demo_retry_loop()

    print()
    print("=" * 60)
    print("  关键概念回顾")
    print("=" * 60)
    print("  1. add_conditional_edges(): 定义条件边(三个参数)")
    print("  2. 路由函数: 接收 State,返回字符串(目标节点名)")
    print("  3. path_map: 路由值 → 目标节点的映射字典")
    print("  4. 循环: 条件边可以指回自身(retry → attempt)")
    print("  5. 安全: 必须设置最大循环次数防止死循环!")
    print()

    return {}


if __name__ == "__main__":
    run_demo()

(二)关键代码说明

本 Demo 展示了三种典型使用方式。

1. 场景一:简单二选一

python 复制代码
def review_router(state):
    return (
        "approve"
        if state["score"] >= 50
        else "reject"
    )

graph.add_conditional_edges(
    "evaluate",
    review_router,
    {
        "approve": "approve",
        "reject": "reject",
    }
)

2. 场景二:多路分支

甚至可以直接从 START 开始路由:

python 复制代码
def grade_router(state):
    score = state["score"]

    return (
        "excellent"
        if score >= 90
        else "pass"
        if score >= 60
        else "fail"
    )

graph.add_conditional_edges(
    START,
    grade_router,
    {
        "excellent": "excellent",
        "pass": "pass",
        "fail": "fail",
    }
)

3. 场景三:循环重试

条件边指回自身。

python 复制代码
def retry_router(state):

    if state["result"] == "success":
        return "done"

    if state["attempt"] >= state["max_attempts"]:
        return "give_up"

    return "retry"
python 复制代码
graph.add_conditional_edges(
    "attempt",
    retry_router,
    {
        "retry": "attempt",
        "done": "success",
        "give_up": "give_up",
    }
)

形成:

复制代码
attempt
   ↓
retry
   ↓
attempt
   ↓
retry
   ↓
attempt
   ↓
success

(三)运行效果

实际运行 main.py

复制代码
============================================================
  Demo 03: Conditional Edges --- 条件边与路由函数
============================================================

--- 场景一:简单条件分支(通过/拒绝)---

  输入: 'LangGraph 入门教程,这是一篇高质量的内容'
  结果: ✅ 审核通过,评分 75 分
  路径: evaluate → approve

  输入: '短'
  结果: ❌ 审核未通过,评分 3 分
  路径: evaluate → reject

--- 场景二:多分支路由(优秀/合格/不合格)---

  🏆 小明 成绩优秀! (分数: 95, 等级: 优秀)
  ✅ 小红 成绩合格 (分数: 72, 等级: 合格)
  ⚠️ 小刚 需要补考 (分数: 45, 等级: 不合格)

--- 场景三:循环 + 条件边(重试机制)---

  执行日志:
    开始执行任务: 调用外部 API
    第 1 次尝试: 失败
    第 2 次尝试: 失败
    第 3 次尝试: 成功
    🎉 任务在第 3 次尝试后成功完成!

如何理解这三个场景?

场景一:运行时决策

同一张图:

复制代码
app.invoke(...)

因为输入内容不同:

复制代码
75 分
3 分

最终进入不同节点:

复制代码
approve
reject

这就是运行时路由。

场景二:入口即分流

Conditional Edge 不一定挂在普通节点后。它甚至可以直接挂在:

复制代码
START

图一启动就决定走哪条路径。

场景三:重试机制

这是最重要的应用场景。路由函数不断判断:

复制代码
成功?
↓
否

达到上限?
↓
否

继续重试

于是形成循环。

这里配合:

复制代码
Annotated[list, operator.add]

把每次尝试日志都保留下来。另外 Demo 使用:

复制代码
random.seed(42)

保证每次运行结果一致,方便学习和调试。

六、常见坑与排查

(一)坑 1:返回值不在 path_map 中

现象

复制代码
Route returned 'xxx'

原因

返回了未覆盖的字符串。例如:

复制代码
return "aprove"

拼写错误。

解决

确保所有返回值都被覆盖。

python 复制代码
graph.add_conditional_edges(
    "eval",
    router,
    {
        "approve": "publish",
        "reject": "revise",
    }
)

(二)坑 2:循环没有退出条件

现象

复制代码
GraphRecursionError:
Recursion limit of 25 reached

原因

永远返回:

复制代码
"retry"

导致无限循环。

解决

维护计数器:

python 复制代码
def retry_router(state):

    if state["result"] == "success":
        return "done"

    if state["attempt"] >= state["max_attempts"]:
        return "give_up"

    return "retry"

(三)坑 3:在 Router 中修改 State

错误示例

python 复制代码
def bad_router(state):
    state["count"] += 1
    return "next"

路由函数应该是纯函数。

正确写法

python 复制代码
def good_router(state):
    return (
        "next"
        if state["count"] < 3
        else "stop"
    )

所有状态修改都应该在 Node 中完成。

(四)坑 4:普通边与条件边混用

错误

同一个节点:

python 复制代码
graph.add_edge(...)
graph.add_conditional_edges(...)

同时存在。

正确

二选一:

  • 普通边
  • 条件边

不要混用。

七、工程化实践与生产级建议

(一)工程化实践

1. 防死循环

State 中维护计数器:

复制代码
attempt
max_attempts

同时配置:

复制代码
recursion_limit

作为最后保险。

2. 路由全覆盖

确保:

复制代码
router()

所有返回值都能在:

复制代码
path_map

中找到对应目标。

3. 增加可观测性

路由决策最好打日志:

复制代码
当前评分: 72
路由结果: approve

线上排障时非常有用。

4. 设计默认分支

例如:

复制代码
"default": END

避免边界输入直接导致异常。

(二)生产级方案

python 复制代码
result = app.invoke(
    state,
    config={
        "recursion_limit": 50
    }
)

将阈值配置化:

python 复制代码
APPROVE_THRESHOLD = 60

def review_router(state):
    return (
        "approve"
        if state["score"] >= APPROVE_THRESHOLD
        else "reject"
    )

重试策略配置化:

复制代码
max_attempts
backoff_strategy

后续再结合 Checkpointer:

复制代码
checkpoint
resume

实现长任务断点续跑。监控路由命中率:

复制代码
approve: 70%
reject : 25%
retry  : 5%

如果某个分支突然异常升高:

复制代码
reject: 90%

往往意味着模型、Prompt 或业务逻辑出了问题。

八、总结

Conditional Edge 是 LangGraph 区别于传统线性 Chain 的核心能力之一。

掌握它之后,你就拥有了构建 Agent 决策流程的关键工具:

  • 条件分支
  • 多路路由
  • 循环重试
  • 审批流
  • 状态机

再结合前两篇介绍的:

  • State
  • Reducer

你已经具备搭建绝大多数 Agent 工作流的基础能力。

下一篇预告

下一篇我们进入 Agent 最常见的场景:对话状态管理(Conversation State)

系列导航: LangGraph从零构建生产级 AI Agent 平台的递进式学习项目

相关推荐
ZFSS1 小时前
BYOK(自带密钥)使用指南
运维·服务器·前端·人工智能·midjourney
装不满的克莱因瓶1 小时前
掌握典型卷积神经网络的搭建
人工智能·python·深度学习·神经网络·机器学习·ai·cnn
ting94520001 小时前
InsForge Backend Branching 后端全链路 Git 式分支技术原理、架构实现与底层源码剖析
人工智能·git·elasticsearch·架构
Mr.朱鹏1 小时前
科技资讯日报 · 2026-06-05
科技·ai·大模型·业界资讯
程序猿阿伟1 小时前
《扣子如何让OpenClaw技能开发提速》
人工智能·git·github
_Evan_Yao1 小时前
AI Agent下半场:模型能力过剩,Skill生态成为新壁垒
人工智能
圣殿骑士-Khtangc1 小时前
多智能体协作架构深度解析:MCP + A2A 协议栈,构建企业级 Multi-Agent 系统
人工智能