首页 / 文章 / 实用提示:智能代理AI项目:为某公司构建客服聊天机器人

实用提示:智能代理AI项目:为某公司构建客服聊天机器人

《实用笔记》操作指南:代理型人工智能项目——为处理合同、支票相关业务构建客户服务聊天机器人,同时为采用该模式的团队提供代码插入模块。

5381 词

以下说明为“智能代理AI项目:为诊所构建客服聊天机器人”提供了可行的实施路径。重点在于合同条款、验证步骤以及代码占位符,而非激励性表述。 在完成概览阶段时,首先列出合同要求:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 同时记录正常流程与异常恢复流程。重试机制、人工干预环节以及错误处理都属于产品功能的一部分,而非后续的优化内容。

引言

在将“引入阶段”视为可度量的对象时,其效果最佳。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。 相较于庞大的脚本,应优先选择小型且可测试的单元。当某个步骤失败时,故障应能指向单一的责任主体,而非复杂的流程链。 保持图结构的状态简洁且具有类型定义。嵌套的数据块会掩盖哪个节点修改了哪个字段的信息,还会在流程中断后导致无法继续执行。

问题描述

将“问题描述”阶段视为可度量的对象来处理效果最佳。在扩大范围之前,先记录一份完美的测试用例、一个故障案例以及回滚说明。把这一阶段视为输入与已验证输出之间的契约,为相关文档命名、明确成功标准,绝不允许出现悄无声息的半完成状态。要保持图表状态的简洁性与类型一致性,嵌套的数据块会掩盖是哪个节点修改了哪个字段,还会在中断后导致无法继续处理。

解决方案

将“解决方案”阶段视为可度量的对象来处理效果最佳。在扩大范围之前,需记录一份理想的操作流程、一个故障案例以及回滚说明。 在功能结果旁同时记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外账单。 保持图表状态简洁且类型明确。嵌套的数据块会掩盖哪个节点修改了哪个字段的信息,还会在中断后导致流程无法继续。 将“解决方案”阶段视为可度量的对象来处理效果最佳。在扩大范围之前,需记录一份理想的操作流程、一个故障案例以及回滚说明。 需同时记录正常流程与恢复流程。重试机制、人工审核环节以及死信处理都是产品本身的组成部分,而非后续需要补充的内容。

设置

在设置阶段,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 优先选择小型、可测试的单元,而非冗长的脚本。当某个步骤失败时,故障应指向单一责任点,而非复杂的流程链。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的连接方式并不等同于业务功能的完整性。

Mac / Linux / Windows

在 Mac、Linux、Windows 阶段,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将此阶段视为输入与已验证输出之间的契约。为相关成果命名,定义成功检测标准,并拒绝默许的半完成状态。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的连接方式并不等同于业务上的完整性。

python -m venv .venv
source .venv/bin/activate
.venv\Scripts\Activate.ps1
.venv\Scripts\activate
(.venv) your-folder-name %
streamlit>=1.50.0
python-dotenv==1.0.0
pydantic==2.12.5
pandas==2.3.3
python-dateutil==2.8.2
langgraph>=1.0.7
openai>=2.16.0
pygraphviz==1.14
cd clinic-agent
pip install -r requirements.txt
OPENAI_API_KEY=your_openai_api_key_here

第一部分:数据库设置(data/db.py)

在第一步的数据库设置阶段,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。除了功能结果外,还需记录执行时间以及令牌或查询成本。提前了解这些成本可以避免在系统从演示环境切换到共享环境时出现意外费用。对于那些会消耗资金或修改生产数据的操作,必须经过人工审批。仅靠编译时的配置并不足以确保业务的完整性。在第一步的数据库设置阶段,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。需同时记录正常流程和故障恢复流程。重试机制、人工审核环节以及错误处理措施都是产品不可或缺的组成部分,而非后续需要补充的内容。

1. 医生表

在处理“1 医生表”阶段时,首先写下相关契约:所需输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 优先选择小型、可测试的单元,而非庞大的脚本。当某个步骤失败时,故障应指向单一责任点,而非复杂的流程链。 在成本较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次调用相同的大型语言模型。

2. 客户表

在处理“2个客户表”阶段时,首先写下合同条款:所需输入、成功标志以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 将此阶段视为输入与验证后输出之间的契约。为相关成果命名,明确成功判定标准,杜绝无声的半完成状态。 在成本较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次调用相同的大型语言模型。

3. 预订表

