Files
ai-agent-dev/19_循环与记忆.ipynb
2026-07-08 10:09:42 +08:00

832 lines
29 KiB
Plaintext
Raw Permalink 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",
"metadata": {},
"source": [
"# 19 循环与记忆\n",
"\n",
"## 学习目标\n",
"1. 理解LangGraph中循环和持久化记忆的实现方式\n",
"2. 掌握MemorySaver的使用实现对话状态持久化\n",
"3. 能够构建支持多轮交互的智能体流程"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. 为什么需要循环和记忆\n",
"\n",
"在之前的课程中,我们构建的图流程都是**单次执行**的:\n",
"\n",
"```\n",
"START -> node_a -> node_b -> END\n",
"```\n",
"\n",
"这种流程跑一次就结束了。但真实场景中,很多需求需要**多次循环**和**记忆状态**\n",
"\n",
"- **聊天机器人**:需要记住之前说过的话,才能进行多轮对话\n",
"- **任务分解**:可能需要反复调用工具,直到任务完成\n",
"- **信息收集**:需要逐步收集用户的信息,直到完整\n",
"- **多用户系统**:每个用户的对话历史需要独立保存\n",
"\n",
"简单来说:\n",
"\n",
"- **循环**:让流程可以重复执行某些节点\n",
"- **记忆**:让状态可以在多次执行之间保持"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. 循环的实现方式\n",
"\n",
"在LangGraph中循环主要通过**条件边**来实现。关键思路是:\n",
"\n",
"1. 执行某个节点\n",
"2. 判断是否需要继续循环\n",
"3. 如果需要,回到之前的节点重新执行\n",
"4. 如果不需要,走向结束\n",
"\n",
"流程图如下:\n",
"\n",
"```\n",
"START -> work_node -> 判断节点\n",
" |\n",
" ┌─────────┴─────────┐\n",
" ▼ ▼\n",
" 继续循环 结束\n",
" | |\n",
" └───────► work_node ◄──┘\n",
"```\n",
"\n",
"用条件边实现就是:`判断节点` 根据状态返回 `'loop'` 或 `'end'`,然后:\n",
"\n",
"- `'loop'` → 回到 `work_node`\n",
"- `'end'` → 走到 `END`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. 第一个例子:简单的计数器循环\n",
"\n",
"我们用一个计数器来演示循环的基本实现。\n",
"\n",
"流程目标:\n",
"- 从0开始计数\n",
"- 每次加1\n",
"- 当计数达到5时停止\n",
"- 每次循环都打印当前计数值"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from typing_extensions import TypedDict\n",
"from langgraph.graph import StateGraph, START, END\n",
"\n",
"class CounterState(TypedDict):\n",
" count: int\n",
"\n",
"def increment_node(state: CounterState):\n",
" new_count = state['count'] + 1\n",
" print(f'当前计数:{new_count}')\n",
" return {'count': new_count}\n",
"\n",
"def should_continue(state: CounterState):\n",
" if state['count'] < 5:\n",
" return 'loop'\n",
" return 'end'\n",
"\n",
"builder = StateGraph(CounterState)\n",
"builder.add_node('increment', increment_node)\n",
"\n",
"builder.add_edge(START, 'increment')\n",
"builder.add_conditional_edges(\n",
" 'increment',\n",
" should_continue,\n",
" {\n",
" 'loop': 'increment',\n",
" 'end': END\n",
" }\n",
")\n",
"\n",
"graph = builder.compile()\n",
"result = graph.invoke({'count': 0})\n",
"print(result)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"1. **State 定义**`CounterState` 只包含一个字段 `count`,用于存储当前计数值。\n",
"\n",
"2. **increment_node**:每次执行时,从状态中取出 `count`加1后放回状态。同时打印当前计数值。\n",
"\n",
"3. **should_continue**:路由函数,判断是否继续循环。如果 `count < 5` 返回 `'loop'`,否则返回 `'end'`。\n",
"\n",
"4. **条件边设置**\n",
" - 来源节点是 `'increment'`\n",
" - 路由函数是 `should_continue`\n",
" - 如果返回 `'loop'`,就回到 `'increment'` 节点(形成循环)\n",
" - 如果返回 `'end'`,就走到 `END`\n",
"\n",
"5. **执行**:初始状态是 `{'count': 0}`,图会自动循环执行,直到 `count` 达到5。\n",
"\n",
"运行结果显示计数从1到5说明循环成功执行了5次。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. 状态累积:消息历史的保存\n",
"\n",
"循环的核心价值在于**状态累积**。最常见的场景是保存对话历史。\n",
"\n",
"在聊天机器人中每次用户输入和AI回复都需要保存下来这样AI才能理解上下文。\n",
"\n",
"我们来构建一个简单的对话系统,每次循环都会把消息追加到历史中。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from typing_extensions import TypedDict\n",
"from langgraph.graph import StateGraph, START, END\n",
"\n",
"class ChatState(TypedDict):\n",
" messages: list\n",
" current_user_input: str\n",
"\n",
"def process_message(state: ChatState):\n",
" user_input = state['current_user_input']\n",
" \n",
" responses = {\n",
" '你好': '你好我是一个AI助手。',\n",
" '我叫张三': '你好张三!很高兴认识你。',\n",
" '我今天心情很好': '太好了!祝你有美好的一天!'\n",
" }\n",
" \n",
" assistant_reply = responses.get(user_input, '抱歉,我不太理解。')\n",
"\n",
" new_message = {'user': user_input, 'assistant': assistant_reply}\n",
" \n",
" return {\n",
" 'messages': state['messages'] + [new_message],\n",
" 'current_user_input': ''\n",
" }\n",
"\n",
"def route(state: ChatState):\n",
" if state['current_user_input']:\n",
" return 'process'\n",
" return END\n",
"\n",
"builder = StateGraph(ChatState)\n",
"builder.add_node('process', process_message)\n",
"\n",
"builder.add_edge(START, 'process')\n",
"builder.add_conditional_edges(\n",
" 'process',\n",
" route,\n",
" {\n",
" 'process': 'process',\n",
" END: END\n",
" }\n",
")\n",
"\n",
"graph = builder.compile()\n",
"\n",
"state = {'messages': [], 'current_user_input': '你好'}\n",
"state = graph.invoke(state)\n",
"\n",
"state['current_user_input'] = '我叫张三'\n",
"state = graph.invoke(state)\n",
"\n",
"state['current_user_input'] = '我今天心情很好'\n",
"state = graph.invoke(state)\n",
"\n",
"print(state['messages'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"1. **State 定义**`ChatState` 包含两个字段:\n",
" - `messages`:保存所有对话历史的列表\n",
" - `current_user_input`:当前用户输入\n",
"\n",
"2. **process_message**:处理消息的核心逻辑:\n",
" - 从状态中获取当前用户输入\n",
" - 根据预设的规则生成助手回复\n",
" - 将新消息追加到 `messages` 列表中(关键!)\n",
" - 清空 `current_user_input`\n",
"\n",
"3. **route**:路由函数判断是否有新输入需要处理。\n",
"\n",
"4. **执行流程**\n",
" - 第一次调用:输入'你好',消息列表变成 `[{'user': '你好', ...}]`\n",
" - 第二次调用:输入'我叫张三',消息列表变成两条记录\n",
" - 第三次调用:输入'我今天心情很好',消息列表变成三条记录\n",
"\n",
"运行结果显示消息历史被正确累积,这就是状态持久化的基础。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. MemorySaver持久化状态存储\n",
"\n",
"上面的例子中,我们手动传递状态。但在真实应用中,状态需要:\n",
"\n",
"- 在多次请求之间保持\n",
"- 支持多个用户同时使用(会话隔离)\n",
"- 重启后不丢失\n",
"\n",
"LangGraph 提供了 `MemorySaver` 来解决这个问题。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from typing_extensions import TypedDict\n",
"from langgraph.graph import StateGraph, START, END\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"class ChatState(TypedDict):\n",
" messages: list\n",
" current_user_input: str\n",
"\n",
"def process_message(state: ChatState):\n",
" user_input = state['current_user_input']\n",
" \n",
" responses = {\n",
" '你好': '你好我是一个AI助手。',\n",
" '我叫张三': '你好张三!很高兴认识你。',\n",
" '我今天心情很好': '太好了!祝你有美好的一天!'\n",
" }\n",
" \n",
" assistant_reply = responses.get(user_input, '抱歉,我不太理解。')\n",
"\n",
" new_message = {'user': user_input, 'assistant': assistant_reply}\n",
" \n",
" return {\n",
" 'messages': state['messages'] + [new_message],\n",
" 'current_user_input': ''\n",
" }\n",
"\n",
"def route(state: ChatState):\n",
" if state['current_user_input']:\n",
" return 'process'\n",
" return END\n",
"\n",
"builder = StateGraph(ChatState)\n",
"builder.add_node('process', process_message)\n",
"\n",
"builder.add_edge(START, 'process')\n",
"builder.add_conditional_edges(\n",
" 'process',\n",
" route,\n",
" {\n",
" 'process': 'process',\n",
" END: END\n",
" }\n",
")\n",
"\n",
"memory = MemorySaver()\n",
"graph = builder.compile(checkpointer=memory)\n",
"\n",
"config = {'configurable': {'thread_id': 'session_1'}}\n",
"\n",
"graph.invoke({'messages': [], 'current_user_input': '你好'}, config)\n",
"graph.invoke({'current_user_input': '我叫张三'}, config)\n",
"graph.invoke({'current_user_input': '我今天心情很好'}, config)\n",
"\n",
"final_state = graph.get_state(config)\n",
"print(final_state.values['messages'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"1. **导入 MemorySaver**:从 `langgraph.checkpoint.memory` 导入。\n",
"\n",
"2. **创建 MemorySaver 实例**`memory = MemorySaver()`\n",
"\n",
"3. **编译时传入 checkpointer**`graph = builder.compile(checkpointer=memory)`\n",
"\n",
"4. **配置会话ID**\n",
" ```python\n",
" config = {'configurable': {'thread_id': 'session_1'}}\n",
" ```\n",
" `thread_id` 是会话的唯一标识,不同用户用不同的 `thread_id`。\n",
"\n",
"5. **多次调用**:每次调用只传入新的输入,不需要手动传递状态。\n",
"\n",
"6. **获取状态**`graph.get_state(config)` 可以获取指定会话的当前状态。\n",
"\n",
"关键区别:之前需要手动传递 `state`,现在只需传入 `config`MemorySaver 会自动保存和恢复状态。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 6. 多会话隔离\n",
"\n",
"MemorySaver 的一个重要特性是**会话隔离**。不同的 `thread_id` 会维护独立的状态。\n",
"\n",
"下面的例子演示两个用户同时使用同一个图,但各自的对话历史完全独立。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from typing_extensions import TypedDict\n",
"from langgraph.graph import StateGraph, START, END\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"class ChatState(TypedDict):\n",
" messages: list\n",
" current_user_input: str\n",
"\n",
"def process_message(state: ChatState):\n",
" user_input = state['current_user_input']\n",
" \n",
" responses = {\n",
" '你好': '你好我是一个AI助手。',\n",
" '我是Alice': '你好Alice很高兴认识你。',\n",
" '我是Bob': '你好Bob很高兴认识你。'\n",
" }\n",
" \n",
" assistant_reply = responses.get(user_input, '抱歉,我不太理解。')\n",
"\n",
" new_message = {'user': user_input, 'assistant': assistant_reply}\n",
" \n",
" return {\n",
" 'messages': state['messages'] + [new_message],\n",
" 'current_user_input': ''\n",
" }\n",
"\n",
"def route(state: ChatState):\n",
" if state['current_user_input']:\n",
" return 'process'\n",
" return END\n",
"\n",
"builder = StateGraph(ChatState)\n",
"builder.add_node('process', process_message)\n",
"\n",
"builder.add_edge(START, 'process')\n",
"builder.add_conditional_edges(\n",
" 'process',\n",
" route,\n",
" {\n",
" 'process': 'process',\n",
" END: END\n",
" }\n",
")\n",
"\n",
"memory = MemorySaver()\n",
"graph = builder.compile(checkpointer=memory)\n",
"\n",
"config_a = {'configurable': {'thread_id': 'user_a'}}\n",
"config_b = {'configurable': {'thread_id': 'user_b'}}\n",
"\n",
"graph.invoke({'messages': [], 'current_user_input': '你好'}, config_a)\n",
"graph.invoke({'current_user_input': '我是Alice'}, config_a)\n",
"\n",
"graph.invoke({'messages': [], 'current_user_input': 'Hi'}, config_b)\n",
"graph.invoke({'current_user_input': '我是Bob'}, config_b)\n",
"\n",
"state_a = graph.get_state(config_a)\n",
"state_b = graph.get_state(config_b)\n",
"\n",
"print('用户A的对话历史:')\n",
"print(state_a.values['messages'])\n",
"print()\n",
"print('用户B的对话历史:')\n",
"print(state_b.values['messages'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"1. **创建两个配置**\n",
" - `config_a` 使用 `thread_id: 'user_a'`\n",
" - `config_b` 使用 `thread_id: 'user_b'`\n",
"\n",
"2. **分别调用**\n",
" - 用户A发送了'你好'和'我是Alice'\n",
" - 用户B发送了'Hi'和'我是Bob'\n",
"\n",
"3. **获取各自状态**\n",
" - 用户A的消息历史只有自己的两条消息\n",
" - 用户B的消息历史只有自己的两条消息\n",
"\n",
"运行结果清楚地展示了会话隔离:两个用户的对话历史完全独立,互不干扰。\n",
"\n",
"这就是多用户聊天系统的核心原理。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. 使用标准消息类型管理对话\n",
"\n",
"在实际应用中,我们通常使用 LangChain 的标准消息类型(如 `HumanMessage`、`AIMessage`)来管理对话。\n",
"\n",
"为了让 MemorySaver 能够自动追加消息而不是覆盖,我们需要使用 `add_messages` reducer。\n",
"\n",
"这种方式的特点:\n",
"- 使用标准的消息类型便于与LangChain组件集成\n",
"- 自动追加消息,避免手动拼接\n",
"- 方便与 LLM 集成"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from typing_extensions import TypedDict\n",
"from typing import Annotated\n",
"from langgraph.graph import StateGraph, START, END\n",
"from langgraph.graph.message import add_messages\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langchain_core.messages import HumanMessage, AIMessage\n",
"\n",
"class ChatState(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"def process_message(state: ChatState):\n",
" last_message = state['messages'][-1]\n",
" \n",
" if isinstance(last_message, HumanMessage):\n",
" user_input = last_message.content\n",
" \n",
" responses = {\n",
" '你好': '你好我是一个AI助手。',\n",
" '我叫张三': '你好张三!很高兴认识你。',\n",
" }\n",
" \n",
" assistant_reply = responses.get(user_input, '抱歉,我不太理解。')\n",
" \n",
" return {'messages': [AIMessage(content=assistant_reply)]}\n",
" \n",
" return state\n",
"\n",
"def route(state: ChatState):\n",
" last_message = state['messages'][-1]\n",
" if isinstance(last_message, HumanMessage):\n",
" return 'process'\n",
" return END\n",
"\n",
"builder = StateGraph(ChatState)\n",
"builder.add_node('process', process_message)\n",
"\n",
"builder.add_edge(START, 'process')\n",
"builder.add_conditional_edges(\n",
" 'process',\n",
" route,\n",
" {\n",
" 'process': 'process',\n",
" END: END\n",
" }\n",
")\n",
"\n",
"memory = MemorySaver()\n",
"graph = builder.compile(checkpointer=memory)\n",
"\n",
"config = {'configurable': {'thread_id': 'session_with_messages'}}\n",
"\n",
"graph.invoke({'messages': [HumanMessage(content='你好')]}, config)\n",
"graph.invoke({'messages': [HumanMessage(content='我叫张三')]}, config)\n",
"graph.invoke({'messages': [HumanMessage(content='今天天气怎么样')]}, config)\n",
"\n",
"final_state = graph.get_state(config)\n",
"for msg in final_state.values['messages']:\n",
" if isinstance(msg, HumanMessage):\n",
" print(f'Human: {msg.content}')\n",
" elif isinstance(msg, AIMessage):\n",
" print(f'AI: {msg.content}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"1. **导入 add_messages reducer**`from langgraph.graph.message import add_messages`\n",
" - reducer 用于控制状态字段的更新方式\n",
" - `add_messages` 会自动将新消息追加到列表中,而不是覆盖\n",
"\n",
"2. **定义 ChatState**:使用 `Annotated[list, add_messages]` 定义消息字段\n",
" - `Annotated` 是 Python 的类型提示工具,用于添加元数据\n",
" - 这里告诉 LangGraph 对 `messages` 字段使用 `add_messages` reducer\n",
"\n",
"3. **导入消息类型**\n",
" - `HumanMessage`:用户消息\n",
" - `AIMessage`AI回复\n",
"\n",
"4. **process_message**\n",
" - 获取最后一条消息\n",
" - 判断是否是用户消息\n",
" - 如果是,生成回复并返回 `{'messages': [AIMessage(...)]}`\n",
" - 由于使用了 `add_messages`,返回的新消息会自动追加到历史中\n",
"\n",
"5. **route**\n",
" - 如果最后一条是用户消息,继续处理\n",
" - 如果是AI消息说明已经处理过了结束\n",
"\n",
"6. **调用方式**:每次传入 `{'messages': [HumanMessage(content=...)]}`\n",
" - MemorySaver 会自动保存状态\n",
" - `add_messages` reducer 会自动追加新消息\n",
"\n",
"这种方式的优点:\n",
"- 使用标准的消息类型便于与LangChain组件集成\n",
"- 自动追加消息,避免手动拼接\n",
"- 后续可以直接将消息列表传给LLM"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 8. 综合案例:带工具调用的循环智能体\n",
"\n",
"现在我们把循环、记忆和工具调用结合起来,构建一个完整的智能体。\n",
"\n",
"流程目标:\n",
"- 用户询问关于天气或时间的问题\n",
"- 智能体判断是否需要调用工具\n",
"- 如果需要,调用工具获取信息\n",
"- 如果不需要,直接回答\n",
"- 循环直到任务完成\n",
"- 使用MemorySaver保存对话历史"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from typing_extensions import TypedDict\n",
"from langgraph.graph import StateGraph, START, END\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"from langchain_core.messages import HumanMessage, AIMessage, ToolMessage\n",
"\n",
"class AgentState(TypedDict):\n",
" messages: list\n",
" next_tool: str\n",
"\n",
"def decide_tool(state: AgentState):\n",
" last_message = state['messages'][-1]\n",
" user_input = last_message.content\n",
" \n",
" print(f'用户问了:{user_input}')\n",
" \n",
" if '几点' in user_input or '时间' in user_input:\n",
" print('需要调用工具get_time')\n",
" return {'next_tool': 'get_time'}\n",
" elif '天气' in user_input:\n",
" print('需要调用工具get_weather')\n",
" return {'next_tool': 'get_weather'}\n",
" else:\n",
" print('不需要调用工具,直接回答')\n",
" return {'next_tool': None}\n",
"\n",
"def call_tool(state: AgentState):\n",
" tool_name = state['next_tool']\n",
" \n",
" if tool_name == 'get_time':\n",
" result = '2024-01-15 14:30:00'\n",
" elif tool_name == 'get_weather':\n",
" result = '晴天25度'\n",
" else:\n",
" result = '未知工具'\n",
" \n",
" print(f'调用工具 {tool_name},结果:{result}')\n",
" \n",
" return {\n",
" 'messages': state['messages'] + [ToolMessage(content=result, tool_call_id='1')],\n",
" 'next_tool': None\n",
" }\n",
"\n",
"def summarize(state: AgentState):\n",
" last_message = state['messages'][-1]\n",
" \n",
" if isinstance(last_message, ToolMessage):\n",
" tool_result = last_message.content\n",
" reply = f'总结回答:{tool_result}'\n",
" else:\n",
" reply = '抱歉,我不太理解你的问题。'\n",
" \n",
" print(reply)\n",
" \n",
" return {\n",
" 'messages': state['messages'] + [AIMessage(content=reply)]\n",
" }\n",
"\n",
"def route_after_decide(state: AgentState):\n",
" if state['next_tool']:\n",
" return 'call_tool'\n",
" return 'summarize'\n",
"\n",
"def route_after_summarize(state: AgentState):\n",
" last_message = state['messages'][-1]\n",
" if isinstance(last_message, HumanMessage):\n",
" return 'decide_tool'\n",
" return END\n",
"\n",
"builder = StateGraph(AgentState)\n",
"builder.add_node('decide_tool', decide_tool)\n",
"builder.add_node('call_tool', call_tool)\n",
"builder.add_node('summarize', summarize)\n",
"\n",
"builder.add_edge(START, 'decide_tool')\n",
"builder.add_conditional_edges(\n",
" 'decide_tool',\n",
" route_after_decide,\n",
" {\n",
" 'call_tool': 'call_tool',\n",
" 'summarize': 'summarize'\n",
" }\n",
")\n",
"builder.add_edge('call_tool', 'summarize')\n",
"builder.add_conditional_edges(\n",
" 'summarize',\n",
" route_after_summarize,\n",
" {\n",
" 'decide_tool': 'decide_tool',\n",
" END: END\n",
" }\n",
")\n",
"\n",
"memory = MemorySaver()\n",
"graph = builder.compile(checkpointer=memory)\n",
"\n",
"config = {'configurable': {'thread_id': 'tool_agent_session'}}\n",
"\n",
"graph.invoke({'messages': [HumanMessage(content='现在几点了')], 'next_tool': None}, config)\n",
"\n",
"current_state = graph.get_state(config)\n",
"new_messages = current_state.values['messages'] + [HumanMessage(content='今天天气怎么样')]\n",
"graph.invoke({'messages': new_messages, 'next_tool': None}, config)\n",
"\n",
"final_state = graph.get_state(config)\n",
"print()\n",
"print('完整对话历史:')\n",
"for msg in final_state.values['messages']:\n",
" if isinstance(msg, HumanMessage):\n",
" print(f'Human: {msg.content}')\n",
" elif isinstance(msg, AIMessage):\n",
" print(f'AI: {msg.content}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这个案例展示了一个完整的智能体流程:\n",
"\n",
"1. **AgentState**:使用 TypedDict 定义状态,包含:\n",
" - `messages`:消息列表\n",
" - `next_tool`:下一个要调用的工具\n",
"\n",
"2. **decide_tool**:判断是否需要调用工具\n",
" - 如果用户问时间,返回 `{'next_tool': 'get_time'}`\n",
" - 如果用户问天气,返回 `{'next_tool': 'get_weather'}`\n",
" - 如果不需要工具,返回 `{'next_tool': None}`\n",
"\n",
"3. **call_tool**:执行工具调用\n",
" - 根据 `next_tool` 调用相应工具\n",
" - 将结果追加到 `messages` 列表\n",
"\n",
"4. **summarize**:总结回答\n",
" - 如果最后是工具消息,提取结果并生成回答\n",
" - 将回答追加到 `messages` 列表\n",
"\n",
"5. **route_after_decide**decide_tool 之后的路由\n",
" - 如果 `next_tool` 有值,进入 `call_tool`\n",
" - 如果 `next_tool` 为 None进入 `summarize`\n",
"\n",
"6. **route_after_summarize**summarize 之后的路由\n",
" - 如果最后是用户消息,回到 `decide_tool`\n",
" - 否则结束\n",
"\n",
"流程图:\n",
"\n",
"```\n",
"START -> decide_tool -> [条件边] -> call_tool -> summarize\n",
" ^ |\n",
" | |\n",
" +--------------+\n",
"```\n",
"\n",
"这个流程可以处理多轮对话,每轮都可以调用工具并保存历史。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 9. 总结\n",
"\n",
"### 核心知识点\n",
"\n",
"1. **循环实现**:通过条件边让流程回到之前的节点,形成循环\n",
"2. **状态累积**:在节点中不断追加数据到状态字段(如消息列表)\n",
"3. **MemorySaver**:自动保存和恢复状态,无需手动传递\n",
"4. **会话隔离**:通过 `thread_id` 区分不同用户的会话\n",
"5. **add_messages reducer**:使用 `Annotated[list, add_messages]` 实现消息自动追加\n",
"\n",
"### 关键API\n",
"\n",
"| API | 作用 |\n",
"| --- | --- |\n",
"| `MemorySaver()` | 创建内存状态存储 |\n",
"| `builder.compile(checkpointer=memory)` | 编译时启用状态持久化 |\n",
"| `config = {'configurable': {'thread_id': ...}}` | 设置会话ID |\n",
"| `graph.get_state(config)` | 获取指定会话的状态 |\n",
"| `add_messages` | 自动追加消息的 reducer |\n",
"\n",
"### 实践要点\n",
"\n",
"- 循环的关键是路由函数返回某个已存在的节点名\n",
"- 状态累积需要在节点中返回新的状态值\n",
"- 使用 `Annotated[list, add_messages]` 可以让 MemorySaver 自动追加消息\n",
"- MemorySaver 适合开发和测试,生产环境可使用其他存储后端\n",
"- 会话隔离通过 `thread_id` 实现必须为每个用户分配唯一ID"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 练习\n",
"\n",
"1. 修改计数器示例让它从10递减到0\n",
"2. 扩展对话示例,添加更多回复规则\n",
"3. 创建一个多轮问答系统,支持用户追问\n",
"4. 尝试使用不同的 `thread_id` 模拟三个用户同时对话"
]
}
],
"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": 4
}