{ "cells": [ { "cell_type": "markdown", "id": "cc615887", "metadata": {}, "source": [ "# 16 状态管理\n", "\n", "## 学习目标\n", "1. 理解 LangGraph 中状态(State)的设计与作用\n", "2. 掌握使用 TypedDict 定义图状态\n", "3. 学会在节点间传递、读取和更新状态\n", "4. 理解多字段状态在实际流程中的用法\n", "5. 避免状态设计中的常见问题" ] }, { "cell_type": "markdown", "id": "dfebe943", "metadata": {}, "source": [ "## 1. 什么是状态管理\n", "\n", "在 LangGraph 中,**状态(State)** 可以理解为“流程运行时一直随身携带的一份数据”。\n", "\n", "前一节我们已经接触过状态,例如:\n", "\n", "```python\n", "{'value': 3}\n", "```\n", "\n", "或者:\n", "\n", "```python\n", "{'score': 85, 'result': '成绩合格,顺利通过'}\n", "```\n", "\n", "这些数据会在图的不同节点之间传来传去。\n", "\n", "状态管理要解决的问题,其实很简单:\n", "\n", "- 当前流程已经走到哪一步了?\n", "- 前面节点产出的结果,后面节点怎么接着用?\n", "- 某个节点更新了数据,后续节点如何读取最新值?\n", "\n", "你可以把状态想象成一张‘流程记录卡’:\n", "\n", "- 每经过一个节点,就在卡上补充信息\n", "- 后面的节点只需要看这张卡,就知道前面发生了什么\n", "\n", "所以,**状态管理的本质,就是管理这张共享记录卡。**" ] }, { "cell_type": "markdown", "id": "a40150ac", "metadata": {}, "source": [ "## 2. 为什么状态很重要\n", "\n", "如果没有状态,节点之间就很难协作。\n", "\n", "例如一个流程要完成下面几步:\n", "\n", "1. 接收用户输入\n", "2. 提取关键词\n", "3. 根据关键词生成结论\n", "\n", "那么问题来了:\n", "\n", "- 第二步提取出来的关键词,怎么交给第三步?\n", "- 第一节点记录的原始问题,第三步还能不能访问?\n", "- 如果流程还要继续加步骤,数据还能不能统一管理?\n", "\n", "答案就是:都放进状态里。\n", "\n", "状态的几个核心价值:\n", "\n", "- **统一传递数据**:所有节点都从同一个地方读取信息\n", "- **减少参数混乱**:不用手动给每个函数传很多单独变量\n", "- **便于扩展流程**:后面增加新节点时,只要继续读写状态即可\n", "- **便于调试**:看状态就能知道流程执行到了什么程度" ] }, { "cell_type": "markdown", "id": "644129aa", "metadata": {}, "source": [ "## 3. 用 TypedDict 定义状态\n", "\n", "在 LangGraph 中,我们通常用 `TypedDict` 来定义状态结构。\n", "\n", "这样做有两个好处:\n", "\n", "1. 让自己清楚状态里有哪些字段\n", "2. 让代码更容易维护和理解\n", "\n", "下面先看一个最简单的例子。" ] }, { "cell_type": "code", "execution_count": null, "id": "08a43781", "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "\n", "class UserState(TypedDict):\n", " name: str\n", " age: int\n", " city: str\n", "\n", "example_state: UserState = {\n", " 'name': '张三',\n", " 'age': 20,\n", " 'city': '北京'\n", "}\n", "\n", "print(example_state)" ] }, { "cell_type": "markdown", "id": "4cdd8784", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这段代码本身还没有进入 LangGraph,只是在说明状态应该怎样定义。\n", "\n", "#### `TypedDict` 是什么\n", "`TypedDict` 可以理解为‘带字段说明的字典’。\n", "\n", "普通字典也能写成:\n", "\n", "```python\n", "{'name': '张三', 'age': 20, 'city': '北京'}\n", "```\n", "\n", "但如果项目变复杂,单纯靠大家记忆“这个字典里应该有哪些字段”会很容易出错。\n", "\n", "`TypedDict` 的作用就是把这种约定明确写出来。\n", "\n", "#### `class UserState(TypedDict)`\n", "这表示我们定义了一种状态结构,名字叫 `UserState`。\n", "\n", "它要求状态中有三个字段:\n", "\n", "- `name`:字符串\n", "- `age`:整数\n", "- `city`:字符串\n", "\n", "#### `example_state: UserState = {...}`\n", "这表示我们创建了一个符合 `UserState` 结构的状态对象。\n", "\n", "从教学角度看,这一步很重要,因为它让你意识到:\n", "\n", "- 状态本质上仍然是字典\n", "- `TypedDict` 只是帮助我们把字典结构写清楚\n", "\n", "在 LangGraph 中,节点之间传递的就是这种结构化字典。" ] }, { "cell_type": "markdown", "id": "dceebf80", "metadata": {}, "source": [ "## 4. 读取状态:节点如何使用已有数据\n", "\n", "定义完状态之后,下一步就是在节点中读取它。\n", "\n", "下面这个例子演示:节点如何从状态里取出数据并生成一句自我介绍。" ] }, { "cell_type": "code", "execution_count": null, "id": "cb6536ba", "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class ProfileState(TypedDict):\n", " name: str\n", " age: int\n", " city: str\n", " intro: str\n", "\n", "def create_intro(state: ProfileState):\n", " intro_text = f'大家好,我叫{state[\"name\"]},今年{state[\"age\"]}岁,来自{state[\"city\"]}。'\n", " return {'intro': intro_text}\n", "\n", "builder = StateGraph(ProfileState)\n", "builder.add_node('create_intro', create_intro)\n", "builder.add_edge(START, 'create_intro')\n", "builder.add_edge('create_intro', END)\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({\n", " 'name': '李雷',\n", " 'age': 18,\n", " 'city': '上海',\n", " 'intro': ''\n", "})\n", "\n", "print(result)\n", "print(result['intro'])" ] }, { "cell_type": "markdown", "id": "120b0a74", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个例子重点不是图结构本身,而是“节点如何读取状态中的已有字段”。\n", "\n", "#### 状态结构\n", "这里的状态有四个字段:\n", "\n", "- `name`\n", "- `age`\n", "- `city`\n", "- `intro`\n", "\n", "前三个字段是输入信息,最后一个字段是我们希望在流程中生成的新结果。\n", "\n", "#### `create_intro(state)`\n", "这个节点做的事情很直观:\n", "\n", "1. 从状态中读取 `name`\n", "2. 从状态中读取 `age`\n", "3. 从状态中读取 `city`\n", "4. 组装成一句完整介绍\n", "5. 把结果写回 `intro` 字段\n", "\n", "也就是说,这个节点没有改变原来的 `name`、`age`、`city`,只是新增或更新了 `intro`。\n", "\n", "#### 节点返回值为什么只写 `{'intro': intro_text}`\n", "这是 LangGraph 状态管理里非常重要的一点:\n", "\n", "**节点只需要返回自己负责修改的字段。**\n", "\n", "原来的字段不会凭空消失,而是继续保留在状态里。\n", "\n", "所以最终 `result` 中既有原始输入,也有新生成的 `intro`。\n", "\n", "#### 这个例子的意义\n", "它展示了状态管理的最基础工作方式:\n", "\n", "- 输入状态中先有一部分信息\n", "- 节点读取这些信息进行加工\n", "- 再把加工结果写回状态\n", "\n", "这就是后面所有复杂流程的基础。" ] }, { "cell_type": "markdown", "id": "28600cec", "metadata": {}, "source": [ "## 5. 更新状态:后面的节点继续接着用\n", "\n", "状态管理真正强大的地方,不在于‘一个节点能读写状态’,而在于**前一个节点更新的内容,后一个节点可以继续使用**。\n", "\n", "下面这个例子演示两个节点接力处理状态:\n", "\n", "1. 第一个节点生成问候语\n", "2. 第二个节点在问候语后面再补一句欢迎词" ] }, { "cell_type": "code", "execution_count": null, "id": "98e52d28", "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class GreetingState(TypedDict):\n", " name: str\n", " greeting: str\n", " final_message: str\n", "\n", "def make_greeting(state: GreetingState):\n", " return {'greeting': f'你好,{state[\"name\"]}!'}\n", "\n", "def make_final_message(state: GreetingState):\n", " final_text = state['greeting'] + ' 欢迎来到状态管理课程。'\n", " return {'final_message': final_text}\n", "\n", "builder = StateGraph(GreetingState)\n", "builder.add_node('make_greeting', make_greeting)\n", "builder.add_node('make_final_message', make_final_message)\n", "\n", "builder.add_edge(START, 'make_greeting')\n", "builder.add_edge('make_greeting', 'make_final_message')\n", "builder.add_edge('make_final_message', END)\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({\n", " 'name': '小王',\n", " 'greeting': '',\n", " 'final_message': ''\n", "})\n", "\n", "print(result)\n", "print(result['final_message'])" ] }, { "cell_type": "markdown", "id": "70fa1f92", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个例子体现了状态在多个节点之间的‘接力传递’。\n", "\n", "#### 第一个节点 `make_greeting`\n", "它只做一件事:根据 `name` 生成一句问候语,写入 `greeting`。\n", "\n", "例如:\n", "\n", "```python\n", "{'name': '小王'}\n", "```\n", "\n", "会生成:\n", "\n", "```python\n", "{'greeting': '你好,小王!'}\n", "```\n", "\n", "#### 第二个节点 `make_final_message`\n", "它并不重新根据 `name` 生成问候,而是直接读取上一个节点已经写入状态的 `greeting`。\n", "\n", "然后在它后面拼上一句:\n", "\n", "```text\n", "欢迎来到状态管理课程。\n", "```\n", "\n", "最后写入 `final_message`。\n", "\n", "#### 这说明了什么\n", "这说明状态不仅能存放“输入数据”,还能存放“中间结果”。\n", "\n", "这是状态管理特别重要的一点:\n", "\n", "- 输入数据:用户最初给的信息\n", "- 中间结果:某个节点处理出来的阶段性结果\n", "- 最终结果:流程结束时输出的内容\n", "\n", "如果没有状态,你就得手动把这些内容一级一级传下去;有了状态,流程自然就串起来了。" ] }, { "cell_type": "markdown", "id": "f28536c4", "metadata": {}, "source": [ "## 6. 多字段状态:让流程更接近真实业务\n", "\n", "真实项目里的状态,通常不会只有 1 个或 2 个字段。\n", "\n", "例如一个订单处理流程,可能需要同时记录:\n", "\n", "- 用户姓名\n", "- 商品名称\n", "- 数量\n", "- 总价\n", "- 订单状态\n", "\n", "下面我们用一个简单的订单示例,演示多字段状态的管理方式。" ] }, { "cell_type": "code", "execution_count": null, "id": "b3d1077b", "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "from langgraph.graph import StateGraph, START, END\n", "\n", "class OrderState(TypedDict):\n", " customer_name: str\n", " product_name: str\n", " price: float\n", " quantity: int\n", " total_price: float\n", " order_status: str\n", "\n", "def calculate_total(state: OrderState):\n", " total = state['price'] * state['quantity']\n", " return {'total_price': total}\n", "\n", "def confirm_order(state: OrderState):\n", " status = f'订单已确认:{state[\"customer_name\"]} 购买了 {state[\"quantity\"]} 件 {state[\"product_name\"]},总价 {state[\"total_price\"]} 元。'\n", " return {'order_status': status}\n", "\n", "builder = StateGraph(OrderState)\n", "builder.add_node('calculate_total', calculate_total)\n", "builder.add_node('confirm_order', confirm_order)\n", "\n", "builder.add_edge(START, 'calculate_total')\n", "builder.add_edge('calculate_total', 'confirm_order')\n", "builder.add_edge('confirm_order', END)\n", "\n", "graph = builder.compile()\n", "result = graph.invoke({\n", " 'customer_name': '王芳',\n", " 'product_name': '机械键盘',\n", " 'price': 299.0,\n", " 'quantity': 2,\n", " 'total_price': 0.0,\n", " 'order_status': ''\n", "})\n", "\n", "print(result)\n", "print(result['order_status'])" ] }, { "cell_type": "markdown", "id": "ad35700b", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个例子很适合用来理解‘多字段状态为什么有必要’。\n", "\n", "#### 状态里有哪些信息\n", "这里的状态已经不只是一个简单变量,而是一组完整业务数据:\n", "\n", "- 谁在下单:`customer_name`\n", "- 买的是什么:`product_name`\n", "- 单价是多少:`price`\n", "- 买了几件:`quantity`\n", "- 总价是多少:`total_price`\n", "- 当前订单描述:`order_status`\n", "\n", "#### `calculate_total` 节点\n", "这个节点只负责计算总价:\n", "\n", "```python\n", "总价 = 单价 * 数量\n", "```\n", "\n", "它返回 `{'total_price': total}`,把结果写回状态。\n", "\n", "#### `confirm_order` 节点\n", "这个节点读取多个字段:\n", "\n", "- `customer_name`\n", "- `quantity`\n", "- `product_name`\n", "- `total_price`\n", "\n", "然后把这些信息拼成完整订单说明,再写入 `order_status`。\n", "\n", "#### 这个例子传达的核心思想\n", "状态不是只给“一个节点”准备的,而是给“整个流程”准备的。\n", "\n", "每个节点只关心自己需要的部分字段,但所有字段加起来,构成了完整的业务上下文。\n", "\n", "这也是为什么状态设计要尽量清晰:因为它决定了整个流程的数据组织方式。" ] }, { "cell_type": "markdown", "id": "4858b6f3", "metadata": {}, "source": [ "## 7. 状态设计的常见原则\n", "\n", "写状态时,建议遵循下面几个原则。\n", "\n", "### 7.1 字段名要清楚\n", "不要用太模糊的名字,例如:\n", "\n", "- `data`\n", "- `info`\n", "- `result1`\n", "\n", "更好的方式是:\n", "\n", "- `user_question`\n", "- `retrieved_docs`\n", "- `final_answer`\n", "\n", "字段名越清楚,后面越不容易乱。\n", "\n", "### 7.2 一个字段只表达一种含义\n", "例如不要今天把 `result` 用来存字符串,明天又用来存字典。\n", "\n", "字段含义最好稳定,不要随流程变化得太厉害。\n", "\n", "### 7.3 把中间结果保留下来\n", "有些同学会只保留最终结果,把中间结果全部覆盖掉。这样虽然表面简洁,但调试时会非常痛苦。\n", "\n", "如果中间结果后面可能还会用到,或者你希望排查流程问题,就应该保留。\n", "\n", "### 7.4 不要把无关信息全塞进去\n", "状态不是越大越好。\n", "\n", "只保留这个流程真正需要的数据,避免状态越来越臃肿。" ] }, { "cell_type": "markdown", "id": "d701714f", "metadata": {}, "source": [ "## 8. 一个常见误区:以为节点要返回完整状态\n", "\n", "很多初学者会误以为:每个节点都必须返回整份状态,例如:\n", "\n", "```python\n", "return {\n", " 'name': state['name'],\n", " 'age': state['age'],\n", " 'city': state['city'],\n", " 'intro': intro_text\n", "}\n", "```\n", "\n", "其实在很多情况下,这么写没有必要。\n", "\n", "更简洁的方式通常是:\n", "\n", "```python\n", "return {'intro': intro_text}\n", "```\n", "\n", "原因是:LangGraph 会自动把更新结果合并回原状态。\n", "\n", "所以,除非你确实要同时改很多字段,否则通常只返回你真正改动的那部分。\n", "\n", "这会让代码更短,也更不容易出错。" ] }, { "cell_type": "markdown", "id": "a28700b1", "metadata": {}, "source": [ "## 9. 状态管理和后续 Agent 的关系\n", "\n", "后面学 Agent、RAG、多轮工具调用时,你会越来越频繁地用到状态。\n", "\n", "例如一个复杂 Agent 的状态里,可能会放这些字段:\n", "\n", "- 用户问题 `user_query`\n", "- 历史消息 `messages`\n", "- 工具调用结果 `tool_result`\n", "- 检索到的资料 `retrieved_docs`\n", "- 当前决策 `decision`\n", "- 最终回答 `final_answer`\n", "\n", "所以可以说:\n", "\n", "**图结构解决的是“流程怎么走”,状态管理解决的是“数据怎么跟着流程走”。**\n", "\n", "这两者是 LangGraph 最核心的两根主线。" ] }, { "cell_type": "markdown", "id": "2a08da16", "metadata": {}, "source": [ "## 10. 本节小结\n", "\n", "本节你需要重点记住以下几点:\n", "\n", "1. **状态本质上是共享字典**,只是通常用 `TypedDict` 把结构定义清楚\n", "2. **节点通过读取状态拿到已有数据**\n", "3. **节点通过返回字典更新状态**\n", "4. **后面的节点可以继续读取前面节点写入的中间结果**\n", "5. **好的状态设计,会让整个图流程更清晰、更稳定、更容易调试**\n", "\n", "理解了状态管理,后面再看复杂流程时,你就不会只盯着‘节点怎么连’,而会同时关注‘数据怎么流动’。" ] }, { "cell_type": "markdown", "id": "caa87287", "metadata": {}, "source": [ "## 11. 本节练习\n", "\n", "1. 修改 `ProfileState` 示例,在状态中增加 `hobby` 字段,并把爱好加入自我介绍\n", "2. 修改 `GreetingState` 示例,让第二个节点在最终消息后面再追加一句祝福\n", "3. 修改 `OrderState` 示例,增加一个 `discount` 字段,并让总价计算支持折扣\n", "4. 思考:哪些字段属于输入数据,哪些字段属于中间结果,哪些字段属于最终输出?\n", "5. 思考:如果一个 Agent 需要反复调用工具,你觉得状态里至少应该保留哪些字段?" ] } ], "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 }