AI智能租房助手

1.案例需求说明

1.1 案例演示

看下我们要实现的案例:

1.1.2 房源推荐

● 通过对话分析用户需求(预算、地段、通勤、户型偏好等),实时筛选并推荐房源。

● 展示思考过程,如调用了哪些工具、工具调用结果,体现对话结果准确性。

● 若用户需求模糊(如"给我推荐房子"),智能体会追问具体需求("为了给您推荐合适的房源,请提供以下信息 .... ")。

1.1.3 房源预定

● 支持直接预定,也可以通过房源推荐引导预定。

● 引导合同签署,自动生成代办清单(如"下一步需上传身份证号码")。

1.1.4 查询我的

● 支持跨会话查询用户历史预算、已租房源信息

1.1.5 常规问答

支持进行常规问答

1.2 涉及知识点

● 对话式交互

● LangGraph图,包括状态、节点、边和条件边

● 子图与子图中断

● 线程级持久化与跨会话持久化

● 内存存储与Postgres存储

● 路由模式

● 读取SQL数据库的工具

● 人机交互(中断)

● 运行时上下文

● 多种流模式混合输出

● LangGraph 项目结构

● LangGraph 生态部署


2.搭建一个 LangGraph 项目

1、将调用图:invoke、stream 通过三方库 封装成 web http 接口

2、LangChain 拥有整套的生态:编码、部署运维(将项目按照 LangGraph 要求的方式进行搭建)

我们可以自行搭建 Python 服务,基于 LangGraph 图调用(自己制定协议)提供对外服务。也可以使 LangChain生态 中LangSmith Deployment 部署应用程序,这需要我们准备一个结构化的项目。这里我们选择后者。

LangSmith Deployment 构建在开源的 LangGraph框架上,用于开发有状态的应用程序。LangGraph 提供核心抽象和执行模型,而 LangSmith 支持从开发到生产的整个生命周期 ,且增加了托管基础设施、可观察性、部署选项、助手和并发控制等能力。

2.1 搭建项目

2.1.1 步骤1 - 安装LangGraph CLI

使用pip安装 langgraph-cliinmem (需Python 3.11或更高版本)。

复制代码
# 安装 
pip install -U "langgraph-cli[inmem]"
验证(注意:中间不要空格!是 --help,不是 -- help)
langgraph --help
查看版本(推荐,快速确认)
langgraph --version

LangGraph CLI 是用于构建和运行 LangGraph 应用程序的命令行工具,可以将工作流构建成Docker镜像。LangGraph CLI 提供了如下指令:

**langgraph new**是 langgraph-cli 提供的脚手架命令,一键从官方模板生成完整 LangGraph 项目骨架 ,不用手动新建一堆文件、配置langgraph.json,是 LangGraph 官方推荐的项目初始化方式PyPI

由此产生的服务器公开了:

● 助手(Assistants):是图的配置实例

● 线程(Threads):线程包含一组运行的累积输出。实际表示为一组会话。

● 线程执行(Thread Runs):运行是对线程上的图形/助手的调用。它更新线程的状态。

公开的所有API端点见这里。可以看到也包括用于检查点和存储的托管数据库。

2.1.2 步骤2 - 创建应用

使用 langgraph new 命令从指定模板创建新应用。

● 方式1:创建时不指定具体模板。而是通过交互式菜单,允许你从可用模板列表中选择。

可以选择的模板有:

  1. 新LangGraph项目:一个简单、精简且具有记忆功能的聊天机器人。

  2. ReAct代理:一种简单且可灵活扩展至多种工具的代理。

  3. 记忆代理:一种采用ReAct风格的代理,额外配备了一个工具,用于存储记忆,以便在不同对话线程间使用。

  4. 检索代理:包含基于检索的问答系统的代理。

  5. 数据丰富代理:一种执行网络搜索并将搜索结果整理成结构化格式的代理。

方式2:用 new-langgraph-project-python 模板创建一个新应用。这个模板展示了一个单节点应用,你可以用自己的逻辑进行扩展。

复制代码
langgraph new path/to/your/app -- template new-langgraph-project-python

【项目结构】见下文,这里先不讲解。

项目创建完成后,可以使用 PyCharm 打开项目。


2.1.3 步骤3 - 安装依赖

进入应用目录,以编辑模式安装依赖。

复制代码
cd path/to/your/app
pip install -e .

环境准备可能遇到的问题:由于 Debian/Ubuntu 等系统从Python 3.11开始引入了新的保护机制,为了保护 Python 环境不被意外破坏,默认禁止直接使用 pip install 安装包到系统Python。

