Files
ai-agent-dev/16_状态管理.ipynb
2026-07-08 10:09:42 +08:00

636 lines
20 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": "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
}