1040 lines
30 KiB
Plaintext
1040 lines
30 KiB
Plaintext
{
|
||
"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
|
||
}
|