587 lines
17 KiB
Plaintext
587 lines
17 KiB
Plaintext
{
|
||
"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
|
||
}
|