Files
ai-agent-dev/26_项目实战三.ipynb
2026-07-08 10:09:42 +08:00

1040 lines
30 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": [
"# 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
}