Files
ai-agent-dev/17_节点与边.ipynb
2026-07-08 10:09:42 +08:00

587 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"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
}