Files
ai-agent-dev/21_对话代理.ipynb
2026-07-08 10:09:42 +08:00

837 lines
26 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",
"id": "a24e74a7",
"metadata": {},
"source": [
"# 21 对话代理\n",
"\n",
"## 学习目标\n",
"1. 掌握基于 LangGraph 构建对话代理的方法\n",
"2. 理解消息列表(`messages`)在对话状态中的作用\n",
"3. 能够实现具备历史记忆和上下文理解的对话机器人\n",
"4. 理解 `HumanMessage`、`AIMessage`、`SystemMessage` 的区别\n",
"5. 学会用检查点机制保存多轮对话状态"
]
},
{
"cell_type": "markdown",
"id": "04d2a1aa",
"metadata": {},
"source": [
"## 1. 什么是对话代理\n",
"\n",
"对话代理可以理解为一个能够和用户连续交流的 AI 程序。\n",
"\n",
"普通的大模型调用通常是这样的:\n",
"\n",
"```\n",
"用户输入 -> 模型回答 -> 结束\n",
"```\n",
"\n",
"而对话代理更像这样:\n",
"\n",
"```\n",
"用户第 1 轮输入 -> 模型回答\n",
"用户第 2 轮输入 -> 模型结合上一轮继续回答\n",
"用户第 3 轮输入 -> 模型继续理解上下文\n",
"```\n",
"\n",
"它和一次性问答最大的区别是:**对话代理需要记住前面说过什么。**\n",
"\n",
"例如用户先说:\n",
"\n",
"```text\n",
"我叫小王。\n",
"```\n",
"\n",
"下一轮再问:\n",
"\n",
"```text\n",
"我叫什么?\n",
"```\n",
"\n",
"如果代理能回答‘你叫小王’,说明它具备了基本的对话记忆。"
]
},
{
"cell_type": "markdown",
"id": "2c678a5b",
"metadata": {},
"source": [
"## 2. 为什么对话代理要用 `messages`\n",
"\n",
"在 LangChain 和 LangGraph 中,对话通常不是用一个普通字符串保存,而是用消息列表 `messages` 保存。\n",
"\n",
"一个简单的消息列表可能长这样:\n",
"\n",
"```python\n",
"[\n",
" HumanMessage(content='我叫小王'),\n",
" AIMessage(content='你好,小王!'),\n",
" HumanMessage(content='我叫什么?')\n",
"]\n",
"```\n",
"\n",
"这个列表的好处是:\n",
"\n",
"- 能保存多轮对话历史\n",
"- 能区分每句话是谁说的\n",
"- 能直接传给聊天模型\n",
"- 很适合放进 LangGraph 的状态里\n",
"\n",
"可以把 `messages` 理解成一份‘聊天记录本’。每轮对话都会往记录本里追加新内容。"
]
},
{
"cell_type": "markdown",
"id": "9b9665f1",
"metadata": {},
"source": [
"## 3. 三种常见消息类型\n",
"\n",
"对话代理中最常见的消息类型有三种:\n",
"\n",
"| 消息类型 | 含义 | 常见用途 |\n",
"| --- | --- | --- |\n",
"| `SystemMessage` | 系统消息 | 规定 AI 的角色、规则、风格 |\n",
"| `HumanMessage` | 用户消息 | 保存用户输入 |\n",
"| `AIMessage` | AI 消息 | 保存模型回复 |\n",
"\n",
"举个例子:\n",
"\n",
"- `SystemMessage`:你是一个耐心的 Python 老师\n",
"- `HumanMessage`:请解释什么是列表\n",
"- `AIMessage`:列表是 Python 中用于存放多个元素的数据结构\n",
"\n",
"这三类消息合在一起,就能表达一段完整对话。"
]
},
{
"cell_type": "markdown",
"id": "cd8ce90c",
"metadata": {},
"source": [
"## 4. 第一个例子:手动维护消息列表\n",
"\n",
"在进入 LangGraph 之前,先用一个最简单的例子理解 `messages`。\n",
"\n",
"这个例子不调用大模型,只演示消息列表如何保存对话历史。"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2e02c643",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import HumanMessage, AIMessage, SystemMessage\n",
"\n",
"messages = [\n",
" SystemMessage(content='你是一个耐心的课程助教。'),\n",
" HumanMessage(content='我叫小王。'),\n",
" AIMessage(content='你好,小王,很高兴认识你。'),\n",
" HumanMessage(content='我叫什么?')\n",
"]\n",
"\n",
"for message in messages:\n",
" print(type(message).__name__, ':', message.content)"
]
},
{
"cell_type": "markdown",
"id": "d3d58143",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这个例子没有使用 LangGraph也没有调用大模型目的只是先把消息列表讲清楚。\n",
"\n",
"#### `SystemMessage`\n",
"第一条消息是系统消息:\n",
"\n",
"```python\n",
"SystemMessage(content='你是一个耐心的课程助教。')\n",
"```\n",
"\n",
"它不是用户说的话,也不是模型回答的话,而是给模型设置角色和行为规则。\n",
"\n",
"#### `HumanMessage`\n",
"用户说的话用 `HumanMessage` 表示。\n",
"\n",
"例如:\n",
"\n",
"```python\n",
"HumanMessage(content='我叫小王。')\n",
"```\n",
"\n",
"这表示用户告诉代理:自己的名字是小王。\n",
"\n",
"#### `AIMessage`\n",
"AI 的回复用 `AIMessage` 表示。\n",
"\n",
"例如:\n",
"\n",
"```python\n",
"AIMessage(content='你好,小王,很高兴认识你。')\n",
"```\n",
"\n",
"它记录了上一轮模型回答过什么。\n",
"\n",
"#### 为什么要保存完整列表\n",
"最后一条用户消息是:\n",
"\n",
"```python\n",
"HumanMessage(content='我叫什么?')\n",
"```\n",
"\n",
"如果只看这一句话,模型不知道‘我’是谁。\n",
"\n",
"但如果把完整 `messages` 传给模型,模型就能从前面的聊天记录里看到:用户之前说过自己叫小王。\n",
"\n",
"这就是对话历史的作用。"
]
},
{
"cell_type": "markdown",
"id": "fed8eded",
"metadata": {},
"source": [
"## 5. 在 LangGraph 状态中保存 messages\n",
"\n",
"对话代理的核心状态通常就是 `messages`。\n",
"\n",
"但是这里有一个重要细节:多轮对话不是每次覆盖旧消息,而是要把新消息追加到旧消息后面。\n",
"\n",
"LangGraph 提供了 `add_messages`,可以帮助我们把消息列表自动追加合并。\n",
"\n",
"下面先看一个不调用模型的 LangGraph 示例。"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d199158f",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"from typing_extensions import TypedDict\n",
"from langchain_core.messages import HumanMessage, AIMessage\n",
"from langgraph.graph import StateGraph, START, END\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"class ChatState(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"def simple_reply(state: ChatState):\n",
" last_message = state['messages'][-1]\n",
" reply = AIMessage(content=f'我收到了你的消息:{last_message.content}')\n",
" return {'messages': [reply]}\n",
"\n",
"builder = StateGraph(ChatState)\n",
"builder.add_node('simple_reply', simple_reply)\n",
"builder.add_edge(START, 'simple_reply')\n",
"builder.add_edge('simple_reply', END)\n",
"\n",
"graph = builder.compile()\n",
"result = graph.invoke({\n",
" 'messages': [HumanMessage(content='你好,请介绍一下你自己')]\n",
"})\n",
"\n",
"for message in result['messages']:\n",
" print(type(message).__name__, ':', message.content)"
]
},
{
"cell_type": "markdown",
"id": "76db6c52",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这个例子已经进入 LangGraph但为了降低理解难度暂时没有调用真正的大模型。\n",
"\n",
"#### `Annotated[list, add_messages]`\n",
"这是本节最关键的一行:\n",
"\n",
"```python\n",
"messages: Annotated[list, add_messages]\n",
"```\n",
"\n",
"它表示:`messages` 是一个列表,并且当节点返回新的 `messages` 时,不是直接覆盖旧列表,而是把新消息追加进去。\n",
"\n",
"如果没有 `add_messages`,你返回:\n",
"\n",
"```python\n",
"{'messages': [reply]}\n",
"```\n",
"\n",
"可能会覆盖原来的消息。\n",
"\n",
"有了 `add_messages`,它的效果更像:\n",
"\n",
"```python\n",
"旧消息列表 + 新消息列表\n",
"```\n",
"\n",
"这正是多轮对话需要的行为。\n",
"\n",
"#### `simple_reply` 节点\n",
"这个节点做了三件事:\n",
"\n",
"1. 从 `state['messages']` 中取出最后一条消息\n",
"2. 根据最后一条用户消息生成一个简单回复\n",
"3. 把回复包装成 `AIMessage` 返回\n",
"\n",
"这里的回复不是模型生成的,而是我们手写的。这样做是为了先看懂对话状态怎么流动。\n",
"\n",
"#### 为什么返回 `{'messages': [reply]}`\n",
"节点返回的是一个只包含新回复的列表。\n",
"\n",
"因为状态字段配置了 `add_messages`,所以 LangGraph 会把这个新回复追加到原来的消息列表后面。\n",
"\n",
"最终结果中会同时包含:\n",
"\n",
"- 用户原始消息\n",
"- AI 新回复\n",
"\n",
"这就是对话状态的最小工作方式。"
]
},
{
"cell_type": "markdown",
"id": "1a3b6c97",
"metadata": {},
"source": [
"## 6. 使用大模型构建对话节点\n",
"\n",
"现在把手写回复换成真正的大模型调用。\n",
"\n",
"注意:下面代码会读取项目 `.env` 中的环境变量,因此第一步要先执行 `load_dotenv()`。\n",
"\n",
"你需要在项目根目录配置:\n",
"\n",
"```env\n",
"OPENAI_BASE_URL=你的接口地址\n",
"OPENAI_API_KEY=你的 API Key\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7f7639be",
"metadata": {},
"outputs": [],
"source": [
"from dotenv import load_dotenv\n",
"\n",
"load_dotenv()"
]
},
{
"cell_type": "markdown",
"id": "347ee1fe",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这段代码只有两行,但很重要。\n",
"\n",
"#### `from dotenv import load_dotenv`\n",
"它从 `python-dotenv` 中导入 `load_dotenv` 函数。\n",
"\n",
"#### `load_dotenv()`\n",
"它会尝试读取当前项目目录下的 `.env` 文件,并把里面的变量加载到当前 Python 进程中。\n",
"\n",
"后面的 `ChatOpenAI` 会依赖 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL`。\n",
"\n",
"如果没有先执行这一步Jupyter 很可能读不到环境变量,从而出现缺少 API Key 的报错。"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "74ef82dd",
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated\n",
"from typing_extensions import TypedDict\n",
"from langchain_core.messages import HumanMessage, SystemMessage\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.graph import StateGraph, START, END\n",
"from langgraph.graph.message import add_messages\n",
"\n",
"class AgentState(TypedDict):\n",
" messages: Annotated[list, add_messages]\n",
"\n",
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0)\n",
"\n",
"def chatbot(state: AgentState):\n",
" response = llm.invoke(state['messages'])\n",
" return {'messages': [response]}\n",
"\n",
"builder = StateGraph(AgentState)\n",
"builder.add_node('chatbot', chatbot)\n",
"builder.add_edge(START, 'chatbot')\n",
"builder.add_edge('chatbot', END)\n",
"\n",
"chat_graph = builder.compile()\n",
"\n",
"result = chat_graph.invoke({\n",
" 'messages': [\n",
" SystemMessage(content='你是一个通俗易懂的 Python 课程助教。'),\n",
" HumanMessage(content='请用一句话解释什么是函数。')\n",
" ]\n",
"})\n",
"\n",
"print(result['messages'][-1].content)"
]
},
{
"cell_type": "markdown",
"id": "ed5dc0e5",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这是本节第一个真正调用大模型的对话代理。\n",
"\n",
"#### `AgentState`\n",
"状态中只有一个字段:\n",
"\n",
"```python\n",
"messages: Annotated[list, add_messages]\n",
"```\n",
"\n",
"这表示整张图最重要的数据就是聊天记录。\n",
"\n",
"#### `llm = ChatOpenAI(...)`\n",
"这里创建了一个聊天模型对象。\n",
"\n",
"- `model='qwen3.6-35b-A3b'` 表示使用的模型名称\n",
"- `temperature=0` 表示回答尽量稳定,不要太随机\n",
"\n",
"只要 `.env` 中配置了 `OPENAI_BASE_URL` 和 `OPENAI_API_KEY`,这里就可以正常使用对应接口。\n",
"\n",
"#### `chatbot(state)` 节点\n",
"这是图里的核心节点。它做的事情是:\n",
"\n",
"1. 读取当前状态中的 `messages`\n",
"2. 把完整消息列表传给大模型\n",
"3. 得到模型回复 `response`\n",
"4. 把回复追加回 `messages`\n",
"\n",
"关键代码是:\n",
"\n",
"```python\n",
"response = llm.invoke(state['messages'])\n",
"```\n",
"\n",
"这不是只传最后一句用户问题,而是把整个消息列表传给模型。\n",
"\n",
"#### 为什么返回 `[response]`\n",
"`response` 本身就是一个 AI 消息对象。\n",
"\n",
"返回:\n",
"\n",
"```python\n",
"{'messages': [response]}\n",
"```\n",
"\n",
"表示把模型刚刚生成的回复追加到历史记录中。\n",
"\n",
"#### 这张图的流程\n",
"整张图非常简单:\n",
"\n",
"```\n",
"START -> chatbot -> END\n",
"```\n",
"\n",
"虽然流程简单,但它已经具备对话代理的核心结构:用 `messages` 作为状态,用模型节点生成回复。"
]
},
{
"cell_type": "markdown",
"id": "023bdf1a",
"metadata": {},
"source": [
"## 7. 多轮对话:手动传入历史消息\n",
"\n",
"刚才的示例只运行了一轮。\n",
"\n",
"如果想让模型记住前面说过什么,最直接的方法是:第二轮调用时,把第一轮返回的 `messages` 继续传进去。"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "621a179c",
"metadata": {},
"outputs": [],
"source": [
"messages = [\n",
" SystemMessage(content='你是一个简洁的中文助手。'),\n",
" HumanMessage(content='我叫小王,正在学习 LangGraph。')\n",
"]\n",
"\n",
"first_result = chat_graph.invoke({'messages': messages})\n",
"print('第一轮回答:')\n",
"print(first_result['messages'][-1].content)\n",
"\n",
"second_messages = first_result['messages'] + [\n",
" HumanMessage(content='我叫什么?我正在学什么?')\n",
"]\n",
"\n",
"second_result = chat_graph.invoke({'messages': second_messages})\n",
"print('\\n第二轮回答')\n",
"print(second_result['messages'][-1].content)"
]
},
{
"cell_type": "markdown",
"id": "c8ce506f",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这个例子演示了最直接的多轮对话方式:手动保存并传递历史消息。\n",
"\n",
"#### 第一轮对话\n",
"第一轮消息包括:\n",
"\n",
"- 一条系统消息:规定助手要简洁\n",
"- 一条用户消息:告诉模型‘我叫小王,正在学习 LangGraph\n",
"\n",
"调用后,`first_result['messages']` 中会包含:\n",
"\n",
"- 原来的系统消息\n",
"- 原来的用户消息\n",
"- 模型第一轮回复\n",
"\n",
"#### 第二轮为什么要用 `first_result['messages']`\n",
"第二轮用户问:\n",
"\n",
"```text\n",
"我叫什么?我正在学什么?\n",
"```\n",
"\n",
"如果只把这句话传给模型,模型不知道答案。\n",
"\n",
"所以代码这样写:\n",
"\n",
"```python\n",
"second_messages = first_result['messages'] + [HumanMessage(...)]\n",
"```\n",
"\n",
"意思是:把上一轮完整聊天记录拿过来,再追加新的用户问题。\n",
"\n",
"#### 这个例子的局限\n",
"这种方式能工作,但需要我们自己手动管理 `messages`。\n",
"\n",
"如果对话轮数很多,手动传来传去会比较麻烦。\n",
"\n",
"所以 LangGraph 还提供了检查点机制,可以帮我们保存对话状态。"
]
},
{
"cell_type": "markdown",
"id": "40a885e7",
"metadata": {},
"source": [
"## 8. 使用检查点保存对话记忆\n",
"\n",
"如果希望图自动保存多轮对话状态可以使用检查点checkpoint。\n",
"\n",
"最常见的入门方式是使用 `MemorySaver`。\n",
"\n",
"它会根据 `thread_id` 保存不同会话的状态。\n",
"\n",
"你可以把 `thread_id` 理解成一个聊天窗口 ID\n",
"\n",
"- 同一个 `thread_id`:共享同一段对话历史\n",
"- 不同 `thread_id`:互相独立,互不影响"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c2880d4c",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = MemorySaver()\n",
"chat_graph_with_memory = builder.compile(checkpointer=memory)\n",
"\n",
"config = {'configurable': {'thread_id': 'student-001'}}\n",
"\n",
"first_result = chat_graph_with_memory.invoke(\n",
" {'messages': [HumanMessage(content='我叫小王,正在学习 LangGraph。')]},\n",
" config=config\n",
")\n",
"print('第一轮回答:')\n",
"print(first_result['messages'][-1].content)\n",
"\n",
"second_result = chat_graph_with_memory.invoke(\n",
" {'messages': [HumanMessage(content='我叫什么?我正在学什么?')]},\n",
" config=config\n",
")\n",
"print('\\n第二轮回答')\n",
"print(second_result['messages'][-1].content)"
]
},
{
"cell_type": "markdown",
"id": "a50620b3",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这个例子是本节最接近真实对话机器人的写法。\n",
"\n",
"#### `MemorySaver()`\n",
"`MemorySaver` 是一个内存版检查点保存器。\n",
"\n",
"它会把图运行过程中的状态保存下来。\n",
"\n",
"这里的状态主要就是 `messages`。\n",
"\n",
"#### `builder.compile(checkpointer=memory)`\n",
"普通的 `compile()` 只是把图编译成可运行对象。\n",
"\n",
"加上 `checkpointer=memory` 后,图在运行时会自动保存和读取状态。\n",
"\n",
"#### `thread_id`\n",
"配置中最关键的是:\n",
"\n",
"```python\n",
"config = {'configurable': {'thread_id': 'student-001'}}\n",
"```\n",
"\n",
"它表示当前对话属于 `student-001` 这个会话。\n",
"\n",
"只要第二次调用继续使用同一个 `thread_id`LangGraph 就会自动接上之前的消息历史。\n",
"\n",
"#### 为什么第二轮只传新消息也能记住上下文\n",
"第二轮调用时,我们只传了新的用户消息:\n",
"\n",
"```python\n",
"{'messages': [HumanMessage(content='我叫什么?我正在学什么?')]}\n",
"```\n",
"\n",
"但是因为 `thread_id` 没变,检查点里已经保存了上一轮的历史。\n",
"\n",
"所以图会把旧消息和新消息合并起来,再交给模型。\n",
"\n",
"这就是它能回答出‘你叫小王,正在学习 LangGraph的原因。\n",
"\n",
"#### 这个机制的价值\n",
"有了检查点机制,你不需要每次手动传完整历史。\n",
"\n",
"这对真实聊天机器人非常重要,因为真实系统中会有很多用户、很多会话,每个会话都需要独立保存上下文。"
]
},
{
"cell_type": "markdown",
"id": "7df1fdbb",
"metadata": {},
"source": [
"## 9. 不同 thread_id 表示不同对话\n",
"\n",
"为了理解 `thread_id`,可以再看一个例子。\n",
"\n",
"同一个图,如果使用不同的 `thread_id`,就相当于打开了两个不同的聊天窗口。"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "db8a613d",
"metadata": {},
"outputs": [],
"source": [
"config_a = {'configurable': {'thread_id': 'chat-a'}}\n",
"config_b = {'configurable': {'thread_id': 'chat-b'}}\n",
"\n",
"chat_graph_with_memory.invoke(\n",
" {'messages': [HumanMessage(content='我叫小李。')]},\n",
" config=config_a\n",
")\n",
"\n",
"chat_graph_with_memory.invoke(\n",
" {'messages': [HumanMessage(content='我叫小张。')]},\n",
" config=config_b\n",
")\n",
"\n",
"result_a = chat_graph_with_memory.invoke(\n",
" {'messages': [HumanMessage(content='我叫什么?')]},\n",
" config=config_a\n",
")\n",
"\n",
"result_b = chat_graph_with_memory.invoke(\n",
" {'messages': [HumanMessage(content='我叫什么?')]},\n",
" config=config_b\n",
")\n",
"\n",
"print('chat-a', result_a['messages'][-1].content)\n",
"print('chat-b', result_b['messages'][-1].content)"
]
},
{
"cell_type": "markdown",
"id": "296052e3",
"metadata": {},
"source": [
"### 代码解释\n",
"\n",
"这个例子用来说明:记忆不是全局混在一起的,而是按 `thread_id` 分开的。\n",
"\n",
"#### `config_a` 和 `config_b`\n",
"这两个配置分别代表两个会话:\n",
"\n",
"- `chat-a`\n",
"- `chat-b`\n",
"\n",
"它们使用同一张图、同一个模型,但是历史记录互相独立。\n",
"\n",
"#### 第一轮分别告诉不同名字\n",
"在 `chat-a` 中,用户说自己叫小李。\n",
"\n",
"在 `chat-b` 中,用户说自己叫小张。\n",
"\n",
"#### 第二轮都问‘我叫什么?’\n",
"虽然第二轮问题完全一样,但因为 `thread_id` 不同,模型看到的历史也不同。\n",
"\n",
"所以理想情况下:\n",
"\n",
"- `chat-a` 会回答小李\n",
"- `chat-b` 会回答小张\n",
"\n",
"这就是多会话对话机器人的基础。\n",
"\n",
"真实产品中,每个用户、每个聊天窗口、每个任务线程,都可以用不同的 `thread_id` 管理。"
]
},
{
"cell_type": "markdown",
"id": "9353884b",
"metadata": {},
"source": [
"## 10. 对话代理的整体结构\n",
"\n",
"到这里,我们可以把对话代理总结成下面这张流程图:\n",
"\n",
"```\n",
"用户输入\n",
" ↓\n",
"追加到 messages\n",
" ↓\n",
"chatbot 节点读取 messages\n",
" ↓\n",
"大模型基于完整上下文生成回复\n",
" ↓\n",
"AI 回复追加到 messages\n",
" ↓\n",
"检查点保存当前状态\n",
"```\n",
"\n",
"其中最重要的两件事是:\n",
"\n",
"- `messages` 负责保存对话内容\n",
"- `thread_id` 负责区分不同对话线程\n",
"\n",
"理解这两点,就能看懂大多数 LangGraph 对话代理的基础写法。"
]
},
{
"cell_type": "markdown",
"id": "b720b8d0",
"metadata": {},
"source": [
"## 11. 常见问题\n",
"\n",
"### 11.1 为什么模型还是不记得前面说过的话\n",
"通常有几个原因:\n",
"\n",
"- 没有把旧 `messages` 传入下一轮\n",
"- 没有使用 `add_messages`\n",
"- 使用检查点时,第二轮换了不同的 `thread_id`\n",
"- 每次都重新编译图并重新创建内存保存器\n",
"\n",
"### 11.2 `messages` 会不会越来越长\n",
"会。\n",
"\n",
"真实项目中通常需要做历史裁剪、摘要记忆或长期记忆存储。\n",
"\n",
"本节先学习基础机制,后面再考虑复杂记忆管理。\n",
"\n",
"### 11.3 `SystemMessage` 每轮都要传吗\n",
"如果你手动维护 `messages`,通常第一轮放进去后,后续继续传完整历史即可。\n",
"\n",
"如果使用检查点,只要第一轮已经进入同一个 `thread_id`,后续通常不需要重复传同一条系统消息。\n",
"\n",
"但在真实项目中,也有人会在每次调用前固定补充系统提示,保证行为稳定。"
]
},
{
"cell_type": "markdown",
"id": "2bdf3988",
"metadata": {},
"source": [
"## 12. 本节小结\n",
"\n",
"本节最重要的内容有五点:\n",
"\n",
"1. **对话代理和一次性问答最大的区别是要保存历史上下文**\n",
"2. **`messages` 是对话状态的核心,负责保存多轮聊天记录**\n",
"3. **`add_messages` 可以让新消息追加到旧消息后面,而不是覆盖旧消息**\n",
"4. **对话节点通常会把完整 `messages` 传给聊天模型,再把模型回复追加回来**\n",
"5. **检查点和 `thread_id` 可以帮助我们自动保存不同会话的对话记忆**\n",
"\n",
"掌握了本节内容,你就已经具备了构建基础聊天机器人的能力。后面再加入工具调用、条件边和长期记忆,就可以逐步扩展成更完整的智能体。"
]
},
{
"cell_type": "markdown",
"id": "9ea83fb3",
"metadata": {},
"source": [
"## 13. 本节练习\n",
"\n",
"1. 修改 `SystemMessage`,让对话代理变成一个英语学习助手\n",
"2. 修改第一轮用户输入,让代理记住你的名字和学习目标\n",
"3. 用同一个 `thread_id` 连续提问三轮,观察它是否能记住前文\n",
"4. 换一个新的 `thread_id`,观察新会话是否还记得旧会话内容\n",
"5. 思考:如果 `messages` 很长,可能会带来哪些问题?应该如何处理?"
]
}
],
"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
}