在处理“3个预订表”阶段时,首先需明确合同条款:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在系统从演示环境过渡到共享环境时出现意外费用。 在耗时较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次收取相同的LLM调用费用。 在处理“3个预订表”阶段时,首先需明确合同条款:所需输入、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 需同时记录正常流程和故障恢复流程。重试机制、人工审核环节以及死信处理都是产品的一部分,而非后续需要补充的功能。

4. 创建数据库

将“创建数据库”的四个阶段视为可度量的对象来处理效果最佳。在扩大范围之前,先记录一份完美的操作日志、一个故障案例以及回滚说明。 优先选择小型且可测试的单元,而非庞大的脚本。当某一步骤出现故障时,故障应指向单一的责任主体,而非复杂的流程链。 保持图结构的层次清晰且类型明确。嵌套的数据块会掩盖哪个节点修改了哪个字段的信息,还会在流程中断后导致无法继续执行。

# data/db.py - Database initialization and operations

import sqlite3
import os
from datetime import datetime, timedelta

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

def get_connection():
    """Get a database connection."""
    return sqlite3.connect(DB_PATH)

def init_db():
    """Initialize the database with tables and sample data."""
    conn = get_connection()
    cursor = conn.cursor()

    # Doctors table
    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
        )
    """)

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

    # Bookings table
    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)
        )
    """)

    # Insert sample doctors
    doctors = [
        ("D1", "Dr. Anil Sharma", "General Physician", "10:00-14:00"),
        ("D2", "Dr. Neha Verma", "Dermatologist", "11:00-16:00"),
        ("D3", "Dr. Rohit Mehta", "Orthopedic", "09:00-13:00"),
        ("D4", "Dr. Kavita Rao", "Pediatrician", "10:00-15:00"),
        ("D5", "Dr. Sanjay Iyer", "ENT Specialist", "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("Initializing database...")
    init_db()
    print("Database initialized successfully.")
cd clinic-agent
python data/db.py

第二部分:服务层(代理工具)

将第二阶段的服务层视为可度量的界面最为有效。在扩大范围之前,先记录一份最佳处理方案、一个故障案例以及回滚说明。 把这一阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,绝不允许默默地完成部分任务。 提供具有严格结构定义和明确副作用标注的工具。主机需要在自动批准之前知道哪些调用会改变状态。

Doctor服务(services/doctor_service.py)

将 Doctor Service 的 doctorservice 阶段视为可度量的对象来处理时,其效果最佳。在扩大范围之前,需记录一份理想的操作流程、一个故障案例以及回滚说明。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在系统从演示环境过渡到共享环境时出现意外账单。 保持图表状态简洁且类型明确。嵌套的数据块会掩盖具体是哪个节点修改了哪个字段,且在中断后会导致流程无法继续。 将 Doctor Service 的 doctorservice 阶段视为可度量的对象来处理时,其效果最佳。在扩大范围之前,需记录一份理想的操作流程、一个故障案例以及回滚说明。 需同时记录正常流程与恢复流程。重试机制、人工审核环节以及死信处理都是产品本身的组成部分,而非后续需要补充的功能。

# services/doctor_service.py - Doctor operations

from data.db import get_all_doctors, get_doctor_by_speciality, get_doctor_by_id

def get_specialities_list():
    """Get list of all specialities."""
    doctors = get_all_doctors()
    # Return unique specialities
    return list(dict.fromkeys([doc[2] for doc in doctors]))

def get_doctor_info(speciality):
    """Get doctor information by 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):
    """Generate hourly time slots from office timing string.

    Args:
        office_timing: String like "11:00-16:00"

    Returns:
        List of time slots like ["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 = "AM"
            display_hour = hour if hour > 0 else 12
        elif hour == 12:
            suffix = "PM"
            display_hour = 12
        else:
            suffix = "PM"
            display_hour = hour - 12
        slots.append(f"{display_hour}:00 {suffix}")

    return slots

def parse_time_slot(slot_str):
    """Parse time slot string to 24-hour format.

    Args:
        slot_str: String like "1:00 PM"

    Returns:
        String like "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)

对于预订服务中的 bookingservice 阶段,在修改代码之前需明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 优先选择小型、可测试的单元,而非冗长的脚本。当某个步骤失败时,故障应指向单一责任点,而非复杂的流程链。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的连接方式并不等同于业务功能的完整性。

# services/booking_service.py - Booking operations

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):
    """Get existing customer or create new one."""
    customer = get_customer_by_phone(phone)
    if customer:
        return customer[0]  # Return customer_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):
    """Get available time slots for a doctor for today.

    Args:
        doctor_id: Doctor ID
        office_timing: Office timing string like "11:00-16:00"

    Returns:
        List of available time slots
    """
    from services.doctor_service import generate_time_slots

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

    # Get booked slots
    booked_times = get_bookings_by_doctor_and_date(doctor_id, today)

    # Filter out booked slots
    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):
    """Confirm a booking.

    Args:
        doctor_id: Doctor ID
        customer_name: Customer name
        customer_phone: Customer phone
        time_slot: Time slot like "1:00 PM"
        appointment_date: Optional date in YYYY-MM-DD format. Defaults to today.

    Returns:
        Booking ID
    """
    # Get or create customer
    customer_id = get_or_create_customer(customer_name, customer_phone)

    # Generate booking ID
    booking_id = f"BKG-{uuid.uuid4().hex[:6].upper()}"

    # Format appointment time
    if not appointment_date:
        appointment_date = datetime.now().strftime("%Y-%m-%d")
    appointment_time = parse_time_slot(time_slot)

    # Create booking
    create_booking(booking_id, doctor_id, customer_id, appointment_date, appointment_time)

    return booking_id

服务测试

在服务测试阶段,应在修改代码之前明确输入参数、各步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行相应步骤,而无需猜测隐藏状态。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,定义成功判定标准,并杜绝默许的半完成状态。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的配置并不等同于业务上的完整性。

# test/test_service.py - to test the services created

from pathlib import Path
import sys

# Allow running this file directly: `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

# Get all specialities
specialities = get_specialities_list()
print("Available specialities:", specialities)

# Generate time slots for a doctor (11:00 AM - 4:00 PM)
slots = generate_time_slots("11:00-16:00")
print("Available slots:", slots)

# Confirm a booking
booking_id = confirm_booking(
    doctor_id="D1",
    customer_name="John Doe",
    customer_phone="9876543210",
    time_slot="2:00 PM"
)
print(f"Booking confirmed: {booking_id}")
cd clinic-agent
python test_service.py
(.venv) (base) my-mac clinic-agent % python test_service.py
Available specialities: ['General Physician', 'Dermatologist', 'Orthopedic', 'Pediatrician', 'ENT Specialist']
Available slots: ['11:00 AM', '12:00 PM', '1:00 PM', '2:00 PM', '3:00 PM']
Booking confirmed: BKG-C4F60A

第三部分:智能代理层

在第三部分的智能代理层阶段,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 在功能结果旁记录执行时间以及令牌或查询成本。提前显示成本可避免在流程从演示环境转向共享环境时出现意外费用。 对于会消耗资金或修改生产数据的操作,必须经过人工审批。编译时的配置并不等同于业务功能的完整性。 在第三部分的智能代理层阶段,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理都是产品不可或缺的部分,而非后续需要补充的内容。

预订状态:

在处理预订状态阶段时,首先写下合同内容:所需输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 建议使用小型、可测试的单元,而非庞大的脚本。当某个步骤失败时,故障应指向单一责任模块,而非复杂的流程链。 在成本较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次调用相同的LLM接口。

# agents/booking_agent.py - LangGraph agent implementation

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):
    """State for the booking conversation."""
    messages: List[dict]                    # Chat history
    stage: str                               # greeting, select_speciality, select_doctor, etc.
    selected_speciality: Optional[str]      # Chosen medical specialty
    selected_doctor: Optional[dict]         # Selected doctor details
    selected_date: Optional[str]            # Appointment date
    selected_slot: Optional[str]            # Time slot
    customer_name: Optional[str]            # Customer name
    customer_phone: Optional[str]           # Customer phone
    booking_id: Optional[str]               # Confirmation ID
    available_options: List[str]            # UI options

def create_initial_state():
    """Create initial state for the conversation."""
    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": []
    }

LLM辅助函数

在处理大语言模型辅助函数阶段时,首先需明确约定:所需的输入参数、成功信号以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 将此阶段视为输入与验证后输出之间的契约。为相关成果命名,定义成功判定标准,并杜绝无声的半完成状态。 缓存稳定的系统指令和工具结构。重复发送相同的前置信息是导致资源浪费的常见原因。

# agents/booking_agent.py - LangGraph agent implementation
def call_llm(
    system_prompt: str,
    user_prompt: str,
    *,
    model: str = "gpt-4o-mini",
    temperature: float = 0,
    max_tokens: int = 50,
) -> str:
    """
    Centralized helper for all LLM calls.
    Returns the assistant's response
    """
    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"LLM call error: {e}")
        return ""

智能体节点

在处理智能体节点阶段时,首先需明确相关规范:所需的输入参数、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及代币或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 在耗时较高的步骤之后设置检查点。当操作员重新尝试后续节点时,恢复流程不应再次收取相同的LLM调用费用。 在处理智能体节点阶段时,首先需明确相关规范:所需的输入参数、成功信号以及部分失败时的处理方式。这份清单能确保后续的代码修改保持一致性。 需同时记录正常流程与故障恢复流程。重试机制、人工审核环节以及死信处理都是产品功能的一部分,而非后续需要补充的内容。

构建图结构

将“构建图结构”这一阶段视为可度量的对象来处理效果最佳。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。 优先选择小型且可测试的单元,而非庞大的脚本。当某个步骤失败时,故障应指向单一的责任主体,而非复杂的流程链。 保持图结构的状态简洁且具有类型定义。嵌套的数据块会掩盖哪个节点修改了哪个字段的信息,还会在流程中断后导致无法继续执行。

# agents/booking_agent.py - LangGraph agent implementation
from langgraph.checkpoint.memory import MemorySaver

def build_booking_graph():
    """Build the LangGraph workflow."""
    workflow = StateGraph(BookingState)

    # Add all nodes
    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)

    # Set entry point
    workflow.set_entry_point("greeting")

    # Add conditional edges based on routing
    workflow.add_conditional_edges(
        "greeting",
        llm_router,
        {
            "greeting": "greeting",
            "select_speciality": "select_speciality",
            "cancelled": "cancelled"
        }
    )

    # Similar conditional edges for other nodes...

    # Final edges to END
    workflow.add_edge("completed", END)
    workflow.add_edge("cancelled", END)

    # Compile with checkpointer for session management
    return workflow.compile(checkpointer=MemorySaver())

# Create the compiled graph
booking_graph = build_booking_graph()
#agents/save_langgraph_flow.py
"""Save the clinic booking LangGraph flow to png format in this folder."""


from pathlib import Path
import sys


# Ensure imports work whether the script is run from project root or this folder.
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:
    """Export graph as 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"Saved PNG flow to: {png_path}")


if __name__ == "__main__":
    save_graph_files()
cd clinic-agent
python save_langgraph_flow.py

处理消息:

将“流程消息”阶段视为可度量的对象来处理效果最佳。在扩大范围之前,先记录一份完美的测试案例、一个失败案例以及回滚说明。 把这一阶段视为输入与已验证输出之间的契约。为相关成果命名,明确成功标准,绝不允许出现无声的半完成状态。 保持图结构的状态简洁且类型明确。嵌套的数据块会掩盖是哪个节点修改了哪个字段,还会在中断后导致无法继续处理。

# agents/booking_agent.py - LangGraph agent implementation
def process_message(state: BookingState, user_message: str, thread_id: str = "default_session") -> BookingState:
    """Process a user message through the booking graph."""
    config = {"configurable": {"thread_id": thread_id}}

    # Check if the graph is currently interrupted
    current_state = booking_graph.get_state(config)

    if current_state.tasks and current_state.tasks[0].interrupts:
        # Resume the graph with the user's message
        result = booking_graph.invoke(Command(resume=user_message), config=config)
    else:
        # No interrupt, so start/continue normally
        # Add user message to state (unless it's an initial trigger)
        if user_message.lower() != "hi" or state["messages"]:
            # Avoid duplicate user messages if already added
            if not state["messages"] or state["messages"][-1].get("content") != user_message:
                state["messages"].append({
                    "role": "user",
                    "content": user_message
                })
        # Run the graph
        result = booking_graph.invoke(state, config=config)

    # Update available_options and ensure message is in history
    snapshot = booking_graph.get_state(config)
    if snapshot.tasks and snapshot.tasks[0].interrupts:
        interrupt_value = snapshot.tasks[0].interrupts[0].value

        # Handle both dict and string interrupt values
        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)

        # Ensure the interrupt message is in the chat history
        if msg_content:
            # Check if it was already added by the node
            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:
                # If already added, just update it with options if missing
                result["messages"][-1]["options"] = options

        result["available_options"] = options
    else:
        # If not interrupted, use whatever set in state, or default to empty
        if "available_options" not in result:
            result["available_options"] = []

    return result

测试智能体

将“测试智能体”阶段视为可度量的对象来处理,效果最佳。在扩大范围之前,需记录一份理想运行案例、一个失败案例以及回滚说明。 在功能结果旁同时记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在从演示环境过渡到共享环境时出现意外费用。 保持图表状态简洁且类型明确。嵌套的数据块会掩盖哪个节点修改了哪个字段的信息,还会在中断后导致流程无法继续。 将“测试智能体”阶段视为可度量的对象来处理,效果最佳。在扩大范围之前,需记录一份理想运行案例、一个失败案例以及回滚说明。 需同时记录正常流程与故障恢复流程。重试机制、人工审核环节以及死信处理都是产品功能的一部分,而非后续需要补充的内容。

# test/test_agent.py - test agent
# Initialize state

from pathlib import Path
import sys

# Allow running this file directly: `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()

# Process messages
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

第4部分:Streamlit用户界面

在第四部分的Streamlit UI阶段,应在修改代码之前明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 优先选择小型、可测试的单元,而非冗长的脚本。当某个步骤失败时,故障应指向单一责任点,而非复杂的流程链。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的连接方式并不等同于业务功能的完整性。

# ui/chat_ui.py - Streamlit chatbot interface

"""Streamlit UI for the clinic booking chatbot."""

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


def initialize_session():
    """Initialize session state."""
    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():
    """Display the chat history with persistent options and styling."""
    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"])

                # Show options if they exist
                options = message.get("options", [])
                if options:
                    # If this is the last message in history, show as clickable buttons
                    if i == len(messages) - 1 and st.session_state.state["stage"] not in ["completed", "cancelled"]:
                        st.markdown("---")
                        # Create columns for buttons
                        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:
                        # For older messages, show options as pills/text to keep history
                        options_str = "  ".join([f"`{opt}`" for opt in options])
                        st.markdown(f"**Available options:** {options_str}")
        else:
            with st.chat_message("user"):
                st.markdown(message["content"])


def handle_user_input(user_input: str):
    """Handle user input and process through agent."""
    # Process the message
    st.session_state.state = process_message(
        st.session_state.state,
        user_input,
        thread_id=st.session_state.session_id
    )

    # Rerun to update UI
    st.rerun()


def run_chat_ui():
    """Run the chat UI."""
    # Page config
    st.set_page_config(
        page_title="CarePlus Clinic - Book Appointment",
        page_icon="🏥",
        layout="centered"
    )

    # Custom CSS for distinction between messages
    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)

    # Initialize database
    init_db()

    # Initialize session
    initialize_session()

    # Header
    st.title("🏥 CarePlus Clinic")
    st.markdown("*Book your doctor appointment easily*")
    st.markdown("---")

    # Send initial greeting if not initialized
    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
    display_chat_history()

    # Chat input (only show if not completed)
    if st.session_state.state["stage"] not in ["completed", "cancelled"]:
        if prompt := st.chat_input("Type your message here..."):
            handle_user_input(prompt)
    else:
        # Show restart button after completion
        st.markdown("---")
        if st.button("🔄 Start New Booking", use_container_width=True):
            st.session_state.state = create_initial_state()
            st.session_state.initialized = False
            st.rerun()