解决:

1、使用虚拟环境(推荐)

2、在 pip install 命令后添加 -- break-system-packages 参数强制覆盖系统保护(不推荐,急用可临时使用)

2.1.4 步骤4 - 配置环境

复制 . env. example 文件为 .env ,并填入必要的 LANGSMITH_API_KEY

复制代码
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 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

由于我们要完成一个 web 应用,本案例将不采用SDK方式调用,而是选择 REST API 请求。它们的概念、使用流程都一样,只是使用姿势不一样,一通百通。后面我们可以参考 API Docs,使用 API Docs 提供的工具或 Apifox 进行测试。

可以通过 Python SDK(异步/同步)或直接发送 REST API 请求来测试应用功能。

● 对于Python SDK,使用pip install langgraph-sdk先进行安装,再通过其内置接口进行访问。

● 对于REST API,接口文档已经由 LangGraph CLI 提供好了,点击 API Docs 即可查看。

流式调用常见的 stream_mode 有:

可以把列表作为 stream_mode 参数传递,同时放多个模式(如"stream_mode":"updates", "messages")。


这里还需要注意流式返回的结构 ,服务器会以 SSE格式发送一系列事件:

复制代码
event: metadata
data: {"run_id":"019ba0cd-d6b7-7ce0-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 流模式有关

常见返回的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 项目的标准配置文件,替代了传统的 requirements.txt 和 setup.py 文件。主要用于:

● 项目元数据定义(名称、版本、作者等)

● 依赖管理(生产依赖和开发依赖)

● 构建系统配置

● 代码质量工具配置(如ruff代码检查器)

下面对于 pyproject.toml 配置信息进行说明:

1. project - 项目基本信息

复制代码
name = "agent"                                  # 项目名称
version = "0.0.1"                               # 项目版本号
description = "Starter template for making a new agent LangGraph."  # 项目描述
readme = "README.md"                            # 项目说明文档
license = { text = "MIT" }                      # 许可证类型(MIT许可证)
requires-python = ">=3.10"                      # 支持的Python版本(3.10及以上)

2. project.dependencies - 生产环境依赖

复制代码
# 以下列举了本次案例中需要用到的依赖包
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 - 可选依赖(按组)

复制代码
dev = ["mypy>=1.11.1", "ruff>=0.6.1"] # 开发依赖:类型检查 + 代码检查⼯具

4. build-system - 构建系统配置

复制代码
requires = ["setuptools>=73.0.0", "wheel"] # 构建所需⼯具
build-backend = "setuptools.build_meta"    # 构建后端(setuptools)

5. tool.setuptools - 包目录结构配置

复制代码
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 代码检查工具,集成了多种检查器。

复制代码
lint.select = [  # 启用的检查规则
  "E",  # pycodestyle (PEP8代码风格检查)
  "F",  # pyflakes (语法和逻辑错误检查)
  "I",  # isort (导入排序检查)
  "D",  # pydocstyle (文档字符串检查)
  "D401",  # 要求文档字符串第一行用命令式语气
  "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/*" = ["D", "UP"]  # 测试文件中忽略文档检查和语法升级检查
[tool.ruff.lint.pydocstyle]
convention = "google"  # 使用Google风格的文档字符串格式

7.dependency-groups - 依赖组(开发依赖)

复制代码
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",                   # 代码检查器
]

可以看到,这是一个 LangGraph 项目模板配置,用于快速创建基于 LangGraph 框架的AI应用。项目结构主要包含:

● 源代码位于 src/agent/ 目录

● 支持类型提示(包含 py. typed 文件)

● 使用了现代Python开发工具链(ruff、mypy、pytest等)

2.4 langgraph.json 配置文件

要使用 LangSmith Deployment 部署应用程序,核心是通过一个配置文件将各个组件整合起来。默认情况下,CLI会在当前目录查找名为 langgraph.json 的文件。

下表汇总了配置文件的主要字段。

配置键 是否必需 类型 / 选项 描述与示例
$schema 必需 字符串 指向 JSON Schema 以进行验证,例如:"https://langgra.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.0.11+)。需指定认证处理器路径。示例:{"path": "./auth.py:auth", ...}
base_image 可选 字符串 基础 Docker 镜像。默认为 langchain/langgraph-api。可用于固定版本,例如 "langchain/langgraph-server:0.2"。
image_distro (>=0.2.11) 可选 枚举 基础镜像的 Linux 发行版。可选:"debian"(默认)、"wolfi"、"bookworm"、"bullseye"。推荐使用更安全的 "wolfi"。
env 可选 字符串或对象 环境变量配置。可以是 .env 文件路径,或直接是键值对映射。
store 可选 对象 存储配置,用于添加语义搜索和 / 或设置存活时间。index:语义搜索索引配置(需指定 embed 嵌入模型、dims 维度、fields 字段)。ttl:数据过期配置(可设置 refresh_on_read、default_ttl、sweep_interval_minutes)。
checkpointer 可选 对象 检查点配置。ttl:检查点过期策略(strategy、sweep_interval_minutes、default_ttl)。serde(Agent server 0.5+):反序列化行为控制(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" 或 "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.案例思路解析

构建 LangGraph 系统的核心是将流程分解为离散的节点,通过共享的状态连接,每个节点可读取和写入状态,并自主决定后续路径。

3.1 整体思路 - 五步构建法

第一步:工作流分解

● 将流程拆分为独立步骤,每个步骤成为一个节点。如有复杂操作,可拆分为子图处理。

● 绘制节点间连接关系

第二步:节点功能识别

● LLM节点:用于理解、分析、生成文本或推理决策

● 数据节点:从外部源检索信息

● 动作节点:执行外部操作

● 用户输入节点:需要人工干预

第三步:状态设计

● 状态是共享内存,所有节点均可访问

● 只存储原始数据,不存储格式化文本或提示模板

● 设计原则:

。跨步骤需要持久化的数据才存入状态

。可从其他数据推导的信息不存储

第四步:节点实现

● 每个节点是接收状态并返回更新的函数

● 使用 Command 对象指定状态更新和下一节点

● 错误处理策略:

。 瞬时错误(网络问题):自动重试

。 LLM可恢复错误:将错误存入状态,让LLM重试

。 用户可修复错误:使用 interrupt()暂停等待人工输入

。 意外错误:抛出供调试

第五步:连接组装

● 添加节点和必要边

● 使用检查点器( checkpointer )实现持久化,支持暂停/恢复

● 编译为可执行应用


3.2 按功能模块拆分为多图

根据案例需求(房源推荐、房源预定、查询我的、常规问答),可以将不同的能力拆分为不同的子图完成。如:

recommended_graph 推荐子图:负责进行房源推荐,包括查询数据库,根据条件推荐合适的房

源。

reserve_graph 预定子图:负责房源预定,生成工单。

extend_graph 其它子图:负责其它问题处理,如常规问答等。

除此之外,我们还需要一个主图负责路由,判定用户请求该路由到哪个子图中去执行具体逻辑处理。


3.3 主图 - 智能分流

节点设计如下:

● get_store_info 节点:查询持久化消息,例如用户历史偏好数据。

● identify_question 节点:识别用户输入的问题,进行智能路由。

● recommended_graph 子图节点:路由1,进行房源推荐。

● reserve_graph 子图节点:路由2,预定房源子图。

● extend_graph 子图节点:路由3,其它问题处理,如常规问答等。

● get_user_preferences 节点:路由4,用户查询自己的信息。

● need_reserve 节点:是否需要预定房源。当推荐房源结束后,主动咨询是否需要预定房源。


3.4 推荐子图 - 构建自定义SQL代理

节点设计如下:

● collect_user_info 节点:收集用户希望推荐的房源关键信息。

● list_tables 节点:调用SQL工具,查询表有哪些。

● call_get_schema 节点:LLM绑定 get_schema 工具,强制调用 get_schema 工具。

● get_schema 节点:执行工具,获取表的详细信息,如表结构、示例数据等。

● generate_query 节点:LLM绑定 run_query 工具,非强制工具调用。用来生成查询 SQL 或生成最终结果

● check_query 节点:LLM绑定 run_query 工具,强制调用 run_query 工具。

● run_query 节点:执行工具,用来运行 generate_query 节点生成的SQL,检测SQL是否正确。


3.5 预定子图 - 人工介入的预定系统

节点设计如下:

● get_title 节点:中断获取要预定的房源标题

● get_phone 节点:中断获取预定人的电话

● get_id 节点:中断获取预定人的身份证

● add_reserve_message 节点:构造并添加预定消息

● call_orders 节点:LLM绑定生成工单的工具

● tool_node 节点:生成工单的工具

3.6 扩展子图 - 除业务外的智能问答助手

extend_node 节点:LLM对话助手

4.案例开发

4.1 通用设计

4.1.1 LLM

在案例中,无论主图还是子图,涉及到 LLM 调用统一使用OpenAl。

复制代码
from langchain.chat_models import init_chat_model
model = init_chat_model("gpt-4o-mini", temperature=0)

4.1.2 持久化存储

本案例中,由于我们要收集用户偏好信息与历史预定信息,因此要使用持久化存储。

定义存储值的结构:

复制代码
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="预定过的房源列表"
)

将来可以在节点中获取或添加持久化信息,如图所示:

复制代码
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 获取用户持久化信息。

复制代码
from typing import TypedDict
class ContextSchema(TypedDict):
    user_id: str

将来可以在节点中获取上下文信息,如下所示:

复制代码
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:当推荐子图执行完成后,用户获取用户是否想要

预定房源的意向。从而决定是否执行预定子图。

复制代码
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 部署时,持久化可以通过配置进行设置。

复制代码
from typing import Literal
from langgraph.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
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(need_reserve)            # 是否需要预定房源
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")  # 识别问题
def router_message(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",
router_message,  # 消息路由。
["recommended_graph", "reserve_graph", "extend_graph", "get_user_preferences"]
)
路由1:推荐房源->(中断询问)预定房源
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 节点实现

复制代码
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):
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 状态定义

