{ "cells": [ { "cell_type": "markdown", "id": "5acad58d", "metadata": {}, "source": [ "# 17 节点与边\n", "\n", "## 学习目标\n", "1. 掌握添加节点(`add_node`)和添加边(`add_edge`)的方法\n", "2. 理解 `START` 和 `END` 在图中的含义\n", "3. 能够构建包含多个节点的顺序执行图\n", "4. 理解节点负责做事、边负责决定顺序的分工\n", "5. 能够看懂并修改一个简单的图流程" ] }, { "cell_type": "markdown", "id": "0be17af5", "metadata": {}, "source": [ "## 1. 为什么要单独讲节点与边\n", "\n", "在前两节中,我们已经接触过图结构和状态管理。现在要进一步回答一个更基础的问题:\n", "\n", "**一张图到底是怎么拼出来的?**\n", "\n", "答案就是:\n", "\n", "- 用 **节点(Node)** 表示每一步做什么\n", "- 用 **边(Edge)** 表示下一步往哪里走\n", "\n", "也就是说:\n", "\n", "- 节点负责‘干活’\n", "- 边负责‘指路’\n", "\n", "你可以把它想象成一张旅游路线图:\n", "\n", "- 景点 = 节点\n", "- 景点之间的道路 = 边\n", "- 你游览的先后顺序 = 图的执行顺序\n", "\n", "如果没有节点,图里就没有真正执行的步骤;如果没有边,这些步骤之间就连不起来。" ] }, { "cell_type": "markdown", "id": "e7e11691", "metadata": {}, "source": [ "## 2. 节点是什么\n", "\n", "在 LangGraph 中,节点通常就是一个 Python 函数。\n", "\n", "这个函数会:\n", "\n", "1. 接收当前状态 `state`\n", "2. 处理数据\n", "3. 返回要更新的状态字段\n", "\n", "例如一个节点可以做这些事:\n", "\n", "- 给数字加 1\n", "- 拼接一段字符串\n", "- 计算总价\n", "- 调用大模型\n", "- 调用工具\n", "\n", "所以,节点本质上就是流程中的‘处理步骤’。\n", "\n", "一个图里可以有一个节点,也可以有很多节点。节点越多,流程通常越细。" ] }, { "cell_type": "markdown", "id": "9599df9d", "metadata": {}, "source": [ "## 3. 边是什么\n", "\n", "边表示节点和节点之间的连接关系。\n", "\n", "如果说节点回答的是:\n", "\n", "> 这一步做什么?\n", "\n", "那么边回答的就是:\n", "\n", "> 做完这一步以后,下一步去哪?\n", "\n", "例如:\n", "\n", "```\n", "START -> node_a -> node_b -> END\n", "```\n", "\n", "这里就有三条边:\n", "\n", "- `START -> node_a`\n", "- `node_a -> node_b`\n", "- `node_b -> END`\n", "\n", "所以,边并不负责处理数据,它只负责规定执行顺序。" ] }, { "cell_type": "markdown", "id": "707d8841", "metadata": {}, "source": [ "## 4. `START` 和 `END` 是什么\n", "\n", "在 LangGraph 中,我们通常会看到两个特殊标记:\n", "\n", "- `START`:图的起点\n", "- `END`:图的终点\n", "\n", "它们不是普通业务节点,而是系统提供的特殊位置。\n", "\n", "你可以把它们理解成:\n", "\n", "- `START` = 流程从哪里进入\n", "- `END` = 流程在哪里结束\n", "\n", "注意:\n", "\n", "- `START` 不负责处理数据\n", "- `END` 也不负责处理数据\n", "- 它们的作用只是帮助我们把流程的起点和终点标出来\n", "\n", "因此,很多图都长这样:\n", "\n", "```\n", "START -> 第一个节点 -> 第二个节点 -> END\n", "```" ] }, { "cell_type": "markdown", "id": "1ddf8a66", "metadata": {}, "source": [ "## 5. 第一个例子:一个节点的最小图\n", "\n", "先从最简单的图开始,只放一个节点。\n", "\n", "流程如下:\n", "\n", "```\n", "START -> greet -> END\n", "```\n", "\n", "也就是说:\n", "\n", "1. 流程从 `START` 进入\n", "2. 执行 `greet` 节点\n", "3. 执行完后直接结束" ] }, { "cell_type": "code", "execution_count": null, "id": "d23c2d17", "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class HelloState(TypedDict):\n", " name: str\n", " message: str\n", "\n", "def greet(state: HelloState):\n", " return {'message': f'你好,{state[\"name\"]}!'}\n", "\n", "builder = StateGraph(HelloState)\n", "builder.add_node('greet', greet)\n", "builder.add_edge(START, 'greet')\n", "builder.add_edge('greet', END)\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({'name': '小明', 'message': ''})\n", "print(result)" ] }, { "cell_type": "markdown", "id": "93b3f4bc", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个例子非常短,但已经完整包含了节点与边的核心写法。\n", "\n", "#### `class HelloState(TypedDict)`\n", "这里定义了图中的状态结构,包含两个字段:\n", "\n", "- `name`:输入的人名\n", "- `message`:节点生成的问候语\n", "\n", "#### `def greet(state)`\n", "这就是一个节点函数。它读取 `name`,然后生成一句问候语,写入 `message`。\n", "\n", "也就是说,这个节点真正做的事情是:\n", "\n", "```text\n", "读取名字 -> 生成问候 -> 写回状态\n", "```\n", "\n", "#### `builder.add_node('greet', greet)`\n", "这是把节点加入图中的关键代码。\n", "\n", "它的意思是:\n", "\n", "- 节点名字叫 `greet`\n", "- 节点实际执行的函数是 `greet`\n", "\n", "你可以把前面的字符串 `'greet'` 理解为节点标签,把后面的函数 `greet` 理解为节点的工作内容。\n", "\n", "#### `builder.add_edge(START, 'greet')`\n", "表示流程从起点进入 `greet` 节点。\n", "\n", "#### `builder.add_edge('greet', END)`\n", "表示执行完 `greet` 节点之后,流程直接结束。\n", "\n", "#### 整体流程\n", "整个图的执行路径就是:\n", "\n", "```\n", "START -> greet -> END\n", "```\n", "\n", "这是最小的完整图,也是理解节点与边的最好起点。" ] }, { "cell_type": "markdown", "id": "926caf0b", "metadata": {}, "source": [ "## 6. 两个节点的顺序图\n", "\n", "一个节点的图很简单,但很多实际流程至少会有两个步骤。\n", "\n", "下面我们做一个两步流程:\n", "\n", "1. 先生成问候语\n", "2. 再给问候语后面补一句欢迎信息\n", "\n", "流程如下:\n", "\n", "```\n", "START -> make_greeting -> add_welcome -> END\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "0a529a83", "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class WelcomeState(TypedDict):\n", " name: str\n", " greeting: str\n", " final_text: str\n", "\n", "def make_greeting(state: WelcomeState):\n", " return {'greeting': f'你好,{state[\"name\"]}!'}\n", "\n", "def add_welcome(state: WelcomeState):\n", " return {'final_text': state['greeting'] + ' 欢迎学习 LangGraph。'}\n", "\n", "builder = StateGraph(WelcomeState)\n", "builder.add_node('make_greeting', make_greeting)\n", "builder.add_node('add_welcome', add_welcome)\n", "\n", "builder.add_edge(START, 'make_greeting')\n", "builder.add_edge('make_greeting', 'add_welcome')\n", "builder.add_edge('add_welcome', END)\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({\n", " 'name': '小红',\n", " 'greeting': '',\n", " 'final_text': ''\n", "})\n", "\n", "print(result)\n", "print(result['final_text'])" ] }, { "cell_type": "markdown", "id": "a2dd0d07", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个例子比前一个例子多了一个节点,因此也多了一条边。\n", "\n", "#### 两个节点分别做什么\n", "- `make_greeting`:生成问候语\n", "- `add_welcome`:在问候语后面追加欢迎信息\n", "\n", "这两个节点就像工厂里的两个工位:\n", "\n", "- 第一个工位先加工出半成品\n", "- 第二个工位接着加工成完整成品\n", "\n", "#### 为什么这里要有三条边\n", "因为整个流程要从头走到尾,所以必须把三段路径都写出来:\n", "\n", "1. `START -> make_greeting`\n", "2. `make_greeting -> add_welcome`\n", "3. `add_welcome -> END`\n", "\n", "少写任何一条,图的路径都会不完整。\n", "\n", "#### `add_edge('make_greeting', 'add_welcome')` 的意义\n", "这行代码非常关键,它表达的是:\n", "\n", "> 第一个节点做完以后,不是结束,而是继续把结果交给第二个节点。\n", "\n", "所以我们可以说:\n", "\n", "- 节点负责定义每一步做什么\n", "- 边负责把这些步骤串成一条完整路线\n", "\n", "#### 整个执行过程\n", "执行时,状态会这样流动:\n", "\n", "1. 初始状态进入 `make_greeting`\n", "2. 生成 `greeting='你好,小红!'`\n", "3. 再进入 `add_welcome`\n", "4. 生成 `final_text='你好,小红! 欢迎学习 LangGraph。'`\n", "5. 最后进入 `END`\n", "\n", "这就是最典型的多节点顺序图。" ] }, { "cell_type": "markdown", "id": "41a9b843", "metadata": {}, "source": [ "## 7. 三个节点的顺序图\n", "\n", "为了进一步理解边的作用,我们再看一个三个节点的例子。\n", "\n", "流程目标:\n", "\n", "1. 输入一个数字\n", "2. 先加 2\n", "3. 再乘 3\n", "4. 最后减 1\n", "\n", "流程图如下:\n", "\n", "```\n", "START -> add_two -> multiply_three -> minus_one -> END\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "a95e97f2", "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class NumberState(TypedDict):\n", " value: int\n", "\n", "def add_two(state: NumberState):\n", " return {'value': state['value'] + 2}\n", "\n", "def multiply_three(state: NumberState):\n", " return {'value': state['value'] * 3}\n", "\n", "def minus_one(state: NumberState):\n", " return {'value': state['value'] - 1}\n", "\n", "builder = StateGraph(NumberState)\n", "builder.add_node('add_two', add_two)\n", "builder.add_node('multiply_three', multiply_three)\n", "builder.add_node('minus_one', minus_one)\n", "\n", "builder.add_edge(START, 'add_two')\n", "builder.add_edge('add_two', 'multiply_three')\n", "builder.add_edge('multiply_three', 'minus_one')\n", "builder.add_edge('minus_one', END)\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({'value': 4})\n", "print(result)" ] }, { "cell_type": "markdown", "id": "e095e193", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个例子的重点是:**节点一多,边就必须更清楚地把顺序连接起来。**\n", "\n", "#### 三个节点的分工\n", "- `add_two`:把数字加 2\n", "- `multiply_three`:把结果乘 3\n", "- `minus_one`:把结果减 1\n", "\n", "每个节点都只做一件非常明确的事情。\n", "\n", "#### 四条边的作用\n", "这里总共有四条边:\n", "\n", "- `START -> add_two`\n", "- `add_two -> multiply_three`\n", "- `multiply_three -> minus_one`\n", "- `minus_one -> END`\n", "\n", "从这个例子你能很明显地看出:\n", "\n", "- 节点数量增加,边的数量通常也会跟着增加\n", "- 边并不是可有可无的,它是图的骨架\n", "\n", "#### 为什么顺序会影响结果\n", "如果初始值是 `4`,按当前顺序执行:\n", "\n", "1. `4 + 2 = 6`\n", "2. `6 * 3 = 18`\n", "3. `18 - 1 = 17`\n", "\n", "最终结果是:\n", "\n", "```python\n", "{'value': 17}\n", "```\n", "\n", "如果你把边的顺序改掉,例如先乘再加,结果就会完全不同。\n", "\n", "这说明一件事:\n", "\n", "**边不仅是在连线,它其实是在定义业务流程的顺序逻辑。**" ] }, { "cell_type": "markdown", "id": "f8b86cad", "metadata": {}, "source": [ "## 8. 常见错误:只加节点,不连边\n", "\n", "初学者最常见的错误之一,就是把节点都加进去了,但忘了把它们连起来。\n", "\n", "例如你可能写了:\n", "\n", "- `add_node('a', node_a)`\n", "- `add_node('b', node_b)`\n", "- `add_node('c', node_c)`\n", "\n", "但如果没有边,图并不知道该怎么走。\n", "\n", "所以一定要记住:\n", "\n", "- 节点 = 把工位摆出来\n", "- 边 = 把工位之间的传送带接起来\n", "\n", "只有节点没有边,就像车间里摆了很多机器,但它们之间没有流水线,产品根本流不过去。" ] }, { "cell_type": "markdown", "id": "2653a1ac", "metadata": {}, "source": [ "## 9. 常见错误:边的顺序写错\n", "\n", "另一个常见错误,是边都写了,但顺序写错了。\n", "\n", "比如本来你想要:\n", "\n", "```\n", "START -> add_two -> multiply_three -> minus_one -> END\n", "```\n", "\n", "结果却写成:\n", "\n", "```\n", "START -> multiply_three -> add_two -> minus_one -> END\n", "```\n", "\n", "这样虽然图也能运行,但业务逻辑已经变了。\n", "\n", "所以在写边的时候,不要只关心‘能不能连上’,更要关心‘是不是按我想要的顺序连上’。" ] }, { "cell_type": "markdown", "id": "c519d51c", "metadata": {}, "source": [ "## 10. 节点与边的分工总结\n", "\n", "到这里,你可以把节点与边的关系总结成一句话:\n", "\n", "**节点定义处理步骤,边定义执行路径。**\n", "\n", "更具体一点:\n", "\n", "| 组件 | 作用 | 你可以怎么理解 |\n", "| --- | --- | --- |\n", "| Node(节点) | 做具体工作 | 流程中的工位 |\n", "| Edge(边) | 决定下一步去哪 | 工位之间的路线 |\n", "| START | 流程入口 | 从哪里开始 |\n", "| END | 流程出口 | 在哪里结束 |\n", "\n", "只要把这四个角色分清楚,写简单的图就不会乱。" ] }, { "cell_type": "markdown", "id": "31760dde", "metadata": {}, "source": [ "## 11. 本节小结\n", "\n", "本节最重要的内容有五点:\n", "\n", "1. **节点就是处理步骤,通常由函数表示**\n", "2. **边就是连接关系,负责决定执行顺序**\n", "3. **`START` 是起点,`END` 是终点**\n", "4. **节点越多,越要靠边把流程顺序明确写出来**\n", "5. **图能不能正确运行,不仅取决于节点有没有写对,也取决于边有没有连对**\n", "\n", "理解了节点与边,后面再学条件边、分支流程、循环流程时,你会更容易看懂整张图是怎么运转的。" ] }, { "cell_type": "markdown", "id": "2de9c0d2", "metadata": {}, "source": [ "## 12. 本节练习\n", "\n", "1. 修改第一个示例,把 `greet` 节点输出改成‘欢迎你,某某同学!’\n", "2. 在第二个示例中再增加一个节点 `add_goodbye`,让最后再追加一句‘祝你学习顺利!’\n", "3. 修改第三个示例的边顺序,观察最终计算结果如何变化\n", "4. 尝试自己画出三个示例的流程图\n", "5. 思考:如果一个图有 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 }