第五部分:应用程序入口点

在第五阶段的应用录入阶段,应在修改代码之前明确输入内容、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,定义成功检测标准,并拒绝默许的半完成状态。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的连接方式并不等同于业务上的完整性。

# app.py - Main entry point for the application

from ui.chat_ui import run_chat_ui

if __name__ == "__main__":
    run_chat_ui()

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

在“运行聊天机器人”第二阶段中,应在修改代码之前明确输入内容、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。除了功能结果外,还需记录执行时间以及令牌或查询成本。提前了解成本情况可以避免在从演示环境切换到共享环境时出现意外费用。对于会产生费用或修改生产数据的操作,必须经过人工审批。仅靠编译时的配置并不足以确保业务的完整性。

方法1:Streamlit网页应用

对于方案1的Streamlit Web阶段,在修改代码之前需先定义输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能开关应集中存放于一个位置,这样操作人员无需查看整个流程图即可进行审核。 对于涉及资金支出或修改生产数据的节点,需设置人工审批环节。编译时的连接方式并不能保证业务流程的完整性。

streamlit run app.py

方案2:Jupyter Notebook

在方法2的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
)
# clinic-agent.ipynb
# Initialize the booking graph
booking_graph = build_booking_graph()

## Visualize the booking graph structure
from IPython.display import Image, display

png_bytes = booking_graph.get_graph().draw_mermaid_png()
display(Image(png_bytes))

运行聊天机器人会话:

在“运行聊天机器人会话”阶段,应在修改代码之前明确输入参数、该步骤的负责人以及终止条件。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 优先选择小型、可测试的单元,而非庞大的脚本。当某个步骤失败时,故障应指向单一责任点,而非复杂的流程链。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的连接方式并不等同于业务功能的完整性。

# 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. Start or Reset logic
    current_state = graph.get_state(config)
    if reset or not current_state.values:
        print(f"--- {'🔄 Resetting' if reset else '🆕 Initializing'} Session ---")
        # Using invoke() here kicks off the 'greeting' node immediately
        graph.invoke(create_initial_state(), config=config)

    print("---⚕⚕ Starting CarePlus Booking Session ---")

    last_displayed_message_idx = -1  # Track which messages have been displayed

    while True:
        state = graph.get_state(config)

        # Display any new assistant messages that haven't been shown yet
        # (This handles guardrail/off-topic responses)
        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. Check for Interrupts
        if state.tasks and state.tasks[0].interrupts:
            interrupt_info = state.tasks[0].interrupts[0].value

            # --- FIX: Safely handle both String and Dict interrupts ---
            if isinstance(interrupt_info, dict):
                ai_message = interrupt_info.get('content', 'No message content')
                options = interrupt_info.get('available_options', [])
            else:
                ai_message = interrupt_info
                options = []

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

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

            # Resume the graph with the user's input
            graph.invoke(Command(resume=user_input), config=config)

        # 3. Check if the graph has finished
        elif not state.next:
            # Before ending, check if there's a final assistant message to print
            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--- ⚑⚑ Session Ended ---")
            break

        # 4. If nodes are pending but no interrupt, let them run (the gas pedal)
        else:
            graph.invoke(None, config=config)