复制代码
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.返回结果

复制代码
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 节点

该节点收集用户希望推荐的房源关键信息。交互流程:

  1. 从历史偏好初始化

  2. 从当前对话提取

  3. 缺失信息中断询问

  4. 持久化更新

    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. 获取需要被解析的数据: 最新的用户消息+偏好数据
    pref = state.get("user_preferences")
    user_messages = filter_messages(state["messages"], include_types="human")
    if pref and (pref['budget_min'] or pref['budget_max']):
    extract_messages = [
    HumanMessage(
    content="用户的历史偏好信息如下: "
    f"1. 最低预算: {pref['budget_min']}"
    f"2. 最高预算: {pref['budget_max']}"
    ),
    user_messages[-1]
    ]
    else:
    extract_messages = [user_messages[-1]]

    复制代码
     # 2. 提取信息函数
     def extract_info(messages) -> UserInfo:
         system_message = SystemMessage(
             content="""

    你是一个租房需求信息提取专家。请从用户的描述与历史信息中提取租房相关信息。

    如果用户历史偏好信息与最新用户消息冲突,以最新的用户消息为主。
    只提取用户明确提到的信息,不要猜测或推断。
    如果某个信息用户没有提到,就返回null。
    注意预算的单位可能是元/月、元/天等,请统一转换为元/月。
    如果用户提到价格范围,请分别提取最低和最高预算。
    如果用户提到推荐几套房,提取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.get('budget_min')}-{updated_state.get('budget_max')}")
         else:
             # 用户提供了更多信息,再次提取
             user_response_msg = HumanMessage(content=str(answer))
             extracted_info = extract_info([user_response_msg])
             updated_state = update_state(updated_state, extracted_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'] = [HumanMessage(content=get_recommend_info(updated_state))]
    
     # 打印日志
     print(f"已收集用户信息: 城市={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 主要功能:

提供专用工具:

  1. QuerySQLDatabaseTool:执行SQL查询并返回结果。
  2. InfoSQLDatabaseTool:获取指定表的schema和示例数据。
  3. ListSQLDatabaseTool:列出数据库中的所有表。
  4. QuerySQLCheckerTool:在运行前检查SQL查询的正确性。

**持智能体工作流:**Graph可以使用这些工具自主探索数据库结构、编写查询、检查并执行,最终给出自然语言答案。

4.3.3.2.2 接入SQL工具包

● 安装工具包:SQLDatabase Toolkit 工具包包含在 langchain-community 包中:

复制代码
pip install -qU langchain-community

● 准备数据表并配置SQL环境变量(.env)

复制代码
# MYSQL 配置
DB_USER=bitedev
DB_PASSWORD=bite%40123
DB_HOST=192.168.100.233
DB_PORT=3308
DB_NAME=bitehouse_prd

● 实例化工具

复制代码
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 query, 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(db= <langchain_community.utilities.sql_database.SQLDatabase object at 0x103d5fa60>),
# QuerySQLCheckerTool(description='Use this tool to double check if your query is correct before executing it. 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_chain=LLMChain(verbose=False, prompt=PromptTemplate(input_variables=['dialect', 'query'], input_types={}, partial_variables={}, template='\n{query}\nDouble check the {dialect} query above for common mistakes, including:\n- Using NOT IN with NULL values\n- Using UNION when UNION ALL should have been used\n- Using BETWEEN for exclusive ranges\n- Data type mismatch in predicates\n- Properly quoting identifiers\n- Using the correct number of arguments for functions\n- Casting to the correct data type\n- Using the proper columns for joins\n\nIf there are any of the above mistakes, rewrite the query. If there are no mistakes, just reproduce the original query.\n\nOutput the final SQL query only.\n\nSQL Query: '), 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('**********')), output_parser=StrOutputParser(), llm_kwargs={})),]

● 工具节点封装

复制代码
# 获取表信息
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工具包实现。

复制代码
# 节点: 获取全量表
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 工具。

复制代码
# 节点: 强制创建一个获取表信息的工具调用
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]}

这一步相当于在构建第一个 Al message。前面我们定义好的 get_schema_node 是在构建 Tool messageo

4.3.3.5 generate-query 节点

该节点有两个作用:

1、用来生成查询SQL的工具调用。(生成第一个Al message)

2、生成最终结果。(生成最后一个Al message)

因此需要LLM绑定 run_query 工具,但无需强制工具调用。

复制代码
# 节点: 根据输入判断是否调用查询SQL的工具
def generate_query(state: RecommendState):
    generate_query_system_prompt = """
您是一个设计用于与SQL数据库交互的代理。
给定一个输入问题,创建一个语法正确的{dialect}查询来运行,然后查看查询的结果并返回答案。
需要根据rows from table的示例设置真实查询的值。
除非用户指定了他们希望获得的特定数量的示例,否则始终将查询限制为最多{top_k}个结果。
您可以按相关列对结果排序,以返回最感兴趣的结果。不要查询特定表中的所有列,只查询给定问题的相关列。
不要对数据库做任何DML语句(INSERT,UPDATE,DELETE,DROP等)。
"""
    # 构建包含用户信息的系统提示
    system_prompt = generate_query_system_prompt.format(
        dialect=db.dialect,
        top_k=state.get('room_count', 5) or 5
    )
    system_message = SystemMessage(content=system_prompt)

    # 在这里没有强制工具调用,以允许模型在获得解决方案时自然响应。
    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。回顾我们之前设置的条件边,代码如下:

复制代码
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 工具,且设置强制调用!

复制代码
# 节点: 强制创建一个调用查询SQL的工具调用
def check_query(state: RecommendState):
    check_query_system_prompt = """
你是一个非常注重细节的SQL专家。仔细检查{dialect}查询中的常见错误,包括:
-使用NULL值的NOT IN
-在应该使用UNION ALL时使用UNION
-使用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 状态定义

复制代码
from langgraph.graph import MessagesState

# 预定状态
class ReserveState(MessagesState):
    title: str            # 预定的房源
    phone_number: str     # 预定电话
    id_card: str          # 身份证

4.4.2 工作流定义

复制代码
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 节点实现

对于预定子图,获取用户预定信息的几个节点,都采用循环验证模式:用户输入有效信息才继续往后执行。代码如下:

复制代码
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 langchain.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]}


# 工具: 生成工单
# store: Annotated[Any, InjectedStore()]
@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,
        title=house_title,
        phone_number=phone_number
    )

    # 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文件进行描述:

复制代码
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)
    .add_edge(START, "extend_node")
    .compile()
)
相关推荐
浮生若城1 小时前
MySQL:库与表的操作
数据库·mysql
薛定谔的悦1 小时前
光储充一体机从主控到执行单元的双层架构
数据库·mongodb·架构
数字新视界1 小时前
动环监控信创适配在智能机房管理中的应用价值与未来展望
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
csdn_aspnet2 小时前
Redis 入门:Windows、Mac安装指南
数据库·windows·redis·缓存·mac
山甫aa3 小时前
锁一篇搞定:乐观锁、悲观锁、CAS、数据库锁与分布式锁【从零开始的 Web 后端学习】
java·数据库·分布式·后端·学习·web
Nturmoils14 小时前
集中式与分布式怎么选:从订单读写看 KingbaseES 的架构边界
数据库
xiaomuzuirenzhen14 小时前
大专电子商务从事电商运营分析师需要考证吗?
大数据·数据库
翔云12345616 小时前
RocksDB中,客户端每写入一次数据,需要访问几次磁盘
运维·数据库·mysql
DevOpenClub16 小时前
职教高考控制线 API:招生类别枚举、筛选与空分数含义
java·数据库·高考