智能体AI项目:搭建诊所客服聊天机器人

0 阅读20分钟

Image

基于LangGraph与OpenAI模型,端到端实现智能体AI客服及预约聊天机器人

Image

引言

医疗行政工作往往繁琐且耗时。从预约挂号到管理医生出诊时间,诊所面临诸多组织管理层面的挑战。设想一套能通过对话式AI界面自动化完成全流程预约挂号的系统。

本文将讲解如何搭建一款智能诊所预约聊天机器人,使用LangGraph实现状态驱动的智能体调度,OpenAI的gpt 4o-mini完成自然语言理解,SQLite实现数据持久化,以及Streamlit打造直观的用户界面。

在这份完整指南中,我们将逐步拆解诊所预约系统的整体架构,详解每个组件的作用,以及各组件如何协同工作,实现流畅的预约体验。

Image

整体解决方案设计

问题背景

诊所通常依赖人工流程或零散的系统管理患者预约,这会导致等待时间过长、排班冲突、医护人员工作效率低下、患者体验差等问题。患者可能难以查询可接诊医生、选择对应科室,或是便捷完成预约。诊所需要一套自动化智能系统,能够通过自然对话处理预约挂号,同时精准记录医生、患者及预约信息。

解决方案

本文提出的方案是一款AI驱动的诊所预约聊天机器人,结合前沿AI技术与网页技术,实现预约流程自动化。

系统使用LangGraph管理多步骤对话工作流,OpenAI的gpt-4o-mini理解用户自然语言请求,SQLite存储医生、患者及预约数据,Streamlit提供交互式聊天界面。

通过引导式对话,聊天机器人可实现:

  1. 问候用户并询问是否需要预约挂号

  2. 展示可预约的医学科室

  3. 显示对应医生及可预约时段

  4. 收集患者信息(姓名与电话)

  5. 确认预约并将预约信息存入数据库

该方案减轻行政工作负担、提升预约效率、避免排班冲突,同时通过AI对话界面为患者提供流畅的使用体验。

接下来我们开始实操。

先预览我们即将搭建的成品效果:

完整的端到端代码可参考我的GitHub仓库。我会在该仓库持续更新更多智能体AI应用案例,欢迎Star支持。

环境配置

  1. 创建虚拟环境
Mac / Linux / Windows
python -m venv .venv

2. 激活虚拟环境
Mac / Linux

source .venv/bin/activate

Windows(PowerShell)

.venv\Scripts\Activate.ps1

Windows(命令提示符)

.venv\Scripts\activate

激活后,终端会显示类似内容:

(.venv) your-folder-name %

3. 安装依赖包
以下是requirements.txt文件内容:

streamlit>=1.50.0
python-dotenv1.0.0
pydantic2.12.5
pandas2.3.3
python-dateutil2.8.2
langgraph>=1.0.7
openai>=2.16.0
pygraphviz==1.14

在终端执行:

cd clinic-agent
pip install -r requirements.txt
  1. 配置环境变量
    clinic-agent目录下创建.env文件:
OPENAI_API_KEY=your_openai_api_key_here

第一部分:数据库搭建(data/db.py)

任何预约系统的基础都是稳定的数据库。本诊所系统采用SQLite,包含三张核心表:医生表、患者表、预约表。

三张表相互关联,分别存储医生信息、患者信息及已预约记录。

数据库设计保证数据结构清晰、关联完整,便于预约流程中快速查询调用。

1. 医生表

医生表用于存储诊所内出诊医生的相关信息。
每条记录对应一位医生,包含其专业科室与出诊时间。聊天机器人可根据用户选择的科室,匹配对应出诊医生。

关键字段:
doctor_id:医生唯一标识
doctor_name:医生姓名
speciality:医学专业科室
office_timing:医生出诊时间

本案例设置5个科室:

  • 皮肤科医生

  • 骨科医生

  • 全科医生

  • 儿科医生

  • 耳鼻喉科医生

下表为预设的医生姓名与出诊时间,你可自行扩展更多科室。

Image

医生表

2. 患者表

患者表用于存储预约所需的患者信息。
用户每次预约时,系统会通过手机号校验该患者是否已存在;若不存在,则新建一条患者记录。

关键字段:
customer_id:患者唯一标识
name:患者姓名
phone:患者联系电话

3. 预约表

预约表是存储预约信息的核心数据表,每条预约记录关联一位医生一位患者,并记录具体预约日期与时间。

