文章目录
- [1. 子图(Subgraphs)](#1. 子图(Subgraphs))
-
- [1.1 什么是子图?](#1.1 什么是子图?)
- [1.2 使用子图的两种方式](#1.2 使用子图的两种方式)
-
- [1.2.1 方式一:从节点调用子图(不同状态模式)](#1.2.1 方式一:从节点调用子图(不同状态模式))
- [1.2.2 方式二:将子图作为节点(共享状态模式)](#1.2.2 方式二:将子图作为节点(共享状态模式))
- [1.3 为子图添加短期记忆](#1.3 为子图添加短期记忆)
- [1.4 在子图中使用中断](#1.4 在子图中使用中断)
-
- [1.4.1 基本用法](#1.4.1 基本用法)
- [1.4.2 恢复时的注意事项](#1.4.2 恢复时的注意事项)
-
- [1.4.2.1 将子图作为节点时](#1.4.2.1 将子图作为节点时)
- [1.4.2.2 节点内调用子图时](#1.4.2.2 节点内调用子图时)
- [2. 综合案例](#2. 综合案例)
-
- [1. 涉及知识点](#1. 涉及知识点)
- [2. 搭建一个LangGraph项目](#2. 搭建一个LangGraph项目)
-
- [2.1 搭建项目](#2.1 搭建项目)
-
- [2.1.1 步骤1:安装LangGraph CLI](#2.1.1 步骤1:安装LangGraph CLI)
- [2.1.2 步骤2:创建应用](#2.1.2 步骤2:创建应用)
- [2.1.3 步骤3:安装依赖](#2.1.3 步骤3:安装依赖)
- [2.1.4 步骤4:配置环境](#2.1.4 步骤4:配置环境)
- [2.1.5 步骤5:查看并启动服务器](#2.1.5 步骤5:查看并启动服务器)
- [2.1.6 步骤6:在 Studio 中测试](#2.1.6 步骤6:在 Studio 中测试)
- [2.1.7 步骤7:测试 API](#2.1.7 步骤7:测试 API)
- [2.2 项目结构说明](#2.2 项目结构说明)
- [2.3 pyproject.toml 配置文件](#2.3 pyproject.toml 配置文件)
-
- [1. project - 项目基本信息](#1. [project] - 项目基本信息)
- [2. project.dependencies - 生产环境依赖](#2. [project.dependencies] - 生产环境依赖)
- [3. project.optional-dependencies - 可选依赖](#3. [project.optional-dependencies] - 可选依赖)
- [4. build-system - 构建系统配置](#4. [build-system] - 构建系统配置)
- [5. tool.setuptools - 包目录结构配置](#5. [tool.setuptools] - 包目录结构配置)
- [6. tool.ruff - Ruff代码检查器配置](#6. [tool.ruff] - Ruff代码检查器配置)
- [7. dependency-groups - 依赖组(开发依赖)](#7. [dependency-groups] - 依赖组(开发依赖))
- [2.4 langgraph.json 配置文件](#2.4 langgraph.json 配置文件)
- [3. 思路](#3. 思路)
-
- [3.1 五步构建](#3.1 五步构建)
- [3.2 按功能模块拆分为多图](#3.2 按功能模块拆分为多图)
- [3.3 主图-智能分流](#3.3 主图-智能分流)
- [3.4 推荐子图--构建自定义 SQL 代理](#3.4 推荐子图--构建自定义 SQL 代理)
- [3.5 预定子图--人工介入的预定系统](#3.5 预定子图--人工介入的预定系统)
- [3.6 扩展子图--除业务外的智能问答助手](#3.6 扩展子图--除业务外的智能问答助手)
- [4. 案例开发](#4. 案例开发)
-
- [4.1 通用设计](#4.1 通用设计)
-
- [4.1.1 LLM](#4.1.1 LLM)
- [4.1.2 持久化存储](#4.1.2 持久化存储)
- [4.1.3 运行时上下文](#4.1.3 运行时上下文)
- [4.2 主图-智能分流](#4.2 主图-智能分流)
-
- [4.2.1 状态定义](#4.2.1 状态定义)
- [4.2.2 工作流定义](#4.2.2 工作流定义)
- [4.2.3 节点实现](#4.2.3 节点实现)
- [4.3 推荐子图--构建自定义 SQL 代理](#4.3 推荐子图--构建自定义 SQL 代理)
-
- [4.3.1 状态定义](#4.3.1 状态定义)
- [4.3.2 工作流定义](#4.3.2 工作流定义)
- [4.3.3 节点实现](#4.3.3 节点实现)
-
- [4.3.3.1 collect_user_info节点](#4.3.3.1 collect_user_info节点)
- [4.3.3.2 get_schema节点和run_query节点](#4.3.3.2 get_schema节点和run_query节点)
-
- [4.3.3.2.1 SQLDatabase Toolkit工具包](#4.3.3.2.1 SQLDatabase Toolkit工具包)
- [4.3.3.2.2 接入SQL工具包](#4.3.3.2.2 接入SQL工具包)
- [4.3.3.3 list_tables节点](#4.3.3.3 list_tables节点)
- [4.3.3.4 call_get_schema节点](#4.3.3.4 call_get_schema节点)
- [4.3.3.5 generate_query节点](#4.3.3.5 generate_query节点)
- [4.3.3.6 check_query节点](#4.3.3.6 check_query节点)
- [4.4 预定子图--人工介入的预定系统](#4.4 预定子图--人工介入的预定系统)
-
- [4.4.1 状态定义](#4.4.1 状态定义)
- [4.4.2 工作流定义](#4.4.2 工作流定义)
- [4.4.3 节点实现](#4.4.3 节点实现)
- [4.5 扩展子图--除业务外的智能问答助手](#4.5 扩展子图--除业务外的智能问答助手)
- [5. 项目部署](#5. 项目部署)
-
- [5.1 自托管部署 & 涉及到的相关组件解释](#5.1 自托管部署 & 涉及到的相关组件解释)
- [5.2 本地启动并测试](#5.2 本地启动并测试)
- [5.3 LangSmith 部署方式](#5.3 LangSmith 部署方式)
- [5.4 独立部署 Agent Server](#5.4 独立部署 Agent Server)
-
- [5.4.1 工作流程](#5.4.1 工作流程)
- [5.4.2 部署前的准备工作](#5.4.2 部署前的准备工作)
-
- [使用 Docker 启动一个 Redis 容器](#使用 Docker 启动一个 Redis 容器)
- [使用 Docker 快速安装并启动 postgres](#使用 Docker 快速安装并启动 postgres)
- [5.4.3 部署姿势 1:使用 LangGraph CLI 构建 Docker 镜像并运行](#5.4.3 部署姿势 1:使用 LangGraph CLI 构建 Docker 镜像并运行)
- [5.4.4 部署姿势 2:生成 Dockerfile 并结合 Docker Compose 启动](#5.4.4 部署姿势 2:生成 Dockerfile 并结合 Docker Compose 启动)
-
- [第一步:生成 Dockerfile](#第一步:生成 Dockerfile)
- [第二步:结合 Docker compose 启动](#第二步:结合 Docker compose 启动)
-
- [参考 1:Redis、PostgreSQL、MySQL 均由外部提供](#参考 1:Redis、PostgreSQL、MySQL 均由外部提供)
- [参考 2:使用本项目内置的完整 Compose](#参考 2:使用本项目内置的完整 Compose)
- [5.4.5 部署姿势 3:使用 langgraph up 在 Data Plane 中运行](#5.4.5 部署姿势 3:使用 langgraph up 在 Data Plane 中运行)
1. 子图(Subgraphs)
1.1 什么是子图?
在LangGraph中,子图是另一个图中的一个节点,可以独立开发和测试,也可以被多个主图复用。子图可用于:
- 模块化开发:不同团队可以独立开发不同部分
- 代码复用:相同逻辑的图只需开发一次
1.2 使用子图的两种方式
1.2.1 方式一:从节点调用子图(不同状态模式)
这种方式是从一个图(如主图)的节点内部调用另一个图(如子图)。因此其使用特点是:子图和主图的状态结构可以完全不同。
#mermaid-svg-AoZPSpNk473HXzIu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AoZPSpNk473HXzIu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AoZPSpNk473HXzIu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AoZPSpNk473HXzIu .error-icon{fill:#552222;}#mermaid-svg-AoZPSpNk473HXzIu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AoZPSpNk473HXzIu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AoZPSpNk473HXzIu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AoZPSpNk473HXzIu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AoZPSpNk473HXzIu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AoZPSpNk473HXzIu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AoZPSpNk473HXzIu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AoZPSpNk473HXzIu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AoZPSpNk473HXzIu .marker.cross{stroke:#333333;}#mermaid-svg-AoZPSpNk473HXzIu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AoZPSpNk473HXzIu p{margin:0;}#mermaid-svg-AoZPSpNk473HXzIu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AoZPSpNk473HXzIu .cluster-label text{fill:#333;}#mermaid-svg-AoZPSpNk473HXzIu .cluster-label span{color:#333;}#mermaid-svg-AoZPSpNk473HXzIu .cluster-label span p{background-color:transparent;}#mermaid-svg-AoZPSpNk473HXzIu .label text,#mermaid-svg-AoZPSpNk473HXzIu span{fill:#333;color:#333;}#mermaid-svg-AoZPSpNk473HXzIu .node rect,#mermaid-svg-AoZPSpNk473HXzIu .node circle,#mermaid-svg-AoZPSpNk473HXzIu .node ellipse,#mermaid-svg-AoZPSpNk473HXzIu .node polygon,#mermaid-svg-AoZPSpNk473HXzIu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AoZPSpNk473HXzIu .rough-node .label text,#mermaid-svg-AoZPSpNk473HXzIu .node .label text,#mermaid-svg-AoZPSpNk473HXzIu .image-shape .label,#mermaid-svg-AoZPSpNk473HXzIu .icon-shape .label{text-anchor:middle;}#mermaid-svg-AoZPSpNk473HXzIu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AoZPSpNk473HXzIu .rough-node .label,#mermaid-svg-AoZPSpNk473HXzIu .node .label,#mermaid-svg-AoZPSpNk473HXzIu .image-shape .label,#mermaid-svg-AoZPSpNk473HXzIu .icon-shape .label{text-align:center;}#mermaid-svg-AoZPSpNk473HXzIu .node.clickable{cursor:pointer;}#mermaid-svg-AoZPSpNk473HXzIu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AoZPSpNk473HXzIu .arrowheadPath{fill:#333333;}#mermaid-svg-AoZPSpNk473HXzIu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AoZPSpNk473HXzIu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AoZPSpNk473HXzIu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AoZPSpNk473HXzIu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AoZPSpNk473HXzIu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AoZPSpNk473HXzIu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AoZPSpNk473HXzIu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AoZPSpNk473HXzIu .cluster text{fill:#333;}#mermaid-svg-AoZPSpNk473HXzIu .cluster span{color:#333;}#mermaid-svg-AoZPSpNk473HXzIu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AoZPSpNk473HXzIu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AoZPSpNk473HXzIu rect.text{fill:none;stroke-width:0;}#mermaid-svg-AoZPSpNk473HXzIu .icon-shape,#mermaid-svg-AoZPSpNk473HXzIu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AoZPSpNk473HXzIu .icon-shape p,#mermaid-svg-AoZPSpNk473HXzIu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AoZPSpNk473HXzIu .icon-shape .label rect,#mermaid-svg-AoZPSpNk473HXzIu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AoZPSpNk473HXzIu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AoZPSpNk473HXzIu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AoZPSpNk473HXzIu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} node_2节点内调用
start
node_1
sub_node_1
sub_node_2
end
代码块
python
# 子图的几种调用方式
from typing import TypedDict
from langgraph.graph import END, START, StateGraph
# 子图
class State(TypedDict):
sub_1: str
sub_2: str
def sub_node_1(state: State):
"""节点1"""
return {"sub_1": "pass node 1"}
def sub_node_2(state: State):
"""节点2"""
return {"sub_2": "pass node 2"}
sub_builder = StateGraph(State)
sub_builder.add_node("sub_node_1", sub_node_1)
sub_builder.add_node("sub_node_2", sub_node_2)
sub_builder.add_edge(START, "sub_node_1")
sub_builder.add_edge("sub_node_1", "sub_node_2")
sub_builder.add_edge("sub_node_2", END)
sub_graph = sub_builder.compile()
# 主图
class ParentState(TypedDict):
parent: str # 输入
def node_1(state: ParentState):
return {"parent": "111" + state["parent"]}
def node_2(state: ParentState):
"""更新parent,替换为子图中的sub_2参数值"""
result = sub_graph.invoke({}) # 直接执行子图,拿到结果
return {"parent": "222" + result["sub_2"]}
builder = StateGraph(ParentState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
graph = builder.compile()
for chunk in graph.stream(input={"parent": "000"}, stream_mode="updates", subgraphs=True):
print(chunk)
结果:
((), {'node_1': {'parent': '111000'}})
(('node_2:80961f18-8efd-ceef-c025-896b6b77b822',), {'sub_node_1': {'sub_1': 'pass node 1'}})
(('node_2:80961f18-8efd-ceef-c025-896b6b77b822',), {'sub_node_2': {'sub_2': 'pass node 2'}})
((), {'node_2': {'parent': '222pass node 2'}})
扩展:定义主图、子图、孙子图进行调用。
上述代码中,要在流式输出中包含子图的输出,可以在父图的 .stream() 方法中设置 subgraphs=True。这将从父图和任何子图流式传输输出。
1.2.2 方式二:将子图作为节点(共享状态模式)
这种方式可以将图添加为另一个图中的节点,如下所示:
python
# 子图和主图使用相同的状态结构
子图 = 创建子图()
# 直接把子图作为节点加入主图
主图.add_node("子图节点", 子图)
其特点是:子图和主图共享部分状态。
#mermaid-svg-L5mxrHy7cnC6Goy7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-L5mxrHy7cnC6Goy7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-L5mxrHy7cnC6Goy7 .error-icon{fill:#552222;}#mermaid-svg-L5mxrHy7cnC6Goy7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-L5mxrHy7cnC6Goy7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-L5mxrHy7cnC6Goy7 .marker.cross{stroke:#333333;}#mermaid-svg-L5mxrHy7cnC6Goy7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-L5mxrHy7cnC6Goy7 p{margin:0;}#mermaid-svg-L5mxrHy7cnC6Goy7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-L5mxrHy7cnC6Goy7 .cluster-label text{fill:#333;}#mermaid-svg-L5mxrHy7cnC6Goy7 .cluster-label span{color:#333;}#mermaid-svg-L5mxrHy7cnC6Goy7 .cluster-label span p{background-color:transparent;}#mermaid-svg-L5mxrHy7cnC6Goy7 .label text,#mermaid-svg-L5mxrHy7cnC6Goy7 span{fill:#333;color:#333;}#mermaid-svg-L5mxrHy7cnC6Goy7 .node rect,#mermaid-svg-L5mxrHy7cnC6Goy7 .node circle,#mermaid-svg-L5mxrHy7cnC6Goy7 .node ellipse,#mermaid-svg-L5mxrHy7cnC6Goy7 .node polygon,#mermaid-svg-L5mxrHy7cnC6Goy7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-L5mxrHy7cnC6Goy7 .rough-node .label text,#mermaid-svg-L5mxrHy7cnC6Goy7 .node .label text,#mermaid-svg-L5mxrHy7cnC6Goy7 .image-shape .label,#mermaid-svg-L5mxrHy7cnC6Goy7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-L5mxrHy7cnC6Goy7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-L5mxrHy7cnC6Goy7 .rough-node .label,#mermaid-svg-L5mxrHy7cnC6Goy7 .node .label,#mermaid-svg-L5mxrHy7cnC6Goy7 .image-shape .label,#mermaid-svg-L5mxrHy7cnC6Goy7 .icon-shape .label{text-align:center;}#mermaid-svg-L5mxrHy7cnC6Goy7 .node.clickable{cursor:pointer;}#mermaid-svg-L5mxrHy7cnC6Goy7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-L5mxrHy7cnC6Goy7 .arrowheadPath{fill:#333333;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-L5mxrHy7cnC6Goy7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-L5mxrHy7cnC6Goy7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-L5mxrHy7cnC6Goy7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-L5mxrHy7cnC6Goy7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-L5mxrHy7cnC6Goy7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-L5mxrHy7cnC6Goy7 .cluster text{fill:#333;}#mermaid-svg-L5mxrHy7cnC6Goy7 .cluster span{color:#333;}#mermaid-svg-L5mxrHy7cnC6Goy7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-L5mxrHy7cnC6Goy7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-L5mxrHy7cnC6Goy7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-L5mxrHy7cnC6Goy7 .icon-shape,#mermaid-svg-L5mxrHy7cnC6Goy7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-L5mxrHy7cnC6Goy7 .icon-shape p,#mermaid-svg-L5mxrHy7cnC6Goy7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-L5mxrHy7cnC6Goy7 .icon-shape .label rect,#mermaid-svg-L5mxrHy7cnC6Goy7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-L5mxrHy7cnC6Goy7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-L5mxrHy7cnC6Goy7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-L5mxrHy7cnC6Goy7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 初始状态
foo: bar
Sub Graph 1
Sub Graph 2
共享状态 foo:bar
私有状态 hello:world
共享状态 foo:bar
私有状态 fizz:buzz
代码块
python
class Substate(TypedDict):
sub: str # sub私有
parent: str # 可以与主图进行共享
def sub_node_1(state: Substate):
return {"sub": "pass node 1"}
def sub_node_2(state: Substate):
return {"sub": "pass node 2","parent":state["parent"]+"pass node 2"}
sub_builder = StateGraph(Substate)
sub_builder.add_node("sub_node_1", sub_node_1)
sub_builder.add_node("sub_node_2", sub_node_2)
sub_builder.add_edge(START, "sub_node_1")
sub_builder.add_edge("sub_node_1", "sub_node_2")
sub_builder.add_edge("sub_node_2", END)
sub_graph = sub_builder.compile()
# 主图
class State(TypedDict):
parent: str
def node_1(state: State):
return {"parent": "111" + state["parent"]}
builder=StateGraph(State)
builder.add_node("node_1", node_1)
builder.add_node("node_2",sub_graph) # 直接把子图作为节点添加到主图中
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
graph = builder.compile()
for chunk in graph.stream(input={"parent": "111"}):
print(chunk)
输出结果:
{'node_1': {'parent': '111111'}}
{'node_2': {'parent': '111111pass node 2'}}
1.3 为子图添加短期记忆
如果图包含子图(将子图作为节点直接添加到主图) ,则只需在编译父图时提供 checkpoint。LangGraph 会自动将 checkpoint 传播到子图。
代码块
python
from langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import InMemorySaver
from typing import TypedDict
class State(TypedDict):
foo: str
# 子图
def subgraph_node_1(state: State):
return {"foo": state["foo"] + "bar"}
subgraph_builder = StateGraph(State)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_edge(START, "subgraph_node_1")
subgraph = subgraph_builder.compile()
# 主图
builder = StateGraph(State)
builder.add_node("node_1", subgraph)
builder.add_edge(START, "node_1")
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
1.4 在子图中使用中断
1.4.1 基本用法
在子图中,同样可以使用中断。且添加短期记忆后,可以检查图状态(检查点)。但要注意,只有当子图中断时,才能查看子图状态;恢复后,将无法访问子图形状态。例如:
代码块
python
from langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command
from typing_extensions import TypedDict
class State(TypedDict):
foo: str
# 子图
def subgraph_node_1(state: State):
print("sub_node_1")
return {}
def subgraph_node_2(state: State):
print("sub_node_2")
value = interrupt("输入值:")
return {"foo": state["foo"] + value}
subgraph_builder = StateGraph(State)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_node(subgraph_node_2)
subgraph_builder.add_edge(START, "subgraph_node_1")
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")
subgraph = subgraph_builder.compile()
# 主图
builder = StateGraph(State)
builder.add_node("node_1", subgraph)
builder.add_edge(START, "node_1")
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"foo": ""}, config)
parent_state = graph.get_state(config)
# 访问子图状态只能在子图被中断时才可用。
# 一旦恢复了图,将无法访问子图状态。
subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state
print(subgraph_state)
print(graph.invoke(Command(resume="bar"), config))
结果如下:
sub_node_1
sub_node_2
StateSnapshot(
values={'foo': ''},
next=('subgraph_node_2',),
config={...},
metadata={...},
parent_config={...},
tasks=(
PregelTask(
id='4afcaec2-1fd1-7bfc-cab9-dcd3f1b6980d',
name='subgraph_node_2',
path=('__pregel_pull', 'subgraph_node_2'),
error=None,
interrupts=(
Interrupt(
value='输入值:',
id='f62f6bce645a53af213c432eee562e49'
),
),
state=None,
result=None
),
),
)
interrupts=(Interrupt(value='输入值:', id='f62f6bce645a53af213c432eee562e49'),)
sub_node_2
{'foo': 'bar'}
1.4.2 恢复时的注意事项
之前讲过,当节点恢复执行时,发起中断的节点会从头再跑一遍。因此,对于中断前的代码,会多重复执行!
#mermaid-svg-V6XtPkFsNB2xBqUW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V6XtPkFsNB2xBqUW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V6XtPkFsNB2xBqUW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V6XtPkFsNB2xBqUW .error-icon{fill:#552222;}#mermaid-svg-V6XtPkFsNB2xBqUW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V6XtPkFsNB2xBqUW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V6XtPkFsNB2xBqUW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V6XtPkFsNB2xBqUW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V6XtPkFsNB2xBqUW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V6XtPkFsNB2xBqUW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V6XtPkFsNB2xBqUW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V6XtPkFsNB2xBqUW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V6XtPkFsNB2xBqUW .marker.cross{stroke:#333333;}#mermaid-svg-V6XtPkFsNB2xBqUW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V6XtPkFsNB2xBqUW p{margin:0;}#mermaid-svg-V6XtPkFsNB2xBqUW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-V6XtPkFsNB2xBqUW .cluster-label text{fill:#333;}#mermaid-svg-V6XtPkFsNB2xBqUW .cluster-label span{color:#333;}#mermaid-svg-V6XtPkFsNB2xBqUW .cluster-label span p{background-color:transparent;}#mermaid-svg-V6XtPkFsNB2xBqUW .label text,#mermaid-svg-V6XtPkFsNB2xBqUW span{fill:#333;color:#333;}#mermaid-svg-V6XtPkFsNB2xBqUW .node rect,#mermaid-svg-V6XtPkFsNB2xBqUW .node circle,#mermaid-svg-V6XtPkFsNB2xBqUW .node ellipse,#mermaid-svg-V6XtPkFsNB2xBqUW .node polygon,#mermaid-svg-V6XtPkFsNB2xBqUW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-V6XtPkFsNB2xBqUW .rough-node .label text,#mermaid-svg-V6XtPkFsNB2xBqUW .node .label text,#mermaid-svg-V6XtPkFsNB2xBqUW .image-shape .label,#mermaid-svg-V6XtPkFsNB2xBqUW .icon-shape .label{text-anchor:middle;}#mermaid-svg-V6XtPkFsNB2xBqUW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-V6XtPkFsNB2xBqUW .rough-node .label,#mermaid-svg-V6XtPkFsNB2xBqUW .node .label,#mermaid-svg-V6XtPkFsNB2xBqUW .image-shape .label,#mermaid-svg-V6XtPkFsNB2xBqUW .icon-shape .label{text-align:center;}#mermaid-svg-V6XtPkFsNB2xBqUW .node.clickable{cursor:pointer;}#mermaid-svg-V6XtPkFsNB2xBqUW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-V6XtPkFsNB2xBqUW .arrowheadPath{fill:#333333;}#mermaid-svg-V6XtPkFsNB2xBqUW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-V6XtPkFsNB2xBqUW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-V6XtPkFsNB2xBqUW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V6XtPkFsNB2xBqUW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-V6XtPkFsNB2xBqUW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V6XtPkFsNB2xBqUW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-V6XtPkFsNB2xBqUW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-V6XtPkFsNB2xBqUW .cluster text{fill:#333;}#mermaid-svg-V6XtPkFsNB2xBqUW .cluster span{color:#333;}#mermaid-svg-V6XtPkFsNB2xBqUW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-V6XtPkFsNB2xBqUW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-V6XtPkFsNB2xBqUW rect.text{fill:none;stroke-width:0;}#mermaid-svg-V6XtPkFsNB2xBqUW .icon-shape,#mermaid-svg-V6XtPkFsNB2xBqUW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V6XtPkFsNB2xBqUW .icon-shape p,#mermaid-svg-V6XtPkFsNB2xBqUW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-V6XtPkFsNB2xBqUW .icon-shape .label rect,#mermaid-svg-V6XtPkFsNB2xBqUW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V6XtPkFsNB2xBqUW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-V6XtPkFsNB2xBqUW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-V6XtPkFsNB2xBqUW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Command 恢复执行
节点1
节点2
第1行代码
第2行代码
第3行代码
...
执行 interrupt()
...
第n行代码
节点3
用户介入
而在子图场景下,子图的不同调用方式有不同的执行结果。
1.4.2.1 将子图作为节点时
代码如下:
python
class State(TypedDict):
foo: str
# 子图
def subgraph_node_1(state: State):
print("sub_node_1")
return {}
def subgraph_node_2(state: State):
print("sub_node_2")
value = interrupt("输入值:")
return {"foo": state["foo"] + value}
subgraph_builder = StateGraph(State)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_node(subgraph_node_2)
subgraph_builder.add_edge(START, "subgraph_node_1")
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")
subgraph_builder.add_edge("subgraph_node_2", END)
subgraph = subgraph_builder.compile()
# 主图1
builder = StateGraph(State)
builder.add_node("node_1", subgraph)
builder.add_edge(START, "node_1")
# 将子图直接当作节点添加,可以使用checkpoint和store
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"foo":""}, config)
parent_state = graph.get_state(config)
# 访问子图状态只能在子图被中断时才可用。
# 一旦恢复了图,将无法访问子图状态。
subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state
print(subgraph_state)
print(graph.invoke(Command(resume="bar"), config))
sub_node_1
sub_node_2
StateSnapshot(values={'foo': ''}, next=('subgraph_node_2',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': 'node_1:25fffd09-6b9c-f290-f16a-051ff7140e38', 'checkpoint_id': '1f185972-3314-67d6-8001-c77726b48c25', 'checkpoint_map': {'': '1f185972-3313-60b6-8000-885c9b0c3443', 'node_1:25fffd09-6b9c-f290-f16a-051ff7140e38': '1f185972-3314-67d6-8001-c77726b48c25'}}}, metadata={'source': 'loop', 'step': 1, 'parents': {'': '1f185972-3313-60b6-8000-885c9b0c3443'}}, created_at='2026-07-22T06:32:40.218412+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': 'node_1:25fffd09-6b9c-f290-f16a-051ff7140e38', 'checkpoint_id': '1f185972-3314-6164-8000-9550e27ce789', 'checkpoint_map': {'': '1f185972-3313-60b6-8000-885c9b0c3443', 'node_1:25fffd09-6b9c-f290-f16a-051ff7140e38': '1f185972-3314-6164-8000-9550e27ce789'}}}, tasks=(PregelTask(id='3f85038f-6a3c-c922-8fe5-20597b5bde2b', name='subgraph_node_2', path=('__pregel_pull', 'subgraph_node_2'), error=None, interrupts=(Interrupt(value='输入值:', id='5d03cddedf34a079935bd64b77b37696'),), state=None, result=None),), interrupts=(Interrupt(value='输入值:', id='5d03cddedf34a079935bd64b77b37696'),))
sub_node_2
{'foo': 'bar'}
由于是 subgraph_node_2 节点发起中断调用,可以看到 subgraph_node_2 节点被调用两次,符合预期。
1.4.2.2 节点内调用子图时
但当是节点内调用子图时:
- 父图将从调用子图并触发中断的节点的开头恢复执行。
- 同样,子图也将从调用中断的节点的开头恢复。
修改代码:
python
from langgraph.graph import START, StateGraph
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command
from typing_extensions import TypedDict
class State(TypedDict):
foo: str
# 子图
def subgraph_node_1(state: State):
print("sub_node_1")
return {}
def subgraph_node_2(state: State):
print("sub_node_2")
value = interrupt("输入值:")
return {"foo": state["foo"] + value}
subgraph_builder = StateGraph(State)
subgraph_builder.add_node(subgraph_node_1)
subgraph_builder.add_node(subgraph_node_2)
subgraph_builder.add_edge(START, "subgraph_node_1")
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")
subgraph_builder.add_edge("subgraph_node_2", END)
subgraph = subgraph_builder.compile()
# 主图1
# builder = StateGraph(State)
# builder.add_node("node_1", subgraph)
# builder.add_edge(START, "node_1")
# # 将子图直接当作节点添加,可以使用checkpoint和store
# graph = builder.compile(checkpointer=InMemorySaver())
# 主图2,如果是在主图的节点中调用子图,则即便存在相同的字段也互不影响
def node_1(state: State):
print("node_1")
# 调用子图后返回的结果是子图的最终state,和主图的foo字段没有关系
# 子图中断再恢复,调用子图节点也会再执行一次
result=subgraph.invoke({"foo":state["foo"]})
return {
"foo":result["foo"]
}
builder = StateGraph(State)
builder.add_node("node_1", node_1)
builder.add_edge(START, "node_1")
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "1"}}
graph.invoke({"foo":""}, config)
parent_state = graph.get_state(config)
# 访问子图状态只能在子图被中断时才可用。
# 一旦恢复了图,将无法访问子图状态。
subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state
print(subgraph_state)
print(graph.invoke(Command(resume="bar"), config))
打印结果:
node_1
sub_node_1
sub_node_2
StateSnapshot(values={'foo': ''}, next=('subgraph_node_2',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': 'node_1:b33f4f6d-06f2-7276-a064-54e6a5bd67ac', 'checkpoint_id': '1f185974-9294-63a8-8001-82b4c2b3719d', 'checkpoint_map': {'': '1f185974-9292-6bb6-8000-964a806f2c06', 'node_1:b33f4f6d-06f2-7276-a064-54e6a5bd67ac': '1f185974-9294-63a8-8001-82b4c2b3719d'}}}, metadata={'source': 'loop', 'step': 1, 'parents': {'': '1f185974-9292-6bb6-8000-964a806f2c06'}}, created_at='2026-07-22T06:33:43.919297+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': 'node_1:b33f4f6d-06f2-7276-a064-54e6a5bd67ac', 'checkpoint_id': '1f185974-9293-6d36-8000-42e33c22ce23', 'checkpoint_map': {'': '1f185974-9292-6bb6-8000-964a806f2c06', 'node_1:b33f4f6d-06f2-7276-a064-54e6a5bd67ac': '1f185974-9293-6d36-8000-42e33c22ce23'}}}, tasks=(PregelTask(id='e457c5d5-acef-c754-cf3b-640e9c8d2a5b', name='subgraph_node_2', path=('__pregel_pull', 'subgraph_node_2'), error=None, interrupts=(Interrupt(value='输入值:', id='fab161956600d5f6e1b609259a833f6a'),), state=None, result=None),), interrupts=(Interrupt(value='输入值:', id='fab161956600d5f6e1b609259a833f6a'),))
node_1
sub_node_2
{'foo': 'bar'}
2. 综合案例
这里来设计一个房地产房源信息查询Agent,实现:
- 房源推荐;
- 预定房源;
- 常规问答;
- 查询订单;
等四个主要功能。
1. 涉及知识点
-
对话式交互
-
LangGraph,包括状态、节点、边和条件边
-
子图与子图中断
-
线程级持久化与跨会话持久化
-
内存存储与Postgres存储
-
路由模式
-
读取SQL数据库的工具
-
人机交互(中断)
-
运行时上下文
-
多种流式混合输出
-
LangGraph项目结构
-
LangGraph生态部署
2. 搭建一个LangGraph项目
自行搭建Python服务,基于LangGraph 图调用(自己制定协议)提供对外服务。也可以使用LangChain生态中LangSmith Deployment部署应用程序,这需要我们准备一个结构化的项目。这里直接采用第二种方式进行部署。
LangSmith Deployment 构建在开源的 LangGraph 框架上,用于开发有状态的应用程序。LangGraph 提供核心抽象和执行模型,而 LangSmith 支持从开发到生产的整个生命周期,LangSmith 增加了托管基础设施、可观察性、部署选项、助手和并发控制等能力。
2.1 搭建项目
2.1.1 步骤1:安装LangGraph CLI
使用pip安装 langgraph-cli[inmem](需 Python 3.11 或更高版本)。
bash
pip install -U "langgraph-cli[inmem]"
# 验证安装情况
langgraph --help
LangGraph CLI 是用于构建和运行LangGraph应用程序的命令行工具。LangGraph CLI 提供了如下指令:
| 指令 | 解释 |
|---|---|
langgraph dev |
启动一个轻量级本地开发服务器(无需 Docker),非常适合快速测试。 |
langgraph build |
构建你的 LangGraph API 服务器的 Docker 镜像以便部署。 |
langgraph dockerfile |
它会根据你的配置导出一个 Docker 文件,用于自定义构建。 |
langgraph up |
在本地 Docker 启动 LangGraph API 服务器。需要运行 Docker;本地开发用 LangSmith API 密钥。 |
由此产生的服务器公开了:
- 助手(Assistants):是图的配置实例
- 线程(Threads):线程包含一组运行的累积输出。实际表示为一组会话。
- 线程执行(Thread Runs):是对线程上的图形/助手的调用。它更新线程的状态。
公开的所有 API 端点这里。可以看到也包括用于检查点和存储的托管数据库。
2.1.2 步骤2:创建应用
使用 langgraph new 命令从指定模板创建新应用。
方式1: 创建时不指定具体模板。而是通过交互式菜单,允许你从可用模板列表中选择。
bash
langgraph new
- 输入项目路径 :直接填
./test_tmp回车(创建test_tmp文件夹) - 选择模板序号 :输入
1回车(对应New LangGraph Project基础记忆聊天机器人模板) - 选择开发语言 :输入
1回车(1=Python,2=JS/TS) - 等待自动从GitHub下载模板、解压,终端输出
New project created at /Users/你的用户名/Desktop/test_tmp即创建完成
可以选择的模板有:
- New LangGraph 项目:一个简单、精简而且具有记忆功能的聊天机器人。
- ReAct 代理:一种简单且可灵活扩展至多种工具的代理。
- 记忆代理:一种采用ReAct风格的代理,额外配备了一个工具,用于存储记忆,以便在不同对话线程间使用。
- 检索代理:包含基于检索的问答系统的代理。
- 数据丰富代理:一种执行网络搜索并将搜索结果整理成结构化格式的代理。
方式2: 用 new-langgraph-project-python 模板创建一个新应用。这个模板展示了一个单节点应用,你可以用自己的逻辑进行扩展。
bash
langgraph new path/to/your/app --template new-langgraph-project-python
项目创建完成后,可以使用 PyCharm 打开项目。
plain
((.venv) ) drw@drwdeMacBook-Pro LangChain-LangGraph % langgraph new ./Project --template new-langgraph-project-python
📥 Attempting to download repository as a ZIP archive...
URL: https://github.com/langchain-ai/new-langgraph-project/archive/refs/heads/main.zip
✅ Downloaded and extracted repository to /Users/drw/PycharmProjects/LangChain-LangGraph/Project
🎉 New project created at /Users/drw/PycharmProjects/LangChain-LangGraph/Project
2.1.3 步骤3:安装依赖
进入应用目录,以编辑模式安装依赖。
bash
cd path/to/your/app
pip install -e .
环境准备可能遇到的问题:由于 Debian/Ubuntu 等系统从 Python 3.11 开始引入了新的保护机制,为了保护 Python 环境不被意外破坏,默认禁止直接使用 pip install 安装到系统 Python。
解决:
- 使用虚拟环境(推荐)
- 在 pip install 命令后添加
--break-system-packages参数强制覆盖系统保护(不推荐,急用可临时使用)
2.1.4 步骤4:配置环境
复制 .env.example 文件为 .env,并填入必要的 LANGSMITH_API_KEY。
env
LANGSMITH_PROJECT=new-agent
LANGSMITH_API_KEY=lsv2_.....
LANGSMITH_TRACING=true
OPENAI_API_KEY=...
2.1.5 步骤5:查看并启动服务器
可以看到,这个模板展示了一个单节点 Graph,可以用自己的逻辑进行扩展。

除此之外,要构建和运行一个有效的应用程序,LangGraph CLI 需要遵循一个 langgraph.json 配置文件。其中必须要配置 "graphs":从图 ID( agent )映射到已编译图( ./src/agent/graph.py:graph )路径。
运行 langgraph dev 命令启动本地开发服务器(输出中会提供 API 和 Studio UI 地址)。
langgraph dev 命令会以内存模式启动 Agent Server。该模式适合开发和测试。

2.1.6 步骤6:在 Studio 中测试
通过输出的 URL 在 LangGraph Studio 中可视化并调试你的应用。Studio 是一个图形界面,用于与你的 Agent Server 交互。它不会持久化任何私有数据(你发送到服务器的数据不会发送到 LangSmith)。虽然 Studio 接口在 smith.langchain.com 提供,但它运行在浏览器中,并直接连接到本地 Agent Server。

2.1.7 步骤7:测试 API
可以通过 Python SDK(异步/同步) 或直接发送 REST API 请求来测试应用功能。
- 对于 Python SDK,使用
pip install langgraph-sdk先进行安装,再通过其内置接口进行访问。 - 对于 REST API,接口文档已经由 LangGraph CLI 提供好了,点击 API Docs 即可查看。
由于完成一个 web 应用,案例将不采用 SDK 方式调用,而是选择 REST API 请求。它们的概念、使用流程都一样,只是使用姿势不一样,一通百通。
接下来参考 API Docs,使用 API Docs 提供的工具或 Apifox 进行测试。如下所示:
-
创建会话线程:

-
基于线程发起流式调用:

流式调用常见的 stream_mode 有:
| Mode 模式 | Description 描述 |
|---|---|
values |
在每个超级步骤后流式传输完整的图状态。 |
updates |
在图的每一步后,将更新状态。如果同一步进行多次更新(例如运行多个节点),这些更新会分别流式传输。 |
messages 或 messages-tuple |
流式传输 LLM 令牌和元数据,用于调用 LLM 的图节点(对聊天应用非常有用)。 |
custom |
从你的图内部流式传输自定义数据 |
events |
流式传输所有事件(包括图表状态);主要用于迁移大型 LCEL 应用。 |
也可以把列表作为 stream_mode 参数传递,同时流放多个模式(如 "stream_mode": ["updates", "messages"])。
这里还需要注意流式返回的结构,服务器会以 SSE 格式发送一系列事件:
event: metadata
data: {"run_id":"019ba0cd6-d7b7-6ce0-87b9-d28d2f0c756d","attempt":1}
id: 1767929403693-0
event: updates
data: {"call_model":{"changeme":"output from call_model. Configured with None!"}}
id: 1767929403699-0
event:该流部分的事件类型data:与事件相关的数据负载id:事件的ID
Event 与 data 的值与传入的 stream_mode 流模式有关,传了 "update",换成 "values" (用 API Docs 进行测试),如下所示:

常见返回的 event 有:
- 事件为
metadata,data 中会包含图节点和 LLM 调用细节及其他信息。 - 事件为
updates,data 中会包含每个节点更新的状态。 - 事件为
values,data 中会包含每个节点最新的状态。 - 事件为
messages,data 中会包含来自聊天模型调用的单个 LLM 令牌。
......
2.2 项目结构说明
要使用 LangSmith Deployment 部署应用程序,需要准备一个结构化的项目。一个可部署的 LangSmith 应用必须包含以下四个部分:
- 一个或多个图:承载应用程序的核心逻辑。
- 配置文件(
langgraph.json):定义应用的依赖项、图和环境变量。 - 依赖文件:声明项目所需的包或库(如
requirements.txt,pyproject.toml)。 - 环境变量文件(可选,
.env):存放环境变量配置信息。
项目文件结构示例:
my-app/
├── .env # 环境变量
├── langgraph.json # 配置文件
├── pyproject.toml # 管理项目元数据,指定依赖关系
└── src # src 下面是包名,包下面是业务代码,所有的项目代码都在这里
└──agent
├── graph.py # 构造 graph 的代码
└── __init__.py
2.3 pyproject.toml 配置文件
pyproject.toml 配置文件是现代 Python 项目的标准配置文件,替代了传统的 setup.py 和 requirements.txt 文件。主要用于:
- 项目元数据定义(名称、版本、作者等)
- 依赖管理(生产依赖和开发依赖)
- 构建系统配置
- 代码质量工具配置(如ruff代码检查器)
下面对于 pyproject.toml 配置信息进行说明:
1. project - 项目基本信息
toml
name = "agent" # 项目名称
version = "0.0.1" # 项目版本号
description = "Starter template for making a new agent LangGraph." # 项目描述
readme = "README.md" # 项目说明文档
license = { text = "MIT" } # 许可证类型
requires-python = ">=3.10" # 支持的Python版本(3.10及以上)
2. project.dependencies - 生产环境依赖
toml
# 以下列举了本次案例中需要用到的依赖包
dependencies = [
"langchain>=1.0.5",
"langchain_openai>=1.0.2",
"langchain-community>=0.4.1",
"langgraph>=1.0.0",
"langgraph-checkpoint>=3.0.1",
"langgraph-checkpoint-postgres>=3.0.2",
"langgraph-cli>=0.4.11",
"python-dotenv>=1.0.1", # 环境变量管理工具
"PyMySQL>=1.1.2",
"numpy>=2.3.4",
"pycryptodome>=3.23.0",
]
3. project.optional-dependencies - 可选依赖
toml
dev = ["mypy>=1.11.1", "ruff>=0.6.1"] # 开发依赖:类型检查 + 代码检查工具
4. build-system - 构建系统配置
toml
requires = ["setuptools>=73.0.0", "wheel"] # 构建所需工具
build-backend = "setuptools.build_meta" # 构建后端(setuptools)
5. tool.setuptools - 包目录结构配置
toml
packages = ["langgraph.templates.agent", "agent"] # 包含的Python包
[tool.setuptools.package-dir] # 包目录映射
"langgraph.templates.agent" = "src/agent" # 将源代码映射到指定目录
"agent" = "src/agent"
[tool.setuptools.package-data]
"*" = ["py.typed"] # 包含类型提示文件(支持类型检查)
6. tool.ruff - Ruff代码检查器配置
Ruff是一个快速的Python代码检查工具,集成了多种检查器。
toml
lint.select = [ # 启用的检查规则
"E", # pycodestyle (PEP8代码风格检查)
"F", # pyflakes (语法和逻辑错误检查)
"I", # isort (导入排序检查)
"D", # pydocstyle (文档字符串检查)
"UP", # pyupgrade (要求字符串第一行用命令式语气
"T201", # 打印语句检查
"UP", # pyupgrade (自动升级Python语法)
]
lint.ignore = [ # 忽略的规则
"UP006", # 忽略"建议使用list[]而非typing.List"的警告
"UP007", # 忽略"建议使用dict[]而非typing.Dict"的警告
"UP035", # 允许从typing_extensions导入
"D417", # 不要求每个函数参数都有文档
"E501", # 忽略行长度限制
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["UP", "UP"] # 测试文件中忽略文档检查和语法升级检查
[tool.ruff.lint.pydocstyle]
convention = "google" # 使用Google风格的文档字符串格式
7. dependency-groups - 依赖组(开发依赖)
toml
dev = [
"anyio>=4.7.0", # 异步IO库
"langgraph-cli[inmem]>=0.4.7", # LangGraph命令行工具(带内存后端)
"mypy>=1.13.0", # 静态类型检查器
"pytest>=8.3.5", # 测试框架
"ruff>=0.8.2", # 代码检查器
]
2.4 langgraph.json 配置文件
要使用 LangSmith Deployment 部署应用程序,核心是通过一个配置文件将各个组件整合起来。默认情况下,CLI会在当前目录查找名为 langgraph.json 的文件。
下表汇总了配置文件的主要字段。
| 配置键 | 是否必需 | 类型/选项 | 描述与示例 |
|---|---|---|---|
$schema |
必需 | 字符串 | 指向 JSON Schema 以进行验证,例如:"https://langra.ph/schema.json" |
dependencies |
必需 | 数组 | 项目依赖。可以是: 1. 单个点 ".":查找本地 Python 包。 2. 目录路径:如 "./" 或 "./local_package",该目录需包含 pyproject.toml、setup.py 或 requirements.txt。 3. Python 包名。 |
graphs |
必需 | 对象 | 图定义映射。格式为 {"图ID": "文件路径:导出对象"}。 示例: { "chat": "./chat.graph:graph" } 。 |
auth |
可选 | 对象 | 自定义认证配置(v0.11+)。需指定认证处理器路径。 示例: { "path": "./auth.py:auth", ... } |
base_image |
可选 | 字符串 | 基础 Docker 镜像。默认为 langchain/langgraph-api;可用于固定版本,例如 "langchain/langgraph-server:0.2"。 |
image_distro (p>=0.2.11) |
可选 | 枚举 | 基础镜像的 Linux 发行版。可选:"debian"(默认)、"wolfi"、"bookworm"、"bullseye"。推荐使用更安全的 "wolfi"。 |
env |
可选 | 字符串或对象 | 环境变量配置。可以是 .env 文件路径,或直接是键值映射。 |
store |
可选 | 对象 | 存储配置, 语义搜索索引配置(需指定 embed 时间。 index: 定义了搜索索引和设置(embedded 嵌入模型、dims 维度、fields 字段)。 ttl:数据过期配置(sweep_interval_minutes、default_ttl、sweep_interval_read)。 |
checkpointer |
可选 | 对象 | 检查点配置。 ttl:检查点过期策略(strategy,sweep_interval_minutes,default_ttl)。 serde(allowed_json_modules,pickle_fallback)。 |
http |
可选 | 对象 | HTTP 服务器配置。可配置自定义应用(app)、CORS、中间件顺序(middleware_order)、自定义头、挂载前缀(mount_prefix)等。 |
webhooks (v0.5.36+) |
可选 | 对象 | 出站 Webhook 配置。包含 env_prefix、headers(静态请求头)、url(URL 验证策略)等。 |
python_version |
可选 | 字符串 | Python 版本。可选 "3.11"(默认)、"3.12" 或 "3.13"。 |
node_version |
可选 | 字符串 | Node.js 版本。如需使用 LangGraph.js,可设为 "20"。 |
pip_installer (v0.3+) |
可选 | 枚举 | Python 包安装器。可选 "auto"(默认,使用 uv pip)、"pip" 或 "uv"。 |
api_version (v0.3.7+) |
可选 | 字符串 | LangGraph API 服务器语义版本(如 "0.3")。默认为最新版。 |
- 版本注意:部分配置键有最低 CLI 版本要求,已在表中标出(如
image_distro需 langgraph-cli>=0.2.11)。 - 示例参考:官方提供了多个完整配置示例,例如"基础配置"、"使用 Wolfi 基础镜像"、"为存储添加语义搜索"、"配置存储 TTL" 等,是很好的选择。
- 自定义嵌入:
store.index.embed字段除了使用模型名(如"openai:text-embedding-3-small"),还支持指向自定义嵌入函数的本地路径(如"./embeddings.py:embed_texts")。
3. 思路
3.1 五步构建
构建 LangGraph 系统的核心是将流程分解为离散的节点,通过共享的状态连接,每个节点可读取和写入状态,并自主决定后续路径。
第一步:工作流分解
- 将流程拆分为独立步骤,每个步骤成为一个节点。如有复杂操作,可拆分为子图处理。
- 绘制节点间连接关系
第二步:节点功能识别
- LLM节点:用于理解、分析、生成文本或推理决策
- 数据节点:从外部源检索信息
- 动作节点:执行外部操作
- 用户输入节点:需要人工干预
第三步:状态设计
- 状态是共享内存,所有节点均可访问
- 只存储原始数据
- 设计原则:
- 跨步骤需要持久化的数据才存入状态
- 可从其他数据推导的信息不存储
第四步:节点实现
- 每个节点是接收状态并返回更新的函数
- 使用
Command对象指定状态更新和下一节点 - 错误处理策略:
- 瞬时错误(网络问题):自动重试
- LLM可恢复错误:将错误存入状态,让LLM重试
- 用户可修复错误:使用
interrupt()暂停等待人工输入 - 意外错误:抛出供调试
第五步:连接组装
- 添加节点和必要边
- 使用检查点器(checkpointer)实现持久化,支持暂停/恢复
- 编译为可执行应用
3.2 按功能模块拆分为多图
根据案例需求(房源推荐、房源预定、查询我的、常规问答),可以将不同的能力拆分为不同的子图完成。如:
recommended_graph推荐子图:负责进行房源推荐,包括查询数据库,根据条件推荐合适的房源。reserve_graph预定子图:负责房源预定,生成工单。extend_graph其它子图:负责其它问题处理,如常规问答等。
除此之外,我们还需要一个主图负责路由,判定用户请求该路由到哪个子图中去执行具体逻辑处理。
3.3 主图-智能分流
#mermaid-svg-ldkmipjLxbfYBB2Q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ldkmipjLxbfYBB2Q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ldkmipjLxbfYBB2Q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ldkmipjLxbfYBB2Q .error-icon{fill:#552222;}#mermaid-svg-ldkmipjLxbfYBB2Q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ldkmipjLxbfYBB2Q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ldkmipjLxbfYBB2Q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ldkmipjLxbfYBB2Q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ldkmipjLxbfYBB2Q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ldkmipjLxbfYBB2Q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ldkmipjLxbfYBB2Q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ldkmipjLxbfYBB2Q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ldkmipjLxbfYBB2Q .marker.cross{stroke:#333333;}#mermaid-svg-ldkmipjLxbfYBB2Q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ldkmipjLxbfYBB2Q p{margin:0;}#mermaid-svg-ldkmipjLxbfYBB2Q .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ldkmipjLxbfYBB2Q .cluster-label text{fill:#333;}#mermaid-svg-ldkmipjLxbfYBB2Q .cluster-label span{color:#333;}#mermaid-svg-ldkmipjLxbfYBB2Q .cluster-label span p{background-color:transparent;}#mermaid-svg-ldkmipjLxbfYBB2Q .label text,#mermaid-svg-ldkmipjLxbfYBB2Q span{fill:#333;color:#333;}#mermaid-svg-ldkmipjLxbfYBB2Q .node rect,#mermaid-svg-ldkmipjLxbfYBB2Q .node circle,#mermaid-svg-ldkmipjLxbfYBB2Q .node ellipse,#mermaid-svg-ldkmipjLxbfYBB2Q .node polygon,#mermaid-svg-ldkmipjLxbfYBB2Q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ldkmipjLxbfYBB2Q .rough-node .label text,#mermaid-svg-ldkmipjLxbfYBB2Q .node .label text,#mermaid-svg-ldkmipjLxbfYBB2Q .image-shape .label,#mermaid-svg-ldkmipjLxbfYBB2Q .icon-shape .label{text-anchor:middle;}#mermaid-svg-ldkmipjLxbfYBB2Q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ldkmipjLxbfYBB2Q .rough-node .label,#mermaid-svg-ldkmipjLxbfYBB2Q .node .label,#mermaid-svg-ldkmipjLxbfYBB2Q .image-shape .label,#mermaid-svg-ldkmipjLxbfYBB2Q .icon-shape .label{text-align:center;}#mermaid-svg-ldkmipjLxbfYBB2Q .node.clickable{cursor:pointer;}#mermaid-svg-ldkmipjLxbfYBB2Q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ldkmipjLxbfYBB2Q .arrowheadPath{fill:#333333;}#mermaid-svg-ldkmipjLxbfYBB2Q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ldkmipjLxbfYBB2Q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ldkmipjLxbfYBB2Q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ldkmipjLxbfYBB2Q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ldkmipjLxbfYBB2Q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ldkmipjLxbfYBB2Q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ldkmipjLxbfYBB2Q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ldkmipjLxbfYBB2Q .cluster text{fill:#333;}#mermaid-svg-ldkmipjLxbfYBB2Q .cluster span{color:#333;}#mermaid-svg-ldkmipjLxbfYBB2Q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ldkmipjLxbfYBB2Q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ldkmipjLxbfYBB2Q rect.text{fill:none;stroke-width:0;}#mermaid-svg-ldkmipjLxbfYBB2Q .icon-shape,#mermaid-svg-ldkmipjLxbfYBB2Q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ldkmipjLxbfYBB2Q .icon-shape p,#mermaid-svg-ldkmipjLxbfYBB2Q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ldkmipjLxbfYBB2Q .icon-shape .label rect,#mermaid-svg-ldkmipjLxbfYBB2Q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ldkmipjLxbfYBB2Q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ldkmipjLxbfYBB2Q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ldkmipjLxbfYBB2Q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} start
get_store_info
identify_question
recommended_graph
reserve_graph
extend_graph
get_user_preferences
need_reserve
end
节点设计如下:
get_store_info节点:查询持久化消息,例如用户历史偏好数据。identify_question节点:识别用户输入的问题,进行智能路由。recommended_graph子图节点:路由1,进行房源推荐reserve_graph子图节点:路由2,预定房源子图extend_graph子图节点:路由3,其它问题处理,如常规问答等。get_user_preferences节点:路由4,用户查询自己的信息need_reserve节点:是否需要预定房源。当推荐房源结束后,主动咨询是否需要预定房源,这里是一个中断节点。
3.4 推荐子图--构建自定义 SQL 代理
#mermaid-svg-l1UwbHpPSAsJTFBu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-l1UwbHpPSAsJTFBu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-l1UwbHpPSAsJTFBu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-l1UwbHpPSAsJTFBu .error-icon{fill:#552222;}#mermaid-svg-l1UwbHpPSAsJTFBu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-l1UwbHpPSAsJTFBu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-l1UwbHpPSAsJTFBu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-l1UwbHpPSAsJTFBu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-l1UwbHpPSAsJTFBu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-l1UwbHpPSAsJTFBu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-l1UwbHpPSAsJTFBu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-l1UwbHpPSAsJTFBu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-l1UwbHpPSAsJTFBu .marker.cross{stroke:#333333;}#mermaid-svg-l1UwbHpPSAsJTFBu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-l1UwbHpPSAsJTFBu p{margin:0;}#mermaid-svg-l1UwbHpPSAsJTFBu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-l1UwbHpPSAsJTFBu .cluster-label text{fill:#333;}#mermaid-svg-l1UwbHpPSAsJTFBu .cluster-label span{color:#333;}#mermaid-svg-l1UwbHpPSAsJTFBu .cluster-label span p{background-color:transparent;}#mermaid-svg-l1UwbHpPSAsJTFBu .label text,#mermaid-svg-l1UwbHpPSAsJTFBu span{fill:#333;color:#333;}#mermaid-svg-l1UwbHpPSAsJTFBu .node rect,#mermaid-svg-l1UwbHpPSAsJTFBu .node circle,#mermaid-svg-l1UwbHpPSAsJTFBu .node ellipse,#mermaid-svg-l1UwbHpPSAsJTFBu .node polygon,#mermaid-svg-l1UwbHpPSAsJTFBu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-l1UwbHpPSAsJTFBu .rough-node .label text,#mermaid-svg-l1UwbHpPSAsJTFBu .node .label text,#mermaid-svg-l1UwbHpPSAsJTFBu .image-shape .label,#mermaid-svg-l1UwbHpPSAsJTFBu .icon-shape .label{text-anchor:middle;}#mermaid-svg-l1UwbHpPSAsJTFBu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-l1UwbHpPSAsJTFBu .rough-node .label,#mermaid-svg-l1UwbHpPSAsJTFBu .node .label,#mermaid-svg-l1UwbHpPSAsJTFBu .image-shape .label,#mermaid-svg-l1UwbHpPSAsJTFBu .icon-shape .label{text-align:center;}#mermaid-svg-l1UwbHpPSAsJTFBu .node.clickable{cursor:pointer;}#mermaid-svg-l1UwbHpPSAsJTFBu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-l1UwbHpPSAsJTFBu .arrowheadPath{fill:#333333;}#mermaid-svg-l1UwbHpPSAsJTFBu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-l1UwbHpPSAsJTFBu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-l1UwbHpPSAsJTFBu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l1UwbHpPSAsJTFBu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-l1UwbHpPSAsJTFBu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l1UwbHpPSAsJTFBu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-l1UwbHpPSAsJTFBu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-l1UwbHpPSAsJTFBu .cluster text{fill:#333;}#mermaid-svg-l1UwbHpPSAsJTFBu .cluster span{color:#333;}#mermaid-svg-l1UwbHpPSAsJTFBu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-l1UwbHpPSAsJTFBu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-l1UwbHpPSAsJTFBu rect.text{fill:none;stroke-width:0;}#mermaid-svg-l1UwbHpPSAsJTFBu .icon-shape,#mermaid-svg-l1UwbHpPSAsJTFBu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-l1UwbHpPSAsJTFBu .icon-shape p,#mermaid-svg-l1UwbHpPSAsJTFBu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-l1UwbHpPSAsJTFBu .icon-shape .label rect,#mermaid-svg-l1UwbHpPSAsJTFBu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-l1UwbHpPSAsJTFBu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-l1UwbHpPSAsJTFBu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-l1UwbHpPSAsJTFBu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} start
collect_user_info
list_tables
call_get_schema
get_schema
generate_query
check_query
run_query
end
节点设计如下:
collect_user_info节点:收集用户希望推荐的房源关键信息list_tables节点:调用 SQL 工具,获取所有表,查询表有哪些(如果想要减轻数据库压力,则这里可以在前面新增一个节点让LLM决定是否需要调用工具,如果确实需要调用工具,则执行)call_get_schema节点:LLM 绑定get_schema工具,强制调用get_schema工具get_schema节点:执行工具,获取表的详细信息,如表结构、示例数据等generate_query节点:LLM 绑定run_query工具,非强制工具调用。用来生成查询 SQL 或整合查询结果,用来生成最终结果check_query节点:用来检查sql是否有错误,绑定run_query工具,强制调用run_query工具。run_query节点:执行工具,用来运行generate_query节点生成的 SQL,检测 SQL 是否正确
3.5 预定子图--人工介入的预定系统
#mermaid-svg-pC8YCsA6QweEe8fi{font-size:16px;fill:#e8e8ee;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pC8YCsA6QweEe8fi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pC8YCsA6QweEe8fi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pC8YCsA6QweEe8fi .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-pC8YCsA6QweEe8fi .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-pC8YCsA6QweEe8fi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pC8YCsA6QweEe8fi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pC8YCsA6QweEe8fi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pC8YCsA6QweEe8fi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pC8YCsA6QweEe8fi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pC8YCsA6QweEe8fi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pC8YCsA6QweEe8fi .marker{fill:#d9d9df;stroke:#d9d9df;}#mermaid-svg-pC8YCsA6QweEe8fi .marker.cross{stroke:#d9d9df;}#mermaid-svg-pC8YCsA6QweEe8fi svg{font-size:16px;}#mermaid-svg-pC8YCsA6QweEe8fi p{margin:0;}#mermaid-svg-pC8YCsA6QweEe8fi .label{color:#e8e8ee;}#mermaid-svg-pC8YCsA6QweEe8fi .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-pC8YCsA6QweEe8fi .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-pC8YCsA6QweEe8fi .cluster-label span p{background-color:transparent;}#mermaid-svg-pC8YCsA6QweEe8fi .label text,#mermaid-svg-pC8YCsA6QweEe8fi span{fill:#e8e8ee;color:#e8e8ee;}#mermaid-svg-pC8YCsA6QweEe8fi .node rect,#mermaid-svg-pC8YCsA6QweEe8fi .node circle,#mermaid-svg-pC8YCsA6QweEe8fi .node ellipse,#mermaid-svg-pC8YCsA6QweEe8fi .node polygon,#mermaid-svg-pC8YCsA6QweEe8fi .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-pC8YCsA6QweEe8fi .rough-node .label text,#mermaid-svg-pC8YCsA6QweEe8fi .node .label text,#mermaid-svg-pC8YCsA6QweEe8fi .image-shape .label,#mermaid-svg-pC8YCsA6QweEe8fi .icon-shape .label{text-anchor:middle;}#mermaid-svg-pC8YCsA6QweEe8fi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pC8YCsA6QweEe8fi .rough-node .label,#mermaid-svg-pC8YCsA6QweEe8fi .node .label,#mermaid-svg-pC8YCsA6QweEe8fi .image-shape .label,#mermaid-svg-pC8YCsA6QweEe8fi .icon-shape .label{text-align:center;}#mermaid-svg-pC8YCsA6QweEe8fi .node.clickable{cursor:pointer;}#mermaid-svg-pC8YCsA6QweEe8fi .root .anchor path{fill:#d9d9df!important;stroke-width:0;stroke:#d9d9df;}#mermaid-svg-pC8YCsA6QweEe8fi .arrowheadPath{fill:#e6e3d7;}#mermaid-svg-pC8YCsA6QweEe8fi .edgePath .path{stroke:#d9d9df;stroke-width:2.0px;}#mermaid-svg-pC8YCsA6QweEe8fi .flowchart-link{stroke:#d9d9df;fill:none;}#mermaid-svg-pC8YCsA6QweEe8fi .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-pC8YCsA6QweEe8fi .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-pC8YCsA6QweEe8fi .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-pC8YCsA6QweEe8fi .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-pC8YCsA6QweEe8fi .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-pC8YCsA6QweEe8fi .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-pC8YCsA6QweEe8fi .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-pC8YCsA6QweEe8fi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pC8YCsA6QweEe8fi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#e8e8ee;}#mermaid-svg-pC8YCsA6QweEe8fi rect.text{fill:none;stroke-width:0;}#mermaid-svg-pC8YCsA6QweEe8fi .icon-shape,#mermaid-svg-pC8YCsA6QweEe8fi .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-pC8YCsA6QweEe8fi .icon-shape p,#mermaid-svg-pC8YCsA6QweEe8fi .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-pC8YCsA6QweEe8fi .icon-shape .label rect,#mermaid-svg-pC8YCsA6QweEe8fi .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-pC8YCsA6QweEe8fi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pC8YCsA6QweEe8fi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pC8YCsA6QweEe8fi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-pC8YCsA6QweEe8fi .startEnd>*{fill:#353740!important;stroke:#85858c!important;stroke-width:1.2px!important;color:#f2f2f4!important;font-weight:bold!important;}#mermaid-svg-pC8YCsA6QweEe8fi .startEnd span{fill:#353740!important;stroke:#85858c!important;stroke-width:1.2px!important;color:#f2f2f4!important;font-weight:bold!important;}#mermaid-svg-pC8YCsA6QweEe8fi .startEnd tspan{fill:#f2f2f4!important;}#mermaid-svg-pC8YCsA6QweEe8fi .redNode>*{fill:#2d2028!important;stroke:#a72b32!important;stroke-width:1.2px!important;color:#e5adb3!important;font-weight:bold!important;}#mermaid-svg-pC8YCsA6QweEe8fi .redNode span{fill:#2d2028!important;stroke:#a72b32!important;stroke-width:1.2px!important;color:#e5adb3!important;font-weight:bold!important;}#mermaid-svg-pC8YCsA6QweEe8fi .redNode tspan{fill:#e5adb3!important;}#mermaid-svg-pC8YCsA6QweEe8fi .greenNode>*{fill:#213538!important;stroke:#4caf8a!important;stroke-width:1.2px!important;color:#a6dfcc!important;font-weight:bold!important;}#mermaid-svg-pC8YCsA6QweEe8fi .greenNode span{fill:#213538!important;stroke:#4caf8a!important;stroke-width:1.2px!important;color:#a6dfcc!important;font-weight:bold!important;}#mermaid-svg-pC8YCsA6QweEe8fi .greenNode tspan{fill:#a6dfcc!important;}#mermaid-svg-pC8YCsA6QweEe8fi .purpleNode>*{fill:#25223d!important;stroke:#6844b7!important;stroke-width:1.2px!important;color:#d2c4ed!important;}#mermaid-svg-pC8YCsA6QweEe8fi .purpleNode span{fill:#25223d!important;stroke:#6844b7!important;stroke-width:1.2px!important;color:#d2c4ed!important;}#mermaid-svg-pC8YCsA6QweEe8fi .purpleNode tspan{fill:#d2c4ed!important;}#mermaid-svg-pC8YCsA6QweEe8fi .reserveNode>*{fill:#20363a!important;stroke:#48a67f!important;stroke-width:1.2px!important;color:#a8ddc9!important;font-weight:bold!important;}#mermaid-svg-pC8YCsA6QweEe8fi .reserveNode span{fill:#20363a!important;stroke:#48a67f!important;stroke-width:1.2px!important;color:#a8ddc9!important;font-weight:bold!important;}#mermaid-svg-pC8YCsA6QweEe8fi .reserveNode tspan{fill:#a8ddc9!important;}#mermaid-svg-pC8YCsA6QweEe8fi .toolNode>*{fill:#262743!important;stroke:#595cc2!important;stroke-width:1.2px!important;color:#c5c3ee!important;}#mermaid-svg-pC8YCsA6QweEe8fi .toolNode span{fill:#262743!important;stroke:#595cc2!important;stroke-width:1.2px!important;color:#c5c3ee!important;}#mermaid-svg-pC8YCsA6QweEe8fi .toolNode tspan{fill:#c5c3ee!important;} start
get_title
get_phone
get_id
add_reserve_message
call_orders
end
tool_node
节点设计如下:
get_title节点:中断获取要预定的房源标题get_phone节点:中断获取预定人的电话get_id节点:中断获取预定人的身份证add_reserve_message节点:构造并添加预定消息call_orders节点:LLM 绑定生成工单的工具tool_node节点:生成工单的工具
3.6 扩展子图--除业务外的智能问答助手
#mermaid-svg-i592alWO4Xj6VG6Y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-i592alWO4Xj6VG6Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-i592alWO4Xj6VG6Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-i592alWO4Xj6VG6Y .error-icon{fill:#552222;}#mermaid-svg-i592alWO4Xj6VG6Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-i592alWO4Xj6VG6Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-i592alWO4Xj6VG6Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-i592alWO4Xj6VG6Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-i592alWO4Xj6VG6Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-i592alWO4Xj6VG6Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-i592alWO4Xj6VG6Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-i592alWO4Xj6VG6Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-i592alWO4Xj6VG6Y .marker.cross{stroke:#333333;}#mermaid-svg-i592alWO4Xj6VG6Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-i592alWO4Xj6VG6Y p{margin:0;}#mermaid-svg-i592alWO4Xj6VG6Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-i592alWO4Xj6VG6Y .cluster-label text{fill:#333;}#mermaid-svg-i592alWO4Xj6VG6Y .cluster-label span{color:#333;}#mermaid-svg-i592alWO4Xj6VG6Y .cluster-label span p{background-color:transparent;}#mermaid-svg-i592alWO4Xj6VG6Y .label text,#mermaid-svg-i592alWO4Xj6VG6Y span{fill:#333;color:#333;}#mermaid-svg-i592alWO4Xj6VG6Y .node rect,#mermaid-svg-i592alWO4Xj6VG6Y .node circle,#mermaid-svg-i592alWO4Xj6VG6Y .node ellipse,#mermaid-svg-i592alWO4Xj6VG6Y .node polygon,#mermaid-svg-i592alWO4Xj6VG6Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-i592alWO4Xj6VG6Y .rough-node .label text,#mermaid-svg-i592alWO4Xj6VG6Y .node .label text,#mermaid-svg-i592alWO4Xj6VG6Y .image-shape .label,#mermaid-svg-i592alWO4Xj6VG6Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-i592alWO4Xj6VG6Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-i592alWO4Xj6VG6Y .rough-node .label,#mermaid-svg-i592alWO4Xj6VG6Y .node .label,#mermaid-svg-i592alWO4Xj6VG6Y .image-shape .label,#mermaid-svg-i592alWO4Xj6VG6Y .icon-shape .label{text-align:center;}#mermaid-svg-i592alWO4Xj6VG6Y .node.clickable{cursor:pointer;}#mermaid-svg-i592alWO4Xj6VG6Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-i592alWO4Xj6VG6Y .arrowheadPath{fill:#333333;}#mermaid-svg-i592alWO4Xj6VG6Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-i592alWO4Xj6VG6Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-i592alWO4Xj6VG6Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i592alWO4Xj6VG6Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-i592alWO4Xj6VG6Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i592alWO4Xj6VG6Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-i592alWO4Xj6VG6Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-i592alWO4Xj6VG6Y .cluster text{fill:#333;}#mermaid-svg-i592alWO4Xj6VG6Y .cluster span{color:#333;}#mermaid-svg-i592alWO4Xj6VG6Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-i592alWO4Xj6VG6Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-i592alWO4Xj6VG6Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-i592alWO4Xj6VG6Y .icon-shape,#mermaid-svg-i592alWO4Xj6VG6Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-i592alWO4Xj6VG6Y .icon-shape p,#mermaid-svg-i592alWO4Xj6VG6Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-i592alWO4Xj6VG6Y .icon-shape .label rect,#mermaid-svg-i592alWO4Xj6VG6Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-i592alWO4Xj6VG6Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-i592alWO4Xj6VG6Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-i592alWO4Xj6VG6Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} start
extend_node
end
节点设计如下:
extend_node节点:LLM 对话助手
4. 案例开发
4.1 通用设计
4.1.1 LLM
在案例中,无论主图还是子图,涉及到 LLM 调用统一使用 OpenAI。
python
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv()
# 定义好模型,统一使用
model = ChatOpenAI(
model="gpt-5.4",
temperature=0,
api_key=os.getenv("PACKYAPI_API_KEY"),
base_url="https://www.packyapi.com/v1",
)
4.1.2 持久化存储
由于要收集用户偏好信息与历史预定信息,因此要使用持久化存储。定义存储值的结构:
python
from typing import Optional
from pydantic import BaseModel, Field
class ReservedInfo(BaseModel):
"""房源的预定信息"""
order_id: str = Field(description="预定id")
title: str = Field(description="预定的房源标题")
phone_number: str = Field(description="预定电话")
price: Optional[float] = Field(
default=None,
description="预定的房源价格,单位为元/月"
)
intro: Optional[str] = Field(
default=None,
description="预定的房源介绍"
)
city_name: Optional[str] = Field(
default=None,
description="预定的房源所在城市名"
)
region_name: Optional[str] = Field(
default=None,
description="预定的房源所在区/县"
)
class UserPreferences(BaseModel):
"""用户偏好信息"""
budget_min: Optional[float] = Field(
default=None,
description="用户的最低预算,单位为元/月"
)
budget_max: Optional[float] = Field(
default=None,
description="用户的最高预算,单位为元/月"
)
reserved_info: Optional[list[ReservedInfo]] = Field(
default=None,
description="预定过的房源列表"
)
可以在节点中获取或添加持久化信息,如下所示:
python
def node(state: State, runtime: Runtime[ContextSchema], *, store: BaseStore):
# 1. 通过 Namespace 获取用户偏好数据
namespace = (user_id, "preferences")
# 2. 查询
prefs_result = store.search(namespace)
# 3. 存储
prefs = UserPreferences(
budget_min=updated_state.get('budget_min'),
budget_max=updated_state.get('budget_max'),
)
store.put(
namespace,
str(uuid.uuid4()),
prefs.model_dump(exclude_none=True)
)
4.1.3 运行时上下文
由于获取用户偏好数据时,构建的 Namespace 需要根据用户id进行区分。则可以将用户id添加进上下文,单次运行时可根据用户id获取用户持久化信息。
python
from typing import TypedDict
class ContextSchema(TypedDict):
user_id: str
将来可以在节点中获取上下文信息,如下所示:
python
def node(state: State, runtime: Runtime[ContextSchema], *, store: BaseStore):
user_id = runtime.context.get("user_id")
4.2 主图-智能分流
4.2.1 状态定义
- 主状态 State :全局共享状态。状态继承自
MessagesState,自动管理对话历史。user_intent:用户意图,表示用户输入的问题含义(推荐 or 预定 or 查询 or 其它)user_preferences:用户偏好信息,实现数据共享
- 节点间传输私有状态 NeedReserveOutput:当推荐子图执行完成后,用户获取用户是否想要预定房源的意向。从而决定是否执行预定子图。
state/main.py
python
from typing import TypedDict
from langgraph.graph import MessagesState
class State(MessagesState):
user_intent: str # 用户意图
user_preferences: dict # 用户偏好
class NeedReserveOutput(TypedDict):
reserve: str # 这个字段不会出现在最终状态中
4.2.2 工作流定义
注意,编译 graph 时,无需设置 checkpointer 和 store。将来进行 LangSmith 部署时,持久化可以通过配置进行设置。
python
from typing import Literal
from langgraph.graph.constants import START, END
from langgraph.graph import StateGraph
from src.agent.common.content import ContextSchema
from src.agent.extend import extend_graph
from src.agent.node.main import get_store_info, identify_question, need_reserve, get_user_preferences
from src.agent.recommend import recommended_graph
from src.agent.reserve import reserve_graph, NeedReserveOutput
from src.agent.state.main import State, NeedReserveOutput
# 构建图
builder = StateGraph(State, context_schema=ContextSchema)
builder.add_node(get_store_info) # 查询持久化消息
builder.add_node(identify_question) # 识别用户输入的问题
builder.add_node("recommended_graph", recommended_graph) # 推荐房源子图,注意需要指定名称
builder.add_node("reserve_graph", reserve_graph) # 预定房源子图
builder.add_node("extend_graph", extend_graph) # 扩展子图
builder.add_node(get_user_preferences)
builder.add_edge(START, "get_store_info")
builder.add_edge("get_store_info", "identify_question") # 识别问题
# 路由1:智能识别问题
def route_user_intent(state: State) -> Literal["recommended_graph", "reserve_graph", "extend_graph", "get_user_preferences"]:
user_intent = state["user_intent"]
if user_intent == "recommend_house":
return "recommended_graph"
elif user_intent == "reserve_house":
return "reserve_graph"
elif user_intent == "get_info":
return "get_user_preferences"
else:
return "extend_graph"
builder.add_conditional_edges(
"identify_question", # 消息路由
route_user_intent,
["recommended_graph", "reserve_graph", "extend_graph", "get_user_preferences"]
)
# 路由2:推荐房源(中断询问)预定房源
builder.add_edge("recommended_graph", "need_reserve")
def should_reserve(state: NeedReserveOutput) -> Literal[END, "reserve_graph"]:
reserve = state["reserve"]
if reserve == '需要':
return "reserve_graph"
else:
return END
builder.add_conditional_edges(
"need_reserve",
should_reserve, # 不需要预定就结束对话
[END, "reserve_graph"]
)
# 路由2:预定房源
builder.add_edge("reserve_graph", END)
# 路由3:查询我的信息
builder.add_edge("get_user_preferences", END)
# 路由4:其它
builder.add_edge("extend_graph", END)
graph = builder.compile()
4.2.3 节点实现
node/main.py
python
from typing import Literal
from langchain_core.messages import SystemMessage, HumanMessage, filter_messages, AIMessage
from langgraph.runtime import Runtime
from langgraph.store.base import BaseStore
from langgraph.types import interrupt
from pydantic import BaseModel, Field
from src.agent.common.content import ContextSchema
from src.agent.common.llm import model
from src.agent.state.main import State, NeedReserveOutput
class UserMessage(BaseModel):
"""用户提问的消息摘要"""
type: Literal["recommend_house", "reserve_house", "get_info", "others"] = (
Field(description="根据用户问题描述判断问题类型:推荐房源、预定房源、获取信息、其他内容")
)
# 节点:识别用户问题:预定、推荐、我的
def identify_question(state: State) -> State:
def extract_info(messages) -> UserMessage:
system_message = SystemMessage(
content="""
你是一个根据描述提取信息提取专家。请从用户的描述中提取用户想要咨询的相关信息。
请严格根据语义推断信息,但不能猜测或编造信息。"""
)
# 创建结构化提取模型
return (model.with_structured_output(schema=UserMessage)
.invoke([system_message] + messages))
# 最新的用户消息
user_question = state["messages"][-1].content
user_message = extract_info([HumanMessage(content=user_question)])
return {"user_intent": user_message.type}
# 节点:查询持久化消息
def get_store_info(state: State, runtime: Runtime[ContextSchema], *, store: BaseStore):
# 搜索用户信息
user_id = runtime.context.get("user_id")
namespace = (user_id, "preferences")
pref_result = store.search(namespace)
if pref_result and pref_result[0]:
return {"user_preferences": pref_result[0].value}
else:
return {}
# 节点:中断询问是否主要帮助预定房源
def need_reserve(state: State) -> NeedReserveOutput:
prompt = "F已经为您推荐合适的房源,是否需要帮您预订房源?\n"
prompt += "如果不需要,请输入'**不需要**'。\n"
prompt += "如果需要,请输入'**需要**'。\n(注意输入其它值无效)"
# 中断,等待用户输入
answer = interrupt(prompt)
return {"reserve": str(answer).strip()}
# 节点:返回用户偏好信息
def get_user_preferences(state: State):
prefs = state.get("user_preferences", {})
user_messages = filter_messages(state["messages"], include_types="human")
# 格式化已预定过的信息
reserved_list = prefs.get('reserved_info', [])
if reserved_list:
reserved_str = "\n"
for i, item in enumerate(reserved_list, 1):
reserved_str += f"{i}. 预定工单ID:{item.get('order_id')}, " \
f"房源标题:{item.get('title')}, " \
f"预定电话:{item.get('phone_number')}\n"
else:
reserved_str = "无"
response = model.invoke([
SystemMessage(content="""你是一个乐于助人的助手,可以根据用户偏好信息进行回复。
如果有的偏好数据为空,不要猜测或编造数据。
不要直接回复偏好数据是什么,要结合问题进行主动回复。
如果问题与用户偏好数据无关,直接回复即可。"""),
HumanMessage(
content="用户的历史偏好信息如下:"
f"1. 最低预算:{prefs.get('budget_min')}"
f"2. 最高预算:{prefs.get('budget_max')}"
f"3. 已预定过的信息:{reserved_str}"
),
user_messages[-1]
])
return {"messages": [response]}
4.3 推荐子图--构建自定义 SQL 代理
4.3.1 状态定义
state/recommend.py
python
from langgraph.graph import MessagesState
# 推荐房源状态
class RecommendState(MessagesState):
# 用户偏好(数据共享)
user_preferences: dict
# 以下是推荐的关键参数
city: str # 城市
budget_min: float # 最低预算
budget_max: float # 最高预算
district: str # 区域
room_type: str # 房屋类型
orientation: str # 朝向
room_count: int # 推荐数量
others: str # 其它要求
# 获取推荐信息方法
def get_recommend_info(state: dict) -> str:
info_prompt = """
提取用户期望推荐的房源信息如下:
- 城市: {city}
- 区域: {district}
- 预算: {budget_min} - {budget_max} 元/月
- 房屋类型: {room_type}
- 朝向: {orientation}
- 特殊要求: {others}
- 推荐数量: {room_count}
如果某些信息未指定,请使用合适的默认值或放宽条件。"""
return info_prompt.format(
city=state.get('city', '未指定'),
district=state.get('district', '未指定'),
budget_min=state.get('budget_min', '未指定'),
budget_max=state.get('budget_max', '未指定'),
room_type=state.get('room_type', '未指定'),
orientation=state.get('orientation', '未指定'),
others=state.get('others', '无'),
room_count=state.get('room_count', 5)
)
4.3.2 工作流定义
1.收集用户信息 → 2.列出数据库表 → 3.获取表结构 → 4.生成SQL查询 → 5.检查查询 → 6.执行查询 → 7.返回结果
python
from typing import Literal
from langgraph.graph import END, START, StateGraph
from src.agent.common.content import ContextSchema
from src.agent.node.recommend import (
collect_user_info,
list_tables,
call_get_schema,
get_schema_node,
generate_query,
check_query,
run_query_node
)
from src.agent.state.recommend import RecommendState
# 构建图
builder = StateGraph(RecommendState, context_schema=ContextSchema)
builder.add_node(collect_user_info) # 收集用户信息节点
builder.add_node(list_tables) # 调用sql_db_list_tables工具
builder.add_node(call_get_schema) # LLM绑定sql_db_schema工具,强制工具调用
builder.add_node("get_schema", get_schema_node) # sql_db_schema工具
builder.add_node(generate_query) # LLM绑定sql_db_query工具,非强制工具调用
builder.add_node(check_query) # LLM绑定sql_db_query工具,强制工具调用
builder.add_node("run_query", run_query_node) # sql_db_query工具
# 添加边
builder.add_edge(START, "collect_user_info") # 从开始节点到用户信息收集节点
builder.add_edge("collect_user_info", "list_tables") # 用户信息收集完成后,获取所有表
builder.add_edge("list_tables", "call_get_schema") # LLM:(强制调用sql_db_schema工具),确保这些表确实存在,且过滤出需要的表
builder.add_edge("call_get_schema", "get_schema") # 工具: 输出需要的表的模式和示例行
builder.add_edge("get_schema", "generate_query") # LLM:(非强制调用sql_db_query工具)给定一个输入问题,创建一个语法正确的{dialect}查询来运行,然后查看查询的结果并返回答案。
def should_continue(state: RecommendState) -> Literal[END, "check_query"]:
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return END
else:
return "check_query"
builder.add_conditional_edges(
"generate_query",
should_continue, # 查看最后一条消息是否是工具调用。
# 是: LLM:(强制调用sql_db_query工具)执行SQL,生成人工用户消息进行检查
# 否: end
[END, "check_query"]
)
builder.add_edge("check_query", "run_query") # 工具: 输入详细而正确的SQL查询,输出是来自数据库的结果
builder.add_edge("run_query", "generate_query") # LLM:(非强制调用sql_db_query工具)给定一个数据库的结果,然后查看查询的结果并返回答案。
recommended_graph = builder.compile()
4.3.3 节点实现
4.3.3.1 collect_user_info节点
该节点收集用户希望推荐的房源关键信息。交互流程:
- 从历史偏好初始化
- 从当前对话提取
- 缺失信息中断询问
- 持久化更新
python
import uuid
import os
from langchain_community.utilities import SQLDatabase
from langchain_community.agent_toolkits import SQLDatabaseToolkit
from typing import Optional
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, filter_messages
from langgraph.prebuilt import ToolNode
from langgraph.runtime import Runtime
from langgraph.store.base import BaseStore
from langgraph.types import interrupt
from pydantic import BaseModel, Field
from src.agent.common.content import ContextSchema
from src.agent.common.llm import model
from src.agent.state.recommend import RecommendState, get_recommend_info
from src.agent.common.store import UserPreferences
# 定义用户信息的数据模型(结构化输出)
class UserInfo(BaseModel):
"""用户的租房需求信息"""
city: Optional[str] = Field(
default=None,
description="用户所在或想要租房的城市,例如:西安、北京、上海"
)
district: Optional[str] = Field(
default=None,
description="用户想要租房的具体区域或行政区,例如:雁塔区、碑林区、海淀区"
)
budget_min: Optional[float] = Field(
default=None,
description="用户的最低预算,单位为元/月"
)
budget_max: Optional[float] = Field(
default=None,
description="用户的最高预算,单位为元/月"
)
room_type: Optional[str] = Field(
default=None,
description="房屋类型,例如:整租、合租、公寓、一室一厅、两室一厅"
)
orientation: Optional[str] = Field(
default=None,
description="房屋朝向,例如:朝南、朝北、东南、南北通透"
)
room_count: Optional[int] = Field(
default=None,
description="需要推荐的房屋数量"
)
others: Optional[str] = Field(
default=None,
description="特殊要求,例如:带阳台、独立卫生间、近地铁、可养宠物、有电梯等"
)
# 节点:收集用户信息
def collect_user_info(state: RecommendState, runtime: Runtime[ContextSchema], *, store: BaseStore):
# 1. 获取需要解析的数据:最新的用户消息+偏好数据
user_preferences = store.get(("user_preferences",))
user_messages = filter_messages(state["messages"], include_types="human")
if user_preferences and (user_preferences.get('budget_min') or user_preferences.get('budget_max')):
extract_messages = [
HumanMessage(
content=f"用户的历史偏好信息如下:\n"
f"1. 最低预算:{user_preferences['budget_min']}\n"
f"2. 最高预算:{user_preferences['budget_max']}"
),
user_messages[-1]
]
else:
extract_messages = [user_messages[-1]]
# 2. 提取信息函数
def extract_info(messages) -> UserInfo:
system_message = SystemMessage(
content="""你是一个租房需求信息提取专家。请从用户的描述与历史信息中提取租房相关信息。
如果用户历史偏好信息与最新用户消息冲突,以最新的用户消息为主。
只提取用户明确提到的信息,不要猜测或推断。
如果某个信息用户没有提到,就返回空。
注意预算的单位可能是元/月、元/天等,请统一转换为元/月。
如果用户提到价格范围,请分别提取最低和最高预算。
如果用户提到推荐几套,请提取room_count字段。"""
)
# 创建结构化提取模型
return model.with_structured_output(schema=UserInfo)\
.invoke([system_message] + messages)
# 3. 更新状态函数
def update_state(current_state: dict, info: UserInfo) -> dict:
if not info:
return current_state
# 获取所有非None的字段
user_info_dict = info.model_dump(exclude_none=True)
current_state.update(user_info_dict)
return current_state
# 初始化更新后的状态
updated_state = {}
# 4. 初次提取信息
extracted_info = extract_info(extract_messages)
updated_state = update_state(updated_state, extracted_info)
# 5. 检查是否缺失关键信息
missing_info = []
if not updated_state.get("city"):
missing_info.append("**城市**")
if updated_state.get("budget_min") is None or updated_state.get("budget_max") is None:
missing_info.append("**预算范围**")
# 6. 处理缺失信息(中断并询问用户)
if missing_info:
prompt = f"为了给您推荐合适的房源,请提供以下信息:{', '.join(missing_info)}和其它信息。\n"
prompt += "如果您不想提供,请输入'**不提供**',我会根据已有信息为您推荐房源。"
# 中断,等待用户输入
answer = interrupt(prompt)
if str(answer).strip() == "不提供":
# 如果用户选择不提供,设置默认值
if not updated_state.get("city"):
updated_state["city"] = "随机城市"
if not updated_state.get("budget_min"):
updated_state["budget_min"] = 500.0
if not updated_state.get("budget_max"):
updated_state["budget_max"] = 5000.0
if not updated_state.get("room_count"):
updated_state["room_count"] = 5
print(f"用户选择不提供信息,使用默认值:城市={updated_state.get('city')},"
f"预算={updated_state['budget_min']}-{updated_state['budget_max']}")
else:
# 用户提供了更多信息,再次提取
user_response_msg = HumanMessage(content=str(answer))
extracted_response_info = extract_info([user_response_msg])
updated_state = update_state(updated_state, extracted_response_info)
# 7.持久化用户信息(updated_state有值才更新)
if updated_state.get("budget_min") or updated_state.get("budget_max"):
user_id = runtime.context.get("user_id")
namespace = (user_id, "preferences")
# 这里需要重新查询,获取key用来更新
prefs_result = store.search(namespace)
if len(prefs_result) == 0:
# 没有持久化信息,就新增
prefs = UserPreferences(
budget_min=updated_state.get('budget_min'),
budget_max=updated_state.get('budget_max'),
)
store.put(
namespace,
str(uuid.uuid4()),
prefs.model_dump(exclude_none=True)
)
# 更新用户偏好
updated_state['user_preferences'] = prefs.model_dump()
else:
# 有持久化信息,判断更新
prefs = prefs_result[0].value
store_min = prefs['budget_min']
store_max = prefs['budget_max']
cur_min = updated_state.get('budget_min')
cur_max = updated_state.get('budget_max')
update_min = False
update_max = False
if store_min and cur_min and cur_min < store_min:
# 都有,就比较
update_min = True
elif not store_min and cur_min:
# store 没有,cur 有,就更新
update_min = True
if store_max and cur_max and cur_max > store_max:
update_max = True
elif not store_max and cur_max:
update_max = True
if update_min or update_max:
if update_min:
prefs['budget_min'] = cur_min
print(f"更新用户最低预算={cur_min}")
if update_max:
prefs['budget_max'] = cur_max
print(f"更新用户最高预算={cur_max}")
store.put(
namespace,
prefs_result[0].key,
prefs
)
# 更新用户偏好
updated_state['user_preferences'] = prefs
# 8. 准备最终消息并更新消息,确保消息列表中包含最新消息
updated_state['messages'] = state['messages'] + [
HumanMessage(content=get_recommend_info(updated_state))
]
# 打印日志
# print("已收集用户信息:城市={updated_state.get('city')},"
# f"区域={updated_state.get('district')},"
# f"预算={updated_state.get('budget_min')}-{updated_state.get('budget_max')}"
# f"房间数={updated_state.get('room_count')}")
return updated_state
4.3.3.2 get_schema节点和run_query节点
get_schema工具节点:获取表的详细信息,如表结构、示例数据等。
run_query工具节点:用来执行SQL。
这两个节点由于数据库交互相关。在langchain中,要想与SQL数据库进行交互,可以到一个SQL交互工具包:SQLDatabase Toolkit
4.3.3.2.1 SQLDatabase Toolkit工具包
SQLDatabase Toolkit是langchain-community中的一个工具包,旨在帮助系统与SQL数据库进行交互。其主要应用是构建能够通过查询关系数据库来回答问题的问答系统,并支持迭代式错误恢复。
SQLDatabase Toolkit主要功能:
- 提供专用工具:
QuerySQLDataBaseTool:执行SQL查询并返回结果。InfoSQLDatabaseTool:获取指定表的schema和示例数据。ListSQLDatabaseTool:列出数据库中的所有表。QuerySQLCheckerTool:在运行前检查SQL查询的正确性。
- 支持智能工作流:Graph可以使用这些工具自主探索数据库结构、编写查询、检查并执行,最终给出自然语言答案。
4.3.3.2.2 接入SQL工具包
安装工具包
SQLDatabase Toolkit工具包包含在langchain-community包中:
bash
pip install -q langchain-community
准备数据表并配置环境变量(.env)
env
# MYSQL 配置
DB_USER=root
DB_PASSWORD=dairenwen1092
DB_HOST=127.0.0.1
DB_PORT=3308
DB_NAME=project
实例化工具
python
db_user = os.getenv('DB_USER')
db_password = os.getenv('DB_PASSWORD')
db_host = os.getenv('DB_HOST')
db_port = os.getenv('DB_PORT')
db_name = os.getenv('DB_NAME')
db = SQLDatabase.from_uri(f"mysql+pymysql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}")
# 获取数据库工具
toolkit = SQLDatabaseToolkit(db=db, llm=model)
tools = toolkit.get_tools()
# [QuerySQLDatabaseTool(description="Input to this tool is a detailed and correct SQL query, output is a result from the database. If the query is not correct, an error message will be returned. If an error is returned, rewrite the query, check the error message, and try again. If you encounter an issue with Unknown column 'xxxx' in 'field list', use sql_db_schema to query the correct table fields.", db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x103d5fa60>),
# InfoSQLDatabaseTool(description='Input to this tool is a comma-separated list of tables, output is the schema and sample rows for those tables. Be sure that the tables actually exist by calling sql_db_list_tables first! Example Input: table1, table2, table3', db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x103d5fa60>),
# ListSQLDatabaseTool(<langchain_community.utilities.sql_database.SQLDatabase object at 0x103d5fa60>),
# QuerySQLCheckerTool(description='Use this tool to double check if your query is correct before executing a query. Always use this tool before executing a query with sql_db_query!', db=<langchain_community.utilities.sql_database.SQLDatabase object at 0x103d5fa60>, llm=ChatOpenAI(client=<openai.resources.chat.completions.Completions object at 0x10742d720>, async_client=<openai.resources.chat.completions.AsyncCompletions object at 0x10742f7f0>, root_client=<openai.OpenAI object at 0x103d5fac0>, root_async_client=<openai.AsyncOpenAI object at 0x10742d780>, temperature=0.0, model_kwargs={}, openai_api_key=SecretStr('**********')), llm_kwargs={})]
工具节点封装
python
# 获取表信息
get_schema_tool = next(tool for tool in tools if tool.name == "sql_db_schema")
get_schema_node = ToolNode([get_schema_tool], name="get_schema")
# 根据SQL查询结果
run_query_tool = next(tool for tool in tools if tool.name == "sql_db_query")
run_query_node = ToolNode([run_query_tool], name="run_query")
4.3.3.3 list_tables节点
该节点调用SQL工具,查询表有哪些。也同样可以使用SQLDatabase Toolkit工具包实现。
python
# 节点:获取全量表
def list_tables(state: RecommendState):
tool_call = {
"name": "sql_db_list_tables",
"args": {},
"id": "abc123",
"type": "tool_call",
}
tool_call_message = AIMessage(content="", tool_calls=[tool_call])
list_tables_tool = next(tool for tool in tools if tool.name == "sql_db_list_tables")
tool_message = list_tables_tool.invoke(tool_call)
response = AIMessage(f"可用的表:{tool_message.content}")
return {"messages": [tool_call_message, tool_message, response]}
注意:构造了三个Messages返回,包含【有工具调用的AI消息,工具消息,最终的AI结果消息】,是聊天模型对话模式
4.3.3.4 call_get_schema节点
该节点中,需要LLM绑定get_schema工具,强制调用get_schema工具。
python
# 节点:强制创建一个获取表信息的工具调用
def call_get_schema(state: RecommendState):
llm_with_tools = model.bind_tools([get_schema_tool], tool_choice="any")
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
这一步相当于在构建第一个AI message。前面定义好的
get_schema_node是在构建Tool message。
4.3.3.5 generate_query节点
该节点有两个作用:
- 用来生成查询SQL的工具调用。(生成第一个AI message)
- 生成最终结果。(生成最后一个AI message)
因此需要LLM绑定run_query工具,但无需强制工具调用。
python
# 节点:根据输入判断是否调用查询SQL的工具
def generate_query(state: RecommendState):
system_prompt = """
您是一个设计用于与SQL数据库交互的代理。
需要一个输入问题,创建一个语法正确的{dialect}查询来运行,然后查看查询的结果并返回答案。
需要根据rows_from_table的示例设置真实查询的值。
您可以按行对结果排序,以返回最感兴趣的结果。请始终将查询限制为最多{top_k}个结果。
不要对数据库做任何SQL语句(INSERT,UPDATE,DELETE,DROP等)。
"""
# 构建包含用户信息的系统提示
system_message = SystemMessage(content=system_prompt.format(
dialect=db.dialect,
top_k=state.get("room_count", 5) or 5
))
# 在这里没有强制工具调用,以允许模型在获得解决方案时自然响应。
llm_with_tools = model.bind_tools([run_query_tool])
response = llm_with_tools.invoke([system_message] + state["messages"])
return {"messages": [response]}
4.3.3.6 check_query节点
该节点的作用是,检查generate_query节点生成的SQL。回顾我们之前设置的条件边,代码如下:
python
def should_continue(state: RecommendState) -> Literal[END, "check_query"]:
messages = state["messages"]
last_message = messages[-1]
if not last_message.tool_calls:
return END
else:
return "check_query"
builder.add_conditional_edges(
"generate_query",
should_continue, # 查看最后一条消息是否是工具调用。
# 是: LLM:(强制调用sql_db_query工具)执行SQL,生成人工用户消息进行检查
# 否: end
[END, "check_query"]
)
当generate_query节点要执行工具调用时,会先走到check_query节点,这表示在执行前进行SQL检查,等待检查完毕后再执行工具。
因此。check_query节点中的LLM必须绑定run_query工具,且设置强制调用!
python
# 节点:强制创建一个调用查询SQL的工具调用
def check_query(state: RecommendState):
check_query_system_prompt = """
你是一个非常注重细节的SQL专家。仔细检查{dialect}查询中的常见错误,包括:
-使用非值使用NOT IN
-使用UNION而非UNION ALL
-使用BETWEEN表示独立范围
-谓词中的数据类型不匹配
-正确引用标识符
-使用正确数量的函数参数
-转换为正确的数据类型
-使用合适的列进行连接
如果存在上述任何错误,请重写查询。如果没有错误,只需复制原始查询即可。
在运行此检查之后,您将调用适当的工具来执行查询。
""".format(dialect=db.dialect)
system_message = SystemMessage(content=check_query_system_prompt)
# 生成人工用户消息进行检查
# 上一个节点是generate_query。如果走到这,必定调用了工具。这样获取到的SQL是准确的。
tool_call = state["messages"][-1].tool_calls[0]
# 将SQL当作用户消息传入进行检查
user_message = HumanMessage(content=tool_call["args"]["query"])
llm_with_tools = model.bind_tools([run_query_tool], tool_choice="any")
response = llm_with_tools.invoke([system_message, user_message])
response.id = state["messages"][-1].id
return {"messages": [response]}
4.4 预定子图--人工介入的预定系统
4.4.1 状态定义
python
from langgraph.graph import MessagesState
# 预定状态
class ReserveState(MessagesState):
title: str # 预定的房源
phone_number: str # 预定电话
id_card: str # 身份证
4.4.2 工作流定义
python
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.prebuilt import ToolNode, tools_condition
from src.agent.node.reserve import (
get_title,
get_phone,
get_id,
add_reserve_message,
call_orders,
generate_orders
)
from src.agent.state.reserve import ReserveState
builder = StateGraph(ReserveState)
builder.add_sequence([get_title, get_phone, get_id, add_reserve_message, call_orders])
builder.add_node("tool_node", ToolNode([generate_orders]))
builder.add_edge(START, "get_title")
builder.add_conditional_edges(
"call_orders",
tools_condition,
{
"tools": "tool_node",
"__end__": END,
},
)
builder.add_edge("tool_node", "call_orders")
reserve_graph = builder.compile()
4.4.3 节点实现
对于预定子图,获取用户预定信息的几个节点,都采用循环验证模式:用户输入有效信息才继续往后执行。
python
import uuid
from typing import Annotated, Any
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.prebuilt import InjectedStore, ToolRuntime
from langgraph.types import interrupt
from langgraph.tools import tool
from src.agent.common.llm import model
from src.agent.common.store import UserPreferences, ReservedInfo
from src.agent.state.reserve import ReserveState
# 节点:获取用户预定房源
def get_title(state: ReserveState):
prompt = "请输入要预定的房源名称"
while True:
title = interrupt(prompt)
if title: # 可以进行验证
return {"title": title}
# 每次验证失败后,提示信息会更新
prompt = f"'{title}' 不是一个有效的房源名称,请更正。"
# 节点:获取用户预定电话
def get_phone(state: ReserveState):
prompt = "请输入要预定的手机号"
while True:
phone_number = interrupt(prompt)
if phone_number: # 可以进行验证
return {"phone_number": phone_number}
# 每次验证失败后,提示信息会更新
prompt = f"'{phone_number}' 不是一个有效的电话,请更正。"
# 节点:获取用户身份证
def get_id(state: ReserveState):
prompt = "请输入要预定的身份证号码"
while True:
id_card = interrupt(prompt)
if id_card:
return {"id_card": id_card}
# 每次验证失败后,提示信息会更正
prompt = f"'{id_card}' 不是一个有效的身份证,请更正。"
# 节点:新增预定消息
def add_reserve_message(state: ReserveState):
reserve_prompt = """根据提供的信息,帮我预定房源。
- 预定的房源标题:{title}
- 用户预定号码:{phone_number}
- 用户身份证号码:{id_card}"""
reserve_message = HumanMessage(content=reserve_prompt.format(
title=state['title'],
phone_number=state['phone_number'],
id_card=state['id_card']
))
return {"messages": [reserve_message]}
# 工具:生成工单
@tool
def generate_orders(phone_number: str, id_card: str, house_title: str,
runtime: ToolRuntime, store: Annotated[Any, InjectedStore()]) -> str:
"""根据用户电话,身份证,预定房源。
Args:
phone_number: 用户电话
id_card: 用户身份证
house_title: 用户预定的房源标题
runtime: 注入工具运行时信息
store: 注入的持久信息
"""
# 1. 生成工单号
order_id = str(uuid.uuid4())
# 2. 构建预定信息
reserved_house = ReservedInfo(
order_id=order_id,
phone_number=phone_number,
house_title=house_title
)
# 3. 持久化用户偏好(预定信息)
user_id = runtime.context.get("user_id")
namespace = (user_id, "preferences")
prefs_result = store.search(namespace)
if len(prefs_result) == 0:
# 没有持久化信息,新增
prefs = UserPreferences(
reserved_info=[reserved_house]
)
store.put(
namespace,
str(uuid.uuid4()),
prefs.model_dump(exclude_none=True)
)
else:
# 有值,更新
prefs = prefs_result[0].value or {}
prefs.setdefault("reserved_info", []).append(reserved_house)
store.put(
namespace,
prefs_result[0].key,
prefs
)
# 4. 扩展:持久化工单表
return f"已成功预定房源:{house_title},预订单号为:{order_id}"
# 节点:生成工单结果
def call_orders(state: ReserveState):
response = model.bind_tools([generate_orders]).invoke(
[SystemMessage(content="你是一个工单生成助手,支持调用工具进行房源预定工单生成。查看工具的结果并返回最终答案")]
+ state["messages"]
)
return {"messages": [response]}
4.5 扩展子图--除业务外的智能问答助手
只用一个py文件进行描述:
python
from langchain_core.messages import AIMessage, SystemMessage
from langgraph.constants import START
from langgraph.graph import StateGraph, MessagesState
from src.agent.common.llm import model
def extend_node(state: MessagesState):
response = model.invoke(
[SystemMessage(content="你是一个乐于助人的助手,可以根据历史对话进行回复。")]
+ state["messages"]
)
return {
"messages": [response]
}
extend_graph = (
StateGraph(MessagesState)
.add_node(extend_node)
# ...
)
5. 项目部署
5.1 自托管部署 & 涉及到的相关组件解释
LangSmith Deployment构建在开源的LangGraph框架上,用于开发有状态的应用程序。LangGraph提供核心抽象和执行模型,而LangSmith支持从开发到生产的整个生命周期,增加了托管基础设施、可观察性、部署选项、助手和并发控制等能力。这会将Agent应用打包、构建为Agent Server,并将其进行部署。其核心由以下几个协同工作的组件构成:
- Agent Server
- 角色:部署和运行图的核心运行时环境。
- 功能:提供标准化的API,处理执行、状态管理和持久化,让开发者专注于业务逻辑而非服务器基础设施。
- LangGraph CLI
- 角色:命令行工具。
- 功能:用于在本地构建、打包图,并与图进行交互,同时为部署到Agent Server做准备。
- Studio
- 角色:集成开发环境。
- 功能:可可视化、交互和调试的专门IDE。可连接本地Agent Server进行开发和测试。
- Python SDK
- 角色:软件开发工具包。
- 功能:为应用程序提供编程接口,以与已部署的图和Agent Server进行交互。
- RemoteGraph
- 角色:本地代理包装器。
- 功能:让你能够像调用本地运行的图一样,与远程部署的图进行交互。
- Control Plane
- 角色:管理和配置层。
- 功能:用于创建、更新和管理Agent Server部署的用户界面和API。
- Data plane
- 角色:执行层。
- 功能:实际运行图的运行时层,包括Agent Server实例及其依赖的后端服务(如PostgreSQL、Redis等)。
简单来说,开发者使用LangGraph CLI和Studio在本地开发和测试图,然后将其部署为Agent Server。应用程序通过【SDK】、【RemoteGraph】或通过【Agent Server提供的API】调用部署好的服务。整个系统的部署和生命周期由Control Plane管理,而具体的任务执行则由Data plane完成。
5.2 本地启动并测试
- 修改
langgraph.json配置文件,添加新增的图:
json
{
"$schema": "https://langgra.ph/schema.json",
"dependencies": ["."],
"graphs": {
"house_agent": "./src/agent/graph.py:graph",
"recommend_agent": "./src/agent/recommend.py:recommended_graph",
"reserve_agent": "./src/agent/reserve.py:reserve_graph",
"extend_agent": "./src/agent/extend.py:extend_graph"
},
"env": ".env",
"image_distro": "wolfi"
}
- 运行
langgraph dev命令启动本地开发服务器。langgraph dev命令会以内存模式启动Agent Server。该模式适合开发和测试。 - 测试Graph,示例测试问题:
python
[
"在西安,我的预算是1000-2000一个月,帮我推荐4套房子。要求在雁塔区。",
"我想在北京海淀租个1室1厅,预算5000以内,最好近地铁。",
"帮我推荐几套房子", # 这个会触发信息收集中断
]
5.3 LangSmith 部署方式
Agent Server 可以根据我们的基础设施采用不同的部署方式:
- 云部署 :云是一种完全托管的模式,其中 LangChain 承担并运营所有 LangSmith 的基础设施和服务:
- 完全托管的基础设施:LangChain 负责所有基础设施、更新、扩展和维护。
- 从 GitHub 部署:连接您的代码库,只需点击几下即可部署。
- 自动化 CI/CD(持续集成/持续部署):构建过程由平台自动处理。
- LangSmith UI:全面访问可观测性、评估、部署管理和工作室。
- 带 Control Plane 的混合/自托管:通过 Control Plane,可以在本地构建 Docker 镜像,将它们推送到 Kubernetes 集群可以访问的注册表中,并使用 LangSmith UI 部署它们。
- 独立服务器:直接部署 Agent Server,不通过 Control Plane 和 LangSmith UI。
5.4 独立部署 Agent Server
5.4.1 工作流程
步骤1:使用 langgraph-cli 或 Studio 在本地定义和测试图形
步骤2:将应用服务打包为 Docker 镜像
步骤3:将 Agent Server 部署到平台:
- Kubernetes:使用 LangSmith Helm 图表在 Kubernetes 集群中运行 Agent Server。这是生产级部署的推荐选项。(官网自行研究)
- Docker:在任何支持 Docker 的计算平台(本地开发机、VM、ECS等)上运行。这最适合于开发或小规模工作负载。
5.4.2 部署前的准备工作
部署 Agent Server 时,实际上部署了一个或多个【图】、一个用于持久化的【数据库】和一个【任务队列】。Agent Server 利用数据库实现持久化和任务队列:
- PostgreSQL 作为 Agent Server 的数据库支持:所有持久化数据(检查点、助手等)都存储在 PostgreSQL 数据库中
- Redis 作为任务队列:被用作发布订阅连接,以实现事件的实时流传输。
如果使用 LangSmith 云部署,这些组件会被自动管理。这里选择的是独立部署 Agent Server,需要自己搭建和管理这些组件。
使用 Docker 启动一个 Redis 容器
bash
sudo docker run -d \
--name redis-6380 \
-p 6380:6379 \
-v redis-data:/data \
redis:7-alpine \
redis-server --appendonly yes --requirepass "your_password"
使用 Docker 快速安装并启动 postgres
bash
# 1. 拉取 PostgreSQL 镜像
docker pull postgres:latest
# 2. 运行 PostgreSQL 容器
# -p 5432:5432:将容器的5432端口映射到宿主机的5432端口
# -e POSTGRES_PASSWORD=bit:设置 PostgreSQL 的postgres用户密码
# --name postgres-sql:给容器命名
# -d:后台运行
docker run --name postgres-sql -e POSTGRES_PASSWORD=bit -p 5432:5432 -d postgres
在 .env 中加入 redis 和 postgres 的配置:
env
# DOCKER 必须
DATABASE_URI=postgresql://postgres:xxx@192.168.100.233:5432/postgres
REDIS_URI=redis://xxx@40123@192.168.100.233:6380
5.4.3 部署姿势 1:使用 LangGraph CLI 构建 Docker 镜像并运行
该方式适合 Redis、PostgreSQL 和 MySQL 已由外部环境提供的场景。
运行前,在项目根目录创建 .env:
env
# 模型配置
PACKYAPI_API_KEY=你的密钥
PACKYAPI_BASE_URL=https://你的兼容接口/v1
PACKYAPI_MODEL=gpt-5.4//或者你自己选择模型
# 外部 MySQL 房源数据库
DB_HOST=你的mysql主机
DB_PORT=3306
DB_USER=数据库用户名
DB_PASSWORD=数据库密码
DB_NAME=rental
# 外部 Redis 与 PostgreSQL
REDIS_URI=redis://你的redis主机:6379/0
DATABASE_URI=postgresql://用户名:密码@你的postgres主机:5432/langgraph?sslmode=disable
# 自托管 LangGraph Server 所需配置
LANGSMITH_API_KEY=你的密钥
LANGGRAPH_CLOUD_LICENSE_KEY=你的许可证
构建镜像:
bash
cd /Users/drw/PycharmProjects/LangChain-LangGraph/Project
langgraph build -t wandor-house-agent:latest
查看镜像:
bash
docker image list | grep wandor-house-agent
启动容器。项目 API 容器内端口为 8000,将宿主机的 50699 映射到该端口:
bash
docker run -d \
--name wandor-house-agent \
--env-file .env \
-p 50699:8000 \
wandor-house-agent:latest
验证服务:
bash
curl http://127.0.0.1:50699/ok
启动成功后可访问:
text
API 文档:http://127.0.0.1:50699/docs
查看日志:
bash
docker logs -f wandor-house-agent
停止并删除容器:
bash
docker stop wandor-house-agent
docker rm wandor-house-agent
5.4.4 部署姿势 2:生成 Dockerfile 并结合 Docker Compose 启动
该方式适合将服务及其依赖统一编排。需要提供完整的 Compose 文件,包含:
backend:LangGraph 房源智能体;frontend:React 对话页面;langgraph-postgres:用户档案、会话线程、检查点和长期记忆;langgraph-redis:流式事件分发;mysql:房源数据;seed-houses:首次启动自动导入 1,000 条中国一二线城市演示房源。
第一步:生成 Dockerfile
如需严格使用 LangGraph CLI 自动生成 Dockerfile,可执行:
bash
cd /Users/drw/PycharmProjects/LangChain-LangGraph/Project
langgraph dockerfile -c langgraph.json Dockerfile.generated
生成后,在 Compose 中将后端构建文件改为:
yaml
backend:
build:
context: .
dockerfile: Dockerfile.generated
重点说明:langgraph.json 更新后,应重新执行该命令生成 Dockerfile。
第二步:结合 Docker compose 启动
参考 1:Redis、PostgreSQL、MySQL 均由外部提供
yaml
services:
langgraph-api:
build:
context: .
dockerfile: Dockerfile.generated
ports:
- "50699:8000"
env_file:
- .env
此时 .env 必须配置外部服务地址:
env
DB_HOST=外部MySQL地址
REDIS_URI=redis://外部Redis地址:6379/0
DATABASE_URI=postgresql://用户名:密码@外部PostgreSQL地址:5432/langgraph?sslmode=disable
启动命令:
bash
docker compose up --build
参考 2:使用本项目内置的完整 Compose
本项目已提供 <docker-compose.yml>,直接执行:
bash
cd /Users/drw/PycharmProjects/LangChain-LangGraph/Project
cp .env.example .env
填写 .env 中的模型密钥和 LangGraph Server 密钥后启动:
bash
docker compose up --build
后台运行:
bash
docker compose up --build -d
查看服务状态:
bash
docker compose ps
查看后端日志:
bash
docker compose logs -f backend
服务地址:
text
前端页面:http://127.0.0.1:5173
API 文档:http://127.0.0.1:50699/docs
健康检查:http://127.0.0.1:50699/ok
源码更新后重新构建指定服务:
bash
docker compose up --build backend
docker compose up --build frontend
停止服务但保留数据库数据:
bash
docker compose down
5.4.5 部署姿势 3:使用 langgraph up 在 Data Plane 中运行
langgraph up 会依赖 Docker 自动构建 LangGraph API 镜像,并启动 Agent Server 所需的数据平面服务。
首先准备 .env:
env
PACKYAPI_API_KEY=你的密钥
PACKYAPI_BASE_URL=https://你的兼容接口/v1
PACKYAPI_MODEL=gpt-5.4
# 房源 MySQL;需提前准备或使用项目 docker-compose 中的 mysql 服务
DB_HOST=你的mysql主机
DB_PORT=3306
DB_USER=数据库用户名
DB_PASSWORD=数据库密码
DB_NAME=rental
LANGSMITH_API_KEY=你的密钥
LANGGRAPH_CLOUD_LICENSE_KEY=你的许可证
启动命令:
bash
cd /Users/drw/PycharmProjects/LangChain-LangGraph/Project
langgraph up -c langgraph.json -p 50699
如果需要指定 PostgreSQL 地址:
bash
langgraph up \
-c langgraph.json \
-p 50699 \
--postgres-uri "postgresql://用户名:密码@postgres主机:5432/langgraph?sslmode=disable"
启动后查看 Docker 镜像:
bash
docker image list
检查服务:
bash
curl http://127.0.0.1:50699/ok
访问接口文档:
text
http://127.0.0.1:50699/docs
该方式中,LangGraph Data Plane 负责线程、运行记录、检查点和长期记忆的 PostgreSQL 持久化;项目仍需额外保证 MySQL 房源库可访问。
作者这里将所有的源码放在了这里:综合案例,可以拉取下来自主实现docker compuse部署,并看看效果,首页如下:
