{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 18 条件边\n", "\n", "## 学习目标\n", "1. 理解条件边(Conditional Edges)的作用和使用场景\n", "2. 掌握 `add_conditional_edges` 实现分支流程\n", "3. 能够根据状态值动态决定图的执行路径\n", "4. 理解路由函数在图中的作用\n", "5. 能够读懂并修改一个带分支的图流程" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. 为什么需要条件边\n", "\n", "前一节我们学习了普通边。普通边的特点是:**下一步固定不变**。\n", "\n", "例如:\n", "\n", "```\n", "START -> node_a -> node_b -> END\n", "```\n", "\n", "这条流程里,不管输入是什么,`node_a` 做完以后都会进入 `node_b`。\n", "\n", "但真实业务中,流程往往不是固定直走的,而是会根据情况分支。比如:\n", "\n", "- 分数及格,就进入‘通过’节点\n", "- 分数不及格,就进入‘补考’节点\n", "- 用户问题完整,就直接回答\n", "- 用户问题不完整,就先追问\n", "- 检索结果足够,就结束\n", "- 检索结果不够,就继续查\n", "\n", "这类‘根据当前状态决定下一步去哪’的场景,就要用到**条件边**。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. 条件边和普通边的区别\n", "\n", "可以把两种边对比着理解:\n", "\n", "| 类型 | 决定方式 | 典型场景 |\n", "| --- | --- | --- |\n", "| 普通边 `add_edge` | 固定写死 | 顺序执行流程 |\n", "| 条件边 `add_conditional_edges` | 根据状态动态判断 | 分支、回路、路由控制 |\n", "\n", "最核心的区别只有一句话:\n", "\n", "- 普通边:下一步是提前写死的\n", "- 条件边:下一步是运行时判断出来的\n", "\n", "也就是说,条件边让图开始‘有判断力’。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. 条件边的基本思路\n", "\n", "使用条件边时,通常有三样东西要配合起来:\n", "\n", "1. **一个普通节点**:先处理当前状态\n", "2. **一个路由函数**:根据状态判断下一步去哪\n", "3. **一个分支映射表**:告诉图不同返回值分别对应哪个节点\n", "\n", "写法通常像这样:\n", "\n", "```python\n", "builder.add_conditional_edges(\n", " '某个节点',\n", " route_function,\n", " {\n", " '路径A': 'node_a',\n", " '路径B': 'node_b'\n", " }\n", ")\n", "```\n", "\n", "它的意思是:\n", "\n", "- 先执行‘某个节点’\n", "- 执行完以后,不直接写死下一步\n", "- 而是调用 `route_function(state)` 看返回什么\n", "- 如果返回 `'路径A'`,就走到 `node_a`\n", "- 如果返回 `'路径B'`,就走到 `node_b`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. 第一个例子:根据分数决定是否通过\n", "\n", "先从最直观的例子开始。\n", "\n", "流程目标:\n", "\n", "- 如果分数大于等于 60,进入通过节点\n", "- 如果分数小于 60,进入补考节点\n", "\n", "流程图如下:\n", "\n", "```\n", "START -> check_score -> pass_node / fail_node -> END\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class ScoreState(TypedDict):\n", " score: int\n", " result: str\n", "\n", "def check_score(state: ScoreState):\n", " print(f'当前分数:{state[\"score\"]}')\n", " return {}\n", "\n", "def pass_node(state: ScoreState):\n", " return {'result': '成绩合格,顺利通过'}\n", "\n", "def fail_node(state: ScoreState):\n", " return {'result': '成绩不合格,需要补考'}\n", "\n", "def route_by_score(state: ScoreState):\n", " if state['score'] >= 60:\n", " return 'pass'\n", " return 'fail'\n", "\n", "builder = StateGraph(ScoreState)\n", "builder.add_node('check_score', check_score)\n", "builder.add_node('pass_node', pass_node)\n", "builder.add_node('fail_node', fail_node)\n", "\n", "builder.add_edge(START, 'check_score')\n", "builder.add_conditional_edges(\n", " 'check_score',\n", " route_by_score,\n", " {\n", " 'pass': 'pass_node',\n", " 'fail': 'fail_node'\n", " }\n", ")\n", "builder.add_edge('pass_node', END)\n", "builder.add_edge('fail_node', END)\n", "\n", "graph = builder.compile()\n", "\n", "print('分数 85:')\n", "print(graph.invoke({'score': 85, 'result': ''}))\n", "\n", "print('\\n分数 45:')\n", "print(graph.invoke({'score': 45, 'result': ''}))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个例子非常典型,几乎把条件边最核心的要点都展示出来了。\n", "\n", "#### 状态结构\n", "状态里有两个字段:\n", "\n", "- `score`:输入分数\n", "- `result`:最终判断结果\n", "\n", "#### `check_score` 节点\n", "这个节点本身没有修改状态,只是打印当前分数。\n", "\n", "它返回 `{}`,表示‘这个节点不更新任何字段’。\n", "\n", "这说明一个节点不一定非要负责生成结果,它也可以只是一个‘分支前的准备步骤’。\n", "\n", "#### `pass_node` 和 `fail_node`\n", "这两个节点分别代表分支后的两条路径:\n", "\n", "- 通过路径:写入‘成绩合格,顺利通过’\n", "- 失败路径:写入‘成绩不合格,需要补考’\n", "\n", "#### `route_by_score(state)`\n", "这是本例的路由函数。它的任务不是修改状态,而是‘判断下一步去哪’。\n", "\n", "判断逻辑很简单:\n", "\n", "- 如果 `score >= 60`,返回 `'pass'`\n", "- 否则返回 `'fail'`\n", "\n", "注意:这里返回的不是节点函数本身,而是一个**分支标记**。\n", "\n", "#### `add_conditional_edges(...)`\n", "这一段可以拆成三层意思:\n", "\n", "1. 从 `check_score` 节点出来以后,不直接写死下一步\n", "2. 调用 `route_by_score(state)` 得到一个返回值\n", "3. 根据映射表决定真正跳去哪个节点\n", "\n", "也就是说:\n", "\n", "- 返回 `'pass'` -> 去 `pass_node`\n", "- 返回 `'fail'` -> 去 `fail_node`\n", "\n", "#### 执行路径是怎么变化的\n", "当输入 `score=85` 时:\n", "\n", "```\n", "START -> check_score -> pass_node -> END\n", "```\n", "\n", "当输入 `score=45` 时:\n", "\n", "```\n", "START -> check_score -> fail_node -> END\n", "```\n", "\n", "这就是条件边最基础的价值:**同一张图,面对不同状态时,可以自动走不同路线。**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. 第二个例子:问题完整就直接回答,不完整就先追问\n", "\n", "下面我们做一个更贴近 AI 助手的例子。\n", "\n", "假设用户提问时,有的问题已经很清楚,有的问题信息还不够。\n", "\n", "流程目标:\n", "\n", "- 如果问题完整,直接进入回答节点\n", "- 如果问题不完整,先进入追问节点\n", "\n", "这个例子可以帮助你理解:条件边不仅能处理分数、数字,也能处理业务判断。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class QuestionState(TypedDict):\n", " user_question: str\n", " is_complete: bool\n", " response: str\n", "\n", "def analyze_question(state: QuestionState):\n", " print(f'收到问题:{state[\"user_question\"]}')\n", " return {}\n", "\n", "def ask_more(state: QuestionState):\n", " return {'response': '你的问题还不够完整,请补充更多背景信息。'}\n", "\n", "def answer_directly(state: QuestionState):\n", " return {'response': '问题信息已经足够,我现在直接为你回答。'}\n", "\n", "def route_question(state: QuestionState):\n", " if state['is_complete']:\n", " return 'answer'\n", " return 'ask_more'\n", "\n", "builder = StateGraph(QuestionState)\n", "builder.add_node('analyze_question', analyze_question)\n", "builder.add_node('ask_more', ask_more)\n", "builder.add_node('answer_directly', answer_directly)\n", "\n", "builder.add_edge(START, 'analyze_question')\n", "builder.add_conditional_edges(\n", " 'analyze_question',\n", " route_question,\n", " {\n", " 'answer': 'answer_directly',\n", " 'ask_more': 'ask_more'\n", " }\n", ")\n", "builder.add_edge('ask_more', END)\n", "builder.add_edge('answer_directly', END)\n", "\n", "graph = builder.compile()\n", "\n", "print('完整问题:')\n", "print(graph.invoke({\n", " 'user_question': '请解释什么是条件边,并举一个例子',\n", " 'is_complete': True,\n", " 'response': ''\n", "}))\n", "\n", "print('\\n不完整问题:')\n", "print(graph.invoke({\n", " 'user_question': '帮我看看这个',\n", " 'is_complete': False,\n", " 'response': ''\n", "}))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个例子和前面的‘分数判断’相比,更接近真实 AI 产品中的流程控制。\n", "\n", "#### 状态字段的意义\n", "这里的状态有三个字段:\n", "\n", "- `user_question`:用户输入的问题\n", "- `is_complete`:问题是否完整\n", "- `response`:最终回复\n", "\n", "#### `analyze_question` 节点\n", "这个节点先接收问题并打印出来,相当于‘进入分析阶段’。\n", "\n", "它没有直接修改状态,而是把路由判断交给后面的路由函数。\n", "\n", "#### `route_question(state)`\n", "这个函数根据 `is_complete` 的真假做判断:\n", "\n", "- `True` -> 返回 `'answer'`\n", "- `False` -> 返回 `'ask_more'`\n", "\n", "这意味着流程下一步是动态的,而不是提前写死的。\n", "\n", "#### 两条分支的业务含义\n", "- `answer_directly`:说明信息足够,可以直接处理\n", "- `ask_more`:说明信息不足,需要先追问\n", "\n", "#### 为什么这个例子很重要\n", "它让你看到:条件边并不只是‘程序里的 if 判断’,更像是业务流程里的‘路由器’。\n", "\n", "很多 Agent 系统都会有类似逻辑:\n", "\n", "- 信息够不够?\n", "- 要不要调用工具?\n", "- 要不要继续搜索?\n", "- 能不能直接输出答案?\n", "\n", "而这些判断,往往都可以通过条件边表达出来。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. 第三个例子:条件边也可以用于继续或结束\n", "\n", "条件边不仅可以做左右分支,还经常用来做‘继续/停止’的判断。\n", "\n", "这类场景非常常见,例如:\n", "\n", "- 检索结果不够,就继续检索\n", "- 输出格式不对,就继续生成\n", "- 数量还没到要求,就继续累加\n", "\n", "下面用一个简单例子说明:如果 `count < 3`,就继续执行;否则结束。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class LoopState(TypedDict):\n", " count: int\n", "\n", "def add_one(state: LoopState):\n", " new_count = state['count'] + 1\n", " print(f'当前 count 从 {state[\"count\"]} 变成 {new_count}')\n", " return {'count': new_count}\n", "\n", "def route_loop(state: LoopState):\n", " if state['count'] < 3:\n", " return 'continue'\n", " return 'stop'\n", "\n", "builder = StateGraph(LoopState)\n", "builder.add_node('add_one', add_one)\n", "\n", "builder.add_edge(START, 'add_one')\n", "builder.add_conditional_edges(\n", " 'add_one',\n", " route_loop,\n", " {\n", " 'continue': 'add_one',\n", " 'stop': END\n", " }\n", ")\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({'count': 0})\n", "print('最终结果:', result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个例子非常重要,因为它说明:**条件边不仅能分叉,也能回跳。**\n", "\n", "#### `add_one` 节点\n", "这个节点每次把 `count` 加 1。\n", "\n", "也就是说,它是一个不断重复执行的工作步骤。\n", "\n", "#### `route_loop(state)`\n", "这个路由函数负责判断接下来是继续,还是停止:\n", "\n", "- 如果 `count < 3`,返回 `'continue'`\n", "- 否则返回 `'stop'`\n", "\n", "#### 条件边映射\n", "这里映射关系很关键:\n", "\n", "- `'continue'` -> `'add_one'`\n", "- `'stop'` -> `END`\n", "\n", "也就是说:\n", "\n", "- 还没满足条件,就重新回到原节点\n", "- 满足条件后,就离开流程\n", "\n", "#### 执行过程\n", "如果初始值是 `count=0`,路径会是:\n", "\n", "1. `0 -> 1`,继续\n", "2. `1 -> 2`,继续\n", "3. `2 -> 3`,停止\n", "4. 进入 `END`\n", "\n", "从这个例子你可以看到,条件边不只是“二选一分支”,它还是循环流程的基础。\n", "\n", "后面很多 Agent 的‘反复尝试直到成功’流程,本质上都是这种写法。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. 路由函数应该怎么理解\n", "\n", "很多初学者最容易混淆的地方,就是把‘节点函数’和‘路由函数’混在一起。\n", "\n", "你可以这样区分:\n", "\n", "| 类型 | 主要职责 | 是否负责业务处理 |\n", "| --- | --- | --- |\n", "| 节点函数 | 读写状态、执行处理逻辑 | 是 |\n", "| 路由函数 | 判断下一步走哪条路 | 通常不是重点 |\n", "\n", "换句话说:\n", "\n", "- 节点函数更像‘工人’\n", "- 路由函数更像‘调度员’\n", "\n", "工人负责真正干活,调度员负责决定下一站去哪。\n", "\n", "理解这一点以后,条件边的结构就会非常清晰。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 8. 使用条件边时的常见错误\n", "\n", "### 8.1 路由函数返回值和映射表对不上\n", "例如路由函数返回 `'pass'`,但映射表里只有 `'ok'` 和 `'fail'`。\n", "\n", "这样图就不知道该往哪里走。\n", "\n", "### 8.2 把节点名字和路由标记混为一谈\n", "路由函数返回的可以是一个标记,例如 `'continue'`、`'stop'`、`'pass'`。\n", "\n", "这些标记不一定要和节点名完全相同,但必须和映射表中的 key 对应。\n", "\n", "### 8.3 以为条件边只能做两条分支\n", "其实不止两条。\n", "\n", "你完全可以写三条、四条甚至更多路线,只要业务场景需要。\n", "\n", "### 8.4 忘记给分支节点连到 `END` 或后续节点\n", "条件边只负责从当前节点分出去,不代表后面的路径已经自动补全。\n", "\n", "分支出去以后,该怎么结束、怎么汇合,仍然要继续写边。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 9. 条件边在 Agent 场景中的意义\n", "\n", "到了这里,你可以把条件边理解成 LangGraph 里的‘流程决策器’。\n", "\n", "在 Agent 场景中,它特别常见,因为 Agent 天生就不是固定直线流程。\n", "\n", "例如:\n", "\n", "- 如果用户问题明确,直接回答\n", "- 如果用户问题不明确,先追问\n", "- 如果需要外部信息,调用工具\n", "- 如果工具结果不够,再查一次\n", "- 如果结果足够,就进入最终回答\n", "\n", "你会发现,这些逻辑本质上都属于:\n", "\n", "**根据当前状态,动态决定下一步去哪。**\n", "\n", "这正是条件边最核心的价值。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 10. 本节小结\n", "\n", "本节最重要的内容有五点:\n", "\n", "1. **条件边用于动态决定流程路径**\n", "2. **`add_conditional_edges` 是实现分支和回路的关键方法**\n", "3. **路由函数负责返回分支标记,映射表负责把标记映射到节点**\n", "4. **条件边不仅能做左右分支,也能做继续/停止这样的循环控制**\n", "5. **很多 Agent 工作流,本质上都是条件边在控制路径**\n", "\n", "理解了条件边,你就真正进入了‘动态流程控制’这一层。后面的复杂图,大多都离不开它。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 11. 本节练习\n", "\n", "1. 修改第一个示例,把及格线从 60 改成 80,观察分支结果变化\n", "2. 修改第二个示例,再增加一种情况:如果问题为空,就返回一个专门的提示节点\n", "3. 修改第三个示例,把停止条件从 `count < 3` 改成 `count < 5`\n", "4. 思考:如果一个流程有 3 条不同分支,路由函数和映射表应该怎么设计?\n", "5. 思考:条件边和普通边最大的本质区别是什么?" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.4" } }, "nbformat": 4, "nbformat_minor": 5 }