关键字段:
booking_id:预约单唯一标识
doctor_id:关联接诊医生
customer_id:关联预约患者
appointment_date:预约日期
appointment_time:预约时间
status:预约状态(已确认、已取消等)

4. 创建数据库

我们先按上述设计定义数据库表结构,创建clinic.db数据库。

# data/db.py - 数据库初始化与操作

import sqlite3
import os
from datetime import datetime, timedelta

DB_PATH = os.path.join(os.path.dirname(__file__), "clinic.db")

def get_connection():
    """获取数据库连接"""
    return sqlite3.connect(DB_PATH)

def init_db():
    """初始化数据库,创建表并插入示例数据"""
    conn = get_connection()
    cursor = conn.cursor()

    # 医生表
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS doctors (
            doctor_id TEXT PRIMARY KEY,
            doctor_name TEXT NOT NULL,
            speciality TEXT NOT NULL,
            office_timing TEXT NOT NULL
        )
    """)

    # 患者表
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS customers (
            customer_id TEXT PRIMARY KEY,
            name TEXT NOT NULL,
            phone TEXT NOT NULL
        )
    """)

    # 预约表
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS bookings (
            booking_id TEXT PRIMARY KEY,
            doctor_id TEXT NOT NULL,
            customer_id TEXT NOT NULL,
            appointment_date TEXT NOT NULL,
            appointment_time TEXT NOT NULL,
            status TEXT NOT NULL,
            FOREIGN KEY (doctor_id) REFERENCES doctors (doctor_id),
            FOREIGN KEY (customer_id) REFERENCES customers (customer_id)
        )
    """)

    # 插入示例医生数据
    doctors = [
        ("D1""阿尼尔·夏尔马医生""全科医生""10:00-14:00"),
        ("D2""内哈·维尔马医生""皮肤科医生""11:00-16:00"),
        ("D3""罗希特·梅塔医生""骨科医生""09:00-13:00"),
        ("D4""卡维塔·拉奥医生""儿科医生""10:00-15:00"),
        ("D5""桑杰·耶尔医生""耳鼻喉科医生""12:00-17:00"),
    ]

    for doctor in doctors:
        cursor.execute(
            "INSERT OR IGNORE INTO doctors (doctor_id, doctor_name, speciality, office_timing) VALUES (?, ?, ?, ?)",
            doctor
        )

    conn.commit()
    conn.close()

if __name__ == "__main__":
    print("正在初始化数据库...")
    init_db()
    print("数据库初始化完成。")

在终端执行以下命令,运行数据库初始化脚本db.py

cd clinic-agent
python data/db.py

执行后将生成包含三张表的clinic.db数据库文件。

第二部分:服务层(智能体调用工具)

服务层封装业务逻辑,实现智能体与数据库的交互。
LangGraph智能体不直接查询数据库,而是调用服务层函数完成查询医生信息、生成可预约时段、确认预约等操作。

在智能体架构中,这类服务可视为智能体执行操作的工具,智能体本身则专注于管理对话流程。

医生服务(services/doctor_service.py)

医生服务封装与医生相关的操作,提供科室、医生、时段管理的业务逻辑,主要功能:

  • 从数据库查询可预约科室列表

  • 根据所选科室获取医生详情

  • 根据医生出诊时间生成可预约时段

  • 展示时段(如下午2:00) 转换为24小时制用于数据库存储

# services/doctor_service.py - 医生相关操作

from data.db import get_all_doctors, get_doctor_by_speciality, get_doctor_by_id

def get_specialities_list():
    """获取所有科室列表"""
    doctors = get_all_doctors()
    # 返回不重复的科室
    return list(dict.fromkeys([doc[2] for doc in doctors]))

def get_doctor_info(speciality):
    """根据科室获取医生信息"""
    doctor = get_doctor_by_speciality(speciality)
    if doctor:
        return {
            "doctor_id": doctor[0],
            "doctor_name": doctor[1],
            "speciality": doctor[2],
            "office_timing": doctor[3]
        }
    return None

def generate_time_slots(office_timing):
    """根据出诊时间生成按小时划分的预约时段

    参数:
        office_timing: 格式为"11:00-16:00"的字符串

    返回:
        预约时段列表,如["11:00 AM", "12:00 PM", ...]
    """
    start_time, end_time = office_timing.split("-")
    start_hour = int(start_time.split(":")[0])
    end_hour = int(end_time.split(":")[0])

    slots = []
    for hour in range(start_hour, end_hour):
        if hour < 12:
            suffix = "上午"
            display_hour = hour if hour > 0 else 12
        elif hour == 12:
            suffix = "下午"
            display_hour = 12
        else:
            suffix = "下午"
            display_hour = hour - 12
        slots.append(f"{display_hour}:00 {suffix}")

    return slots

def parse_time_slot(slot_str):
    """将预约时段字符串转换为24小时制

    参数:
        slot_str: 格式为"1:00 PM"的字符串

    返回:
        格式为"13:00"的字符串
    """
    time_part, suffix = slot_str.split(" ")
    hour, minute = time_part.split(":")
    hour = int(hour)

    if suffix == "PM" and hour != 12:
        hour += 12
    elif suffix  "AM" and hour  12:
        hour = 0

    return f"{hour:02d}:{minute}"
预约服务(services/booking_service.py)

预约服务处理患者管理与预约相关操作,主要功能:

  • 通过手机号新建患者记录或查询已有患者

  • 查询医生某一日期已被预约的时段

  • 筛选并返回可预约时段

  • 生成唯一预约单号,并将预约信息存入数据库

  • 确认并保存最终预约详情(医生、患者、日期、时间)

# services/booking_service.py - 预约相关操作

import uuid
from datetime import datetime
from data.db import (
    create_customer,
    create_booking,
    get_customer_by_phone,
    get_bookings_by_doctor_and_date,
    get_booking_by_id
)
from services.doctor_service import parse_time_slot

def get_or_create_customer(name, phone):
    """查询已有患者或新建患者"""
    customer = get_customer_by_phone(phone)
    if customer:
        return customer[0]  # 返回患者ID

    customer_id = f"CUST-{uuid.uuid4().hex[:6].upper()}"
    create_customer(customer_id, name, phone)
    return customer_id

def get_available_slots(doctor_id, office_timing):
    """获取医生当日可预约时段

    参数:
        doctor_id: 医生ID
        office_timing: 格式为"11:00-16:00"的出诊时间字符串

    返回:
        可预约时段列表
    """
    from services.doctor_service import generate_time_slots

    today = datetime.now().strftime("%Y-%m-%d")
    all_slots = generate_time_slots(office_timing)

    # 查询已预约时段
    booked_times = get_bookings_by_doctor_and_date(doctor_id, today)

    # 过滤已预约时段
    available = []
    for slot in all_slots:
        slot_24h = parse_time_slot(slot)
        if slot_24h not in booked_times:
            available.append(slot)

    return available

def confirm_booking(doctor_id, customer_name, customer_phone, time_slot, appointment_date=None):
    """确认预约

    参数:
        doctor_id: 医生ID
        customer_name: 患者姓名
        customer_phone: 患者电话
        time_slot: 预约时段,如"1:00 PM"
        appointment_date: 可选,格式为YYYY-MM-DD的预约日期,默认为当日

    返回:
        预约单号
    """
    # 查询或创建患者
    customer_id = get_or_create_customer(customer_name, customer_phone)

    # 生成预约单号
    booking_id = f"BKG-{uuid.uuid4().hex[:6].upper()}"

    # 格式化预约时间
    if not appointment_date:
        appointment_date = datetime.now().strftime("%Y-%m-%d")
    appointment_time = parse_time_slot(time_slot)

    # 创建预约记录
    create_booking(booking_id, doctor_id, customer_id, appointment_date, appointment_time)

    return booking_id
服务层测试
# test/test_service.py - 测试已创建的服务

from pathlib import Path
import sys

# 允许直接运行该文件:`python test/test_service.py`
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from services.doctor_service import get_specialities_list, generate_time_slots
from services.booking_service import confirm_booking

# 获取所有科室
specialities = get_specialities_list()
print("可预约科室:", specialities)

# 为医生生成预约时段(上午11:00 - 下午4:00)
slots = generate_time_slots("11:00-16:00")
print("可预约时段:", slots)

# 确认预约
booking_id = confirm_booking(
    doctor_id="D1",
    customer_name="张三",
    customer_phone="13800138000",
    time_slot="2:00 PM"
)
print(f"预约已确认:{booking_id}")

在终端执行:

cd clinic-agent
python test_service.py

终端会输出以下内容:

(.venv) (base) my-mac clinic-agent % python test_service.py
可预约科室: ['全科医生', '皮肤科医生', '骨科医生', '儿科医生', '耳鼻喉科医生']
可预约时段: ['11:00 上午', '12:00 下午', '1:00 下午', '2:00 下午', '3:00 下午']
预约已确认:BKG-C4F60A

完美!服务层运行正常!

接下来进入最核心的部分:智能体层。

第三部分:智能体层

聊天机器人的核心是基于LangGraph的智能体,通过多个阶段管理完整的预约工作流。

预约状态

我们首先定义预约状态,这是存储对话全程所有信息的结构化记忆体。

核心作用:

  • 跟踪当前对话阶段(如问候、选择医生等),引导工作流执行

  • 存储用户选择的科室、医生、时段等信息

  • 维护患者与预约详情,支撑智能体完成预约流程

# agents/booking_agent.py - LangGraph智能体实现

from typing import TypedDict, Annotated, List, Optional
from langgraph.graph import StateGraph, END
from openai import OpenAI
import os
from dotenv import load_dotenv

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

class BookingState(TypedDict):
    """预约对话状态"""
    messages: List[dict]                    # 聊天记录
    stage: str                               # 问候、选择科室、选择医生等阶段
    selected_speciality: Optional[str]      # 所选医学科室
    selected_doctor: Optional[dict]         # 所选医生信息
    selected_date: Optional[str]            # 预约日期
    selected_slot: Optional[str]            # 预约时段
    customer_name: Optional[str]            # 患者姓名
    customer_phone: Optional[str]           # 患者电话
    booking_id: Optional[str]               # 预约确认单号
    available_options: List[str]            # 界面可选选项

def create_initial_state():
    """创建对话初始状态"""
    return {
        "messages": [],
        "stage""greeting",
        "selected_speciality": None,
        "selected_doctor": None,
        "selected_date": None,
        "selected_slot": None,
        "customer_name": None,
        "customer_phone": None,
        "booking_id": None,
        "available_options": []
    }
大语言模型工具函数

接下来定义call_llm函数,作为与OpenAI模型交互的统一入口。

核心作用:

  • 向大模型发送系统提示与用户提示

  • 返回智能体所需的生成结果

  • 保证智能体代码中大模型交互逻辑统一、可复用

# agents/booking_agent.py - LangGraph智能体实现
def call_llm(
    system_prompt: str,
    user_prompt: str,
    *,
    model: str = "gpt-4o-mini",
    temperature: float = 0,
    max_tokens: int = 50,
) -> str:
    """
    统一封装大模型调用工具
    返回助手回复内容
    """
    try:
        response = client.chat.completions.create(
            model=model,
            messages=[
                {"role""system""content": system_prompt},
                {"role""user""content": user_prompt},
            ],
            temperature=temperature,
            max_tokens=max_tokens,
        )
        return response
    except Exception as e:
        print(f"大模型调用异常:{e}")
        return ""
智能体节点

代表预约工作流中处理特定任务的独立步骤。

核心作用:

  • 处理问候、选择科室、确认预约等不同阶段

  • 处理用户输入并更新对话状态

  • 保证聊天机器人工作流模块化、易维护

智能体中定义多个节点,每个节点对应一个处理阶段:

  1. greeting_node:问候用户并询问是否需要预约

  2. select_speciality_node:展示可预约科室

  3. select_doctor_node:展示所选科室对应的医生

  4. select_date_node:供用户选择预约日期

  5. select_slot_node:展示可预约时段

  6. confirm_node:请求用户确认预约

  7. collect_details_node:收集患者姓名与电话

  8. completed_node:确认预约完成

  9. cancelled_node:处理预约取消操作

构建流程图

接下来通过连接智能体节点,搭建状态驱动的对话工作流。

build_booking_graph函数核心作用:

  • 添加代表不同预约阶段的节点

  • 定义节点间的条件跳转逻辑

  • 构建聊天机器人完整对话路径

# agents/booking_agent.py - LangGraph智能体实现
from langgraph.checkpoint.memory import MemorySaver

def build_booking_graph():
    """构建LangGraph工作流"""
    workflow = StateGraph(BookingState)

    # 添加所有节点
    workflow.add_node("greeting", greeting_node)
    workflow.add_node("select_speciality", select_speciality_node)
    workflow.add_node("select_doctor", select_doctor_node)
    workflow.add_node("select_date", select_date_node)
    workflow.add_node("select_slot", select_slot_node)
    workflow.add_node("confirm", confirm_node)
    workflow.add_node("collect_details", collect_details_node)
    workflow.add_node("completed", completed_node)
    workflow.add_node("cancelled", cancelled_node)

    # 设置入口节点
    workflow.set_entry_point("greeting")

    # 添加基于路由的条件边
    workflow.add_conditional_edges(
        "greeting",
        llm_router,
        {
            "greeting""greeting",
            "select_speciality""select_speciality",
            "cancelled""cancelled"
        }
    )

    # 其他节点的条件边配置同理...

    # 最终指向结束节点
    workflow.add_edge("completed", END)
    workflow.add_edge("cancelled", END)

    # 编译并添加内存检查点用于会话管理
    return workflow.compile(checkpointer=MemorySaver())

# 创建编译后的流程图
booking_graph = build_booking_graph()

接下来通过save_langgraph_flow.py可视化流程图:

#agents/save_langgraph_flow.py
"""将诊所预约LangGraph流程保存为PNG格式至当前文件夹"""

from pathlib import Path
import sys

# 保证脚本可在项目根目录或当前文件夹运行
AGENTS_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = AGENTS_DIR.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from agents.booking_agent import booking_graph  # noqa: E402

def save_graph_files() -> None:
    """导出流程图为PNG"""
    graph = booking_graph.get_graph()

    png_path = AGENTS_DIR / "langgraph_flow.png"
    png_data = graph.draw_mermaid_png()
    png_path.write_bytes(png_data)
    print(f"流程图PNG已保存至:{png_path}")

if __name__ == "__main__":
    save_graph_files()

在终端执行以下命令:

cd clinic-agent
python save_langgraph_flow.py

执行后会在agents文件夹下生成langgraph_flow.png

Image

LangGraph流程图

消息处理

该函数接收用户输入,并通过LangGraph工作流处理。

核心操作:

  • 接收来自用户界面的消息

  • 将消息传入智能体流程图处理

  • 返回更新后的状态与助手回复

# agents/booking_agent.py - LangGraph智能体实现
def process_message(state: BookingState, user_message: str, thread_id: str = "default_session") -> BookingState:
    """通过预约流程图处理用户消息"""
    config = {"configurable": {"thread_id": thread_id}}

    # 检查流程图是否处于中断状态
    current_state = booking_graph.get_state(config)

    if current_state.tasks and current_state.tasks[0].interrupts:
        # 使用用户输入恢复流程图执行
        result = booking_graph.invoke(Command(resume=user_message), config=config)
    else:
        # 无中断,正常启动/继续流程
        # 若为初始问候则不重复添加用户消息
        if user_message.lower() != "hi" or state["messages"]:
            # 避免重复添加用户消息
            if not state["messages"] or state["messages"][-1].get("content") != user_message:
                state["messages"].append({
                    "role""user",
                    "content": user_message
                })
        # 运行流程图
        result = booking_graph.invoke(state, config=config)

    # 更新可选选项并确保消息存入聊天记录
    snapshot = booking_graph.get_state(config)
    if snapshot.tasks and snapshot.tasks[0].interrupts:
        interrupt_value = snapshot.tasks[0].interrupts[0].value

        # 同时兼容字典与字符串类型的中断值
        msg_content = ""
        options = []
        if isinstance(interrupt_value, dict):
            msg_content = interrupt_value.get("content""")
            options = interrupt_value.get("available_options", [])
        else:
            msg_content = str(interrupt_value)

        # 确保中断消息存入聊天记录
        if msg_content:
            # 检查节点是否已添加该消息
            last_msg_content = result["messages"][-1].get("content"""if result["messages"else ""
            if last_msg_content != msg_content:
                result["messages"].append({
                    "role""assistant",
                    "content": msg_content,
                    "options": options
                })
            else:
                # 若已添加则补充缺失的选项
                result["messages"][-1]["options"] = options

        result["available_options"] = options
    else:
        # 无中断时使用状态内设置,默认空列表
        if "available_options" not in result:
            result["available_options"] = []

    return result
智能体测试

接下来测试智能体功能:

# test/test_agent.py - 智能体测试
# 初始化状态

from pathlib import Path
import sys

# 允许直接运行该文件:`python test/test_agent.py`
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from agents.booking_agent import create_initial_state, process_message

state = create_initial_state()

# 处理消息
state = process_message(state, "Hi", thread_id="session_1")
print(state["messages"][-1]["content"])

state = process_message(state, "I want to book", thread_id="session_1")
print(state["available_options"])

在终端执行:

cd clinic-agent
python test/test_agent.py

终端会正常展示可选择项。

接下来我们基于Streamlit搭建用户界面。

第四部分:Streamlit界面

Streamlit界面为用户提供美观、交互式的聊天体验。

功能特点:

  • 实时聊天界面

  • 可点击的选项按钮

  • 唯一ID会话管理

# ui/chat_ui.py - 诊所预约聊天机器人Streamlit界面

"""诊所预约聊天机器人Streamlit界面"""

import streamlit as st
from agents.booking_agent import create_initial_state, process_message
from data.db import init_db

def initialize_session():
    """初始化会话状态"""
    if "state" not in st.session_state:
        st.session_state.state = create_initial_state()
    if "initialized" not in st.session_state:
        st.session_state.initialized = False
    if "session_id" not in st.session_state:
        import uuid
        st.session_state.session_id = str(uuid.uuid4())

def display_chat_history():
    """展示聊天记录,保留选项与样式"""
    messages = st.session_state.state.get("messages", [])
    for i, message in enumerate(messages):
        if message["role"] == "assistant":
            with st.chat_message("assistant"):
                st.markdown(message["content"])

                # 存在选项则展示
                options = message.get("options", [])
                if options:
                    # 若为最新消息且未完成预约,展示可点击按钮
                    if i == len(messages) - 1 and st.session_state.state["stage"] not in ["completed""cancelled"]:
                        st.markdown("---")
                        # 创建按钮列
                        cols = st.columns(min(len(options), 3))
                        for idx, option in enumerate(options):
                            col_idx = idx % 3
                            with cols[col_idx]:
                                if st.button(option, key=f"btn_{i}_{idx}", use_container_width=True):
                                    handle_user_input(option)
                    else:
                        # 历史消息以标签形式展示选项
                        options_str = "  ".join([f"`{opt}`" for opt in options])
                        st.markdown(f"**可选选项:** {options_str}")
        else:
            with st.chat_message("user"):
                st.markdown(message["content"])

def handle_user_input(user_input: str):
    """处理用户输入并通过智能体执行"""
    # 处理消息
    st.session_state.state = process_message(
        st.session_state.state,
        user_input,
        thread_id=st.session_state.session_id
    )

    # 刷新界面更新
    st.rerun()

def run_chat_ui():
    """运行聊天界面"""
    # 页面配置
    st.set_page_config(
        page_title="康护佳诊所——预约挂号",
        page_icon="🏥",
        layout="centered"
    )

    # 自定义CSS区分聊天消息
    st.markdown("""
        <style>
        [data-testid="stChatMessageUser"] {
            flex-direction: row-reverse;
            text-align: right;
            background-color: #e0f2f1;
            border-radius: 15px 15px 0px 15px;
        }
        [data-testid="stChatMessageAssistant"] {
            background-color: #f5f5f5;
            border-radius: 15px 15px 15px 0px;
        }
        </style>
    """, unsafe_allow_html=True)

    # 初始化数据库
    init_db()

    # 初始化会话
    initialize_session()

    # 标题
    st.title("🏥 康护佳诊所")
    st.markdown("*轻松预约医生号源*")
    st.markdown("---")

    # 未初始化时发送初始问候
    if not st.session_state.initialized:
        st.session_state.state = process_message(
            st.session_state.state,
            "Hi",
            thread_id=st.session_state.session_id
        )
        st.session_state.initialized = True
        st.rerun()

    # 展示聊天记录
    display_chat_history()

    # 预约未完成时显示输入框
    if st.session_state.state["stage"] not in ["completed""cancelled"]:
        if prompt := st.chat_input("在此输入消息..."):
            handle_user_input(prompt)
    else:
        # 完成后显示重新预约按钮
        st.markdown("---")
        if st.button("🔄 重新预约", use_container_width=True):
            st.session_state.state = create_initial_state()
            st.session_state.initialized = False
            st.rerun()

第五部分:应用入口文件

接下来定义app.py,作为启动整个聊天机器人应用的入口。

# app.py - 应用主入口

from ui.chat_ui import run_chat_ui

if __name__ == "__main__":
    run_chat_ui()

运行聊天机器人:两种方式

方式一:Streamlit网页应用

最简单、用户体验最优的运行方式。

执行以下命令启动应用:

streamlit run app.py

聊天机器人将在http://localhost:8501打开。

Image

预约聊天机器人Streamlit应用界面

方式二:Jupyter Notebook

适用于开发、测试与调试聊天机器人逻辑。

在Jupyter Notebook中导入所需函数:

# clinic-agent.ipynb
from services.doctor_service import get_specialities_list, get_doctor_info, generate_time_slots
from services.booking_service import confirm_booking
from agents.booking_agent import (
    BookingState,
    create_initial_state,
    build_booking_graph,
    process_message
)

随后构建流程图并在Notebook中可视化:

# clinic-agent.ipynb
# 初始化预约流程图
booking_graph = build_booking_graph()

## 可视化预约流程图结构
from IPython.display import Image, display

png_bytes = booking_graph.get_graph().draw_mermaid_png()
display(Image(png_bytes))
运行机器人会话:

在Notebook内运行交互式聊天机器人会话:

# clinic-agent.ipynb
from langgraph.types import Command

def run_booking_session(graph, thread_id="notebook_session", reset=False):
    config = {"configurable": {"thread_id": thread_id}}

    # 1. 启动或重置逻辑
    current_state = graph.get_state(config)
    if reset or not current_state.values:
        print(f"--- {'🔄 正在重置' if reset else '🆕 正在初始化'} 会话 ---")
        # 调用invoke直接启动问候节点
        graph.invoke(create_initial_state(), config=config)

    print("---⚕⚕ 康护佳预约会话开始 ---")

    last_displayed_message_idx = -1  # 记录已展示的消息索引

    while True:
        state = graph.get_state(config)

        # 展示未显示的新助手消息(处理无关话题回复)
        if state.values and state.values.get('messages'):
            messages = state.values['messages']
            for idx in range(last_displayed_message_idx + 1, len(messages)):
                msg = messages[idx]
                if msg.get("role") == "assistant":
                    print(f"\n[AI]:{msg['content']}")
            last_displayed_message_idx = len(messages) - 1

        # 2. 检查中断状态
        if state.tasks and state.tasks[0].interrupts:
            interrupt_info = state.tasks[0].interrupts[0].value

            # --- 修复:兼容字符串与字典类型中断 ---
            if isinstance(interrupt_info, dict):
                ai_message = interrupt_info.get('content''无消息内容')
                options = interrupt_info.get('available_options', [])
            else:
                ai_message = interrupt_info
                options = []

            print(f"\n[AI]:{ai_message}")
            if options:
                print(f"可选选项:{', '.join(options)}")
            # ---------------------------------------

            user_input = input("\n[你]:")
            print(f"[你]:{user_input}")

            # 使用用户输入恢复流程
            graph.invoke(Command(resume=user_input), config=config)

        # 3. 检查流程是否结束
        elif not state.next:
            # 结束前打印最终助手消息
            if state.values and state.values.get('messages') and state.values['messages'][-1]["role"] == "assistant":
                if last_displayed_message_idx < len(state.values['messages']) - 1:
                    print(f"\n[AI]:{state.values['messages'][-1]['content']}")
            print("\n--- ⚑⚑ 会话结束 ---")
            break

        # 4. 存在待执行节点且无中断,继续执行
        else:
            graph.invoke(None, config=config)

# 重要:仅需重置历史时设置reset=True
# 设为False可继续原有对话
run_booking_session(booking_graph, reset=True)

Image

Notebook中的诊所智能体聊天机器人

注意:Notebook的输入框会显示在顶部,如上图所示。

仓库内已提供clinic-agent.ipynb文件,可直接运行完整流程。

总结

本文基于LangGraph、OpenAI GPT、SQLite与Streamlit,搭建了一款AI驱动的诊所预约聊天机器人。通过结合状态驱动的智能体工作流与对话界面,系统可引导用户完成从选择科室到确认预约的全流程。分层架构将数据库、服务层、智能体逻辑与界面解耦,使应用模块化、易维护、易扩展。

该方案展示了对话式AI如何简化预约挂号这类实际工作流程,同时减少人工行政工作量。系统可进一步扩展日历集成、消息通知、多语言支持、社交平台对接等功能。整体而言,本项目是现代AI框架与轻量化网页工具结合,打造智能、易用医疗应用的实用案例。

-------------------------------------------------------------

微信公众号:算子之心