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