07-08-周三_10-09-42
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# 忽略虚拟环境
|
||||
.venv/
|
||||
|
||||
# 忽略所有文件
|
||||
*
|
||||
|
||||
# 但保留 Jupyter Notebook 课件
|
||||
!*.ipynb
|
||||
|
||||
# 保留 README
|
||||
!README.md
|
||||
|
||||
# 保留本文件本身
|
||||
!.gitignore
|
||||
213
01_开发环境搭建.ipynb
Normal file
213
01_开发环境搭建.ipynb
Normal file
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 01 开发环境搭建\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 了解课程整体安排与学习目标\n",
|
||||
"2. 明确 LangChain 和 LangGraph 在 AI 智能体开发中的定位\n",
|
||||
"3. 熟悉本课程使用的技术栈和工具链(VS Code + Python 3.12 + 虚拟环境)\n",
|
||||
"4. 学会使用 venv 创建虚拟环境并使用清华镜像源加速 pip 安装"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 课程概述与技术栈\n",
|
||||
"\n",
|
||||
"本课程面向具备 Python 基础语法的大学生,系统讲解 **LangChain** 和 **LangGraph** 两大框架,帮助你从零构建 AI 智能体应用。\n",
|
||||
"\n",
|
||||
"### 核心技术栈\n",
|
||||
"- **开发工具**:VS Code(Visual Studio Code)\n",
|
||||
"- **编程语言**:Python 3.12.11\n",
|
||||
"- **虚拟环境**:venv(Python 内置)\n",
|
||||
"- **主要框架**:LangChain、LangGraph\n",
|
||||
"- **镜像源**:清华大学 TUNA 镜像(加速 pip 安装)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. VS Code 安装与配置\n",
|
||||
"\n",
|
||||
"### 步骤 1:下载并安装 VS Code\n",
|
||||
"1. 访问官网:https://code.visualstudio.com/\n",
|
||||
"2. 下载 Windows 版本安装包\n",
|
||||
"3. 运行安装程序,建议勾选:\n",
|
||||
" - **添加到 PATH**(添加到系统环境变量)\n",
|
||||
" - **右键菜单集成**(在资源管理器中打开)\n",
|
||||
"\n",
|
||||
"### 步骤 2:安装 Python 扩展\n",
|
||||
"打开 VS Code,按 `Ctrl+Shift+X` 进入扩展市场,搜索并安装:\n",
|
||||
"- **Python**(Microsoft 官方扩展)\n",
|
||||
"- **Jupyter**(用于运行 .ipynb 文件)\n",
|
||||
"\n",
|
||||
"### 步骤 3:选择 Python 解释器\n",
|
||||
"按 `Ctrl+Shift+P` 打开命令面板,输入 `Python: Select Interpreter`,选择后续步骤中创建的虚拟环境。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Python 3.12.11 环境准备\n",
|
||||
"\n",
|
||||
"本课程统一使用 **Python 3.12.11**,确保版本一致可避免兼容性问题。\n",
|
||||
"\n",
|
||||
"### 检查当前 Python 版本\n",
|
||||
"在 VS Code 终端(`Ctrl+ ``)或 PowerShell 中执行:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import sys\n",
|
||||
"print(f\"Python 版本:{sys.version}\")\n",
|
||||
"print(f\"Python 路径:{sys.executable}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"如果你的系统中没有 Python 3.12.11,请前往 [Python 官网](https://www.python.org/downloads/release/python-31211/) 下载安装,或使用 `pyenv-win` 管理多版本。\n",
|
||||
"\n",
|
||||
"> ⚠️ **注意**:安装时请务必勾选 **Add Python to PATH**。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 创建虚拟环境(venv)\n",
|
||||
"\n",
|
||||
"使用虚拟环境可以隔离项目依赖,避免不同项目之间的包冲突。Python 3.12 内置了 `venv` 模块,无需额外安装。\n",
|
||||
"\n",
|
||||
"### 在项目目录下创建虚拟环境\n",
|
||||
"\n",
|
||||
"在 VS Code 终端(确保终端为 PowerShell 或 CMD)中执行:\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"# 创建名为 .venv 的虚拟环境\n",
|
||||
"python -m venv .venv\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"执行后,你会在当前目录下看到 `.venv` 文件夹。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 激活虚拟环境\n",
|
||||
"\n",
|
||||
"### Windows 系统(PowerShell)\n",
|
||||
"```powershell\n",
|
||||
".venv\\Scripts\\Activate.ps1\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"### Windows 系统(CMD)\n",
|
||||
"```cmd\n",
|
||||
".venv\\Scripts\\activate.bat\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"> ⚠️ **PowerShell 执行策略提示**:如果提示无法执行脚本,请以管理员身份运行 PowerShell 并执行:\n",
|
||||
"> ```powershell\n",
|
||||
"> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser\n",
|
||||
"> ```\n",
|
||||
"\n",
|
||||
"激活成功后,终端提示符前会显示 `(.venv)`。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 使用清华镜像源安装依赖\n",
|
||||
"\n",
|
||||
"为了加快 pip 下载速度,我们使用清华大学 TUNA 镜像源。\n",
|
||||
"\n",
|
||||
"### 临时使用清华源(推荐)\n",
|
||||
"在 VS Code 终端中执行以下命令(确保虚拟环境已激活):\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"pip install langchain langgraph langchain-openai -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"### 配置全局默认镜像源(可选)\n",
|
||||
"如果你想让所有 pip 安装默认使用清华源:\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 验证环境安装\n",
|
||||
"\n",
|
||||
"运行以下代码,确认核心库已正确安装:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import sys\n",
|
||||
"from importlib.metadata import version\n",
|
||||
"\n",
|
||||
"print(f\"Python 版本:{sys.version}\")\n",
|
||||
"print(f\"LangChain 版本:{version('langchain')}\")\n",
|
||||
"print(f\"LangGraph 版本:{version('langgraph')}\")\n",
|
||||
"print(\"✅ 环境安装成功!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. VS Code 关联虚拟环境\n",
|
||||
"\n",
|
||||
"1. 按 `Ctrl+Shift+P` 打开命令面板\n",
|
||||
"2. 输入 `Python: Select Interpreter`\n",
|
||||
"3. 选择 `./.venv/Scripts/python.exe`\n",
|
||||
"4. 重新启动 Jupyter 内核(点击右上角内核选择器,选择 `.venv` 环境)\n",
|
||||
"\n",
|
||||
"现在你可以在 VS Code 中直接运行本课程的所有 `.ipynb` 文件了!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
293
02_Python环境配置.ipynb
Normal file
293
02_Python环境配置.ipynb
Normal file
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 02 Python 环境配置\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解 Python 虚拟环境的作用,掌握 venv 的进阶用法\n",
|
||||
"2. 学会使用 `requirements.txt` 管理项目依赖\n",
|
||||
"3. 掌握 Jupyter Notebook / JupyterLab 在 VS Code 中的配置\n",
|
||||
"4. 学会为 Jupyter 注册虚拟环境内核并排查常见环境问题"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要虚拟环境\n",
|
||||
"\n",
|
||||
"在 Python 开发中,不同项目可能依赖不同版本的库。如果不加隔离,全局安装的库会相互冲突。\n",
|
||||
"\n",
|
||||
"### 虚拟环境的好处\n",
|
||||
"- **依赖隔离**:每个项目有独立的包集合\n",
|
||||
"- **版本可控**:避免全局包版本混乱\n",
|
||||
"- **便于迁移**:通过 `requirements.txt` 快速复现环境\n",
|
||||
"- **与生产一致**:开发、测试、生产环境使用相同依赖"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. venv 进阶用法\n",
|
||||
"\n",
|
||||
"Python 3.12 内置 `venv` 模块,无需额外安装。除了创建环境,你还可以指定 Python 解释器、选择是否包含系统站点包等。\n",
|
||||
"\n",
|
||||
"### 常用命令\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"# 创建虚拟环境(默认当前目录下 .venv 文件夹)\n",
|
||||
"python -m venv .venv\n",
|
||||
"\n",
|
||||
"# 指定 Python 版本创建环境(需该版本在 PATH 中)\n",
|
||||
"python3.12 -m venv .venv\n",
|
||||
"\n",
|
||||
"# 创建时继承系统已安装的包(谨慎使用)\n",
|
||||
"python -m venv .venv --system-site-packages\n",
|
||||
"\n",
|
||||
"# 删除虚拟环境(直接删除文件夹即可)\n",
|
||||
"Remove-Item -Recurse -Force .venv\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 虚拟环境目录结构\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
".venv/\n",
|
||||
"├── Scripts/ # Windows 可执行文件(python.exe、pip.exe、activate)\n",
|
||||
"├── Lib/ # 安装的第三方库\n",
|
||||
"├── include/ # C 扩展头文件\n",
|
||||
"└── pyvenv.cfg # 环境配置信息\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 查看当前解释器所在环境信息\n",
|
||||
"import sys\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"print(f\"当前 Python:{sys.executable}\")\n",
|
||||
"print(f\"是否处于虚拟环境:{hasattr(sys, 'real_prefix') or sys.base_prefix != sys.prefix}\")\n",
|
||||
"print(f\"虚拟环境路径:{sys.prefix}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 使用 requirements.txt 管理依赖\n",
|
||||
"\n",
|
||||
"`requirements.txt` 是 Python 项目的依赖清单,便于团队共享和部署复现。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 导出当前环境依赖\n",
|
||||
"\n",
|
||||
"在已激活的虚拟环境中执行:\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"pip freeze > requirements.txt\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"### 从 requirements.txt 安装依赖\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 一份适合本课程的 requirements.txt 示例\n",
|
||||
"\n",
|
||||
"你可以将以下内容保存为项目根目录下的 `requirements.txt`:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"# 核心框架\n",
|
||||
"langchain>=0.3.0\n",
|
||||
"langgraph>=0.2.0\n",
|
||||
"langchain-openai>=0.2.0\n",
|
||||
"\n",
|
||||
"# 向量数据库与 Embedding\n",
|
||||
"langchain-chroma>=0.1.0\n",
|
||||
"sentence-transformers>=3.0.0\n",
|
||||
"\n",
|
||||
"# Jupyter 与工具\n",
|
||||
"jupyter>=1.0.0\n",
|
||||
"ipykernel>=6.29.0\n",
|
||||
"python-dotenv>=1.0.0\n",
|
||||
"\n",
|
||||
"# 可选:搜索/网络请求工具\n",
|
||||
"requests>=2.32.0\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 示例:查看当前环境已安装的核心包\n",
|
||||
"import subprocess\n",
|
||||
"result = subprocess.run(['pip', 'list'], capture_output=True, text=True)\n",
|
||||
"print(result.stdout[:2000]) # 只打印前 2000 字符"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Jupyter Notebook 配置\n",
|
||||
"\n",
|
||||
"Jupyter Notebook 是交互式运行 Python 代码的理想工具,本课程所有课件都以 `.ipynb` 格式提供。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 安装 Jupyter\n",
|
||||
"\n",
|
||||
"在虚拟环境中执行:\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"pip install jupyter ipykernel -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 为 Jupyter 注册当前虚拟环境内核\n",
|
||||
"\n",
|
||||
"这样 VS Code 的 Jupyter 面板就能识别并使用 `.venv` 中的解释器:\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"python -m ipykernel install --user --name=langchain-env --display-name=\"LangChain课程环境\"\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"参数说明:\n",
|
||||
"- `--user`:安装到用户目录,无需管理员权限\n",
|
||||
"- `--name`:内核内部名称\n",
|
||||
"- `--display-name`:在 VS Code 中显示的名称"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 在 VS Code 中切换 Jupyter 内核\n",
|
||||
"\n",
|
||||
"1. 打开任意 `.ipynb` 文件\n",
|
||||
"2. 点击右上角内核选择器(默认显示为 Python 版本号)\n",
|
||||
"3. 选择 `LangChain课程环境` 或 `.venv` 对应的解释器\n",
|
||||
"4. 重新运行单元格即可"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 测试 Jupyter 运行是否正常\n",
|
||||
"print(\"Hello, LangChain + LangGraph!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. pip 常用命令速查\n",
|
||||
"\n",
|
||||
"| 命令 | 作用 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| `pip install 包名` | 安装指定包 |\n",
|
||||
"| `pip install 包名==1.0.0` | 安装指定版本 |\n",
|
||||
"| `pip install -U 包名` | 升级指定包 |\n",
|
||||
"| `pip uninstall 包名` | 卸载指定包 |\n",
|
||||
"| `pip list` | 列出已安装包 |\n",
|
||||
"| `pip show 包名` | 查看包详细信息 |\n",
|
||||
"| `pip freeze > requirements.txt` | 导出依赖清单 |\n",
|
||||
"| `pip install -r requirements.txt` | 按清单安装依赖 |"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 常见环境问题排查\n",
|
||||
"\n",
|
||||
"### 问题 1:终端显示未激活虚拟环境\n",
|
||||
"- 检查终端是否从 VS Code 正确启动\n",
|
||||
"- 手动执行 `.venv\\Scripts\\Activate.ps1`\n",
|
||||
"\n",
|
||||
"### 问题 2:pip 安装速度慢或超时\n",
|
||||
"- 使用清华镜像源:`-i https://pypi.tuna.tsinghua.edu.cn/simple`\n",
|
||||
"- 升级 pip:`python -m pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple`\n",
|
||||
"\n",
|
||||
"### 问题 3:Jupyter 内核无法导入虚拟环境中的包\n",
|
||||
"- 确认内核选择的是 `.venv` 对应的解释器\n",
|
||||
"- 重新注册内核:`python -m ipykernel install --user --name=langchain-env --force`\n",
|
||||
"\n",
|
||||
"### 问题 4:安装包时报依赖冲突\n",
|
||||
"- 优先在干净虚拟环境中安装\n",
|
||||
"- 使用 `pip install 包名 --no-deps` 跳过依赖检查(谨慎)\n",
|
||||
"- 使用 `pip install -r requirements.txt` 统一安装"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 在当前项目目录下创建一个名为 `.venv` 的虚拟环境(如果尚未创建)\n",
|
||||
"2. 激活虚拟环境后,使用清华镜像源安装 `jupyter`、`ipykernel`、`langchain`、`langgraph`\n",
|
||||
"3. 注册当前环境为 Jupyter 内核,名称为 `langchain-env`\n",
|
||||
"4. 在 VS Code 中切换到这个内核,并成功运行本 notebook 中的所有代码单元格\n",
|
||||
"5. 使用 `pip freeze > requirements.txt` 导出依赖清单,查看文件内容"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
356
03_大模型API调用.ipynb
Normal file
356
03_大模型API调用.ipynb
Normal file
@@ -0,0 +1,356 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 03 大模型 API 调用\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解大模型 API 的基本概念(OpenAI、Anthropic 等)\n",
|
||||
"2. 学会使用 Python 代码调用大模型 API 进行对话\n",
|
||||
"3. 掌握 API 密钥的安全管理和环境变量配置\n",
|
||||
"4. 能够区分 OpenAI 格式与 Anthropic 格式的 API 调用方式"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 大模型 API 概述\n",
|
||||
"\n",
|
||||
"大模型(Large Language Model, LLM)通常通过 **HTTP API** 提供服务。你发送一段文本(Prompt),模型返回生成的回复。\n",
|
||||
"\n",
|
||||
"### 主流 API 提供商\n",
|
||||
"\n",
|
||||
"| 提供商 | 代表模型 | 特点 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| **OpenAI** | GPT-4o、GPT-4o-mini | 生态最成熟,很多第三方平台兼容其格式 |\n",
|
||||
"| **Anthropic** | Claude 3.5 Sonnet、Claude 3 Opus | 长文本处理能力强,安全性设计突出 |\n",
|
||||
"| **国内厂商** | 文心一言、通义千问、智谱 GLM | 中文优化好,无需翻墙 |\n",
|
||||
"\n",
|
||||
"> 💡 **注意**:很多国内平台(如硅基流动、智谱 AI、DashScope)提供兼容 OpenAI 格式的 API,可以直接使用 OpenAI SDK 调用。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. API 调用核心概念\n",
|
||||
"\n",
|
||||
"调用大模型 API 时,你需要了解以下几个核心要素:\n",
|
||||
"\n",
|
||||
"- **Base URL**:API 服务的基础地址,例如 `https://api.openai.com/v1` 或 `https://api.anthropic.com`\n",
|
||||
"- **API Key**:身份验证密钥,类似于密码,**绝对不能泄露**\n",
|
||||
"- **Model**:模型名称,本课程统一使用 `qwen3.6-35b-A3b`\n",
|
||||
"- **Message / Prompt**:发送给模型的输入内容\n",
|
||||
"- **Temperature**:控制输出随机性(0~2,越低越确定,越高越有创意)\n",
|
||||
"- **Max Tokens**:限制模型输出的最大长度"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. API 密钥安全管理\n",
|
||||
"\n",
|
||||
"**切勿将 API Key 直接写入代码并上传到 GitHub!** 推荐做法是将密钥存放在环境变量中。\n",
|
||||
"\n",
|
||||
"### 方法一:使用 .env 文件(推荐)\n",
|
||||
"\n",
|
||||
"1. 在项目根目录创建 `.env` 文件(注意文件名以点开头)\n",
|
||||
"2. 添加以下内容(填入老师提供的实际值):\n",
|
||||
"\n",
|
||||
"```env\n",
|
||||
"# 请使用老师提供的 base_url 和 api_key\n",
|
||||
"OPENAI_BASE_URL=https://your-openai-compatible-base-url.com/v1\n",
|
||||
"OPENAI_API_KEY=sk-your-openai-api-key\n",
|
||||
"\n",
|
||||
"ANTHROPIC_BASE_URL=https://api.anthropic.com\n",
|
||||
"ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"> ⚠️ **重要**:`.env` 文件已加入 `.gitignore`,不会上传到 Git,切勿手动删除该忽略配置。\n",
|
||||
"\n",
|
||||
"### 方法二:手动设置系统环境变量(Windows)\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"# 当前终端会话有效\n",
|
||||
"$env:OPENAI_API_KEY=\"sk-your-api-key\"\n",
|
||||
"$env:OPENAI_BASE_URL=\"https://your-base-url.com/v1\"\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 加载 .env 文件中的环境变量\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"load_dotenv() # 默认加载当前目录下的 .env 文件\n",
|
||||
"\n",
|
||||
"# 验证环境变量是否加载成功\n",
|
||||
"openai_base_url = os.getenv(\"OPENAI_BASE_URL\")\n",
|
||||
"openai_api_key = os.getenv(\"OPENAI_API_KEY\")\n",
|
||||
"anthropic_base_url = os.getenv(\"ANTHROPIC_BASE_URL\")\n",
|
||||
"anthropic_api_key = os.getenv(\"ANTHROPIC_API_KEY\")\n",
|
||||
"\n",
|
||||
"print(\"✅ OPENAI_BASE_URL 已设置\" if openai_base_url else \"❌ OPENAI_BASE_URL 未设置\")\n",
|
||||
"print(\"✅ OPENAI_API_KEY 已设置\" if openai_api_key else \"❌ OPENAI_API_KEY 未设置\")\n",
|
||||
"print(\"✅ ANTHROPIC_BASE_URL 已设置\" if anthropic_base_url else \"❌ ANTHROPIC_BASE_URL 未设置\")\n",
|
||||
"print(\"✅ ANTHROPIC_API_KEY 已设置\" if anthropic_api_key else \"❌ ANTHROPIC_API_KEY 未设置\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 方式一:OpenAI 格式调用(兼容 OpenAI SDK)\n",
|
||||
"\n",
|
||||
"OpenAI 格式的 API 是目前最通用的标准。很多国内厂商(如硅基流动、智谱、DeepSeek)也提供兼容此格式的接口。\n",
|
||||
"\n",
|
||||
"### 安装依赖\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"pip install openai python-dotenv -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from openai import OpenAI\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# 初始化客户端(使用环境变量中的配置)\n",
|
||||
"client = OpenAI(\n",
|
||||
" base_url=os.getenv(\"OPENAI_BASE_URL\"),\n",
|
||||
" api_key=os.getenv(\"OPENAI_API_KEY\")\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 发送对话请求\n",
|
||||
"response = client.chat.completions.create(\n",
|
||||
" model=\"qwen3.6-35b-A3b\", # 请根据老师提供的实际模型名称修改\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"system\", \"content\": \"你是一个 helpful 的 AI 助手。\"},\n",
|
||||
" {\"role\": \"user\", \"content\": \"请用一句话解释什么是大语言模型。\"}\n",
|
||||
" ],\n",
|
||||
" temperature=0.7,\n",
|
||||
" max_tokens=200\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 输出模型回复\n",
|
||||
"print(\"🤖 AI 回复:\")\n",
|
||||
"print(response.choices[0].message.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### OpenAI 格式调用要点\n",
|
||||
"\n",
|
||||
"| 参数 | 说明 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| `model` | 模型名称,本课程默认使用 `qwen3.6-35b-A3b` |\n",
|
||||
"| `messages` | 消息列表,每条消息包含 `role`(system/user/assistant)和 `content` |\n",
|
||||
"| `temperature` | 随机性(0~2),0 最确定,2 最有创意 |\n",
|
||||
"| `max_tokens` | 最大输出 token 数 |\n",
|
||||
"| `response.choices[0].message.content` | 获取模型回复的文本内容 |"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 扩展阅读:Anthropic 格式调用(Claude API)\n",
|
||||
"\n",
|
||||
"Anthropic 的 Claude 系列模型使用独立的 API 格式,与 OpenAI 格式不同。需要安装 `anthropic` Python SDK。\n",
|
||||
"\n",
|
||||
"> 课程主线统一使用 OpenAI 兼容格式调用 `qwen3.6-35b-A3b`。以下 Anthropic/Claude 内容仅用于了解不同 API 格式,不作为默认运行示例。\n",
|
||||
"\n",
|
||||
"### 安装依赖\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"pip install anthropic python-dotenv -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
|
||||
"```\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Anthropic/Claude 调用格式示意(扩展阅读)\n",
|
||||
"\n",
|
||||
"课程默认不执行 Claude 调用,主线统一使用 OpenAI 兼容格式调用 `qwen3.6-35b-A3b`。如需了解 Anthropic SDK 的写法,可参考下面的非执行示例:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from anthropic import Anthropic\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"client = Anthropic(\n",
|
||||
" base_url=os.getenv(\"ANTHROPIC_BASE_URL\"),\n",
|
||||
" api_key=os.getenv(\"ANTHROPIC_API_KEY\"),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"response = client.messages.create(\n",
|
||||
" model=\"<your-claude-model>\",\n",
|
||||
" max_tokens=200,\n",
|
||||
" temperature=0.7,\n",
|
||||
" system=\"你是一个 helpful 的 AI 助手。\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"user\", \"content\": \"请用一句话解释什么是大语言模型。\"}\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(response.content[0].text)\n",
|
||||
"```\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Anthropic 格式与 OpenAI 格式的区别\n",
|
||||
"\n",
|
||||
"| 特性 | OpenAI 格式 | Anthropic 格式 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 系统提示 | 放入 `messages` 列表,role=\"system\" | 独立的 `system` 参数 |\n",
|
||||
"| 回复获取 | `response.choices[0].message.content` | `response.content[0].text` |\n",
|
||||
"| SDK 包名 | `openai` | `anthropic` |\n",
|
||||
"| 客户端类 | `OpenAI()` | `Anthropic()` |\n",
|
||||
"| 方法名 | `chat.completions.create()` | `messages.create()` |"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 多轮对话示例\n",
|
||||
"\n",
|
||||
"多轮对话需要保留历史消息,让模型理解上下文。以下以 OpenAI 格式为例:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from openai import OpenAI\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"client = OpenAI(\n",
|
||||
" base_url=os.getenv(\"OPENAI_BASE_URL\"),\n",
|
||||
" api_key=os.getenv(\"OPENAI_API_KEY\")\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 维护对话历史\n",
|
||||
"messages = [\n",
|
||||
" {\"role\": \"system\", \"content\": \"你是一个 helpful 的 AI 助手。\"}\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# 第一轮对话\n",
|
||||
"messages.append({\"role\": \"user\", \"content\": \"你好,我叫小明。\"})\n",
|
||||
"response = client.chat.completions.create(\n",
|
||||
" model=\"qwen3.6-35b-A3b\",\n",
|
||||
" messages=messages,\n",
|
||||
" temperature=0.7\n",
|
||||
")\n",
|
||||
"reply = response.choices[0].message.content\n",
|
||||
"print(f\"🤖: {reply}\")\n",
|
||||
"\n",
|
||||
"# 将模型回复加入历史\n",
|
||||
"messages.append({\"role\": \"assistant\", \"content\": reply})\n",
|
||||
"\n",
|
||||
"# 第二轮对话(模型应该记得我叫小明)\n",
|
||||
"messages.append({\"role\": \"user\", \"content\": \"你还记得我的名字吗?\"})\n",
|
||||
"response = client.chat.completions.create(\n",
|
||||
" model=\"qwen3.6-35b-A3b\",\n",
|
||||
" messages=messages,\n",
|
||||
" temperature=0.7\n",
|
||||
")\n",
|
||||
"reply = response.choices[0].message.content\n",
|
||||
"print(f\"🤖: {reply}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 流式输出(Streaming)\n",
|
||||
"\n",
|
||||
"流式输出让模型内容逐字返回,提升用户体验:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from openai import OpenAI\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"client = OpenAI(\n",
|
||||
" base_url=os.getenv(\"OPENAI_BASE_URL\"),\n",
|
||||
" api_key=os.getenv(\"OPENAI_API_KEY\")\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"stream = client.chat.completions.create(\n",
|
||||
" model=\"qwen3.6-35b-A3b\",\n",
|
||||
" messages=[{\"role\": \"user\", \"content\": \"写一首关于春天的短诗。\"}],\n",
|
||||
" stream=True # 开启流式输出\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"🤖: \", end=\"\")\n",
|
||||
"for chunk in stream:\n",
|
||||
" # 某些 chunk 的 choices 为空,需要先判断\n",
|
||||
" if chunk.choices and chunk.choices[0].delta.content is not None:\n",
|
||||
" print(chunk.choices[0].delta.content, end=\"\")\n",
|
||||
"print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 根据老师提供的 `base_url` 和 `api_key`,在 `.env` 文件中正确配置 OpenAI 和 Anthropic 两种环境变量\n",
|
||||
"2. 运行 OpenAI 格式的示例代码,成功获取模型回复\n",
|
||||
"3. 阅读 Anthropic 格式的扩展说明,对比两种 API 的差异\n",
|
||||
"4. 尝试修改 `temperature` 和 `max_tokens` 参数,观察输出变化\n",
|
||||
"5. 使用流式输出模式,让模型生成一段关于人工智能的短文"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
303
04_LangChain概述.ipynb
Normal file
303
04_LangChain概述.ipynb
Normal file
@@ -0,0 +1,303 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 04 LangChain 概述\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解 LangChain 产生的背景和解决的问题\n",
|
||||
"2. 掌握 LangChain 的核心架构与组件组成\n",
|
||||
"3. 能够运行第一个 LangChain 程序,体验链式调用的便利性\n",
|
||||
"4. 了解 LangChain 生态中的相关工具(LangServe、LangSmith 等)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要 LangChain\n",
|
||||
"\n",
|
||||
"前两节课中,我们直接通过 openai 或 anthropic SDK 调用大模型 API。这种方式简单直接,但当应用变得复杂时,会遇到以下问题:\n",
|
||||
"\n",
|
||||
"- **Prompt 管理混乱**:硬编码在代码中,难以维护和复用\n",
|
||||
"- **代码重复**:每次调用都要写重复的 API 初始化、参数配置\n",
|
||||
"- **模型切换困难**:换一个模型提供商需要重写大量代码\n",
|
||||
"- **缺乏标准流程**:没有统一的输入处理、输出解析规范\n",
|
||||
"\n",
|
||||
"**LangChain** 就是为了解决这些问题而生的框架。它将大模型应用的开发流程抽象为标准化组件,让开发者可以像搭积木一样构建复杂的 AI 应用。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. LangChain 是什么\n",
|
||||
"\n",
|
||||
"LangChain 是一个用于构建基于大语言模型(LLM)应用的 Python/JS 框架,由 Harrison Chase 于 2022 年创建。\n",
|
||||
"\n",
|
||||
"### 核心设计思想\n",
|
||||
"\n",
|
||||
"- **组件化**:将大模型应用拆解为可复用的标准组件\n",
|
||||
"- **链式组合**:通过管道(|)将多个组件串联成处理流程\n",
|
||||
"- **模型无关**:同一套代码可以切换不同厂商的模型\n",
|
||||
"- **生态丰富**:提供大量预集成工具(向量库、搜索引擎、数据库等)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. LangChain 核心架构\n",
|
||||
"\n",
|
||||
"LangChain 将大模型应用抽象为四大核心模块:\n",
|
||||
"\n",
|
||||
"### 四大核心模块\n",
|
||||
"\n",
|
||||
"| 模块 | 作用 | 本课程对应章节 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| **Model I/O** | 模型输入输出管理(Prompt、Model、Output Parser) | 05-07 节 |\n",
|
||||
"| **Retrieval** | 检索增强生成(RAG):文档加载、向量化、检索 | 11-13 节 |\n",
|
||||
"| **Chains** | 链式组合,将多个组件串联成工作流 | 05、08 节 |\n",
|
||||
"| **Agents** | 智能体:让模型自主决策、调用工具完成任务 | 09-10 节 |\n",
|
||||
"\n",
|
||||
"### 组件关系图\n",
|
||||
"\n",
|
||||
"用户输入 -> Prompt模板 -> LLM模型 -> 输出解析 -> 最终结果\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"chain = prompt | llm | parser\n",
|
||||
"result = chain.invoke({'input': '用户问题'})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 第一个 LangChain 程序\n",
|
||||
"\n",
|
||||
"我们先看一个对比示例:直接调用 API vs 使用 LangChain。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 方式一:直接调用 API(传统方式)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from openai import OpenAI\n",
|
||||
"import os\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"client = OpenAI(\n",
|
||||
" base_url=os.getenv('OPENAI_BASE_URL'),\n",
|
||||
" api_key=os.getenv('OPENAI_API_KEY')\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"response = client.chat.completions.create(\n",
|
||||
" model='qwen3.6-35b-A3b',\n",
|
||||
" messages=[\n",
|
||||
" {'role': 'system', 'content': '你是一个翻译助手,将中文翻译成英文。'},\n",
|
||||
" {'role': 'user', 'content': '你好,世界'}\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"print(response.choices[0].message.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 方式二:使用 LangChain(推荐方式)\n",
|
||||
"\n",
|
||||
"LangChain 将 Prompt、模型调用、输出解析封装成标准组件,代码更简洁、更易维护。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# 1. 创建模型(自动读取环境变量 OPENAI_BASE_URL 和 OPENAI_API_KEY)\n",
|
||||
"llm = ChatOpenAI(\n",
|
||||
" model='qwen3.6-35b-A3b',\n",
|
||||
" temperature=0.7\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 2. 创建 Prompt 模板\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个翻译助手,将中文翻译成英文。'),\n",
|
||||
" ('user', '{input}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 3. 构建链:Prompt -> LLM\n",
|
||||
"chain = prompt | llm\n",
|
||||
"\n",
|
||||
"# 4. 运行链\n",
|
||||
"result = chain.invoke({'input': '你好,世界'})\n",
|
||||
"print(result.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 对比总结\n",
|
||||
"\n",
|
||||
"| 特性 | 直接 API | LangChain |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| Prompt 管理 | 硬编码在 messages 中 | 模板化,支持变量复用 |\n",
|
||||
"| 代码可读性 | 每次都要写完整 API 调用 | 语义清晰:prompt \\| llm |\n",
|
||||
"| 模型切换 | 重写 SDK 初始化代码 | 换一行 model= 即可 |\n",
|
||||
"| 输出处理 | 手动解析 choices[0] | 自动解析,支持结构化输出 |\n",
|
||||
"| 扩展性 | 难以添加后续处理步骤 | 可继续追加 \\| parser 等组件 |"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. LangChain 核心组件速览\n",
|
||||
"\n",
|
||||
"本课程后续章节会逐一深入讲解,这里先建立整体印象。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 5.1 Model(模型)\n",
|
||||
"\n",
|
||||
"LangChain 封装了各大厂商的模型,统一接口:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from langchain_openai import ChatOpenAI # OpenAI 兼容格式\n",
|
||||
"from langchain_anthropic import ChatAnthropic # Anthropic Claude\n",
|
||||
"from langchain_community.llms import Tongyi # 阿里通义千问\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b')\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"所有模型都支持 .invoke()、.stream()、.batch() 等统一方法。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 5.2 Prompt(提示模板)\n",
|
||||
"\n",
|
||||
"将 Prompt 模板化,支持变量插入和复用:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个{role}。'),\n",
|
||||
" ('user', '请{task}:{content}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"prompt.format(role='翻译助手', task='翻译', content='Hello')\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 5.3 Output Parser(输出解析器)\n",
|
||||
"\n",
|
||||
"将模型输出的文本解析为结构化数据(JSON、列表、Pydantic 对象等):\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from langchain_core.output_parsers import JsonOutputParser\n",
|
||||
"\n",
|
||||
"parser = JsonOutputParser()\n",
|
||||
"chain = prompt | llm | parser # 模型输出自动解析为 JSON\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 5.4 Chain(链)\n",
|
||||
"\n",
|
||||
"链是 LangChain 的核心概念,表示数据的处理流程。使用 | 运算符组合:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"chain = prompt | llm | parser\n",
|
||||
"result = chain.invoke({'input': '用户输入'})\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"数据流:用户输入 -> Prompt模板 -> LLM -> 输出解析 -> 最终结果"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. LangChain 生态工具\n",
|
||||
"\n",
|
||||
"| 工具 | 作用 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| **LangGraph** | 构建复杂状态图和多智能体工作流(本课程核心内容) |\n",
|
||||
"| **LangServe** | 将 LangChain 链部署为 REST API 服务 |\n",
|
||||
"| **LangSmith** | 调试、监控和评估 LangChain 应用的可观测性平台 |\n",
|
||||
"| **LangChain Hub** | 社区共享的 Prompt 模板市场 |\n",
|
||||
"\n",
|
||||
"本课程重点讲解 LangChain 核心组件 + LangGraph 智能体,LangSmith 会在最后的部署评估环节介绍。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 对比直接调用 API 和 LangChain 的代码,体会 LangChain 的便利性\n",
|
||||
"2. 修改上面的 LangChain 示例,将 system 提示改为「你是一个诗人」,将输入改为「写一句关于月亮的诗」\n",
|
||||
"3. 尝试在 ChatOpenAI 中切换 temperature(0.0、1.0、2.0),观察输出变化\n",
|
||||
"4. 尝试将 prompt | llm 链保存为一个变量,多次调用 invoke() 传入不同输入"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
393
05_LLM链.ipynb
Normal file
393
05_LLM链.ipynb
Normal file
@@ -0,0 +1,393 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 05 LLM 链\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解 LLMChain 的本质:Prompt 模板 + 模型 + 输出解析的组合\n",
|
||||
"2. 掌握使用 LCEL(LangChain Expression Language)管道符构建链\n",
|
||||
"3. 理解数据在链中的流动过程:输入 -> Prompt -> LLM -> 输出\n",
|
||||
"4. 学会使用链进行单条调用、批量调用和流式输出"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 什么是 LLMChain\n",
|
||||
"\n",
|
||||
"LLMChain 是 LangChain 中最基础、最常用的链类型。它表示一个简单的处理流程:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"用户输入 -> Prompt模板 -> 大模型 -> 输出结果\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"在 LangChain 的新版(LCEL)中,我们不再使用专门的 `LLMChain` 类,而是直接用 `|` 管道符将组件串联起来:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"chain = prompt | llm\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这种方式更直观、更灵活,也是本课程推荐的标准写法。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 构建第一个 LLMChain\n",
|
||||
"\n",
|
||||
"我们需要三个基本组件:\n",
|
||||
"1. **Prompt 模板**:定义输入格式和系统提示\n",
|
||||
"2. **LLM 模型**:实际调用的大模型\n",
|
||||
"3. **链**:将两者用 `|` 连接"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# 1. 创建模型\n",
|
||||
"llm = ChatOpenAI(\n",
|
||||
" model='qwen3.6-35b-A3b',\n",
|
||||
" temperature=0.7\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 2. 创建 Prompt 模板\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个专业的翻译助手,只输出翻译结果,不做解释。'),\n",
|
||||
" ('user', '请将以下文本翻译成英文:{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 3. 构建链\n",
|
||||
"chain = prompt | llm\n",
|
||||
"\n",
|
||||
"# 4. 运行链\n",
|
||||
"result = chain.invoke({'text': '人工智能正在改变世界'})\n",
|
||||
"print(result.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解析\n",
|
||||
"\n",
|
||||
"| 步骤 | 代码 | 作用 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 1 | `ChatOpenAI(...)` | 创建模型实例,指定模型名称和温度 |\n",
|
||||
"| 2 | `ChatPromptTemplate.from_messages(...)` | 创建提示模板,包含 system 和 user 消息 |\n",
|
||||
"| 3 | `prompt \\| llm` | 用管道符连接,形成处理链 |\n",
|
||||
"| 4 | `chain.invoke({'text': '...'})` | 传入变量,执行链并获取结果 |\n",
|
||||
"\n",
|
||||
"`invoke()` 返回的是 `AIMessage` 对象,用 `.content` 获取文本内容。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 数据流详解\n",
|
||||
"\n",
|
||||
"当调用 `chain.invoke({'text': '人工智能正在改变世界'})` 时,数据在链中是这样流动的:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"输入: {'text': '人工智能正在改变世界'}\n",
|
||||
" |\n",
|
||||
" v\n",
|
||||
"prompt: 将变量填入模板\n",
|
||||
" system: 你是一个专业的翻译助手,只输出翻译结果,不做解释。\n",
|
||||
" user: 请将以下文本翻译成英文:人工智能正在改变世界\n",
|
||||
" |\n",
|
||||
" v\n",
|
||||
"llm: 调用大模型 API\n",
|
||||
" |\n",
|
||||
" v\n",
|
||||
"输出: AIMessage(content='AI is changing the world.')\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"链的每个环节都是 **Runnable**(可运行对象),它们都实现了统一的接口:\n",
|
||||
"- `.invoke(input)` — 单条调用\n",
|
||||
"- `.batch(inputs)` — 批量调用\n",
|
||||
"- `.stream(input)` — 流式输出\n",
|
||||
"- `.ainvoke(input)` — 异步调用"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 多变量输入\n",
|
||||
"\n",
|
||||
"Prompt 模板中可以定义多个变量,invoke 时传入字典即可:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 多变量 Prompt 模板\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一位{style}作家。'),\n",
|
||||
" ('user', '请用{style}的风格写一段关于{topic}的{format},约{length}字。')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"chain = prompt | llm\n",
|
||||
"\n",
|
||||
"result = chain.invoke({\n",
|
||||
" 'style': '浪漫主义',\n",
|
||||
" 'topic': '星空',\n",
|
||||
" 'format': '散文',\n",
|
||||
" 'length': '100'\n",
|
||||
"})\n",
|
||||
"print(result.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 批量调用(Batch)\n",
|
||||
"\n",
|
||||
"当需要处理多条输入时,使用 `.batch()` 可以一次性发送多个请求,效率更高:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个简洁的助手,只输出结果。'),\n",
|
||||
" ('user', '将以下中文翻译成英文:{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"chain = prompt | llm\n",
|
||||
"\n",
|
||||
"# 批量输入\n",
|
||||
"inputs = [\n",
|
||||
" {'text': '你好'},\n",
|
||||
" {'text': '谢谢'},\n",
|
||||
" {'text': '再见'},\n",
|
||||
" {'text': '人工智能'}\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"results = chain.batch(inputs)\n",
|
||||
"for i, result in enumerate(results):\n",
|
||||
" print(f'{i+1}. {result.content}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 流式输出(Stream)\n",
|
||||
"\n",
|
||||
"链也支持流式输出,模型会逐字返回结果:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个诗人。'),\n",
|
||||
" ('user', '写一首关于{topic}的短诗。')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"chain = prompt | llm\n",
|
||||
"\n",
|
||||
"print('生成中:', end='')\n",
|
||||
"for chunk in chain.stream({'topic': '秋天'}):\n",
|
||||
" print(chunk.content, end='')\n",
|
||||
"print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 链的重用与组合\n",
|
||||
"\n",
|
||||
"定义好的链可以像函数一样多次复用,也可以作为更大链的一部分继续组合。本节演示三个核心概念:\n",
|
||||
"\n",
|
||||
"### 核心概念\n",
|
||||
"\n",
|
||||
"1. **StrOutputParser()**:输出解析器,自动把模型的 AIMessage 对象转换成纯字符串\n",
|
||||
"2. **链的复用**:同一个 chain 变量可以多次调用 invoke(),传入不同输入\n",
|
||||
"3. **链的组合**:把多个链用 `|` 连接起来,前一个链的输出作为后一个链的输入\n",
|
||||
"\n",
|
||||
"### 组合链的数据流\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"输入: {'text': '大语言模型正在改变软件开发的方式'}\n",
|
||||
" |\n",
|
||||
" v\n",
|
||||
"translate_chain: 中文 -> 英文\n",
|
||||
" 输出: 'Large language models are changing the way software is developed.'\n",
|
||||
" |\n",
|
||||
" v\n",
|
||||
"lambda x: {'text': x}: 把字符串包装成 summarize_chain 需要的字典格式\n",
|
||||
" 输出: {'text': 'Large language models are changing...'}\n",
|
||||
" |\n",
|
||||
" v\n",
|
||||
"summarize_chain: 总结成一句话\n",
|
||||
" 输出: '大型语言模型正在改变软件开发的方式。'\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"\n",
|
||||
"# 创建共享的 LLM 实例\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# ========== 1. 定义可复用的翻译链 ==========\n",
|
||||
"# 先创建翻译用的 Prompt 模板\n",
|
||||
"translate_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个翻译助手,将中文翻译成英文。'),\n",
|
||||
" ('user', '{text}') # {text} 是输入变量\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 构建翻译链:Prompt -> LLM -> 字符串输出解析器\n",
|
||||
"# StrOutputParser() 的作用:把模型返回的 AIMessage 对象直接转成 str\n",
|
||||
"translate_chain = translate_prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"# ========== 2. 链的复用 ==========\n",
|
||||
"# 同一个 translate_chain 可以像函数一样多次调用\n",
|
||||
"# 每次 invoke() 传入不同的 {'text': '...'},得到对应的英文翻译\n",
|
||||
"print(translate_chain.invoke({'text': '苹果'}))\n",
|
||||
"print(translate_chain.invoke({'text': '香蕉'}))\n",
|
||||
"print(translate_chain.invoke({'text': '桃子'}))\n",
|
||||
"\n",
|
||||
"# ========== 3. 链的组合 ==========\n",
|
||||
"# 再定义一个总结链,同样接收 {text} 变量\n",
|
||||
"summarize_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '请用一句话总结以下内容。'),\n",
|
||||
" ('user', '{text}')\n",
|
||||
"])\n",
|
||||
"summarize_chain = summarize_prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"# 组合链:翻译后的英文传给总结链\n",
|
||||
"# translate_chain 的输出是字符串,但 summarize_chain 需要字典 {'text': ...}\n",
|
||||
"# 所以中间加一个 lambda 函数做格式转换\n",
|
||||
"full_chain = translate_chain | (lambda x: {'text': x}) | summarize_chain\n",
|
||||
"\n",
|
||||
"# 运行组合链:输入中文 -> 翻译成英文 -> 总结成中文\n",
|
||||
"result = full_chain.invoke({'text': '大语言模型写代码的能力越来越强。使用ai开发代码让传统的软件开发方式被改变。'})\n",
|
||||
"print(f'\\n翻译+总结:{result}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 链的调试与查看中间结果\n",
|
||||
"\n",
|
||||
"使用 `StrOutputParser()` 可以自动将模型输出转换为字符串,省去手动 `.content` 的步骤。\n",
|
||||
"\n",
|
||||
"如果想查看链运行过程中的中间状态(比如格式化后的 Prompt 是什么样的),可以使用回调或调试工具:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个助手。'),\n",
|
||||
" ('user', '你好,我叫{name}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 查看格式化后的 Prompt\n",
|
||||
"formatted = prompt.format_messages(name='小明')\n",
|
||||
"print('格式化后的 Prompt:')\n",
|
||||
"for msg in formatted:\n",
|
||||
" print(f' {msg.type}: {msg.content}')\n",
|
||||
"\n",
|
||||
"chain = prompt | llm\n",
|
||||
"result = chain.invoke({'name': '小明'})\n",
|
||||
"print(f'\\n模型回复:{result.content}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 创建一个 LLMChain,将用户的输入翻译成法文(修改 system 提示和 user 模板)\n",
|
||||
"2. 使用多变量输入,创建一个可以根据「风格」和「主题」生成不同文章的链\n",
|
||||
"3. 使用 `.batch()` 一次性翻译 5 个不同的中文句子\n",
|
||||
"4. 使用 `.stream()` 让模型逐字生成一段关于「未来科技」的短文\n",
|
||||
"5. 尝试在链末尾加上 `| StrOutputParser()`,观察返回结果类型的变化"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
579
06_Prompt模板.ipynb
Normal file
579
06_Prompt模板.ipynb
Normal file
@@ -0,0 +1,579 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 06 Prompt 模板\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解 Prompt 模板的作用:将 Prompt 从硬编码变为可复用、可配置的组件\n",
|
||||
"2. 掌握两种最常用的模板类型:PromptTemplate 和 ChatPromptTemplate\n",
|
||||
"3. 学会使用变量占位符 {name} 动态填充内容\n",
|
||||
"4. 理解 Few-Shot Prompt 少样本提示的构建方法\n",
|
||||
"5. 了解常见的 Prompt Engineering 技巧及其对模型输出的影响"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要 Prompt 模板\n",
|
||||
"\n",
|
||||
"直接拼接字符串来构造 Prompt 会带来几个问题:\n",
|
||||
"\n",
|
||||
"- **可读性差**:Python 字符串拼接难以看清最终给模型的提示长什么样\n",
|
||||
"- **容易出错**:变量多的时候容易遗漏、顺序错乱\n",
|
||||
"- **难以复用**:同样的提示结构无法直接复用到不同输入\n",
|
||||
"- **难以管理**:无法集中管理和版本化 Prompt\n",
|
||||
"\n",
|
||||
"**Prompt 模板** 就是解决这些问题的方案。它允许你:\n",
|
||||
"\n",
|
||||
"- 把 Prompt 写成一个带占位符的模板\n",
|
||||
"- 运行时传入变量自动填充\n",
|
||||
"- 复用同一个结构处理不同输入\n",
|
||||
"- 把提示逻辑与业务逻辑分离"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. PromptTemplate:基础字符串模板\n",
|
||||
"\n",
|
||||
"`PromptTemplate` 是最基础的模板类,适用于构造单条文本提示。它只有一个 `template` 字符串和若干 {变量}。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# 创建模型\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 使用元组列表定义消息模板\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一位{role},擅长用{style}风格回答问题。'),\n",
|
||||
" ('user', '{question}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 查看组装后的 Prompt\n",
|
||||
"input_vars = {\n",
|
||||
" 'role': '计算机科学家',\n",
|
||||
" 'style': '通俗易懂',\n",
|
||||
" 'question': '什么是递归?'\n",
|
||||
"}\n",
|
||||
"print('===== 组装后的 Prompt =====')\n",
|
||||
"for msg in prompt.format_messages(**input_vars):\n",
|
||||
" print(f'{msg.type}: {msg.content}')\n",
|
||||
"\n",
|
||||
"# 构建链并调用\n",
|
||||
"chain = prompt | llm\n",
|
||||
"print('\\n===== 大模型输出 =====')\n",
|
||||
"result = chain.invoke(input_vars)\n",
|
||||
"print(result.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 2.1 使用 from_template 快速创建\n",
|
||||
"\n",
|
||||
"如果你习惯简单写法,可以使用 `from_template` 类方法,自动识别模板中的变量。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.prompts import SystemMessagePromptTemplate, HumanMessagePromptTemplate\n",
|
||||
"\n",
|
||||
"# 创建模型\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" SystemMessagePromptTemplate.from_template('你是一位耐心的老师。'),\n",
|
||||
" HumanMessagePromptTemplate.from_template('请解释{concept},要求{requirement}。')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 查看组装后的 Prompt\n",
|
||||
"input_vars = {\n",
|
||||
" 'concept': '神经网络',\n",
|
||||
" 'requirement': '用小学生能听懂的语言'\n",
|
||||
"}\n",
|
||||
"print('===== 组装后的 Prompt =====')\n",
|
||||
"for msg in prompt.format_messages(**input_vars):\n",
|
||||
" print(f'{msg.type}: {msg.content}')\n",
|
||||
"\n",
|
||||
"# 构建链并调用\n",
|
||||
"chain = prompt | llm\n",
|
||||
"print('\\n===== 大模型输出 =====')\n",
|
||||
"result = chain.invoke(input_vars)\n",
|
||||
"print(result.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. ChatPromptTemplate:聊天消息模板\n",
|
||||
"\n",
|
||||
"现代大模型通常采用对话格式,每条消息都有角色(role)。`ChatPromptTemplate` 就是为这种场景设计的。\n",
|
||||
"\n",
|
||||
"常见的消息类型:\n",
|
||||
"\n",
|
||||
"| 消息类型 | 作用 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| system | 设定模型的身份、能力和行为规则 |\n",
|
||||
"| user / human | 用户的输入问题 |\n",
|
||||
"| assistant / ai | 模型的回复,可用于 Few-Shot 示例 |\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"# 创建模型\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一位{role}。'),\n",
|
||||
" ('user', '请{action}:{content}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 先固定 role 变量,得到一个新的模板\n",
|
||||
"translator_prompt = prompt.partial(role='专业翻译')\n",
|
||||
"\n",
|
||||
"# 查看组装后的 Prompt\n",
|
||||
"input_vars = {\n",
|
||||
" 'action': '把以下中文翻译成英文',\n",
|
||||
" 'content': '今天天气很好'\n",
|
||||
"}\n",
|
||||
"print('===== 组装后的 Prompt =====')\n",
|
||||
"for msg in translator_prompt.format_messages(**input_vars):\n",
|
||||
" print(f'{msg.type}: {msg.content}')\n",
|
||||
"\n",
|
||||
"# 构建链并调用\n",
|
||||
"chain = translator_prompt | llm\n",
|
||||
"print('\\n===== 大模型输出 =====')\n",
|
||||
"result = chain.invoke(input_vars)\n",
|
||||
"print(result.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 3.1 使用消息模板对象构建模板\n",
|
||||
"\n",
|
||||
"除了元组,也可以使用 LangChain 提供的**消息模板类**来构建模板。这种方式语义更清晰,而且同样支持变量替换。\n",
|
||||
"\n",
|
||||
"**注意区分两类对象**:\n",
|
||||
"- `SystemMessage` / `HumanMessage`:普通消息对象,内容固定,不会解析 `{变量}`\n",
|
||||
"- `SystemMessagePromptTemplate` / `HumanMessagePromptTemplate`:消息模板对象,内容中的 `{变量}` 会被替换"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"# 创建模型\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个情绪分析助手,只输出正面或负面。'),\n",
|
||||
" ('human', '这个产品太差了'),\n",
|
||||
" ('ai', '负面'),\n",
|
||||
" ('human', '这次的体验非常愉快'),\n",
|
||||
" ('ai', '正面'),\n",
|
||||
" ('human', '{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 查看组装后的 Prompt\n",
|
||||
"input_vars = {'text': '物流速度还可以,但包装破损了'}\n",
|
||||
"print('===== 组装后的 Prompt =====')\n",
|
||||
"for msg in prompt.format_messages(**input_vars):\n",
|
||||
" print(f'{msg.type}: {msg.content}')\n",
|
||||
"\n",
|
||||
"# 构建链并调用\n",
|
||||
"chain = prompt | llm\n",
|
||||
"print('\\n===== 大模型输出 =====')\n",
|
||||
"result = chain.invoke(input_vars)\n",
|
||||
"print(result.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 模板变量的高级用法\n",
|
||||
"\n",
|
||||
"### 4.1 默认值与 partial\n",
|
||||
"\n",
|
||||
"你可以先填充部分变量,得到一个新的模板,后续再填充剩余变量。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate\n",
|
||||
"\n",
|
||||
"# 创建模型\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"# 准备示例数据\n",
|
||||
"examples = [\n",
|
||||
" {'input': '苹果', 'output': 'apple'},\n",
|
||||
" {'input': '香蕉', 'output': 'banana'},\n",
|
||||
" {'input': '人工智能', 'output': 'artificial intelligence'},\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# 定义每个示例的格式\n",
|
||||
"example_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('human', '{input}'),\n",
|
||||
" ('ai', '{output}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 构建 Few-Shot 提示模板\n",
|
||||
"few_shot_prompt = FewShotChatMessagePromptTemplate(\n",
|
||||
" example_prompt=example_prompt,\n",
|
||||
" examples=examples\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 组合成完整提示\n",
|
||||
"final_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个中英文翻译助手,请参考示例回答问题。'),\n",
|
||||
" few_shot_prompt,\n",
|
||||
" ('human', '{input}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 查看组装后的 Prompt\n",
|
||||
"input_vars = {'input': '机器学习'}\n",
|
||||
"print('===== 组装后的 Prompt =====')\n",
|
||||
"for msg in final_prompt.format_messages(**input_vars):\n",
|
||||
" print(f'{msg.type}: {msg.content}')\n",
|
||||
"\n",
|
||||
"# 构建链并调用\n",
|
||||
"chain = final_prompt | llm\n",
|
||||
"print('\\n===== 大模型输出 =====')\n",
|
||||
"result = chain.invoke(input_vars)\n",
|
||||
"print(result.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 4.2 多轮对话模板\n",
|
||||
"\n",
|
||||
"模板中可以预设多轮对话历史,常用于构建 Few-Shot 示例或保持上下文:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个情绪分析助手,只输出正面或负面。'),\n",
|
||||
" ('human', '这个产品太差了'),\n",
|
||||
" ('ai', '负面'),\n",
|
||||
" ('human', '这次的体验非常愉快'),\n",
|
||||
" ('ai', '正面'),\n",
|
||||
" ('human', '{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"messages = prompt.format_messages(text='物流速度还可以,但包装破损了')\n",
|
||||
"for msg in messages:\n",
|
||||
" print(f'{msg.type}: {msg.content}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Few-Shot Prompt:少样本提示\n",
|
||||
"\n",
|
||||
"Few-Shot Prompt 是 Prompt Engineering 中最有效的技巧之一。它通过在输入前给出若干「示例-答案」对,让模型学习期望的输出格式和风格。\n",
|
||||
"\n",
|
||||
"LangChain 提供了 `FewShotPromptTemplate` 和 `FewShotChatMessagePromptTemplate` 两种主要方式。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate\n",
|
||||
"\n",
|
||||
"# 准备示例数据\n",
|
||||
"examples = [\n",
|
||||
" {'input': '苹果', 'output': 'apple'},\n",
|
||||
" {'input': '香蕉', 'output': 'banana'},\n",
|
||||
" {'input': '人工智能', 'output': 'artificial intelligence'},\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# 定义每个示例的格式\n",
|
||||
"example_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('human', '{input}'),\n",
|
||||
" ('ai', '{output}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 构建 Few-Shot 提示模板\n",
|
||||
"few_shot_prompt = FewShotChatMessagePromptTemplate(\n",
|
||||
" example_prompt=example_prompt,\n",
|
||||
" examples=examples\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 组合成完整提示\n",
|
||||
"final_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个中英文翻译助手,请参考示例回答问题。'),\n",
|
||||
" few_shot_prompt,\n",
|
||||
" ('human', '{input}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"messages = final_prompt.format_messages(input='机器学习')\n",
|
||||
"for msg in messages:\n",
|
||||
" print(f'{msg.type}: {msg.content}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. Prompt Engineering 技巧\n",
|
||||
"\n",
|
||||
"Prompt Engineering 是指通过设计和优化提示词,让模型输出更准确、更符合预期。下面介绍几个最常用、最有效的技巧。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 6.1 角色扮演(Role Prompting)\n",
|
||||
"\n",
|
||||
"给模型设定一个清晰的角色,可以显著提升回答质量。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 无角色设定\n",
|
||||
"prompt1 = ChatPromptTemplate.from_messages([\n",
|
||||
" ('user', '解释什么是区块链')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 有角色设定\n",
|
||||
"prompt2 = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一位资深技术讲师,擅长用比喻和例子解释复杂概念。'),\n",
|
||||
" ('user', '解释什么是区块链')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"chain1 = prompt1 | llm\n",
|
||||
"chain2 = prompt2 | llm\n",
|
||||
"\n",
|
||||
"print('=== 无角色 ===')\n",
|
||||
"print(chain1.invoke({}).content[:250])\n",
|
||||
"print('\\n=== 有角色 ===')\n",
|
||||
"print(chain2.invoke({}).content[:250])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 6.2 明确输出格式(Output Formatting)\n",
|
||||
"\n",
|
||||
"如果你希望模型按特定格式输出,要在 Prompt 中明确说明。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import JsonOutputParser\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 把 JSON 格式示例放在 system 消息中\n",
|
||||
"# 注意:{ 和 } 是 LangChain 的变量占位符,如果要显示字面量大括号,必须写成 {{ 和 }}\n",
|
||||
"system_template = '''你是一个信息提取助手。请只输出 JSON 格式,不要包含任何解释。\n",
|
||||
"\n",
|
||||
"输出格式如下:\n",
|
||||
"{{\n",
|
||||
" \"names\": [...],\n",
|
||||
" \"locations\": [...],\n",
|
||||
" \"time\": \"\"\n",
|
||||
"}}'''\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', system_template),\n",
|
||||
" ('user', '文本:{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"chain = prompt | llm | JsonOutputParser()\n",
|
||||
"\n",
|
||||
"input_vars = {\n",
|
||||
" 'text': '2024年5月1日,李明和王芳一起去了北京故宫参观。'\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"print('===== 组装后的 Prompt =====')\n",
|
||||
"for msg in prompt.format_messages(**input_vars):\n",
|
||||
" print(f'{msg.type}: {msg.content}')\n",
|
||||
"\n",
|
||||
"print('\\n===== 大模型输出 =====')\n",
|
||||
"result = chain.invoke(input_vars)\n",
|
||||
"print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 6.3 分步骤思考(Chain-of-Thought)\n",
|
||||
"\n",
|
||||
"对于推理类问题,让模型「一步步思考」往往能显著提升准确率。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"# 直接提问\n",
|
||||
"prompt1 = ChatPromptTemplate.from_messages([\n",
|
||||
" ('user', '问题:一个农场有鸡和兔共35只,脚共94只。鸡和兔各几只?请直接给出答案。')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 分步思考\n",
|
||||
"prompt2 = ChatPromptTemplate.from_messages([\n",
|
||||
" ('user', '问题:一个农场有鸡和兔共35只,脚共94只。鸡和兔各几只?请一步步思考并给出答案。')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"print('=== 直接提问 ===')\n",
|
||||
"print((prompt1 | llm).invoke({}).content)\n",
|
||||
"print('\\n=== 分步思考 ===')\n",
|
||||
"print((prompt2 | llm).invoke({}).content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 完整示例:Prompt 模板与链结合\n",
|
||||
"\n",
|
||||
"下面是一个综合示例:使用模板构建一个「学习助手」,根据学生年级和知识点生成适合难度的解释。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一位{grade}水平的优秀教师。'),\n",
|
||||
" ('user', '请用适合{grade}学生理解的方式解释 {topic},并举一个生活中的例子。')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"chain = prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"result = chain.invoke({\n",
|
||||
" 'grade': '小学三年级',\n",
|
||||
" 'topic': '浮力'\n",
|
||||
"})\n",
|
||||
"print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 使用 `PromptTemplate` 创建一个邮件生成模板,变量包含收件人姓名、主题、正文要点,调用 `format` 输出完整邮件\n",
|
||||
"2. 使用 `ChatPromptTemplate` 创建一个「代码审查助手」模板,system 设定角色,user 传入代码片段\n",
|
||||
"3. 使用 `.partial()` 固定 system 角色为「技术文档写手」,只传入 user 变量运行\n",
|
||||
"4. 使用 `FewShotChatMessagePromptTemplate` 创建一个三示例的情绪分类器,然后输入新句子测试\n",
|
||||
"5. 对比实验:分别用「直接提问」和「分步骤思考」两种方式向模型提问同一道数学题,观察输出差异"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
560
07_输出解析器.ipynb
Normal file
560
07_输出解析器.ipynb
Normal file
@@ -0,0 +1,560 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 07 输出解析器\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解输出解析器(Output Parsers)在 LangChain 链中的作用\n",
|
||||
"2. 掌握常见解析器的用法:StrOutputParser、JsonOutputParser、PydanticOutputParser、CommaSeparatedListOutputParser\n",
|
||||
"3. 学会使用 `手动 try-except + LLM 修复` 处理模型输出格式异常\n",
|
||||
"4. 了解如何编写自定义输出解析器\n",
|
||||
"5. 理解「Prompt 中说明格式 + 解析器约束」配合的重要性"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要输出解析器\n",
|
||||
"\n",
|
||||
"大模型返回的通常是**自由文本**,但很多时候我们需要:\n",
|
||||
"\n",
|
||||
"- 提取结构化的 JSON 数据\n",
|
||||
"- 把文本转换成 Python 列表、字典或对象\n",
|
||||
"- 验证输出是否符合预期的数据格式\n",
|
||||
"- 过滤掉模型多余的解释,只保留关键信息\n",
|
||||
"\n",
|
||||
"**输出解析器**就是链中负责把模型输出转换为结构化数据的组件。\n",
|
||||
"\n",
|
||||
"在 LCEL 中,链的常见结构是:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"chain = prompt | llm | output_parser\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"其中 `output_parser` 接收 `AIMessage` 对象,输出我们需要的 Python 数据结构。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. StrOutputParser:最简单的字符串解析器\n",
|
||||
"\n",
|
||||
"`StrOutputParser` 会把模型返回的 `AIMessage` 对象直接转换成纯字符串。\n",
|
||||
"\n",
|
||||
"它适合的场景:\n",
|
||||
"- 只需要文本回答\n",
|
||||
"- 不想每次手动写 `.content`\n",
|
||||
"- 作为链的最后一步统一输出类型"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# 创建模型\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 创建 Prompt 模板\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个简洁的助手。'),\n",
|
||||
" ('user', '用一句话介绍{topic}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 构建链:prompt -> llm -> 字符串解析器\n",
|
||||
"chain = prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"# 调用链,直接得到字符串\n",
|
||||
"result = chain.invoke({'topic': '机器学习'})\n",
|
||||
"\n",
|
||||
"print('返回类型:', type(result))\n",
|
||||
"print('返回内容:', result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"| 代码 | 作用 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| `prompt \\| llm \\| StrOutputParser()` | 用管道符串联三个组件 |\n",
|
||||
"| `StrOutputParser()` | 自动提取 `.content` 并返回字符串 |\n",
|
||||
"| `type(result)` | 验证返回结果是否为 `str` |\n",
|
||||
"\n",
|
||||
"不加 `StrOutputParser()` 时,`chain.invoke()` 返回的是 `AIMessage` 对象;加了之后返回的是 `str`。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. JsonOutputParser:解析 JSON 输出\n",
|
||||
"\n",
|
||||
"`JsonOutputParser` 会自动把模型输出的 JSON 字符串解析成 Python 字典。\n",
|
||||
"\n",
|
||||
"使用要点:\n",
|
||||
"1. 在 Prompt 中明确要求模型输出 JSON\n",
|
||||
"2. JSON 中的大括号 `{` 在 Prompt 模板中需要写成 `{{` 进行转义\n",
|
||||
"3. 模型输出必须能被 `json.loads()` 解析"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import JsonOutputParser\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"# 注意:模板中的 {{ 和 }} 会被 LangChain 渲染成单个 { 和 }\n",
|
||||
"system_template = '''你是一个信息提取助手。请只输出 JSON 格式,不要包含任何解释。\n",
|
||||
"\n",
|
||||
"输出格式如下:\n",
|
||||
"{{\\n\n",
|
||||
" \"names\": [...],\\n\n",
|
||||
" \"locations\": [...],\\n\n",
|
||||
" \"time\": \"\"\\n\n",
|
||||
"}}'''\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', system_template),\n",
|
||||
" ('user', '文本:{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"chain = prompt | llm | JsonOutputParser()\n",
|
||||
"\n",
|
||||
"result = chain.invoke({\n",
|
||||
" 'text': '2024年5月1日,李明和王芳一起去了北京故宫参观。'\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print('返回类型:', type(result))\n",
|
||||
"print('返回内容:', result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `system_template` 中的 `{{` 和 `}}` 是**LangChain 模板转义语法**,表示最终 Prompt 中显示为单个 `{` 和 `}`\n",
|
||||
"- `JsonOutputParser()` 会调用 `json.loads()` 解析模型输出\n",
|
||||
"- 如果模型输出不是合法 JSON,会抛出异常\n",
|
||||
"\n",
|
||||
"返回类型是 `dict`,可以直接像 `result['names']` 这样访问字段。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. PydanticOutputParser:解析为 Pydantic 对象\n",
|
||||
"\n",
|
||||
"如果你需要更严格的类型校验和结构化数据,可以使用 `PydanticOutputParser`。\n",
|
||||
"\n",
|
||||
"Pydantic 是 Python 中非常流行的数据验证库。通过定义数据模型,可以:\n",
|
||||
"- 明确每个字段的类型\n",
|
||||
"- 自动验证数据格式\n",
|
||||
"- 把模型输出转换成可操作的 Python 对象"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import PydanticOutputParser\n",
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"# 第一步:定义 Pydantic 数据模型\n",
|
||||
"class PersonInfo(BaseModel):\n",
|
||||
" name: str = Field(description='人物姓名')\n",
|
||||
" age: int = Field(description='人物年龄')\n",
|
||||
" city: str = Field(description='所在城市')\n",
|
||||
" hobbies: list[str] = Field(description='兴趣爱好列表')\n",
|
||||
"\n",
|
||||
"# 第二步:创建解析器\n",
|
||||
"parser = PydanticOutputParser(pydantic_object=PersonInfo)\n",
|
||||
"\n",
|
||||
"# 第三步:在 Prompt 中嵌入格式说明\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '''你是一个信息提取助手。请从文本中提取人物信息,按指定格式输出。\\n\n",
|
||||
"{format_instructions}'''),\n",
|
||||
" ('user', '文本:{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# format_instructions 会自动生成 JSON Schema 说明\n",
|
||||
"prompt_with_format = prompt.partial(format_instructions=parser.get_format_instructions())\n",
|
||||
"\n",
|
||||
"# 构建链\n",
|
||||
"chain = prompt_with_format | llm | parser\n",
|
||||
"\n",
|
||||
"result = chain.invoke({\n",
|
||||
" 'text': '张三今年25岁,住在杭州,喜欢打篮球和编程。'\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print('返回类型:', type(result))\n",
|
||||
"print('返回对象:', result)\n",
|
||||
"print('姓名:', result.name)\n",
|
||||
"print('年龄:', result.age)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"| 步骤 | 代码 | 说明 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 1 | `class PersonInfo(BaseModel)` | 定义数据结构,每个字段都有类型和描述 |\n",
|
||||
"| 2 | `PydanticOutputParser(pydantic_object=PersonInfo)` | 创建解析器,指定要解析的模型 |\n",
|
||||
"| 3 | `parser.get_format_instructions()` | 自动生成格式说明,告诉模型如何输出 |\n",
|
||||
"| 4 | `prompt.partial(format_instructions=...)` | 把格式说明预先填充到 Prompt 中 |\n",
|
||||
"| 5 | `chain.invoke(...)` | 返回的是 `PersonInfo` 对象 |\n",
|
||||
"\n",
|
||||
"Pydantic 会自动校验类型,比如 `age` 必须是整数,否则报错。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. CommaSeparatedListOutputParser:解析逗号分隔列表\n",
|
||||
"\n",
|
||||
"当你需要模型返回一个列表时,可以使用 `CommaSeparatedListOutputParser`。\n",
|
||||
"\n",
|
||||
"它适合的场景:\n",
|
||||
"- 提取关键词\n",
|
||||
"- 生成待办事项\n",
|
||||
"- 输出多个选项"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import CommaSeparatedListOutputParser\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 创建解析器\n",
|
||||
"parser = CommaSeparatedListOutputParser()\n",
|
||||
"\n",
|
||||
"# 获取格式说明\n",
|
||||
"format_instructions = parser.get_format_instructions()\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个关键词提取助手。{format_instructions}'),\n",
|
||||
" ('user', '请从以下文本中提取3-5个关键词:{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 预填充格式说明\n",
|
||||
"prompt_with_format = prompt.partial(format_instructions=format_instructions)\n",
|
||||
"\n",
|
||||
"chain = prompt_with_format | llm | parser\n",
|
||||
"\n",
|
||||
"result = chain.invoke({\n",
|
||||
" 'text': '人工智能、机器学习和深度学习正在改变各个行业的运作方式。'\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print('返回类型:', type(result))\n",
|
||||
"print('关键词列表:', result)\n",
|
||||
"print('第一个关键词:', result[0])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `CommaSeparatedListOutputParser()` 会自动生成格式说明:「用逗号分隔各项,不要带编号和多余解释」\n",
|
||||
"- `parser.get_format_instructions()` 返回一段英文说明文本\n",
|
||||
"- 返回结果是 Python 列表 `list[str]`\n",
|
||||
"- 可以直接用索引访问,比如 `result[0]`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 处理格式异常:手动 try-except + LLM 修复\n",
|
||||
"\n",
|
||||
"大模型有时不会严格按 JSON 格式输出,可能包含多余文字、缺失引号或注释。\n",
|
||||
"\n",
|
||||
"`手动 try-except + LLM 修复` 可以包装另一个解析器,当第一次解析失败时,自动调用 LLM 修复输出格式。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import JsonOutputParser\n",
|
||||
"import json\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"# 定义一个 JSON 解析器\n",
|
||||
"parser = JsonOutputParser()\n",
|
||||
"\n",
|
||||
"# 提示模型输出 JSON,但不强制约束格式\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '请把用户信息转换成 JSON 格式输出。'),\n",
|
||||
" ('user', '内容:{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 先获取模型的原始文本输出\n",
|
||||
"raw_chain = prompt | llm\n",
|
||||
"raw_output = raw_chain.invoke({'text': '姓名:李四,年龄:30,城市:上海'})\n",
|
||||
"\n",
|
||||
"print('===== 原始模型输出 =====')\n",
|
||||
"print(raw_output.content)\n",
|
||||
"\n",
|
||||
"# 尝试解析 JSON\n",
|
||||
"try:\n",
|
||||
" result = parser.invoke(raw_output)\n",
|
||||
" print('\\n===== 直接解析成功 =====')\n",
|
||||
" print(result)\n",
|
||||
"except Exception as e:\n",
|
||||
" print('\\n===== 解析失败,尝试用 LLM 修复 =====')\n",
|
||||
" print('错误信息:', e)\n",
|
||||
"\n",
|
||||
" # 构造修复 Prompt,让模型把错误输出修正为合法 JSON\n",
|
||||
" fix_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个 JSON 修复专家。请把用户提供的内容修正为合法 JSON,只输出 JSON 字符串,不要任何解释。'),\n",
|
||||
" ('user', '原始内容:\\n{raw_output}\\n\\n错误信息:{error}')\n",
|
||||
" ])\n",
|
||||
"\n",
|
||||
" fix_chain = fix_prompt | llm\n",
|
||||
" fixed_output = fix_chain.invoke({\n",
|
||||
" 'raw_output': raw_output.content,\n",
|
||||
" 'error': str(e)\n",
|
||||
" })\n",
|
||||
"\n",
|
||||
" # 手动用 json.loads 解析修复后的内容\n",
|
||||
" result = json.loads(fixed_output.content)\n",
|
||||
" print('\\n===== 修复后的结果 =====')\n",
|
||||
" print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `手动 try-except + LLM 修复.from_llm(parser=parser, llm=llm)` 用同一个 LLM 修复格式错误\n",
|
||||
"- 修复逻辑:第一次解析失败 → 把原始输出和错误信息传给 LLM → 请求模型修正为合法格式 → 再次解析\n",
|
||||
"- 适合用于对稳定性要求较高的场景\n",
|
||||
"- 注意:修复不一定 100% 成功,极端情况下仍会报错"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 自定义输出解析器\n",
|
||||
"\n",
|
||||
"如果内置解析器不能满足需求,你可以继承 `BaseOutputParser` 自己实现。\n",
|
||||
"\n",
|
||||
"下面是一个简单的自定义解析器:把模型输出按指定分隔符切分成列表。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import BaseOutputParser\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 自定义解析器:按换行符切分,并去除空行和序号\n",
|
||||
"class NumberedListParser(BaseOutputParser[list[str]]):\n",
|
||||
" \"\"\"把模型输出的编号列表解析成字符串列表。\"\"\"\n",
|
||||
"\n",
|
||||
" def parse(self, text: str) -> list[str]:\n",
|
||||
" # 按行切分\n",
|
||||
" lines = text.strip().split('\\n')\n",
|
||||
" # 去除空行,并去掉每行开头的 1. 2. 3. 等序号\n",
|
||||
" items = []\n",
|
||||
" for line in lines:\n",
|
||||
" line = line.strip()\n",
|
||||
" if not line:\n",
|
||||
" continue\n",
|
||||
" # 去掉行首的数字和点,例如 \"1. \"\n",
|
||||
" if '. ' in line[:4]:\n",
|
||||
" line = line.split('. ', 1)[1]\n",
|
||||
" items.append(line)\n",
|
||||
" return items\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个清单生成助手。请输出编号列表,每行一个项目。'),\n",
|
||||
" ('user', '请列出学习{topic}的5个步骤')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"chain = prompt | llm | NumberedListParser()\n",
|
||||
"\n",
|
||||
"result = chain.invoke({'topic': 'Python 编程'})\n",
|
||||
"\n",
|
||||
"print('返回类型:', type(result))\n",
|
||||
"print('返回内容:', result)\n",
|
||||
"print('步骤数量:', len(result))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- 继承 `BaseOutputParser[T]`,泛型 `T` 表示返回类型\n",
|
||||
"- 必须实现 `parse(self, text: str) -> T` 方法\n",
|
||||
"- `text` 是模型输出的字符串(已经过 `StrOutputParser` 提取 `.content`)\n",
|
||||
"- 可以在 `parse` 中做任何字符串处理:切分、清洗、正则匹配、校验等\n",
|
||||
"\n",
|
||||
"自定义解析器的优势是完全可控,适合处理模型输出不规范的情况。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 完整示例:结构化数据提取\n",
|
||||
"\n",
|
||||
"下面是一个综合示例:从一段商品评价中同时提取评分、优点、缺点和关键词。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import PydanticOutputParser\n",
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"class Review(BaseModel):\n",
|
||||
" sentiment: str = Field(description='整体情感,正面/负面/中性')\n",
|
||||
" pros: list[str] = Field(description='优点列表')\n",
|
||||
" cons: list[str] = Field(description='缺点列表')\n",
|
||||
" keywords: list[str] = Field(description='关键词列表')\n",
|
||||
"\n",
|
||||
"parser = PydanticOutputParser(pydantic_object=Review)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '''你是一个商品评价分析助手。请从评价中提取结构化信息。\\n\n",
|
||||
"{format_instructions}\\n\n",
|
||||
"只输出 JSON,不要其他内容。'''),\n",
|
||||
" ('user', '商品评价:{review}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"prompt_with_format = prompt.partial(format_instructions=parser.get_format_instructions())\n",
|
||||
"\n",
|
||||
"chain = prompt_with_format | llm | parser\n",
|
||||
"\n",
|
||||
"review_text = '''这款手机外观很漂亮,拍照效果也不错。\n",
|
||||
"但是电池续航一般,充电速度有点慢。\n",
|
||||
"总体来说性价比还可以。'''\n",
|
||||
"\n",
|
||||
"result = chain.invoke({'review': review_text})\n",
|
||||
"\n",
|
||||
"print('情感:', result.sentiment)\n",
|
||||
"print('优点:', result.pros)\n",
|
||||
"print('缺点:', result.cons)\n",
|
||||
"print('关键词:', result.keywords)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 常见解析器对比\n",
|
||||
"\n",
|
||||
"| 解析器 | 返回类型 | 适用场景 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| `StrOutputParser` | `str` | 只需要文本内容 |\n",
|
||||
"| `JsonOutputParser` | `dict` | 需要 JSON 字典 |\n",
|
||||
"| `PydanticOutputParser` | Pydantic 对象 | 需要类型校验和结构化对象 |\n",
|
||||
"| `CommaSeparatedListOutputParser` | `list[str]` | 逗号分隔的列表 |\n",
|
||||
"| `手动 try-except + LLM 修复` | 依赖被包装的解析器 | 自动修复格式错误 |\n",
|
||||
"| 自定义 `BaseOutputParser` | 任意类型 | 特殊格式或复杂后处理 |"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 使用 `StrOutputParser()` 构建一个链,让模型介绍一门编程语言,验证返回类型是否为 `str`\n",
|
||||
"2. 使用 `JsonOutputParser()` 从一段地址文本中提取省、市、区、街道,输出为 JSON 字典\n",
|
||||
"3. 定义一个 Pydantic 模型 `BookInfo`(书名、作者、出版年份、类型),用 `PydanticOutputParser` 从文本中提取图书信息\n",
|
||||
"4. 使用 `CommaSeparatedListOutputParser()` 让模型列出学习 LangChain 的 5 个关键知识点\n",
|
||||
"5. 尝试让模型故意输出不规范的 JSON,然后用 `手动 try-except + LLM 修复` 包装 `JsonOutputParser` 观察是否能修复"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
558
08_链式组合.ipynb
Normal file
558
08_链式组合.ipynb
Normal file
@@ -0,0 +1,558 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 08 链式组合\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 深入理解 LCEL(LangChain Expression Language)管道符 `|` 的组合原理\n",
|
||||
"2. 掌握 `Runnable` 接口的核心方法:`invoke`、`batch`、`stream`、`ainvoke`\n",
|
||||
"3. 学会在链中使用 `RunnableLambda` 进行数据格式转换\n",
|
||||
"4. 理解 `RunnableParallel` 并行执行多个子链\n",
|
||||
"5. 能够构建复杂的多步骤处理链"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 什么是 LCEL 链式组合\n",
|
||||
"\n",
|
||||
"LCEL 是 LangChain 推荐的新一代链式组合语法。它用管道符 `|` 把各个组件串联起来,就像 Linux 命令行一样直观。\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"chain = component_a | component_b | component_c\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"数据流:\n",
|
||||
"```\n",
|
||||
"输入 -> component_a -> 中间结果 -> component_b -> 中间结果 -> component_c -> 输出\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"LCEL 中的每个组件都实现了 `Runnable` 接口,它们都有统一的方法签名。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Runnable 接口的核心方法\n",
|
||||
"\n",
|
||||
"所有 LCEL 组件都实现了以下方法:\n",
|
||||
"\n",
|
||||
"| 方法 | 作用 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| `invoke(input)` | 单条同步调用 |\n",
|
||||
"| `batch(inputs)` | 批量同步调用 |\n",
|
||||
"| `stream(input)` | 流式输出 |\n",
|
||||
"| `ainvoke(input)` | 异步单条调用 |\n",
|
||||
"| `abatch(inputs)` | 异步批量调用 |\n",
|
||||
"\n",
|
||||
"这些方法不仅单个组件有,组合后的链也有。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 构建一个简单链\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个翻译助手。'),\n",
|
||||
" ('user', '把以下中文翻译成英文:{text}')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"chain = prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"# 使用 invoke 单条调用\n",
|
||||
"result = chain.invoke({'text': '人工智能'})\n",
|
||||
"print('invoke 结果:', result)\n",
|
||||
"\n",
|
||||
"# 使用 batch 批量调用\n",
|
||||
"results = chain.batch([\n",
|
||||
" {'text': '苹果'},\n",
|
||||
" {'text': '香蕉'},\n",
|
||||
" {'text': '机器学习'}\n",
|
||||
"])\n",
|
||||
"print('\\nbatch 结果:', results)\n",
|
||||
"\n",
|
||||
"# 使用 stream 流式输出\n",
|
||||
"print('\\nstream 结果:')\n",
|
||||
"for chunk in chain.stream({'text': '未来科技'}):\n",
|
||||
" print(chunk, end='')\n",
|
||||
"print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `prompt` 接收字典,输出格式化后的消息列表\n",
|
||||
"- `llm` 接收消息列表,输出 `AIMessage`\n",
|
||||
"- `StrOutputParser()` 接收 `AIMessage`,输出字符串\n",
|
||||
"- 组合后的 `chain` 继承了三个组件的能力,可以直接调用 `invoke`、`batch`、`stream`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 使用 RunnableLambda 转换数据\n",
|
||||
"\n",
|
||||
"在组合链时,前一个组件的输出格式可能和后一个组件的输入格式不匹配。\n",
|
||||
"\n",
|
||||
"例如:前一个链输出字符串,后一个链需要字典。这时可以用 `RunnableLambda` 或普通 lambda 函数做转换。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnableLambda\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 第一步:把中文翻译成英文\n",
|
||||
"translate_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个翻译助手。'),\n",
|
||||
" ('user', '把以下中文翻译成英文:{text}')\n",
|
||||
"])\n",
|
||||
"translate_chain = translate_prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"# 第二步:把英文总结成一句话\n",
|
||||
"summarize_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个总结助手。'),\n",
|
||||
" ('user', '请用一句话总结以下内容:{text}')\n",
|
||||
"])\n",
|
||||
"summarize_chain = summarize_prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"# 翻译链输出字符串,但总结链需要字典 {text: ...}\n",
|
||||
"# 使用 RunnableLambda 做格式转换\n",
|
||||
"def to_dict(text):\n",
|
||||
" return {'text': text}\n",
|
||||
"\n",
|
||||
"# 组合链:翻译 -> 转字典 -> 总结\n",
|
||||
"full_chain = translate_chain | RunnableLambda(to_dict) | summarize_chain\n",
|
||||
"\n",
|
||||
"result = full_chain.invoke({'text': '大语言模型开发效率比较高,而且比较聪明,而且成本比较低,大家都在学习怎么使用,正在改变软件的开发方式。'})\n",
|
||||
"print('最终结果:', result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"| 组件 | 输入 | 输出 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| `translate_chain` | `{'text': '...'}` | 英文字符串 |\n",
|
||||
"| `RunnableLambda(to_dict)` | 英文字符串 | `{'text': '...'}` |\n",
|
||||
"| `summarize_chain` | `{'text': '...'}` | 总结字符串 |\n",
|
||||
"\n",
|
||||
"`RunnableLambda` 把一个普通函数包装成 Runnable,可以无缝接入链中。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 直接传递多个变量:RunnableParallel\n",
|
||||
"\n",
|
||||
"`RunnableParallel` 允许你同时运行多个子链,并把多个结果合并成一个字典。\n",
|
||||
"\n",
|
||||
"它适合的场景:\n",
|
||||
"- 同时让模型做翻译和总结\n",
|
||||
"- 同时提取不同类型的信息\n",
|
||||
"- 一个输入需要生成多个输出"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnableParallel\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 定义多个子链\n",
|
||||
"translate_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是翻译助手。'),\n",
|
||||
" ('user', '把以下中文翻译成英文:{text}')\n",
|
||||
"])\n",
|
||||
"translate_chain = translate_prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"summary_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是总结助手。'),\n",
|
||||
" ('user', '用一句话总结:{text}')\n",
|
||||
"])\n",
|
||||
"summary_chain = summary_prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"keywords_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是关键词提取助手。'),\n",
|
||||
" ('user', '提取3个关键词,用逗号分隔:{text}')\n",
|
||||
"])\n",
|
||||
"keywords_chain = keywords_prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"# 并行执行三个子链,输入都是 {'text': '...'}\n",
|
||||
"parallel_chain = RunnableParallel(\n",
|
||||
" translation=translate_chain,\n",
|
||||
" summary=summary_chain,\n",
|
||||
" keywords=keywords_chain\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"result = parallel_chain.invoke({'text': '人工智能正在改变教育、医疗和交通行业'})\n",
|
||||
"\n",
|
||||
"print('返回类型:', type(result))\n",
|
||||
"print('翻译结果:', result['translation'])\n",
|
||||
"print('总结结果:', result['summary'])\n",
|
||||
"print('关键词结果:', result['keywords'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `RunnableParallel(...)` 接收一个字典,键是输出字段名,值是子链\n",
|
||||
"- 所有子链会同时接收同一个输入 `{'text': '...'}`\n",
|
||||
"- 返回结果是一个字典,包含每个子链的输出\n",
|
||||
"- 适合需要「一次输入、多种处理」的场景"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 组合并行与串行\n",
|
||||
"\n",
|
||||
"`RunnableParallel` 的输出是一个字典,可以继续传给下一个组件使用。\n",
|
||||
"\n",
|
||||
"例如:先并行做翻译和总结,再把结果合并成一个报告。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnableParallel\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"# 并行生成翻译和总结\n",
|
||||
"parallel_chain = RunnableParallel(\n",
|
||||
" translation=(\n",
|
||||
" ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '翻译助手'),\n",
|
||||
" ('user', '把中文翻译成英文:{text}')\n",
|
||||
" ]) | llm | StrOutputParser()\n",
|
||||
" ),\n",
|
||||
" summary=(\n",
|
||||
" ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '总结助手'),\n",
|
||||
" ('user', '用一句话总结:{text}')\n",
|
||||
" ]) | llm | StrOutputParser()\n",
|
||||
" )\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 把并行结果合并成最终报告\n",
|
||||
"report_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个报告生成助手。'),\n",
|
||||
" ('user', '''请根据以下翻译和总结生成一份简短报告:\\n\\n翻译:{translation}\\n总结:{summary}\\n\\n请输出包含「原文概要」和「英文翻译」两部分的报告。''')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"full_chain = parallel_chain | report_prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"result = full_chain.invoke({'text': '大语言模型开发效率比较高,而且比较聪明,而且成本比较低,大家都在学习怎么使用,正在改变软件的开发方式。'})\n",
|
||||
"print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"| 阶段 | 说明 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| `parallel_chain` | 同时生成翻译和总结,输出 `{'translation': ..., 'summary': ...}` |\n",
|
||||
"| `report_prompt` | 接收字典,用 `{translation}` 和 `{summary}` 填充模板 |\n",
|
||||
"| `llm \\| StrOutputParser()` | 生成最终报告 |\n",
|
||||
"\n",
|
||||
"这种「先并行、再串行」的模式在实际应用中非常常见。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 条件分支:RunnableBranch\n",
|
||||
"\n",
|
||||
"`RunnableBranch` 可以根据条件选择不同的子链执行。\n",
|
||||
"\n",
|
||||
"例如:根据输入语言类型选择不同的处理链。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnableBranch, RunnableLambda\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"# 定义两个处理链\n",
|
||||
"chinese_chain = (\n",
|
||||
" ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是中文助手。'),\n",
|
||||
" ('user', '请用中文解释:{text}')\n",
|
||||
" ]) | llm | StrOutputParser()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"english_chain = (\n",
|
||||
" ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', 'You are an English assistant.'),\n",
|
||||
" ('user', 'Please explain in English: {text}')\n",
|
||||
" ]) | llm | StrOutputParser()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 判断输入文本是否包含中文字符\n",
|
||||
"def is_chinese(input_dict):\n",
|
||||
" text = input_dict['text']\n",
|
||||
" return any('\\u4e00' <= char <= '\\u9fff' for char in text)\n",
|
||||
"\n",
|
||||
"# 构建分支链\n",
|
||||
"branch_chain = RunnableBranch(\n",
|
||||
" (is_chinese, chinese_chain),\n",
|
||||
" english_chain # 默认分支\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print('中文输入结果:')\n",
|
||||
"print(branch_chain.invoke({'text': '神经网络'})[:250])\n",
|
||||
"\n",
|
||||
"print('\\n英文输入结果:')\n",
|
||||
"print(branch_chain.invoke({'text': 'neural network'})[:250])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `RunnableBranch` 接收多个 `(条件函数, 子链)` 元组,最后是默认子链\n",
|
||||
"- 条件函数接收链的输入,返回 `True` 或 `False`\n",
|
||||
"- 从上往下匹配,第一个条件为 `True` 的分支会被执行\n",
|
||||
"- 如果没有匹配,执行默认分支\n",
|
||||
"\n",
|
||||
"注意:条件函数处理的输入是整个链的输入(这里是字典),不是前一个组件的输出。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 查看链的结构\n",
|
||||
"\n",
|
||||
"复杂的链可以通过 `get_graph().print_ascii()` 方法查看结构。它会用 ASCII 字符打印出链中的节点和连接关系,非常直观。\n",
|
||||
"\n",
|
||||
"> 需要安装依赖:`pip install grandalf`\n",
|
||||
"> 安装后请**重启 Jupyter 内核**再运行本单元格。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"chain = (\n",
|
||||
" ChatPromptTemplate.from_messages([\n",
|
||||
" ('user', '把以下内容翻译成英文:{text}')\n",
|
||||
" ])\n",
|
||||
" | llm\n",
|
||||
" | StrOutputParser()\n",
|
||||
" | (lambda x: {'text': x})\n",
|
||||
" | ChatPromptTemplate.from_messages([\n",
|
||||
" ('user', '用一句话总结:{text}')\n",
|
||||
" ])\n",
|
||||
" | llm\n",
|
||||
" | StrOutputParser()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 打印链的 ASCII 结构图\n",
|
||||
"chain.get_graph().print_ascii()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `get_graph()` 获取链的图结构对象\n",
|
||||
"- `print_ascii()` 用 ASCII 字符画出节点和边\n",
|
||||
"- 可以清楚地看到数据从输入到输出经过了哪些步骤\n",
|
||||
"- 如果提示缺少 `grandalf`,请先安装:`pip install grandalf`,然后重启 Jupyter 内核"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 完整示例:智能客服工单处理\n",
|
||||
"\n",
|
||||
"下面是一个综合示例:用户输入一条投诉或咨询,系统同时完成情绪分析、分类、生成回复建议。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnableParallel\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.3)\n",
|
||||
"\n",
|
||||
"# 并行分析:情绪 + 分类 + 摘要\n",
|
||||
"# 注意:reply_prompt 中还需要原始输入 text,所以这里用 lambda 把 text 一起传下去\n",
|
||||
"analysis_chain = RunnableParallel(\n",
|
||||
" text=lambda x: x['text'],\n",
|
||||
" sentiment=(\n",
|
||||
" ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '情绪分析助手,只输出:正面/负面/中性'),\n",
|
||||
" ('user', '{text}')\n",
|
||||
" ]) | llm | StrOutputParser()\n",
|
||||
" ),\n",
|
||||
" category=(\n",
|
||||
" ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '分类助手,只输出类别:售后/产品/物流/其他'),\n",
|
||||
" ('user', '{text}')\n",
|
||||
" ]) | llm | StrOutputParser()\n",
|
||||
" ),\n",
|
||||
" summary=(\n",
|
||||
" ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '摘要助手,用一句话概括'),\n",
|
||||
" ('user', '{text}')\n",
|
||||
" ]) | llm | StrOutputParser()\n",
|
||||
" )\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 根据分析结果生成回复建议\n",
|
||||
"reply_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是客服主管,请根据分析结果生成回复建议。'),\n",
|
||||
" ('user', '''用户反馈:{text}\n",
|
||||
"\n",
|
||||
"分析结果:\n",
|
||||
"- 情绪:{sentiment}\n",
|
||||
"- 类别:{category}\n",
|
||||
"- 摘要:{summary}\n",
|
||||
"\n",
|
||||
"请生成一段礼貌、专业的回复建议。''')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"full_chain = analysis_chain | reply_prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
"result = full_chain.invoke({\n",
|
||||
" 'text': '你们的发货速度太慢了,说好三天到,结果等了一周,非常失望!'\n",
|
||||
"})\n",
|
||||
"print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"| 步骤 | 说明 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| `analysis_chain` | 并行执行三个分析任务,输出字典 |\n",
|
||||
"| `reply_prompt` | 把原始输入和分析结果一起填充到回复模板 |\n",
|
||||
"| `llm \\| StrOutputParser()` | 生成最终回复建议 |\n",
|
||||
"\n",
|
||||
"注意:`reply_prompt` 中既用到了原始输入 `{text}`,也用到了并行分析结果 `{sentiment}`、`{category}`、`{summary}`。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 用 `|` 构建一个链:输入中文 -> 翻译为英文 -> 再翻译回中文,观察「回译」后的变化\n",
|
||||
"2. 使用 `RunnableLambda` 把字符串输出转换为字典,再接第二个 Prompt 模板\n",
|
||||
"3. 使用 `RunnableParallel` 让一个输入同时生成「正式版」和「口语版」两种翻译\n",
|
||||
"4. 使用 `RunnableBranch` 根据输入长度选择不同模型:短文本用轻量提示,长文本用详细提示\n",
|
||||
"5. 用 `chain.get_graph().print_ascii()` 查看你自己构建的链的结构\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
470
09_工具定义.ipynb
Normal file
470
09_工具定义.ipynb
Normal file
@@ -0,0 +1,470 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 09 工具定义\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解工具(Tools)在智能体开发中的核心作用\n",
|
||||
"2. 掌握使用 `@tool` 装饰器定义自定义工具\n",
|
||||
"3. 学会为工具编写清晰的参数说明和文档字符串\n",
|
||||
"4. 理解工具描述如何影响模型选择工具的能力\n",
|
||||
"5. 能够定义计算类、查询类和文件处理类工具"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要工具\n",
|
||||
"\n",
|
||||
"大语言模型本身只能处理文本,它的知识截止于训练数据,也无法直接访问外部世界。\n",
|
||||
"\n",
|
||||
"**工具(Tools)** 让模型能够:\n",
|
||||
"\n",
|
||||
"- 执行数学计算(模型不擅长精确计算)\n",
|
||||
"- 查询数据库、API、搜索引擎\n",
|
||||
"- 读写文件、操作数据库\n",
|
||||
"- 获取实时信息(天气、股价、新闻)\n",
|
||||
"\n",
|
||||
"智能体(Agent)的核心工作就是:**根据用户问题选择合适的工具,调用工具,再根据结果回答问题**。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 使用 @tool 装饰器定义第一个工具\n",
|
||||
"\n",
|
||||
"LangChain 提供了 `@tool` 装饰器,可以把一个普通 Python 函数变成智能体可调用的工具。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"# 使用 @tool 装饰器定义一个加法工具\n",
|
||||
"@tool\n",
|
||||
"def add(a: int, b: int) -> int:\n",
|
||||
" \"\"\"计算两个整数的和。\"\"\"\n",
|
||||
" return a + b\n",
|
||||
"\n",
|
||||
"# 查看工具信息\n",
|
||||
"print('工具名称:', add.name)\n",
|
||||
"print('工具描述:', add.description)\n",
|
||||
"print('参数Schema:', add.args)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `@tool`:把 `add` 函数注册为 LangChain 工具\n",
|
||||
"- `a: int, b: int`:参数类型会被自动解析为工具的输入参数格式\n",
|
||||
"- 函数文档字符串 `\"\"\"...\"\"\"`:会被自动提取为工具描述(description)\n",
|
||||
"- `add.name`:工具名,默认使用函数名\n",
|
||||
"- `add.description`:工具描述,来自函数的 docstring\n",
|
||||
"- `add.args`:参数的 JSON Schema,模型根据它决定如何传参"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 手动调用工具\n",
|
||||
"\n",
|
||||
"工具定义好后,可以通过 `invoke` 方法或 `run` 方法调用。\n",
|
||||
"\n",
|
||||
"注意:`@tool` 装饰后的对象是一个 `StructuredTool` 对象,**不能直接像普通函数那样加括号调用**(如 `multiply(3, 4)`),需要传入字典参数。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def multiply(a: int, b: int) -> int:\n",
|
||||
" \"\"\"计算两个整数的乘积。\"\"\"\n",
|
||||
" return a * b\n",
|
||||
"\n",
|
||||
"# 通过 invoke 调用,传入字典参数\n",
|
||||
"print('invoke 调用:', multiply.invoke({'a': 3, 'b': 4}))\n",
|
||||
"\n",
|
||||
"# 通过 run 调用(LangChain 工具的传统调用方式)\n",
|
||||
"print('run 调用:', multiply.run({'a': 5, 'b': 6}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `@tool` 装饰后的对象是一个 `StructuredTool`,不是普通函数\n",
|
||||
"- `multiply.invoke({'a': 3, 'b': 4})`:LangChain 风格调用,智能体实际使用这种方式\n",
|
||||
"- `multiply.run({'a': 5, 'b': 6})`:传统工具调用方式,与 `invoke` 效果相同\n",
|
||||
"- 注意:不能写成 `multiply(3, 4)`,否则会报 `StructuredTool object is not callable`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 自定义工具名称和描述\n",
|
||||
"\n",
|
||||
"默认情况下,工具名就是函数名,描述来自 docstring。你也可以通过 `@tool` 参数自定义。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"@tool('weather-query', description='查询指定城市的当前天气情况')\n",
|
||||
"def get_weather(city: str) -> str:\n",
|
||||
" \"\"\"\n",
|
||||
" 查询天气。\n",
|
||||
" 参数:\n",
|
||||
" city: 城市名称,例如 \"北京\"、\"上海\"\n",
|
||||
" \"\"\"\n",
|
||||
" # 这里用模拟数据,实际可接入天气 API\n",
|
||||
" weather_data = {\n",
|
||||
" '北京': '晴,25°C',\n",
|
||||
" '上海': '多云,28°C',\n",
|
||||
" '广州': '小雨,30°C'\n",
|
||||
" }\n",
|
||||
" return weather_data.get(city, f'未找到 {city} 的天气信息')\n",
|
||||
"\n",
|
||||
"print('工具名称:', get_weather.name)\n",
|
||||
"print('工具描述:', get_weather.description)\n",
|
||||
"print('调用结果:', get_weather.invoke({'city': '北京'}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `@tool('weather-query', description='...')`:自定义工具名和描述\n",
|
||||
"- 工具描述非常重要,模型主要依赖它判断什么时候使用该工具\n",
|
||||
"- 描述要清晰、具体,包含「工具能做什么」和「参数含义」\n",
|
||||
"- 函数 docstring 可以继续补充参数细节"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 定义带多个参数的工具\n",
|
||||
"\n",
|
||||
"工具可以有多个参数,LangChain 会自动根据类型注解生成参数 Schema。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def calculate_bmi(height: float, weight: float) -> str:\n",
|
||||
" \"\"\"\n",
|
||||
" 根据身高和体重计算 BMI 指数,并返回健康建议。\n",
|
||||
" 参数:\n",
|
||||
" height: 身高,单位米\n",
|
||||
" weight: 体重,单位千克\n",
|
||||
" \"\"\"\n",
|
||||
" bmi = weight / (height ** 2)\n",
|
||||
" if bmi < 18.5:\n",
|
||||
" suggestion = '体重过轻,建议加强营养'\n",
|
||||
" elif bmi < 24:\n",
|
||||
" suggestion = '体重正常,请保持'\n",
|
||||
" elif bmi < 28:\n",
|
||||
" suggestion = '超重,建议适当运动'\n",
|
||||
" else:\n",
|
||||
" suggestion = '肥胖,建议咨询医生'\n",
|
||||
" return f'BMI: {bmi:.2f},{suggestion}'\n",
|
||||
"\n",
|
||||
"print(calculate_bmi.invoke({'height': 1.75, 'weight': 70}))\n",
|
||||
"print('\\n参数 Schema:')\n",
|
||||
"print(calculate_bmi.args)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `height: float, weight: float`:类型注解会被 LangChain 自动识别\n",
|
||||
"- 生成的 `args` 中会包含参数名、类型、是否必填等信息\n",
|
||||
"- 模型会根据这个 Schema 构造正确的 JSON 参数调用工具"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 定义模拟 API 查询工具\n",
|
||||
"\n",
|
||||
"实际应用中,工具通常会调用外部 API。下面我们用一个模拟的「产品库存查询」工具来演示。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def query_product_stock(product_name: str) -> str:\n",
|
||||
" \"\"\"\n",
|
||||
" 查询指定商品的库存数量。\n",
|
||||
" 参数:\n",
|
||||
" product_name: 商品名称\n",
|
||||
" \"\"\"\n",
|
||||
" # 模拟数据库查询\n",
|
||||
" stock_db = {\n",
|
||||
" '手机': 150,\n",
|
||||
" '笔记本电脑': 45,\n",
|
||||
" '耳机': 200,\n",
|
||||
" '充电宝': 80\n",
|
||||
" }\n",
|
||||
" stock = stock_db.get(product_name)\n",
|
||||
" if stock is None:\n",
|
||||
" return f'未找到商品 \"{product_name}\" 的库存信息'\n",
|
||||
" return f'商品 \"{product_name}\" 当前库存:{stock} 件'\n",
|
||||
"\n",
|
||||
"# 测试工具\n",
|
||||
"print(query_product_stock.invoke({'product_name': '手机'}))\n",
|
||||
"print(query_product_stock.invoke({'product_name': '相机'}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- 这个工具模拟了查询数据库或 API 的场景\n",
|
||||
"- 返回的字符串会作为观察结果(Observation)交给模型继续处理\n",
|
||||
"- 工具内部可以替换为真实的数据库查询、HTTP 请求等"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 定义文件处理工具\n",
|
||||
"\n",
|
||||
"工具也可以读写文件。下面是读取文本文件和写入文本文件的工具示例。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def read_text_file(file_path: str) -> str:\n",
|
||||
" \"\"\"\n",
|
||||
" 读取指定文本文件的内容。\n",
|
||||
" 参数:\n",
|
||||
" file_path: 文件路径\n",
|
||||
" \"\"\"\n",
|
||||
" try:\n",
|
||||
" with open(file_path, 'r', encoding='utf-8') as f:\n",
|
||||
" return f.read()\n",
|
||||
" except FileNotFoundError:\n",
|
||||
" return f'文件不存在:{file_path}'\n",
|
||||
" except Exception as e:\n",
|
||||
" return f'读取失败:{str(e)}'\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def write_text_file(file_path: str, content: str) -> str:\n",
|
||||
" \"\"\"\n",
|
||||
" 将内容写入指定文本文件。\n",
|
||||
" 参数:\n",
|
||||
" file_path: 文件路径\n",
|
||||
" content: 要写入的内容\n",
|
||||
" \"\"\"\n",
|
||||
" try:\n",
|
||||
" with open(file_path, 'w', encoding='utf-8') as f:\n",
|
||||
" f.write(content)\n",
|
||||
" return f'已成功写入文件:{file_path}'\n",
|
||||
" except Exception as e:\n",
|
||||
" return f'写入失败:{str(e)}'\n",
|
||||
"\n",
|
||||
"# 测试写入和读取\n",
|
||||
"write_text_file.invoke({'file_path': 'test_note.txt', 'content': '这是测试内容'})\n",
|
||||
"print(read_text_file.invoke({'file_path': 'test_note.txt'}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `read_text_file`:读取指定文件内容\n",
|
||||
"- `write_text_file`:把字符串写入指定文件\n",
|
||||
"- 文件路径使用相对路径时,会以当前工作目录为基准\n",
|
||||
"- 工具内部要做好异常处理,避免程序崩溃"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 工具列表与工具选择\n",
|
||||
"\n",
|
||||
"在构建智能体时,通常会把多个工具放在一个列表里,交给模型选择使用。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def add(a: int, b: int) -> int:\n",
|
||||
" \"\"\"计算两个整数的和。\"\"\"\n",
|
||||
" return a + b\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def multiply(a: int, b: int) -> int:\n",
|
||||
" \"\"\"计算两个整数的乘积。\"\"\"\n",
|
||||
" return a * b\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def power(base: int, exponent: int) -> int:\n",
|
||||
" \"\"\"计算 base 的 exponent 次方。\"\"\"\n",
|
||||
" return base ** exponent\n",
|
||||
"\n",
|
||||
"# 把工具放入列表\n",
|
||||
"tools = [add, multiply, power]\n",
|
||||
"\n",
|
||||
"# 查看工具列表信息\n",
|
||||
"for t in tools:\n",
|
||||
" print(f'名称:{t.name},描述:{t.description}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- 智能体通过工具列表了解有哪些工具可用\n",
|
||||
"- 模型会根据用户问题和工具描述,决定调用哪个工具、传什么参数\n",
|
||||
"- 工具名称和描述的清晰度直接影响模型选择工具的准确率"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 完整示例:多功能计算器工具集\n",
|
||||
"\n",
|
||||
"下面把多个数学工具组合成一个工具集,模拟智能体可使用的工具环境。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def add(a: float, b: float) -> float:\n",
|
||||
" \"\"\"计算两个数的和。\"\"\"\n",
|
||||
" return a + b\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def subtract(a: float, b: float) -> float:\n",
|
||||
" \"\"\"计算两个数的差。\"\"\"\n",
|
||||
" return a - b\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def multiply(a: float, b: float) -> float:\n",
|
||||
" \"\"\"计算两个数的乘积。\"\"\"\n",
|
||||
" return a * b\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def divide(a: float, b: float) -> float:\n",
|
||||
" \"\"\"计算两个数的商,如果除数为0则返回错误信息。\"\"\"\n",
|
||||
" if b == 0:\n",
|
||||
" return '错误:除数不能为0'\n",
|
||||
" return a / b\n",
|
||||
"\n",
|
||||
"math_tools = [add, subtract, multiply, divide]\n",
|
||||
"\n",
|
||||
"# 测试每个工具\n",
|
||||
"for t in math_tools:\n",
|
||||
" print(f'{t.name}: {t.description}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 使用 `@tool` 定义一个摄氏度转华氏度的工具,参数和返回值都使用 float 类型\n",
|
||||
"2. 定义一个「查询课程信息」工具,用字典模拟数据库,根据课程名返回上课时间\n",
|
||||
"3. 定义一个「写入学习笔记」工具,接收文件名和内容,把内容写入指定文件\n",
|
||||
"4. 把上面三个工具放入一个列表,打印每个工具的名称、描述和参数 Schema\n",
|
||||
"5. 尝试修改一个工具的描述,观察参数 Schema 是否会变化"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
468
10_工具调用.ipynb
Normal file
468
10_工具调用.ipynb
Normal file
@@ -0,0 +1,468 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 10 工具调用\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解工具调用(Tool Calling)的基本机制和流程\n",
|
||||
"2. 掌握手动解析模型返回的工具调用请求\n",
|
||||
"3. 学会使用 `bind_tools` 让模型选择工具\n",
|
||||
"4. 掌握 `create_tool_calling_agent` 和 `AgentExecutor` 构建智能体\n",
|
||||
"5. 能够处理工具调用中的常见错误"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 工具调用的基本流程\n",
|
||||
"\n",
|
||||
"工具调用不是由模型直接执行代码,而是模型「决定」要调用哪个工具、传什么参数,然后由程序代为执行。\n",
|
||||
"\n",
|
||||
"完整流程如下:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"用户提问 -> 模型分析 -> 输出工具调用请求 -> 程序执行工具 -> 结果返回模型 -> 模型生成最终回答\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"模型输出的是结构化的调用请求,包含:\n",
|
||||
"- 工具名称(name)\n",
|
||||
"- 工具参数(arguments)\n",
|
||||
"\n",
|
||||
"程序根据请求执行对应函数,再把结果返回给模型。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 准备工具集\n",
|
||||
"\n",
|
||||
"首先定义几个简单的工具,用于后续演示。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def add(a: float, b: float) -> float:\n",
|
||||
" \"\"\"计算两个数的和。\"\"\"\n",
|
||||
" return a + b\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def multiply(a: float, b: float) -> float:\n",
|
||||
" \"\"\"计算两个数的乘积。\"\"\"\n",
|
||||
" return a * b\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def query_weather(city: str) -> str:\n",
|
||||
" \"\"\"\n",
|
||||
" 查询指定城市的天气。\n",
|
||||
" 参数:\n",
|
||||
" city: 城市名称,如北京、上海\n",
|
||||
" \"\"\"\n",
|
||||
" weather_db = {'北京': '晴 25°C', '上海': '多云 28°C', '广州': '小雨 30°C'}\n",
|
||||
" return weather_db.get(city, f'未找到 {city} 的天气信息')\n",
|
||||
"\n",
|
||||
"tools = [add, multiply, query_weather]\n",
|
||||
"\n",
|
||||
"for t in tools:\n",
|
||||
" print(f'工具:{t.name},描述:{t.description}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 手动执行工具调用\n",
|
||||
"\n",
|
||||
"在使用智能体之前,先手动走一遍工具调用流程,理解底层机制。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.messages import HumanMessage\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"# 把工具绑定到模型上\n",
|
||||
"llm_with_tools = llm.bind_tools(tools)\n",
|
||||
"\n",
|
||||
"# 用户提问\n",
|
||||
"question = '北京和上海的气温相差多少度?'\n",
|
||||
"messages = [HumanMessage(content=question)]\n",
|
||||
"\n",
|
||||
"# 模型输出,可能包含工具调用请求\n",
|
||||
"response = llm_with_tools.invoke(messages)\n",
|
||||
"\n",
|
||||
"print('模型回复类型:', type(response))\n",
|
||||
"print('工具调用请求:', response.tool_calls)\n",
|
||||
"print('文本内容:', response.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `llm.bind_tools(tools)`:把工具列表绑定到模型上\n",
|
||||
"- 模型看到问题后,如果觉得需要工具,会在 `response.tool_calls` 中返回调用请求\n",
|
||||
"- 每个 `tool_call` 包含 `name`(工具名)和 `args`(参数)\n",
|
||||
"- 如果不需要工具,`response.content` 中直接包含文本回答"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 手动解析并执行工具调用\n",
|
||||
"\n",
|
||||
"模型只负责「决定」调用什么工具,真正的执行需要程序完成。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.messages import HumanMessage, ToolMessage\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"llm_with_tools = llm.bind_tools(tools)\n",
|
||||
"\n",
|
||||
"# 使用一个只需一次工具调用的简单问题\n",
|
||||
"question = '北京今天的天气怎么样?'\n",
|
||||
"messages = [HumanMessage(content=question)]\n",
|
||||
"\n",
|
||||
"# 第一步:获取模型的工具调用请求\n",
|
||||
"response = llm_with_tools.invoke(messages)\n",
|
||||
"messages.append(response)\n",
|
||||
"\n",
|
||||
"print('第一次模型输出:')\n",
|
||||
"print('tool_calls:', response.tool_calls)\n",
|
||||
"print('content:', response.content)\n",
|
||||
"\n",
|
||||
"# 第二步:执行工具调用\n",
|
||||
"for tool_call in response.tool_calls:\n",
|
||||
" tool_name = tool_call['name']\n",
|
||||
" tool_args = tool_call['args']\n",
|
||||
" \n",
|
||||
" # 找到对应的工具并执行\n",
|
||||
" selected_tool = {t.name: t for t in tools}[tool_name]\n",
|
||||
" tool_result = selected_tool.invoke(tool_args)\n",
|
||||
" \n",
|
||||
" # 把工具执行结果加入对话历史\n",
|
||||
" messages.append(ToolMessage(content=str(tool_result), tool_call_id=tool_call['id']))\n",
|
||||
" \n",
|
||||
" print(f'\\n执行工具:{tool_name},参数:{tool_args},结果:{tool_result}')\n",
|
||||
"\n",
|
||||
"# 第三步:把工具结果返回模型,生成最终回答\n",
|
||||
"final_response = llm_with_tools.invoke(messages)\n",
|
||||
"print('\\n最终回答:', final_response.content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `ToolMessage`:专门用于承载工具执行结果的消息类型\n",
|
||||
"- `tool_call_id`:必须和模型请求的 `id` 对应,这是 LangChain 的要求\n",
|
||||
"- 工具执行后,需要把结果追加到 `messages` 中,再次调用模型\n",
|
||||
"- 这种手动流程帮助我们理解智能体的底层工作原理"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 使用 create_agent 构建智能体\n",
|
||||
"\n",
|
||||
"LangChain 1.x 推荐使用 `create_agent` 构建工具调用智能体。它会自动处理「模型决定调用工具 -> 执行工具 -> 返回结果 -> 再次调用模型」的循环,并返回一个可执行的状态图。\n",
|
||||
"\n",
|
||||
"`create_agent` 的核心参数:\n",
|
||||
"\n",
|
||||
"| 参数 | 说明 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| `model` | 语言模型实例 |\n",
|
||||
"| `tools` | 工具列表 |\n",
|
||||
"| `system_prompt` | 系统提示词 |\n",
|
||||
"| `checkpointer` | 可选,用于保存对话历史 |"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain.agents import create_agent\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"# 创建工具调用智能体\n",
|
||||
"# create_agent 是 LangChain 1.x 推荐的智能体构建方式,返回一个可执行的状态图\n",
|
||||
"agent = create_agent(\n",
|
||||
" model=llm,\n",
|
||||
" tools=tools,\n",
|
||||
" system_prompt='你是一个 helpful 的数学和天气助手,可以使用工具帮助用户。'\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 运行智能体\n",
|
||||
"result = agent.invoke({'messages': [('user', '3 加 5 乘以 2 等于多少?')]})\n",
|
||||
"print('\\n最终输出:', result['messages'][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `create_agent(model=llm, tools=tools, system_prompt='...')`:创建工具调用智能体\n",
|
||||
"- `create_agent` 会自动处理工具调用循环:模型选择工具 -> 执行工具 -> 返回结果 -> 再次调用模型\n",
|
||||
"- 返回的 `agent` 是一个 LangGraph 编译后的状态图(CompiledStateGraph),可以直接调用 `invoke`\n",
|
||||
"- 调用时传入 `{'messages': [('user', '...')]}`,表示用户消息\n",
|
||||
"- `result['messages'][-1].content`:获取最后一条消息,即智能体的最终回答"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 观察智能体的执行过程\n",
|
||||
"\n",
|
||||
"`create_agent` 返回的是一个 LangGraph 状态图。运行后,`result['messages']` 会包含完整的对话历史,包括模型的思考过程、工具调用请求、工具执行结果和最终回答。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 同样的智能体,换个问题运行\n",
|
||||
"result = agent.invoke({'messages': [('user', '北京今天天气怎么样?广州呢?')]})\n",
|
||||
"for i in result['messages']:\n",
|
||||
" print(i)\n",
|
||||
"print('\\n最终输出:', result['messages'][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 多轮对话:带记忆的智能体\n",
|
||||
"\n",
|
||||
"如果希望智能体能记住上下文,可以在 `create_agent` 中传入 `checkpointer`。这里使用 `MemorySaver` 作为内存型的 checkpointer。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"# 创建带记忆的智能体\n",
|
||||
"# checkpointer 会自动保存和加载对话历史\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"agent_with_history = create_agent(\n",
|
||||
" model=llm,\n",
|
||||
" tools=tools,\n",
|
||||
" system_prompt='你是一个 helpful 的数学和天气助手,可以使用工具帮助用户。',\n",
|
||||
" checkpointer=memory\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 第一轮对话\n",
|
||||
"config = {'configurable': {'thread_id': 'user_001'}}\n",
|
||||
"result1 = agent_with_history.invoke(\n",
|
||||
" {'messages': [('user', '我叫张三')]},\n",
|
||||
" config=config\n",
|
||||
")\n",
|
||||
"print('第一轮:', result1['messages'][-1].content)\n",
|
||||
"\n",
|
||||
"# 第二轮对话,测试是否记得名字\n",
|
||||
"result2 = agent_with_history.invoke(\n",
|
||||
" {'messages': [('user', '我叫什么名字?')]},\n",
|
||||
" config=config\n",
|
||||
")\n",
|
||||
"print('第二轮:', result2['messages'][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `MemorySaver()`:内存型 checkpointer,用于保存对话历史\n",
|
||||
"- `checkpointer=memory`:把 checkpointer 传给 `create_agent`,让智能体具备记忆能力\n",
|
||||
"- `config={'configurable': {'thread_id': 'user_001'}}`:通过 thread_id 区分不同会话\n",
|
||||
"- 同一个 `thread_id` 下的历史消息会被保留并传给模型"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 处理工具调用错误\n",
|
||||
"\n",
|
||||
"工具执行过程中可能出现异常,比如参数错误、网络超时、文件不存在等。好的智能体需要能优雅处理这些错误。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def safe_divide(a: float, b: float) -> str:\n",
|
||||
" \"\"\"安全除法,除数为0时返回友好提示。\"\"\"\n",
|
||||
" try:\n",
|
||||
" if b == 0:\n",
|
||||
" return '错误:除数不能为0'\n",
|
||||
" return str(a / b)\n",
|
||||
" except Exception as e:\n",
|
||||
" return f'计算出错:{str(e)}'\n",
|
||||
"\n",
|
||||
"safe_tools = [safe_divide, add, multiply]\n",
|
||||
"\n",
|
||||
"safe_agent = create_agent(\n",
|
||||
" model=llm,\n",
|
||||
" tools=safe_tools,\n",
|
||||
" system_prompt='你是一个 helpful 的数学助手,可以使用工具帮助用户。'\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"result = safe_agent.invoke({'messages': [('user', '10 除以 0 等于多少?')]})\n",
|
||||
"print('\\n最终输出:', result['messages'][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- 工具内部用 try-except 捕获异常,返回错误信息而不是抛出异常\n",
|
||||
"- 这样模型能看到错误原因,并据此调整或告知用户\n",
|
||||
"- 不要把异常直接抛给 AgentExecutor,否则会导致智能体运行中断"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 完整示例:智能客服助手\n",
|
||||
"\n",
|
||||
"下面把工具调用应用到智能客服场景:根据用户问题查询订单、库存或计算退款金额。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain.tools import tool\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain.agents import create_agent\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def query_order(order_id: str) -> str:\n",
|
||||
" \"\"\"根据订单号查询订单信息。\"\"\"\n",
|
||||
" orders = {\n",
|
||||
" '1001': '已发货,预计明天送达',\n",
|
||||
" '1002': '已签收',\n",
|
||||
" '1003': '处理中'\n",
|
||||
" }\n",
|
||||
" return orders.get(order_id, '未找到该订单')\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def query_stock(product_name: str) -> str:\n",
|
||||
" \"\"\"查询商品库存。\"\"\"\n",
|
||||
" stock = {'手机': 100, '耳机': 50, '充电器': 200}\n",
|
||||
" return f'{product_name} 库存:{stock.get(product_name, 0)} 件'\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def calculate_refund(price: float, days: int) -> str:\n",
|
||||
" \"\"\"计算退款金额,每天折旧 1%。\"\"\"\n",
|
||||
" refund = price * max(0, 1 - days * 0.01)\n",
|
||||
" return f'预计退款金额:{refund:.2f} 元'\n",
|
||||
"\n",
|
||||
"cs_tools = [query_order, query_stock, calculate_refund]\n",
|
||||
"\n",
|
||||
"cs_agent = create_agent(\n",
|
||||
" model=llm,\n",
|
||||
" tools=cs_tools,\n",
|
||||
" system_prompt='你是电商客服助手,可以通过工具查询订单、库存和计算退款。'\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"cs_result = cs_agent.invoke({\n",
|
||||
" 'messages': [('user', '我的订单 1001 现在状态怎样?手机还有库存吗?如果手机 3000 元买了 10 天,退款多少?')]\n",
|
||||
"})\n",
|
||||
"print('\\n最终输出:', cs_result['messages'][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 手动执行一次工具调用:让模型选择 `add` 或 `multiply`,然后自己解析 `tool_calls` 并执行\n",
|
||||
"2. 使用 `create_tool_calling_agent` 和 `AgentExecutor` 构建一个能计算 BMI 的工具智能体\n",
|
||||
"3. 给智能体添加 `verbose=True`,观察它执行工具调用的完整过程\n",
|
||||
"4. 修改一个工具,让它在参数错误时返回友好提示,而不是抛出异常\n",
|
||||
"5. 使用 `RunnableWithMessageHistory` 让智能体记住用户的连续提问"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
505
11_检索增强.ipynb
Normal file
505
11_检索增强.ipynb
Normal file
@@ -0,0 +1,505 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 11 检索增强\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解 RAG(Retrieval-Augmented Generation,检索增强生成)的核心原理\n",
|
||||
"2. 掌握文档加载、文本分割、向量化的基本流程\n",
|
||||
"3. 学会使用 LangChain 构建简单的 RAG 问答链\n",
|
||||
"4. 理解检索器(Retriever)在智能体中的作用\n",
|
||||
"5. 能够处理本地文本数据并基于其回答问题"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要 RAG\n",
|
||||
"\n",
|
||||
"大语言模型有两个明显局限:\n",
|
||||
"\n",
|
||||
"- **知识过时**:模型训练数据有截止日期,不知道最新信息\n",
|
||||
"- **容易产生幻觉**:对未训练过的问题可能编造答案\n",
|
||||
"\n",
|
||||
"**RAG** 通过在回答前先从外部知识库中检索相关信息,把检索到的内容作为上下文喂给模型,从而:\n",
|
||||
"\n",
|
||||
"- 让模型基于最新、准确的资料回答\n",
|
||||
"- 减少幻觉\n",
|
||||
"- 实现私有知识库问答(如公司文档、个人笔记)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. RAG 的核心流程\n",
|
||||
"\n",
|
||||
"一个完整的 RAG 系统通常包含以下步骤:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"1. 加载文档 -> DocumentLoader\n",
|
||||
"2. 分割文档 -> TextSplitter\n",
|
||||
"3. 文本向量化 -> Embeddings\n",
|
||||
"4. 存入向量库 -> VectorStore\n",
|
||||
"5. 用户提问 -> Query\n",
|
||||
"6. 检索相关片段 -> Retriever\n",
|
||||
"7. 拼接上下文 -> Context\n",
|
||||
"8. 生成回答 -> LLM\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"其中第 1-4 步是「离线」的索引阶段,第 5-8 步是「在线」的查询阶段。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 环境准备\n",
|
||||
"\n",
|
||||
"本节课会用到文档加载、文本分割组件。如果你的虚拟环境中还没有安装,请先执行:\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"pip install langchain-community langchain-text-splitters -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"> 如果暂时不想安装,本课件也提供了用 Python 原生代码实现的备用方案。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 准备示例文档\n",
|
||||
"\n",
|
||||
"先创建一个示例文本文件,后面会用它来演示 RAG 流程。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"sample_text = \"\"\"\n",
|
||||
"人工智能(Artificial Intelligence,简称 AI)是计算机科学的一个分支,旨在创建能够执行通常需要人类智能才能完成的任务的系统。\n",
|
||||
"\n",
|
||||
"机器学习是人工智能的一个重要子领域。它通过数据训练模型,使计算机能够从经验中学习,而无需进行明确的编程。\n",
|
||||
"\n",
|
||||
"深度学习是机器学习的一个分支,使用多层神经网络来模拟人脑的工作方式。深度学习在图像识别、自然语言处理和语音识别等领域取得了显著成果。\n",
|
||||
"\n",
|
||||
"自然语言处理(Natural Language Processing,简称 NLP)是人工智能和语言学的交叉领域,研究如何让计算机理解、解释和生成人类语言。\n",
|
||||
"\n",
|
||||
"大语言模型(Large Language Model,简称 LLM)是基于深度学习的自然语言处理模型。它们通过在海量文本数据上进行预训练,能够生成连贯的文本、回答问题并完成多种语言任务。\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"with open('sample_rag.txt', 'w', encoding='utf-8') as f:\n",
|
||||
" f.write(sample_text.strip())\n",
|
||||
"\n",
|
||||
"print('示例文档已创建:sample_rag.txt')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 5.1 使用 LangChain 的 TextLoader(推荐)\n",
|
||||
"\n",
|
||||
"`TextLoader` 可以自动处理编码、元数据等问题。\n",
|
||||
"\n",
|
||||
"> 注意:`langchain-community` 在 LangChain 1.x 中已被标记为弃用,但仍然可用。代码中用 `warnings.filterwarnings('ignore', category=DeprecationWarning)` 过滤了警告。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import warnings\n",
|
||||
"\n",
|
||||
"# langchain-community 在 LangChain 1.x 中已被标记为弃用,但仍然可用\n",
|
||||
"# 这里过滤掉 DeprecationWarning,避免输出中显示警告信息\n",
|
||||
"warnings.filterwarnings('ignore', category=DeprecationWarning)\n",
|
||||
"\n",
|
||||
"from langchain_community.document_loaders import TextLoader\n",
|
||||
"\n",
|
||||
"loader = TextLoader('sample_rag.txt', encoding='utf-8')\n",
|
||||
"documents = loader.load()\n",
|
||||
"\n",
|
||||
"print(f'加载了 {len(documents)} 个文档')\n",
|
||||
"print(f'第一个文档长度:{len(documents[0].page_content)} 字符')\n",
|
||||
"print(f'元数据:{documents[0].metadata}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 5.2 原生 Python 加载方式(备用)\n",
|
||||
"\n",
|
||||
"如果暂时不想安装 `langchain-community`,可以直接用 Python 读取文件。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.documents import Document\n",
|
||||
"\n",
|
||||
"with open('sample_rag.txt', 'r', encoding='utf-8') as f:\n",
|
||||
" text = f.read()\n",
|
||||
"\n",
|
||||
"documents = [Document(page_content=text, metadata={'source': 'sample_rag.txt'})]\n",
|
||||
"print(f'加载了 {len(documents)} 个文档,长度:{len(documents[0].page_content)} 字符')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 文档分割\n",
|
||||
"\n",
|
||||
"大模型一次能处理的文本长度有限,而且过长的文档会影响检索精度。因此需要把长文档切分成小块。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
|
||||
"\n",
|
||||
"# 创建一个文本分割器\n",
|
||||
"# chunk_size: 每个块的最大字符数\n",
|
||||
"# chunk_overlap: 相邻块之间的重叠字符数,用于保持上下文连贯\n",
|
||||
"splitter = RecursiveCharacterTextSplitter(\n",
|
||||
" chunk_size=100,\n",
|
||||
" chunk_overlap=20,\n",
|
||||
" separators=['\\n\\n', '\\n', '。', ',', ' ', '']\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"chunks = splitter.split_documents(documents)\n",
|
||||
"\n",
|
||||
"print(f'分割成 {len(chunks)} 个文本块')\n",
|
||||
"for i, chunk in enumerate(chunks[:3]):\n",
|
||||
" print(f'\\n--- 第 {i+1} 块 ---')\n",
|
||||
" print(chunk.page_content[:80] + '...')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `chunk_size`:每个文本块的最大长度\n",
|
||||
"- `chunk_overlap`:相邻块重叠的字符数,避免关键信息被切分断掉\n",
|
||||
"- `separators`:切分优先级,先按段落切,再按句子切,最后按字符切\n",
|
||||
"- `RecursiveCharacterTextSplitter` 会尽量在语义边界处切分"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 文本向量化\n",
|
||||
"\n",
|
||||
"向量化是把文本转换成数值向量的过程。语义相近的文本,向量距离也更近。\n",
|
||||
"\n",
|
||||
"本课件使用 `OpenAIEmbeddings`。如果你的 API 不支持 `qwen3-embedding`,请把 `model` 参数改成你实际可用的 embedding 模型名。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import OpenAIEmbeddings\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# 创建 embedding 模型\n",
|
||||
"# 如果你的 API 提供的是其他 embedding 模型名,请修改 model 参数\n",
|
||||
"embeddings = OpenAIEmbeddings(model='qwen3-embedding')\n",
|
||||
"\n",
|
||||
"# 测试向量化\n",
|
||||
"test_text = '人工智能是计算机科学的分支'\n",
|
||||
"vector = embeddings.embed_query(test_text)\n",
|
||||
"\n",
|
||||
"print(f'文本:{test_text}')\n",
|
||||
"print(f'向量维度:{len(vector)}')\n",
|
||||
"print(f'向量前5个值:{vector[:5]}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 备用方案:使用 FakeEmbeddings 测试流程\n",
|
||||
"\n",
|
||||
"如果你暂时没有可用的 embedding 接口,可以用 `FakeEmbeddings` 仅测试 RAG 流程。注意:FakeEmbeddings 生成的向量是随机的,检索结果没有实际语义意义。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.embeddings import FakeEmbeddings\n",
|
||||
"\n",
|
||||
"# 仅用于测试流程,向量维度设为 384\n",
|
||||
"fake_embeddings = FakeEmbeddings(size=384)\n",
|
||||
"\n",
|
||||
"fake_vector = fake_embeddings.embed_query('测试文本')\n",
|
||||
"print(f'Fake 向量维度:{len(fake_vector)}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 向量存储与检索\n",
|
||||
"\n",
|
||||
"把分割后的文本块存入向量数据库。这里使用 `InMemoryVectorStore`,它是一个内存型向量存储,适合学习和测试。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.vectorstores import InMemoryVectorStore\n",
|
||||
"\n",
|
||||
"# 创建内存向量库\n",
|
||||
"vectorstore = InMemoryVectorStore(embeddings)\n",
|
||||
"\n",
|
||||
"# 把文本块加入向量库\n",
|
||||
"vectorstore.add_documents(chunks)\n",
|
||||
"\n",
|
||||
"# 创建检索器,k=2 表示返回最相关的 2 个片段\n",
|
||||
"retriever = vectorstore.as_retriever(search_kwargs={'k': 2})\n",
|
||||
"\n",
|
||||
"# 测试检索\n",
|
||||
"query = '什么是深度学习?'\n",
|
||||
"retrieved_docs = retriever.invoke(query)\n",
|
||||
"\n",
|
||||
"print(f'检索到 {len(retrieved_docs)} 个相关片段:\\n')\n",
|
||||
"for i, doc in enumerate(retrieved_docs):\n",
|
||||
" print(f'--- 片段 {i+1} ---')\n",
|
||||
" print(doc.page_content)\n",
|
||||
" print(f'来源:{doc.metadata}\\n')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `InMemoryVectorStore(embeddings)`:创建内存向量库,使用指定的 embedding 模型\n",
|
||||
"- `add_documents(chunks)`:把文本块存入向量库\n",
|
||||
"- `as_retriever(search_kwargs={'k': 2})`:把向量库转成检索器,每次返回前 2 个最相关结果\n",
|
||||
"- `retriever.invoke(query)`:根据用户问题检索相关文本块"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 构建 RAG 问答链\n",
|
||||
"\n",
|
||||
"把检索器和 LLM 组合起来,构建一个完整的 RAG 链。流程是:\n",
|
||||
"\n",
|
||||
"1. 接收用户问题\n",
|
||||
"2. 用检索器找到相关文档\n",
|
||||
"3. 把文档内容拼接到 Prompt 中\n",
|
||||
"4. 让 LLM 基于上下文回答"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnablePassthrough\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"# 定义 RAG Prompt 模板\n",
|
||||
"rag_prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是一个知识库问答助手。请根据下面的上下文回答问题,如果上下文没有相关信息,请说「我不知道」。'),\n",
|
||||
" ('user', '''上下文:\\n{context}\\n\\n问题:{question}''')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# 把检索到的文档拼接成字符串\n",
|
||||
"def format_docs(docs):\n",
|
||||
" return '\\n\\n'.join([doc.page_content for doc in docs])\n",
|
||||
"\n",
|
||||
"# 构建 RAG 链\n",
|
||||
"rag_chain = (\n",
|
||||
" {'context': retriever | format_docs, 'question': RunnablePassthrough()}\n",
|
||||
" | rag_prompt\n",
|
||||
" | llm\n",
|
||||
" | StrOutputParser()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 测试\n",
|
||||
"question = '深度学习和大语言模型有什么关系?'\n",
|
||||
"answer = rag_chain.invoke(question)\n",
|
||||
"print('问题:', question)\n",
|
||||
"print('\\n回答:', answer)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `{'context': retriever | format_docs, 'question': RunnablePassthrough()}`:\n",
|
||||
" - `context` 分支:用检索器找到相关文档,再用 `format_docs` 拼接成字符串\n",
|
||||
" - `question` 分支:用 `RunnablePassthrough()` 把原始问题传递下去\n",
|
||||
"- `rag_prompt`:把 context 和 question 填充到模板中\n",
|
||||
"- `llm`:让模型基于上下文生成回答\n",
|
||||
"- `StrOutputParser()`:把 AIMessage 转成字符串"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 完整示例:基于私有文档的问答系统\n",
|
||||
"\n",
|
||||
"下面把前面的步骤整合成一个完整的 RAG 系统。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_community.document_loaders import TextLoader\n",
|
||||
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
|
||||
"from langchain_core.vectorstores import InMemoryVectorStore\n",
|
||||
"from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnablePassthrough\n",
|
||||
"\n",
|
||||
"# 1. 加载文档\n",
|
||||
"loader = TextLoader('sample_rag.txt', encoding='utf-8')\n",
|
||||
"documents = loader.load()\n",
|
||||
"\n",
|
||||
"# 2. 分割文档\n",
|
||||
"splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)\n",
|
||||
"chunks = splitter.split_documents(documents)\n",
|
||||
"\n",
|
||||
"# 3. 创建 embedding 和向量库\n",
|
||||
"embeddings = OpenAIEmbeddings(model='qwen3-embedding')\n",
|
||||
"vectorstore = InMemoryVectorStore(embeddings)\n",
|
||||
"vectorstore.add_documents(chunks)\n",
|
||||
"\n",
|
||||
"# 4. 创建检索器\n",
|
||||
"retriever = vectorstore.as_retriever(search_kwargs={'k': 2})\n",
|
||||
"\n",
|
||||
"# 5. 创建 LLM 和 Prompt\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是知识库问答助手,请严格根据上下文回答。'),\n",
|
||||
" ('user', '''上下文:\\n{context}\\n\\n问题:{question}''')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"def format_docs(docs):\n",
|
||||
" return '\\n\\n'.join([doc.page_content for doc in docs])\n",
|
||||
"\n",
|
||||
"# 6. 构建 RAG 链\n",
|
||||
"qa_chain = (\n",
|
||||
" {'context': retriever | format_docs, 'question': RunnablePassthrough()}\n",
|
||||
" | prompt\n",
|
||||
" | llm\n",
|
||||
" | StrOutputParser()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 7. 提问\n",
|
||||
"questions = [\n",
|
||||
" '机器学习是什么?',\n",
|
||||
" '自然语言处理和大语言模型有什么关系?',\n",
|
||||
" '苹果是什么颜色的?' # 这个问题不在文档中,观察模型是否说不知道\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for q in questions:\n",
|
||||
" print(f'Q: {q}')\n",
|
||||
" print(f'A: {qa_chain.invoke(q)}\\n')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. RAG 系统的优化方向\n",
|
||||
"\n",
|
||||
"实际应用中,RAG 系统还有很多优化空间:\n",
|
||||
"\n",
|
||||
"| 优化方向 | 说明 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| **更好的分块策略** | 按语义、按段落、按标题切分,避免切断关键信息 |\n",
|
||||
"| **更优的 Embedding 模型** | 使用针对中文优化的模型,如 BGE、M3E 等 |\n",
|
||||
"| **重排序(Rerank)** | 先检索多个候选,再用更精确的模型排序 |\n",
|
||||
"| **查询改写** | 把用户问题改写成更适合检索的形式 |\n",
|
||||
"| **持久化向量库** | 使用 Chroma、FAISS、Milvus 等持久化存储 |\n",
|
||||
"| **元数据过滤** | 根据来源、时间、类别等元数据进行筛选 |"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 创建一个新的文本文件,写入关于你感兴趣的主题(如 Python、篮球、电影),用 TextLoader 加载并分割\n",
|
||||
"2. 调整 `chunk_size` 和 `chunk_overlap`,观察分割结果的变化\n",
|
||||
"3. 用 `OpenAIEmbeddings` 或 `FakeEmbeddings` 把文本块存入 `InMemoryVectorStore`\n",
|
||||
"4. 针对你的文档提出 3 个问题,观察 RAG 链的回答质量\n",
|
||||
"5. 尝试问一个文档中没有的问题,观察模型是否按 Prompt 要求回答「我不知道」"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
398
12_向量数据库.ipynb
Normal file
398
12_向量数据库.ipynb
Normal file
@@ -0,0 +1,398 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 12 向量数据库\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解向量数据库与传统数据库的区别\n",
|
||||
"2. 掌握 ChromaDB 的基本使用方法\n",
|
||||
"3. 学会向量数据库的持久化存储和加载\n",
|
||||
"4. 理解元数据过滤在检索中的作用\n",
|
||||
"5. 能够选择合适的向量数据库方案"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要向量数据库\n",
|
||||
"\n",
|
||||
"上一节课我们用 `InMemoryVectorStore` 把向量存在内存中,它适合学习和测试,但有几个明显缺点:\n",
|
||||
"\n",
|
||||
"- **数据无法持久化**:程序关闭后向量就消失了\n",
|
||||
"- **无法增量更新**:不能方便地添加、删除文档\n",
|
||||
"- **不支持复杂查询**:无法按元数据过滤\n",
|
||||
"- **不适合大规模数据**:内存容量有限\n",
|
||||
"\n",
|
||||
"**向量数据库** 就是为解决这些问题而生的专用数据库。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 向量数据库 vs 传统数据库\n",
|
||||
"\n",
|
||||
"| 特性 | 传统数据库(MySQL) | 向量数据库(ChromaDB) |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 存储内容 | 结构化数据 | 向量 + 原始文本 + 元数据 |\n",
|
||||
"| 查询方式 | 精确匹配、范围查询 | 相似度搜索 |\n",
|
||||
"| 索引类型 | B+ 树 | HNSW、IVF 等近似最近邻索引 |\n",
|
||||
"| 典型应用 | 订单、用户管理 | 语义搜索、RAG、推荐 |\n",
|
||||
"\n",
|
||||
"向量数据库的核心能力:**根据语义相似度快速找到最相关的向量**。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 核心概念\n",
|
||||
"\n",
|
||||
"### 3.1 向量(Vector)\n",
|
||||
"文本经过 Embedding 模型编码后变成的高维数组。例如一个 2560 维的浮点数数组。\n",
|
||||
"\n",
|
||||
"### 3.2 相似度(Similarity)\n",
|
||||
"两个向量之间的距离。常用度量方式:\n",
|
||||
"- **余弦相似度(Cosine Similarity)**:衡量方向是否一致,范围 -1 到 1\n",
|
||||
"- **欧氏距离(Euclidean Distance)**:衡量向量空间中的直线距离\n",
|
||||
"\n",
|
||||
"### 3.3 索引(Index)\n",
|
||||
"为了加速海量向量的相似度搜索,向量数据库会构建专门的索引结构,如 HNSW。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 常见向量数据库\n",
|
||||
"\n",
|
||||
"| 数据库 | 特点 | 适用场景 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| **ChromaDB** | 轻量、易用、支持持久化 | 本地开发、中小规模 |\n",
|
||||
"| **FAISS** | Meta 开源、高性能 | 大规模向量检索 |\n",
|
||||
"| **Milvus** | 企业级、分布式 | 生产环境、海量数据 |\n",
|
||||
"| **Pinecone** | 云端托管、无需运维 | 快速上线、Serverless |\n",
|
||||
"| **Weaviate** | 支持多模态、GraphQL | 复杂查询、多模态 |\n",
|
||||
"\n",
|
||||
"本节课以 **ChromaDB** 为例,因为它安装简单、接口友好,非常适合学习。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 安装 ChromaDB\n",
|
||||
"\n",
|
||||
"在虚拟环境中执行:\n",
|
||||
"\n",
|
||||
"```powershell\n",
|
||||
"pip install chromadb -i https://pypi.tuna.tsinghua.edu.cn/simple\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"安装完成后需要**重启 Jupyter 内核**。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 使用 ChromaDB 创建向量库\n",
|
||||
"\n",
|
||||
"下面演示如何把文档存入 ChromaDB 并进行检索。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import warnings\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"warnings.filterwarnings('ignore', category=DeprecationWarning)\n",
|
||||
"\n",
|
||||
"from langchain_community.vectorstores import Chroma\n",
|
||||
"from langchain_core.documents import Document\n",
|
||||
"from langchain_openai import OpenAIEmbeddings\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# 创建 embedding 模型\n",
|
||||
"embeddings = OpenAIEmbeddings(\n",
|
||||
" model='qwen3-embedding',\n",
|
||||
" openai_api_base=os.environ.get('OPENAI_BASE_URL'),\n",
|
||||
" openai_api_key=os.environ.get('OPENAI_API_KEY')\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 准备文档\n",
|
||||
"documents = [\n",
|
||||
" Document(page_content='人工智能是计算机科学的一个分支。', metadata={'source': 'ai_book', 'category': 'AI'}),\n",
|
||||
" Document(page_content='深度学习使用多层神经网络。', metadata={'source': 'ai_book', 'category': 'AI'}),\n",
|
||||
" Document(page_content='机器学习让计算机从数据中学习。', metadata={'source': 'ai_book', 'category': 'AI'}),\n",
|
||||
" Document(page_content='香蕉是一种黄色的水果。', metadata={'source': 'food_book', 'category': 'food'}),\n",
|
||||
" Document(page_content='苹果通常是红色或绿色的。', metadata={'source': 'food_book', 'category': 'food'})\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# 从文档创建 ChromaDB 向量库\n",
|
||||
"# persist_directory 指定数据保存目录\n",
|
||||
"vectorstore = Chroma.from_documents(\n",
|
||||
" documents=documents,\n",
|
||||
" embedding=embeddings,\n",
|
||||
" persist_directory='./chroma_db_demo'\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print('向量库创建成功!')\n",
|
||||
"print(f'文档数量:{vectorstore._collection.count()}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `Chroma.from_documents()`:从 Document 列表直接创建向量库\n",
|
||||
"- `embedding=embeddings`:指定向量化模型\n",
|
||||
"- `persist_directory`:数据持久化目录,程序关闭后数据仍存在"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 相似度检索\n",
|
||||
"\n",
|
||||
"创建检索器后,就可以根据用户问题进行语义搜索。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 创建检索器\n",
|
||||
"retriever = vectorstore.as_retriever(search_kwargs={'k': 3})\n",
|
||||
"\n",
|
||||
"# 检索与问题最相关的文档\n",
|
||||
"query = '什么是人工智能?'\n",
|
||||
"results = retriever.invoke(query)\n",
|
||||
"\n",
|
||||
"print(f'问题:{query}\\n')\n",
|
||||
"for i, doc in enumerate(results):\n",
|
||||
" print(f'--- 结果 {i+1} ---')\n",
|
||||
" print(f'内容:{doc.page_content}')\n",
|
||||
" print(f'元数据:{doc.metadata}\\n')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 持久化加载\n",
|
||||
"\n",
|
||||
"ChromaDB 的数据已经保存在 `./chroma_db_demo` 目录中。下次启动程序时,可以直接加载,不需要重新向量化。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 从已有目录加载向量库\n",
|
||||
"loaded_vectorstore = Chroma(\n",
|
||||
" persist_directory='./chroma_db_demo',\n",
|
||||
" embedding_function=embeddings\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"loaded_retriever = loaded_vectorstore.as_retriever(search_kwargs={'k': 2})\n",
|
||||
"results = loaded_retriever.invoke('深度学习和神经网络的关系')\n",
|
||||
"\n",
|
||||
"print('从持久化数据库加载后检索:')\n",
|
||||
"for doc in results:\n",
|
||||
" print(f'- {doc.page_content}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 元数据过滤\n",
|
||||
"\n",
|
||||
"元数据过滤可以让我们在检索时只搜索特定类别的文档。例如只搜索 `category='AI'` 的文档。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 只检索 category 为 AI 的文档\n",
|
||||
"filtered_results = loaded_vectorstore.similarity_search(\n",
|
||||
" '水果的颜色',\n",
|
||||
" filter={'category': 'AI'},\n",
|
||||
" k=3\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print('过滤后只搜索 AI 类文档:')\n",
|
||||
"for doc in filtered_results:\n",
|
||||
" print(f'- [{doc.metadata[\"category\"]}] {doc.page_content}')\n",
|
||||
"\n",
|
||||
"# 只检索 category 为 food 的文档\n",
|
||||
"food_results = loaded_vectorstore.similarity_search(\n",
|
||||
" '人工智能',\n",
|
||||
" filter={'category': 'food'},\n",
|
||||
" k=3\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print('\\n过滤后只搜索 food 类文档:')\n",
|
||||
"for doc in food_results:\n",
|
||||
" print(f'- [{doc.metadata[\"category\"]}] {doc.page_content}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `filter={'category': 'AI'}`:只返回 category 字段等于 AI 的文档\n",
|
||||
"- 即使查询词是「水果的颜色」,由于过滤条件限制,也只会返回 AI 类文档\n",
|
||||
"- 元数据过滤常用于多租户、多知识库场景"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 增量更新:添加和删除文档\n",
|
||||
"\n",
|
||||
"向量数据库支持动态增删文档,不需要每次重新构建整个库。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 添加新文档\n",
|
||||
"new_docs = [\n",
|
||||
" Document(page_content='自然语言处理让计算机理解人类语言。', metadata={'source': 'ai_book', 'category': 'AI'})\n",
|
||||
"]\n",
|
||||
"vectorstore.add_documents(new_docs)\n",
|
||||
"print(f'添加后文档数量:{vectorstore._collection.count()}')\n",
|
||||
"\n",
|
||||
"# 查询刚添加的文档\n",
|
||||
"results = vectorstore.similarity_search('NLP 是什么', k=2)\n",
|
||||
"for doc in results:\n",
|
||||
" print(f'- {doc.page_content}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. 完整示例:基于 ChromaDB 的本地知识库问答\n",
|
||||
"\n",
|
||||
"把第 11 节课的 RAG 链和 ChromaDB 结合起来,构建一个可持久化的本地知识库。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnablePassthrough\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是知识库问答助手,请根据上下文回答问题。如果不知道,请说「我不知道」。'),\n",
|
||||
" ('user', '''上下文:\\n{context}\\n\\n问题:{question}''')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"def format_docs(docs):\n",
|
||||
" return '\\n\\n'.join([doc.page_content for doc in docs])\n",
|
||||
"\n",
|
||||
"# 使用 ChromaDB 检索器\n",
|
||||
"chroma_retriever = loaded_vectorstore.as_retriever(search_kwargs={'k': 2})\n",
|
||||
"\n",
|
||||
"qa_chain = (\n",
|
||||
" {'context': chroma_retriever | format_docs, 'question': RunnablePassthrough()}\n",
|
||||
" | prompt\n",
|
||||
" | llm\n",
|
||||
" | StrOutputParser()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"question = '深度学习是什么?'\n",
|
||||
"answer = qa_chain.invoke(question)\n",
|
||||
"print(f'Q: {question}')\n",
|
||||
"print(f'A: {answer}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12. 向量数据库选型建议\n",
|
||||
"\n",
|
||||
"| 场景 | 推荐方案 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| 本地学习、原型开发 | ChromaDB / InMemoryVectorStore |\n",
|
||||
"| 大规模生产环境 | Milvus / FAISS |\n",
|
||||
"| 不想自己运维 | Pinecone / Weaviate Cloud |\n",
|
||||
"| 需要多模态检索 | Weaviate |\n",
|
||||
"| 已有 Elasticsearch 集群 | Elasticsearch 向量检索 |\n",
|
||||
"\n",
|
||||
"选择向量数据库时主要考虑:数据规模、查询性能、运维成本、是否支持元数据过滤。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 13. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 创建 5 条以上不同类别的文档,存入 ChromaDB,并指定 source 和 category 元数据\n",
|
||||
"2. 分别用无过滤和有过滤两种方式检索,对比结果差异\n",
|
||||
"3. 关闭 Jupyter 后重新打开,加载已持久化的 ChromaDB,验证数据没有丢失\n",
|
||||
"4. 尝试添加一条新文档,然后检索这条新文档相关的内容\n",
|
||||
"5. 用 ChromaDB 替换第 11 节课中的 InMemoryVectorStore,重新构建 RAG 链"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
465
13_RAG构建.ipynb
Normal file
465
13_RAG构建.ipynb
Normal file
@@ -0,0 +1,465 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 13 RAG 构建\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 掌握从零构建完整 RAG 应用的整体流程\n",
|
||||
"2. 学会将文档加载、分割、向量化、存储、检索、生成串联成系统\n",
|
||||
"3. 理解 RAG 系统的评估方法和常见优化策略\n",
|
||||
"4. 能够将 RAG 代码模块化封装,便于复用\n",
|
||||
"5. 完成一个可运行的多文档问答系统"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. RAG 应用整体架构\n",
|
||||
"\n",
|
||||
"一个可落地的 RAG 应用通常包含以下模块:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"数据准备阶段:\n",
|
||||
" 原始文档 -> 文档加载器 -> 文本分割器 -> 文本块\n",
|
||||
"\n",
|
||||
"索引阶段:\n",
|
||||
" 文本块 -> Embedding 模型 -> 向量 -> 向量数据库\n",
|
||||
"\n",
|
||||
"查询阶段:\n",
|
||||
" 用户问题 -> Embedding 模型 -> 向量检索 -> 相关文本块\n",
|
||||
" 相关文本块 + 用户问题 -> Prompt -> LLM -> 最终回答\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"本节课将以「公司产品手册问答系统」为例,完整演示上述流程。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 准备知识库文档\n",
|
||||
"\n",
|
||||
"先创建两份示例文档,模拟真实的产品手册。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# 创建产品手册目录\n",
|
||||
"os.makedirs('product_docs', exist_ok=True)\n",
|
||||
"\n",
|
||||
"# 文档 1:智能手表使用手册\n",
|
||||
"watch_manual = \"\"\"\n",
|
||||
"智能手表使用手册\n",
|
||||
"\n",
|
||||
"1. 开机与配对\n",
|
||||
"长按右侧电源键 3 秒开机。首次使用需在手机上下载 HealthApp,打开蓝牙后搜索设备并完成配对。\n",
|
||||
"\n",
|
||||
"2. 心率监测\n",
|
||||
"手表背面配备 PPG 心率传感器。在表盘界面下滑进入功能菜单,点击「心率」即可开始测量。测量时请保持手腕静止。\n",
|
||||
"\n",
|
||||
"3. 睡眠监测\n",
|
||||
"佩戴手表入睡后,系统会自动记录睡眠数据。次日可在 HealthApp 中查看深睡、浅睡和 REM 睡眠时长。\n",
|
||||
"\n",
|
||||
"4. 防水说明\n",
|
||||
"本手表支持 5ATM 防水,可佩戴游泳,但不适用于潜水、热水淋浴或桑拿。\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"# 文档 2:无线耳机使用手册\n",
|
||||
"earphone_manual = \"\"\"\n",
|
||||
"无线耳机使用手册\n",
|
||||
"\n",
|
||||
"1. 开机与配对\n",
|
||||
"打开充电盒,耳机自动开机并进入配对模式。在手机蓝牙设置中选择 SoundPod 即可完成配对。\n",
|
||||
"\n",
|
||||
"2. 触控操作\n",
|
||||
"单击左耳或右耳可播放/暂停音乐。双击右耳切换下一首,双击左耳切换上一首。长按 2 秒唤醒语音助手。\n",
|
||||
"\n",
|
||||
"3. 充电说明\n",
|
||||
"耳机单次续航 6 小时,充电盒可提供额外 24 小时续航。使用 Type-C 线缆为充电盒充电,约 1.5 小时充满。\n",
|
||||
"\n",
|
||||
"4. 降噪功能\n",
|
||||
"耳机支持主动降噪和环境音模式。在连接状态下,长按右耳 3 秒可在两种模式间切换。\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"with open('product_docs/watch_manual.txt', 'w', encoding='utf-8') as f:\n",
|
||||
" f.write(watch_manual.strip())\n",
|
||||
"\n",
|
||||
"with open('product_docs/earphone_manual.txt', 'w', encoding='utf-8') as f:\n",
|
||||
" f.write(earphone_manual.strip())\n",
|
||||
"\n",
|
||||
"print('产品手册已创建:')\n",
|
||||
"for f in os.listdir('product_docs'):\n",
|
||||
" print(f' - {f}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 完整 RAG 构建流程\n",
|
||||
"\n",
|
||||
"下面按照「加载 -> 分割 -> 向量化 -> 存储 -> 检索 -> 生成」的顺序,一步步构建系统。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import warnings\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"warnings.filterwarnings('ignore', category=DeprecationWarning)\n",
|
||||
"\n",
|
||||
"from langchain_community.document_loaders import DirectoryLoader, TextLoader\n",
|
||||
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
|
||||
"from langchain_openai import OpenAIEmbeddings\n",
|
||||
"from langchain_community.vectorstores import Chroma\n",
|
||||
"\n",
|
||||
"# 1. 加载目录下所有 txt 文件\n",
|
||||
"loader = DirectoryLoader(\n",
|
||||
" 'product_docs',\n",
|
||||
" glob='*.txt',\n",
|
||||
" loader_cls=TextLoader,\n",
|
||||
" loader_kwargs={'encoding': 'utf-8'}\n",
|
||||
")\n",
|
||||
"documents = loader.load()\n",
|
||||
"print(f'加载文档数:{len(documents)}')\n",
|
||||
"\n",
|
||||
"# 2. 分割文档\n",
|
||||
"splitter = RecursiveCharacterTextSplitter(\n",
|
||||
" chunk_size=150,\n",
|
||||
" chunk_overlap=30,\n",
|
||||
" separators=['\\n\\n', '\\n', '。', ' ']\n",
|
||||
")\n",
|
||||
"chunks = splitter.split_documents(documents)\n",
|
||||
"print(f'分割后文本块数:{len(chunks)}')\n",
|
||||
"\n",
|
||||
"# 3. 创建 embedding 模型\n",
|
||||
"embeddings = OpenAIEmbeddings(\n",
|
||||
" model='qwen3-embedding',\n",
|
||||
" openai_api_base=os.environ.get('OPENAI_BASE_URL'),\n",
|
||||
" openai_api_key=os.environ.get('OPENAI_API_KEY')\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 4. 存入 ChromaDB\n",
|
||||
"vectorstore = Chroma.from_documents(\n",
|
||||
" documents=chunks,\n",
|
||||
" embedding=embeddings,\n",
|
||||
" persist_directory='./product_knowledge_db'\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print('向量数据库创建成功!')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `DirectoryLoader`:批量加载目录下所有匹配文件\n",
|
||||
"- `glob='*.txt'`:只加载 txt 文件\n",
|
||||
"- `loader_cls=TextLoader`:指定使用 TextLoader 加载每个文件\n",
|
||||
"- `RecursiveCharacterTextSplitter`:按语义边界切分文档\n",
|
||||
"- `Chroma.from_documents`:一次性完成向量化和存储"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 构建问答链\n",
|
||||
"\n",
|
||||
"使用 LCEL 把检索器和 LLM 组合起来。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnablePassthrough\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '''你是公司产品客服助手。请严格根据提供的产品手册内容回答用户问题。\n",
|
||||
"如果手册中没有相关信息,请明确告知「根据现有手册,我无法回答这个问题」,不要编造。'''),\n",
|
||||
" ('user', '''相关手册内容:\\n{context}\\n\\n用户问题:{question}''')\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"def format_docs(docs):\n",
|
||||
" return '\\n\\n'.join([f'[来自 {doc.metadata[\"source\"]}] {doc.page_content}' for doc in docs])\n",
|
||||
"\n",
|
||||
"retriever = vectorstore.as_retriever(search_kwargs={'k': 3})\n",
|
||||
"\n",
|
||||
"qa_chain = (\n",
|
||||
" {'context': retriever | format_docs, 'question': RunnablePassthrough()}\n",
|
||||
" | prompt\n",
|
||||
" | llm\n",
|
||||
" | StrOutputParser()\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# 测试\n",
|
||||
"questions = [\n",
|
||||
" '智能手表怎么测心率?',\n",
|
||||
" '耳机的降噪功能怎么用?',\n",
|
||||
" '这款手表支持潜水吗?',\n",
|
||||
" '手机怎么连接打印机?' # 不在手册中\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for q in questions:\n",
|
||||
" print(f'Q: {q}')\n",
|
||||
" print(f'A: {qa_chain.invoke(q)}\\n')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 模块化封装\n",
|
||||
"\n",
|
||||
"把 RAG 系统封装成一个类,便于在项目中复用。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class ProductQASystem:\n",
|
||||
" \"\"\"基于 ChromaDB 的产品手册问答系统。\"\"\"\n",
|
||||
"\n",
|
||||
" def __init__(self, persist_dir, llm_model='qwen3.6-35b-A3b', embedding_model='qwen3-embedding'):\n",
|
||||
" self.embeddings = OpenAIEmbeddings(\n",
|
||||
" model=embedding_model,\n",
|
||||
" openai_api_base=os.environ.get('OPENAI_BASE_URL'),\n",
|
||||
" openai_api_key=os.environ.get('OPENAI_API_KEY')\n",
|
||||
" )\n",
|
||||
" self.vectorstore = Chroma(\n",
|
||||
" persist_directory=persist_dir,\n",
|
||||
" embedding_function=self.embeddings\n",
|
||||
" )\n",
|
||||
" self.llm = ChatOpenAI(model=llm_model, temperature=0.1)\n",
|
||||
" self.chain = self._build_chain()\n",
|
||||
"\n",
|
||||
" def _format_docs(self, docs):\n",
|
||||
" return '\\n\\n'.join([doc.page_content for doc in docs])\n",
|
||||
"\n",
|
||||
" def _build_chain(self):\n",
|
||||
" prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是产品客服助手,请根据手册内容回答问题。'),\n",
|
||||
" ('user', '''手册内容:\\n{context}\\n\\n问题:{question}''')\n",
|
||||
" ])\n",
|
||||
" retriever = self.vectorstore.as_retriever(search_kwargs={'k': 3})\n",
|
||||
" return (\n",
|
||||
" {'context': retriever | self._format_docs, 'question': RunnablePassthrough()}\n",
|
||||
" | prompt\n",
|
||||
" | self.llm\n",
|
||||
" | StrOutputParser()\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" def ask(self, question):\n",
|
||||
" \"\"\"回答用户问题。\"\"\"\n",
|
||||
" return self.chain.invoke(question)\n",
|
||||
"\n",
|
||||
" def add_documents(self, file_paths):\n",
|
||||
" \"\"\"增量添加新文档。\"\"\"\n",
|
||||
" loaders = [TextLoader(path, encoding='utf-8') for path in file_paths]\n",
|
||||
" docs = []\n",
|
||||
" for loader in loaders:\n",
|
||||
" docs.extend(loader.load())\n",
|
||||
" splitter = RecursiveCharacterTextSplitter(chunk_size=150, chunk_overlap=30)\n",
|
||||
" chunks = splitter.split_documents(docs)\n",
|
||||
" self.vectorstore.add_documents(chunks)\n",
|
||||
" return f'已添加 {len(chunks)} 个文本块'\n",
|
||||
"\n",
|
||||
"# 使用封装类\n",
|
||||
"qa_system = ProductQASystem('./product_knowledge_db')\n",
|
||||
"print(qa_system.ask('耳机续航多久?'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 评估检索效果\n",
|
||||
"\n",
|
||||
"RAG 系统的效果取决于两个环节:检索是否找到相关文档、生成是否基于文档准确回答。下面通过打印检索结果来评估检索环节。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 评估检索效果\n",
|
||||
"test_queries = [\n",
|
||||
" '智能手表怎么测心率?',\n",
|
||||
" '耳机怎么切换下一首歌?',\n",
|
||||
" '手表防水吗?',\n",
|
||||
" '耳机充电要多久?'\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for q in test_queries:\n",
|
||||
" print(f'\\n问题:{q}')\n",
|
||||
" docs = vectorstore.as_retriever(search_kwargs={'k': 2}).invoke(q)\n",
|
||||
" for i, doc in enumerate(docs):\n",
|
||||
" print(f' {i+1}. [{doc.metadata[\"source\"]}] {doc.page_content[:60]}...')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 常见问题与优化策略\n",
|
||||
"\n",
|
||||
"### 7.1 检索不到相关内容\n",
|
||||
"- 检查 embedding 模型是否正常工作\n",
|
||||
"- 调整 `chunk_size` 和 `chunk_overlap`\n",
|
||||
"- 尝试增加 `k` 值,返回更多候选\n",
|
||||
"\n",
|
||||
"### 7.2 回答包含错误信息\n",
|
||||
"- 加强 Prompt 中的约束,如「不知道就说不知道」\n",
|
||||
"- 检查检索片段是否包含答案\n",
|
||||
"- 使用更大的语言模型\n",
|
||||
"\n",
|
||||
"### 7.3 回答过长或偏离问题\n",
|
||||
"- 在 Prompt 中明确要求简洁回答\n",
|
||||
"- 限制 `max_tokens`\n",
|
||||
"\n",
|
||||
"### 7.4 多文档冲突\n",
|
||||
"- 在 `format_docs` 中标注每个片段的来源\n",
|
||||
"- 使用元数据过滤区分不同产品或版本"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 完整项目:多文档知识库问答系统\n",
|
||||
"\n",
|
||||
"下面把前面的代码整合成一个可复用的完整项目结构。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 完整流程整合\n",
|
||||
"import warnings\n",
|
||||
"warnings.filterwarnings('ignore', category=DeprecationWarning)\n",
|
||||
"\n",
|
||||
"from langchain_community.document_loaders import DirectoryLoader, TextLoader\n",
|
||||
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
|
||||
"from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
|
||||
"from langchain_community.vectorstores import Chroma\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.runnables import RunnablePassthrough\n",
|
||||
"\n",
|
||||
"def build_knowledge_base(docs_dir, db_dir):\n",
|
||||
" \"\"\"构建知识库:加载、分割、向量化、持久化。\"\"\"\n",
|
||||
" loader = DirectoryLoader(docs_dir, glob='*.txt', loader_cls=TextLoader, loader_kwargs={'encoding': 'utf-8'})\n",
|
||||
" documents = loader.load()\n",
|
||||
" splitter = RecursiveCharacterTextSplitter(chunk_size=150, chunk_overlap=30)\n",
|
||||
" chunks = splitter.split_documents(documents)\n",
|
||||
" embeddings = OpenAIEmbeddings(\n",
|
||||
" model='qwen3-embedding',\n",
|
||||
" openai_api_base=os.environ.get('OPENAI_BASE_URL'),\n",
|
||||
" openai_api_key=os.environ.get('OPENAI_API_KEY')\n",
|
||||
" )\n",
|
||||
" vectorstore = Chroma.from_documents(chunks, embeddings, persist_directory=db_dir)\n",
|
||||
" return vectorstore\n",
|
||||
"\n",
|
||||
"def build_qa_chain(vectorstore, k=3):\n",
|
||||
" \"\"\"构建 RAG 问答链。\"\"\"\n",
|
||||
" llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.1)\n",
|
||||
" prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是产品客服助手,请根据手册内容准确回答,不知道就说不知道。'),\n",
|
||||
" ('user', '''参考内容:\\n{context}\\n\\n问题:{question}''')\n",
|
||||
" ])\n",
|
||||
" retriever = vectorstore.as_retriever(search_kwargs={'k': k})\n",
|
||||
" def format_docs(docs):\n",
|
||||
" return '\\n\\n'.join([doc.page_content for doc in docs])\n",
|
||||
" return (\n",
|
||||
" {'context': retriever | format_docs, 'question': RunnablePassthrough()}\n",
|
||||
" | prompt\n",
|
||||
" | llm\n",
|
||||
" | StrOutputParser()\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"# 构建或加载知识库\n",
|
||||
"if not os.path.exists('product_knowledge_db'):\n",
|
||||
" vectorstore = build_knowledge_base('product_docs', 'product_knowledge_db')\n",
|
||||
"else:\n",
|
||||
" embeddings = OpenAIEmbeddings(\n",
|
||||
" model='qwen3-embedding',\n",
|
||||
" openai_api_base=os.environ.get('OPENAI_BASE_URL'),\n",
|
||||
" openai_api_key=os.environ.get('OPENAI_API_KEY')\n",
|
||||
" )\n",
|
||||
" vectorstore = Chroma(persist_directory='product_knowledge_db', embedding_function=embeddings)\n",
|
||||
"\n",
|
||||
"# 构建问答链并提问\n",
|
||||
"chain = build_qa_chain(vectorstore)\n",
|
||||
"print(chain.invoke('无线耳机怎么配对?'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 在 `product_docs` 目录下添加第三份产品手册(如「智能音箱使用手册」),重新构建知识库\n",
|
||||
"2. 修改 `format_docs` 函数,在 Prompt 中显示每个片段来自哪个文件\n",
|
||||
"3. 分别测试 `k=1`、`k=3`、`k=5` 时的回答质量差异\n",
|
||||
"4. 设计 5 个产品手册中的问题和 2 个手册外的问题,评估系统表现\n",
|
||||
"5. 把 `ProductQASystem` 类封装保存到单独的 `qa_system.py` 文件中,实现命令行问答"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
299
14_LangGraph概述.ipynb
Normal file
299
14_LangGraph概述.ipynb
Normal file
@@ -0,0 +1,299 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 14 LangGraph 概述\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解为什么需要 LangGraph:复杂智能体流程的控制需求\n",
|
||||
"2. 掌握 LangGraph 的核心概念:StateGraph、State、Node、Edge\n",
|
||||
"3. 理解 LangGraph 与 LangChain 的关系和分工\n",
|
||||
"4. 能够编写一个最简单的 LangGraph 状态图\n",
|
||||
"5. 了解 LangGraph 的典型应用场景"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要 LangGraph\n",
|
||||
"\n",
|
||||
"前面我们用 LangChain 构建了链式应用和简单的工具调用智能体。但当智能体流程变得复杂时,会遇到一些问题:\n",
|
||||
"\n",
|
||||
"- **流程不可控**:标准 Agent 的黑盒决策难以调试和约束\n",
|
||||
"- **循环和分支困难**:难以实现多轮循环、条件分支、人机协作\n",
|
||||
"- **状态管理复杂**:多步骤之间的状态传递不清晰\n",
|
||||
"- **缺乏持久化**:无法暂停、恢复或检查执行历史\n",
|
||||
"\n",
|
||||
"**LangGraph** 就是为了解决这些问题而设计的。它把智能体流程建模为一个**状态图(StateGraph)**,让开发者精确控制每个步骤。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. LangGraph 是什么\n",
|
||||
"\n",
|
||||
"LangGraph 是 LangChain 生态中的一个库,用于构建基于**状态图**的智能体应用。它的核心思想是:\n",
|
||||
"\n",
|
||||
"- 把应用看作一个图(Graph)\n",
|
||||
"- 图中的每个节点(Node)是一个函数或操作\n",
|
||||
"- 边(Edge)控制流程走向\n",
|
||||
"- 状态(State)在节点之间传递\n",
|
||||
"\n",
|
||||
"LangGraph 基于 LangChain 的 Runnable 接口构建,因此可以无缝使用 LangChain 的模型、工具、Prompt 等组件。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. LangGraph 与 LangChain 的关系\n",
|
||||
"\n",
|
||||
"| 对比项 | LangChain | LangGraph |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 核心抽象 | Chain / Agent | StateGraph |\n",
|
||||
"| 流程控制 | 线性或简单循环 | 任意图结构、条件分支、循环 |\n",
|
||||
"| 状态管理 | 隐式传递 | 显式 State 对象 |\n",
|
||||
"| 适用场景 | 简单流水线、单次调用 | 复杂多智能体、多轮交互 |\n",
|
||||
"| 关系 | 基础组件库 | 基于 LangChain 构建的流程编排层 |\n",
|
||||
"\n",
|
||||
"可以把 LangChain 理解为「零件库」,LangGraph 理解为「装配线控制系统」。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 核心概念\n",
|
||||
"\n",
|
||||
"### 4.1 State(状态)\n",
|
||||
"状态是整个图共享的数据结构。每个节点读取状态、修改状态,然后把状态传递给下一个节点。\n",
|
||||
"\n",
|
||||
"### 4.2 StateGraph(状态图)\n",
|
||||
"状态图是 LangGraph 最核心的类。它负责定义节点、边和状态类型。\n",
|
||||
"\n",
|
||||
"### 4.3 Node(节点)\n",
|
||||
"节点是图中的执行单元,通常是一个 Python 函数。节点接收当前状态,返回对状态的更新。\n",
|
||||
"\n",
|
||||
"### 4.4 Edge(边)\n",
|
||||
"边定义节点之间的连接关系。LangGraph 支持:\n",
|
||||
"- 普通边:固定从一个节点到另一个节点\n",
|
||||
"- 条件边:根据状态决定下一个节点\n",
|
||||
"- START / END:图的起点和终点"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 第一个 LangGraph 程序\n",
|
||||
"\n",
|
||||
"下面构建一个最简单的图:两个节点 `node_a` 和 `node_b`,`node_a` 给状态加 1,`node_b` 再给状态乘 2。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"# 1. 定义状态类型\n",
|
||||
"class MyState(TypedDict):\n",
|
||||
" \"\"\"图中的共享状态。\"\"\"\n",
|
||||
" value: int\n",
|
||||
"\n",
|
||||
"# 2. 定义节点函数\n",
|
||||
"def node_a(state: MyState):\n",
|
||||
" \"\"\"把 value 加 1。\"\"\"\n",
|
||||
" print(f'Node A 接收:{state[\"value\"]}')\n",
|
||||
" return {'value': state['value'] + 1}\n",
|
||||
"\n",
|
||||
"def node_b(state: MyState):\n",
|
||||
" \"\"\"把 value 乘 2。\"\"\"\n",
|
||||
" print(f'Node B 接收:{state[\"value\"]}')\n",
|
||||
" return {'value': state['value'] * 2}\n",
|
||||
"\n",
|
||||
"# 3. 构建状态图\n",
|
||||
"builder = StateGraph(MyState)\n",
|
||||
"builder.add_node('node_a', node_a)\n",
|
||||
"builder.add_node('node_b', node_b)\n",
|
||||
"\n",
|
||||
"# 4. 添加边\n",
|
||||
"builder.add_edge(START, 'node_a')\n",
|
||||
"builder.add_edge('node_a', 'node_b')\n",
|
||||
"builder.add_edge('node_b', END)\n",
|
||||
"\n",
|
||||
"# 5. 编译图\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"# 6. 运行图\n",
|
||||
"result = graph.invoke({'value': 3})\n",
|
||||
"print(f'\\n最终结果:{result}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `TypedDict` 定义状态结构,告诉 LangGraph 图中有哪些字段\n",
|
||||
"- `StateGraph(MyState)` 创建状态图实例\n",
|
||||
"- `add_node(name, func)` 添加节点\n",
|
||||
"- `add_edge(START, 'node_a')` 设置从图起点到 node_a 的边\n",
|
||||
"- `compile()` 编译图,生成可执行的 Runnable\n",
|
||||
"- `invoke({'value': 3})` 运行图,传入初始状态\n",
|
||||
"\n",
|
||||
"这个例子的执行流程是:`START -> node_a(value=3 -> 4) -> node_b(value=4 -> 8) -> END`,最终结果是 8。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. LangGraph 执行流程可视化\n",
|
||||
"\n",
|
||||
"LangGraph 提供了 `get_graph()` 方法,可以查看图的结构。\n",
|
||||
"\n",
|
||||
"在线查看Mermaid 图:https://mermaid.live/edit\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 打印图的节点和边\n",
|
||||
"print(graph.get_graph().nodes)\n",
|
||||
"print(graph.get_graph().edges)\n",
|
||||
"\n",
|
||||
"# 使用 mermaid 格式可视化(在支持 mermaid 的 Markdown 查看器中可显示)\n",
|
||||
"print('\\nMermaid 图:')\n",
|
||||
"print(graph.get_graph().draw_mermaid())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 在 LangGraph 中使用 LangChain 组件\n",
|
||||
"\n",
|
||||
"LangGraph 的节点可以使用任何 LangChain 组件。下面是一个使用 LLM 和 Prompt 的节点示例。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# 定义带消息历史的状态\n",
|
||||
"class ChatState(TypedDict):\n",
|
||||
" messages: list # 对话历史\n",
|
||||
"\n",
|
||||
"llm = ChatOpenAI(model='qwen3.6-35b-A3b', temperature=0.7)\n",
|
||||
"\n",
|
||||
"def chat_node(state: ChatState):\n",
|
||||
" \"\"\"调用 LLM 回复用户。\"\"\"\n",
|
||||
" prompt = ChatPromptTemplate.from_messages([\n",
|
||||
" ('system', '你是友好的 AI 助手。'),\n",
|
||||
" *state['messages']\n",
|
||||
" ])\n",
|
||||
" response = (prompt | llm).invoke({})\n",
|
||||
" return {'messages': [response]}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ChatState)\n",
|
||||
"builder.add_node('chat', chat_node)\n",
|
||||
"builder.add_edge(START, 'chat')\n",
|
||||
"builder.add_edge('chat', END)\n",
|
||||
"\n",
|
||||
"chat_graph = builder.compile()\n",
|
||||
"\n",
|
||||
"result = chat_graph.invoke({\n",
|
||||
" 'messages': [('user', '请用一句话介绍 LangGraph')]\n",
|
||||
"})\n",
|
||||
"print(result['messages'][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"- `messages` 是列表类型,每个元素是 `(role, content)` 元组\n",
|
||||
"- `ChatPromptTemplate.from_messages([...])` 中的 `*state['messages']` 把历史消息展开\n",
|
||||
"- 节点返回 `{'messages': [response]}`,LangGraph 会自动合并到状态中\n",
|
||||
"- 这里只是一个单节点图,后续课程会扩展到多轮对话和循环"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. LangGraph 的典型应用场景\n",
|
||||
"\n",
|
||||
"LangGraph 特别适合以下场景:\n",
|
||||
"\n",
|
||||
"| 场景 | 说明 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| **多轮工具调用** | 在循环中反复调用工具,直到获得足够信息 |\n",
|
||||
"| **人机协作(Human-in-the-loop)** | 在关键节点暂停,等待人类确认 |\n",
|
||||
"| **多智能体系统** | 多个 Agent 通过图结构协作完成任务 |\n",
|
||||
"| **复杂审批流程** | 根据条件分支决定流程走向 |\n",
|
||||
"| **长期记忆** | 通过持久化状态实现跨会话记忆 |\n",
|
||||
"\n",
|
||||
"这些场景在后面几节课会逐一展开。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 本节课练习\n",
|
||||
"\n",
|
||||
"1. 修改第一个 LangGraph 示例,增加第三个节点 `node_c`,实现 value = value - 2\n",
|
||||
"2. 改变三个节点的连接顺序,观察最终结果如何变化\n",
|
||||
"3. 在 ChatState 中增加一个 `topic` 字段,让 system prompt 根据 topic 动态变化\n",
|
||||
"4. 尝试画出本节课第一个示例的状态图(START -> node_a -> node_b -> END)\n",
|
||||
"5. 思考:LangGraph 与你之前学过的 LangChain Chain 相比,最大的优势是什么?"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
656
15_图结构.ipynb
Normal file
656
15_图结构.ipynb
Normal file
@@ -0,0 +1,656 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1801d2fc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 15 图结构\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解为什么要用图结构来组织复杂流程\n",
|
||||
"2. 掌握 LangGraph 中节点、边、状态的配合方式\n",
|
||||
"3. 学会构建顺序执行、条件分支和循环三种常见图结构\n",
|
||||
"4. 能够看懂图结构的执行路径,并根据需求修改流程\n",
|
||||
"5. 为后续多智能体、工作流和复杂 Agent 打下基础"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1f4e8fef",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要图结构\n",
|
||||
"\n",
|
||||
"在前面的课程中,我们已经学过链(Chain):\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"输入 -> Prompt -> 模型 -> 输出\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这种结构很适合**线性流程**,也就是每一步都按固定顺序往下走。\n",
|
||||
"\n",
|
||||
"但是在真实应用中,流程往往没有这么简单。例如:\n",
|
||||
"\n",
|
||||
"- 如果用户的问题不完整,要先追问再继续\n",
|
||||
"- 如果模型判断需要调用工具,要进入工具节点\n",
|
||||
"- 如果工具返回的信息还不够,要再循环一次\n",
|
||||
"- 如果满足某个条件,要走 A 路线,否则走 B 路线\n",
|
||||
"\n",
|
||||
"这时,单纯的链就不够用了。我们需要一种更灵活的结构,让流程能够:\n",
|
||||
"\n",
|
||||
"- **分支**:根据条件走不同路线\n",
|
||||
"- **回路**:重复执行直到满足条件\n",
|
||||
"- **汇合**:多条路径最后回到同一个结果\n",
|
||||
"\n",
|
||||
"这就是**图结构(Graph)**的价值。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "761e8840",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 图结构的核心组成\n",
|
||||
"\n",
|
||||
"在 LangGraph 中,一个图通常由三部分组成:\n",
|
||||
"\n",
|
||||
"### 2.1 State(状态)\n",
|
||||
"状态可以理解为‘流程执行时共享的一份数据’。\n",
|
||||
"\n",
|
||||
"例如一个状态可能长这样:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'value': 3, 'result': '通过', 'step': 2}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"图中的每个节点都可以读取这份状态,也可以返回对状态的更新。\n",
|
||||
"\n",
|
||||
"### 2.2 Node(节点)\n",
|
||||
"节点就是图中的一个处理步骤。\n",
|
||||
"\n",
|
||||
"你可以把节点理解成‘流水线上的一个工位’。每个工位做一件事,例如:\n",
|
||||
"\n",
|
||||
"- 给数字加 1\n",
|
||||
"- 判断成绩是否及格\n",
|
||||
"- 调用大模型生成回复\n",
|
||||
"- 调用搜索工具获取资料\n",
|
||||
"\n",
|
||||
"### 2.3 Edge(边)\n",
|
||||
"边表示‘从哪个节点走到哪个节点’。\n",
|
||||
"\n",
|
||||
"边决定了执行顺序。常见有两种:\n",
|
||||
"\n",
|
||||
"- **普通边**:固定走向下一个节点\n",
|
||||
"- **条件边**:根据状态判断下一步走哪条路\n",
|
||||
"\n",
|
||||
"可以把图结构想象成一张地铁路线图:\n",
|
||||
"\n",
|
||||
"- 状态 = 你当前身上的信息\n",
|
||||
"- 节点 = 你经过的站点\n",
|
||||
"- 边 = 站点之间的路线"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "bf0b4be0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 第一个图:顺序执行\n",
|
||||
"\n",
|
||||
"先从最简单的情况开始:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> 加一 -> 乘二 -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"也就是说:\n",
|
||||
"\n",
|
||||
"1. 初始值是 `value=3`\n",
|
||||
"2. 第一个节点先把它加 1,变成 4\n",
|
||||
"3. 第二个节点再把它乘 2,变成 8\n",
|
||||
"4. 最后输出结果"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "527c7a13",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"# 1. 定义状态结构\n",
|
||||
"class NumberState(TypedDict):\n",
|
||||
" value: int\n",
|
||||
"\n",
|
||||
"# 2. 定义节点函数\n",
|
||||
"def add_one(state: NumberState):\n",
|
||||
" print(f'进入 add_one 前,value = {state[\"value\"]}')\n",
|
||||
" return {'value': state['value'] + 1}\n",
|
||||
"\n",
|
||||
"def multiply_two(state: NumberState):\n",
|
||||
" print(f'进入 multiply_two 前,value = {state[\"value\"]}')\n",
|
||||
" return {'value': state['value'] * 2}\n",
|
||||
"\n",
|
||||
"# 3. 创建图\n",
|
||||
"builder = StateGraph(NumberState)\n",
|
||||
"\n",
|
||||
"# 4. 添加节点\n",
|
||||
"builder.add_node('add_one', add_one)\n",
|
||||
"builder.add_node('multiply_two', multiply_two)\n",
|
||||
"\n",
|
||||
"# 5. 添加边\n",
|
||||
"builder.add_edge(START, 'add_one')\n",
|
||||
"builder.add_edge('add_one', 'multiply_two')\n",
|
||||
"builder.add_edge('multiply_two', END)\n",
|
||||
"\n",
|
||||
"# 6. 编译图\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"# 7. 执行图\n",
|
||||
"result = graph.invoke({'value': 3})\n",
|
||||
"print('最终结果:', result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "62406ccb",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码虽然不长,但非常关键,值得逐行理解。\n",
|
||||
"\n",
|
||||
"#### 第 1 步:定义状态\n",
|
||||
"`NumberState` 规定了图里共享的数据结构,这里只有一个字段:`value`。\n",
|
||||
"\n",
|
||||
"也就是说,整张图传来传去的核心数据就是一个整数。\n",
|
||||
"\n",
|
||||
"#### 第 2 步:定义节点函数\n",
|
||||
"`add_one(state)` 和 `multiply_two(state)` 都接收当前状态。\n",
|
||||
"\n",
|
||||
"例如当状态是:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'value': 3}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"那么 `add_one` 返回:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'value': 4}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"节点函数**不需要返回完整状态**,只要返回要更新的字段即可。LangGraph 会把更新结果合并回状态中。\n",
|
||||
"\n",
|
||||
"#### 第 3 步:创建图\n",
|
||||
"`StateGraph(NumberState)` 表示:我们要创建一张图,并且这张图中的状态结构由 `NumberState` 定义。\n",
|
||||
"\n",
|
||||
"#### 第 4 步:添加节点\n",
|
||||
"`builder.add_node('add_one', add_one)` 的意思是:\n",
|
||||
"\n",
|
||||
"- 节点名字叫 `add_one`\n",
|
||||
"- 这个节点真正执行的函数是 Python 函数 `add_one`\n",
|
||||
"\n",
|
||||
"节点名字更像是图里的‘标签’,函数才是‘真正干活的人’。\n",
|
||||
"\n",
|
||||
"#### 第 5 步:添加边\n",
|
||||
"这三行定义了执行路径:\n",
|
||||
"\n",
|
||||
"- 从 `START` 进入 `add_one`\n",
|
||||
"- 从 `add_one` 进入 `multiply_two`\n",
|
||||
"- 从 `multiply_two` 进入 `END`\n",
|
||||
"\n",
|
||||
"因此执行顺序是固定的,没有分支,也没有循环。\n",
|
||||
"\n",
|
||||
"#### 第 6 步:编译图\n",
|
||||
"`compile()` 会把‘图的定义’变成一个真正可运行的对象。\n",
|
||||
"\n",
|
||||
"你可以把它理解为:前面是在画流程图,这一步是把流程图变成程序。\n",
|
||||
"\n",
|
||||
"#### 第 7 步:运行图\n",
|
||||
"`graph.invoke({'value': 3})` 表示用初始状态 `{'value': 3}` 启动整张图。\n",
|
||||
"\n",
|
||||
"执行过程是:\n",
|
||||
"\n",
|
||||
"1. `value=3` 进入 `add_one`\n",
|
||||
"2. 更新为 `value=4`\n",
|
||||
"3. `value=4` 进入 `multiply_two`\n",
|
||||
"4. 更新为 `value=8`\n",
|
||||
"5. 图结束,输出 `{'value': 8}`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e86fbc2b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 查看图结构\n",
|
||||
"\n",
|
||||
"除了运行图,我们还可以把图的结构打印出来,帮助自己检查流程是否连对了。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "029c26ae",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print('节点:')\n",
|
||||
"print(graph.get_graph().nodes)\n",
|
||||
"\n",
|
||||
"print('\\n边:')\n",
|
||||
"print(graph.get_graph().edges)\n",
|
||||
"\n",
|
||||
"print('\\nMermaid 图:')\n",
|
||||
"print(graph.get_graph().draw_mermaid())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "996a24d3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这一段代码的重点不在‘计算’,而在‘观察图’。\n",
|
||||
"\n",
|
||||
"#### `graph.get_graph().nodes`\n",
|
||||
"它会返回当前图中有哪些节点。你可以把它理解为‘流程图里有哪些方框’。\n",
|
||||
"\n",
|
||||
"#### `graph.get_graph().edges`\n",
|
||||
"它会返回节点之间的连接关系,也就是‘箭头从哪里指向哪里’。\n",
|
||||
"\n",
|
||||
"#### `draw_mermaid()`\n",
|
||||
"它会生成 Mermaid 格式的流程图文本。\n",
|
||||
"\n",
|
||||
"Mermaid 是一种用文本描述流程图的语法,例如:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START --> add_one --> multiply_two --> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"你可以把输出复制到支持 Mermaid 的工具中查看图形化效果。\n",
|
||||
"\n",
|
||||
"这在图结构变复杂之后尤其有用,因为:\n",
|
||||
"\n",
|
||||
"- 可以快速确认自己有没有连错边\n",
|
||||
"- 可以帮助团队成员理解流程\n",
|
||||
"- 可以作为调试流程的一部分"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "9117ea6b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 条件分支图\n",
|
||||
"\n",
|
||||
"前面的图是固定顺序执行。现在我们来做一个更像真实业务的例子:**根据分数决定是否通过**。\n",
|
||||
"\n",
|
||||
"流程如下:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> 判断分数 -> 合格 / 不合格 -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"如果分数大于等于 60,就走‘通过’节点;否则走‘补考’节点。\n",
|
||||
"\n",
|
||||
"这就是典型的**条件分支**。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ed38a5d4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class ScoreState(TypedDict):\n",
|
||||
" score: int\n",
|
||||
" result: str\n",
|
||||
"\n",
|
||||
"def check_score(state: ScoreState):\n",
|
||||
" print(f'当前分数:{state[\"score\"]}')\n",
|
||||
" return {}\n",
|
||||
"\n",
|
||||
"def pass_node(state: ScoreState):\n",
|
||||
" return {'result': '成绩合格,顺利通过'}\n",
|
||||
"\n",
|
||||
"def fail_node(state: ScoreState):\n",
|
||||
" return {'result': '成绩不合格,需要补考'}\n",
|
||||
"\n",
|
||||
"def route_by_score(state: ScoreState):\n",
|
||||
" if state['score'] >= 60:\n",
|
||||
" return 'pass_node'\n",
|
||||
" return 'fail_node'\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ScoreState)\n",
|
||||
"builder.add_node('check_score', check_score)\n",
|
||||
"builder.add_node('pass_node', pass_node)\n",
|
||||
"builder.add_node('fail_node', fail_node)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'check_score')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'check_score',\n",
|
||||
" route_by_score,\n",
|
||||
" {\n",
|
||||
" 'pass_node': 'pass_node',\n",
|
||||
" 'fail_node': 'fail_node'\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"builder.add_edge('pass_node', END)\n",
|
||||
"builder.add_edge('fail_node', END)\n",
|
||||
"\n",
|
||||
"score_graph = builder.compile()\n",
|
||||
"\n",
|
||||
"print('分数 85 的结果:')\n",
|
||||
"print(score_graph.invoke({'score': 85, 'result': ''}))\n",
|
||||
"\n",
|
||||
"print('\\n分数 45 的结果:')\n",
|
||||
"print(score_graph.invoke({'score': 45, 'result': ''}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a8a9080b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子第一次出现了**条件边**,所以需要重点掌握。\n",
|
||||
"\n",
|
||||
"#### 状态结构\n",
|
||||
"这里的状态包含两个字段:\n",
|
||||
"\n",
|
||||
"- `score`:输入分数\n",
|
||||
"- `result`:最终判断结果\n",
|
||||
"\n",
|
||||
"也就是说,这张图的目标是:根据 `score` 计算出 `result`。\n",
|
||||
"\n",
|
||||
"#### `check_score` 节点\n",
|
||||
"这个节点本身没有修改状态,只是把当前分数打印出来。\n",
|
||||
"\n",
|
||||
"它返回 `{}`,表示‘这个节点不更新任何字段’。\n",
|
||||
"\n",
|
||||
"这说明一个节点不一定非要做数据修改,它也可以只承担‘中转’或‘判断前准备’的作用。\n",
|
||||
"\n",
|
||||
"#### `pass_node` 和 `fail_node`\n",
|
||||
"这两个节点分别代表两条不同路径的处理结果:\n",
|
||||
"\n",
|
||||
"- 如果通过,更新 `result='成绩合格,顺利通过'`\n",
|
||||
"- 如果不通过,更新 `result='成绩不合格,需要补考'`\n",
|
||||
"\n",
|
||||
"这两个节点就像流程图里分叉后的两条支路。\n",
|
||||
"\n",
|
||||
"#### `route_by_score`:路由函数\n",
|
||||
"这是本例最核心的部分。\n",
|
||||
"\n",
|
||||
"它不是真正的业务节点,而是一个‘路线选择器’。它根据当前状态判断:\n",
|
||||
"\n",
|
||||
"- `score >= 60` 返回 `'pass_node'`\n",
|
||||
"- 否则返回 `'fail_node'`\n",
|
||||
"\n",
|
||||
"返回值必须和后面条件边映射表中的 key 对应上。\n",
|
||||
"\n",
|
||||
"#### `add_conditional_edges(...)`\n",
|
||||
"这段代码的意思是:\n",
|
||||
"\n",
|
||||
"从 `check_score` 节点出来后,不是固定走下一步,而是调用 `route_by_score(state)`。\n",
|
||||
"\n",
|
||||
"如果返回 `'pass_node'`,就走到 `pass_node`;如果返回 `'fail_node'`,就走到 `fail_node`。\n",
|
||||
"\n",
|
||||
"你可以把它理解成:\n",
|
||||
"\n",
|
||||
"- 普通边 = 固定导航\n",
|
||||
"- 条件边 = 智能导航\n",
|
||||
"\n",
|
||||
"#### 最终执行效果\n",
|
||||
"当输入 `score=85` 时,流程是:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> check_score -> pass_node -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"当输入 `score=45` 时,流程是:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> check_score -> fail_node -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这就是图结构中‘根据条件走不同路线’的最基本模式。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3cf69b96",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 循环图\n",
|
||||
"\n",
|
||||
"除了分支,图结构还有一个非常重要的能力:**循环**。\n",
|
||||
"\n",
|
||||
"在很多智能体任务中,我们都需要重复执行某些步骤,比如:\n",
|
||||
"\n",
|
||||
"- 检索不到足够信息就继续搜索\n",
|
||||
"- 模型输出格式不对就重新生成\n",
|
||||
"- 工具调用结果不完整就继续补充\n",
|
||||
"\n",
|
||||
"下面我们用一个简单例子说明循环:\n",
|
||||
"\n",
|
||||
"让 `count` 从 0 开始,每次加 1,直到加到 3 为止。\n",
|
||||
"\n",
|
||||
"流程如下:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> increment -> 判断是否结束\n",
|
||||
" ↘ 未结束,回到 increment\n",
|
||||
" ↘ 已结束,进入 END\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d33d64ec",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class LoopState(TypedDict):\n",
|
||||
" count: int\n",
|
||||
"\n",
|
||||
"def increment(state: LoopState):\n",
|
||||
" new_count = state['count'] + 1\n",
|
||||
" print(f'当前 count 从 {state[\"count\"]} 变成 {new_count}')\n",
|
||||
" return {'count': new_count}\n",
|
||||
"\n",
|
||||
"def should_continue(state: LoopState):\n",
|
||||
" if state['count'] < 3:\n",
|
||||
" return 'continue'\n",
|
||||
" return 'stop'\n",
|
||||
"\n",
|
||||
"builder = StateGraph(LoopState)\n",
|
||||
"builder.add_node('increment', increment)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'increment')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'increment',\n",
|
||||
" should_continue,\n",
|
||||
" {\n",
|
||||
" 'continue': 'increment',\n",
|
||||
" 'stop': END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"loop_graph = builder.compile()\n",
|
||||
"result = loop_graph.invoke({'count': 0})\n",
|
||||
"print('最终状态:', result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "26274c73",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子看起来简单,但它已经具备了很多 Agent 工作流的核心思想。\n",
|
||||
"\n",
|
||||
"#### `increment` 节点\n",
|
||||
"它的职责非常明确:每次把 `count` 加 1。\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"- 输入 `count=0`,输出 `count=1`\n",
|
||||
"- 输入 `count=1`,输出 `count=2`\n",
|
||||
"- 输入 `count=2`,输出 `count=3`\n",
|
||||
"\n",
|
||||
"#### `should_continue` 路由函数\n",
|
||||
"它负责判断是继续循环,还是结束流程。\n",
|
||||
"\n",
|
||||
"规则很简单:\n",
|
||||
"\n",
|
||||
"- 如果 `count < 3`,返回 `'continue'`\n",
|
||||
"- 否则返回 `'stop'`\n",
|
||||
"\n",
|
||||
"#### 条件边的映射\n",
|
||||
"这里最关键的一点是:\n",
|
||||
"\n",
|
||||
"- `'continue'` 映射到 `'increment'`\n",
|
||||
"- `'stop'` 映射到 `END`\n",
|
||||
"\n",
|
||||
"这就意味着:\n",
|
||||
"\n",
|
||||
"- 只要还没达到条件,就重新回到同一个节点\n",
|
||||
"- 达到条件后,就退出图\n",
|
||||
"\n",
|
||||
"这就是循环结构的本质。\n",
|
||||
"\n",
|
||||
"#### 执行过程\n",
|
||||
"如果初始状态是 `{'count': 0}`,执行路径如下:\n",
|
||||
"\n",
|
||||
"1. 进入 `increment`,变成 `count=1`\n",
|
||||
"2. 判断 `1 < 3`,继续\n",
|
||||
"3. 再次进入 `increment`,变成 `count=2`\n",
|
||||
"4. 判断 `2 < 3`,继续\n",
|
||||
"5. 再次进入 `increment`,变成 `count=3`\n",
|
||||
"6. 判断 `3 < 3` 不成立,停止\n",
|
||||
"7. 进入 `END`\n",
|
||||
"\n",
|
||||
"也就是说,图结构中的循环并不是 `for` 或 `while` 语法写出来的,而是通过‘节点 + 条件边回跳’实现的。\n",
|
||||
"\n",
|
||||
"这也是 LangGraph 很强大的地方:它把‘流程控制’变成了图本身的一部分。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8c2c4ae1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 图结构和普通 Python 流程控制有什么区别\n",
|
||||
"\n",
|
||||
"你可能会问:\n",
|
||||
"\n",
|
||||
"‘这些分支和循环,我直接用 `if`、`while` 不也能写吗?’\n",
|
||||
"\n",
|
||||
"当然可以。但图结构有几个明显优势:\n",
|
||||
"\n",
|
||||
"| 对比点 | 普通 Python 代码 | 图结构 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 流程表达方式 | 写在代码逻辑里 | 显式画成节点和边 |\n",
|
||||
"| 可视化能力 | 弱 | 强 |\n",
|
||||
"| 分支/回路可读性 | 复杂时容易乱 | 更直观 |\n",
|
||||
"| 与 Agent / 工作流结合 | 需要手动组织 | 天然适合 |\n",
|
||||
"| 状态传递 | 自己管理变量 | 统一用 State 管理 |\n",
|
||||
"\n",
|
||||
"最核心的一点是:\n",
|
||||
"\n",
|
||||
"**图结构不是为了替代 Python,而是为了让复杂流程更容易设计、理解、调试和扩展。**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f82467cf",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 一个更贴近 Agent 的理解方式\n",
|
||||
"\n",
|
||||
"如果把图结构放到 AI Agent 场景中,可以这样理解:\n",
|
||||
"\n",
|
||||
"- 一个节点可以是‘调用大模型’\n",
|
||||
"- 一个节点可以是‘调用工具’\n",
|
||||
"- 一个节点可以是‘检查结果是否满足要求’\n",
|
||||
"- 条件边可以决定‘是继续调用工具,还是给用户最终答案’\n",
|
||||
"\n",
|
||||
"也就是说,后面你学到的复杂 Agent,本质上大多都可以拆成:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"理解问题 -> 决策 -> 调工具 -> 检查结果 -> 继续/结束\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"而图结构正是把这条路线显式表达出来的最好方式。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f8ced671",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 本节小结\n",
|
||||
"\n",
|
||||
"本节课你需要记住三个最重要的结论:\n",
|
||||
"\n",
|
||||
"1. **State 是共享数据**:节点之间通过状态传递信息\n",
|
||||
"2. **Node 是处理步骤**:每个节点负责完成一个明确任务\n",
|
||||
"3. **Edge 决定执行路径**:普通边用于顺序执行,条件边用于分支和循环\n",
|
||||
"\n",
|
||||
"掌握了这三点,你就已经理解了图结构的核心。后面无论是多轮工作流、工具调用循环,还是多智能体协作,本质上都只是图结构的进一步扩展。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3ec471d7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 本节练习\n",
|
||||
"\n",
|
||||
"1. 修改顺序执行示例,在 `multiply_two` 后面再加一个节点 `minus_three`,让最终结果再减 3\n",
|
||||
"2. 修改条件分支示例,把及格线从 60 改成 80,观察结果变化\n",
|
||||
"3. 修改循环示例,把终止条件从 `count == 3` 改成 `count == 5`\n",
|
||||
"4. 尝试自己画出三个示例的流程图\n",
|
||||
"5. 思考:如果要做一个‘查询资料直到找到答案为止’的 Agent,它更像本节中的分支图,还是循环图?为什么?"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
635
16_状态管理.ipynb
Normal file
635
16_状态管理.ipynb
Normal file
@@ -0,0 +1,635 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cc615887",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 16 状态管理\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解 LangGraph 中状态(State)的设计与作用\n",
|
||||
"2. 掌握使用 TypedDict 定义图状态\n",
|
||||
"3. 学会在节点间传递、读取和更新状态\n",
|
||||
"4. 理解多字段状态在实际流程中的用法\n",
|
||||
"5. 避免状态设计中的常见问题"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "dfebe943",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 什么是状态管理\n",
|
||||
"\n",
|
||||
"在 LangGraph 中,**状态(State)** 可以理解为“流程运行时一直随身携带的一份数据”。\n",
|
||||
"\n",
|
||||
"前一节我们已经接触过状态,例如:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'value': 3}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"或者:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'score': 85, 'result': '成绩合格,顺利通过'}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这些数据会在图的不同节点之间传来传去。\n",
|
||||
"\n",
|
||||
"状态管理要解决的问题,其实很简单:\n",
|
||||
"\n",
|
||||
"- 当前流程已经走到哪一步了?\n",
|
||||
"- 前面节点产出的结果,后面节点怎么接着用?\n",
|
||||
"- 某个节点更新了数据,后续节点如何读取最新值?\n",
|
||||
"\n",
|
||||
"你可以把状态想象成一张‘流程记录卡’:\n",
|
||||
"\n",
|
||||
"- 每经过一个节点,就在卡上补充信息\n",
|
||||
"- 后面的节点只需要看这张卡,就知道前面发生了什么\n",
|
||||
"\n",
|
||||
"所以,**状态管理的本质,就是管理这张共享记录卡。**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a40150ac",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 为什么状态很重要\n",
|
||||
"\n",
|
||||
"如果没有状态,节点之间就很难协作。\n",
|
||||
"\n",
|
||||
"例如一个流程要完成下面几步:\n",
|
||||
"\n",
|
||||
"1. 接收用户输入\n",
|
||||
"2. 提取关键词\n",
|
||||
"3. 根据关键词生成结论\n",
|
||||
"\n",
|
||||
"那么问题来了:\n",
|
||||
"\n",
|
||||
"- 第二步提取出来的关键词,怎么交给第三步?\n",
|
||||
"- 第一节点记录的原始问题,第三步还能不能访问?\n",
|
||||
"- 如果流程还要继续加步骤,数据还能不能统一管理?\n",
|
||||
"\n",
|
||||
"答案就是:都放进状态里。\n",
|
||||
"\n",
|
||||
"状态的几个核心价值:\n",
|
||||
"\n",
|
||||
"- **统一传递数据**:所有节点都从同一个地方读取信息\n",
|
||||
"- **减少参数混乱**:不用手动给每个函数传很多单独变量\n",
|
||||
"- **便于扩展流程**:后面增加新节点时,只要继续读写状态即可\n",
|
||||
"- **便于调试**:看状态就能知道流程执行到了什么程度"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "644129aa",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 用 TypedDict 定义状态\n",
|
||||
"\n",
|
||||
"在 LangGraph 中,我们通常用 `TypedDict` 来定义状态结构。\n",
|
||||
"\n",
|
||||
"这样做有两个好处:\n",
|
||||
"\n",
|
||||
"1. 让自己清楚状态里有哪些字段\n",
|
||||
"2. 让代码更容易维护和理解\n",
|
||||
"\n",
|
||||
"下面先看一个最简单的例子。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "08a43781",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"class UserState(TypedDict):\n",
|
||||
" name: str\n",
|
||||
" age: int\n",
|
||||
" city: str\n",
|
||||
"\n",
|
||||
"example_state: UserState = {\n",
|
||||
" 'name': '张三',\n",
|
||||
" 'age': 20,\n",
|
||||
" 'city': '北京'\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"print(example_state)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4cdd8784",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码本身还没有进入 LangGraph,只是在说明状态应该怎样定义。\n",
|
||||
"\n",
|
||||
"#### `TypedDict` 是什么\n",
|
||||
"`TypedDict` 可以理解为‘带字段说明的字典’。\n",
|
||||
"\n",
|
||||
"普通字典也能写成:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'name': '张三', 'age': 20, 'city': '北京'}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"但如果项目变复杂,单纯靠大家记忆“这个字典里应该有哪些字段”会很容易出错。\n",
|
||||
"\n",
|
||||
"`TypedDict` 的作用就是把这种约定明确写出来。\n",
|
||||
"\n",
|
||||
"#### `class UserState(TypedDict)`\n",
|
||||
"这表示我们定义了一种状态结构,名字叫 `UserState`。\n",
|
||||
"\n",
|
||||
"它要求状态中有三个字段:\n",
|
||||
"\n",
|
||||
"- `name`:字符串\n",
|
||||
"- `age`:整数\n",
|
||||
"- `city`:字符串\n",
|
||||
"\n",
|
||||
"#### `example_state: UserState = {...}`\n",
|
||||
"这表示我们创建了一个符合 `UserState` 结构的状态对象。\n",
|
||||
"\n",
|
||||
"从教学角度看,这一步很重要,因为它让你意识到:\n",
|
||||
"\n",
|
||||
"- 状态本质上仍然是字典\n",
|
||||
"- `TypedDict` 只是帮助我们把字典结构写清楚\n",
|
||||
"\n",
|
||||
"在 LangGraph 中,节点之间传递的就是这种结构化字典。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "dceebf80",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 读取状态:节点如何使用已有数据\n",
|
||||
"\n",
|
||||
"定义完状态之后,下一步就是在节点中读取它。\n",
|
||||
"\n",
|
||||
"下面这个例子演示:节点如何从状态里取出数据并生成一句自我介绍。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cb6536ba",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class ProfileState(TypedDict):\n",
|
||||
" name: str\n",
|
||||
" age: int\n",
|
||||
" city: str\n",
|
||||
" intro: str\n",
|
||||
"\n",
|
||||
"def create_intro(state: ProfileState):\n",
|
||||
" intro_text = f'大家好,我叫{state[\"name\"]},今年{state[\"age\"]}岁,来自{state[\"city\"]}。'\n",
|
||||
" return {'intro': intro_text}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ProfileState)\n",
|
||||
"builder.add_node('create_intro', create_intro)\n",
|
||||
"builder.add_edge(START, 'create_intro')\n",
|
||||
"builder.add_edge('create_intro', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"result = graph.invoke({\n",
|
||||
" 'name': '李雷',\n",
|
||||
" 'age': 18,\n",
|
||||
" 'city': '上海',\n",
|
||||
" 'intro': ''\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print(result)\n",
|
||||
"print(result['intro'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "120b0a74",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子重点不是图结构本身,而是“节点如何读取状态中的已有字段”。\n",
|
||||
"\n",
|
||||
"#### 状态结构\n",
|
||||
"这里的状态有四个字段:\n",
|
||||
"\n",
|
||||
"- `name`\n",
|
||||
"- `age`\n",
|
||||
"- `city`\n",
|
||||
"- `intro`\n",
|
||||
"\n",
|
||||
"前三个字段是输入信息,最后一个字段是我们希望在流程中生成的新结果。\n",
|
||||
"\n",
|
||||
"#### `create_intro(state)`\n",
|
||||
"这个节点做的事情很直观:\n",
|
||||
"\n",
|
||||
"1. 从状态中读取 `name`\n",
|
||||
"2. 从状态中读取 `age`\n",
|
||||
"3. 从状态中读取 `city`\n",
|
||||
"4. 组装成一句完整介绍\n",
|
||||
"5. 把结果写回 `intro` 字段\n",
|
||||
"\n",
|
||||
"也就是说,这个节点没有改变原来的 `name`、`age`、`city`,只是新增或更新了 `intro`。\n",
|
||||
"\n",
|
||||
"#### 节点返回值为什么只写 `{'intro': intro_text}`\n",
|
||||
"这是 LangGraph 状态管理里非常重要的一点:\n",
|
||||
"\n",
|
||||
"**节点只需要返回自己负责修改的字段。**\n",
|
||||
"\n",
|
||||
"原来的字段不会凭空消失,而是继续保留在状态里。\n",
|
||||
"\n",
|
||||
"所以最终 `result` 中既有原始输入,也有新生成的 `intro`。\n",
|
||||
"\n",
|
||||
"#### 这个例子的意义\n",
|
||||
"它展示了状态管理的最基础工作方式:\n",
|
||||
"\n",
|
||||
"- 输入状态中先有一部分信息\n",
|
||||
"- 节点读取这些信息进行加工\n",
|
||||
"- 再把加工结果写回状态\n",
|
||||
"\n",
|
||||
"这就是后面所有复杂流程的基础。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "28600cec",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 更新状态:后面的节点继续接着用\n",
|
||||
"\n",
|
||||
"状态管理真正强大的地方,不在于‘一个节点能读写状态’,而在于**前一个节点更新的内容,后一个节点可以继续使用**。\n",
|
||||
"\n",
|
||||
"下面这个例子演示两个节点接力处理状态:\n",
|
||||
"\n",
|
||||
"1. 第一个节点生成问候语\n",
|
||||
"2. 第二个节点在问候语后面再补一句欢迎词"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "98e52d28",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class GreetingState(TypedDict):\n",
|
||||
" name: str\n",
|
||||
" greeting: str\n",
|
||||
" final_message: str\n",
|
||||
"\n",
|
||||
"def make_greeting(state: GreetingState):\n",
|
||||
" return {'greeting': f'你好,{state[\"name\"]}!'}\n",
|
||||
"\n",
|
||||
"def make_final_message(state: GreetingState):\n",
|
||||
" final_text = state['greeting'] + ' 欢迎来到状态管理课程。'\n",
|
||||
" return {'final_message': final_text}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(GreetingState)\n",
|
||||
"builder.add_node('make_greeting', make_greeting)\n",
|
||||
"builder.add_node('make_final_message', make_final_message)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'make_greeting')\n",
|
||||
"builder.add_edge('make_greeting', 'make_final_message')\n",
|
||||
"builder.add_edge('make_final_message', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"result = graph.invoke({\n",
|
||||
" 'name': '小王',\n",
|
||||
" 'greeting': '',\n",
|
||||
" 'final_message': ''\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print(result)\n",
|
||||
"print(result['final_message'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "70fa1f92",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子体现了状态在多个节点之间的‘接力传递’。\n",
|
||||
"\n",
|
||||
"#### 第一个节点 `make_greeting`\n",
|
||||
"它只做一件事:根据 `name` 生成一句问候语,写入 `greeting`。\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'name': '小王'}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"会生成:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'greeting': '你好,小王!'}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"#### 第二个节点 `make_final_message`\n",
|
||||
"它并不重新根据 `name` 生成问候,而是直接读取上一个节点已经写入状态的 `greeting`。\n",
|
||||
"\n",
|
||||
"然后在它后面拼上一句:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"欢迎来到状态管理课程。\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"最后写入 `final_message`。\n",
|
||||
"\n",
|
||||
"#### 这说明了什么\n",
|
||||
"这说明状态不仅能存放“输入数据”,还能存放“中间结果”。\n",
|
||||
"\n",
|
||||
"这是状态管理特别重要的一点:\n",
|
||||
"\n",
|
||||
"- 输入数据:用户最初给的信息\n",
|
||||
"- 中间结果:某个节点处理出来的阶段性结果\n",
|
||||
"- 最终结果:流程结束时输出的内容\n",
|
||||
"\n",
|
||||
"如果没有状态,你就得手动把这些内容一级一级传下去;有了状态,流程自然就串起来了。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f28536c4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 多字段状态:让流程更接近真实业务\n",
|
||||
"\n",
|
||||
"真实项目里的状态,通常不会只有 1 个或 2 个字段。\n",
|
||||
"\n",
|
||||
"例如一个订单处理流程,可能需要同时记录:\n",
|
||||
"\n",
|
||||
"- 用户姓名\n",
|
||||
"- 商品名称\n",
|
||||
"- 数量\n",
|
||||
"- 总价\n",
|
||||
"- 订单状态\n",
|
||||
"\n",
|
||||
"下面我们用一个简单的订单示例,演示多字段状态的管理方式。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "b3d1077b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class OrderState(TypedDict):\n",
|
||||
" customer_name: str\n",
|
||||
" product_name: str\n",
|
||||
" price: float\n",
|
||||
" quantity: int\n",
|
||||
" total_price: float\n",
|
||||
" order_status: str\n",
|
||||
"\n",
|
||||
"def calculate_total(state: OrderState):\n",
|
||||
" total = state['price'] * state['quantity']\n",
|
||||
" return {'total_price': total}\n",
|
||||
"\n",
|
||||
"def confirm_order(state: OrderState):\n",
|
||||
" status = f'订单已确认:{state[\"customer_name\"]} 购买了 {state[\"quantity\"]} 件 {state[\"product_name\"]},总价 {state[\"total_price\"]} 元。'\n",
|
||||
" return {'order_status': status}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(OrderState)\n",
|
||||
"builder.add_node('calculate_total', calculate_total)\n",
|
||||
"builder.add_node('confirm_order', confirm_order)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'calculate_total')\n",
|
||||
"builder.add_edge('calculate_total', 'confirm_order')\n",
|
||||
"builder.add_edge('confirm_order', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"result = graph.invoke({\n",
|
||||
" 'customer_name': '王芳',\n",
|
||||
" 'product_name': '机械键盘',\n",
|
||||
" 'price': 299.0,\n",
|
||||
" 'quantity': 2,\n",
|
||||
" 'total_price': 0.0,\n",
|
||||
" 'order_status': ''\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print(result)\n",
|
||||
"print(result['order_status'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ad35700b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子很适合用来理解‘多字段状态为什么有必要’。\n",
|
||||
"\n",
|
||||
"#### 状态里有哪些信息\n",
|
||||
"这里的状态已经不只是一个简单变量,而是一组完整业务数据:\n",
|
||||
"\n",
|
||||
"- 谁在下单:`customer_name`\n",
|
||||
"- 买的是什么:`product_name`\n",
|
||||
"- 单价是多少:`price`\n",
|
||||
"- 买了几件:`quantity`\n",
|
||||
"- 总价是多少:`total_price`\n",
|
||||
"- 当前订单描述:`order_status`\n",
|
||||
"\n",
|
||||
"#### `calculate_total` 节点\n",
|
||||
"这个节点只负责计算总价:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"总价 = 单价 * 数量\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"它返回 `{'total_price': total}`,把结果写回状态。\n",
|
||||
"\n",
|
||||
"#### `confirm_order` 节点\n",
|
||||
"这个节点读取多个字段:\n",
|
||||
"\n",
|
||||
"- `customer_name`\n",
|
||||
"- `quantity`\n",
|
||||
"- `product_name`\n",
|
||||
"- `total_price`\n",
|
||||
"\n",
|
||||
"然后把这些信息拼成完整订单说明,再写入 `order_status`。\n",
|
||||
"\n",
|
||||
"#### 这个例子传达的核心思想\n",
|
||||
"状态不是只给“一个节点”准备的,而是给“整个流程”准备的。\n",
|
||||
"\n",
|
||||
"每个节点只关心自己需要的部分字段,但所有字段加起来,构成了完整的业务上下文。\n",
|
||||
"\n",
|
||||
"这也是为什么状态设计要尽量清晰:因为它决定了整个流程的数据组织方式。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4858b6f3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 状态设计的常见原则\n",
|
||||
"\n",
|
||||
"写状态时,建议遵循下面几个原则。\n",
|
||||
"\n",
|
||||
"### 7.1 字段名要清楚\n",
|
||||
"不要用太模糊的名字,例如:\n",
|
||||
"\n",
|
||||
"- `data`\n",
|
||||
"- `info`\n",
|
||||
"- `result1`\n",
|
||||
"\n",
|
||||
"更好的方式是:\n",
|
||||
"\n",
|
||||
"- `user_question`\n",
|
||||
"- `retrieved_docs`\n",
|
||||
"- `final_answer`\n",
|
||||
"\n",
|
||||
"字段名越清楚,后面越不容易乱。\n",
|
||||
"\n",
|
||||
"### 7.2 一个字段只表达一种含义\n",
|
||||
"例如不要今天把 `result` 用来存字符串,明天又用来存字典。\n",
|
||||
"\n",
|
||||
"字段含义最好稳定,不要随流程变化得太厉害。\n",
|
||||
"\n",
|
||||
"### 7.3 把中间结果保留下来\n",
|
||||
"有些同学会只保留最终结果,把中间结果全部覆盖掉。这样虽然表面简洁,但调试时会非常痛苦。\n",
|
||||
"\n",
|
||||
"如果中间结果后面可能还会用到,或者你希望排查流程问题,就应该保留。\n",
|
||||
"\n",
|
||||
"### 7.4 不要把无关信息全塞进去\n",
|
||||
"状态不是越大越好。\n",
|
||||
"\n",
|
||||
"只保留这个流程真正需要的数据,避免状态越来越臃肿。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d701714f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 一个常见误区:以为节点要返回完整状态\n",
|
||||
"\n",
|
||||
"很多初学者会误以为:每个节点都必须返回整份状态,例如:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"return {\n",
|
||||
" 'name': state['name'],\n",
|
||||
" 'age': state['age'],\n",
|
||||
" 'city': state['city'],\n",
|
||||
" 'intro': intro_text\n",
|
||||
"}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"其实在很多情况下,这么写没有必要。\n",
|
||||
"\n",
|
||||
"更简洁的方式通常是:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"return {'intro': intro_text}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"原因是:LangGraph 会自动把更新结果合并回原状态。\n",
|
||||
"\n",
|
||||
"所以,除非你确实要同时改很多字段,否则通常只返回你真正改动的那部分。\n",
|
||||
"\n",
|
||||
"这会让代码更短,也更不容易出错。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a28700b1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 状态管理和后续 Agent 的关系\n",
|
||||
"\n",
|
||||
"后面学 Agent、RAG、多轮工具调用时,你会越来越频繁地用到状态。\n",
|
||||
"\n",
|
||||
"例如一个复杂 Agent 的状态里,可能会放这些字段:\n",
|
||||
"\n",
|
||||
"- 用户问题 `user_query`\n",
|
||||
"- 历史消息 `messages`\n",
|
||||
"- 工具调用结果 `tool_result`\n",
|
||||
"- 检索到的资料 `retrieved_docs`\n",
|
||||
"- 当前决策 `decision`\n",
|
||||
"- 最终回答 `final_answer`\n",
|
||||
"\n",
|
||||
"所以可以说:\n",
|
||||
"\n",
|
||||
"**图结构解决的是“流程怎么走”,状态管理解决的是“数据怎么跟着流程走”。**\n",
|
||||
"\n",
|
||||
"这两者是 LangGraph 最核心的两根主线。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2a08da16",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 本节小结\n",
|
||||
"\n",
|
||||
"本节你需要重点记住以下几点:\n",
|
||||
"\n",
|
||||
"1. **状态本质上是共享字典**,只是通常用 `TypedDict` 把结构定义清楚\n",
|
||||
"2. **节点通过读取状态拿到已有数据**\n",
|
||||
"3. **节点通过返回字典更新状态**\n",
|
||||
"4. **后面的节点可以继续读取前面节点写入的中间结果**\n",
|
||||
"5. **好的状态设计,会让整个图流程更清晰、更稳定、更容易调试**\n",
|
||||
"\n",
|
||||
"理解了状态管理,后面再看复杂流程时,你就不会只盯着‘节点怎么连’,而会同时关注‘数据怎么流动’。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "caa87287",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. 本节练习\n",
|
||||
"\n",
|
||||
"1. 修改 `ProfileState` 示例,在状态中增加 `hobby` 字段,并把爱好加入自我介绍\n",
|
||||
"2. 修改 `GreetingState` 示例,让第二个节点在最终消息后面再追加一句祝福\n",
|
||||
"3. 修改 `OrderState` 示例,增加一个 `discount` 字段,并让总价计算支持折扣\n",
|
||||
"4. 思考:哪些字段属于输入数据,哪些字段属于中间结果,哪些字段属于最终输出?\n",
|
||||
"5. 思考:如果一个 Agent 需要反复调用工具,你觉得状态里至少应该保留哪些字段?"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
586
17_节点与边.ipynb
Normal file
586
17_节点与边.ipynb
Normal file
@@ -0,0 +1,586 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "5acad58d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 17 节点与边\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 掌握添加节点(`add_node`)和添加边(`add_edge`)的方法\n",
|
||||
"2. 理解 `START` 和 `END` 在图中的含义\n",
|
||||
"3. 能够构建包含多个节点的顺序执行图\n",
|
||||
"4. 理解节点负责做事、边负责决定顺序的分工\n",
|
||||
"5. 能够看懂并修改一个简单的图流程"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0be17af5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么要单独讲节点与边\n",
|
||||
"\n",
|
||||
"在前两节中,我们已经接触过图结构和状态管理。现在要进一步回答一个更基础的问题:\n",
|
||||
"\n",
|
||||
"**一张图到底是怎么拼出来的?**\n",
|
||||
"\n",
|
||||
"答案就是:\n",
|
||||
"\n",
|
||||
"- 用 **节点(Node)** 表示每一步做什么\n",
|
||||
"- 用 **边(Edge)** 表示下一步往哪里走\n",
|
||||
"\n",
|
||||
"也就是说:\n",
|
||||
"\n",
|
||||
"- 节点负责‘干活’\n",
|
||||
"- 边负责‘指路’\n",
|
||||
"\n",
|
||||
"你可以把它想象成一张旅游路线图:\n",
|
||||
"\n",
|
||||
"- 景点 = 节点\n",
|
||||
"- 景点之间的道路 = 边\n",
|
||||
"- 你游览的先后顺序 = 图的执行顺序\n",
|
||||
"\n",
|
||||
"如果没有节点,图里就没有真正执行的步骤;如果没有边,这些步骤之间就连不起来。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e7e11691",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 节点是什么\n",
|
||||
"\n",
|
||||
"在 LangGraph 中,节点通常就是一个 Python 函数。\n",
|
||||
"\n",
|
||||
"这个函数会:\n",
|
||||
"\n",
|
||||
"1. 接收当前状态 `state`\n",
|
||||
"2. 处理数据\n",
|
||||
"3. 返回要更新的状态字段\n",
|
||||
"\n",
|
||||
"例如一个节点可以做这些事:\n",
|
||||
"\n",
|
||||
"- 给数字加 1\n",
|
||||
"- 拼接一段字符串\n",
|
||||
"- 计算总价\n",
|
||||
"- 调用大模型\n",
|
||||
"- 调用工具\n",
|
||||
"\n",
|
||||
"所以,节点本质上就是流程中的‘处理步骤’。\n",
|
||||
"\n",
|
||||
"一个图里可以有一个节点,也可以有很多节点。节点越多,流程通常越细。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "9599df9d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 边是什么\n",
|
||||
"\n",
|
||||
"边表示节点和节点之间的连接关系。\n",
|
||||
"\n",
|
||||
"如果说节点回答的是:\n",
|
||||
"\n",
|
||||
"> 这一步做什么?\n",
|
||||
"\n",
|
||||
"那么边回答的就是:\n",
|
||||
"\n",
|
||||
"> 做完这一步以后,下一步去哪?\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> node_a -> node_b -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这里就有三条边:\n",
|
||||
"\n",
|
||||
"- `START -> node_a`\n",
|
||||
"- `node_a -> node_b`\n",
|
||||
"- `node_b -> END`\n",
|
||||
"\n",
|
||||
"所以,边并不负责处理数据,它只负责规定执行顺序。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "707d8841",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. `START` 和 `END` 是什么\n",
|
||||
"\n",
|
||||
"在 LangGraph 中,我们通常会看到两个特殊标记:\n",
|
||||
"\n",
|
||||
"- `START`:图的起点\n",
|
||||
"- `END`:图的终点\n",
|
||||
"\n",
|
||||
"它们不是普通业务节点,而是系统提供的特殊位置。\n",
|
||||
"\n",
|
||||
"你可以把它们理解成:\n",
|
||||
"\n",
|
||||
"- `START` = 流程从哪里进入\n",
|
||||
"- `END` = 流程在哪里结束\n",
|
||||
"\n",
|
||||
"注意:\n",
|
||||
"\n",
|
||||
"- `START` 不负责处理数据\n",
|
||||
"- `END` 也不负责处理数据\n",
|
||||
"- 它们的作用只是帮助我们把流程的起点和终点标出来\n",
|
||||
"\n",
|
||||
"因此,很多图都长这样:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> 第一个节点 -> 第二个节点 -> END\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1ddf8a66",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 第一个例子:一个节点的最小图\n",
|
||||
"\n",
|
||||
"先从最简单的图开始,只放一个节点。\n",
|
||||
"\n",
|
||||
"流程如下:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> greet -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"也就是说:\n",
|
||||
"\n",
|
||||
"1. 流程从 `START` 进入\n",
|
||||
"2. 执行 `greet` 节点\n",
|
||||
"3. 执行完后直接结束"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d23c2d17",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class HelloState(TypedDict):\n",
|
||||
" name: str\n",
|
||||
" message: str\n",
|
||||
"\n",
|
||||
"def greet(state: HelloState):\n",
|
||||
" return {'message': f'你好,{state[\"name\"]}!'}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(HelloState)\n",
|
||||
"builder.add_node('greet', greet)\n",
|
||||
"builder.add_edge(START, 'greet')\n",
|
||||
"builder.add_edge('greet', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"result = graph.invoke({'name': '小明', 'message': ''})\n",
|
||||
"print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "93b3f4bc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子非常短,但已经完整包含了节点与边的核心写法。\n",
|
||||
"\n",
|
||||
"#### `class HelloState(TypedDict)`\n",
|
||||
"这里定义了图中的状态结构,包含两个字段:\n",
|
||||
"\n",
|
||||
"- `name`:输入的人名\n",
|
||||
"- `message`:节点生成的问候语\n",
|
||||
"\n",
|
||||
"#### `def greet(state)`\n",
|
||||
"这就是一个节点函数。它读取 `name`,然后生成一句问候语,写入 `message`。\n",
|
||||
"\n",
|
||||
"也就是说,这个节点真正做的事情是:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"读取名字 -> 生成问候 -> 写回状态\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"#### `builder.add_node('greet', greet)`\n",
|
||||
"这是把节点加入图中的关键代码。\n",
|
||||
"\n",
|
||||
"它的意思是:\n",
|
||||
"\n",
|
||||
"- 节点名字叫 `greet`\n",
|
||||
"- 节点实际执行的函数是 `greet`\n",
|
||||
"\n",
|
||||
"你可以把前面的字符串 `'greet'` 理解为节点标签,把后面的函数 `greet` 理解为节点的工作内容。\n",
|
||||
"\n",
|
||||
"#### `builder.add_edge(START, 'greet')`\n",
|
||||
"表示流程从起点进入 `greet` 节点。\n",
|
||||
"\n",
|
||||
"#### `builder.add_edge('greet', END)`\n",
|
||||
"表示执行完 `greet` 节点之后,流程直接结束。\n",
|
||||
"\n",
|
||||
"#### 整体流程\n",
|
||||
"整个图的执行路径就是:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> greet -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这是最小的完整图,也是理解节点与边的最好起点。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "926caf0b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 两个节点的顺序图\n",
|
||||
"\n",
|
||||
"一个节点的图很简单,但很多实际流程至少会有两个步骤。\n",
|
||||
"\n",
|
||||
"下面我们做一个两步流程:\n",
|
||||
"\n",
|
||||
"1. 先生成问候语\n",
|
||||
"2. 再给问候语后面补一句欢迎信息\n",
|
||||
"\n",
|
||||
"流程如下:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> make_greeting -> add_welcome -> END\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "0a529a83",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class WelcomeState(TypedDict):\n",
|
||||
" name: str\n",
|
||||
" greeting: str\n",
|
||||
" final_text: str\n",
|
||||
"\n",
|
||||
"def make_greeting(state: WelcomeState):\n",
|
||||
" return {'greeting': f'你好,{state[\"name\"]}!'}\n",
|
||||
"\n",
|
||||
"def add_welcome(state: WelcomeState):\n",
|
||||
" return {'final_text': state['greeting'] + ' 欢迎学习 LangGraph。'}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(WelcomeState)\n",
|
||||
"builder.add_node('make_greeting', make_greeting)\n",
|
||||
"builder.add_node('add_welcome', add_welcome)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'make_greeting')\n",
|
||||
"builder.add_edge('make_greeting', 'add_welcome')\n",
|
||||
"builder.add_edge('add_welcome', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"result = graph.invoke({\n",
|
||||
" 'name': '小红',\n",
|
||||
" 'greeting': '',\n",
|
||||
" 'final_text': ''\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print(result)\n",
|
||||
"print(result['final_text'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a2dd0d07",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子比前一个例子多了一个节点,因此也多了一条边。\n",
|
||||
"\n",
|
||||
"#### 两个节点分别做什么\n",
|
||||
"- `make_greeting`:生成问候语\n",
|
||||
"- `add_welcome`:在问候语后面追加欢迎信息\n",
|
||||
"\n",
|
||||
"这两个节点就像工厂里的两个工位:\n",
|
||||
"\n",
|
||||
"- 第一个工位先加工出半成品\n",
|
||||
"- 第二个工位接着加工成完整成品\n",
|
||||
"\n",
|
||||
"#### 为什么这里要有三条边\n",
|
||||
"因为整个流程要从头走到尾,所以必须把三段路径都写出来:\n",
|
||||
"\n",
|
||||
"1. `START -> make_greeting`\n",
|
||||
"2. `make_greeting -> add_welcome`\n",
|
||||
"3. `add_welcome -> END`\n",
|
||||
"\n",
|
||||
"少写任何一条,图的路径都会不完整。\n",
|
||||
"\n",
|
||||
"#### `add_edge('make_greeting', 'add_welcome')` 的意义\n",
|
||||
"这行代码非常关键,它表达的是:\n",
|
||||
"\n",
|
||||
"> 第一个节点做完以后,不是结束,而是继续把结果交给第二个节点。\n",
|
||||
"\n",
|
||||
"所以我们可以说:\n",
|
||||
"\n",
|
||||
"- 节点负责定义每一步做什么\n",
|
||||
"- 边负责把这些步骤串成一条完整路线\n",
|
||||
"\n",
|
||||
"#### 整个执行过程\n",
|
||||
"执行时,状态会这样流动:\n",
|
||||
"\n",
|
||||
"1. 初始状态进入 `make_greeting`\n",
|
||||
"2. 生成 `greeting='你好,小红!'`\n",
|
||||
"3. 再进入 `add_welcome`\n",
|
||||
"4. 生成 `final_text='你好,小红! 欢迎学习 LangGraph。'`\n",
|
||||
"5. 最后进入 `END`\n",
|
||||
"\n",
|
||||
"这就是最典型的多节点顺序图。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "41a9b843",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 三个节点的顺序图\n",
|
||||
"\n",
|
||||
"为了进一步理解边的作用,我们再看一个三个节点的例子。\n",
|
||||
"\n",
|
||||
"流程目标:\n",
|
||||
"\n",
|
||||
"1. 输入一个数字\n",
|
||||
"2. 先加 2\n",
|
||||
"3. 再乘 3\n",
|
||||
"4. 最后减 1\n",
|
||||
"\n",
|
||||
"流程图如下:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> add_two -> multiply_three -> minus_one -> END\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a95e97f2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class NumberState(TypedDict):\n",
|
||||
" value: int\n",
|
||||
"\n",
|
||||
"def add_two(state: NumberState):\n",
|
||||
" return {'value': state['value'] + 2}\n",
|
||||
"\n",
|
||||
"def multiply_three(state: NumberState):\n",
|
||||
" return {'value': state['value'] * 3}\n",
|
||||
"\n",
|
||||
"def minus_one(state: NumberState):\n",
|
||||
" return {'value': state['value'] - 1}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(NumberState)\n",
|
||||
"builder.add_node('add_two', add_two)\n",
|
||||
"builder.add_node('multiply_three', multiply_three)\n",
|
||||
"builder.add_node('minus_one', minus_one)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'add_two')\n",
|
||||
"builder.add_edge('add_two', 'multiply_three')\n",
|
||||
"builder.add_edge('multiply_three', 'minus_one')\n",
|
||||
"builder.add_edge('minus_one', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"result = graph.invoke({'value': 4})\n",
|
||||
"print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e095e193",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子的重点是:**节点一多,边就必须更清楚地把顺序连接起来。**\n",
|
||||
"\n",
|
||||
"#### 三个节点的分工\n",
|
||||
"- `add_two`:把数字加 2\n",
|
||||
"- `multiply_three`:把结果乘 3\n",
|
||||
"- `minus_one`:把结果减 1\n",
|
||||
"\n",
|
||||
"每个节点都只做一件非常明确的事情。\n",
|
||||
"\n",
|
||||
"#### 四条边的作用\n",
|
||||
"这里总共有四条边:\n",
|
||||
"\n",
|
||||
"- `START -> add_two`\n",
|
||||
"- `add_two -> multiply_three`\n",
|
||||
"- `multiply_three -> minus_one`\n",
|
||||
"- `minus_one -> END`\n",
|
||||
"\n",
|
||||
"从这个例子你能很明显地看出:\n",
|
||||
"\n",
|
||||
"- 节点数量增加,边的数量通常也会跟着增加\n",
|
||||
"- 边并不是可有可无的,它是图的骨架\n",
|
||||
"\n",
|
||||
"#### 为什么顺序会影响结果\n",
|
||||
"如果初始值是 `4`,按当前顺序执行:\n",
|
||||
"\n",
|
||||
"1. `4 + 2 = 6`\n",
|
||||
"2. `6 * 3 = 18`\n",
|
||||
"3. `18 - 1 = 17`\n",
|
||||
"\n",
|
||||
"最终结果是:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'value': 17}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"如果你把边的顺序改掉,例如先乘再加,结果就会完全不同。\n",
|
||||
"\n",
|
||||
"这说明一件事:\n",
|
||||
"\n",
|
||||
"**边不仅是在连线,它其实是在定义业务流程的顺序逻辑。**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f8b86cad",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 常见错误:只加节点,不连边\n",
|
||||
"\n",
|
||||
"初学者最常见的错误之一,就是把节点都加进去了,但忘了把它们连起来。\n",
|
||||
"\n",
|
||||
"例如你可能写了:\n",
|
||||
"\n",
|
||||
"- `add_node('a', node_a)`\n",
|
||||
"- `add_node('b', node_b)`\n",
|
||||
"- `add_node('c', node_c)`\n",
|
||||
"\n",
|
||||
"但如果没有边,图并不知道该怎么走。\n",
|
||||
"\n",
|
||||
"所以一定要记住:\n",
|
||||
"\n",
|
||||
"- 节点 = 把工位摆出来\n",
|
||||
"- 边 = 把工位之间的传送带接起来\n",
|
||||
"\n",
|
||||
"只有节点没有边,就像车间里摆了很多机器,但它们之间没有流水线,产品根本流不过去。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2653a1ac",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 常见错误:边的顺序写错\n",
|
||||
"\n",
|
||||
"另一个常见错误,是边都写了,但顺序写错了。\n",
|
||||
"\n",
|
||||
"比如本来你想要:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> add_two -> multiply_three -> minus_one -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"结果却写成:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> multiply_three -> add_two -> minus_one -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这样虽然图也能运行,但业务逻辑已经变了。\n",
|
||||
"\n",
|
||||
"所以在写边的时候,不要只关心‘能不能连上’,更要关心‘是不是按我想要的顺序连上’。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c519d51c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 节点与边的分工总结\n",
|
||||
"\n",
|
||||
"到这里,你可以把节点与边的关系总结成一句话:\n",
|
||||
"\n",
|
||||
"**节点定义处理步骤,边定义执行路径。**\n",
|
||||
"\n",
|
||||
"更具体一点:\n",
|
||||
"\n",
|
||||
"| 组件 | 作用 | 你可以怎么理解 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| Node(节点) | 做具体工作 | 流程中的工位 |\n",
|
||||
"| Edge(边) | 决定下一步去哪 | 工位之间的路线 |\n",
|
||||
"| START | 流程入口 | 从哪里开始 |\n",
|
||||
"| END | 流程出口 | 在哪里结束 |\n",
|
||||
"\n",
|
||||
"只要把这四个角色分清楚,写简单的图就不会乱。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "31760dde",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. 本节小结\n",
|
||||
"\n",
|
||||
"本节最重要的内容有五点:\n",
|
||||
"\n",
|
||||
"1. **节点就是处理步骤,通常由函数表示**\n",
|
||||
"2. **边就是连接关系,负责决定执行顺序**\n",
|
||||
"3. **`START` 是起点,`END` 是终点**\n",
|
||||
"4. **节点越多,越要靠边把流程顺序明确写出来**\n",
|
||||
"5. **图能不能正确运行,不仅取决于节点有没有写对,也取决于边有没有连对**\n",
|
||||
"\n",
|
||||
"理解了节点与边,后面再学条件边、分支流程、循环流程时,你会更容易看懂整张图是怎么运转的。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2de9c0d2",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12. 本节练习\n",
|
||||
"\n",
|
||||
"1. 修改第一个示例,把 `greet` 节点输出改成‘欢迎你,某某同学!’\n",
|
||||
"2. 在第二个示例中再增加一个节点 `add_goodbye`,让最后再追加一句‘祝你学习顺利!’\n",
|
||||
"3. 修改第三个示例的边顺序,观察最终计算结果如何变化\n",
|
||||
"4. 尝试自己画出三个示例的流程图\n",
|
||||
"5. 思考:如果一个图有 5 个节点,它至少需要几条边才能形成一条完整顺序路径?"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
594
18_条件边.ipynb
Normal file
594
18_条件边.ipynb
Normal file
@@ -0,0 +1,594 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 18 条件边\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解条件边(Conditional Edges)的作用和使用场景\n",
|
||||
"2. 掌握 `add_conditional_edges` 实现分支流程\n",
|
||||
"3. 能够根据状态值动态决定图的执行路径\n",
|
||||
"4. 理解路由函数在图中的作用\n",
|
||||
"5. 能够读懂并修改一个带分支的图流程"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要条件边\n",
|
||||
"\n",
|
||||
"前一节我们学习了普通边。普通边的特点是:**下一步固定不变**。\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> node_a -> node_b -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这条流程里,不管输入是什么,`node_a` 做完以后都会进入 `node_b`。\n",
|
||||
"\n",
|
||||
"但真实业务中,流程往往不是固定直走的,而是会根据情况分支。比如:\n",
|
||||
"\n",
|
||||
"- 分数及格,就进入‘通过’节点\n",
|
||||
"- 分数不及格,就进入‘补考’节点\n",
|
||||
"- 用户问题完整,就直接回答\n",
|
||||
"- 用户问题不完整,就先追问\n",
|
||||
"- 检索结果足够,就结束\n",
|
||||
"- 检索结果不够,就继续查\n",
|
||||
"\n",
|
||||
"这类‘根据当前状态决定下一步去哪’的场景,就要用到**条件边**。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 条件边和普通边的区别\n",
|
||||
"\n",
|
||||
"可以把两种边对比着理解:\n",
|
||||
"\n",
|
||||
"| 类型 | 决定方式 | 典型场景 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 普通边 `add_edge` | 固定写死 | 顺序执行流程 |\n",
|
||||
"| 条件边 `add_conditional_edges` | 根据状态动态判断 | 分支、回路、路由控制 |\n",
|
||||
"\n",
|
||||
"最核心的区别只有一句话:\n",
|
||||
"\n",
|
||||
"- 普通边:下一步是提前写死的\n",
|
||||
"- 条件边:下一步是运行时判断出来的\n",
|
||||
"\n",
|
||||
"也就是说,条件边让图开始‘有判断力’。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 条件边的基本思路\n",
|
||||
"\n",
|
||||
"使用条件边时,通常有三样东西要配合起来:\n",
|
||||
"\n",
|
||||
"1. **一个普通节点**:先处理当前状态\n",
|
||||
"2. **一个路由函数**:根据状态判断下一步去哪\n",
|
||||
"3. **一个分支映射表**:告诉图不同返回值分别对应哪个节点\n",
|
||||
"\n",
|
||||
"写法通常像这样:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" '某个节点',\n",
|
||||
" route_function,\n",
|
||||
" {\n",
|
||||
" '路径A': 'node_a',\n",
|
||||
" '路径B': 'node_b'\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"它的意思是:\n",
|
||||
"\n",
|
||||
"- 先执行‘某个节点’\n",
|
||||
"- 执行完以后,不直接写死下一步\n",
|
||||
"- 而是调用 `route_function(state)` 看返回什么\n",
|
||||
"- 如果返回 `'路径A'`,就走到 `node_a`\n",
|
||||
"- 如果返回 `'路径B'`,就走到 `node_b`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 第一个例子:根据分数决定是否通过\n",
|
||||
"\n",
|
||||
"先从最直观的例子开始。\n",
|
||||
"\n",
|
||||
"流程目标:\n",
|
||||
"\n",
|
||||
"- 如果分数大于等于 60,进入通过节点\n",
|
||||
"- 如果分数小于 60,进入补考节点\n",
|
||||
"\n",
|
||||
"流程图如下:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> check_score -> pass_node / fail_node -> END\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class ScoreState(TypedDict):\n",
|
||||
" score: int\n",
|
||||
" result: str\n",
|
||||
"\n",
|
||||
"def check_score(state: ScoreState):\n",
|
||||
" print(f'当前分数:{state[\"score\"]}')\n",
|
||||
" return {}\n",
|
||||
"\n",
|
||||
"def pass_node(state: ScoreState):\n",
|
||||
" return {'result': '成绩合格,顺利通过'}\n",
|
||||
"\n",
|
||||
"def fail_node(state: ScoreState):\n",
|
||||
" return {'result': '成绩不合格,需要补考'}\n",
|
||||
"\n",
|
||||
"def route_by_score(state: ScoreState):\n",
|
||||
" if state['score'] >= 60:\n",
|
||||
" return 'pass'\n",
|
||||
" return 'fail'\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ScoreState)\n",
|
||||
"builder.add_node('check_score', check_score)\n",
|
||||
"builder.add_node('pass_node', pass_node)\n",
|
||||
"builder.add_node('fail_node', fail_node)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'check_score')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'check_score',\n",
|
||||
" route_by_score,\n",
|
||||
" {\n",
|
||||
" 'pass': 'pass_node',\n",
|
||||
" 'fail': 'fail_node'\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"builder.add_edge('pass_node', END)\n",
|
||||
"builder.add_edge('fail_node', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"print('分数 85:')\n",
|
||||
"print(graph.invoke({'score': 85, 'result': ''}))\n",
|
||||
"\n",
|
||||
"print('\\n分数 45:')\n",
|
||||
"print(graph.invoke({'score': 45, 'result': ''}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子非常典型,几乎把条件边最核心的要点都展示出来了。\n",
|
||||
"\n",
|
||||
"#### 状态结构\n",
|
||||
"状态里有两个字段:\n",
|
||||
"\n",
|
||||
"- `score`:输入分数\n",
|
||||
"- `result`:最终判断结果\n",
|
||||
"\n",
|
||||
"#### `check_score` 节点\n",
|
||||
"这个节点本身没有修改状态,只是打印当前分数。\n",
|
||||
"\n",
|
||||
"它返回 `{}`,表示‘这个节点不更新任何字段’。\n",
|
||||
"\n",
|
||||
"这说明一个节点不一定非要负责生成结果,它也可以只是一个‘分支前的准备步骤’。\n",
|
||||
"\n",
|
||||
"#### `pass_node` 和 `fail_node`\n",
|
||||
"这两个节点分别代表分支后的两条路径:\n",
|
||||
"\n",
|
||||
"- 通过路径:写入‘成绩合格,顺利通过’\n",
|
||||
"- 失败路径:写入‘成绩不合格,需要补考’\n",
|
||||
"\n",
|
||||
"#### `route_by_score(state)`\n",
|
||||
"这是本例的路由函数。它的任务不是修改状态,而是‘判断下一步去哪’。\n",
|
||||
"\n",
|
||||
"判断逻辑很简单:\n",
|
||||
"\n",
|
||||
"- 如果 `score >= 60`,返回 `'pass'`\n",
|
||||
"- 否则返回 `'fail'`\n",
|
||||
"\n",
|
||||
"注意:这里返回的不是节点函数本身,而是一个**分支标记**。\n",
|
||||
"\n",
|
||||
"#### `add_conditional_edges(...)`\n",
|
||||
"这一段可以拆成三层意思:\n",
|
||||
"\n",
|
||||
"1. 从 `check_score` 节点出来以后,不直接写死下一步\n",
|
||||
"2. 调用 `route_by_score(state)` 得到一个返回值\n",
|
||||
"3. 根据映射表决定真正跳去哪个节点\n",
|
||||
"\n",
|
||||
"也就是说:\n",
|
||||
"\n",
|
||||
"- 返回 `'pass'` -> 去 `pass_node`\n",
|
||||
"- 返回 `'fail'` -> 去 `fail_node`\n",
|
||||
"\n",
|
||||
"#### 执行路径是怎么变化的\n",
|
||||
"当输入 `score=85` 时:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> check_score -> pass_node -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"当输入 `score=45` 时:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> check_score -> fail_node -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这就是条件边最基础的价值:**同一张图,面对不同状态时,可以自动走不同路线。**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 第二个例子:问题完整就直接回答,不完整就先追问\n",
|
||||
"\n",
|
||||
"下面我们做一个更贴近 AI 助手的例子。\n",
|
||||
"\n",
|
||||
"假设用户提问时,有的问题已经很清楚,有的问题信息还不够。\n",
|
||||
"\n",
|
||||
"流程目标:\n",
|
||||
"\n",
|
||||
"- 如果问题完整,直接进入回答节点\n",
|
||||
"- 如果问题不完整,先进入追问节点\n",
|
||||
"\n",
|
||||
"这个例子可以帮助你理解:条件边不仅能处理分数、数字,也能处理业务判断。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class QuestionState(TypedDict):\n",
|
||||
" user_question: str\n",
|
||||
" is_complete: bool\n",
|
||||
" response: str\n",
|
||||
"\n",
|
||||
"def analyze_question(state: QuestionState):\n",
|
||||
" print(f'收到问题:{state[\"user_question\"]}')\n",
|
||||
" return {}\n",
|
||||
"\n",
|
||||
"def ask_more(state: QuestionState):\n",
|
||||
" return {'response': '你的问题还不够完整,请补充更多背景信息。'}\n",
|
||||
"\n",
|
||||
"def answer_directly(state: QuestionState):\n",
|
||||
" return {'response': '问题信息已经足够,我现在直接为你回答。'}\n",
|
||||
"\n",
|
||||
"def route_question(state: QuestionState):\n",
|
||||
" if state['is_complete']:\n",
|
||||
" return 'answer'\n",
|
||||
" return 'ask_more'\n",
|
||||
"\n",
|
||||
"builder = StateGraph(QuestionState)\n",
|
||||
"builder.add_node('analyze_question', analyze_question)\n",
|
||||
"builder.add_node('ask_more', ask_more)\n",
|
||||
"builder.add_node('answer_directly', answer_directly)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'analyze_question')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'analyze_question',\n",
|
||||
" route_question,\n",
|
||||
" {\n",
|
||||
" 'answer': 'answer_directly',\n",
|
||||
" 'ask_more': 'ask_more'\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"builder.add_edge('ask_more', END)\n",
|
||||
"builder.add_edge('answer_directly', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"print('完整问题:')\n",
|
||||
"print(graph.invoke({\n",
|
||||
" 'user_question': '请解释什么是条件边,并举一个例子',\n",
|
||||
" 'is_complete': True,\n",
|
||||
" 'response': ''\n",
|
||||
"}))\n",
|
||||
"\n",
|
||||
"print('\\n不完整问题:')\n",
|
||||
"print(graph.invoke({\n",
|
||||
" 'user_question': '帮我看看这个',\n",
|
||||
" 'is_complete': False,\n",
|
||||
" 'response': ''\n",
|
||||
"}))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子和前面的‘分数判断’相比,更接近真实 AI 产品中的流程控制。\n",
|
||||
"\n",
|
||||
"#### 状态字段的意义\n",
|
||||
"这里的状态有三个字段:\n",
|
||||
"\n",
|
||||
"- `user_question`:用户输入的问题\n",
|
||||
"- `is_complete`:问题是否完整\n",
|
||||
"- `response`:最终回复\n",
|
||||
"\n",
|
||||
"#### `analyze_question` 节点\n",
|
||||
"这个节点先接收问题并打印出来,相当于‘进入分析阶段’。\n",
|
||||
"\n",
|
||||
"它没有直接修改状态,而是把路由判断交给后面的路由函数。\n",
|
||||
"\n",
|
||||
"#### `route_question(state)`\n",
|
||||
"这个函数根据 `is_complete` 的真假做判断:\n",
|
||||
"\n",
|
||||
"- `True` -> 返回 `'answer'`\n",
|
||||
"- `False` -> 返回 `'ask_more'`\n",
|
||||
"\n",
|
||||
"这意味着流程下一步是动态的,而不是提前写死的。\n",
|
||||
"\n",
|
||||
"#### 两条分支的业务含义\n",
|
||||
"- `answer_directly`:说明信息足够,可以直接处理\n",
|
||||
"- `ask_more`:说明信息不足,需要先追问\n",
|
||||
"\n",
|
||||
"#### 为什么这个例子很重要\n",
|
||||
"它让你看到:条件边并不只是‘程序里的 if 判断’,更像是业务流程里的‘路由器’。\n",
|
||||
"\n",
|
||||
"很多 Agent 系统都会有类似逻辑:\n",
|
||||
"\n",
|
||||
"- 信息够不够?\n",
|
||||
"- 要不要调用工具?\n",
|
||||
"- 要不要继续搜索?\n",
|
||||
"- 能不能直接输出答案?\n",
|
||||
"\n",
|
||||
"而这些判断,往往都可以通过条件边表达出来。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 第三个例子:条件边也可以用于继续或结束\n",
|
||||
"\n",
|
||||
"条件边不仅可以做左右分支,还经常用来做‘继续/停止’的判断。\n",
|
||||
"\n",
|
||||
"这类场景非常常见,例如:\n",
|
||||
"\n",
|
||||
"- 检索结果不够,就继续检索\n",
|
||||
"- 输出格式不对,就继续生成\n",
|
||||
"- 数量还没到要求,就继续累加\n",
|
||||
"\n",
|
||||
"下面用一个简单例子说明:如果 `count < 3`,就继续执行;否则结束。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class LoopState(TypedDict):\n",
|
||||
" count: int\n",
|
||||
"\n",
|
||||
"def add_one(state: LoopState):\n",
|
||||
" new_count = state['count'] + 1\n",
|
||||
" print(f'当前 count 从 {state[\"count\"]} 变成 {new_count}')\n",
|
||||
" return {'count': new_count}\n",
|
||||
"\n",
|
||||
"def route_loop(state: LoopState):\n",
|
||||
" if state['count'] < 3:\n",
|
||||
" return 'continue'\n",
|
||||
" return 'stop'\n",
|
||||
"\n",
|
||||
"builder = StateGraph(LoopState)\n",
|
||||
"builder.add_node('add_one', add_one)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'add_one')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'add_one',\n",
|
||||
" route_loop,\n",
|
||||
" {\n",
|
||||
" 'continue': 'add_one',\n",
|
||||
" 'stop': END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"result = graph.invoke({'count': 0})\n",
|
||||
"print('最终结果:', result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子非常重要,因为它说明:**条件边不仅能分叉,也能回跳。**\n",
|
||||
"\n",
|
||||
"#### `add_one` 节点\n",
|
||||
"这个节点每次把 `count` 加 1。\n",
|
||||
"\n",
|
||||
"也就是说,它是一个不断重复执行的工作步骤。\n",
|
||||
"\n",
|
||||
"#### `route_loop(state)`\n",
|
||||
"这个路由函数负责判断接下来是继续,还是停止:\n",
|
||||
"\n",
|
||||
"- 如果 `count < 3`,返回 `'continue'`\n",
|
||||
"- 否则返回 `'stop'`\n",
|
||||
"\n",
|
||||
"#### 条件边映射\n",
|
||||
"这里映射关系很关键:\n",
|
||||
"\n",
|
||||
"- `'continue'` -> `'add_one'`\n",
|
||||
"- `'stop'` -> `END`\n",
|
||||
"\n",
|
||||
"也就是说:\n",
|
||||
"\n",
|
||||
"- 还没满足条件,就重新回到原节点\n",
|
||||
"- 满足条件后,就离开流程\n",
|
||||
"\n",
|
||||
"#### 执行过程\n",
|
||||
"如果初始值是 `count=0`,路径会是:\n",
|
||||
"\n",
|
||||
"1. `0 -> 1`,继续\n",
|
||||
"2. `1 -> 2`,继续\n",
|
||||
"3. `2 -> 3`,停止\n",
|
||||
"4. 进入 `END`\n",
|
||||
"\n",
|
||||
"从这个例子你可以看到,条件边不只是“二选一分支”,它还是循环流程的基础。\n",
|
||||
"\n",
|
||||
"后面很多 Agent 的‘反复尝试直到成功’流程,本质上都是这种写法。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 路由函数应该怎么理解\n",
|
||||
"\n",
|
||||
"很多初学者最容易混淆的地方,就是把‘节点函数’和‘路由函数’混在一起。\n",
|
||||
"\n",
|
||||
"你可以这样区分:\n",
|
||||
"\n",
|
||||
"| 类型 | 主要职责 | 是否负责业务处理 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 节点函数 | 读写状态、执行处理逻辑 | 是 |\n",
|
||||
"| 路由函数 | 判断下一步走哪条路 | 通常不是重点 |\n",
|
||||
"\n",
|
||||
"换句话说:\n",
|
||||
"\n",
|
||||
"- 节点函数更像‘工人’\n",
|
||||
"- 路由函数更像‘调度员’\n",
|
||||
"\n",
|
||||
"工人负责真正干活,调度员负责决定下一站去哪。\n",
|
||||
"\n",
|
||||
"理解这一点以后,条件边的结构就会非常清晰。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 使用条件边时的常见错误\n",
|
||||
"\n",
|
||||
"### 8.1 路由函数返回值和映射表对不上\n",
|
||||
"例如路由函数返回 `'pass'`,但映射表里只有 `'ok'` 和 `'fail'`。\n",
|
||||
"\n",
|
||||
"这样图就不知道该往哪里走。\n",
|
||||
"\n",
|
||||
"### 8.2 把节点名字和路由标记混为一谈\n",
|
||||
"路由函数返回的可以是一个标记,例如 `'continue'`、`'stop'`、`'pass'`。\n",
|
||||
"\n",
|
||||
"这些标记不一定要和节点名完全相同,但必须和映射表中的 key 对应。\n",
|
||||
"\n",
|
||||
"### 8.3 以为条件边只能做两条分支\n",
|
||||
"其实不止两条。\n",
|
||||
"\n",
|
||||
"你完全可以写三条、四条甚至更多路线,只要业务场景需要。\n",
|
||||
"\n",
|
||||
"### 8.4 忘记给分支节点连到 `END` 或后续节点\n",
|
||||
"条件边只负责从当前节点分出去,不代表后面的路径已经自动补全。\n",
|
||||
"\n",
|
||||
"分支出去以后,该怎么结束、怎么汇合,仍然要继续写边。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 条件边在 Agent 场景中的意义\n",
|
||||
"\n",
|
||||
"到了这里,你可以把条件边理解成 LangGraph 里的‘流程决策器’。\n",
|
||||
"\n",
|
||||
"在 Agent 场景中,它特别常见,因为 Agent 天生就不是固定直线流程。\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"- 如果用户问题明确,直接回答\n",
|
||||
"- 如果用户问题不明确,先追问\n",
|
||||
"- 如果需要外部信息,调用工具\n",
|
||||
"- 如果工具结果不够,再查一次\n",
|
||||
"- 如果结果足够,就进入最终回答\n",
|
||||
"\n",
|
||||
"你会发现,这些逻辑本质上都属于:\n",
|
||||
"\n",
|
||||
"**根据当前状态,动态决定下一步去哪。**\n",
|
||||
"\n",
|
||||
"这正是条件边最核心的价值。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 本节小结\n",
|
||||
"\n",
|
||||
"本节最重要的内容有五点:\n",
|
||||
"\n",
|
||||
"1. **条件边用于动态决定流程路径**\n",
|
||||
"2. **`add_conditional_edges` 是实现分支和回路的关键方法**\n",
|
||||
"3. **路由函数负责返回分支标记,映射表负责把标记映射到节点**\n",
|
||||
"4. **条件边不仅能做左右分支,也能做继续/停止这样的循环控制**\n",
|
||||
"5. **很多 Agent 工作流,本质上都是条件边在控制路径**\n",
|
||||
"\n",
|
||||
"理解了条件边,你就真正进入了‘动态流程控制’这一层。后面的复杂图,大多都离不开它。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. 本节练习\n",
|
||||
"\n",
|
||||
"1. 修改第一个示例,把及格线从 60 改成 80,观察分支结果变化\n",
|
||||
"2. 修改第二个示例,再增加一种情况:如果问题为空,就返回一个专门的提示节点\n",
|
||||
"3. 修改第三个示例,把停止条件从 `count < 3` 改成 `count < 5`\n",
|
||||
"4. 思考:如果一个流程有 3 条不同分支,路由函数和映射表应该怎么设计?\n",
|
||||
"5. 思考:条件边和普通边最大的本质区别是什么?"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
831
19_循环与记忆.ipynb
Normal file
831
19_循环与记忆.ipynb
Normal file
@@ -0,0 +1,831 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 19 循环与记忆\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解LangGraph中循环和持久化记忆的实现方式\n",
|
||||
"2. 掌握MemorySaver的使用,实现对话状态持久化\n",
|
||||
"3. 能够构建支持多轮交互的智能体流程"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要循环和记忆\n",
|
||||
"\n",
|
||||
"在之前的课程中,我们构建的图流程都是**单次执行**的:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> node_a -> node_b -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这种流程跑一次就结束了。但真实场景中,很多需求需要**多次循环**和**记忆状态**:\n",
|
||||
"\n",
|
||||
"- **聊天机器人**:需要记住之前说过的话,才能进行多轮对话\n",
|
||||
"- **任务分解**:可能需要反复调用工具,直到任务完成\n",
|
||||
"- **信息收集**:需要逐步收集用户的信息,直到完整\n",
|
||||
"- **多用户系统**:每个用户的对话历史需要独立保存\n",
|
||||
"\n",
|
||||
"简单来说:\n",
|
||||
"\n",
|
||||
"- **循环**:让流程可以重复执行某些节点\n",
|
||||
"- **记忆**:让状态可以在多次执行之间保持"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 循环的实现方式\n",
|
||||
"\n",
|
||||
"在LangGraph中,循环主要通过**条件边**来实现。关键思路是:\n",
|
||||
"\n",
|
||||
"1. 执行某个节点\n",
|
||||
"2. 判断是否需要继续循环\n",
|
||||
"3. 如果需要,回到之前的节点重新执行\n",
|
||||
"4. 如果不需要,走向结束\n",
|
||||
"\n",
|
||||
"流程图如下:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> work_node -> 判断节点\n",
|
||||
" |\n",
|
||||
" ┌─────────┴─────────┐\n",
|
||||
" ▼ ▼\n",
|
||||
" 继续循环 结束\n",
|
||||
" | |\n",
|
||||
" └───────► work_node ◄──┘\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"用条件边实现就是:`判断节点` 根据状态返回 `'loop'` 或 `'end'`,然后:\n",
|
||||
"\n",
|
||||
"- `'loop'` → 回到 `work_node`\n",
|
||||
"- `'end'` → 走到 `END`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 第一个例子:简单的计数器循环\n",
|
||||
"\n",
|
||||
"我们用一个计数器来演示循环的基本实现。\n",
|
||||
"\n",
|
||||
"流程目标:\n",
|
||||
"- 从0开始计数\n",
|
||||
"- 每次加1\n",
|
||||
"- 当计数达到5时停止\n",
|
||||
"- 每次循环都打印当前计数值"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class CounterState(TypedDict):\n",
|
||||
" count: int\n",
|
||||
"\n",
|
||||
"def increment_node(state: CounterState):\n",
|
||||
" new_count = state['count'] + 1\n",
|
||||
" print(f'当前计数:{new_count}')\n",
|
||||
" return {'count': new_count}\n",
|
||||
"\n",
|
||||
"def should_continue(state: CounterState):\n",
|
||||
" if state['count'] < 5:\n",
|
||||
" return 'loop'\n",
|
||||
" return 'end'\n",
|
||||
"\n",
|
||||
"builder = StateGraph(CounterState)\n",
|
||||
"builder.add_node('increment', increment_node)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'increment')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'increment',\n",
|
||||
" should_continue,\n",
|
||||
" {\n",
|
||||
" 'loop': 'increment',\n",
|
||||
" 'end': END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"result = graph.invoke({'count': 0})\n",
|
||||
"print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"1. **State 定义**:`CounterState` 只包含一个字段 `count`,用于存储当前计数值。\n",
|
||||
"\n",
|
||||
"2. **increment_node**:每次执行时,从状态中取出 `count`,加1后放回状态。同时打印当前计数值。\n",
|
||||
"\n",
|
||||
"3. **should_continue**:路由函数,判断是否继续循环。如果 `count < 5` 返回 `'loop'`,否则返回 `'end'`。\n",
|
||||
"\n",
|
||||
"4. **条件边设置**:\n",
|
||||
" - 来源节点是 `'increment'`\n",
|
||||
" - 路由函数是 `should_continue`\n",
|
||||
" - 如果返回 `'loop'`,就回到 `'increment'` 节点(形成循环)\n",
|
||||
" - 如果返回 `'end'`,就走到 `END`\n",
|
||||
"\n",
|
||||
"5. **执行**:初始状态是 `{'count': 0}`,图会自动循环执行,直到 `count` 达到5。\n",
|
||||
"\n",
|
||||
"运行结果显示计数从1到5,说明循环成功执行了5次。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 状态累积:消息历史的保存\n",
|
||||
"\n",
|
||||
"循环的核心价值在于**状态累积**。最常见的场景是保存对话历史。\n",
|
||||
"\n",
|
||||
"在聊天机器人中,每次用户输入和AI回复都需要保存下来,这样AI才能理解上下文。\n",
|
||||
"\n",
|
||||
"我们来构建一个简单的对话系统,每次循环都会把消息追加到历史中。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class ChatState(TypedDict):\n",
|
||||
" messages: list\n",
|
||||
" current_user_input: str\n",
|
||||
"\n",
|
||||
"def process_message(state: ChatState):\n",
|
||||
" user_input = state['current_user_input']\n",
|
||||
" \n",
|
||||
" responses = {\n",
|
||||
" '你好': '你好!我是一个AI助手。',\n",
|
||||
" '我叫张三': '你好张三!很高兴认识你。',\n",
|
||||
" '我今天心情很好': '太好了!祝你有美好的一天!'\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" assistant_reply = responses.get(user_input, '抱歉,我不太理解。')\n",
|
||||
"\n",
|
||||
" new_message = {'user': user_input, 'assistant': assistant_reply}\n",
|
||||
" \n",
|
||||
" return {\n",
|
||||
" 'messages': state['messages'] + [new_message],\n",
|
||||
" 'current_user_input': ''\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"def route(state: ChatState):\n",
|
||||
" if state['current_user_input']:\n",
|
||||
" return 'process'\n",
|
||||
" return END\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ChatState)\n",
|
||||
"builder.add_node('process', process_message)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'process')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'process',\n",
|
||||
" route,\n",
|
||||
" {\n",
|
||||
" 'process': 'process',\n",
|
||||
" END: END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"state = {'messages': [], 'current_user_input': '你好'}\n",
|
||||
"state = graph.invoke(state)\n",
|
||||
"\n",
|
||||
"state['current_user_input'] = '我叫张三'\n",
|
||||
"state = graph.invoke(state)\n",
|
||||
"\n",
|
||||
"state['current_user_input'] = '我今天心情很好'\n",
|
||||
"state = graph.invoke(state)\n",
|
||||
"\n",
|
||||
"print(state['messages'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"1. **State 定义**:`ChatState` 包含两个字段:\n",
|
||||
" - `messages`:保存所有对话历史的列表\n",
|
||||
" - `current_user_input`:当前用户输入\n",
|
||||
"\n",
|
||||
"2. **process_message**:处理消息的核心逻辑:\n",
|
||||
" - 从状态中获取当前用户输入\n",
|
||||
" - 根据预设的规则生成助手回复\n",
|
||||
" - 将新消息追加到 `messages` 列表中(关键!)\n",
|
||||
" - 清空 `current_user_input`\n",
|
||||
"\n",
|
||||
"3. **route**:路由函数判断是否有新输入需要处理。\n",
|
||||
"\n",
|
||||
"4. **执行流程**:\n",
|
||||
" - 第一次调用:输入'你好',消息列表变成 `[{'user': '你好', ...}]`\n",
|
||||
" - 第二次调用:输入'我叫张三',消息列表变成两条记录\n",
|
||||
" - 第三次调用:输入'我今天心情很好',消息列表变成三条记录\n",
|
||||
"\n",
|
||||
"运行结果显示消息历史被正确累积,这就是状态持久化的基础。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. MemorySaver:持久化状态存储\n",
|
||||
"\n",
|
||||
"上面的例子中,我们手动传递状态。但在真实应用中,状态需要:\n",
|
||||
"\n",
|
||||
"- 在多次请求之间保持\n",
|
||||
"- 支持多个用户同时使用(会话隔离)\n",
|
||||
"- 重启后不丢失\n",
|
||||
"\n",
|
||||
"LangGraph 提供了 `MemorySaver` 来解决这个问题。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"class ChatState(TypedDict):\n",
|
||||
" messages: list\n",
|
||||
" current_user_input: str\n",
|
||||
"\n",
|
||||
"def process_message(state: ChatState):\n",
|
||||
" user_input = state['current_user_input']\n",
|
||||
" \n",
|
||||
" responses = {\n",
|
||||
" '你好': '你好!我是一个AI助手。',\n",
|
||||
" '我叫张三': '你好张三!很高兴认识你。',\n",
|
||||
" '我今天心情很好': '太好了!祝你有美好的一天!'\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" assistant_reply = responses.get(user_input, '抱歉,我不太理解。')\n",
|
||||
"\n",
|
||||
" new_message = {'user': user_input, 'assistant': assistant_reply}\n",
|
||||
" \n",
|
||||
" return {\n",
|
||||
" 'messages': state['messages'] + [new_message],\n",
|
||||
" 'current_user_input': ''\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"def route(state: ChatState):\n",
|
||||
" if state['current_user_input']:\n",
|
||||
" return 'process'\n",
|
||||
" return END\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ChatState)\n",
|
||||
"builder.add_node('process', process_message)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'process')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'process',\n",
|
||||
" route,\n",
|
||||
" {\n",
|
||||
" 'process': 'process',\n",
|
||||
" END: END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = builder.compile(checkpointer=memory)\n",
|
||||
"\n",
|
||||
"config = {'configurable': {'thread_id': 'session_1'}}\n",
|
||||
"\n",
|
||||
"graph.invoke({'messages': [], 'current_user_input': '你好'}, config)\n",
|
||||
"graph.invoke({'current_user_input': '我叫张三'}, config)\n",
|
||||
"graph.invoke({'current_user_input': '我今天心情很好'}, config)\n",
|
||||
"\n",
|
||||
"final_state = graph.get_state(config)\n",
|
||||
"print(final_state.values['messages'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"1. **导入 MemorySaver**:从 `langgraph.checkpoint.memory` 导入。\n",
|
||||
"\n",
|
||||
"2. **创建 MemorySaver 实例**:`memory = MemorySaver()`\n",
|
||||
"\n",
|
||||
"3. **编译时传入 checkpointer**:`graph = builder.compile(checkpointer=memory)`\n",
|
||||
"\n",
|
||||
"4. **配置会话ID**:\n",
|
||||
" ```python\n",
|
||||
" config = {'configurable': {'thread_id': 'session_1'}}\n",
|
||||
" ```\n",
|
||||
" `thread_id` 是会话的唯一标识,不同用户用不同的 `thread_id`。\n",
|
||||
"\n",
|
||||
"5. **多次调用**:每次调用只传入新的输入,不需要手动传递状态。\n",
|
||||
"\n",
|
||||
"6. **获取状态**:`graph.get_state(config)` 可以获取指定会话的当前状态。\n",
|
||||
"\n",
|
||||
"关键区别:之前需要手动传递 `state`,现在只需传入 `config`,MemorySaver 会自动保存和恢复状态。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 多会话隔离\n",
|
||||
"\n",
|
||||
"MemorySaver 的一个重要特性是**会话隔离**。不同的 `thread_id` 会维护独立的状态。\n",
|
||||
"\n",
|
||||
"下面的例子演示两个用户同时使用同一个图,但各自的对话历史完全独立。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"class ChatState(TypedDict):\n",
|
||||
" messages: list\n",
|
||||
" current_user_input: str\n",
|
||||
"\n",
|
||||
"def process_message(state: ChatState):\n",
|
||||
" user_input = state['current_user_input']\n",
|
||||
" \n",
|
||||
" responses = {\n",
|
||||
" '你好': '你好!我是一个AI助手。',\n",
|
||||
" '我是Alice': '你好Alice!很高兴认识你。',\n",
|
||||
" '我是Bob': '你好Bob!很高兴认识你。'\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" assistant_reply = responses.get(user_input, '抱歉,我不太理解。')\n",
|
||||
"\n",
|
||||
" new_message = {'user': user_input, 'assistant': assistant_reply}\n",
|
||||
" \n",
|
||||
" return {\n",
|
||||
" 'messages': state['messages'] + [new_message],\n",
|
||||
" 'current_user_input': ''\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"def route(state: ChatState):\n",
|
||||
" if state['current_user_input']:\n",
|
||||
" return 'process'\n",
|
||||
" return END\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ChatState)\n",
|
||||
"builder.add_node('process', process_message)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'process')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'process',\n",
|
||||
" route,\n",
|
||||
" {\n",
|
||||
" 'process': 'process',\n",
|
||||
" END: END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = builder.compile(checkpointer=memory)\n",
|
||||
"\n",
|
||||
"config_a = {'configurable': {'thread_id': 'user_a'}}\n",
|
||||
"config_b = {'configurable': {'thread_id': 'user_b'}}\n",
|
||||
"\n",
|
||||
"graph.invoke({'messages': [], 'current_user_input': '你好'}, config_a)\n",
|
||||
"graph.invoke({'current_user_input': '我是Alice'}, config_a)\n",
|
||||
"\n",
|
||||
"graph.invoke({'messages': [], 'current_user_input': 'Hi'}, config_b)\n",
|
||||
"graph.invoke({'current_user_input': '我是Bob'}, config_b)\n",
|
||||
"\n",
|
||||
"state_a = graph.get_state(config_a)\n",
|
||||
"state_b = graph.get_state(config_b)\n",
|
||||
"\n",
|
||||
"print('用户A的对话历史:')\n",
|
||||
"print(state_a.values['messages'])\n",
|
||||
"print()\n",
|
||||
"print('用户B的对话历史:')\n",
|
||||
"print(state_b.values['messages'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"1. **创建两个配置**:\n",
|
||||
" - `config_a` 使用 `thread_id: 'user_a'`\n",
|
||||
" - `config_b` 使用 `thread_id: 'user_b'`\n",
|
||||
"\n",
|
||||
"2. **分别调用**:\n",
|
||||
" - 用户A发送了'你好'和'我是Alice'\n",
|
||||
" - 用户B发送了'Hi'和'我是Bob'\n",
|
||||
"\n",
|
||||
"3. **获取各自状态**:\n",
|
||||
" - 用户A的消息历史只有自己的两条消息\n",
|
||||
" - 用户B的消息历史只有自己的两条消息\n",
|
||||
"\n",
|
||||
"运行结果清楚地展示了会话隔离:两个用户的对话历史完全独立,互不干扰。\n",
|
||||
"\n",
|
||||
"这就是多用户聊天系统的核心原理。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 使用标准消息类型管理对话\n",
|
||||
"\n",
|
||||
"在实际应用中,我们通常使用 LangChain 的标准消息类型(如 `HumanMessage`、`AIMessage`)来管理对话。\n",
|
||||
"\n",
|
||||
"为了让 MemorySaver 能够自动追加消息而不是覆盖,我们需要使用 `add_messages` reducer。\n",
|
||||
"\n",
|
||||
"这种方式的特点:\n",
|
||||
"- 使用标准的消息类型,便于与LangChain组件集成\n",
|
||||
"- 自动追加消息,避免手动拼接\n",
|
||||
"- 方便与 LLM 集成"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from typing import Annotated\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langchain_core.messages import HumanMessage, AIMessage\n",
|
||||
"\n",
|
||||
"class ChatState(TypedDict):\n",
|
||||
" messages: Annotated[list, add_messages]\n",
|
||||
"\n",
|
||||
"def process_message(state: ChatState):\n",
|
||||
" last_message = state['messages'][-1]\n",
|
||||
" \n",
|
||||
" if isinstance(last_message, HumanMessage):\n",
|
||||
" user_input = last_message.content\n",
|
||||
" \n",
|
||||
" responses = {\n",
|
||||
" '你好': '你好!我是一个AI助手。',\n",
|
||||
" '我叫张三': '你好张三!很高兴认识你。',\n",
|
||||
" }\n",
|
||||
" \n",
|
||||
" assistant_reply = responses.get(user_input, '抱歉,我不太理解。')\n",
|
||||
" \n",
|
||||
" return {'messages': [AIMessage(content=assistant_reply)]}\n",
|
||||
" \n",
|
||||
" return state\n",
|
||||
"\n",
|
||||
"def route(state: ChatState):\n",
|
||||
" last_message = state['messages'][-1]\n",
|
||||
" if isinstance(last_message, HumanMessage):\n",
|
||||
" return 'process'\n",
|
||||
" return END\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ChatState)\n",
|
||||
"builder.add_node('process', process_message)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'process')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'process',\n",
|
||||
" route,\n",
|
||||
" {\n",
|
||||
" 'process': 'process',\n",
|
||||
" END: END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = builder.compile(checkpointer=memory)\n",
|
||||
"\n",
|
||||
"config = {'configurable': {'thread_id': 'session_with_messages'}}\n",
|
||||
"\n",
|
||||
"graph.invoke({'messages': [HumanMessage(content='你好')]}, config)\n",
|
||||
"graph.invoke({'messages': [HumanMessage(content='我叫张三')]}, config)\n",
|
||||
"graph.invoke({'messages': [HumanMessage(content='今天天气怎么样')]}, config)\n",
|
||||
"\n",
|
||||
"final_state = graph.get_state(config)\n",
|
||||
"for msg in final_state.values['messages']:\n",
|
||||
" if isinstance(msg, HumanMessage):\n",
|
||||
" print(f'Human: {msg.content}')\n",
|
||||
" elif isinstance(msg, AIMessage):\n",
|
||||
" print(f'AI: {msg.content}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"1. **导入 add_messages reducer**:`from langgraph.graph.message import add_messages`\n",
|
||||
" - reducer 用于控制状态字段的更新方式\n",
|
||||
" - `add_messages` 会自动将新消息追加到列表中,而不是覆盖\n",
|
||||
"\n",
|
||||
"2. **定义 ChatState**:使用 `Annotated[list, add_messages]` 定义消息字段\n",
|
||||
" - `Annotated` 是 Python 的类型提示工具,用于添加元数据\n",
|
||||
" - 这里告诉 LangGraph 对 `messages` 字段使用 `add_messages` reducer\n",
|
||||
"\n",
|
||||
"3. **导入消息类型**:\n",
|
||||
" - `HumanMessage`:用户消息\n",
|
||||
" - `AIMessage`:AI回复\n",
|
||||
"\n",
|
||||
"4. **process_message**:\n",
|
||||
" - 获取最后一条消息\n",
|
||||
" - 判断是否是用户消息\n",
|
||||
" - 如果是,生成回复并返回 `{'messages': [AIMessage(...)]}`\n",
|
||||
" - 由于使用了 `add_messages`,返回的新消息会自动追加到历史中\n",
|
||||
"\n",
|
||||
"5. **route**:\n",
|
||||
" - 如果最后一条是用户消息,继续处理\n",
|
||||
" - 如果是AI消息,说明已经处理过了,结束\n",
|
||||
"\n",
|
||||
"6. **调用方式**:每次传入 `{'messages': [HumanMessage(content=...)]}`\n",
|
||||
" - MemorySaver 会自动保存状态\n",
|
||||
" - `add_messages` reducer 会自动追加新消息\n",
|
||||
"\n",
|
||||
"这种方式的优点:\n",
|
||||
"- 使用标准的消息类型,便于与LangChain组件集成\n",
|
||||
"- 自动追加消息,避免手动拼接\n",
|
||||
"- 后续可以直接将消息列表传给LLM"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 综合案例:带工具调用的循环智能体\n",
|
||||
"\n",
|
||||
"现在我们把循环、记忆和工具调用结合起来,构建一个完整的智能体。\n",
|
||||
"\n",
|
||||
"流程目标:\n",
|
||||
"- 用户询问关于天气或时间的问题\n",
|
||||
"- 智能体判断是否需要调用工具\n",
|
||||
"- 如果需要,调用工具获取信息\n",
|
||||
"- 如果不需要,直接回答\n",
|
||||
"- 循环直到任务完成\n",
|
||||
"- 使用MemorySaver保存对话历史"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langchain_core.messages import HumanMessage, AIMessage, ToolMessage\n",
|
||||
"\n",
|
||||
"class AgentState(TypedDict):\n",
|
||||
" messages: list\n",
|
||||
" next_tool: str\n",
|
||||
"\n",
|
||||
"def decide_tool(state: AgentState):\n",
|
||||
" last_message = state['messages'][-1]\n",
|
||||
" user_input = last_message.content\n",
|
||||
" \n",
|
||||
" print(f'用户问了:{user_input}')\n",
|
||||
" \n",
|
||||
" if '几点' in user_input or '时间' in user_input:\n",
|
||||
" print('需要调用工具:get_time')\n",
|
||||
" return {'next_tool': 'get_time'}\n",
|
||||
" elif '天气' in user_input:\n",
|
||||
" print('需要调用工具:get_weather')\n",
|
||||
" return {'next_tool': 'get_weather'}\n",
|
||||
" else:\n",
|
||||
" print('不需要调用工具,直接回答')\n",
|
||||
" return {'next_tool': None}\n",
|
||||
"\n",
|
||||
"def call_tool(state: AgentState):\n",
|
||||
" tool_name = state['next_tool']\n",
|
||||
" \n",
|
||||
" if tool_name == 'get_time':\n",
|
||||
" result = '2024-01-15 14:30:00'\n",
|
||||
" elif tool_name == 'get_weather':\n",
|
||||
" result = '晴天,25度'\n",
|
||||
" else:\n",
|
||||
" result = '未知工具'\n",
|
||||
" \n",
|
||||
" print(f'调用工具 {tool_name},结果:{result}')\n",
|
||||
" \n",
|
||||
" return {\n",
|
||||
" 'messages': state['messages'] + [ToolMessage(content=result, tool_call_id='1')],\n",
|
||||
" 'next_tool': None\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"def summarize(state: AgentState):\n",
|
||||
" last_message = state['messages'][-1]\n",
|
||||
" \n",
|
||||
" if isinstance(last_message, ToolMessage):\n",
|
||||
" tool_result = last_message.content\n",
|
||||
" reply = f'总结回答:{tool_result}'\n",
|
||||
" else:\n",
|
||||
" reply = '抱歉,我不太理解你的问题。'\n",
|
||||
" \n",
|
||||
" print(reply)\n",
|
||||
" \n",
|
||||
" return {\n",
|
||||
" 'messages': state['messages'] + [AIMessage(content=reply)]\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"def route_after_decide(state: AgentState):\n",
|
||||
" if state['next_tool']:\n",
|
||||
" return 'call_tool'\n",
|
||||
" return 'summarize'\n",
|
||||
"\n",
|
||||
"def route_after_summarize(state: AgentState):\n",
|
||||
" last_message = state['messages'][-1]\n",
|
||||
" if isinstance(last_message, HumanMessage):\n",
|
||||
" return 'decide_tool'\n",
|
||||
" return END\n",
|
||||
"\n",
|
||||
"builder = StateGraph(AgentState)\n",
|
||||
"builder.add_node('decide_tool', decide_tool)\n",
|
||||
"builder.add_node('call_tool', call_tool)\n",
|
||||
"builder.add_node('summarize', summarize)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'decide_tool')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'decide_tool',\n",
|
||||
" route_after_decide,\n",
|
||||
" {\n",
|
||||
" 'call_tool': 'call_tool',\n",
|
||||
" 'summarize': 'summarize'\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"builder.add_edge('call_tool', 'summarize')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'summarize',\n",
|
||||
" route_after_summarize,\n",
|
||||
" {\n",
|
||||
" 'decide_tool': 'decide_tool',\n",
|
||||
" END: END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = builder.compile(checkpointer=memory)\n",
|
||||
"\n",
|
||||
"config = {'configurable': {'thread_id': 'tool_agent_session'}}\n",
|
||||
"\n",
|
||||
"graph.invoke({'messages': [HumanMessage(content='现在几点了')], 'next_tool': None}, config)\n",
|
||||
"\n",
|
||||
"current_state = graph.get_state(config)\n",
|
||||
"new_messages = current_state.values['messages'] + [HumanMessage(content='今天天气怎么样')]\n",
|
||||
"graph.invoke({'messages': new_messages, 'next_tool': None}, config)\n",
|
||||
"\n",
|
||||
"final_state = graph.get_state(config)\n",
|
||||
"print()\n",
|
||||
"print('完整对话历史:')\n",
|
||||
"for msg in final_state.values['messages']:\n",
|
||||
" if isinstance(msg, HumanMessage):\n",
|
||||
" print(f'Human: {msg.content}')\n",
|
||||
" elif isinstance(msg, AIMessage):\n",
|
||||
" print(f'AI: {msg.content}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个案例展示了一个完整的智能体流程:\n",
|
||||
"\n",
|
||||
"1. **AgentState**:使用 TypedDict 定义状态,包含:\n",
|
||||
" - `messages`:消息列表\n",
|
||||
" - `next_tool`:下一个要调用的工具\n",
|
||||
"\n",
|
||||
"2. **decide_tool**:判断是否需要调用工具\n",
|
||||
" - 如果用户问时间,返回 `{'next_tool': 'get_time'}`\n",
|
||||
" - 如果用户问天气,返回 `{'next_tool': 'get_weather'}`\n",
|
||||
" - 如果不需要工具,返回 `{'next_tool': None}`\n",
|
||||
"\n",
|
||||
"3. **call_tool**:执行工具调用\n",
|
||||
" - 根据 `next_tool` 调用相应工具\n",
|
||||
" - 将结果追加到 `messages` 列表\n",
|
||||
"\n",
|
||||
"4. **summarize**:总结回答\n",
|
||||
" - 如果最后是工具消息,提取结果并生成回答\n",
|
||||
" - 将回答追加到 `messages` 列表\n",
|
||||
"\n",
|
||||
"5. **route_after_decide**:decide_tool 之后的路由\n",
|
||||
" - 如果 `next_tool` 有值,进入 `call_tool`\n",
|
||||
" - 如果 `next_tool` 为 None,进入 `summarize`\n",
|
||||
"\n",
|
||||
"6. **route_after_summarize**:summarize 之后的路由\n",
|
||||
" - 如果最后是用户消息,回到 `decide_tool`\n",
|
||||
" - 否则结束\n",
|
||||
"\n",
|
||||
"流程图:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> decide_tool -> [条件边] -> call_tool -> summarize\n",
|
||||
" ^ |\n",
|
||||
" | |\n",
|
||||
" +--------------+\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这个流程可以处理多轮对话,每轮都可以调用工具并保存历史。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 总结\n",
|
||||
"\n",
|
||||
"### 核心知识点\n",
|
||||
"\n",
|
||||
"1. **循环实现**:通过条件边让流程回到之前的节点,形成循环\n",
|
||||
"2. **状态累积**:在节点中不断追加数据到状态字段(如消息列表)\n",
|
||||
"3. **MemorySaver**:自动保存和恢复状态,无需手动传递\n",
|
||||
"4. **会话隔离**:通过 `thread_id` 区分不同用户的会话\n",
|
||||
"5. **add_messages reducer**:使用 `Annotated[list, add_messages]` 实现消息自动追加\n",
|
||||
"\n",
|
||||
"### 关键API\n",
|
||||
"\n",
|
||||
"| API | 作用 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| `MemorySaver()` | 创建内存状态存储 |\n",
|
||||
"| `builder.compile(checkpointer=memory)` | 编译时启用状态持久化 |\n",
|
||||
"| `config = {'configurable': {'thread_id': ...}}` | 设置会话ID |\n",
|
||||
"| `graph.get_state(config)` | 获取指定会话的状态 |\n",
|
||||
"| `add_messages` | 自动追加消息的 reducer |\n",
|
||||
"\n",
|
||||
"### 实践要点\n",
|
||||
"\n",
|
||||
"- 循环的关键是路由函数返回某个已存在的节点名\n",
|
||||
"- 状态累积需要在节点中返回新的状态值\n",
|
||||
"- 使用 `Annotated[list, add_messages]` 可以让 MemorySaver 自动追加消息\n",
|
||||
"- MemorySaver 适合开发和测试,生产环境可使用其他存储后端\n",
|
||||
"- 会话隔离通过 `thread_id` 实现,必须为每个用户分配唯一ID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 练习\n",
|
||||
"\n",
|
||||
"1. 修改计数器示例,让它从10递减到0\n",
|
||||
"2. 扩展对话示例,添加更多回复规则\n",
|
||||
"3. 创建一个多轮问答系统,支持用户追问\n",
|
||||
"4. 尝试使用不同的 `thread_id` 模拟三个用户同时对话"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
966
20_多智能体架构.ipynb
Normal file
966
20_多智能体架构.ipynb
Normal file
@@ -0,0 +1,966 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 20 多智能体架构\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解多智能体系统的典型架构(如监督者、协作、竞争)\n",
|
||||
"2. 掌握使用LangGraph设计多节点多智能体工作流\n",
|
||||
"3. 能够实现多个智能体节点之间的消息传递与协作"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么需要多智能体\n",
|
||||
"\n",
|
||||
"在之前的课程中,我们构建的智能体都是**单智能体**:一个智能体完成所有任务。\n",
|
||||
"\n",
|
||||
"但真实场景中,很多任务需要**多个智能体协作**才能完成:\n",
|
||||
"\n",
|
||||
"- **复杂任务分解**:一个任务太大,需要分给多个专业智能体\n",
|
||||
"- **专业分工**:不同智能体有不同的专业知识(如代码专家、写作专家、数据分析专家)\n",
|
||||
"- **协作决策**:需要多个智能体共同讨论才能做出最佳决策\n",
|
||||
"- **竞争博弈**:多个智能体竞争完成同一任务\n",
|
||||
"\n",
|
||||
"简单来说:\n",
|
||||
"\n",
|
||||
"- **单智能体**:一个人做所有事\n",
|
||||
"- **多智能体**:一个团队分工合作"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 多智能体的典型架构模式\n",
|
||||
"\n",
|
||||
"多智能体系统有几种经典的架构模式:\n",
|
||||
"\n",
|
||||
"### 2.1 监督者模式(Supervisor Pattern)\n",
|
||||
"\n",
|
||||
"一个监督者智能体负责协调多个工作智能体:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
" 监督者\n",
|
||||
" │\n",
|
||||
" ┌──────┼──────┬──────┐\n",
|
||||
" ▼ ▼ ▼ ▼\n",
|
||||
" 智能体A 智能体B 智能体C 智能体D\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**适用场景**:任务需要多种专业能力,监督者分配任务并汇总结果。\n",
|
||||
"\n",
|
||||
"### 2.2 协作模式(Collaborative Pattern)\n",
|
||||
"\n",
|
||||
"多个智能体平等协作,共同完成任务:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
" 智能体A ←→ 智能体B\n",
|
||||
" ↑ ↑\n",
|
||||
" │ │\n",
|
||||
" 智能体C ←→ 智能体D\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**适用场景**:任务需要多个智能体之间频繁交流和协同。\n",
|
||||
"\n",
|
||||
"### 2.3 竞争模式(Competitive Pattern)\n",
|
||||
"\n",
|
||||
"多个智能体竞争完成同一任务,胜者获得奖励:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
" 智能体A ─┐\n",
|
||||
" ├──→ 比较器 → 最佳结果\n",
|
||||
" 智能体B ─┘\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**适用场景**:需要从多个方案中选择最优解。\n",
|
||||
"\n",
|
||||
"### 2.4 流水线模式(Pipeline Pattern)\n",
|
||||
"\n",
|
||||
"多个智能体按顺序处理任务,像流水线一样:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
" 智能体A → 智能体B → 智能体C → 智能体D\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**适用场景**:任务可以分解为多个顺序步骤。\n",
|
||||
"\n",
|
||||
"### 2.5 辩论模式(Debate Pattern)\n",
|
||||
"\n",
|
||||
"多个智能体就某个问题展开辩论,最终达成共识:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
" 正方智能体 ←→ 反方智能体\n",
|
||||
" │ │\n",
|
||||
" └────┬──────┘\n",
|
||||
" ▼\n",
|
||||
" 裁判智能体\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**适用场景**:需要深入分析某个问题的正反两面。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 第一个例子:简单的协作智能体\n",
|
||||
"\n",
|
||||
"我们用一个简单的例子来演示多智能体协作。\n",
|
||||
"\n",
|
||||
"场景:\n",
|
||||
"- 用户提出一个问题\n",
|
||||
"- 研究智能体负责收集信息\n",
|
||||
"- 写作智能体负责整理成报告\n",
|
||||
"- 监督者智能体负责协调整个流程"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from openai import OpenAI\n",
|
||||
"import os\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"client = OpenAI(\n",
|
||||
" base_url=os.getenv(\"OPENAI_BASE_URL\"),\n",
|
||||
" api_key=os.getenv(\"OPENAI_API_KEY\")\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"def call_llm(system_prompt: str, user_prompt: str) -> str:\n",
|
||||
" response = client.chat.completions.create(\n",
|
||||
" model=\"qwen3.6-35b-A3b\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"system\", \"content\": system_prompt},\n",
|
||||
" {\"role\": \"user\", \"content\": user_prompt}\n",
|
||||
" ],\n",
|
||||
" temperature=0.7,\n",
|
||||
" max_tokens=500\n",
|
||||
" )\n",
|
||||
" return response.choices[0].message.content\n",
|
||||
"\n",
|
||||
"class AgentState(TypedDict):\n",
|
||||
" user_question: str\n",
|
||||
" research_info: str\n",
|
||||
" report: str\n",
|
||||
" current_agent: str\n",
|
||||
"\n",
|
||||
"def supervisor(state: AgentState):\n",
|
||||
" print(f'监督者:收到用户问题:{state[\"user_question\"]}')\n",
|
||||
" if not state['research_info']:\n",
|
||||
" print('监督者:分配给研究智能体')\n",
|
||||
" return {'current_agent': 'research'}\n",
|
||||
" elif not state['report']:\n",
|
||||
" print('监督者:分配给写作智能体')\n",
|
||||
" return {'current_agent': 'writer'}\n",
|
||||
" else:\n",
|
||||
" print('监督者:任务完成')\n",
|
||||
" return {'current_agent': 'finish'}\n",
|
||||
"\n",
|
||||
"def research_agent(state: AgentState):\n",
|
||||
" print(f'研究智能体:正在研究 \"{state[\"user_question\"]}\"')\n",
|
||||
" system_prompt = '你是一个专业的研究助手,请针对用户问题提供详细的研究信息和分析。'\n",
|
||||
" user_prompt = f'请研究并总结关于\"{state[\"user_question\"]}\"的最新信息,包括主要趋势、关键技术和未来展望。'\n",
|
||||
" research_info = call_llm(system_prompt, user_prompt)\n",
|
||||
" print(f'研究智能体:完成研究,收集到信息')\n",
|
||||
" return {'research_info': research_info}\n",
|
||||
"\n",
|
||||
"def writer_agent(state: AgentState):\n",
|
||||
" print(f'写作智能体:正在撰写报告')\n",
|
||||
" system_prompt = '你是一个专业的报告撰写者,请根据提供的研究信息撰写结构化的报告。'\n",
|
||||
" user_prompt = f'请根据以下研究信息撰写一份完整的报告:\\n\\n研究信息:{state[\"research_info\"]}'\n",
|
||||
" report = call_llm(system_prompt, user_prompt)\n",
|
||||
" print(f'写作智能体:报告完成')\n",
|
||||
" return {'report': report}\n",
|
||||
"\n",
|
||||
"def route(state: AgentState):\n",
|
||||
" if state['current_agent'] == 'research':\n",
|
||||
" return 'research'\n",
|
||||
" elif state['current_agent'] == 'writer':\n",
|
||||
" return 'writer'\n",
|
||||
" elif state['current_agent'] == 'finish':\n",
|
||||
" return END\n",
|
||||
" return 'supervisor'\n",
|
||||
"\n",
|
||||
"builder = StateGraph(AgentState)\n",
|
||||
"builder.add_node('supervisor', supervisor)\n",
|
||||
"builder.add_node('research', research_agent)\n",
|
||||
"builder.add_node('writer', writer_agent)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'supervisor')\n",
|
||||
"builder.add_edge('research', 'supervisor')\n",
|
||||
"builder.add_edge('writer', 'supervisor')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'supervisor',\n",
|
||||
" route,\n",
|
||||
" {\n",
|
||||
" 'research': 'research',\n",
|
||||
" 'writer': 'writer',\n",
|
||||
" END: END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"result = graph.invoke({\n",
|
||||
" 'user_question': '人工智能的发展趋势',\n",
|
||||
" 'research_info': '',\n",
|
||||
" 'report': '',\n",
|
||||
" 'current_agent': ''\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print()\n",
|
||||
"print('最终报告:')\n",
|
||||
"print(result['report'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"1. **环境配置**:\n",
|
||||
" - 加载 `.env` 文件中的 API 密钥\n",
|
||||
" - 创建 OpenAI 客户端\n",
|
||||
" - 定义 `call_llm` 函数,封装大模型调用逻辑\n",
|
||||
"\n",
|
||||
"2. **AgentState**:定义状态包含四个字段:\n",
|
||||
" - `user_question`:用户的问题\n",
|
||||
" - `research_info`:研究智能体收集的信息\n",
|
||||
" - `report`:写作智能体生成的报告\n",
|
||||
" - `current_agent`:当前应该执行的智能体\n",
|
||||
"\n",
|
||||
"3. **supervisor**:监督者智能体\n",
|
||||
" - 检查当前状态,决定下一步分配给哪个智能体\n",
|
||||
" - 如果没有研究信息,分配给研究智能体\n",
|
||||
" - 如果有研究信息但没有报告,分配给写作智能体\n",
|
||||
" - 如果报告已完成,标记任务结束\n",
|
||||
"\n",
|
||||
"4. **research_agent**:研究智能体(使用真实大模型)\n",
|
||||
" - 构建系统提示词和用户提示词\n",
|
||||
" - 调用大模型获取研究结果\n",
|
||||
" - 将结果保存到 `research_info`\n",
|
||||
"\n",
|
||||
"5. **writer_agent**:写作智能体(使用真实大模型)\n",
|
||||
" - 构建系统提示词和用户提示词\n",
|
||||
" - 调用大模型根据研究信息生成报告\n",
|
||||
" - 将结果保存到 `report`\n",
|
||||
"\n",
|
||||
"6. **route**:路由函数\n",
|
||||
" - 根据 `current_agent` 字段决定下一步执行哪个节点\n",
|
||||
"\n",
|
||||
"7. **图结构**:\n",
|
||||
" - START -> supervisor\n",
|
||||
" - supervisor 根据条件边分配给 research 或 writer\n",
|
||||
" - research 和 writer 执行完后都回到 supervisor\n",
|
||||
" - supervisor 判断完成后走到 END\n",
|
||||
"\n",
|
||||
"流程图:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> supervisor\n",
|
||||
" │\n",
|
||||
" ┌──────┴──────┐\n",
|
||||
" ▼ ▼\n",
|
||||
"research writer\n",
|
||||
" │ │\n",
|
||||
" └──────┬──────┘\n",
|
||||
" ▼\n",
|
||||
" supervisor -> END\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 流水线模式:多智能体顺序处理\n",
|
||||
"\n",
|
||||
"流水线模式是最直观的多智能体架构,智能体按顺序处理任务。\n",
|
||||
"\n",
|
||||
"场景:\n",
|
||||
"- 用户输入一段原始文本\n",
|
||||
"- 翻译智能体将其翻译成英文\n",
|
||||
"- 摘要智能体生成摘要\n",
|
||||
"- 情感分析智能体分析情感倾向"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class PipelineState(TypedDict):\n",
|
||||
" original_text: str\n",
|
||||
" translated_text: str\n",
|
||||
" summary: str\n",
|
||||
" sentiment: str\n",
|
||||
"\n",
|
||||
"def translate_agent(state: PipelineState):\n",
|
||||
" print(f'翻译智能体:正在翻译')\n",
|
||||
" text = state['original_text']\n",
|
||||
" translated = f'[Translated] {text}'\n",
|
||||
" print(f'翻译智能体:{translated}')\n",
|
||||
" return {'translated_text': translated}\n",
|
||||
"\n",
|
||||
"def summarize_agent(state: PipelineState):\n",
|
||||
" print(f'摘要智能体:正在生成摘要')\n",
|
||||
" text = state['translated_text']\n",
|
||||
" summary = f'[Summary] {text[:20]}...'\n",
|
||||
" print(f'摘要智能体:{summary}')\n",
|
||||
" return {'summary': summary}\n",
|
||||
"\n",
|
||||
"def sentiment_agent(state: PipelineState):\n",
|
||||
" print(f'情感分析智能体:正在分析情感')\n",
|
||||
" text = state['translated_text']\n",
|
||||
" if '好' in text or '高兴' in text:\n",
|
||||
" sentiment = '积极'\n",
|
||||
" elif '坏' in text or '难过' in text:\n",
|
||||
" sentiment = '消极'\n",
|
||||
" else:\n",
|
||||
" sentiment = '中性'\n",
|
||||
" print(f'情感分析智能体:情感倾向为 {sentiment}')\n",
|
||||
" return {'sentiment': sentiment}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(PipelineState)\n",
|
||||
"builder.add_node('translate', translate_agent)\n",
|
||||
"builder.add_node('summarize', summarize_agent)\n",
|
||||
"builder.add_node('sentiment', sentiment_agent)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'translate')\n",
|
||||
"builder.add_edge('translate', 'summarize')\n",
|
||||
"builder.add_edge('summarize', 'sentiment')\n",
|
||||
"builder.add_edge('sentiment', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"result = graph.invoke({\n",
|
||||
" 'original_text': '今天天气很好,我很高兴。',\n",
|
||||
" 'translated_text': '',\n",
|
||||
" 'summary': '',\n",
|
||||
" 'sentiment': ''\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print()\n",
|
||||
"print('处理结果:')\n",
|
||||
"print(f'原文:{result[\"original_text\"]}')\n",
|
||||
"print(f'翻译:{result[\"translated_text\"]}')\n",
|
||||
"print(f'摘要:{result[\"summary\"]}')\n",
|
||||
"print(f'情感:{result[\"sentiment\"]}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"1. **PipelineState**:定义流水线的状态,包含四个字段,分别对应每个智能体的输入/输出。\n",
|
||||
"\n",
|
||||
"2. **translate_agent**:翻译智能体\n",
|
||||
" - 接收原始文本\n",
|
||||
" - 输出翻译结果\n",
|
||||
"\n",
|
||||
"3. **summarize_agent**:摘要智能体\n",
|
||||
" - 接收翻译后的文本\n",
|
||||
" - 输出摘要\n",
|
||||
"\n",
|
||||
"4. **sentiment_agent**:情感分析智能体\n",
|
||||
" - 接收翻译后的文本\n",
|
||||
" - 分析情感倾向\n",
|
||||
"\n",
|
||||
"5. **图结构**:简单的顺序流程\n",
|
||||
" - START -> translate -> summarize -> sentiment -> END\n",
|
||||
"\n",
|
||||
"流程图:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> translate -> summarize -> sentiment -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这种模式的优点:\n",
|
||||
"- 结构简单,易于理解\n",
|
||||
"- 每个智能体只关注自己的任务\n",
|
||||
"- 便于扩展,可以随时添加新的智能体"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 竞争模式:多个智能体竞争\n",
|
||||
"\n",
|
||||
"竞争模式中,多个智能体同时处理同一个任务,然后比较结果选择最优解。\n",
|
||||
"\n",
|
||||
"场景:\n",
|
||||
"- 用户提出一个问题\n",
|
||||
"- 多个回答智能体给出不同的答案\n",
|
||||
"- 评估智能体选择最佳答案"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class CompetitionState(TypedDict):\n",
|
||||
" question: str\n",
|
||||
" answers: list\n",
|
||||
" best_answer: str\n",
|
||||
"\n",
|
||||
"def answer_agent_a(state: CompetitionState):\n",
|
||||
" print(f'回答智能体A:正在回答问题')\n",
|
||||
" answer = f'A的回答:关于\"{state[\"question\"]}\",这是一个详细的回答。'\n",
|
||||
" print(f'回答智能体A:{answer}')\n",
|
||||
" return {'answers': state['answers'] + [{'agent': 'A', 'content': answer, 'score': 85}]}\n",
|
||||
"\n",
|
||||
"def answer_agent_b(state: CompetitionState):\n",
|
||||
" print(f'回答智能体B:正在回答问题')\n",
|
||||
" answer = f'B的回答:对于\"{state[\"question\"]}\",我的见解是...'\n",
|
||||
" print(f'回答智能体B:{answer}')\n",
|
||||
" return {'answers': state['answers'] + [{'agent': 'B', 'content': answer, 'score': 92}]}\n",
|
||||
"\n",
|
||||
"def answer_agent_c(state: CompetitionState):\n",
|
||||
" print(f'回答智能体C:正在回答问题')\n",
|
||||
" answer = f'C的回答:\"{state[\"question\"]}\"的答案如下...'\n",
|
||||
" print(f'回答智能体C:{answer}')\n",
|
||||
" return {'answers': state['answers'] + [{'agent': 'C', 'content': answer, 'score': 88}]}\n",
|
||||
"\n",
|
||||
"def evaluator_agent(state: CompetitionState):\n",
|
||||
" print(f'评估智能体:正在评估所有回答')\n",
|
||||
" if not state['answers']:\n",
|
||||
" return {'best_answer': '没有回答'}\n",
|
||||
"\n",
|
||||
" best = max(state['answers'], key=lambda x: x['score'])\n",
|
||||
" print(f'评估智能体:最佳回答来自智能体{best[\"agent\"]},得分{best[\"score\"]}')\n",
|
||||
" return {'best_answer': best['content']}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(CompetitionState)\n",
|
||||
"builder.add_node('answer_a', answer_agent_a)\n",
|
||||
"builder.add_node('answer_b', answer_agent_b)\n",
|
||||
"builder.add_node('answer_c', answer_agent_c)\n",
|
||||
"builder.add_node('evaluator', evaluator_agent)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'answer_a')\n",
|
||||
"builder.add_edge('answer_a', 'answer_b')\n",
|
||||
"builder.add_edge('answer_b', 'answer_c')\n",
|
||||
"builder.add_edge('answer_c', 'evaluator')\n",
|
||||
"builder.add_edge('evaluator', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"result = graph.invoke({\n",
|
||||
" 'question': '什么是人工智能?',\n",
|
||||
" 'answers': [],\n",
|
||||
" 'best_answer': ''\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print()\n",
|
||||
"print('最佳回答:')\n",
|
||||
"print(result['best_answer'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"1. **CompetitionState**:定义竞争状态,包含:\n",
|
||||
" - `question`:用户问题\n",
|
||||
" - `answers`:所有智能体的回答列表\n",
|
||||
" - `best_answer`:评估后的最佳回答\n",
|
||||
"\n",
|
||||
"2. **answer_agent_a/b/c**:三个回答智能体\n",
|
||||
" - 每个智能体给出自己的回答\n",
|
||||
" - 回答包含智能体标识、内容和得分\n",
|
||||
" - 将回答追加到 `answers` 列表\n",
|
||||
"\n",
|
||||
"3. **evaluator_agent**:评估智能体\n",
|
||||
" - 比较所有回答的得分\n",
|
||||
" - 选择得分最高的回答作为最佳答案\n",
|
||||
"\n",
|
||||
"4. **图结构**:顺序执行三个回答智能体,然后评估\n",
|
||||
" - START -> answer_a -> answer_b -> answer_c -> evaluator -> END\n",
|
||||
"\n",
|
||||
"流程图:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> answer_a -> answer_b -> answer_c -> evaluator -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这种模式的优点:\n",
|
||||
"- 可以从多个角度解决问题\n",
|
||||
"- 通过竞争提高回答质量\n",
|
||||
"- 易于扩展,随时可以添加新的智能体"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 辩论模式:智能体之间的辩论\n",
|
||||
"\n",
|
||||
"辩论模式中,多个智能体就某个问题展开辩论,最终达成共识或由裁判做出裁决。\n",
|
||||
"\n",
|
||||
"场景:\n",
|
||||
"- 用户提出一个有争议的问题\n",
|
||||
"- 正方智能体支持某个观点\n",
|
||||
"- 反方智能体反对该观点\n",
|
||||
"- 裁判智能体总结辩论结果"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"class DebateState(TypedDict):\n",
|
||||
" question: str\n",
|
||||
" debate_history: list\n",
|
||||
" round: int\n",
|
||||
" conclusion: str\n",
|
||||
"\n",
|
||||
"def pro_agent(state: DebateState):\n",
|
||||
" print(f'正方智能体(第{state[\"round\"]}轮):')\n",
|
||||
" arguments = [\n",
|
||||
" f'支持\"{state[\"question\"]}\"的理由:它可以提高效率。',\n",
|
||||
" f'进一步论证:从长远来看,这是必然趋势。'\n",
|
||||
" ]\n",
|
||||
" arg = arguments[state['round'] - 1]\n",
|
||||
" print(f' {arg}')\n",
|
||||
" return {\n",
|
||||
" 'debate_history': state['debate_history'] + [{'side': '正方', 'argument': arg}],\n",
|
||||
" 'round': state['round']\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"def con_agent(state: DebateState):\n",
|
||||
" print(f'反方智能体(第{state[\"round\"]}轮):')\n",
|
||||
" arguments = [\n",
|
||||
" f'反对\"{state[\"question\"]}\"的理由:它可能带来风险。',\n",
|
||||
" f'进一步反驳:我们需要更谨慎地对待。'\n",
|
||||
" ]\n",
|
||||
" arg = arguments[state['round'] - 1]\n",
|
||||
" print(f' {arg}')\n",
|
||||
" new_round = state['round'] + 1\n",
|
||||
" return {\n",
|
||||
" 'debate_history': state['debate_history'] + [{'side': '反方', 'argument': arg}],\n",
|
||||
" 'round': new_round\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"def judge_agent(state: DebateState):\n",
|
||||
" print(f'裁判智能体:总结辩论')\n",
|
||||
" conclusion = f'关于\"{state[\"question\"]}\"的辩论总结:\\n'\n",
|
||||
" for entry in state['debate_history']:\n",
|
||||
" conclusion += f'- {entry[\"side\"]}:{entry[\"argument\"]}\\n'\n",
|
||||
" conclusion += '\\n结论:双方观点都有道理,需要权衡利弊。'\n",
|
||||
" print(f'裁判智能体:{conclusion}')\n",
|
||||
" return {'conclusion': conclusion}\n",
|
||||
"\n",
|
||||
"def after_con_route(state: DebateState):\n",
|
||||
" if state['round'] <= 2:\n",
|
||||
" return 'pro'\n",
|
||||
" return 'judge'\n",
|
||||
"\n",
|
||||
"builder = StateGraph(DebateState)\n",
|
||||
"builder.add_node('pro', pro_agent)\n",
|
||||
"builder.add_node('con', con_agent)\n",
|
||||
"builder.add_node('judge', judge_agent)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'pro')\n",
|
||||
"builder.add_edge('pro', 'con')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'con',\n",
|
||||
" after_con_route,\n",
|
||||
" {\n",
|
||||
" 'pro': 'pro',\n",
|
||||
" 'judge': 'judge'\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"builder.add_edge('judge', END)\n",
|
||||
"\n",
|
||||
"graph = builder.compile()\n",
|
||||
"\n",
|
||||
"result = graph.invoke({\n",
|
||||
" 'question': '人工智能是否应该取代人类工作?',\n",
|
||||
" 'debate_history': [],\n",
|
||||
" 'round': 1,\n",
|
||||
" 'conclusion': ''\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print()\n",
|
||||
"print('辩论总结:')\n",
|
||||
"print(result['conclusion'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"1. **DebateState**:定义辩论状态,包含:\n",
|
||||
" - `question`:辩论的问题\n",
|
||||
" - `debate_history`:辩论历史记录\n",
|
||||
" - `round`:当前辩论轮次\n",
|
||||
" - `conclusion`:最终结论\n",
|
||||
"\n",
|
||||
"2. **pro_agent**:正方智能体\n",
|
||||
" - 根据当前轮次给出支持观点\n",
|
||||
" - 将论点添加到辩论历史\n",
|
||||
"\n",
|
||||
"3. **con_agent**:反方智能体\n",
|
||||
" - 根据当前轮次给出反对观点\n",
|
||||
" - 将论点添加到辩论历史\n",
|
||||
"\n",
|
||||
"4. **judge_agent**:裁判智能体\n",
|
||||
" - 总结所有辩论内容\n",
|
||||
" - 给出最终结论\n",
|
||||
"\n",
|
||||
"5. **after_con_route**:反方发言后的路由\n",
|
||||
" - 如果还没到第2轮,回到正方开始下一轮\n",
|
||||
" - 如果已经第2轮结束,进入裁判\n",
|
||||
"\n",
|
||||
"6. **图结构**:\n",
|
||||
" - START -> pro -> con\n",
|
||||
" - con 根据轮次决定回到 pro 还是进入 judge\n",
|
||||
" - judge -> END\n",
|
||||
"\n",
|
||||
"流程图:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> pro -> con\n",
|
||||
" │\n",
|
||||
" ┌────────┴────────┐\n",
|
||||
" ▼ ▼\n",
|
||||
" pro → con judge -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这种模式的优点:\n",
|
||||
"- 可以全面分析问题的正反两面\n",
|
||||
"- 通过辩论深入探讨复杂问题\n",
|
||||
"- 最终结论更加客观全面"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 综合案例:多智能体协作完成复杂任务\n",
|
||||
"\n",
|
||||
"现在我们将多种模式结合起来,构建一个复杂的多智能体系统。\n",
|
||||
"\n",
|
||||
"场景:\n",
|
||||
"- 用户提出一个数据分析需求\n",
|
||||
"- 监督者智能体负责协调\n",
|
||||
"- 数据收集智能体收集数据\n",
|
||||
"- 数据处理智能体处理数据\n",
|
||||
"- 数据分析智能体分析数据\n",
|
||||
"- 报告生成智能体生成报告\n",
|
||||
"- 使用MemorySaver保存状态"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from openai import OpenAI\n",
|
||||
"import os\n",
|
||||
"from dotenv import load_dotenv\n",
|
||||
"\n",
|
||||
"load_dotenv()\n",
|
||||
"\n",
|
||||
"client = OpenAI(\n",
|
||||
" base_url=os.getenv(\"OPENAI_BASE_URL\"),\n",
|
||||
" api_key=os.getenv(\"OPENAI_API_KEY\")\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"def call_llm(system_prompt: str, user_prompt: str) -> str:\n",
|
||||
" response = client.chat.completions.create(\n",
|
||||
" model=\"qwen3.6-35b-A3b\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"system\", \"content\": system_prompt},\n",
|
||||
" {\"role\": \"user\", \"content\": user_prompt}\n",
|
||||
" ],\n",
|
||||
" temperature=0.7,\n",
|
||||
" max_tokens=500\n",
|
||||
" )\n",
|
||||
" return response.choices[0].message.content\n",
|
||||
"\n",
|
||||
"class DataAgentState(TypedDict):\n",
|
||||
" user_request: str\n",
|
||||
" raw_data: str\n",
|
||||
" processed_data: str\n",
|
||||
" analysis_result: str\n",
|
||||
" report: str\n",
|
||||
" next_agent: str\n",
|
||||
"\n",
|
||||
"def supervisor(state: DataAgentState):\n",
|
||||
" print(f'监督者:收到请求:{state[\"user_request\"]}')\n",
|
||||
" \n",
|
||||
" if not state['raw_data']:\n",
|
||||
" print('监督者:分配给数据收集智能体')\n",
|
||||
" return {'next_agent': 'collect'}\n",
|
||||
" elif not state['processed_data']:\n",
|
||||
" print('监督者:分配给数据处理智能体')\n",
|
||||
" return {'next_agent': 'process'}\n",
|
||||
" elif not state['analysis_result']:\n",
|
||||
" print('监督者:分配给数据分析智能体')\n",
|
||||
" return {'next_agent': 'analyze'}\n",
|
||||
" elif not state['report']:\n",
|
||||
" print('监督者:分配给报告生成智能体')\n",
|
||||
" return {'next_agent': 'report'}\n",
|
||||
" else:\n",
|
||||
" print('监督者:任务完成')\n",
|
||||
" return {'next_agent': 'finish'}\n",
|
||||
"\n",
|
||||
"def collect_agent(state: DataAgentState):\n",
|
||||
" print(f'数据收集智能体:正在收集数据')\n",
|
||||
" raw_data = f'原始数据:关于\"{state[\"user_request\"]}\"的统计数据、趋势数据、对比数据。'\n",
|
||||
" print(f'数据收集智能体:完成数据收集')\n",
|
||||
" return {'raw_data': raw_data}\n",
|
||||
"\n",
|
||||
"def process_agent(state: DataAgentState):\n",
|
||||
" print(f'数据处理智能体:正在处理数据')\n",
|
||||
" processed_data = f'处理后数据:清洗、转换、标准化后的\"{state[\"user_request\"]}\"数据。'\n",
|
||||
" print(f'数据处理智能体:完成数据处理')\n",
|
||||
" return {'processed_data': processed_data}\n",
|
||||
"\n",
|
||||
"def analyze_agent(state: DataAgentState):\n",
|
||||
" print(f'数据分析智能体:正在分析数据')\n",
|
||||
" \n",
|
||||
" system_prompt = '你是一个专业的数据分析师,请根据提供的数据进行深入分析。'\n",
|
||||
" user_prompt = f'请分析以下数据,找出关键发现和趋势:\\n\\n原始数据:{state[\"raw_data\"]}\\n\\n处理后数据:{state[\"processed_data\"]}'\n",
|
||||
" \n",
|
||||
" analysis_result = call_llm(system_prompt, user_prompt)\n",
|
||||
" \n",
|
||||
" print(f'数据分析智能体:完成数据分析')\n",
|
||||
" return {'analysis_result': analysis_result}\n",
|
||||
"\n",
|
||||
"def report_agent(state: DataAgentState):\n",
|
||||
" print(f'报告生成智能体:正在生成报告')\n",
|
||||
" \n",
|
||||
" system_prompt = '你是一个专业的报告撰写者,请根据数据分析结果撰写结构化的报告。'\n",
|
||||
" user_prompt = f'请根据以下信息撰写一份完整的数据分析报告:\\n\\n请求:{state[\"user_request\"]}\\n\\n分析结果:{state[\"analysis_result\"]}'\n",
|
||||
" \n",
|
||||
" report = call_llm(system_prompt, user_prompt)\n",
|
||||
" print(f'报告生成智能体:完成报告生成')\n",
|
||||
" return {'report': report}\n",
|
||||
"\n",
|
||||
"def route(state: DataAgentState):\n",
|
||||
" if state['next_agent'] == 'collect':\n",
|
||||
" return 'collect'\n",
|
||||
" elif state['next_agent'] == 'process':\n",
|
||||
" return 'process'\n",
|
||||
" elif state['next_agent'] == 'analyze':\n",
|
||||
" return 'analyze'\n",
|
||||
" elif state['next_agent'] == 'report':\n",
|
||||
" return 'report'\n",
|
||||
" elif state['next_agent'] == 'finish':\n",
|
||||
" return END\n",
|
||||
" return 'supervisor'\n",
|
||||
"\n",
|
||||
"builder = StateGraph(DataAgentState)\n",
|
||||
"builder.add_node('supervisor', supervisor)\n",
|
||||
"builder.add_node('collect', collect_agent)\n",
|
||||
"builder.add_node('process', process_agent)\n",
|
||||
"builder.add_node('analyze', analyze_agent)\n",
|
||||
"builder.add_node('report', report_agent)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'supervisor')\n",
|
||||
"builder.add_edge('collect', 'supervisor')\n",
|
||||
"builder.add_edge('process', 'supervisor')\n",
|
||||
"builder.add_edge('analyze', 'supervisor')\n",
|
||||
"builder.add_edge('report', 'supervisor')\n",
|
||||
"builder.add_conditional_edges(\n",
|
||||
" 'supervisor',\n",
|
||||
" route,\n",
|
||||
" {\n",
|
||||
" 'collect': 'collect',\n",
|
||||
" 'process': 'process',\n",
|
||||
" 'analyze': 'analyze',\n",
|
||||
" 'report': 'report',\n",
|
||||
" END: END\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = builder.compile(checkpointer=memory)\n",
|
||||
"\n",
|
||||
"config = {'configurable': {'thread_id': 'data_analysis_session'}}\n",
|
||||
"\n",
|
||||
"result = graph.invoke({\n",
|
||||
" 'user_request': '分析过去一年的销售趋势',\n",
|
||||
" 'raw_data': '',\n",
|
||||
" 'processed_data': '',\n",
|
||||
" 'analysis_result': '',\n",
|
||||
" 'report': '',\n",
|
||||
" 'next_agent': ''\n",
|
||||
"}, config)\n",
|
||||
"\n",
|
||||
"print()\n",
|
||||
"print('最终报告:')\n",
|
||||
"print(result['report'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个案例展示了一个完整的多智能体协作系统:\n",
|
||||
"\n",
|
||||
"1. **环境配置**:\n",
|
||||
" - 加载 `.env` 文件中的 API 密钥\n",
|
||||
" - 创建 OpenAI 客户端\n",
|
||||
" - 定义 `call_llm` 函数,封装大模型调用逻辑\n",
|
||||
"\n",
|
||||
"2. **DataAgentState**:定义状态,包含六个字段,分别对应每个阶段的数据。\n",
|
||||
"\n",
|
||||
"3. **supervisor**:监督者智能体\n",
|
||||
" - 根据当前状态决定下一步分配给哪个智能体\n",
|
||||
" - 按照数据收集→数据处理→数据分析→报告生成的顺序分配\n",
|
||||
"\n",
|
||||
"4. **collect_agent**:数据收集智能体\n",
|
||||
" - 收集原始数据\n",
|
||||
" - 保存到 `raw_data`\n",
|
||||
"\n",
|
||||
"5. **process_agent**:数据处理智能体\n",
|
||||
" - 处理原始数据\n",
|
||||
" - 保存到 `processed_data`\n",
|
||||
"\n",
|
||||
"6. **analyze_agent**:数据分析智能体(使用真实大模型)\n",
|
||||
" - 构建系统提示词和用户提示词\n",
|
||||
" - 调用大模型分析数据\n",
|
||||
" - 保存到 `analysis_result`\n",
|
||||
"\n",
|
||||
"7. **report_agent**:报告生成智能体(使用真实大模型)\n",
|
||||
" - 构建系统提示词和用户提示词\n",
|
||||
" - 调用大模型生成报告\n",
|
||||
" - 保存到 `report`\n",
|
||||
"\n",
|
||||
"8. **MemorySaver**:保存状态\n",
|
||||
" - 使用 `thread_id` 隔离不同会话\n",
|
||||
" - 支持多轮交互\n",
|
||||
"\n",
|
||||
"流程图:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> supervisor\n",
|
||||
" │\n",
|
||||
" ┌──────┼──────┬──────┬──────┐\n",
|
||||
" ▼ ▼ ▼ ▼ ▼\n",
|
||||
" collect process analyze report finish\n",
|
||||
" │ │ │ │\n",
|
||||
" └──────┴──────┴──────┘\n",
|
||||
" │\n",
|
||||
" ▼\n",
|
||||
" supervisor -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这个系统结合了监督者模式和流水线模式,展示了多智能体协作的强大能力。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 总结\n",
|
||||
"\n",
|
||||
"### 核心知识点\n",
|
||||
"\n",
|
||||
"1. **监督者模式**:一个监督者智能体协调多个工作智能体\n",
|
||||
"2. **协作模式**:多个智能体平等协作,共同完成任务\n",
|
||||
"3. **竞争模式**:多个智能体竞争,选择最优解\n",
|
||||
"4. **流水线模式**:多个智能体按顺序处理任务\n",
|
||||
"5. **辩论模式**:多个智能体展开辩论,最终达成共识\n",
|
||||
"\n",
|
||||
"### 架构选择指南\n",
|
||||
"\n",
|
||||
"| 模式 | 适用场景 | 特点 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 监督者 | 需要多种专业能力 | 集中控制,易于管理 |\n",
|
||||
"| 协作 | 需要频繁交流 | 平等协作,灵活适应 |\n",
|
||||
"| 竞争 | 需要最优解 | 多方竞争,提高质量 |\n",
|
||||
"| 流水线 | 可分解为顺序步骤 | 结构清晰,易于扩展 |\n",
|
||||
"| 辩论 | 需要深入分析 | 全面探讨,结论客观 |\n",
|
||||
"\n",
|
||||
"### 实践要点\n",
|
||||
"\n",
|
||||
"- 根据任务特点选择合适的架构模式\n",
|
||||
"- 智能体之间通过共享状态传递信息\n",
|
||||
"- 使用监督者模式时,确保监督者逻辑清晰\n",
|
||||
"- 使用MemorySaver保存状态,支持多轮交互\n",
|
||||
"- 可以组合多种模式构建复杂系统\n",
|
||||
"- 智能体可以调用真实大模型(如GPT、Qwen等)来生成内容\n",
|
||||
"- 使用环境变量管理API密钥,确保安全性\n",
|
||||
"- 通过封装call_llm函数统一管理大模型调用"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 练习\n",
|
||||
"\n",
|
||||
"1. 修改监督者模式示例,添加一个\"质量检查智能体\"\n",
|
||||
"2. 修改流水线模式示例,添加一个\"校对智能体\"\n",
|
||||
"3. 修改竞争模式示例,让智能体可以互相评价\n",
|
||||
"4. 创建一个新的辩论模式示例,增加到3轮辩论\n",
|
||||
"5. 设计一个综合多智能体系统,包含监督者、协作和竞争模式"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
836
21_对话代理.ipynb
Normal file
836
21_对话代理.ipynb
Normal file
@@ -0,0 +1,836 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
604
22_任务调度.ipynb
Normal file
604
22_任务调度.ipynb
Normal file
@@ -0,0 +1,604 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 22_任务调度型智能体\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解任务调度型智能体的设计思路\n",
|
||||
"2. 掌握使用 LangGraph 实现任务分解、执行与结果汇总\n",
|
||||
"3. 能够构建简单的自动化任务处理智能体\n",
|
||||
"\n",
|
||||
"本节课会用一个通俗例子来理解任务调度:**把一个大任务拆成多个小任务,再安排合适的节点依次完成,最后汇总结果**。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 什么是任务调度型智能体\n",
|
||||
"\n",
|
||||
"任务调度型智能体可以理解为一个“项目经理型”智能体。用户只提出一个目标,它不会马上盲目执行,而是先思考:\n",
|
||||
"\n",
|
||||
"1. 这个目标可以拆成哪些步骤?\n",
|
||||
"2. 哪些步骤需要先做,哪些步骤可以后做?\n",
|
||||
"3. 每一步应该交给哪个工具、函数或智能体执行?\n",
|
||||
"4. 每一步完成后,如何把结果合并成最终答案?\n",
|
||||
"\n",
|
||||
"例如用户说:\n",
|
||||
"\n",
|
||||
"> 帮我分析一个产品的用户反馈,并给出改进建议。\n",
|
||||
"\n",
|
||||
"任务调度型智能体可能会拆成:\n",
|
||||
"\n",
|
||||
"1. 收集用户反馈\n",
|
||||
"2. 对反馈进行分类\n",
|
||||
"3. 提取高频问题\n",
|
||||
"4. 生成改进建议\n",
|
||||
"5. 输出结构化报告\n",
|
||||
"\n",
|
||||
"这类智能体的核心不是“单次回答”,而是“组织多个步骤完成复杂任务”。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 任务调度的基本流程\n",
|
||||
"\n",
|
||||
"一个典型任务调度型智能体通常包含 4 个环节:\n",
|
||||
"\n",
|
||||
"| 环节 | 作用 | 通俗理解 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 任务输入 | 接收用户目标 | 用户告诉智能体要做什么 |\n",
|
||||
"| 任务分解 | 把大任务拆成小任务 | 项目经理制定待办清单 |\n",
|
||||
"| 任务执行 | 按顺序执行每个小任务 | 不同同事完成各自工作 |\n",
|
||||
"| 结果汇总 | 整合所有执行结果 | 写成最终报告交给用户 |\n",
|
||||
"\n",
|
||||
"在 LangGraph 中,我们可以把每个环节看成一个 **节点**,节点之间通过 **边** 连接起来,形成一个工作流。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 先不用 LangGraph:用普通 Python 理解任务调度\n",
|
||||
"\n",
|
||||
"在学习 LangGraph 前,先用普通 Python 模拟一个最小任务调度流程。这个例子不依赖任何外部库,更容易理解“任务分解、执行、汇总”的本质。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def split_task(user_goal):\n",
|
||||
" \"\"\"把用户的大目标拆成多个小任务。\"\"\"\n",
|
||||
" tasks = [\n",
|
||||
" f\"理解用户目标:{user_goal}\",\n",
|
||||
" \"收集与目标相关的信息\",\n",
|
||||
" \"分析关键信息\",\n",
|
||||
" \"生成最终建议\",\n",
|
||||
" ]\n",
|
||||
" return tasks\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def execute_task(task):\n",
|
||||
" \"\"\"模拟执行一个小任务。\"\"\"\n",
|
||||
" return f\"已完成:{task}\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def summarize_results(results):\n",
|
||||
" \"\"\"把多个小任务的执行结果汇总成最终输出。\"\"\"\n",
|
||||
" report = \"任务执行报告:\\n\"\n",
|
||||
" for index, result in enumerate(results, start=1):\n",
|
||||
" report += f\"{index}. {result}\\n\"\n",
|
||||
" return report\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"user_goal = \"为一家咖啡店设计会员运营方案\"\n",
|
||||
"tasks = split_task(user_goal)\n",
|
||||
"results = [execute_task(task) for task in tasks]\n",
|
||||
"final_report = summarize_results(results)\n",
|
||||
"\n",
|
||||
"print(final_report)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码展示了任务调度的最小模型:\n",
|
||||
"\n",
|
||||
"1. `split_task(user_goal)` 负责拆解任务。它接收用户的大目标,返回一个任务列表。\n",
|
||||
"2. `execute_task(task)` 负责执行单个小任务。这里为了演示,只返回“已完成”的文字。真实项目中,这里可以调用搜索工具、数据库、LLM 或其他业务函数。\n",
|
||||
"3. `summarize_results(results)` 负责汇总结果。它把每个小任务的输出整理成最终报告。\n",
|
||||
"4. `results = [execute_task(task) for task in tasks]` 是任务调度的执行阶段,会逐个执行任务列表里的每一项。\n",
|
||||
"\n",
|
||||
"这个例子虽然简单,但已经包含任务调度型智能体最重要的 3 个动作:**拆、做、汇总**。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. LangGraph 的核心概念\n",
|
||||
"\n",
|
||||
"LangGraph 是一个用“图”组织智能体流程的框架。\n",
|
||||
"\n",
|
||||
"可以把它想象成一张流程图:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"开始 -> 任务分解 -> 任务执行 -> 结果汇总 -> 结束\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"在 LangGraph 中常见概念如下:\n",
|
||||
"\n",
|
||||
"| 概念 | 含义 | 通俗理解 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| State | 工作流中流动的数据 | 一个不断更新的任务档案袋 |\n",
|
||||
"| Node | 处理数据的函数 | 流程图中的一个步骤 |\n",
|
||||
"| Edge | 节点之间的连接 | 告诉程序下一步去哪里 |\n",
|
||||
"| Graph | 完整工作流 | 整张流程图 |\n",
|
||||
"\n",
|
||||
"接下来我们用 LangGraph 实现同样的“拆、做、汇总”流程。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 安装依赖\n",
|
||||
"\n",
|
||||
"如果当前环境没有安装 LangGraph,可以先运行下面的命令。已经安装过的环境可以跳过。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 如果没有安装 LangGraph,取消下一行注释后运行\n",
|
||||
"# %pip install langgraph\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"`%pip install langgraph` 是 Jupyter Notebook 中安装 Python 包的写法。\n",
|
||||
"\n",
|
||||
"这里默认把安装命令注释掉,是为了避免重复安装。如果运行后提示找不到 `langgraph`,再去掉前面的 `#` 执行即可。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 定义工作流状态 State\n",
|
||||
"\n",
|
||||
"State 是 LangGraph 中非常重要的概念。它表示工作流运行过程中一直携带和更新的数据。\n",
|
||||
"\n",
|
||||
"本例中,我们希望 State 保存 4 类信息:\n",
|
||||
"\n",
|
||||
"1. `goal`:用户输入的大目标\n",
|
||||
"2. `tasks`:拆分后的小任务列表\n",
|
||||
"3. `results`:每个小任务的执行结果\n",
|
||||
"4. `final_answer`:最终汇总答案\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import List, TypedDict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class TaskState(TypedDict):\n",
|
||||
" goal: str\n",
|
||||
" tasks: List[str]\n",
|
||||
" results: List[str]\n",
|
||||
" final_answer: str\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码定义了工作流中的数据结构:\n",
|
||||
"\n",
|
||||
"1. `TypedDict` 用来描述字典里应该有哪些字段。它不会改变程序运行方式,但能让代码更清晰。\n",
|
||||
"2. `TaskState` 表示整个任务调度过程中共享的数据。每个节点都会读取它,也可以返回新的字段值更新它。\n",
|
||||
"3. `goal: str` 表示用户目标是字符串。\n",
|
||||
"4. `tasks: List[str]` 表示任务列表,里面每一项都是字符串。\n",
|
||||
"5. `results: List[str]` 表示执行结果列表。\n",
|
||||
"6. `final_answer: str` 表示最终回答。\n",
|
||||
"\n",
|
||||
"可以把 `TaskState` 理解为一张任务表,工作流每走一步,就在这张表上补充一些内容。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 编写节点函数\n",
|
||||
"\n",
|
||||
"节点函数就是工作流中的一个步骤。每个节点接收当前 `state`,处理后返回要更新的字段。\n",
|
||||
"\n",
|
||||
"下面定义 3 个节点:\n",
|
||||
"\n",
|
||||
"1. `planner_node`:任务分解节点\n",
|
||||
"2. `worker_node`:任务执行节点\n",
|
||||
"3. `summary_node`:结果汇总节点\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def planner_node(state: TaskState):\n",
|
||||
" goal = state[\"goal\"]\n",
|
||||
" tasks = [\n",
|
||||
" f\"明确目标:{goal}\",\n",
|
||||
" \"列出目标用户和使用场景\",\n",
|
||||
" \"设计可执行的行动步骤\",\n",
|
||||
" \"整理成清晰的建议清单\",\n",
|
||||
" ]\n",
|
||||
" return {\"tasks\": tasks}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def worker_node(state: TaskState):\n",
|
||||
" results = []\n",
|
||||
" for task in state[\"tasks\"]:\n",
|
||||
" result = f\"完成任务:{task}\"\n",
|
||||
" results.append(result)\n",
|
||||
" return {\"results\": results}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def summary_node(state: TaskState):\n",
|
||||
" lines = [\"最终任务调度结果:\"]\n",
|
||||
" for index, result in enumerate(state[\"results\"], start=1):\n",
|
||||
" lines.append(f\"{index}. {result}\")\n",
|
||||
" final_answer = \"\\n\".join(lines)\n",
|
||||
" return {\"final_answer\": final_answer}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码定义了 3 个节点函数:\n",
|
||||
"\n",
|
||||
"1. `planner_node(state)`:从 `state[\"goal\"]` 读取用户目标,然后生成 `tasks` 任务列表。返回 `{\"tasks\": tasks}` 表示把任务列表写回 State。\n",
|
||||
"2. `worker_node(state)`:读取 `state[\"tasks\"]`,循环执行每个任务。这里用字符串模拟执行结果,真实项目中可以替换为调用工具、调用模型、查询数据库等操作。\n",
|
||||
"3. `summary_node(state)`:读取 `state[\"results\"]`,把多个结果拼接成一段最终答案。\n",
|
||||
"\n",
|
||||
"需要注意:节点函数通常不需要返回完整 State,只返回自己负责更新的字段即可。LangGraph 会把这些字段合并回当前 State。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 用 LangGraph 连接节点\n",
|
||||
"\n",
|
||||
"现在有了节点函数,下一步就是把这些节点连成一张图。\n",
|
||||
"\n",
|
||||
"流程如下:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"START -> planner -> worker -> summary -> END\n",
|
||||
"```\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.graph import END, START, StateGraph\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"workflow = StateGraph(TaskState)\n",
|
||||
"\n",
|
||||
"workflow.add_node(\"planner\", planner_node)\n",
|
||||
"workflow.add_node(\"worker\", worker_node)\n",
|
||||
"workflow.add_node(\"summary\", summary_node)\n",
|
||||
"\n",
|
||||
"workflow.add_edge(START, \"planner\")\n",
|
||||
"workflow.add_edge(\"planner\", \"worker\")\n",
|
||||
"workflow.add_edge(\"worker\", \"summary\")\n",
|
||||
"workflow.add_edge(\"summary\", END)\n",
|
||||
"\n",
|
||||
"app = workflow.compile()\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码把节点组织成可运行的工作流:\n",
|
||||
"\n",
|
||||
"1. `StateGraph(TaskState)` 创建一张图,并说明这张图中的数据结构是 `TaskState`。\n",
|
||||
"2. `add_node(\"planner\", planner_node)` 把 `planner_node` 注册成名为 `planner` 的节点。\n",
|
||||
"3. `add_edge(START, \"planner\")` 表示工作流从 `planner` 节点开始。\n",
|
||||
"4. `add_edge(\"planner\", \"worker\")` 表示任务分解完成后,进入任务执行节点。\n",
|
||||
"5. `add_edge(\"worker\", \"summary\")` 表示任务执行完成后,进入结果汇总节点。\n",
|
||||
"6. `add_edge(\"summary\", END)` 表示汇总完成后,工作流结束。\n",
|
||||
"7. `workflow.compile()` 会把流程图编译成一个可以调用的应用对象 `app`。\n",
|
||||
"\n",
|
||||
"这一步相当于把流程图画好,并告诉程序每一步应该怎么走。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 运行任务调度智能体\n",
|
||||
"\n",
|
||||
"工作流编译完成后,就可以传入用户目标并运行了。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"initial_state = {\n",
|
||||
" \"goal\": \"为一家咖啡店设计会员运营方案\",\n",
|
||||
" \"tasks\": [],\n",
|
||||
" \"results\": [],\n",
|
||||
" \"final_answer\": \"\",\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"final_state = app.invoke(initial_state)\n",
|
||||
"\n",
|
||||
"print(final_state[\"final_answer\"])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码负责真正运行工作流:\n",
|
||||
"\n",
|
||||
"1. `initial_state` 是初始状态,里面放入用户目标 `goal`,其余字段先给空值。\n",
|
||||
"2. `app.invoke(initial_state)` 会启动 LangGraph 工作流。数据会依次经过 `planner`、`worker`、`summary` 三个节点。\n",
|
||||
"3. 运行结束后返回 `final_state`,它包含整个流程运行后的完整状态。\n",
|
||||
"4. `final_state[\"final_answer\"]` 取出最终汇总结果并打印。\n",
|
||||
"\n",
|
||||
"从这个例子可以看到,LangGraph 的优势是把复杂流程拆成清晰节点,每个节点只负责一件事。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 加入条件判断:根据任务复杂度选择路径\n",
|
||||
"\n",
|
||||
"真实任务中,不是所有问题都需要复杂调度。\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"- 简单问题:直接回答即可\n",
|
||||
"- 复杂问题:先拆分,再执行,再汇总\n",
|
||||
"\n",
|
||||
"LangGraph 支持条件边,可以根据 State 的内容决定下一步走哪条路径。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Literal\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def judge_complexity(state: TaskState) -> Literal[\"simple\", \"complex\"]:\n",
|
||||
" goal = state[\"goal\"]\n",
|
||||
" if len(goal) <= 12:\n",
|
||||
" return \"simple\"\n",
|
||||
" return \"complex\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def direct_answer_node(state: TaskState):\n",
|
||||
" answer = f\"这是一个简单任务,可以直接处理:{state['goal']}\"\n",
|
||||
" return {\"final_answer\": answer}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码新增了两个函数:\n",
|
||||
"\n",
|
||||
"1. `judge_complexity(state)` 用来判断任务复杂度。这里为了教学简单,用目标文字长度作为判断标准:短任务走 `simple`,长任务走 `complex`。真实项目中可以让大模型判断复杂度。\n",
|
||||
"2. `Literal[\"simple\", \"complex\"]` 表示这个函数只会返回两个固定值之一,方便我们理解后续条件分支。\n",
|
||||
"3. `direct_answer_node(state)` 是简单任务的处理节点,不做任务分解,直接生成最终答案。\n",
|
||||
"\n",
|
||||
"这个例子说明:任务调度不一定永远走同一条流程,可以根据任务情况动态选择路径。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. 构建带条件分支的工作流\n",
|
||||
"\n",
|
||||
"下面把简单路径和复杂路径组合到同一张图里。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"branch_workflow = StateGraph(TaskState)\n",
|
||||
"\n",
|
||||
"branch_workflow.add_node(\"direct_answer\", direct_answer_node)\n",
|
||||
"branch_workflow.add_node(\"planner\", planner_node)\n",
|
||||
"branch_workflow.add_node(\"worker\", worker_node)\n",
|
||||
"branch_workflow.add_node(\"summary\", summary_node)\n",
|
||||
"\n",
|
||||
"branch_workflow.add_conditional_edges(\n",
|
||||
" START,\n",
|
||||
" judge_complexity,\n",
|
||||
" {\n",
|
||||
" \"simple\": \"direct_answer\",\n",
|
||||
" \"complex\": \"planner\",\n",
|
||||
" },\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"branch_workflow.add_edge(\"direct_answer\", END)\n",
|
||||
"branch_workflow.add_edge(\"planner\", \"worker\")\n",
|
||||
"branch_workflow.add_edge(\"worker\", \"summary\")\n",
|
||||
"branch_workflow.add_edge(\"summary\", END)\n",
|
||||
"\n",
|
||||
"branch_app = branch_workflow.compile()\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码构建了一个带分支的任务调度工作流:\n",
|
||||
"\n",
|
||||
"1. `add_conditional_edges` 表示添加条件边。它会先调用 `judge_complexity`,再根据返回值选择下一步。\n",
|
||||
"2. 如果返回 `simple`,流程进入 `direct_answer` 节点,然后直接结束。\n",
|
||||
"3. 如果返回 `complex`,流程进入 `planner` 节点,再继续执行 `worker` 和 `summary`。\n",
|
||||
"4. 这张图有两条路径:一条适合简单任务,一条适合复杂任务。\n",
|
||||
"\n",
|
||||
"这就是任务调度型智能体的一个关键能力:**不是固定执行所有步骤,而是根据任务情况选择合适流程**。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12. 测试条件分支效果\n",
|
||||
"\n",
|
||||
"分别输入一个简单任务和一个复杂任务,观察工作流走向的差异。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"simple_state = {\n",
|
||||
" \"goal\": \"写标题\",\n",
|
||||
" \"tasks\": [],\n",
|
||||
" \"results\": [],\n",
|
||||
" \"final_answer\": \"\",\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"complex_state = {\n",
|
||||
" \"goal\": \"为一家咖啡店设计会员运营方案,并给出执行步骤\",\n",
|
||||
" \"tasks\": [],\n",
|
||||
" \"results\": [],\n",
|
||||
" \"final_answer\": \"\",\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"print(\"简单任务结果:\")\n",
|
||||
"print(branch_app.invoke(simple_state)[\"final_answer\"])\n",
|
||||
"\n",
|
||||
"print(\"\\n复杂任务结果:\")\n",
|
||||
"print(branch_app.invoke(complex_state)[\"final_answer\"])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码测试了条件分支:\n",
|
||||
"\n",
|
||||
"1. `simple_state` 的目标是“写标题”,文字较短,所以 `judge_complexity` 返回 `simple`,流程会直接进入 `direct_answer`。\n",
|
||||
"2. `complex_state` 的目标更长,所以返回 `complex`,流程会进入 `planner -> worker -> summary`。\n",
|
||||
"3. 两次都调用 `branch_app.invoke(...)`,但由于输入不同,工作流自动选择了不同执行路径。\n",
|
||||
"\n",
|
||||
"这说明任务调度型智能体可以做到“简单任务快速处理,复杂任务分步处理”。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 13. 小结\n",
|
||||
"\n",
|
||||
"本节课学习了任务调度型智能体的基本设计方法:\n",
|
||||
"\n",
|
||||
"1. 任务调度型智能体适合处理多步骤、复杂目标。\n",
|
||||
"2. 它的核心流程是:任务输入、任务分解、任务执行、结果汇总。\n",
|
||||
"3. LangGraph 可以把每个步骤定义成节点,并用边连接成工作流。\n",
|
||||
"4. State 是工作流中流动的数据,节点通过读取和更新 State 协同完成任务。\n",
|
||||
"5. 条件边可以让智能体根据任务情况选择不同执行路径。\n",
|
||||
"\n",
|
||||
"掌握这些内容后,就可以继续扩展:把 `worker_node` 替换成真实工具调用、搜索引擎调用、数据库查询或大模型调用,从而构建更实用的自动化任务处理智能体。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 14. 练习题\n",
|
||||
"\n",
|
||||
"1. 修改 `planner_node`,让它针对“写一篇公众号文章”生成任务列表。\n",
|
||||
"2. 修改 `worker_node`,让不同任务返回不同格式的结果。\n",
|
||||
"3. 修改 `judge_complexity`,不要用文字长度判断,而是根据是否包含“方案”“分析”“计划”等关键词判断复杂度。\n",
|
||||
"4. 尝试新增一个 `review_node`,在最终输出前检查结果是否完整。\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
834
23_人机协作.ipynb
Normal file
834
23_人机协作.ipynb
Normal file
@@ -0,0 +1,834 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 23 人机协作\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 理解人机协作(Human-in-the-loop)在智能体中的重要性\n",
|
||||
"2. 掌握 LangGraph 中暂停(`interrupt`)和恢复(`resume`)机制\n",
|
||||
"3. 能够设计需要人工确认或补充输入的智能体流程\n",
|
||||
"4. 理解检查点在暂停与恢复中的作用\n",
|
||||
"5. 能够构建带人工审批环节的基础智能体工作流"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 什么是人机协作\n",
|
||||
"\n",
|
||||
"人机协作,也叫 Human-in-the-loop,意思是:**智能体不是从头到尾完全自动执行,而是在关键步骤暂停下来,让人参与判断、确认或补充信息。**\n",
|
||||
"\n",
|
||||
"普通自动流程可能是这样:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"输入 -> 智能体处理 -> 自动执行 -> 输出结果\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"人机协作流程更像这样:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"输入 -> 智能体处理 -> 暂停等待人工确认 -> 继续执行 -> 输出结果\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"也就是说,人不是在流程外面旁观,而是成为流程中的一个关键环节。\n",
|
||||
"\n",
|
||||
"在人机协作中,AI 适合做:\n",
|
||||
"\n",
|
||||
"- 整理信息\n",
|
||||
"- 生成草稿\n",
|
||||
"- 给出建议\n",
|
||||
"- 自动处理低风险步骤\n",
|
||||
"\n",
|
||||
"人更适合做:\n",
|
||||
"\n",
|
||||
"- 最终确认\n",
|
||||
"- 风险判断\n",
|
||||
"- 补充缺失信息\n",
|
||||
"- 决定是否继续执行\n",
|
||||
"\n",
|
||||
"这就是人机协作的核心思想:**让 AI 提高效率,让人把控关键决策。**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 为什么智能体需要人工介入\n",
|
||||
"\n",
|
||||
"很多智能体任务不能完全交给模型自动完成。原因主要有三类。\n",
|
||||
"\n",
|
||||
"### 2.1 有些操作风险较高\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"- 发送正式邮件\n",
|
||||
"- 删除数据\n",
|
||||
"- 提交订单\n",
|
||||
"- 执行付款\n",
|
||||
"- 修改线上配置\n",
|
||||
"\n",
|
||||
"这些操作一旦执行,可能造成真实影响,所以最好先让人确认。\n",
|
||||
"\n",
|
||||
"### 2.2 有些信息 AI 不知道\n",
|
||||
"例如用户说:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"帮我给客户写一封邮件。\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"但 AI 可能不知道客户姓名、邮件目的、语气要求、是否要附带报价。\n",
|
||||
"\n",
|
||||
"这时流程应该暂停,让用户补充信息。\n",
|
||||
"\n",
|
||||
"### 2.3 有些判断需要人负责\n",
|
||||
"例如 AI 生成了一个方案,但最终是否采用,需要人做业务判断。\n",
|
||||
"\n",
|
||||
"所以,人机协作不是因为 AI 不够强,而是因为很多场景天然需要人工负责。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. LangGraph 中的人机协作机制\n",
|
||||
"\n",
|
||||
"在 LangGraph 中,人机协作通常依赖两个核心动作:\n",
|
||||
"\n",
|
||||
"| 动作 | 含义 | 作用 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| `interrupt(...)` | 暂停流程 | 把需要人工处理的信息抛出来 |\n",
|
||||
"| `Command(resume=...)` | 恢复流程 | 把人工输入传回图中,继续执行 |\n",
|
||||
"\n",
|
||||
"可以这样理解:\n",
|
||||
"\n",
|
||||
"- `interrupt` 像是智能体举手说:‘这里需要人来决定一下’\n",
|
||||
"- `Command(resume=...)` 像是人回复:‘我决定好了,你继续吧’\n",
|
||||
"\n",
|
||||
"不过要注意:暂停和恢复必须依赖检查点(checkpoint)。\n",
|
||||
"\n",
|
||||
"因为图暂停后,需要记住自己停在了哪里。等人给出答案后,才能从正确的位置继续执行。\n",
|
||||
"\n",
|
||||
"所以,人机协作一般会和 `MemorySaver` 一起使用。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 第一个例子:人工确认后再继续\n",
|
||||
"\n",
|
||||
"先从最简单的例子开始:智能体准备执行一个操作,但在执行前需要人工确认。\n",
|
||||
"\n",
|
||||
"流程如下:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> request_approval -> perform_action -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"其中 `request_approval` 会暂停,等待用户输入 `yes` 或 `no`。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "7085b2d5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from langgraph.types import interrupt, Command\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"class ApprovalState(TypedDict):\n",
|
||||
" task: str\n",
|
||||
" approved: bool\n",
|
||||
" result: str\n",
|
||||
"\n",
|
||||
"def request_approval(state: ApprovalState):\n",
|
||||
" answer = interrupt({\n",
|
||||
" 'question': '是否允许执行这个任务?',\n",
|
||||
" 'task': state['task'],\n",
|
||||
" 'options': ['yes', 'no']\n",
|
||||
" })\n",
|
||||
" return {'approved': answer == 'yes'}\n",
|
||||
"\n",
|
||||
"def perform_action(state: ApprovalState):\n",
|
||||
" if state['approved']:\n",
|
||||
" return {'result': f'任务已执行:{state[\"task\"]}'}\n",
|
||||
" return {'result': f'任务已取消:{state[\"task\"]}'}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(ApprovalState)\n",
|
||||
"builder.add_node('request_approval', request_approval)\n",
|
||||
"builder.add_node('perform_action', perform_action)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'request_approval')\n",
|
||||
"builder.add_edge('request_approval', 'perform_action')\n",
|
||||
"builder.add_edge('perform_action', END)\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"graph = builder.compile(checkpointer=memory)\n",
|
||||
"\n",
|
||||
"config = {'configurable': {'thread_id': 'approval-demo'}}\n",
|
||||
"\n",
|
||||
"first_result = graph.invoke(\n",
|
||||
" {'task': '发送项目周报邮件', 'approved': False, 'result': ''},\n",
|
||||
" config=config\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(first_result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "07b99ca8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子第一次使用了 `interrupt`,所以需要重点理解。\n",
|
||||
"\n",
|
||||
"#### `ApprovalState`\n",
|
||||
"状态里有三个字段:\n",
|
||||
"\n",
|
||||
"- `task`:准备执行的任务\n",
|
||||
"- `approved`:人工是否批准\n",
|
||||
"- `result`:最终执行结果\n",
|
||||
"\n",
|
||||
"也就是说,整张图围绕一个任务展开,先确认,再执行或取消。\n",
|
||||
"\n",
|
||||
"#### `request_approval` 节点\n",
|
||||
"这个节点的核心代码是:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"answer = interrupt({...})\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"当代码执行到这里时,图不会继续往下走,而是暂停下来。\n",
|
||||
"\n",
|
||||
"`interrupt` 里面传入的是要展示给人的信息。这里包括:\n",
|
||||
"\n",
|
||||
"- 问题:是否允许执行这个任务\n",
|
||||
"- 任务内容:`state['task']`\n",
|
||||
"- 可选项:`yes` 或 `no`\n",
|
||||
"\n",
|
||||
"#### 为什么 `interrupt` 后面还有 return\n",
|
||||
"这行代码:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"return {'approved': answer == 'yes'}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"不是第一次暂停时立刻执行完成的。\n",
|
||||
"\n",
|
||||
"第一次运行到 `interrupt` 时,图会停住。等我们用 `Command(resume='yes')` 恢复时,`interrupt` 才会把 `'yes'` 作为返回值交给 `answer`,然后继续执行后面的 `return`。\n",
|
||||
"\n",
|
||||
"#### `perform_action` 节点\n",
|
||||
"这个节点根据 `approved` 决定结果:\n",
|
||||
"\n",
|
||||
"- 如果 `approved=True`,说明人工批准,任务执行\n",
|
||||
"- 如果 `approved=False`,说明人工拒绝,任务取消\n",
|
||||
"\n",
|
||||
"#### 为什么需要 `MemorySaver`\n",
|
||||
"因为图会暂停。暂停之后,系统必须记住:\n",
|
||||
"\n",
|
||||
"- 当前执行到哪个节点\n",
|
||||
"- 当前状态是什么\n",
|
||||
"- 后面应该从哪里继续\n",
|
||||
"\n",
|
||||
"这些信息都需要检查点保存。`MemorySaver` 就是一个内存版检查点保存器。\n",
|
||||
"\n",
|
||||
"#### 第一次运行会得到什么\n",
|
||||
"第一次 `invoke` 不会直接得到最终结果,而是会得到一个包含中断信息的结果。\n",
|
||||
"\n",
|
||||
"这表示图已经暂停,正在等待人工输入。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "185c4df9",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 恢复流程:用 `Command(resume=...)` 传回人工决定\n",
|
||||
"\n",
|
||||
"上一步图已经暂停。现在我们模拟人工选择 `yes`,让流程继续执行。\n",
|
||||
"\n",
|
||||
"恢复时要使用同一个 `config`,尤其是同一个 `thread_id`。\n",
|
||||
"\n",
|
||||
"因为 LangGraph 需要根据 `thread_id` 找回刚才暂停的那一次运行。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "0c18357d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"second_result = graph.invoke(\n",
|
||||
" Command(resume='yes'),\n",
|
||||
" config=config\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(second_result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "31abd5f8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这一段代码负责恢复刚才暂停的流程。\n",
|
||||
"\n",
|
||||
"#### `Command(resume='yes')`\n",
|
||||
"它的意思是:把人工输入 `'yes'` 传回给刚才的 `interrupt`。\n",
|
||||
"\n",
|
||||
"也就是说,前面这行代码:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"answer = interrupt(...)\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"会在恢复后得到:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"answer = 'yes'\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"然后节点继续执行:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"return {'approved': answer == 'yes'}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"于是 `approved` 会变成 `True`。\n",
|
||||
"\n",
|
||||
"#### 为什么还要传 `config=config`\n",
|
||||
"因为恢复流程必须知道要恢复哪一次暂停。\n",
|
||||
"\n",
|
||||
"`thread_id='approval-demo'` 就像这次流程的编号。\n",
|
||||
"\n",
|
||||
"如果换了另一个 `thread_id`,LangGraph 就找不到刚才暂停的位置。\n",
|
||||
"\n",
|
||||
"#### 恢复后的执行路径\n",
|
||||
"恢复后流程会继续:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"request_approval -> perform_action -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"最终会得到:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"任务已执行:发送项目周报邮件\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这就是最基本的人机协作流程。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1fd00b84",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 第二个例子:人工补充缺失信息\n",
|
||||
"\n",
|
||||
"人机协作不只是审批,也可以用来补充信息。\n",
|
||||
"\n",
|
||||
"例如用户想让智能体写邮件,但没有提供收件人。\n",
|
||||
"\n",
|
||||
"流程可以这样设计:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"START -> check_info -> draft_email -> END\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"如果缺少收件人,`check_info` 就暂停,让用户补充。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "4000512e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from langgraph.types import interrupt, Command\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"class EmailState(TypedDict):\n",
|
||||
" topic: str\n",
|
||||
" recipient: str\n",
|
||||
" email_draft: str\n",
|
||||
"\n",
|
||||
"def check_info(state: EmailState):\n",
|
||||
" if state['recipient']:\n",
|
||||
" return {}\n",
|
||||
"\n",
|
||||
" recipient = interrupt({\n",
|
||||
" 'question': '请补充邮件收件人',\n",
|
||||
" 'current_topic': state['topic']\n",
|
||||
" })\n",
|
||||
" return {'recipient': recipient}\n",
|
||||
"\n",
|
||||
"def draft_email(state: EmailState):\n",
|
||||
" draft = f'''收件人:{state[\"recipient\"]}\n",
|
||||
"主题:{state[\"topic\"]}\n",
|
||||
"\n",
|
||||
"你好,\n",
|
||||
"这里是一封关于“{state[\"topic\"]}”的邮件草稿。\n",
|
||||
"请根据具体情况补充正文细节。\n",
|
||||
"'''\n",
|
||||
" return {'email_draft': draft}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(EmailState)\n",
|
||||
"builder.add_node('check_info', check_info)\n",
|
||||
"builder.add_node('draft_email', draft_email)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'check_info')\n",
|
||||
"builder.add_edge('check_info', 'draft_email')\n",
|
||||
"builder.add_edge('draft_email', END)\n",
|
||||
"\n",
|
||||
"email_graph = builder.compile(checkpointer=MemorySaver())\n",
|
||||
"email_config = {'configurable': {'thread_id': 'email-demo'}}\n",
|
||||
"\n",
|
||||
"first_result = email_graph.invoke(\n",
|
||||
" {'topic': '下周项目会议安排', 'recipient': '', 'email_draft': ''},\n",
|
||||
" config=email_config\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(first_result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "84056ad6",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子展示了人机协作的另一种常见用途:补充缺失信息。\n",
|
||||
"\n",
|
||||
"#### `EmailState`\n",
|
||||
"状态包含三个字段:\n",
|
||||
"\n",
|
||||
"- `topic`:邮件主题\n",
|
||||
"- `recipient`:邮件收件人\n",
|
||||
"- `email_draft`:生成的邮件草稿\n",
|
||||
"\n",
|
||||
"#### `check_info` 节点\n",
|
||||
"这个节点负责检查信息是否完整。\n",
|
||||
"\n",
|
||||
"如果 `recipient` 已经有值,就返回 `{}`,表示不需要修改状态。\n",
|
||||
"\n",
|
||||
"如果 `recipient` 为空,就执行:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"recipient = interrupt({...})\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这会暂停流程,并向用户请求收件人信息。\n",
|
||||
"\n",
|
||||
"#### 为什么这里不直接报错\n",
|
||||
"在真实智能体中,缺少信息不一定是错误。\n",
|
||||
"\n",
|
||||
"更好的做法是:停下来问人。\n",
|
||||
"\n",
|
||||
"这就是人机协作比普通异常处理更适合智能体流程的地方。\n",
|
||||
"\n",
|
||||
"#### `draft_email` 节点\n",
|
||||
"这个节点依赖完整信息生成邮件草稿。\n",
|
||||
"\n",
|
||||
"它会读取:\n",
|
||||
"\n",
|
||||
"- `state['recipient']`\n",
|
||||
"- `state['topic']`\n",
|
||||
"\n",
|
||||
"然后写入 `email_draft`。\n",
|
||||
"\n",
|
||||
"#### 第一次运行为什么会暂停\n",
|
||||
"因为初始状态中:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"'recipient': ''\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"所以流程会停在 `check_info`,等待人补充收件人。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cf5e9b4b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 恢复并补充收件人\n",
|
||||
"\n",
|
||||
"现在模拟人工输入收件人,例如:`张经理`。\n",
|
||||
"\n",
|
||||
"恢复后,流程会继续进入 `draft_email`,并生成邮件草稿。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "081941b0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"final_result = email_graph.invoke(\n",
|
||||
" Command(resume='张经理'),\n",
|
||||
" config=email_config\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(final_result['email_draft'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8c7dade8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这一段代码把人工补充的信息传回给图。\n",
|
||||
"\n",
|
||||
"#### `Command(resume='张经理')`\n",
|
||||
"它会让刚才暂停的 `interrupt` 返回 `'张经理'`。\n",
|
||||
"\n",
|
||||
"于是这行代码:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"recipient = interrupt(...)\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"恢复后就相当于:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"recipient = '张经理'\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"然后节点返回:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"return {'recipient': recipient}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"状态中的收件人就被补上了。\n",
|
||||
"\n",
|
||||
"#### 后续为什么能生成邮件草稿\n",
|
||||
"恢复后,图会继续执行 `draft_email`。\n",
|
||||
"\n",
|
||||
"这时状态已经包含:\n",
|
||||
"\n",
|
||||
"- `topic='下周项目会议安排'`\n",
|
||||
"- `recipient='张经理'`\n",
|
||||
"\n",
|
||||
"所以就可以生成完整的邮件草稿。\n",
|
||||
"\n",
|
||||
"#### 这个例子的重点\n",
|
||||
"人机协作不是只能做‘同意/不同意’。\n",
|
||||
"\n",
|
||||
"它也可以用来让人补充缺失信息,然后让图继续完成后续任务。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6090d74f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 第三个例子:人工修改 AI 草稿\n",
|
||||
"\n",
|
||||
"在很多真实场景中,AI 不一定直接执行最终结果,而是先生成一个草稿,让人修改或确认。\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"- AI 生成邮件草稿,人修改后再发送\n",
|
||||
"- AI 生成计划,人确认后再执行\n",
|
||||
"- AI 生成 SQL,人审核后再运行\n",
|
||||
"\n",
|
||||
"下面我们用一个简单的任务计划示例。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "88a81e02",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from langgraph.types import interrupt, Command\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"class PlanState(TypedDict):\n",
|
||||
" goal: str\n",
|
||||
" draft_plan: str\n",
|
||||
" final_plan: str\n",
|
||||
"\n",
|
||||
"def create_draft_plan(state: PlanState):\n",
|
||||
" draft = f'''目标:{state[\"goal\"]}\n",
|
||||
"步骤 1:明确当前需求\n",
|
||||
"步骤 2:拆分任务并安排执行顺序\n",
|
||||
"步骤 3:检查结果并总结经验\n",
|
||||
"'''\n",
|
||||
" return {'draft_plan': draft}\n",
|
||||
"\n",
|
||||
"def review_plan(state: PlanState):\n",
|
||||
" revised_plan = interrupt({\n",
|
||||
" 'question': '请审核并修改计划草稿',\n",
|
||||
" 'draft_plan': state['draft_plan']\n",
|
||||
" })\n",
|
||||
" return {'final_plan': revised_plan}\n",
|
||||
"\n",
|
||||
"builder = StateGraph(PlanState)\n",
|
||||
"builder.add_node('create_draft_plan', create_draft_plan)\n",
|
||||
"builder.add_node('review_plan', review_plan)\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, 'create_draft_plan')\n",
|
||||
"builder.add_edge('create_draft_plan', 'review_plan')\n",
|
||||
"builder.add_edge('review_plan', END)\n",
|
||||
"\n",
|
||||
"plan_graph = builder.compile(checkpointer=MemorySaver())\n",
|
||||
"plan_config = {'configurable': {'thread_id': 'plan-demo'}}\n",
|
||||
"\n",
|
||||
"review_result = plan_graph.invoke(\n",
|
||||
" {'goal': '准备一次 LangGraph 分享', 'draft_plan': '', 'final_plan': ''},\n",
|
||||
" config=plan_config\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(review_result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "88b97a79",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个例子展示的是‘AI 先生成草稿,人再修改确认’的模式。\n",
|
||||
"\n",
|
||||
"#### `PlanState`\n",
|
||||
"状态中有三个字段:\n",
|
||||
"\n",
|
||||
"- `goal`:目标\n",
|
||||
"- `draft_plan`:AI 或程序生成的草稿计划\n",
|
||||
"- `final_plan`:人工确认后的最终计划\n",
|
||||
"\n",
|
||||
"#### `create_draft_plan` 节点\n",
|
||||
"这个节点根据目标生成一个初步计划。\n",
|
||||
"\n",
|
||||
"这里为了教学简单,计划是用模板字符串生成的。\n",
|
||||
"\n",
|
||||
"在真实项目中,这一步也可以换成大模型生成。\n",
|
||||
"\n",
|
||||
"#### `review_plan` 节点\n",
|
||||
"这个节点调用 `interrupt`,把草稿交给人审核。\n",
|
||||
"\n",
|
||||
"传给人的信息包括:\n",
|
||||
"\n",
|
||||
"- 问题:请审核并修改计划草稿\n",
|
||||
"- 当前草稿:`state['draft_plan']`\n",
|
||||
"\n",
|
||||
"人可以直接接受,也可以修改后再返回。\n",
|
||||
"\n",
|
||||
"#### 为什么返回 `final_plan`\n",
|
||||
"AI 生成的是草稿,不一定等于最终结果。\n",
|
||||
"\n",
|
||||
"人工修改后的内容才写入 `final_plan`。\n",
|
||||
"\n",
|
||||
"这能清楚地区分:\n",
|
||||
"\n",
|
||||
"- AI 草稿是什么\n",
|
||||
"- 人工最终确认的版本是什么\n",
|
||||
"\n",
|
||||
"这在真实工作流中非常重要,因为最终责任通常属于人工确认后的结果。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f0da01c5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 恢复并提交人工修改后的计划\n",
|
||||
"\n",
|
||||
"现在模拟人工审核后,提交一个修改版计划。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "31d862b2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"human_revised_plan = '''目标:准备一次 LangGraph 分享\n",
|
||||
"步骤 1:先介绍图结构、状态和边的概念\n",
|
||||
"步骤 2:演示一个最小可运行的 LangGraph 示例\n",
|
||||
"步骤 3:重点讲解人机协作中的 interrupt 和 resume\n",
|
||||
"步骤 4:最后总结适合人机协作的真实业务场景\n",
|
||||
"'''\n",
|
||||
"\n",
|
||||
"final_plan_result = plan_graph.invoke(\n",
|
||||
" Command(resume=human_revised_plan),\n",
|
||||
" config=plan_config\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(final_plan_result['final_plan'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码模拟人工修改草稿后,把最终版本交回给图。\n",
|
||||
"\n",
|
||||
"#### `human_revised_plan`\n",
|
||||
"这是人工修改后的计划。\n",
|
||||
"\n",
|
||||
"它和原始草稿相比,更具体,也更贴近分享主题。\n",
|
||||
"\n",
|
||||
"#### `Command(resume=human_revised_plan)`\n",
|
||||
"这表示把人工修改后的计划传回给 `interrupt`。\n",
|
||||
"\n",
|
||||
"恢复后,`review_plan` 节点会继续执行:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"return {'final_plan': revised_plan}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"于是最终状态中就有了人工确认后的 `final_plan`。\n",
|
||||
"\n",
|
||||
"#### 这个模式有什么用\n",
|
||||
"这类流程非常适合高风险或高要求任务。\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"- AI 生成合同条款,人审核后确认\n",
|
||||
"- AI 生成客户邮件,人修改后发送\n",
|
||||
"- AI 生成执行计划,人确认后落地\n",
|
||||
"\n",
|
||||
"核心思想是:AI 负责初稿,人负责把关。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 人机协作流程的关键点\n",
|
||||
"\n",
|
||||
"设计人机协作流程时,要特别注意下面几点。\n",
|
||||
"\n",
|
||||
"### 10.1 暂停点要放在关键位置\n",
|
||||
"不是每一步都需要人确认。\n",
|
||||
"\n",
|
||||
"一般只在这些地方暂停:\n",
|
||||
"\n",
|
||||
"- 信息缺失\n",
|
||||
"- 风险较高\n",
|
||||
"- 需要业务判断\n",
|
||||
"- 执行前需要最终确认\n",
|
||||
"\n",
|
||||
"### 10.2 给人的信息要清楚\n",
|
||||
"`interrupt` 传出的内容应该让人一眼看懂:\n",
|
||||
"\n",
|
||||
"- 当前要做什么\n",
|
||||
"- 为什么要暂停\n",
|
||||
"- 希望人提供什么\n",
|
||||
"- 可选项有哪些\n",
|
||||
"\n",
|
||||
"### 10.3 恢复时必须使用同一个线程\n",
|
||||
"恢复流程时,必须使用同一个 `thread_id`。\n",
|
||||
"\n",
|
||||
"否则图找不到之前暂停的位置。\n",
|
||||
"\n",
|
||||
"### 10.4 不要滥用人工确认\n",
|
||||
"如果每一步都让人确认,智能体就会变得很繁琐。\n",
|
||||
"\n",
|
||||
"合理做法是:低风险步骤自动执行,高风险步骤人工把关。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. 人机协作和普通 input() 的区别\n",
|
||||
"\n",
|
||||
"你可能会问:为什么不直接用 Python 的 `input()`?\n",
|
||||
"\n",
|
||||
"因为 LangGraph 的 `interrupt` 不只是简单输入,它更适合智能体工作流。\n",
|
||||
"\n",
|
||||
"| 对比项 | `input()` | `interrupt` |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 是否适合 notebook / 服务端流程 | 不太适合 | 更适合 |\n",
|
||||
"| 是否能保存暂停状态 | 不能 | 可以,依赖检查点 |\n",
|
||||
"| 是否能恢复到图中原位置 | 不能 | 可以 |\n",
|
||||
"| 是否适合多用户多会话 | 弱 | 强,配合 `thread_id` |\n",
|
||||
"| 是否和 LangGraph 状态集成 | 否 | 是 |\n",
|
||||
"\n",
|
||||
"所以在人机协作智能体中,推荐使用 `interrupt`,而不是普通 `input()`。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12. 本节小结\n",
|
||||
"\n",
|
||||
"本节最重要的内容有五点:\n",
|
||||
"\n",
|
||||
"1. **人机协作是让人在关键节点参与智能体流程**\n",
|
||||
"2. **`interrupt` 用于暂停流程,并把需要人工处理的信息交出去**\n",
|
||||
"3. **`Command(resume=...)` 用于把人工输入传回图中,并恢复执行**\n",
|
||||
"4. **暂停和恢复必须依赖检查点,通常会配合 `MemorySaver` 使用**\n",
|
||||
"5. **人工介入适合审批、补充信息、修改草稿和高风险操作确认**\n",
|
||||
"\n",
|
||||
"掌握人机协作后,智能体就不再只是自动执行脚本,而可以变成一个真正能和人配合工作的流程系统。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 13. 本节练习\n",
|
||||
"\n",
|
||||
"1. 修改第一个示例,把任务改成‘删除临时文件’,观察批准和拒绝的结果\n",
|
||||
"2. 修改第二个示例,让缺少邮件主题时也触发一次人工补充\n",
|
||||
"3. 修改第三个示例,让人工可以输入 `accept` 表示直接接受草稿\n",
|
||||
"4. 思考:哪些智能体操作必须经过人工确认?\n",
|
||||
"5. 思考:如果一个系统有多个用户同时使用,为什么 `thread_id` 很重要?"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
662
24_项目实战一.ipynb
Normal file
662
24_项目实战一.ipynb
Normal file
@@ -0,0 +1,662 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 24_项目实战一:个人知识库问答助手\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 综合运用 LangChain 和 LangGraph 知识解决实际问题\n",
|
||||
"2. 掌握项目需求分析、模块划分和技术选型\n",
|
||||
"3. 开始构建第一个综合项目:个人知识库问答助手\n",
|
||||
"\n",
|
||||
"本节课会从项目角度出发,带大家一步步设计一个“个人知识库问答助手”。它的核心能力是:**用户上传或准备一些资料,助手根据资料内容回答问题,而不是凭空编答案**。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 项目背景:为什么需要个人知识库问答助手\n",
|
||||
"\n",
|
||||
"日常学习和工作中,我们经常会有很多资料,例如:\n",
|
||||
"\n",
|
||||
"- 课程笔记\n",
|
||||
"- 项目文档\n",
|
||||
"- 公司制度\n",
|
||||
"- 技术文章\n",
|
||||
"- 会议记录\n",
|
||||
"\n",
|
||||
"如果资料很多,直接人工查找会很慢。个人知识库问答助手要解决的问题就是:\n",
|
||||
"\n",
|
||||
"> 用户提出问题,系统先从资料中找到相关内容,再基于这些内容生成回答。\n",
|
||||
"\n",
|
||||
"这类系统通常使用 RAG 技术。RAG 的全称是 Retrieval-Augmented Generation,中文可以理解为“检索增强生成”。\n",
|
||||
"\n",
|
||||
"通俗地说:\n",
|
||||
"\n",
|
||||
"1. 先查资料\n",
|
||||
"2. 再带着资料回答\n",
|
||||
"\n",
|
||||
"这样可以减少大模型胡编乱造的问题。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 需求分析\n",
|
||||
"\n",
|
||||
"做项目之前,不要急着写代码。第一步应该先想清楚:这个项目到底要解决什么问题。\n",
|
||||
"\n",
|
||||
"本项目的基础需求如下:\n",
|
||||
"\n",
|
||||
"| 需求 | 说明 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| 导入资料 | 准备一批文本资料,作为知识库内容 |\n",
|
||||
"| 文本切分 | 把长文档切成小段,方便检索 |\n",
|
||||
"| 内容检索 | 根据用户问题找到最相关的资料片段 |\n",
|
||||
"| 生成回答 | 根据检索到的资料组织答案 |\n",
|
||||
"| 返回依据 | 告诉用户答案主要参考了哪些内容 |\n",
|
||||
"\n",
|
||||
"第一版项目先做最小可用版本,也就是 MVP。我们先不处理复杂文件上传,也不接入真实数据库,而是用少量文本模拟知识库。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 项目整体流程\n",
|
||||
"\n",
|
||||
"个人知识库问答助手的流程可以画成下面这样:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"准备文档 -> 切分文档 -> 建立索引 -> 用户提问 -> 检索相关片段 -> 生成答案\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"如果用生活中的例子理解:\n",
|
||||
"\n",
|
||||
"- 准备文档:把书放进书架\n",
|
||||
"- 切分文档:给书按章节拆开\n",
|
||||
"- 建立索引:做目录和标签\n",
|
||||
"- 用户提问:用户问“某个知识点是什么”\n",
|
||||
"- 检索相关片段:先翻目录找到相关章节\n",
|
||||
"- 生成答案:根据章节内容整理成自然语言回答\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 技术选型\n",
|
||||
"\n",
|
||||
"在真实项目中,可以使用下面这些技术:\n",
|
||||
"\n",
|
||||
"| 模块 | 可选技术 | 作用 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 文档加载 | LangChain DocumentLoader | 读取 PDF、Markdown、网页等资料 |\n",
|
||||
"| 文本切分 | RecursiveCharacterTextSplitter | 把长文本切成小块 |\n",
|
||||
"| 向量模型 | Embeddings | 把文本转成向量 |\n",
|
||||
"| 向量数据库 | FAISS、Chroma | 存储并检索相似文本 |\n",
|
||||
"| 大模型 | ChatOpenAI 或其他模型 | 根据资料生成答案 |\n",
|
||||
"| 流程编排 | LangGraph | 把检索、回答、检查等步骤组织成工作流 |\n",
|
||||
"\n",
|
||||
"为了让代码更容易运行,本节先用纯 Python 实现一个简化版 RAG。理解原理后,再过渡到 LangChain 和 LangGraph。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 第一步:准备模拟知识库\n",
|
||||
"\n",
|
||||
"我们先不用读取外部文件,而是在代码中准备几段文本,模拟个人知识库中的资料。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"documents = [\n",
|
||||
" \"LangChain 是一个用于开发大模型应用的框架,它提供模型调用、提示词模板、文档加载、检索问答等能力。\",\n",
|
||||
" \"LangGraph 是 LangChain 生态中的流程编排框架,适合构建多步骤、有状态、可分支的智能体应用。\",\n",
|
||||
" \"RAG 是检索增强生成技术,核心流程是先从知识库中检索相关资料,再让大模型基于资料生成答案。\",\n",
|
||||
" \"个人知识库问答助手可以帮助用户从自己的笔记、文档和资料中快速找到答案,减少人工查找成本。\",\n",
|
||||
" \"向量数据库用于存储文本向量,并根据相似度快速找到与用户问题最相关的文本片段。\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"print(f\"知识库中共有 {len(documents)} 条资料\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码完成了知识库的准备工作:\n",
|
||||
"\n",
|
||||
"1. `documents` 是一个列表,列表里的每个字符串代表一条资料。\n",
|
||||
"2. 这里的资料围绕 LangChain、LangGraph、RAG、个人知识库和向量数据库。\n",
|
||||
"3. `len(documents)` 用来统计资料数量。\n",
|
||||
"4. 在真实项目中,`documents` 通常不是手写的,而是从 PDF、Markdown、Word、网页或数据库中读取出来的。\n",
|
||||
"\n",
|
||||
"这一步对应项目流程中的“准备文档”。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 第二步:文本切分\n",
|
||||
"\n",
|
||||
"真实文档可能很长,如果直接把整篇文档拿去检索,效果通常不好。\n",
|
||||
"\n",
|
||||
"所以我们需要把长文本切成较小的片段。这里用一个简单函数演示文本切分。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def split_text(text, chunk_size=30):\n",
|
||||
" chunks = []\n",
|
||||
" for start in range(0, len(text), chunk_size):\n",
|
||||
" chunk = text[start:start + chunk_size]\n",
|
||||
" chunks.append(chunk)\n",
|
||||
" return chunks\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"all_chunks = []\n",
|
||||
"for doc in documents:\n",
|
||||
" chunks = split_text(doc, chunk_size=30)\n",
|
||||
" all_chunks.extend(chunks)\n",
|
||||
"\n",
|
||||
"for index, chunk in enumerate(all_chunks, start=1):\n",
|
||||
" print(f\"片段 {index}: {chunk}\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码实现了一个简化版文本切分器:\n",
|
||||
"\n",
|
||||
"1. `split_text(text, chunk_size=30)` 表示把文本按固定长度切分,每段最多 30 个字符。\n",
|
||||
"2. `range(0, len(text), chunk_size)` 会生成每个片段的起始位置。\n",
|
||||
"3. `text[start:start + chunk_size]` 使用字符串切片取出一小段文本。\n",
|
||||
"4. `all_chunks.extend(chunks)` 把每篇文档切出来的片段合并到总列表中。\n",
|
||||
"5. 最后的循环会打印所有文本片段,方便观察切分效果。\n",
|
||||
"\n",
|
||||
"真实项目中,通常会使用 LangChain 的 `RecursiveCharacterTextSplitter`,它比这里的固定长度切分更智能,会尽量按段落、句子等边界切分。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 第三步:实现一个简单检索器\n",
|
||||
"\n",
|
||||
"标准 RAG 通常会用向量相似度检索。为了让初学者更容易理解,我们先用“关键词重合数量”模拟检索。\n",
|
||||
"\n",
|
||||
"规则很简单:用户问题和某个资料片段中重复出现的字越多,就认为它越相关。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def simple_score(question, chunk):\n",
|
||||
" question_chars = set(question)\n",
|
||||
" chunk_chars = set(chunk)\n",
|
||||
" common_chars = question_chars & chunk_chars\n",
|
||||
" return len(common_chars)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def retrieve(question, chunks, top_k=2):\n",
|
||||
" scored_chunks = []\n",
|
||||
" for chunk in chunks:\n",
|
||||
" score = simple_score(question, chunk)\n",
|
||||
" scored_chunks.append((score, chunk))\n",
|
||||
"\n",
|
||||
" scored_chunks.sort(reverse=True, key=lambda item: item[0])\n",
|
||||
" return scored_chunks[:top_k]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"question = \"什么是 RAG?\"\n",
|
||||
"retrieved_chunks = retrieve(question, all_chunks, top_k=3)\n",
|
||||
"\n",
|
||||
"for score, chunk in retrieved_chunks:\n",
|
||||
" print(f\"相关度分数:{score},内容:{chunk}\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码实现了一个简单检索器:\n",
|
||||
"\n",
|
||||
"1. `simple_score(question, chunk)` 用来计算问题和资料片段的相关度。\n",
|
||||
"2. `set(question)` 会把问题中的字符去重后放入集合。\n",
|
||||
"3. `question_chars & chunk_chars` 表示取两个集合的交集,也就是问题和片段中共同出现的字符。\n",
|
||||
"4. `len(common_chars)` 用共同字符数量作为相关度分数。\n",
|
||||
"5. `retrieve(question, chunks, top_k=2)` 会给所有片段打分,并返回分数最高的前 `top_k` 个。\n",
|
||||
"6. `scored_chunks.sort(reverse=True, key=lambda item: item[0])` 表示按照分数从高到低排序。\n",
|
||||
"\n",
|
||||
"这个检索器很简单,但它帮助我们理解了检索阶段的本质:**从大量资料中找出最可能有用的内容**。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 第四步:根据检索结果生成答案\n",
|
||||
"\n",
|
||||
"在真实 RAG 系统中,生成答案通常由大模型完成。\n",
|
||||
"\n",
|
||||
"这里为了避免依赖外部 API,我们先用一个函数模拟回答生成:把检索到的资料整理成答案。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def generate_answer(question, retrieved_chunks):\n",
|
||||
" context = \"\".join([chunk for score, chunk in retrieved_chunks])\n",
|
||||
" answer = f\"问题:{question}\\n\\n\"\n",
|
||||
" answer += \"根据知识库中检索到的内容,可以参考以下信息回答:\\n\"\n",
|
||||
" answer += context\n",
|
||||
" answer += \"\\n\\n总结:以上内容是从个人知识库中找到的相关资料,回答时应优先依据这些资料。\"\n",
|
||||
" return answer\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"answer = generate_answer(question, retrieved_chunks)\n",
|
||||
"print(answer)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码模拟了回答生成过程:\n",
|
||||
"\n",
|
||||
"1. `generate_answer(question, retrieved_chunks)` 接收用户问题和检索结果。\n",
|
||||
"2. `[chunk for score, chunk in retrieved_chunks]` 从检索结果中取出文本片段,忽略分数。\n",
|
||||
"3. `'\\n'.join(...)` 把多个片段用换行符拼接成上下文。\n",
|
||||
"4. `answer += ...` 逐步拼接最终回答内容。\n",
|
||||
"5. 真实项目中,这一步通常会把 `question` 和 `context` 放进提示词,然后交给大模型生成更自然、更完整的回答。\n",
|
||||
"\n",
|
||||
"这一阶段的关键原则是:**回答要基于检索到的资料,而不是凭空生成**。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 封装成一个完整问答函数\n",
|
||||
"\n",
|
||||
"现在我们已经有了文档片段、检索器和回答生成函数。下一步把它们封装成一个完整的 `ask_knowledge_base` 函数。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def ask_knowledge_base(question, chunks, top_k=3):\n",
|
||||
" retrieved = retrieve(question, chunks, top_k=top_k)\n",
|
||||
" answer = generate_answer(question, retrieved)\n",
|
||||
" return {\n",
|
||||
" \"question\": question,\n",
|
||||
" \"retrieved\": retrieved,\n",
|
||||
" \"answer\": answer,\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"result = ask_knowledge_base(\"LangGraph 适合做什么?\", all_chunks)\n",
|
||||
"print(result[\"answer\"])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码把前面的步骤组合成一个完整功能:\n",
|
||||
"\n",
|
||||
"1. `ask_knowledge_base` 是问答助手的主入口。用户只需要传入问题。\n",
|
||||
"2. `retrieve(question, chunks, top_k=top_k)` 先从知识库片段中找相关内容。\n",
|
||||
"3. `generate_answer(question, retrieved)` 再根据检索结果生成答案。\n",
|
||||
"4. 函数返回一个字典,里面包含原问题、检索结果和最终答案。\n",
|
||||
"5. 返回检索结果的好处是:后续可以展示“参考来源”,让用户知道答案从哪里来。\n",
|
||||
"\n",
|
||||
"到这里,我们已经完成了一个最小版本的个人知识库问答助手。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 用 LangChain 思路改造项目结构\n",
|
||||
"\n",
|
||||
"虽然上面的代码是纯 Python,但它已经对应了 LangChain RAG 的主要模块。\n",
|
||||
"\n",
|
||||
"| 当前示例 | LangChain 中常见模块 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| `documents` | DocumentLoader 加载出的文档 |\n",
|
||||
"| `split_text` | TextSplitter |\n",
|
||||
"| `retrieve` | Retriever |\n",
|
||||
"| `generate_answer` | LLMChain 或 LCEL 链 |\n",
|
||||
"| `ask_knowledge_base` | 完整 RAG Chain |\n",
|
||||
"\n",
|
||||
"所以学习项目时不要只记 API,更重要的是理解每个模块承担什么职责。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. LangChain 版伪代码\n",
|
||||
"\n",
|
||||
"下面给出一个接近真实项目的 LangChain 写法。由于不同环境中的模型和 API Key 不一定相同,这段代码作为结构参考,不要求直接运行。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 下面是 LangChain RAG 的典型结构示例,作为项目结构参考\n",
|
||||
"\n",
|
||||
"# from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
|
||||
"# from langchain_community.vectorstores import FAISS\n",
|
||||
"# from langchain_openai import OpenAIEmbeddings, ChatOpenAI\n",
|
||||
"# from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"\n",
|
||||
"# text_splitter = RecursiveCharacterTextSplitter(\n",
|
||||
"# chunk_size=500,\n",
|
||||
"# chunk_overlap=50,\n",
|
||||
"# )\n",
|
||||
"# docs = text_splitter.create_documents(documents)\n",
|
||||
"\n",
|
||||
"# embeddings = OpenAIEmbeddings(model=\"qwen3-embedding\")\n",
|
||||
"# vectorstore = FAISS.from_documents(docs, embeddings)\n",
|
||||
"# retriever = vectorstore.as_retriever(search_kwargs={\"k\": 3})\n",
|
||||
"\n",
|
||||
"# prompt = ChatPromptTemplate.from_template(\"\"\"\n",
|
||||
"# 请根据下面的资料回答用户问题。\n",
|
||||
"# 如果资料中没有答案,请说明不知道,不要编造。\n",
|
||||
"\n",
|
||||
"# 资料:\n",
|
||||
"# {context}\n",
|
||||
"\n",
|
||||
"# 问题:\n",
|
||||
"# {question}\n",
|
||||
"# \"\"\")\n",
|
||||
"\n",
|
||||
"# llm = ChatOpenAI(model=\"qwen3.6-35b-A3b\")\n",
|
||||
"# chain = prompt | llm\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码展示了真实 LangChain 项目的常见结构:\n",
|
||||
"\n",
|
||||
"1. `RecursiveCharacterTextSplitter` 用于文本切分,`chunk_size` 控制每块大小,`chunk_overlap` 控制相邻片段之间的重叠内容。\n",
|
||||
"2. `FAISS.from_documents(docs, embeddings)` 会把文档转换成向量并存入 FAISS 向量库。\n",
|
||||
"3. `vectorstore.as_retriever(search_kwargs={\"k\": 3})` 会创建检索器,每次返回最相关的 3 个片段。\n",
|
||||
"4. `ChatPromptTemplate.from_template(...)` 定义提示词模板,要求模型必须根据资料回答。\n",
|
||||
"5. `prompt | llm` 是 LangChain 表达式语言 LCEL 的写法,表示把提示词输出交给大模型。\n",
|
||||
"\n",
|
||||
"这段代码被注释掉,是因为它依赖真实模型、Embedding 服务和相关包。课程中先理解结构,后续再接入真实模型即可。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12. 用 LangGraph 设计项目工作流\n",
|
||||
"\n",
|
||||
"如果项目流程变复杂,例如要增加问题改写、检索质量检查、答案审核,就可以使用 LangGraph 编排流程。\n",
|
||||
"\n",
|
||||
"一个基础工作流可以设计为:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"用户问题 -> 检索节点 -> 回答生成节点 -> 输出结果\n",
|
||||
"```\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import List, Tuple, TypedDict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class QAState(TypedDict):\n",
|
||||
" question: str\n",
|
||||
" chunks: List[str]\n",
|
||||
" retrieved: List[Tuple[int, str]]\n",
|
||||
" answer: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def retrieve_node(state: QAState):\n",
|
||||
" retrieved = retrieve(state[\"question\"], state[\"chunks\"], top_k=3)\n",
|
||||
" return {\"retrieved\": retrieved}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def answer_node(state: QAState):\n",
|
||||
" answer = generate_answer(state[\"question\"], state[\"retrieved\"])\n",
|
||||
" return {\"answer\": answer}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码先定义 LangGraph 工作流需要的状态和节点:\n",
|
||||
"\n",
|
||||
"1. `QAState` 表示问答流程中的共享数据。\n",
|
||||
"2. `question` 保存用户问题。\n",
|
||||
"3. `chunks` 保存知识库切分后的文本片段。\n",
|
||||
"4. `retrieved` 保存检索结果,每个结果包含分数和文本片段。\n",
|
||||
"5. `answer` 保存最终答案。\n",
|
||||
"6. `retrieve_node` 是检索节点,负责调用前面写好的 `retrieve` 函数。\n",
|
||||
"7. `answer_node` 是回答节点,负责调用 `generate_answer` 生成答案。\n",
|
||||
"\n",
|
||||
"可以看到,LangGraph 节点并不神秘,本质上就是接收 State、处理数据、返回更新字段的普通函数。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 13. 构建 LangGraph 工作流\n",
|
||||
"\n",
|
||||
"下面把检索节点和回答节点连接起来。\n",
|
||||
"\n",
|
||||
"如果当前环境没有安装 LangGraph,可以先运行:`%pip install langgraph`。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 如果没有安装 LangGraph,取消下一行注释后运行\n",
|
||||
"# %pip install langgraph\n",
|
||||
"\n",
|
||||
"from langgraph.graph import END, START, StateGraph\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"qa_workflow = StateGraph(QAState)\n",
|
||||
"\n",
|
||||
"qa_workflow.add_node(\"retrieve\", retrieve_node)\n",
|
||||
"qa_workflow.add_node(\"answer\", answer_node)\n",
|
||||
"\n",
|
||||
"qa_workflow.add_edge(START, \"retrieve\")\n",
|
||||
"qa_workflow.add_edge(\"retrieve\", \"answer\")\n",
|
||||
"qa_workflow.add_edge(\"answer\", END)\n",
|
||||
"\n",
|
||||
"qa_app = qa_workflow.compile()\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码把问答流程编排成 LangGraph 工作流:\n",
|
||||
"\n",
|
||||
"1. `StateGraph(QAState)` 创建一张基于 `QAState` 的流程图。\n",
|
||||
"2. `add_node(\"retrieve\", retrieve_node)` 添加检索节点。\n",
|
||||
"3. `add_node(\"answer\", answer_node)` 添加回答生成节点。\n",
|
||||
"4. `add_edge(START, \"retrieve\")` 表示从检索节点开始。\n",
|
||||
"5. `add_edge(\"retrieve\", \"answer\")` 表示检索完成后进入回答节点。\n",
|
||||
"6. `add_edge(\"answer\", END)` 表示回答生成后流程结束。\n",
|
||||
"7. `compile()` 把流程图编译成可以调用的 `qa_app`。\n",
|
||||
"\n",
|
||||
"这就是一个最小 LangGraph 版个人知识库问答流程。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 14. 运行 LangGraph 问答助手\n",
|
||||
"\n",
|
||||
"现在传入问题和知识库片段,运行整个工作流。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"initial_state = {\n",
|
||||
" \"question\": \"个人知识库问答助手有什么作用?\",\n",
|
||||
" \"chunks\": all_chunks,\n",
|
||||
" \"retrieved\": [],\n",
|
||||
" \"answer\": \"\",\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"final_state = qa_app.invoke(initial_state)\n",
|
||||
"print(final_state[\"answer\"])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码运行了 LangGraph 工作流:\n",
|
||||
"\n",
|
||||
"1. `initial_state` 是初始状态,包含用户问题、知识库片段、空的检索结果和空答案。\n",
|
||||
"2. `qa_app.invoke(initial_state)` 启动工作流。\n",
|
||||
"3. 数据会先进入 `retrieve_node`,生成 `retrieved`。\n",
|
||||
"4. 然后进入 `answer_node`,生成 `answer`。\n",
|
||||
"5. 最终返回 `final_state`,我们打印其中的 `answer`。\n",
|
||||
"\n",
|
||||
"这个例子说明:LangGraph 适合把多个处理步骤组织成清晰、可维护的项目流程。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 15. 项目可以如何继续升级\n",
|
||||
"\n",
|
||||
"当前版本只是教学版 MVP,后续可以从这些方向升级:\n",
|
||||
"\n",
|
||||
"1. 支持读取 PDF、Markdown、Word 等真实文件。\n",
|
||||
"2. 使用真实 Embedding 模型,把关键词检索升级为向量检索。\n",
|
||||
"3. 使用 Chroma 或 FAISS 保存向量索引。\n",
|
||||
"4. 接入真实大模型,生成更自然的回答。\n",
|
||||
"5. 增加“无法回答”判断,避免资料不足时强行回答。\n",
|
||||
"6. 增加来源展示,让用户看到答案引用了哪些资料。\n",
|
||||
"7. 使用 LangGraph 增加问题改写、答案审核、多轮追问等节点。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 16. 小结\n",
|
||||
"\n",
|
||||
"本节课完成了个人知识库问答助手的第一版项目设计和实现。\n",
|
||||
"\n",
|
||||
"你需要重点掌握:\n",
|
||||
"\n",
|
||||
"1. 项目开发要先做需求分析,再写代码。\n",
|
||||
"2. RAG 的核心流程是:准备资料、切分资料、检索资料、生成答案。\n",
|
||||
"3. LangChain 更适合提供文档加载、切分、检索、模型调用等组件。\n",
|
||||
"4. LangGraph 更适合把多个步骤编排成清晰工作流。\n",
|
||||
"5. 初学项目时,可以先用纯 Python 理解原理,再逐步替换成成熟框架。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 17. 练习题\n",
|
||||
"\n",
|
||||
"1. 往 `documents` 中新增 3 条自己的学习笔记,然后重新运行问答流程。\n",
|
||||
"2. 修改 `top_k`,观察返回 1 条、3 条、5 条资料时答案有什么变化。\n",
|
||||
"3. 修改 `simple_score`,让它按词语而不是按字符计算相关度。\n",
|
||||
"4. 在 `generate_answer` 中增加“参考资料”部分,把检索到的片段编号展示出来。\n",
|
||||
"5. 给 LangGraph 工作流新增一个 `check_node`,判断是否检索到了相关资料。\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
772
25_项目实战二.ipynb
Normal file
772
25_项目实战二.ipynb
Normal file
@@ -0,0 +1,772 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 25_项目实战二:多工具协作型智能体\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 掌握多工具协作型智能体的项目开发方法\n",
|
||||
"2. 实现结合搜索引擎、计算工具和文件读取的复合型智能体\n",
|
||||
"3. 学会调试和优化智能体的执行流程\n",
|
||||
"\n",
|
||||
"本节课会完成一个小型项目:构建一个可以根据用户问题自动选择工具的智能体。它可以模拟搜索资料、执行计算、读取文件,并把结果整理成最终回答。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 什么是多工具协作型智能体\n",
|
||||
"\n",
|
||||
"普通问答智能体主要依靠大模型自身回答问题,但大模型并不适合完成所有事情。\n",
|
||||
"\n",
|
||||
"例如:\n",
|
||||
"\n",
|
||||
"- 想知道最新信息,需要搜索工具\n",
|
||||
"- 想做精确数学计算,需要计算工具\n",
|
||||
"- 想分析本地资料,需要文件读取工具\n",
|
||||
"- 想保存结果,需要写文件工具\n",
|
||||
"\n",
|
||||
"多工具协作型智能体就像一个会安排工作的助理:它先理解用户问题,再判断该用哪个工具,最后把工具结果整理成答案。\n",
|
||||
"\n",
|
||||
"通俗地说,它的核心流程是:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"用户问题 -> 判断需要什么工具 -> 调用工具 -> 整理工具结果 -> 返回答案\n",
|
||||
"```\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. 项目需求分析\n",
|
||||
"\n",
|
||||
"本项目要实现一个教学版多工具智能体。为了让代码容易运行,我们先不用真实搜索引擎和真实文件系统,而是用 Python 函数模拟工具。\n",
|
||||
"\n",
|
||||
"项目需求如下:\n",
|
||||
"\n",
|
||||
"| 需求 | 说明 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| 搜索工具 | 根据关键词返回模拟搜索结果 |\n",
|
||||
"| 计算工具 | 执行简单数学表达式 |\n",
|
||||
"| 文件读取工具 | 从模拟文件中读取内容 |\n",
|
||||
"| 工具选择 | 根据用户问题判断应该调用哪个工具 |\n",
|
||||
"| 结果汇总 | 把工具返回结果整理为自然语言答案 |\n",
|
||||
"\n",
|
||||
"第一版项目重点不是做得复杂,而是把“工具定义、工具选择、工具调用、结果汇总”的完整流程跑通。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 项目模块划分\n",
|
||||
"\n",
|
||||
"我们把项目拆成 5 个模块:\n",
|
||||
"\n",
|
||||
"1. 定义工具:准备搜索、计算、文件读取函数\n",
|
||||
"2. 注册工具:把工具放入统一的工具表\n",
|
||||
"3. 选择工具:根据用户问题决定用哪个工具\n",
|
||||
"4. 执行工具:调用对应函数并拿到结果\n",
|
||||
"5. 汇总答案:把工具执行结果整理给用户\n",
|
||||
"\n",
|
||||
"这种拆法的好处是每个模块只负责一件事,后续调试和扩展都更方便。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 第一步:定义搜索工具\n",
|
||||
"\n",
|
||||
"真实项目中,搜索工具可能会调用搜索引擎 API。这里为了教学简单,用一个字典模拟搜索数据库。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"search_database = {\n",
|
||||
" \"LangChain\": \"LangChain 是用于开发大模型应用的框架,常用于提示词、链、工具和 RAG 应用开发。\",\n",
|
||||
" \"LangGraph\": \"LangGraph 是用于构建有状态、多步骤、可分支智能体流程的框架。\",\n",
|
||||
" \"RAG\": \"RAG 是检索增强生成技术,先检索资料,再基于资料生成答案。\",\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def search_tool(query):\n",
|
||||
" for keyword, result in search_database.items():\n",
|
||||
" if keyword.lower() in query.lower():\n",
|
||||
" return result\n",
|
||||
" return \"没有找到相关搜索结果。\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(search_tool(\"请介绍 LangGraph\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码定义了一个模拟搜索工具:\n",
|
||||
"\n",
|
||||
"1. `search_database` 是一个字典,用来模拟搜索引擎中的资料。\n",
|
||||
"2. 字典的 key 是关键词,例如 `LangChain`、`LangGraph`、`RAG`。\n",
|
||||
"3. 字典的 value 是搜索结果,也就是工具返回给智能体的信息。\n",
|
||||
"4. `search_tool(query)` 接收用户查询内容。\n",
|
||||
"5. `keyword.lower() in query.lower()` 用来做不区分大小写的关键词匹配。\n",
|
||||
"6. 如果命中关键词,就返回对应搜索结果;如果没有命中,就返回“没有找到相关搜索结果”。\n",
|
||||
"\n",
|
||||
"这个工具虽然简单,但已经体现了搜索工具的基本思想:输入查询,返回相关资料。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 第二步:定义计算工具\n",
|
||||
"\n",
|
||||
"大模型有时会算错数,所以精确计算应该交给计算工具。\n",
|
||||
"\n",
|
||||
"下面实现一个简单计算器,只允许安全的数学字符,避免执行危险代码。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def calculator_tool(expression):\n",
|
||||
" allowed_chars = set(\"0123456789+-*/(). \" )\n",
|
||||
" if not set(expression) <= allowed_chars:\n",
|
||||
" return \"表达式中包含不支持的字符。\"\n",
|
||||
"\n",
|
||||
" try:\n",
|
||||
" result = eval(expression)\n",
|
||||
" return f\"计算结果是:{result}\"\n",
|
||||
" except Exception as error:\n",
|
||||
" return f\"计算失败:{error}\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(calculator_tool(\"128 * 36 + 50\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码定义了一个计算工具:\n",
|
||||
"\n",
|
||||
"1. `calculator_tool(expression)` 接收一个数学表达式字符串。\n",
|
||||
"2. `allowed_chars` 定义允许出现的字符,包括数字、加减乘除、小括号、小数点和空格。\n",
|
||||
"3. `set(expression) <= allowed_chars` 用来检查表达式中的字符是否都在允许范围内。\n",
|
||||
"4. `eval(expression)` 会执行数学表达式并得到结果。\n",
|
||||
"5. `try...except` 用来捕获计算错误,例如表达式格式不正确。\n",
|
||||
"\n",
|
||||
"注意:真实项目中要谨慎使用 `eval`。这里已经做了简单字符限制,但生产环境通常会使用更安全的数学表达式解析库。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 第三步:定义文件读取工具\n",
|
||||
"\n",
|
||||
"文件读取工具适合回答和本地资料相关的问题。这里用字典模拟文件内容。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"fake_files = {\n",
|
||||
" \"project_plan.txt\": \"项目计划:第一周完成需求分析,第二周完成原型开发,第三周完成测试和优化。\",\n",
|
||||
" \"meeting_notes.txt\": \"会议纪要:团队决定优先开发知识库问答功能,然后再增加多工具调用能力。\",\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def file_reader_tool(file_name):\n",
|
||||
" if file_name in fake_files:\n",
|
||||
" return fake_files[file_name]\n",
|
||||
" return \"没有找到这个文件。\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(file_reader_tool(\"project_plan.txt\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码定义了一个模拟文件读取工具:\n",
|
||||
"\n",
|
||||
"1. `fake_files` 是模拟文件系统,key 是文件名,value 是文件内容。\n",
|
||||
"2. `file_reader_tool(file_name)` 接收文件名。\n",
|
||||
"3. 如果文件名存在于 `fake_files` 中,就返回对应文件内容。\n",
|
||||
"4. 如果文件名不存在,就返回提示信息。\n",
|
||||
"\n",
|
||||
"真实项目中,这个工具可以改成读取本地文件、数据库记录、对象存储文件或企业文档系统。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 第四步:注册工具\n",
|
||||
"\n",
|
||||
"如果工具越来越多,不能每次都手动写大量 if 语句。更好的方式是把工具统一注册到一个工具表中。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"tools = {\n",
|
||||
" \"search\": {\n",
|
||||
" \"name\": \"搜索工具\",\n",
|
||||
" \"description\": \"用于查询 LangChain、LangGraph、RAG 等知识。\",\n",
|
||||
" \"function\": search_tool,\n",
|
||||
" },\n",
|
||||
" \"calculator\": {\n",
|
||||
" \"name\": \"计算工具\",\n",
|
||||
" \"description\": \"用于执行数学表达式计算。\",\n",
|
||||
" \"function\": calculator_tool,\n",
|
||||
" },\n",
|
||||
" \"file_reader\": {\n",
|
||||
" \"name\": \"文件读取工具\",\n",
|
||||
" \"description\": \"用于读取模拟文件内容。\",\n",
|
||||
" \"function\": file_reader_tool,\n",
|
||||
" },\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"print(tools.keys())\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码把所有工具放入统一的 `tools` 字典:\n",
|
||||
"\n",
|
||||
"1. `search`、`calculator`、`file_reader` 是工具编号,后续智能体会用编号选择工具。\n",
|
||||
"2. `name` 是工具的中文名称,方便展示给用户或调试时阅读。\n",
|
||||
"3. `description` 是工具说明,真实智能体可以根据说明判断工具用途。\n",
|
||||
"4. `function` 保存真正要调用的 Python 函数。\n",
|
||||
"5. `tools.keys()` 可以查看当前注册了哪些工具。\n",
|
||||
"\n",
|
||||
"工具注册表的好处是:新增工具时,只需要往表里加一项,不需要大幅修改主流程。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 第五步:实现工具选择器\n",
|
||||
"\n",
|
||||
"工具选择器负责判断用户问题应该交给哪个工具。\n",
|
||||
"\n",
|
||||
"真实项目中,工具选择可以由大模型完成。这里为了便于理解,先用关键词规则实现。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def select_tool(user_input):\n",
|
||||
" if any(word in user_input for word in [\"计算\", \"加\", \"减\", \"乘\", \"除\", \"+\", \"-\", \"*\", \"/\"]):\n",
|
||||
" return \"calculator\"\n",
|
||||
"\n",
|
||||
" if any(word in user_input for word in [\"文件\", \"计划\", \"会议\", \"project_plan\", \"meeting_notes\"]):\n",
|
||||
" return \"file_reader\"\n",
|
||||
"\n",
|
||||
" return \"search\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(select_tool(\"帮我计算 12 * 8\"))\n",
|
||||
"print(select_tool(\"读取项目计划文件\"))\n",
|
||||
"print(select_tool(\"LangChain 是什么\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码实现了一个规则版工具选择器:\n",
|
||||
"\n",
|
||||
"1. `select_tool(user_input)` 接收用户输入。\n",
|
||||
"2. 如果问题中包含计算相关词语或数学符号,就选择 `calculator`。\n",
|
||||
"3. 如果问题中包含文件、计划、会议等词语,就选择 `file_reader`。\n",
|
||||
"4. 如果前面规则都没有命中,默认选择 `search`。\n",
|
||||
"\n",
|
||||
"这种规则方法简单直观,适合教学和小项目。但当问题复杂时,可以让大模型根据工具描述自动选择工具。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 第六步:为不同工具提取参数\n",
|
||||
"\n",
|
||||
"选择工具之后,还需要给工具传入合适的参数。\n",
|
||||
"\n",
|
||||
"例如计算工具需要数学表达式,文件读取工具需要文件名,搜索工具需要搜索关键词。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import re\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def extract_tool_input(user_input, tool_name):\n",
|
||||
" if tool_name == \"calculator\":\n",
|
||||
" match = re.search(r\"[0-9+\\-*/(). ]+\", user_input)\n",
|
||||
" return match.group().strip() if match else user_input\n",
|
||||
"\n",
|
||||
" if tool_name == \"file_reader\":\n",
|
||||
" if \"meeting\" in user_input or \"会议\" in user_input:\n",
|
||||
" return \"meeting_notes.txt\"\n",
|
||||
" return \"project_plan.txt\"\n",
|
||||
"\n",
|
||||
" return user_input\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(extract_tool_input(\"帮我计算 128 * 36 + 50\", \"calculator\"))\n",
|
||||
"print(extract_tool_input(\"读取会议纪要\", \"file_reader\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码负责从用户输入中提取工具参数:\n",
|
||||
"\n",
|
||||
"1. `import re` 引入正则表达式模块。\n",
|
||||
"2. `extract_tool_input(user_input, tool_name)` 根据工具类型提取不同参数。\n",
|
||||
"3. 对计算工具,`re.search(r\"[0-9+\\-*/(). ]+\", user_input)` 会从文本中找出数学表达式。\n",
|
||||
"4. `match.group().strip()` 取出匹配结果并去掉前后空格。\n",
|
||||
"5. 对文件读取工具,如果用户提到会议,就读取 `meeting_notes.txt`;否则默认读取 `project_plan.txt`。\n",
|
||||
"6. 对搜索工具,直接把用户输入作为查询内容。\n",
|
||||
"\n",
|
||||
"参数提取是工具调用中很重要的一步。工具选对了,但参数给错了,结果也会不准确。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 第七步:实现智能体主流程\n",
|
||||
"\n",
|
||||
"现在把工具选择、参数提取、工具调用和答案汇总连接起来,形成完整智能体。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def multi_tool_agent(user_input):\n",
|
||||
" tool_name = select_tool(user_input)\n",
|
||||
" tool_info = tools[tool_name]\n",
|
||||
" tool_input = extract_tool_input(user_input, tool_name)\n",
|
||||
" tool_result = tool_info[\"function\"](tool_input)\n",
|
||||
"\n",
|
||||
" final_answer = f\"用户问题:{user_input}\\n\"\n",
|
||||
" final_answer += f\"选择工具:{tool_info['name']}\\n\"\n",
|
||||
" final_answer += f\"工具输入:{tool_input}\\n\"\n",
|
||||
" final_answer += f\"工具结果:{tool_result}\"\n",
|
||||
" return final_answer\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(multi_tool_agent(\"帮我计算 128 * 36 + 50\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码实现了智能体主流程:\n",
|
||||
"\n",
|
||||
"1. `multi_tool_agent(user_input)` 是智能体入口函数。\n",
|
||||
"2. `select_tool(user_input)` 判断应该使用哪个工具。\n",
|
||||
"3. `tools[tool_name]` 从工具注册表中取出工具信息。\n",
|
||||
"4. `extract_tool_input(user_input, tool_name)` 为工具准备输入参数。\n",
|
||||
"5. `tool_info[\"function\"](tool_input)` 调用真正的工具函数。\n",
|
||||
"6. 最后把用户问题、选择的工具、工具输入和工具结果拼接成最终答案。\n",
|
||||
"\n",
|
||||
"这就是多工具协作型智能体的最小可用版本。它的重点不是某个工具有多复杂,而是能自动完成“选工具、调工具、汇总结果”。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. 测试多个问题\n",
|
||||
"\n",
|
||||
"下面用不同类型的问题测试智能体,观察它是否能选择正确工具。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"test_questions = [\n",
|
||||
" \"LangChain 是什么?\",\n",
|
||||
" \"帮我计算 25 * 4 + 18\",\n",
|
||||
" \"读取项目计划文件\",\n",
|
||||
" \"会议纪要里说了什么?\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for question in test_questions:\n",
|
||||
" print(\"=\" * 40)\n",
|
||||
" print(multi_tool_agent(question))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码用于批量测试:\n",
|
||||
"\n",
|
||||
"1. `test_questions` 保存多个测试问题。\n",
|
||||
"2. 第一个问题适合搜索工具。\n",
|
||||
"3. 第二个问题适合计算工具。\n",
|
||||
"4. 第三个和第四个问题适合文件读取工具。\n",
|
||||
"5. `for question in test_questions` 逐个运行智能体。\n",
|
||||
"6. `print(\"=\" * 40)` 用分隔线区分不同测试结果。\n",
|
||||
"\n",
|
||||
"测试时要重点观察两点:工具是否选对,工具输入是否提取正确。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12. 加入调试日志\n",
|
||||
"\n",
|
||||
"智能体项目经常需要调试,因为错误可能发生在工具选择、参数提取或工具执行阶段。\n",
|
||||
"\n",
|
||||
"下面给主流程增加调试日志,方便定位问题。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def debug_multi_tool_agent(user_input, verbose=True):\n",
|
||||
" if verbose:\n",
|
||||
" print(f\"[调试] 用户输入:{user_input}\")\n",
|
||||
"\n",
|
||||
" tool_name = select_tool(user_input)\n",
|
||||
" if verbose:\n",
|
||||
" print(f\"[调试] 选择工具:{tool_name}\")\n",
|
||||
"\n",
|
||||
" tool_input = extract_tool_input(user_input, tool_name)\n",
|
||||
" if verbose:\n",
|
||||
" print(f\"[调试] 工具输入:{tool_input}\")\n",
|
||||
"\n",
|
||||
" tool_result = tools[tool_name][\"function\"](tool_input)\n",
|
||||
" if verbose:\n",
|
||||
" print(f\"[调试] 工具结果:{tool_result}\")\n",
|
||||
"\n",
|
||||
" return f\"最终回答:{tool_result}\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(debug_multi_tool_agent(\"帮我计算 10 + 20 * 3\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码增加了调试能力:\n",
|
||||
"\n",
|
||||
"1. `verbose=True` 表示默认打印调试信息。\n",
|
||||
"2. 第一条日志显示用户原始输入。\n",
|
||||
"3. 第二条日志显示智能体选择了哪个工具。\n",
|
||||
"4. 第三条日志显示传给工具的输入参数。\n",
|
||||
"5. 第四条日志显示工具返回结果。\n",
|
||||
"6. 如果不想看日志,可以调用 `debug_multi_tool_agent(question, verbose=False)`。\n",
|
||||
"\n",
|
||||
"调试日志能帮助我们判断问题到底出在哪里:是工具没选对,还是参数没提对,还是工具本身执行失败。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 13. 用 LangGraph 表达多工具流程\n",
|
||||
"\n",
|
||||
"当前流程是普通 Python 函数。实际项目中,如果流程变复杂,可以用 LangGraph 把步骤拆成节点。\n",
|
||||
"\n",
|
||||
"工作流可以设计为:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"START -> 选择工具 -> 执行工具 -> 汇总答案 -> END\n",
|
||||
"```\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import TypedDict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class AgentState(TypedDict):\n",
|
||||
" user_input: str\n",
|
||||
" tool_name: str\n",
|
||||
" tool_input: str\n",
|
||||
" tool_result: str\n",
|
||||
" final_answer: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def choose_tool_node(state: AgentState):\n",
|
||||
" tool_name = select_tool(state[\"user_input\"])\n",
|
||||
" tool_input = extract_tool_input(state[\"user_input\"], tool_name)\n",
|
||||
" return {\"tool_name\": tool_name, \"tool_input\": tool_input}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def execute_tool_node(state: AgentState):\n",
|
||||
" tool_result = tools[state[\"tool_name\"]][\"function\"](state[\"tool_input\"])\n",
|
||||
" return {\"tool_result\": tool_result}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def summarize_node(state: AgentState):\n",
|
||||
" tool_label = tools[state[\"tool_name\"]][\"name\"]\n",
|
||||
" final_answer = f\"我使用了{tool_label},得到结果:{state['tool_result']}\"\n",
|
||||
" return {\"final_answer\": final_answer}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码定义了 LangGraph 版本需要的 State 和节点:\n",
|
||||
"\n",
|
||||
"1. `AgentState` 表示工作流中流动的数据。\n",
|
||||
"2. `user_input` 保存用户问题。\n",
|
||||
"3. `tool_name` 保存选择出的工具编号。\n",
|
||||
"4. `tool_input` 保存传给工具的参数。\n",
|
||||
"5. `tool_result` 保存工具执行结果。\n",
|
||||
"6. `final_answer` 保存最终回答。\n",
|
||||
"7. `choose_tool_node` 负责工具选择和参数提取。\n",
|
||||
"8. `execute_tool_node` 负责调用工具。\n",
|
||||
"9. `summarize_node` 负责把工具结果整理成用户能看懂的回答。\n",
|
||||
"\n",
|
||||
"用 LangGraph 后,每个步骤变成独立节点,流程更清晰,也更容易扩展。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 14. 构建 LangGraph 工作流\n",
|
||||
"\n",
|
||||
"如果当前环境没有安装 LangGraph,可以先运行:`%pip install langgraph`。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 如果没有安装 LangGraph,取消下一行注释后运行\n",
|
||||
"# %pip install langgraph\n",
|
||||
"\n",
|
||||
"from langgraph.graph import END, START, StateGraph\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"workflow = StateGraph(AgentState)\n",
|
||||
"\n",
|
||||
"workflow.add_node(\"choose_tool\", choose_tool_node)\n",
|
||||
"workflow.add_node(\"execute_tool\", execute_tool_node)\n",
|
||||
"workflow.add_node(\"summarize\", summarize_node)\n",
|
||||
"\n",
|
||||
"workflow.add_edge(START, \"choose_tool\")\n",
|
||||
"workflow.add_edge(\"choose_tool\", \"execute_tool\")\n",
|
||||
"workflow.add_edge(\"execute_tool\", \"summarize\")\n",
|
||||
"workflow.add_edge(\"summarize\", END)\n",
|
||||
"\n",
|
||||
"agent_app = workflow.compile()\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码把节点连接成完整工作流:\n",
|
||||
"\n",
|
||||
"1. `StateGraph(AgentState)` 创建一张流程图,并指定状态结构。\n",
|
||||
"2. `add_node` 注册节点函数。\n",
|
||||
"3. `START -> choose_tool` 表示从工具选择节点开始。\n",
|
||||
"4. `choose_tool -> execute_tool` 表示选好工具后执行工具。\n",
|
||||
"5. `execute_tool -> summarize` 表示拿到工具结果后进行汇总。\n",
|
||||
"6. `summarize -> END` 表示汇总完成后结束。\n",
|
||||
"7. `compile()` 把流程图编译成可运行对象。\n",
|
||||
"\n",
|
||||
"这和前面的普通 Python 主流程做的是同一件事,只是 LangGraph 更适合管理复杂流程。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 15. 运行 LangGraph 版智能体\n",
|
||||
"\n",
|
||||
"现在用一个问题测试 LangGraph 版多工具智能体。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"initial_state = {\n",
|
||||
" \"user_input\": \"会议纪要里说了什么?\",\n",
|
||||
" \"tool_name\": \"\",\n",
|
||||
" \"tool_input\": \"\",\n",
|
||||
" \"tool_result\": \"\",\n",
|
||||
" \"final_answer\": \"\",\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"final_state = agent_app.invoke(initial_state)\n",
|
||||
"print(final_state[\"final_answer\"])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码运行 LangGraph 工作流:\n",
|
||||
"\n",
|
||||
"1. `initial_state` 是初始状态,只有 `user_input` 有真实内容,其他字段先留空。\n",
|
||||
"2. `agent_app.invoke(initial_state)` 启动工作流。\n",
|
||||
"3. 工作流会依次执行工具选择、工具调用和结果汇总。\n",
|
||||
"4. `final_state` 是运行结束后的完整状态。\n",
|
||||
"5. `final_state[\"final_answer\"]` 取出最终答案。\n",
|
||||
"\n",
|
||||
"这个例子说明:当一个智能体需要多个步骤协作时,LangGraph 可以让流程更有结构。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 16. 调试和优化建议\n",
|
||||
"\n",
|
||||
"多工具智能体常见问题包括:\n",
|
||||
"\n",
|
||||
"| 问题 | 可能原因 | 优化方法 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 工具选错 | 工具描述不清或规则太简单 | 优化工具描述,增加分类规则或让模型判断 |\n",
|
||||
"| 参数提错 | 用户表达复杂 | 增加参数抽取逻辑或使用结构化输出 |\n",
|
||||
"| 工具报错 | 输入格式不符合要求 | 在调用前做参数校验 |\n",
|
||||
"| 答案不清晰 | 只返回原始工具结果 | 增加结果解释和格式化输出 |\n",
|
||||
"| 流程难排查 | 缺少中间状态 | 增加调试日志或保存执行轨迹 |\n",
|
||||
"\n",
|
||||
"开发智能体时,不要只看最终答案,也要观察每一步的中间状态。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 17. 小结\n",
|
||||
"\n",
|
||||
"本节课完成了一个多工具协作型智能体项目。\n",
|
||||
"\n",
|
||||
"你需要重点掌握:\n",
|
||||
"\n",
|
||||
"1. 多工具智能体的核心是根据任务选择合适工具。\n",
|
||||
"2. 工具通常包含名称、描述和函数。\n",
|
||||
"3. 工具调用前要先选择工具,再提取参数。\n",
|
||||
"4. 调试时要观察工具选择、工具输入和工具结果。\n",
|
||||
"5. LangGraph 可以把复杂工具调用流程拆成清晰节点。\n",
|
||||
"\n",
|
||||
"掌握这个项目后,就可以继续接入真实搜索 API、真实文件读取、大模型工具调用和更复杂的工作流。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 18. 练习题\n",
|
||||
"\n",
|
||||
"1. 新增一个 `weather_tool`,模拟查询天气。\n",
|
||||
"2. 修改 `select_tool`,让它可以根据“天气”关键词选择天气工具。\n",
|
||||
"3. 修改 `calculator_tool`,支持平方和取余计算。\n",
|
||||
"4. 给 `multi_tool_agent` 增加错误处理,当工具不存在时返回友好提示。\n",
|
||||
"5. 在 LangGraph 工作流中新增一个 `debug_node`,专门输出当前 State。\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
1039
26_项目实战三.ipynb
Normal file
1039
26_项目实战三.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
687
27_部署与评估.ipynb
Normal file
687
27_部署与评估.ipynb
Normal file
@@ -0,0 +1,687 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 27 部署与评估\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 了解智能体应用的部署方式,例如本地函数、本地 API、Docker 和云平台\n",
|
||||
"2. 掌握智能体效果评估的常见指标和方法\n",
|
||||
"3. 学会用测试集评估智能体的稳定性和准确性\n",
|
||||
"4. 理解日志、追踪和 LangSmith 在智能体项目中的作用\n",
|
||||
"5. 能够为一个智能体项目设计简单的上线检查流程\n",
|
||||
"\n",
|
||||
"前面我们已经完成了一个带交互界面的智能体项目。本节课要解决的问题是:**项目写完以后,怎么交付给别人使用?怎么判断它运行得好不好?**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 为什么要学习部署与评估\n",
|
||||
"\n",
|
||||
"很多初学者做智能体项目时,会停留在 notebook 能运行的阶段。\n",
|
||||
"\n",
|
||||
"但真实项目还需要回答三个问题:\n",
|
||||
"\n",
|
||||
"1. **别人怎么使用它?**\n",
|
||||
"2. **它回答得好不好?**\n",
|
||||
"3. **出问题时怎么排查?**\n",
|
||||
"\n",
|
||||
"这三个问题分别对应:\n",
|
||||
"\n",
|
||||
"| 问题 | 对应能力 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| 怎么使用 | 部署 |\n",
|
||||
"| 好不好用 | 评估 |\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",
|
||||
"| 本地 API | 前后端分离、系统集成 | 其他程序可以通过 HTTP 调用 |\n",
|
||||
"| Docker | 统一环境、方便迁移 | 避免“我电脑能跑,你电脑不能跑” |\n",
|
||||
"| 云平台 | 正式上线、多用户访问 | 稳定性更高,但配置更复杂 |\n",
|
||||
"\n",
|
||||
"初学时不要一上来就追求复杂部署。\n",
|
||||
"\n",
|
||||
"推荐顺序是:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"函数调用 -> 命令行 -> 本地 API -> Docker -> 云平台\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"每一步都是在上一层基础上增加一点工程化能力。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 准备一个可部署的智能体核心函数\n",
|
||||
"\n",
|
||||
"部署前最重要的一件事是:**把核心逻辑封装成一个清晰的函数。**\n",
|
||||
"\n",
|
||||
"不管后面是接命令行、Web API,还是云服务,最终都应该调用同一个核心函数。\n",
|
||||
"\n",
|
||||
"下面我们用一个简化版学习助手作为示例。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def run_learning_assistant(user_input):\n",
|
||||
" if '计划' in user_input or '安排' in user_input:\n",
|
||||
" intent = 'plan'\n",
|
||||
" response = '建议先学习 State、Node、Edge,再学习条件边、工具调用和项目实战。'\n",
|
||||
" elif '解释' in user_input or '什么是' in user_input:\n",
|
||||
" intent = 'explain'\n",
|
||||
" response = 'LangGraph 可以理解为用图结构组织智能体流程的框架。'\n",
|
||||
" else:\n",
|
||||
" intent = 'chat'\n",
|
||||
" response = '我可以帮你制定学习计划、解释概念或梳理项目思路。'\n",
|
||||
"\n",
|
||||
" return {\n",
|
||||
" 'input': user_input,\n",
|
||||
" 'intent': intent,\n",
|
||||
" 'response': response\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"result = run_learning_assistant('帮我制定 LangGraph 学习计划')\n",
|
||||
"print(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码是一个最小可部署单元。\n",
|
||||
"\n",
|
||||
"#### `run_learning_assistant(user_input)`\n",
|
||||
"这个函数接收用户输入,然后返回一个结构化结果。\n",
|
||||
"\n",
|
||||
"它没有直接使用 `input()`,也没有直接打印最终答案。\n",
|
||||
"\n",
|
||||
"这样做是为了方便部署。因为不同界面拿到结果后,可以用自己的方式展示:\n",
|
||||
"\n",
|
||||
"- 命令行可以 `print`\n",
|
||||
"- Web API 可以返回 JSON\n",
|
||||
"- 前端页面可以渲染到聊天窗口\n",
|
||||
"\n",
|
||||
"#### 为什么返回字典\n",
|
||||
"函数返回:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{\n",
|
||||
" 'input': user_input,\n",
|
||||
" 'intent': intent,\n",
|
||||
" 'response': response\n",
|
||||
"}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这比只返回一个字符串更适合工程项目。\n",
|
||||
"\n",
|
||||
"因为调用方不仅能拿到回复,还能看到系统识别出的意图,方便调试和评估。\n",
|
||||
"\n",
|
||||
"#### 部署前的关键原则\n",
|
||||
"无论项目内部多复杂,都建议对外提供一个简单入口。\n",
|
||||
"\n",
|
||||
"可以把它理解成智能体项目的“总开关”。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 本地 API 部署的基本思路\n",
|
||||
"\n",
|
||||
"API 部署的意思是:让其他程序通过 HTTP 请求调用你的智能体。\n",
|
||||
"\n",
|
||||
"例如前端页面可以发送:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"POST /chat\n",
|
||||
"{\"message\": \"帮我制定学习计划\"}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"服务端返回:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"{\"response\": \"建议先学习 State、Node、Edge...\"}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"下面先不用 FastAPI,直接用一个普通函数模拟 API 处理过程。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def chat_api_handler(request_json):\n",
|
||||
" user_message = request_json.get('message', '')\n",
|
||||
"\n",
|
||||
" if not user_message.strip():\n",
|
||||
" return {\n",
|
||||
" 'ok': False,\n",
|
||||
" 'error': 'message 不能为空'\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" result = run_learning_assistant(user_message)\n",
|
||||
"\n",
|
||||
" return {\n",
|
||||
" 'ok': True,\n",
|
||||
" 'data': result\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"request = {'message': '解释一下什么是 LangGraph'}\n",
|
||||
"response = chat_api_handler(request)\n",
|
||||
"print(response)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码模拟了一个 API 接口。\n",
|
||||
"\n",
|
||||
"#### `request_json`\n",
|
||||
"它代表外部系统传来的请求数据。\n",
|
||||
"\n",
|
||||
"真实 API 中,这个数据通常来自 HTTP 请求体。\n",
|
||||
"\n",
|
||||
"#### `request_json.get('message', '')`\n",
|
||||
"这行代码从请求中取出用户消息。\n",
|
||||
"\n",
|
||||
"如果请求里没有 `message`,就默认使用空字符串。\n",
|
||||
"\n",
|
||||
"#### 空输入检查\n",
|
||||
"如果用户没有传有效内容,接口返回:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"{'ok': False, 'error': 'message 不能为空'}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"这是部署时常见的边界检查。\n",
|
||||
"\n",
|
||||
"#### 调用核心函数\n",
|
||||
"真正处理智能体逻辑的是:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"result = run_learning_assistant(user_message)\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"API 层不应该写太多智能体逻辑,它只负责接收请求、调用核心函数、返回结果。\n",
|
||||
"\n",
|
||||
"#### 为什么这样分层\n",
|
||||
"分层后,项目会更容易维护:\n",
|
||||
"\n",
|
||||
"- 智能体逻辑改动,不影响 API 格式\n",
|
||||
"- API 部署方式改动,不影响智能体核心\n",
|
||||
"- 测试时可以单独测试核心函数,也可以测试接口函数"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. FastAPI 部署示意\n",
|
||||
"\n",
|
||||
"真实项目中,经常会用 FastAPI 把智能体包装成 HTTP 服务。\n",
|
||||
"\n",
|
||||
"下面是示意代码。这里放在 Markdown 中展示,不要求你现在必须运行。\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from fastapi import FastAPI\n",
|
||||
"from pydantic import BaseModel\n",
|
||||
"\n",
|
||||
"app = FastAPI()\n",
|
||||
"\n",
|
||||
"class ChatRequest(BaseModel):\n",
|
||||
" message: str\n",
|
||||
"\n",
|
||||
"@app.post('/chat')\n",
|
||||
"def chat(request: ChatRequest):\n",
|
||||
" result = run_learning_assistant(request.message)\n",
|
||||
" return result\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"如果保存为 `app.py`,通常可以用下面命令启动:\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"uvicorn app:app --host 0.0.0.0 --port 8000\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"启动后,其他程序就可以通过接口调用智能体。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. Docker 和云平台部署怎么理解\n",
|
||||
"\n",
|
||||
"当项目只在自己电脑上运行时,问题通常不大。\n",
|
||||
"\n",
|
||||
"但交给别人运行时,经常会出现:\n",
|
||||
"\n",
|
||||
"- Python 版本不同\n",
|
||||
"- 依赖包没安装\n",
|
||||
"- 环境变量没配置\n",
|
||||
"- 系统路径不一致\n",
|
||||
"\n",
|
||||
"Docker 的作用就是把运行环境打包起来。\n",
|
||||
"\n",
|
||||
"可以把 Docker 镜像理解成一个“装好环境的盒子”。别人只要运行这个盒子,就能得到接近一致的环境。\n",
|
||||
"\n",
|
||||
"云平台部署则是在远程服务器上运行这个应用,让更多用户可以访问。\n",
|
||||
"\n",
|
||||
"一个常见上线链路是:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"本地开发 -> 写 requirements.txt -> 写 Dockerfile -> 构建镜像 -> 部署到云服务器\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"本课程重点是智能体开发,所以这里只需要理解部署思路,不要求一次掌握所有运维细节。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 为什么要评估智能体\n",
|
||||
"\n",
|
||||
"部署解决的是“能不能给别人用”。\n",
|
||||
"\n",
|
||||
"评估解决的是“用得好不好”。\n",
|
||||
"\n",
|
||||
"智能体评估通常关注:\n",
|
||||
"\n",
|
||||
"| 指标 | 含义 | 示例 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 正确性 | 回答是否正确 | 是否解释对了概念 |\n",
|
||||
"| 相关性 | 是否回答了用户问题 | 问学习计划,不要回复天气 |\n",
|
||||
"| 完整性 | 回答是否覆盖关键点 | 学习计划是否有步骤 |\n",
|
||||
"| 稳定性 | 多次运行是否表现一致 | 同类问题是否走同类分支 |\n",
|
||||
"| 成本 | 调用模型的费用和耗时 | token、延迟、API 成本 |\n",
|
||||
"| 安全性 | 是否避免危险输出 | 不泄露密钥,不执行高风险操作 |\n",
|
||||
"\n",
|
||||
"初学阶段可以先做最简单的评估:准备一批测试问题,看系统是否识别出正确意图。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 构建一个简单评估集\n",
|
||||
"\n",
|
||||
"评估集就是提前准备好的测试样例。\n",
|
||||
"\n",
|
||||
"每条样例通常包含:\n",
|
||||
"\n",
|
||||
"- 输入问题\n",
|
||||
"- 期望结果\n",
|
||||
"\n",
|
||||
"下面我们先评估意图识别是否正确。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"eval_dataset = [\n",
|
||||
" {'input': '帮我制定 LangGraph 学习计划', 'expected_intent': 'plan'},\n",
|
||||
" {'input': '请安排一个 AI 学习路线', 'expected_intent': 'plan'},\n",
|
||||
" {'input': '解释一下什么是条件边', 'expected_intent': 'explain'},\n",
|
||||
" {'input': '什么是智能体状态', 'expected_intent': 'explain'},\n",
|
||||
" {'input': '你好,你能做什么', 'expected_intent': 'chat'},\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"print('评估集数量:', len(eval_dataset))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码定义了一个小型评估集。\n",
|
||||
"\n",
|
||||
"#### `eval_dataset`\n",
|
||||
"它是一个列表,里面每个元素都是一个测试样例。\n",
|
||||
"\n",
|
||||
"每个样例有两个字段:\n",
|
||||
"\n",
|
||||
"- `input`:用户输入\n",
|
||||
"- `expected_intent`:期望识别出的意图\n",
|
||||
"\n",
|
||||
"#### 为什么先评估意图\n",
|
||||
"因为当前学习助手的核心流程是:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"输入 -> 意图识别 -> 分支处理 -> 回复\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"如果意图识别错了,后面的节点也会走错。\n",
|
||||
"\n",
|
||||
"所以意图识别是一个很适合入门评估的指标。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. 自动运行评估\n",
|
||||
"\n",
|
||||
"有了评估集后,就可以写代码自动跑一遍,并计算准确率。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def evaluate_intent(dataset):\n",
|
||||
" records = []\n",
|
||||
" correct_count = 0\n",
|
||||
"\n",
|
||||
" for item in dataset:\n",
|
||||
" result = run_learning_assistant(item['input'])\n",
|
||||
" predicted = result['intent']\n",
|
||||
" expected = item['expected_intent']\n",
|
||||
" is_correct = predicted == expected\n",
|
||||
"\n",
|
||||
" if is_correct:\n",
|
||||
" correct_count += 1\n",
|
||||
"\n",
|
||||
" records.append({\n",
|
||||
" 'input': item['input'],\n",
|
||||
" 'expected': expected,\n",
|
||||
" 'predicted': predicted,\n",
|
||||
" 'correct': is_correct\n",
|
||||
" })\n",
|
||||
"\n",
|
||||
" accuracy = correct_count / len(dataset)\n",
|
||||
" return accuracy, records\n",
|
||||
"\n",
|
||||
"accuracy, records = evaluate_intent(eval_dataset)\n",
|
||||
"print('准确率:', accuracy)\n",
|
||||
"for record in records:\n",
|
||||
" print(record)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码完成了一个最小自动评估流程。\n",
|
||||
"\n",
|
||||
"#### `evaluate_intent(dataset)`\n",
|
||||
"这个函数接收评估集,然后逐条测试。\n",
|
||||
"\n",
|
||||
"#### `result = run_learning_assistant(item['input'])`\n",
|
||||
"每条测试样例都会调用一次智能体核心函数。\n",
|
||||
"\n",
|
||||
"这和真实用户调用项目的方式是一致的。\n",
|
||||
"\n",
|
||||
"#### `predicted` 和 `expected`\n",
|
||||
"- `predicted` 是系统实际识别出的意图\n",
|
||||
"- `expected` 是我们提前标注的正确意图\n",
|
||||
"\n",
|
||||
"如果两者相等,就认为这条样例通过。\n",
|
||||
"\n",
|
||||
"#### `accuracy`\n",
|
||||
"准确率计算公式是:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"准确率 = 正确数量 / 总样例数量\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"#### `records`\n",
|
||||
"除了给出总分,还要保存每条样例的详细结果。\n",
|
||||
"\n",
|
||||
"因为只有总分不够。我们还需要知道到底哪些问题错了,才能继续优化。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 记录运行日志\n",
|
||||
"\n",
|
||||
"部署后,用户的问题可能非常多。\n",
|
||||
"\n",
|
||||
"如果没有日志,出问题时就很难排查。\n",
|
||||
"\n",
|
||||
"日志至少应该记录:\n",
|
||||
"\n",
|
||||
"- 用户输入\n",
|
||||
"- 系统输出\n",
|
||||
"- 识别出的意图\n",
|
||||
"- 是否成功\n",
|
||||
"- 运行时间\n",
|
||||
"\n",
|
||||
"下面用一个简单包装函数演示如何记录日志。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import time\n",
|
||||
"\n",
|
||||
"run_logs = []\n",
|
||||
"\n",
|
||||
"def run_with_trace(user_input):\n",
|
||||
" start_time = time.time()\n",
|
||||
"\n",
|
||||
" try:\n",
|
||||
" result = run_learning_assistant(user_input)\n",
|
||||
" success = True\n",
|
||||
" error = ''\n",
|
||||
" except Exception as exc:\n",
|
||||
" result = {'input': user_input, 'intent': '', 'response': ''}\n",
|
||||
" success = False\n",
|
||||
" error = str(exc)\n",
|
||||
"\n",
|
||||
" latency = time.time() - start_time\n",
|
||||
"\n",
|
||||
" log = {\n",
|
||||
" 'input': user_input,\n",
|
||||
" 'intent': result['intent'],\n",
|
||||
" 'response': result['response'],\n",
|
||||
" 'success': success,\n",
|
||||
" 'error': error,\n",
|
||||
" 'latency_seconds': round(latency, 4)\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" run_logs.append(log)\n",
|
||||
" return result\n",
|
||||
"\n",
|
||||
"run_with_trace('解释一下什么是 LangGraph')\n",
|
||||
"run_with_trace('帮我安排学习计划')\n",
|
||||
"\n",
|
||||
"for log in run_logs:\n",
|
||||
" print(log)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码演示了最基础的追踪思路。\n",
|
||||
"\n",
|
||||
"#### `run_logs`\n",
|
||||
"这是一个列表,用来保存运行日志。\n",
|
||||
"\n",
|
||||
"真实项目中,日志通常会写入文件、数据库或日志平台。\n",
|
||||
"\n",
|
||||
"#### `start_time = time.time()`\n",
|
||||
"记录开始时间,用来计算本次调用耗时。\n",
|
||||
"\n",
|
||||
"#### `try ... except ...`\n",
|
||||
"部署后的系统不能因为一次错误就直接崩溃。\n",
|
||||
"\n",
|
||||
"所以这里用 `try ... except` 捕获异常,并把错误信息写入日志。\n",
|
||||
"\n",
|
||||
"#### `latency_seconds`\n",
|
||||
"表示运行耗时。\n",
|
||||
"\n",
|
||||
"对于真实大模型应用,耗时是非常重要的指标。\n",
|
||||
"\n",
|
||||
"用户不仅关心回答对不对,也关心等多久。\n",
|
||||
"\n",
|
||||
"#### 追踪的价值\n",
|
||||
"日志可以帮助我们回答:\n",
|
||||
"\n",
|
||||
"- 用户都问了什么?\n",
|
||||
"- 哪些问题经常失败?\n",
|
||||
"- 哪类意图识别不准?\n",
|
||||
"- 哪些请求耗时很长?\n",
|
||||
"\n",
|
||||
"这些信息是后续优化智能体的重要依据。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. LangSmith 是做什么的\n",
|
||||
"\n",
|
||||
"LangSmith 是 LangChain 生态中的调试、追踪和评估平台。\n",
|
||||
"\n",
|
||||
"你可以把它理解成智能体应用的“运行记录仪”。\n",
|
||||
"\n",
|
||||
"它可以帮助你查看:\n",
|
||||
"\n",
|
||||
"- 每次调用输入了什么\n",
|
||||
"- 调用了哪些链、工具或模型\n",
|
||||
"- 每一步输出是什么\n",
|
||||
"- 哪一步出错\n",
|
||||
"- 每次调用耗时多久\n",
|
||||
"- token 和成本情况\n",
|
||||
"\n",
|
||||
"本节我们不强制接入 LangSmith,因为它需要账号和环境变量配置。\n",
|
||||
"\n",
|
||||
"但你需要理解它解决的问题:**让智能体运行过程可观察、可追踪、可评估。**\n",
|
||||
"\n",
|
||||
"在真实项目中,如果智能体流程比较复杂,建议尽早接入类似追踪工具。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12. 上线前检查清单\n",
|
||||
"\n",
|
||||
"智能体项目上线前,建议检查下面这些内容。\n",
|
||||
"\n",
|
||||
"| 检查项 | 说明 |\n",
|
||||
"| --- | --- |\n",
|
||||
"| 环境变量 | API Key、Base URL 是否配置正确 |\n",
|
||||
"| 依赖版本 | requirements 是否完整 |\n",
|
||||
"| 基础测试 | 核心函数是否能正常运行 |\n",
|
||||
"| 评估集 | 常见问题是否通过测试 |\n",
|
||||
"| 错误处理 | 空输入、异常情况是否能返回友好提示 |\n",
|
||||
"| 日志追踪 | 是否能记录输入、输出和错误 |\n",
|
||||
"| 安全检查 | 是否避免泄露密钥和执行危险操作 |\n",
|
||||
"| 成本控制 | 是否限制过长输入和频繁调用 |\n",
|
||||
"\n",
|
||||
"这个清单不复杂,但很实用。\n",
|
||||
"\n",
|
||||
"它能帮助你从“代码能跑”过渡到“项目能交付”。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 13. 本节小结\n",
|
||||
"\n",
|
||||
"本节最重要的内容有五点:\n",
|
||||
"\n",
|
||||
"1. **部署是把智能体从个人代码变成别人可使用的服务**\n",
|
||||
"2. **核心逻辑应该先封装成稳定函数,再接命令行、API 或 Web 界面**\n",
|
||||
"3. **评估集可以帮助我们判断智能体是否稳定、准确**\n",
|
||||
"4. **日志和追踪可以帮助我们排查问题、分析用户行为、持续优化系统**\n",
|
||||
"5. **LangSmith 这类工具可以让复杂智能体流程变得可观察、可评估**\n",
|
||||
"\n",
|
||||
"到这里,一个智能体项目就不只是能运行的 demo,而是开始具备工程化交付能力。"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 14. 本节练习\n",
|
||||
"\n",
|
||||
"1. 给 `eval_dataset` 增加 5 条新的测试样例\n",
|
||||
"2. 修改 `run_learning_assistant`,增加一个 `summary` 意图\n",
|
||||
"3. 修改评估函数,同时评估 `intent` 和 `response` 中是否包含关键字\n",
|
||||
"4. 给 `run_with_trace` 增加一个 `timestamp` 字段,记录调用时间\n",
|
||||
"5. 思考:如果这个助手要部署给 100 个用户使用,还需要补充哪些能力?"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
557
28_课程总结.ipynb
Normal file
557
28_课程总结.ipynb
Normal file
@@ -0,0 +1,557 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 28_课程总结:从入门到实践的 AI 智能体学习路线\n",
|
||||
"\n",
|
||||
"## 学习目标\n",
|
||||
"1. 回顾课程核心知识点,形成完整的知识体系\n",
|
||||
"2. 了解 AI 智能体领域的前沿发展趋势和拓展方向\n",
|
||||
"3. 规划后续自主学习路径和实践建议\n",
|
||||
"\n",
|
||||
"本节课不是学习一个全新的技术点,而是把前面学过的内容串起来,帮助大家形成一张完整的“AI 智能体知识地图”。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. 这门课到底学了什么\n",
|
||||
"\n",
|
||||
"如果用一句话总结这门课:\n",
|
||||
"\n",
|
||||
"> 我们学习了如何把大模型从“聊天机器人”升级为“能够使用工具、执行流程、完成任务的智能体”。\n",
|
||||
"\n",
|
||||
"普通大模型更像一个会回答问题的人,而智能体更像一个会办事的助手。它不只是生成文字,还可以:\n",
|
||||
"\n",
|
||||
"- 理解用户目标\n",
|
||||
"- 拆解任务步骤\n",
|
||||
"- 调用外部工具\n",
|
||||
"- 读取和检索资料\n",
|
||||
"- 根据中间结果继续决策\n",
|
||||
"- 最终交付完整结果\n",
|
||||
"\n",
|
||||
"所以,智能体的核心不是“模型有多聪明”,而是“模型能否被组织进一个可靠的工作流程中”。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. AI 智能体的核心知识地图\n",
|
||||
"\n",
|
||||
"可以把整个课程内容分成 6 个层次:\n",
|
||||
"\n",
|
||||
"| 层次 | 核心内容 | 通俗理解 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 大模型基础 | Prompt、模型调用、消息格式 | 让模型听懂我们的问题 |\n",
|
||||
"| LangChain | 链、提示词、工具、RAG | 把模型能力组件化 |\n",
|
||||
"| 工具调用 | 搜索、计算、文件读取、API | 让模型能使用外部能力 |\n",
|
||||
"| RAG 知识库 | 文档切分、向量检索、基于资料回答 | 让模型先查资料再回答 |\n",
|
||||
"| LangGraph | State、Node、Edge、条件分支 | 把智能体流程画成流程图 |\n",
|
||||
"| 项目实战 | 知识库助手、多工具智能体 | 把知识点组合成完整应用 |\n",
|
||||
"\n",
|
||||
"学习时不要只记住某个库的 API,更重要的是理解这些模块分别解决什么问题。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. 回顾一:Prompt 是智能体的起点\n",
|
||||
"\n",
|
||||
"Prompt 可以理解为我们给大模型的任务说明书。\n",
|
||||
"\n",
|
||||
"一个好的 Prompt 通常会说明:\n",
|
||||
"\n",
|
||||
"1. 你是谁:模型要扮演什么角色\n",
|
||||
"2. 你要做什么:具体任务是什么\n",
|
||||
"3. 你依据什么:是否需要参考资料\n",
|
||||
"4. 你怎么输出:输出格式有什么要求\n",
|
||||
"\n",
|
||||
"下面用一个简单函数模拟 Prompt 拼接。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def build_prompt(role, task, context, output_format):\n",
|
||||
" prompt = f\"角色:{role}\\n\"\n",
|
||||
" prompt += f\"任务:{task}\\n\"\n",
|
||||
" prompt += f\"参考资料:{context}\\n\"\n",
|
||||
" prompt += f\"输出格式:{output_format}\"\n",
|
||||
" return prompt\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"prompt = build_prompt(\n",
|
||||
" role=\"AI 智能体课程助教\",\n",
|
||||
" task=\"解释什么是工具调用\",\n",
|
||||
" context=\"工具调用是指模型根据任务选择并使用外部函数或 API。\",\n",
|
||||
" output_format=\"用三句话通俗解释\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(prompt)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码用于复习 Prompt 的基本组成:\n",
|
||||
"\n",
|
||||
"1. `build_prompt` 是一个提示词构造函数。\n",
|
||||
"2. `role` 表示模型扮演的角色,例如课程助教、数据分析师、代码助手。\n",
|
||||
"3. `task` 表示模型要完成的具体任务。\n",
|
||||
"4. `context` 表示参考资料,能够减少模型凭空发挥。\n",
|
||||
"5. `output_format` 表示输出格式要求,可以让答案更稳定。\n",
|
||||
"6. 函数内部通过字符串拼接生成完整 Prompt。\n",
|
||||
"\n",
|
||||
"真实项目中,Prompt 往往不是随便写一句话,而是要像写任务说明书一样清晰。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. 回顾二:工具调用让智能体拥有外部能力\n",
|
||||
"\n",
|
||||
"大模型擅长理解和生成语言,但不擅长所有事情。\n",
|
||||
"\n",
|
||||
"例如数学计算、实时搜索、文件读取、数据库查询等任务,更适合交给工具完成。\n",
|
||||
"\n",
|
||||
"工具调用的基本流程是:\n",
|
||||
"\n",
|
||||
"```text\n",
|
||||
"用户问题 -> 判断是否需要工具 -> 选择工具 -> 执行工具 -> 整理结果\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"下面用一个简化例子复习工具选择和调用。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def calculate(expression):\n",
|
||||
" return eval(expression)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def search_knowledge(keyword):\n",
|
||||
" knowledge = {\n",
|
||||
" \"LangChain\": \"LangChain 用于构建大模型应用。\",\n",
|
||||
" \"LangGraph\": \"LangGraph 用于编排多步骤智能体流程。\",\n",
|
||||
" }\n",
|
||||
" return knowledge.get(keyword, \"没有找到相关知识。\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def simple_tool_agent(question):\n",
|
||||
" if \"计算\" in question:\n",
|
||||
" expression = question.replace(\"计算\", \"\").strip()\n",
|
||||
" result = calculate(expression)\n",
|
||||
" return f\"我使用了计算工具,结果是:{result}\"\n",
|
||||
"\n",
|
||||
" if \"LangGraph\" in question:\n",
|
||||
" result = search_knowledge(\"LangGraph\")\n",
|
||||
" return f\"我使用了知识查询工具,结果是:{result}\"\n",
|
||||
"\n",
|
||||
" return \"这个问题暂时不需要调用工具。\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(simple_tool_agent(\"计算 25 * 4 + 8\"))\n",
|
||||
"print(simple_tool_agent(\"LangGraph 是什么?\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码复习了工具调用的基本思想:\n",
|
||||
"\n",
|
||||
"1. `calculate(expression)` 是计算工具,负责执行数学表达式。\n",
|
||||
"2. `search_knowledge(keyword)` 是知识查询工具,负责从字典中查找资料。\n",
|
||||
"3. `simple_tool_agent(question)` 是一个简单智能体入口。\n",
|
||||
"4. 如果问题中包含“计算”,就提取表达式并调用计算工具。\n",
|
||||
"5. 如果问题中包含 `LangGraph`,就调用知识查询工具。\n",
|
||||
"6. 最终返回时,会说明使用了哪个工具以及工具结果。\n",
|
||||
"\n",
|
||||
"这里为了复习写得很简单。真实项目中,不建议直接对用户输入使用 `eval`,应使用更安全的计算库或严格校验。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. 回顾三:RAG 让模型基于资料回答\n",
|
||||
"\n",
|
||||
"RAG 是构建知识库问答系统时非常重要的技术。\n",
|
||||
"\n",
|
||||
"它解决的问题是:大模型不知道你的私有资料,也可能记错公开知识。\n",
|
||||
"\n",
|
||||
"RAG 的思路很简单:\n",
|
||||
"\n",
|
||||
"1. 先把资料放入知识库\n",
|
||||
"2. 用户提问时先检索相关资料\n",
|
||||
"3. 再让模型基于检索资料回答\n",
|
||||
"\n",
|
||||
"下面用纯 Python 复习一个最小 RAG 流程。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"documents = [\n",
|
||||
" \"RAG 的核心是先检索资料,再基于资料生成答案。\",\n",
|
||||
" \"LangChain 提供文档加载、文本切分、检索器和链等能力。\",\n",
|
||||
" \"LangGraph 适合构建有状态、多步骤、可分支的智能体流程。\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def retrieve_by_keyword(question, documents):\n",
|
||||
" scored_docs = []\n",
|
||||
" for doc in documents:\n",
|
||||
" score = len(set(question) & set(doc))\n",
|
||||
" scored_docs.append((score, doc))\n",
|
||||
"\n",
|
||||
" scored_docs.sort(reverse=True, key=lambda item: item[0])\n",
|
||||
" return scored_docs[0][1]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def rag_answer(question):\n",
|
||||
" context = retrieve_by_keyword(question, documents)\n",
|
||||
" return f\"问题:{question}\\n参考资料:{context}\\n回答:请优先根据参考资料作答。\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(rag_answer(\"RAG 的核心流程是什么?\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码复习了 RAG 的最小流程:\n",
|
||||
"\n",
|
||||
"1. `documents` 模拟知识库中的资料。\n",
|
||||
"2. `retrieve_by_keyword` 是简化检索器,会给每条资料计算相关度分数。\n",
|
||||
"3. `set(question) & set(doc)` 表示问题和文档中共同出现的字符。\n",
|
||||
"4. `scored_docs.sort(...)` 按分数从高到低排序。\n",
|
||||
"5. `rag_answer(question)` 先检索最相关资料,再把资料作为参考内容组织成回答。\n",
|
||||
"\n",
|
||||
"真实 RAG 项目通常会把关键词检索换成向量检索,把简单回答拼接换成大模型生成。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. 回顾四:LangGraph 让流程更清晰\n",
|
||||
"\n",
|
||||
"当智能体只有一步时,普通函数就够了。\n",
|
||||
"\n",
|
||||
"但当智能体需要多步骤执行、条件判断、循环反思、工具协作时,就需要更清晰的流程编排。\n",
|
||||
"\n",
|
||||
"LangGraph 的核心概念可以简单理解为:\n",
|
||||
"\n",
|
||||
"| 概念 | 作用 | 通俗理解 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| State | 保存流程中的数据 | 任务档案袋 |\n",
|
||||
"| Node | 一个处理步骤 | 流程图里的方框 |\n",
|
||||
"| Edge | 节点之间的连接 | 流程图里的箭头 |\n",
|
||||
"| Conditional Edge | 条件分支 | 根据情况走不同路线 |\n",
|
||||
"\n",
|
||||
"下面不用依赖 LangGraph,先用普通 Python 模拟这种状态流转思想。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def plan_node(state):\n",
|
||||
" goal = state[\"goal\"]\n",
|
||||
" state[\"plan\"] = [f\"理解目标:{goal}\", \"执行任务\", \"汇总结果\"]\n",
|
||||
" return state\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def execute_node(state):\n",
|
||||
" state[\"result\"] = [f\"已完成:{step}\" for step in state[\"plan\"]]\n",
|
||||
" return state\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def summary_node(state):\n",
|
||||
" state[\"answer\"] = \"\\n\".join(state[\"result\"])\n",
|
||||
" return state\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"state = {\"goal\": \"复习 AI 智能体课程\"}\n",
|
||||
"state = plan_node(state)\n",
|
||||
"state = execute_node(state)\n",
|
||||
"state = summary_node(state)\n",
|
||||
"\n",
|
||||
"print(state[\"answer\"])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这段代码用普通 Python 模拟 LangGraph 的状态流转:\n",
|
||||
"\n",
|
||||
"1. `state` 是一个字典,相当于工作流中的共享状态。\n",
|
||||
"2. `plan_node(state)` 模拟规划节点,会根据目标生成计划。\n",
|
||||
"3. `execute_node(state)` 模拟执行节点,会执行计划中的每个步骤。\n",
|
||||
"4. `summary_node(state)` 模拟汇总节点,会把结果整理成最终答案。\n",
|
||||
"5. 每个节点都接收 `state`,修改后再返回 `state`。\n",
|
||||
"\n",
|
||||
"LangGraph 做的事情更规范、更强大,但底层思想就是让数据沿着流程图一步步流动。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. 一个智能体项目的标准开发流程\n",
|
||||
"\n",
|
||||
"开发智能体项目时,可以按照下面的步骤推进:\n",
|
||||
"\n",
|
||||
"1. 明确用户需求:用户到底要解决什么问题\n",
|
||||
"2. 设计输入输出:用户输入什么,系统输出什么\n",
|
||||
"3. 拆分功能模块:需要模型、工具、检索还是工作流\n",
|
||||
"4. 先做最小版本:用简单规则和模拟数据跑通流程\n",
|
||||
"5. 替换真实能力:接入真实模型、数据库、搜索 API 等\n",
|
||||
"6. 增加调试日志:观察每一步中间结果\n",
|
||||
"7. 优化稳定性:处理异常、补充测试、改进提示词\n",
|
||||
"\n",
|
||||
"初学者最容易犯的错误是:一上来就想做完整系统。更推荐先做 MVP,再逐步升级。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8. 智能体常见项目类型\n",
|
||||
"\n",
|
||||
"学完本课程后,可以尝试这些项目方向:\n",
|
||||
"\n",
|
||||
"| 项目类型 | 能力重点 | 示例 |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 知识库问答助手 | RAG、文档检索 | 课程资料问答、企业制度问答 |\n",
|
||||
"| 多工具助手 | 工具选择、工具调用 | 搜索 + 计算 + 文件读取助手 |\n",
|
||||
"| 自动报告生成器 | 任务分解、结果汇总 | 周报生成、调研报告生成 |\n",
|
||||
"| 数据分析智能体 | 代码执行、图表生成 | 自动分析表格并输出结论 |\n",
|
||||
"| 工作流智能体 | LangGraph、条件分支 | 审核流程、客服分流、任务调度 |\n",
|
||||
"| 多智能体协作 | 角色分工、结果协同 | 产品经理 + 开发 + 测试协作 |\n",
|
||||
"\n",
|
||||
"建议从知识库问答和多工具助手开始,因为它们最容易理解,也最接近真实应用。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9. AI 智能体的发展趋势\n",
|
||||
"\n",
|
||||
"AI 智能体还在快速发展,值得关注的方向包括:\n",
|
||||
"\n",
|
||||
"1. **更强的工具调用能力**:模型会更准确地选择工具、填写参数、处理工具错误。\n",
|
||||
"2. **更可靠的工作流编排**:复杂任务会越来越依赖 LangGraph 这类流程框架。\n",
|
||||
"3. **多模态智能体**:智能体不仅处理文字,还能理解图片、语音、视频和表格。\n",
|
||||
"4. **企业级知识库**:RAG 会和权限控制、审计、数据治理结合得更紧密。\n",
|
||||
"5. **多智能体协作**:多个角色智能体分工合作,完成更复杂的任务。\n",
|
||||
"6. **本地化和私有化部署**:越来越多企业会关注数据安全和私有模型部署。\n",
|
||||
"\n",
|
||||
"但无论技术怎么变,底层能力仍然离不开:需求分析、任务拆解、工具设计、流程控制和结果验证。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10. 后续学习路线建议\n",
|
||||
"\n",
|
||||
"可以按照下面路线继续学习:\n",
|
||||
"\n",
|
||||
"### 第一阶段:打牢基础\n",
|
||||
"- 熟悉 Python 函数、类、字典、列表\n",
|
||||
"- 掌握 API 调用和 JSON 数据格式\n",
|
||||
"- 熟悉 Prompt 编写和调试\n",
|
||||
"\n",
|
||||
"### 第二阶段:掌握框架\n",
|
||||
"- 学习 LangChain 的 Prompt、Chain、Tool、Retriever\n",
|
||||
"- 学习 LangGraph 的 State、Node、Edge、条件分支\n",
|
||||
"- 理解 RAG 的文档加载、切分、向量化、检索、生成\n",
|
||||
"\n",
|
||||
"### 第三阶段:完成项目\n",
|
||||
"- 做一个个人知识库问答助手\n",
|
||||
"- 做一个多工具协作助手\n",
|
||||
"- 做一个带 LangGraph 工作流的任务调度智能体\n",
|
||||
"\n",
|
||||
"### 第四阶段:工程化\n",
|
||||
"- 增加日志、测试和异常处理\n",
|
||||
"- 接入数据库、缓存和权限控制\n",
|
||||
"- 部署为 Web 服务或内部工具\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11. 自我检查清单\n",
|
||||
"\n",
|
||||
"学完课程后,可以用下面的问题检查自己是否真正掌握:\n",
|
||||
"\n",
|
||||
"1. 我能否解释普通大模型应用和智能体应用的区别?\n",
|
||||
"2. 我能否写出一个清晰的 Prompt 模板?\n",
|
||||
"3. 我能否封装一个简单工具并让智能体调用?\n",
|
||||
"4. 我能否说明 RAG 的完整流程?\n",
|
||||
"5. 我能否用 State、Node、Edge 解释 LangGraph?\n",
|
||||
"6. 我能否把一个复杂任务拆成多个节点?\n",
|
||||
"7. 我能否通过日志定位智能体执行错误?\n",
|
||||
"8. 我能否独立完成一个小型智能体项目?\n",
|
||||
"\n",
|
||||
"如果这些问题大部分都能回答清楚,说明你已经具备继续做项目的基础。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12. 最后一个综合复习示例\n",
|
||||
"\n",
|
||||
"下面用一个小例子把 Prompt、工具调用、RAG 和流程编排思想合在一起。\n",
|
||||
"\n",
|
||||
"这个示例仍然是教学版,目标是帮助大家看清完整结构。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"course_docs = [\n",
|
||||
" \"智能体可以调用工具完成搜索、计算、文件读取等任务。\",\n",
|
||||
" \"RAG 可以让智能体基于知识库资料回答问题。\",\n",
|
||||
" \"LangGraph 可以把智能体任务拆成多个节点并按流程执行。\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def course_retriever(question):\n",
|
||||
" best_doc = max(course_docs, key=lambda doc: len(set(question) & set(doc)))\n",
|
||||
" return best_doc\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def course_calculator(expression):\n",
|
||||
" return eval(expression)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def final_review_agent(question):\n",
|
||||
" if \"计算\" in question:\n",
|
||||
" expression = question.replace(\"计算\", \"\").strip()\n",
|
||||
" result = course_calculator(expression)\n",
|
||||
" return f\"我判断这是计算任务,调用计算工具后得到:{result}\"\n",
|
||||
"\n",
|
||||
" context = course_retriever(question)\n",
|
||||
" prompt = build_prompt(\n",
|
||||
" role=\"AI 智能体课程总结助手\",\n",
|
||||
" task=f\"回答问题:{question}\",\n",
|
||||
" context=context,\n",
|
||||
" output_format=\"先给结论,再给一句解释\",\n",
|
||||
" )\n",
|
||||
" return prompt\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(final_review_agent(\"RAG 有什么作用?\"))\n",
|
||||
"print(\"-\" * 40)\n",
|
||||
"print(final_review_agent(\"计算 18 * 6 + 2\"))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### 代码解释\n",
|
||||
"\n",
|
||||
"这个综合示例把多个知识点串联起来:\n",
|
||||
"\n",
|
||||
"1. `course_docs` 是一个小型课程知识库。\n",
|
||||
"2. `course_retriever(question)` 根据问题从知识库中找最相关的资料。\n",
|
||||
"3. `course_calculator(expression)` 是计算工具。\n",
|
||||
"4. `final_review_agent(question)` 是综合复习智能体入口。\n",
|
||||
"5. 如果问题中包含“计算”,智能体会调用计算工具。\n",
|
||||
"6. 如果不是计算任务,智能体会先检索课程资料,再调用前面写过的 `build_prompt` 生成提示词。\n",
|
||||
"7. 这个流程体现了智能体的基本思想:先判断任务类型,再选择合适能力,最后组织输出。\n",
|
||||
"\n",
|
||||
"真实项目中,可以把这里的 `return prompt` 换成真实大模型调用,让模型基于 Prompt 生成最终回答。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 13. 课程总结\n",
|
||||
"\n",
|
||||
"这门课的重点不是让大家记住所有 API,而是建立一套解决智能体问题的思维方式:\n",
|
||||
"\n",
|
||||
"1. 先理解用户目标,再决定系统流程。\n",
|
||||
"2. 能用普通函数解决的,先用普通函数跑通。\n",
|
||||
"3. 需要模型生成时,再接入大模型。\n",
|
||||
"4. 需要外部能力时,封装成工具。\n",
|
||||
"5. 需要资料依据时,使用 RAG。\n",
|
||||
"6. 需要多步骤流程时,使用 LangGraph。\n",
|
||||
"7. 需要稳定上线时,补充日志、测试、异常处理和监控。\n",
|
||||
"\n",
|
||||
"真正的智能体开发,不是把所有新技术堆在一起,而是根据问题选择最合适、最简单、最可靠的方案。\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 14. 课后实践建议\n",
|
||||
"\n",
|
||||
"建议完成以下实践:\n",
|
||||
"\n",
|
||||
"1. 整理自己的学习笔记,做一个个人知识库问答助手。\n",
|
||||
"2. 给知识库助手增加“显示参考来源”的功能。\n",
|
||||
"3. 做一个多工具助手,至少支持搜索、计算和文件读取。\n",
|
||||
"4. 用 LangGraph 重构多工具助手,把每一步拆成节点。\n",
|
||||
"5. 给项目增加调试日志,记录每次工具选择和工具结果。\n",
|
||||
"6. 尝试把项目封装成一个简单 Web 应用或命令行工具。\n",
|
||||
"\n",
|
||||
"完成这些练习后,你就不只是理解了 AI 智能体,而是已经具备了独立构建智能体应用的基础能力。\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.10.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
355
README.md
Normal file
355
README.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# AI 智能体开发(LangChain + LangGraph)
|
||||
|
||||
## 课程简介
|
||||
|
||||
本课程面向具备 Python 基础语法的大学生和 AI 应用开发初学者,系统讲解如何使用 **大语言模型 API、LangChain、RAG、向量数据库、LangGraph** 构建 AI 智能体应用。
|
||||
|
||||
课程不是停留在“调用大模型聊天”,而是围绕真实智能体开发流程,帮助学习者理解并实践:如何把大模型、Prompt、工具、知识库、状态管理、流程控制、人机协作、评估与部署组织成一个可运行、可测试、可交付的 AI 应用系统。
|
||||
|
||||
课程主线如下:
|
||||
|
||||
```text
|
||||
开发环境
|
||||
→ 大模型 API 调用
|
||||
→ LangChain 基础组件
|
||||
→ Prompt / Chain / Parser / Tool
|
||||
→ RAG 与向量数据库
|
||||
→ LangGraph 状态图
|
||||
→ 对话代理、任务调度、多智能体、人机协作
|
||||
→ 综合项目实战
|
||||
→ 部署、评估与课程总结
|
||||
```
|
||||
|
||||
## 课程定位
|
||||
|
||||
本课程定位为 **AI 智能体开发入门到项目实战课程**,重点培养以下能力:
|
||||
|
||||
- 能够独立搭建 Python + Jupyter + LangChain + LangGraph 开发环境;
|
||||
- 能够通过 OpenAI 兼容 API 调用大语言模型;
|
||||
- 能够使用 Prompt、链式组合、输出解析器构建稳定的大模型应用;
|
||||
- 能够定义工具,并让模型根据任务调用工具;
|
||||
- 能够使用 RAG 和向量数据库构建知识库问答系统;
|
||||
- 能够使用 LangGraph 构建有状态、可分支、可循环、可记忆的智能体流程;
|
||||
- 能够理解多智能体协作、任务调度和人机协作机制;
|
||||
- 能够完成从需求分析、MVP 实现到部署评估的完整项目闭环。
|
||||
|
||||
## 适合人群
|
||||
|
||||
适合以下学习者:
|
||||
|
||||
- 具备 Python 基础语法的大学生;
|
||||
- 想从大模型 API 调用进入 AI 应用开发的初学者;
|
||||
- 想系统学习 LangChain、RAG、LangGraph 的开发者;
|
||||
- 希望完成 AI 智能体项目作品的学生或工程实践者;
|
||||
- 对工具调用、多智能体、人机协作、任务自动化感兴趣的学习者。
|
||||
|
||||
## 先修要求
|
||||
|
||||
建议学习者具备:
|
||||
|
||||
- Python 基础语法;
|
||||
- 函数、列表、字典、类、异常处理等基础知识;
|
||||
- 基本命令行使用能力;
|
||||
- Jupyter Notebook 基础使用经验;
|
||||
- 对 HTTP API、JSON、环境变量有初步了解更佳。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **开发工具**:VS Code
|
||||
- **编程语言**:Python 3.12.11
|
||||
- **虚拟环境**:venv
|
||||
- **Notebook 环境**:Jupyter Notebook / ipykernel
|
||||
- **主要框架**:LangChain、LangGraph
|
||||
- **向量数据库**:ChromaDB
|
||||
- **大语言模型**:`qwen3.6-35b-A3b`
|
||||
- **向量模型**:`qwen3-embedding`
|
||||
- **API 方式**:OpenAI 兼容 API
|
||||
- **包管理**:pip + 清华大学 TUNA 镜像源
|
||||
- **部署与评估**:FastAPI、Docker、日志、评估集、LangSmith 概念
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Python 3.10 及以上版本,推荐 Python 3.12.11
|
||||
- 已安装 pip
|
||||
- 已安装 VS Code 和 Jupyter 相关插件
|
||||
- 具备 OpenAI 兼容 API 访问权限
|
||||
- 已获取可用的 `OPENAI_BASE_URL` 和 `OPENAI_API_KEY`
|
||||
|
||||
## 默认模型配置
|
||||
|
||||
本课程默认统一使用以下模型:
|
||||
|
||||
| 类型 | 默认模型 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| 大语言模型 | `qwen3.6-35b-A3b` | 对话、生成、推理、工具调用、智能体节点 |
|
||||
| 向量模型 | `qwen3-embedding` | 文档向量化、相似度检索、RAG 知识库 |
|
||||
|
||||
如实际 API 服务不支持上述模型,可根据服务提供方说明替换为等价模型,但建议在整门课程中保持模型名称统一,避免调试混乱。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 克隆本仓库
|
||||
|
||||
```bash
|
||||
git clone <仓库地址>
|
||||
cd AI智能体开发
|
||||
```
|
||||
|
||||
### 2. 创建虚拟环境
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
```
|
||||
|
||||
### 3. 激活虚拟环境
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
macOS / Linux:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
### 4. 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
### 5. 配置环境变量
|
||||
|
||||
在项目根目录创建 `.env` 文件:
|
||||
|
||||
```bash
|
||||
OPENAI_BASE_URL=https://your-base-url.com/v1
|
||||
OPENAI_API_KEY=sk-your-api-key
|
||||
```
|
||||
|
||||
### 6. 选择 Jupyter 内核
|
||||
|
||||
在 VS Code 中选择 `.venv` 对应的 Python 解释器或 Jupyter Kernel,然后按顺序打开 `.ipynb` 课件学习。
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成课程后,学习者应能够:
|
||||
|
||||
1. 搭建 Python、VS Code、venv、Jupyter 的 AI 应用开发环境;
|
||||
2. 使用 OpenAI 兼容 API 调用 `qwen3.6-35b-A3b`;
|
||||
3. 掌握 PromptTemplate、ChatPromptTemplate、Few-shot Prompt 等提示模板方法;
|
||||
4. 使用 LangChain 构建 LLM 链、结构化输出链和复杂 Runnable 链;
|
||||
5. 定义计算、查询、文件处理等工具,并实现模型驱动的工具调用;
|
||||
6. 使用 `qwen3-embedding`、ChromaDB 构建向量检索系统;
|
||||
7. 构建完整 RAG 知识库问答应用;
|
||||
8. 使用 LangGraph 构建状态图、节点、边、条件边、循环与记忆;
|
||||
9. 实现对话代理、任务调度、多智能体协作和人机协作流程;
|
||||
10. 完成知识库问答助手、多工具协作智能体、交互式智能体项目;
|
||||
11. 理解智能体项目的部署、评估、日志、追踪和上线检查方法。
|
||||
|
||||
## 推荐学习路径
|
||||
|
||||
```text
|
||||
01-02:环境准备
|
||||
03-04:大模型 API 与 LangChain 入门
|
||||
05-08:Prompt、LLM 链、输出解析器、链式组合
|
||||
09-10:工具定义与工具调用
|
||||
11-13:RAG、Embedding、向量数据库、知识库问答
|
||||
14-19:LangGraph 状态图基础
|
||||
20-23:高级智能体编排
|
||||
24-26:综合项目实战
|
||||
27-28:部署评估与课程总结
|
||||
```
|
||||
|
||||
> 注:从认知顺序看,建议学习者在学习第 05 课 LLM 链时,同时回看第 06 课 Prompt 模板。后续课程维护时,可考虑将 Prompt 模板提前到 LLM 链之前。
|
||||
|
||||
## 详细课程大纲
|
||||
|
||||
### 模块一:开发环境与 Python 项目配置
|
||||
|
||||
| 序号 | 课件 | 主题 | 核心产出 |
|
||||
| --- | --- | --- | --- |
|
||||
| 01 | `01_开发环境搭建.ipynb` | 开发环境搭建 | 可运行的本地 Python 虚拟环境 |
|
||||
| 02 | `02_Python环境配置.ipynb` | Python 环境配置 | 可复现的 Notebook 开发环境 |
|
||||
|
||||
本模块帮助学习者完成 VS Code、Python、venv、pip、Jupyter、依赖管理等基础准备,为后续运行大模型和智能体课件打好环境基础。
|
||||
|
||||
### 模块二:大模型 API 与 LangChain 入门
|
||||
|
||||
| 序号 | 课件 | 主题 | 核心产出 |
|
||||
| --- | --- | --- | --- |
|
||||
| 03 | `03_大模型API调用.ipynb` | 大模型 API 调用 | 可调用 `qwen3.6-35b-A3b` 的 Python 示例 |
|
||||
| 04 | `04_LangChain概述.ipynb` | LangChain 概述 | 第一个 LangChain 调用链 |
|
||||
|
||||
本模块介绍 OpenAI 兼容 API、环境变量配置、单轮对话、多轮对话、流式输出,以及 LangChain 的框架定位和组件化开发思想。
|
||||
|
||||
### 模块三:LangChain 核心组件
|
||||
|
||||
| 序号 | 课件 | 主题 | 核心产出 |
|
||||
| --- | --- | --- | --- |
|
||||
| 05 | `05_LLM链.ipynb` | LLM 链 | Prompt + LLM + Parser 基础链 |
|
||||
| 06 | `06_Prompt模板.ipynb` | Prompt 模板 | 可复用的 Prompt 模板 |
|
||||
| 07 | `07_输出解析器.ipynb` | 输出解析器 | 稳定结构化输出链 |
|
||||
| 08 | `08_链式组合.ipynb` | 链式组合 | 多步骤、多分支 Runnable 流程 |
|
||||
|
||||
本模块学习 LangChain 应用开发的核心组件,包括 Prompt 模板、模型调用、输出解析、Runnable、并行链、分支链和复杂链式组合。
|
||||
|
||||
### 模块四:工具定义与工具调用
|
||||
|
||||
| 序号 | 课件 | 主题 | 核心产出 |
|
||||
| --- | --- | --- | --- |
|
||||
| 09 | `09_工具定义.ipynb` | 工具定义 | 自定义工具集合 |
|
||||
| 10 | `10_工具调用.ipynb` | 工具调用 | 能自动选择并调用工具的智能体 |
|
||||
|
||||
本模块从 `@tool` 工具定义开始,逐步学习计算工具、查询工具、文件工具、多工具注册、模型选择工具、工具执行与最终答案生成。
|
||||
|
||||
建议在学习本模块后重点理解 Agent 的基本循环:
|
||||
|
||||
```text
|
||||
观察任务 → 思考方案 → 选择工具 → 执行工具 → 观察结果 → 生成答案
|
||||
```
|
||||
|
||||
### 模块五:RAG 与向量数据库
|
||||
|
||||
| 序号 | 课件 | 主题 | 核心产出 |
|
||||
| --- | --- | --- | --- |
|
||||
| 11 | `11_检索增强.ipynb` | 检索增强 | 简单本地 RAG 问答链 |
|
||||
| 12 | `12_向量数据库.ipynb` | 向量数据库 | 基于 ChromaDB 的本地知识库检索 |
|
||||
| 13 | `13_RAG构建.ipynb` | RAG 构建 | 多文档知识库问答系统 |
|
||||
|
||||
本模块学习 RAG 的完整流程:文档加载、文本切分、`qwen3-embedding` 向量化、向量数据库存储、相似度检索、上下文构造、答案生成和检索效果评估。
|
||||
|
||||
推荐理解路径:
|
||||
|
||||
```text
|
||||
为什么需要 RAG
|
||||
→ 文档如何切分
|
||||
→ 文本如何变成向量
|
||||
→ 向量数据库如何检索
|
||||
→ 检索结果如何交给大模型回答
|
||||
→ 如何评估 RAG 效果
|
||||
```
|
||||
|
||||
### 模块六:LangGraph 状态图基础
|
||||
|
||||
| 序号 | 课件 | 主题 | 核心产出 |
|
||||
| --- | --- | --- | --- |
|
||||
| 14 | `14_LangGraph概述.ipynb` | LangGraph 概述 | 最小 LangGraph 状态图 |
|
||||
| 15 | `15_图结构.ipynb` | 图结构 | StateGraph 总体结构示例 |
|
||||
| 16 | `16_状态管理.ipynb` | 状态管理 | 多字段业务状态设计 |
|
||||
| 17 | `17_节点与边.ipynb` | 节点与边 | 多节点顺序执行图 |
|
||||
| 18 | `18_条件边.ipynb` | 条件边 | 带条件判断的状态图 |
|
||||
| 19 | `19_循环与记忆.ipynb` | 循环与记忆 | 支持循环和持久化状态的智能体流程 |
|
||||
|
||||
本模块学习 LangGraph 的核心思想:用状态图管理复杂智能体流程。学习者将掌握 State、Node、Edge、START、END、条件边、循环、MemorySaver 和 thread_id 等关键概念。
|
||||
|
||||
### 模块七:高级智能体编排
|
||||
|
||||
| 序号 | 课件 | 主题 | 核心产出 |
|
||||
| --- | --- | --- | --- |
|
||||
| 20 | `20_多智能体架构.ipynb` | 多智能体架构 | 多智能体协作工作流 |
|
||||
| 21 | `21_对话代理.ipynb` | 对话代理 | 支持上下文记忆的对话机器人 |
|
||||
| 22 | `22_任务调度.ipynb` | 任务调度 | 自动化任务调度智能体 |
|
||||
| 23 | `23_人机协作.ipynb` | 人机协作 | 带人工审批环节的智能体工作流 |
|
||||
|
||||
本模块从单智能体流程扩展到多智能体协作,学习监督者模式、流水线模式、辩论模式、对话代理、任务拆解、任务调度、人机确认与审批流程。
|
||||
|
||||
### 模块八:综合项目实战
|
||||
|
||||
| 序号 | 课件 | 项目 | 核心产出 |
|
||||
| --- | --- | --- | --- |
|
||||
| 24 | `24_项目实战一.ipynb` | 个人知识库问答助手 | 知识库问答助手 MVP |
|
||||
| 25 | `25_项目实战二.ipynb` | 多工具协作型智能体 | 多工具智能体 |
|
||||
| 26 | `26_项目实战三.ipynb` | 带交互界面的学习任务助手 | 可交互智能体项目 |
|
||||
|
||||
三个项目形成如下递进:
|
||||
|
||||
1. **项目一:个人知识库问答助手**
|
||||
聚焦 RAG 思想,完成文档准备、检索、答案生成和参考资料返回。
|
||||
|
||||
2. **项目二:多工具协作型智能体**
|
||||
聚焦工具调用,完成搜索、计算、文件读取、工具选择、工具执行和结果汇总。
|
||||
|
||||
3. **项目三:带交互界面的学习任务助手**
|
||||
聚焦项目封装和交互体验,完成意图识别、条件路由、历史记录和命令行交互。
|
||||
|
||||
后续可将项目三进一步升级为综合收官项目:整合 RAG、多工具、记忆、条件路由、CLI / Web 交互和自动评估用例。
|
||||
|
||||
### 模块九:部署、评估与课程总结
|
||||
|
||||
| 序号 | 课件 | 主题 | 核心产出 |
|
||||
| --- | --- | --- | --- |
|
||||
| 27 | `27_部署与评估.ipynb` | 部署与评估 | 智能体上线检查与评估流程 |
|
||||
| 28 | `28_课程总结.ipynb` | 课程总结 | 后续学习路线与完整知识框架 |
|
||||
|
||||
本模块帮助学习者理解智能体应用如何从 Notebook Demo 走向工程交付,包括函数封装、命令行运行、API 服务、Docker、云部署、评估集、日志、追踪和上线检查清单。
|
||||
|
||||
## 实践项目列表
|
||||
|
||||
| 项目 | 对应课件 | 项目类型 | 关键能力 |
|
||||
| --- | --- | --- | --- |
|
||||
| 第一个大模型 API 对话程序 | `03_大模型API调用.ipynb` | API 入门 | 环境变量、单轮/多轮/流式调用 |
|
||||
| 第一个 LangChain 程序 | `04_LangChain概述.ipynb` | 框架入门 | ChatOpenAI、Prompt、LCEL |
|
||||
| 结构化输出链 | `07_输出解析器.ipynb` | 输出解析 | JSON、Pydantic、格式约束 |
|
||||
| 智能客服工单处理链 | `08_链式组合.ipynb` | 链式组合 | 分类、摘要、并行分析、回复建议 |
|
||||
| 多功能工具集合 | `09_工具定义.ipynb` | 工具定义 | 计算、查询、文件处理工具 |
|
||||
| 工具调用型助手 | `10_工具调用.ipynb` | 工具智能体 | 工具选择、工具执行、结果汇总 |
|
||||
| 私有文档问答系统 | `11_检索增强.ipynb` | RAG 入门 | 文档加载、切分、检索、生成 |
|
||||
| ChromaDB 知识库 | `12_向量数据库.ipynb` | 向量数据库 | 持久化、相似度检索、元数据过滤 |
|
||||
| 多文档 RAG 系统 | `13_RAG构建.ipynb` | 完整 RAG | 多文档加载、检索评估、问答生成 |
|
||||
| 多智能体协作流程 | `20_多智能体架构.ipynb` | 多智能体 | 监督者、流水线、辩论、协作 |
|
||||
| 对话代理机器人 | `21_对话代理.ipynb` | 对话智能体 | messages、检查点、多轮记忆 |
|
||||
| 任务调度智能体 | `22_任务调度.ipynb` | 工作流智能体 | 任务拆解、执行、汇总 |
|
||||
| 人工审批工作流 | `23_人机协作.ipynb` | Human-in-the-loop | interrupt、resume、人工确认 |
|
||||
| 个人知识库问答助手 | `24_项目实战一.ipynb` | 综合项目一 | RAG、检索、回答、依据展示 |
|
||||
| 多工具协作型智能体 | `25_项目实战二.ipynb` | 综合项目二 | 搜索、计算、文件读取、工具选择 |
|
||||
| 学习任务助手 | `26_项目实战三.ipynb` | 综合项目三 | 意图识别、条件路由、历史记录、交互 |
|
||||
| 智能体部署与评估流程 | `27_部署与评估.ipynb` | 工程化项目 | API、评估集、日志、追踪、上线检查 |
|
||||
|
||||
## 课程优化说明
|
||||
|
||||
本课程已按以下原则进行优化:
|
||||
|
||||
- 统一课程主线模型:LLM 使用 `qwen3.6-35b-A3b`,Embedding 使用 `qwen3-embedding`;
|
||||
- 清理 Notebook 已保存运行输出,避免残留报错、API Key 前缀和本地运行结果干扰学习;
|
||||
- 补齐 `requirements.txt` 中课程实际使用的关键依赖;
|
||||
- 将 Claude / Anthropic 调用作为扩展阅读,不作为课程主线默认运行内容;
|
||||
- 修正项目实战中的模型注释示例,避免出现与课程默认模型不一致的示例;
|
||||
- README 课程目录与实际项目内容保持一致。
|
||||
|
||||
## 学习建议
|
||||
|
||||
1. 先完成 01-02 环境配置,确保 `.venv` 和 Jupyter Kernel 正确;
|
||||
2. 学习 03 时重点确认 `.env` 中的 API 配置是否可用;
|
||||
3. 学习 05-08 时重点掌握 Prompt、链、解析器和 Runnable 的组合方式;
|
||||
4. 学习 09-10 时重点理解“工具让模型获得外部能力”;
|
||||
5. 学习 11-13 时重点理解 RAG 的每个环节,而不是只运行最终代码;
|
||||
6. 学习 14-19 时建议画流程图辅助理解 LangGraph 的状态、节点和边;
|
||||
7. 学习 20-23 时关注单智能体到多智能体、人机协作的演进;
|
||||
8. 学习 24-26 时建议主动扩展项目数据和功能,形成自己的项目作品;
|
||||
9. 学习 27-28 时重点整理项目部署、评估、日志和后续学习路线。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 每份课件都是独立的 `.ipynb` 文件,建议按顺序学习;
|
||||
- 运行课件前请确保虚拟环境已激活,并且 Jupyter 内核选择为 `.venv`;
|
||||
- 涉及大模型 API 的课件需要正确配置 `OPENAI_BASE_URL` 和 `OPENAI_API_KEY`;
|
||||
- 涉及 RAG 和向量数据库的课件默认使用 `qwen3-embedding`;
|
||||
- 如果 API 服务不支持工具调用、流式输出或指定模型,请根据服务商文档调整对应参数;
|
||||
- 项目实战中的部分案例为了教学稳定性使用规则模拟,学习者可在掌握流程后替换为真实 API 或真实数据源。
|
||||
|
||||
## 后续拓展方向
|
||||
|
||||
完成本课程后,可以继续学习:
|
||||
|
||||
- 更复杂的 ReAct Agent 和规划型 Agent;
|
||||
- 多模态智能体;
|
||||
- 企业知识库 RAG 优化;
|
||||
- Agent 评估体系与自动化测试;
|
||||
- LangGraph 生产级工作流;
|
||||
- MCP 工具生态;
|
||||
- Streamlit / Gradio / FastAPI 智能体 Web 应用;
|
||||
- 智能体权限控制、审计、安全与成本优化。
|
||||
Reference in New Issue
Block a user