{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 26 项目实战三:带交互界面的智能体项目\n", "\n", "## 学习目标\n", "1. 完成一个具备完整交互界面的智能体项目\n", "2. 掌握将 LangGraph 智能体与简单命令行界面集成\n", "3. 能够进行项目测试、迭代和成果展示\n", "4. 理解项目从需求分析到最终演示的完整开发流程\n", "5. 学会把前面学过的状态、节点、边、条件分支组合成一个可用项目\n", "\n", "本节课会完成一个小型项目:**学习任务助手**。\n", "\n", "它可以根据用户输入,自动识别用户想做什么,并给出不同类型的回复。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. 项目背景\n", "\n", "前面的项目实战中,我们已经学习了知识库问答、多工具智能体等内容。\n", "\n", "但是一个项目如果只能在代码单元里调用函数,体验还不够完整。\n", "\n", "真实项目通常还需要一个交互入口,例如:\n", "\n", "- 命令行界面\n", "- Web 页面\n", "- 桌面应用\n", "- 聊天窗口\n", "- 企业微信、飞书、钉钉机器人\n", "\n", "本节我们先从最简单、最容易理解的命令行界面开始。\n", "\n", "项目目标是做一个学习任务助手,用户可以输入自然语言,例如:\n", "\n", "```text\n", "帮我制定 LangGraph 学习计划\n", "```\n", "\n", "或者:\n", "\n", "```text\n", "解释一下条件边是什么\n", "```\n", "\n", "系统会根据输入内容判断用户意图,并给出对应回复。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. 项目需求分析\n", "\n", "做项目之前,先不要急着写代码。\n", "\n", "我们先分析这个项目要具备哪些能力。\n", "\n", "| 需求 | 说明 |\n", "| --- | --- |\n", "| 接收用户输入 | 用户可以输入一句自然语言请求 |\n", "| 判断用户意图 | 判断用户是想学习计划、概念解释,还是普通聊天 |\n", "| 生成对应回复 | 根据不同意图生成不同内容 |\n", "| 保存执行状态 | 记录用户输入、意图、回复和历史记录 |\n", "| 提供交互界面 | 让用户可以连续输入问题 |\n", "| 支持测试和展示 | 能用示例输入验证系统是否正常 |\n", "\n", "为了让项目容易运行,本节不依赖外部大模型 API,而是用规则模拟智能体的判断和回复。\n", "\n", "这样做的好处是:\n", "\n", "- 不需要 API Key\n", "- 不受网络影响\n", "- 更容易看清楚 LangGraph 的工作流结构\n", "- 适合课堂演示和初学者练习\n", "\n", "等理解项目结构后,再把规则回复替换成真实大模型也很容易。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. 项目整体流程\n", "\n", "学习任务助手的整体流程如下:\n", "\n", "```text\n", "用户输入\n", " ↓\n", "分析意图\n", " ↓\n", "根据意图选择处理节点\n", " ↓\n", "生成回复\n", " ↓\n", "记录历史\n", " ↓\n", "返回结果\n", "```\n", "\n", "如果用 LangGraph 表达,可以拆成几个节点:\n", "\n", "| 节点 | 作用 |\n", "| --- | --- |\n", "| `analyze_intent` | 分析用户意图 |\n", "| `make_plan` | 生成学习计划 |\n", "| `explain_concept` | 解释概念 |\n", "| `chat` | 普通聊天回复 |\n", "| `save_history` | 保存本轮对话历史 |\n", "\n", "其中 `analyze_intent` 后面会接条件边,根据不同意图走到不同节点。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. 第一步:定义项目状态\n", "\n", "LangGraph 项目的第一步通常是定义状态。\n", "\n", "状态可以理解为整个流程共享的数据记录。\n", "\n", "本项目需要记录:\n", "\n", "- 用户输入了什么\n", "- 系统判断出的意图是什么\n", "- 系统生成的回复是什么\n", "- 历史对话有哪些\n", "\n", "下面先定义状态结构。" ] }, { "cell_type": "code", "execution_count": null, "id": "11927b6e", "metadata": {}, "outputs": [], "source": [ "from typing_extensions import TypedDict\n", "\n", "class AssistantState(TypedDict):\n", " user_input: str\n", " intent: str\n", " response: str\n", " history: list[str]\n", "\n", "initial_state: AssistantState = {\n", " 'user_input': '帮我制定 LangGraph 学习计划',\n", " 'intent': '',\n", " 'response': '',\n", " 'history': []\n", "}\n", "\n", "print(initial_state)" ] }, { "cell_type": "markdown", "id": "b1272a7f", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这段代码完成了项目状态设计。\n", "\n", "#### `AssistantState`\n", "这是一个 `TypedDict`,用来规定状态中有哪些字段。\n", "\n", "字段含义如下:\n", "\n", "- `user_input`:用户当前输入的内容\n", "- `intent`:系统分析出的用户意图\n", "- `response`:系统生成的回复\n", "- `history`:保存历史交互记录\n", "\n", "#### 为什么需要 `intent`\n", "因为项目不是简单地收到输入就回复,而是要先判断用户想做什么。\n", "\n", "例如:\n", "\n", "- 用户说‘制定学习计划’,意图就是 `plan`\n", "- 用户说‘解释条件边’,意图就是 `explain`\n", "- 用户说‘你好’,意图就是 `chat`\n", "\n", "后面条件边会根据 `intent` 决定走哪个处理节点。\n", "\n", "#### 为什么需要 `history`\n", "`history` 用来保存交互记录。\n", "\n", "虽然本项目只是简单命令行助手,但保留历史可以帮助我们展示项目结果,也为后续扩展多轮对话打基础。" ] }, { "cell_type": "markdown", "id": "67b2006f", "metadata": {}, "source": [ "## 5. 第二步:编写意图识别节点\n", "\n", "意图识别节点负责判断用户想做什么。\n", "\n", "真实项目中,可以用大模型判断意图。\n", "\n", "本节为了方便运行,先用关键词规则实现。" ] }, { "cell_type": "code", "execution_count": null, "id": "7dc3e04c", "metadata": {}, "outputs": [], "source": [ "def analyze_intent(state: AssistantState):\n", " text = state['user_input']\n", "\n", " if '计划' in text or '安排' in text or '学习路线' in text:\n", " intent = 'plan'\n", " elif '解释' in text or '什么是' in text or '概念' in text:\n", " intent = 'explain'\n", " else:\n", " intent = 'chat'\n", "\n", " return {'intent': intent}\n", "\n", "test_state = {\n", " 'user_input': '请解释一下 LangGraph 的条件边',\n", " 'intent': '',\n", " 'response': '',\n", " 'history': []\n", "}\n", "\n", "print(analyze_intent(test_state))" ] }, { "cell_type": "markdown", "id": "91aa9945", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个节点是项目中的第一个处理节点。\n", "\n", "#### `text = state['user_input']`\n", "从状态中取出用户输入。\n", "\n", "后面的判断都基于这句话进行。\n", "\n", "#### 关键词规则\n", "代码中用了三类规则:\n", "\n", "1. 如果包含 `计划`、`安排`、`学习路线`,判断为学习计划类需求\n", "2. 如果包含 `解释`、`什么是`、`概念`,判断为概念解释类需求\n", "3. 其他情况都归为普通聊天\n", "\n", "#### 返回 `{'intent': intent}`\n", "节点只返回要更新的字段。\n", "\n", "这里不需要返回完整状态,因为 LangGraph 会把这个更新合并回原状态。\n", "\n", "#### 为什么这里不用大模型\n", "规则判断虽然简单,但非常适合教学。\n", "\n", "它让我们先专注理解项目结构:\n", "\n", "```text\n", "输入 -> 判断意图 -> 条件分支\n", "```\n", "\n", "后续如果想升级,只需要把这个函数替换成大模型判断即可。" ] }, { "cell_type": "markdown", "id": "f1684879", "metadata": {}, "source": [ "## 6. 第三步:编写不同意图的处理节点\n", "\n", "识别出意图后,需要根据不同意图生成不同回复。\n", "\n", "本项目设计三个处理节点:\n", "\n", "- `make_plan`:生成学习计划\n", "- `explain_concept`:解释概念\n", "- `chat`:普通聊天\n" ] }, { "cell_type": "code", "execution_count": null, "id": "ea3867af", "metadata": {}, "outputs": [], "source": [ "def make_plan(state: AssistantState):\n", " response = '''这是一个建议的学习计划:\n", "1. 先复习 LangGraph 的 State、Node、Edge\n", "2. 再重点理解条件边和循环结构\n", "3. 然后练习对话代理和工具调用\n", "4. 最后完成一个小项目进行综合应用\n", "'''\n", " return {'response': response}\n", "\n", "def explain_concept(state: AssistantState):\n", " response = '''可以这样理解:\n", "LangGraph 是一个用“图”来组织智能体流程的框架。\n", "节点负责执行具体任务,边负责决定执行顺序,状态负责在节点之间传递数据。\n", "'''\n", " return {'response': response}\n", "\n", "def chat(state: AssistantState):\n", " response = '我可以帮你制定学习计划、解释 AI 智能体概念,或者协助你梳理项目思路。'\n", " return {'response': response}\n", "\n", "print(make_plan(initial_state)['response'])" ] }, { "cell_type": "markdown", "id": "048b0e96", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这段代码定义了三个处理节点。\n", "\n", "#### `make_plan`\n", "当用户想要学习计划时,会进入这个节点。\n", "\n", "它返回一个分步骤学习建议。\n", "\n", "#### `explain_concept`\n", "当用户想要解释概念时,会进入这个节点。\n", "\n", "它用通俗语言解释 LangGraph 的核心思想。\n", "\n", "#### `chat`\n", "当系统没有识别到明确需求时,会进入普通聊天节点。\n", "\n", "这个节点告诉用户当前助手能做什么。\n", "\n", "#### 三个节点的共同点\n", "它们都返回:\n", "\n", "```python\n", "{'response': response}\n", "```\n", "\n", "也就是说,它们都只负责生成回复,不负责保存历史,也不负责判断下一步。\n", "\n", "这体现了项目开发中的一个好习惯:**每个节点只负责一件事。**" ] }, { "cell_type": "markdown", "id": "a48bdefd", "metadata": {}, "source": [ "## 7. 第四步:保存历史记录\n", "\n", "每轮交互结束前,我们希望把用户输入和系统回复保存下来。\n", "\n", "这样后面展示项目成果时,就能看到完整历史。" ] }, { "cell_type": "code", "execution_count": null, "id": "93d75eec", "metadata": {}, "outputs": [], "source": [ "def save_history(state: AssistantState):\n", " new_record = f'用户:{state[\"user_input\"]}\\n助手:{state[\"response\"]}'\n", " history = state['history'] + [new_record]\n", " return {'history': history}\n", "\n", "sample_state = {\n", " 'user_input': '你好',\n", " 'intent': 'chat',\n", " 'response': '你好,我是学习任务助手。',\n", " 'history': []\n", "}\n", "\n", "print(save_history(sample_state))" ] }, { "cell_type": "markdown", "id": "11773b90", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个节点负责保存历史记录。\n", "\n", "#### `new_record`\n", "这一行把当前用户输入和助手回复拼成一条记录。\n", "\n", "格式类似:\n", "\n", "```text\n", "用户:你好\n", "助手:你好,我是学习任务助手。\n", "```\n", "\n", "#### `history = state['history'] + [new_record]`\n", "这行代码不是直接修改原列表,而是创建一个新列表。\n", "\n", "新列表 = 原来的历史记录 + 本轮新记录。\n", "\n", "这样写更清晰,也更符合状态更新的思路。\n", "\n", "#### 返回 `{'history': history}`\n", "节点只更新 `history` 字段。\n", "\n", "它不需要关心用户意图,也不需要重新生成回复。\n", "\n", "这同样体现了节点职责分离。" ] }, { "cell_type": "markdown", "id": "be5775e0", "metadata": {}, "source": [ "## 8. 第五步:构建 LangGraph 工作流\n", "\n", "现在我们已经有了所有节点,接下来把它们组装成一张图。\n", "\n", "这张图的关键是:`analyze_intent` 后面要使用条件边。\n", "\n", "不同意图会进入不同处理节点。" ] }, { "cell_type": "code", "execution_count": null, "id": "ce3f69f9", "metadata": {}, "outputs": [], "source": [ "from langgraph.graph import StateGraph, START, END\n", "\n", "def route_by_intent(state: AssistantState):\n", " if state['intent'] == 'plan':\n", " return 'plan'\n", " if state['intent'] == 'explain':\n", " return 'explain'\n", " return 'chat'\n", "\n", "builder = StateGraph(AssistantState)\n", "\n", "builder.add_node('analyze_intent', analyze_intent)\n", "builder.add_node('make_plan', make_plan)\n", "builder.add_node('explain_concept', explain_concept)\n", "builder.add_node('chat', chat)\n", "builder.add_node('save_history', save_history)\n", "\n", "builder.add_edge(START, 'analyze_intent')\n", "builder.add_conditional_edges(\n", " 'analyze_intent',\n", " route_by_intent,\n", " {\n", " 'plan': 'make_plan',\n", " 'explain': 'explain_concept',\n", " 'chat': 'chat'\n", " }\n", ")\n", "builder.add_edge('make_plan', 'save_history')\n", "builder.add_edge('explain_concept', 'save_history')\n", "builder.add_edge('chat', 'save_history')\n", "builder.add_edge('save_history', END)\n", "\n", "assistant_graph = builder.compile()\n", "\n", "print('图构建完成')" ] }, { "cell_type": "markdown", "id": "5059750c", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这一段是项目的核心,把前面所有函数组装成完整工作流。\n", "\n", "#### `route_by_intent`\n", "这是路由函数,负责告诉条件边下一步去哪。\n", "\n", "它根据 `state['intent']` 返回不同标记:\n", "\n", "- `plan`\n", "- `explain`\n", "- `chat`\n", "\n", "#### `builder.add_node(...)`\n", "这些代码把节点加入图中。\n", "\n", "节点本身只是 Python 函数,加入图之后才成为流程的一部分。\n", "\n", "#### 条件边\n", "核心代码是:\n", "\n", "```python\n", "builder.add_conditional_edges(...)\n", "```\n", "\n", "它表示:`analyze_intent` 执行完以后,不是固定走某一个节点,而是调用 `route_by_intent` 判断下一步。\n", "\n", "映射关系是:\n", "\n", "- `plan` -> `make_plan`\n", "- `explain` -> `explain_concept`\n", "- `chat` -> `chat`\n", "\n", "#### 三个处理节点为什么都连到 `save_history`\n", "不管用户意图是什么,最终都需要保存历史。\n", "\n", "所以三个分支最后都会汇合到 `save_history`。\n", "\n", "这就是图结构中的‘分支后汇合’。\n", "\n", "#### 最终流程\n", "完整执行路径可能是:\n", "\n", "```text\n", "START -> analyze_intent -> make_plan -> save_history -> END\n", "```\n", "\n", "也可能是:\n", "\n", "```text\n", "START -> analyze_intent -> explain_concept -> save_history -> END\n", "```\n", "\n", "具体走哪条路,由用户输入决定。" ] }, { "cell_type": "markdown", "id": "dbe8d633", "metadata": {}, "source": [ "## 9. 第六步:测试单轮运行\n", "\n", "在做交互界面之前,先测试图本身是否能正常运行。\n", "\n", "这是项目开发中的好习惯:先测试核心逻辑,再做界面。" ] }, { "cell_type": "code", "execution_count": null, "id": "179f2115", "metadata": {}, "outputs": [], "source": [ "state = {\n", " 'user_input': '帮我安排一个 LangGraph 学习计划',\n", " 'intent': '',\n", " 'response': '',\n", " 'history': []\n", "}\n", "\n", "result = assistant_graph.invoke(state)\n", "\n", "print('识别意图:', result['intent'])\n", "print('助手回复:')\n", "print(result['response'])\n", "print('历史记录:')\n", "print(result['history'][0])" ] }, { "cell_type": "markdown", "id": "ad1a385b", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这段代码测试了一次完整运行。\n", "\n", "#### 初始状态\n", "初始状态中,只有 `user_input` 是用户给出的。\n", "\n", "其他字段先设置为空:\n", "\n", "- `intent=''`\n", "- `response=''`\n", "- `history=[]`\n", "\n", "这些字段会在图运行过程中被节点逐步填充。\n", "\n", "#### `assistant_graph.invoke(state)`\n", "这行代码启动整张图。\n", "\n", "执行过程是:\n", "\n", "1. `analyze_intent` 判断用户想要学习计划\n", "2. 条件边把流程送到 `make_plan`\n", "3. `make_plan` 生成学习计划\n", "4. `save_history` 保存历史记录\n", "5. 流程结束\n", "\n", "#### 打印结果\n", "最后打印三个重要信息:\n", "\n", "- 系统识别出的意图\n", "- 助手回复\n", "- 历史记录\n", "\n", "如果这一步结果正常,说明核心工作流没有问题。" ] }, { "cell_type": "markdown", "id": "5f5322a5", "metadata": {}, "source": [ "## 10. 第七步:批量测试多个输入\n", "\n", "一个项目不能只测试一个例子。\n", "\n", "我们应该准备多种输入,看看不同分支是否都能正常工作。" ] }, { "cell_type": "code", "execution_count": null, "id": "d45a2a15", "metadata": {}, "outputs": [], "source": [ "test_inputs = [\n", " '帮我制定 AI 智能体学习计划',\n", " '解释一下什么是条件边',\n", " '你好,你能做什么?'\n", "]\n", "\n", "for user_input in test_inputs:\n", " state = {\n", " 'user_input': user_input,\n", " 'intent': '',\n", " 'response': '',\n", " 'history': []\n", " }\n", " result = assistant_graph.invoke(state)\n", " print('=' * 40)\n", " print('用户输入:', user_input)\n", " print('识别意图:', result['intent'])\n", " print('助手回复:')\n", " print(result['response'])" ] }, { "cell_type": "markdown", "id": "e9ad272a", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这段代码是批量测试。\n", "\n", "#### `test_inputs`\n", "这里准备了三类输入:\n", "\n", "1. 学习计划类\n", "2. 概念解释类\n", "3. 普通聊天类\n", "\n", "这正好覆盖了项目中的三个分支。\n", "\n", "#### 每次循环都创建新状态\n", "每个输入都使用一个新的初始状态。\n", "\n", "这样可以单独观察每个输入的结果,不受上一轮影响。\n", "\n", "#### 为什么要做批量测试\n", "因为条件分支项目最容易出现的问题是:\n", "\n", "- 某个分支没有走到\n", "- 路由返回值写错\n", "- 某个节点没有正确返回结果\n", "\n", "批量测试可以帮助我们快速发现这些问题。" ] }, { "cell_type": "markdown", "id": "6916f5ae", "metadata": {}, "source": [ "## 11. 第八步:封装成一个函数\n", "\n", "为了后面接入交互界面,我们把调用图的逻辑封装成函数。\n", "\n", "这样界面部分只需要调用这个函数即可。" ] }, { "cell_type": "code", "execution_count": null, "id": "cd9e649c", "metadata": {}, "outputs": [], "source": [ "def run_assistant(user_input, history=None):\n", " if history is None:\n", " history = []\n", "\n", " state = {\n", " 'user_input': user_input,\n", " 'intent': '',\n", " 'response': '',\n", " 'history': history\n", " }\n", "\n", " result = assistant_graph.invoke(state)\n", " return result\n", "\n", "result = run_assistant('什么是 LangGraph?')\n", "print(result['response'])" ] }, { "cell_type": "markdown", "id": "74ad36ce", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这个函数是核心逻辑和交互界面之间的桥梁。\n", "\n", "#### `run_assistant(user_input, history=None)`\n", "它接收两个参数:\n", "\n", "- `user_input`:用户输入\n", "- `history`:历史记录,默认可以为空\n", "\n", "#### 为什么默认值用 `None`\n", "函数参数不建议直接写 `history=[]`。\n", "\n", "因为列表是可变对象,作为默认参数可能带来意外共享问题。\n", "\n", "所以这里写成:\n", "\n", "```python\n", "if history is None:\n", " history = []\n", "```\n", "\n", "这是 Python 中比较稳妥的写法。\n", "\n", "#### 函数内部做了什么\n", "函数内部重新构造状态,然后调用:\n", "\n", "```python\n", "assistant_graph.invoke(state)\n", "```\n", "\n", "最后把结果返回。\n", "\n", "有了这个函数,后面无论是命令行界面还是 Web 界面,都可以复用同一套智能体逻辑。" ] }, { "cell_type": "markdown", "id": "471f167c", "metadata": {}, "source": [ "## 12. 第九步:构建命令行交互界面\n", "\n", "现在我们给项目加一个简单的命令行界面。\n", "\n", "用户可以连续输入内容,输入 `退出` 时结束。\n", "\n", "注意:在 Jupyter Notebook 中,`input()` 需要手动输入。如果你只是阅读课件,可以先不运行这一格。" ] }, { "cell_type": "code", "execution_count": null, "id": "5701141f", "metadata": {}, "outputs": [], "source": [ "def start_cli():\n", " print('学习任务助手已启动')\n", " print('你可以输入问题,例如:帮我制定学习计划 / 解释条件边 / 你好')\n", " print('输入“退出”结束对话')\n", "\n", " history = []\n", "\n", " while True:\n", " user_input = input('你:')\n", "\n", " if user_input.strip() == '退出':\n", " print('助手:再见,祝你学习顺利!')\n", " break\n", "\n", " result = run_assistant(user_input, history)\n", " history = result['history']\n", "\n", " print('助手:')\n", " print(result['response'])\n", "\n", "# 在 notebook 中如需体验交互,可以取消下一行注释\n", "# start_cli()" ] }, { "cell_type": "markdown", "id": "f1231bf7", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这段代码实现了一个简单但完整的命令行交互界面。\n", "\n", "#### `start_cli()`\n", "这是启动命令行界面的函数。\n", "\n", "运行后,用户可以不断输入问题。\n", "\n", "#### `history = []`\n", "这里创建一个历史记录列表。\n", "\n", "每一轮调用助手后,都会更新历史。\n", "\n", "#### `while True`\n", "这是一个循环,让程序可以一直等待用户输入。\n", "\n", "如果没有这个循环,程序只能回答一次。\n", "\n", "#### `input('你:')`\n", "这是命令行输入入口。\n", "\n", "用户在这里输入自然语言请求。\n", "\n", "#### 退出逻辑\n", "如果用户输入:\n", "\n", "```text\n", "退出\n", "```\n", "\n", "程序会打印告别语,并用 `break` 跳出循环。\n", "\n", "#### 调用智能体\n", "核心代码是:\n", "\n", "```python\n", "result = run_assistant(user_input, history)\n", "```\n", "\n", "这说明界面层并不直接处理智能体逻辑。\n", "\n", "它只是把用户输入交给 `run_assistant`,再把结果展示出来。\n", "\n", "这种分层方式很重要:\n", "\n", "- LangGraph 负责智能体逻辑\n", "- `run_assistant` 负责封装调用\n", "- `start_cli` 负责用户交互\n", "\n", "这样项目结构更清晰,也更容易维护。" ] }, { "cell_type": "markdown", "id": "1c2fde68", "metadata": {}, "source": [ "## 13. 第十步:模拟一次完整展示\n", "\n", "课堂演示或项目展示时,不一定要真的手动输入。\n", "\n", "我们也可以用一组预设输入模拟完整对话。" ] }, { "cell_type": "code", "execution_count": null, "id": "8e478b1e", "metadata": {}, "outputs": [], "source": [ "demo_inputs = [\n", " '你好,你能做什么?',\n", " '帮我制定 LangGraph 学习计划',\n", " '解释一下什么是节点和边'\n", "]\n", "\n", "history = []\n", "\n", "for user_input in demo_inputs:\n", " result = run_assistant(user_input, history)\n", " history = result['history']\n", "\n", " print('用户:', user_input)\n", " print('助手:')\n", " print(result['response'])\n", " print()\n", "\n", "print('最终历史记录数量:', len(history))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 代码解释\n", "\n", "这段代码模拟了一次完整项目演示。\n", "\n", "#### `demo_inputs`\n", "这里准备了三轮输入:\n", "\n", "1. 普通聊天\n", "2. 学习计划\n", "3. 概念解释\n", "\n", "这样可以展示项目的三个主要能力。\n", "\n", "#### 共用一个 `history`\n", "每一轮调用后都会更新历史:\n", "\n", "```python\n", "history = result['history']\n", "```\n", "\n", "这样三轮对话会保存在同一个历史列表中。\n", "\n", "#### 为什么适合成果展示\n", "这种方式不用手动输入,运行一次就能展示完整效果。\n", "\n", "在课堂演示、录屏展示、项目答辩中都很方便。\n", "\n", "#### `len(history)`\n", "最后打印历史记录数量。\n", "\n", "如果进行了三轮对话,历史记录数量应该是 3。\n", "\n", "这说明系统确实把每轮交互保存了下来。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 14. 项目可以如何继续迭代\n", "\n", "当前项目是一个最小可用版本。\n", "\n", "如果继续升级,可以从下面几个方向入手。\n", "\n", "| 迭代方向 | 说明 |\n", "| --- | --- |\n", "| 接入真实大模型 | 用 ChatOpenAI 替换规则回复 |\n", "| 增加更多意图 | 支持总结、翻译、代码解释等能力 |\n", "| 加入工具调用 | 让助手可以查询资料、计算、读取文件 |\n", "| 使用检查点 | 保存多轮对话状态 |\n", "| Web 界面 | 用 Gradio、Streamlit 或 FastAPI 做页面 |\n", "| 持久化历史 | 把历史记录保存到文件或数据库 |\n", "\n", "项目开发通常不是一次写完,而是不断从 MVP 迭代。\n", "\n", "本节完成的是第一版:能运行、能交互、能展示。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 15. 本节小结\n", "\n", "本节完成了一个完整的交互式智能体小项目。\n", "\n", "你需要重点掌握以下几点:\n", "\n", "1. **项目开发要先做需求分析,再写代码**\n", "2. **状态设计决定了智能体流程中能传递哪些信息**\n", "3. **节点负责具体任务,条件边负责根据意图选择路径**\n", "4. **核心逻辑要先测试,再接入交互界面**\n", "5. **命令行界面虽然简单,但已经能体现完整项目闭环**\n", "\n", "到这里,我们已经把 LangGraph 的基础能力组合成了一个可演示、可测试、可继续迭代的项目。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 16. 本节练习\n", "\n", "1. 给 `analyze_intent` 增加一个 `summary` 意图,用来处理‘总结’类请求\n", "2. 增加一个 `summarize` 节点,返回一段固定的总结回复\n", "3. 修改命令行界面,让用户输入空内容时提示‘请输入有效问题’\n", "4. 把历史记录打印得更美观,例如加上轮次编号\n", "5. 思考:如果把这个命令行项目改成 Web 项目,需要保留哪些核心函数?" ] } ], "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 }