# IMPORTANT: Set reset=True only when you want to wipe the history.
# Set it to False to actually continue the conversation!
run_booking_session(booking_graph, reset=True)

结论:

在结论阶段,应在修改代码之前明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 将此阶段视为输入与已验证输出之间的契约。为相关成果命名,定义成功判定标准,并拒绝默许的半完成状态。 对于涉及资金支出或修改生产数据的操作,必须经过人工审批。编译时的连接并不等同于业务上的完整性。

参考资料:

在参考阶段,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 在功能结果旁记录执行时间以及令牌或查询成本。提前显示成本可避免在流程从演示环境转向共享环境时出现意外费用。 对于会消耗资金或修改生产数据的操作,必须经过人工审批。编译时的配置并不等同于业务功能的完整性。 在参考阶段,修改代码之前需明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 需同时记录正常流程和异常恢复流程。重试机制、人工审核环节以及错误处理都是产品不可或缺的部分,而非后续需要补充的内容。

操作检查清单

在制定操作检查清单时,需明确输入参数、各步骤的负责人以及完成标准,然后再进行代码修改。操作人员应能够从已知的检查点重新执行相应步骤,而无需猜测隐藏状态。

将配置信息与应用程序代码分开。环境文件、密钥存储及功能开关应集中存放于一个位置,以便操作人员无需查看全部内容即可进行审计。

对于涉及资金支出或修改生产数据的操作,必须经过人工审批。仅靠编译时的配置并不足以确保业务的完整性。

编写简短的操作手册:说明如何轮换密钥、如何清空队列以及如何回滚最近的导入操作。

同时记录正常流程和故障恢复流程。重试机制、人工审核环节以及死信处理都是产品不可或缺的部分,而非后续需要补充的功能。

对于那些会花费资金或更改生产数据的操作,必须经过人工审批。编译时的配置并不等同于业务功能的完整性。

在推广该技术栈之前,应先冻结版本,为关键流程记录完整的操作日志,并明确回滚步骤。共享环境需要设置速率限制、租户验证机制,以及负责密钥轮换的明确责任人。与其追求花哨的一次性演示,不如注重扎实的可靠性。

关于 9744ef4a5b25 的批处理说明:请将提供商密钥存放在仓库之外,为每个会话设置令牌使用上限,并将操作日志与评估用配置文件放在一起,以便后续模型更换时仍能保持数据可比性。