目录

一、项目整体文件结构

二、后端代码(app.py)全解析

2.1 模块导入与环境初始化

2.2 Coze AI 交互类封装(CozeService)

2.3 Flask 路由与业务接口实现

2.4 项目启动入口

三、前端代码(index.html)全解析

3.1 HTML 结构:聊天界面的骨架

3.2 CSS 样式:美观的视觉设计与响应式布局

3.3 JavaScript 交互:前后端通信与 DOM 操作

四、环境配置与依赖文件

4.1 .env 文件:敏感信息配置

4.2 requirements.txt:Python 依赖清单

五、项目完整运行步骤

5.1 前置准备

5.2 运行步骤

六、代码优化与扩展

6.1 会话清理优化(添加时间阈值)

6.2 前端添加清空聊天记录功能

6.3 生产环境部署优化

七、核心代码要点总结


在 AI 应用开发的轻量场景中,无需复杂的架构设计,仅用 Python + 原生前端技术,就能基于 Flask 框架和 Coze AI 开放平台搭建一款支持会话保持可视化交互异常处理的完整 AI 聊天机器人。本文将逐行拆解核心代码,从环境配置、后端逻辑封装、前端交互实现到整体运行流程,全方位讲解代码的设计思路和实现细节,让你不仅能跑通项目,更能理解每一行代码的作用。

一、项目整体文件结构

本项目采用前后端一体化部署(Flask 直接托管前端页面),无需额外的前端服务器,文件结构极简,仅需 4 个核心文件即可完成开发,新手也能快速上手:

ai_chatbot/
├── app.py          # 后端核心代码:Flask服务、Coze AI交互、会话管理
├── index.html      # 前端核心代码:聊天界面、交互逻辑、样式美化
├── .env            # 环境配置文件:存储Coze API令牌、机器人ID(敏感信息)
└── requirements.txt# 依赖清单:管理Python第三方库

所有代码各司其职,后端专注于业务逻辑和 AI 交互,前端专注于视觉呈现和用户操作,代码解耦且易于维护。

二、后端代码(app.py)全解析

后端基于 Flask 开发,核心功能包括:加载环境变量、封装 Coze AI 交互逻辑、实现聊天 API 接口、维护用户会话状态、清理过期会话,每一行代码都附带详细注释,以下按代码模块逐段解析。

2.1 模块导入与环境初始化

这是项目的基础准备阶段,导入所需的第三方库,并加载环境变量,避免敏感信息硬编码在代码中。

# 导入必要的库和模块
import os  # 用于读取操作系统环境变量(如.env中的配置)
from flask import Flask, request, jsonify, send_file  # Flask核心:创建服务、处理请求、返回JSON、托管静态文件
from cozepy import Coze, TokenAuth, ChatStatus, COZE_CN_BASE_URL, Message  # Coze AI官方SDK:所有AI交互的核心
from dotenv import load_dotenv  # 加载.env文件中的环境变量,隔离敏感信息
import uuid  # 生成唯一的用户ID(保证每个用户会话的唯一性)
import time  # 处理时间戳:记录会话创建时间、最后活动时间,用于清理过期会话

# 加载.env文件中的环境变量,执行后可通过os.getenv("KEY")获取配置值
load_dotenv()

# 创建Flask应用实例,__name__表示当前模块名,Flask会基于此查找静态文件
app = Flask(__name__)

# 定义全局会话字典:存储所有用户的会话状态
# key:用户唯一标识(本文用客户端IP),value:会话详细信息(字典格式)
# 作用:实现多轮对话的会话保持,让Coze AI能识别上下文
user_sessions = {}

核心要点

  • load_dotenv():必须在读取环境变量前执行,否则无法获取.env中的配置;
  • user_sessions:全局内存字典,是实现会话保持的关键,后续所有会话操作都基于此字典;
  • 导入的ChatStatus:Coze AI 的聊天状态枚举(IN_PROGRESS 进行中 / COMPLETED 已完成),用于轮询获取 AI 回复。

2.2 Coze AI 交互类封装(CozeService)

这是后端的核心业务类,将与 Coze AI 的所有交互逻辑封装在一个类中,实现代码复用逻辑解耦。后续若需替换 AI 平台(如 OpenAI、文心一言),仅需修改此类,无需改动其他业务代码。

# 定义Coze服务类,封装与Coze AI的所有交互逻辑,对外提供统一调用接口
class CozeService:
    def __init__(self):
        """初始化Coze客户端,从环境变量加载配置"""
        # 从.env文件读取Coze API令牌(敏感信息,切勿硬编码)
        self.api_token = os.getenv("COZE_API_TOKEN")
        # 从.env读取机器人ID,若未配置则使用默认值(可根据自己的机器人ID修改)
        self.bot_id = os.getenv("BOT_ID", "7552823978826694671")
        # 初始化Coze官方客户端,配置认证方式和服务地址
        self.coze = Coze(
            auth=TokenAuth(token=self.api_token),  # 令牌认证:Coze的核心认证方式
            base_url=COZE_CN_BASE_URL  # 中国区服务地址,无需修改
        )

    def get_sdk_response(self, user_message, user_identifier):
        """
        获取Coze AI的智能回复,基于用户标识保持会话
        :param user_message: 前端传递的用户输入消息
        :param user_identifier: 用户唯一标识(本文为客户端IP)
        :return: 字典格式的响应结果(status+content)
        """
        try:
            # 第一步:判断用户是否已有会话(实现会话保持的核心逻辑)
            if user_identifier in user_sessions:
                # 存在会话:从全局字典中读取已有会话信息
                session_data = user_sessions[user_identifier]
                conversation_id = session_data["conversation_id"]  # Coze的会话ID(关联上下文)
                user_id = session_data["user_id"]  # 生成的唯一用户ID

                # 构建用户消息:符合Coze SDK的Message格式要求
                messages = [
                    Message(
                        role="user",  # 消息角色:user(用户)/assistant(助手)
                        content=user_message,  # 消息内容:用户输入的文本
                        content_type="text",  # 内容类型:text(文本)/image(图片)
                        type="question"  # 消息类型:question(问题)
                    )
                ]

                # 存在会话:调用Coze SDK继续现有聊天(复用conversation_id和user_id)
                chat = self.coze.chat.create(
                    bot_id=self.bot_id,
                    user_id=user_id,
                    conversation_id=conversation_id,  # 复用会话ID,关联上下文
                    additional_messages=messages,
                    auto_save_history=True  # 自动保存聊天历史:Coze平台端可查看
                )
            else:
                # 不存在会话:创建新的用户会话
                # 生成唯一的用户ID:使用uuid4,保证全球唯一,避免冲突
                user_id = str(uuid.uuid4())

                # 构建用户消息:格式与已有会话一致
                messages = [
                    Message(
                        role="user",
                        content=user_message,
                        content_type="text",
                        type="question"
                    )
                ]

                # 不存在会话:调用Coze SDK创建新聊天(无conversation_id)
                chat = self.coze.chat.create(
                    bot_id=self.bot_id,
                    user_id=user_id,  # 新生成的用户ID
                    additional_messages=messages,
                    auto_save_history=True
                )

                # 关键操作:将新会话信息存入全局字典,供后续对话复用
                user_sessions[user_identifier] = {
                    "conversation_id": chat.conversation_id,  # Coze返回的会话ID(核心,关联上下文)
                    "user_id": user_id,  # 本地生成的用户ID
                    "chat_id": chat.id,  # Coze返回的聊天ID(每次对话的唯一标识)
                    "created_at": time.time(),  # 会话创建时间戳(秒)
                    "last_activity": time.time()  # 会话最后活动时间戳(用于清理过期会话)
                }

            # 第二步:更新会话的最后活动时间和chat_id(每次对话都要更新)
            user_sessions[user_identifier]["last_activity"] = time.time()
            user_sessions[user_identifier]["chat_id"] = chat.id  # 每次对话的chat_id不同,需更新

            # 第三步:轮询等待Coze AI处理完成(核心:Coze AI处理需要时间,非即时响应)
            # 当聊天状态为"进行中(IN_PROGRESS)"时,持续查询状态
            while chat.status == ChatStatus.IN_PROGRESS:
                # 调用Coze SDK查询当前聊天状态和结果
                chat = self.coze.chat.retrieve(
                    conversation_id=chat.conversation_id,
                    chat_id=chat.id
                )

            # 第四步:处理Coze AI的响应结果(状态为"已完成(COMPLETED)"时)
            if chat.status == ChatStatus.COMPLETED:
                # 调用Coze SDK获取当前聊天的所有消息
                messages = self.coze.chat.messages.list(
                    conversation_id=chat.conversation_id,
                    chat_id=chat.id
                )
                # 遍历消息列表,找到助手(assistant)的回复(过滤用户消息)
                for msg in messages:
                    if msg.role == "assistant":
                        # 返回成功结果:status=success + 助手回复内容
                        return {
                            "status": "success",
                            "content": msg.content
                        }
            # 若聊天状态非已完成,返回失败结果
            return {"status": "failed", "content": "对话未完成,AI处理超时"}

        except Exception as e:
            # 异常捕获:捕获所有可能的错误(API令牌错误、网络问题、SDK调用错误等)
            # 返回错误结果:status=error + 异常详细信息(便于调试)
            return {"status": "error", "content": str(e)}

# 创建CozeService实例:全局唯一,所有请求复用此实例
coze_service = CozeService()

核心要点

  1. 会话保持的核心conversation_id(Coze 返回的会话 ID),复用此 ID 即可让 Coze AI 识别聊天上下文,实现多轮对话;
  2. Message 格式:必须严格符合 Coze SDK 的要求,role/content_type等字段不能写错,否则 SDK 调用失败;
  3. 轮询机制:Coze AI 处理消息需要时间(约 1-3 秒),需通过while循环轮询chat.status,直到状态变为COMPLETED
  4. 异常捕获:使用try-except捕获所有异常,保证单个请求错误不会导致整个 Flask 服务崩溃,同时返回错误信息便于调试;
  5. 全局实例coze_service = CozeService()放在类外,所有请求复用此实例,避免重复创建客户端。

2.3 Flask 路由与业务接口实现

Flask 的路由是前后端通信的桥梁,本项目实现了 2 个核心路由:首页路由(返回前端页面)、聊天路由(处理用户消息请求),同时实现了过期会话清理函数。

# 路由1:首页路由(GET请求),访问根路径时触发
@app.route('/')
def index():
    # 每次访问首页时,清理过期会话(释放内存,避免字典无限膨胀)
    cleanup_expired_sessions()
    # 返回前端页面index.html:send_file直接托管静态文件,实现前后端一体化
    return send_file('index.html')

# 路由2:聊天核心接口(POST请求),前端发送消息时调用
@app.route('/chat', methods=['POST'])
def chat():
    """处理前端的聊天请求,返回JSON格式的AI回复"""
    # 第一步:解析前端传递的JSON数据(前端通过Fetch API发送{"message": "用户输入"})
    data = request.json
    user_message = data.get('message')  # 获取用户消息字段

    # 第二步:非空校验,避免空消息提交
    if not user_message:
        # 返回JSON格式响应:Flask的jsonify自动设置Content-Type为application/json
        return jsonify({"status": "error", "content": "消息不能为空"})

    # 第三步:获取用户唯一标识,本文使用客户端IP(简单易实现)
    user_identifier = request.remote_addr

    # 第四步:调用CozeService实例的方法,获取AI回复
    response = coze_service.get_sdk_response(user_message, user_identifier)

    # 第五步:将AI回复转换为JSON格式,返回给前端
    return jsonify(response)

# 辅助函数:清理过期会话(核心:释放内存,优化性能)
def cleanup_expired_sessions():
    """
    清理过期会话的核心函数
    注:基础版本暂未设置时间阈值,后续可扩展为“30分钟无活动则清理”
    """
    # 定义空列表,存储需要删除的过期会话标识
    expired_sessions = []

    # 遍历全局会话字典,收集过期会话(基础版本:收集所有会话,可按需修改)
    for user_identifier, session_data in user_sessions.items():
        expired_sessions.append(user_identifier)

    # 遍历过期会话列表,从全局字典中删除(避免遍历中修改字典导致的错误)
    for user_identifier in expired_sessions:
        del user_sessions[user_identifier]

核心要点

  1. 请求方法:聊天接口必须使用POST方法,因为需要传递 JSON 数据,且符合 HTTP 规范(GET 方法不适合传递请求体);
  2. request.json:Flask 内置的 JSON 解析方法,自动解析前端发送的application/json格式请求体;
  3. user_identifier:本文使用request.remote_addr(客户端 IP)作为用户标识,简单易实现,适合个人测试;
  4. send_file:Flask 的静态文件托管方法,直接返回index.html,实现前后端一体化部署,无需额外的 Nginx/Apache;
  5. cleanup_expired_sessions:基础版本暂未设置时间阈值,后续可添加if time.time() - session_data["last_activity"] > 1800(30 分钟),实现真正的过期清理。

2.4 项目启动入口

这是 Python 项目的标准启动方式,判断是否直接运行当前脚本,若是则启动 Flask 服务。

# 项目启动入口:仅当直接运行app.py时,执行以下代码
if __name__ == '__main__':
    # 启动Flask服务:debug=True(调试模式,代码修改后自动重启),port=5000(服务端口)
    app.run(debug=True, port=5000)

核心要点

  • debug=True:仅适合开发环境,生产环境必须关闭(存在安全风险);
  • port=5000:Flask 默认端口,可修改为 8080、80 等(需管理员权限);
  • 启动后服务地址:http://127.0.0.1:5000,直接在浏览器访问即可。

三、前端代码(index.html)全解析

前端采用原生 HTML+CSS+JavaScript开发,无任何前端框架依赖,实现了美观的响应式界面流畅的交互体验,同时兼顾了移动端适配。代码分为HTML 结构CSS 样式JavaScript 交互三部分,以下逐部分解析。

3.1 HTML 结构:聊天界面的骨架

HTML 结构遵循语义化原则,分为头部(标题)、聊天容器(消息展示 + 正在输入提示)、输入区域(输入框 + 发送按钮)三部分,结构清晰,便于后续 DOM 操作。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <!-- 视口设置:移动端适配核心,保证页面在手机上正常显示 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的人生我做主</title>
    <!-- 内嵌CSS样式:便于一体化部署,无需单独的css文件 -->
    <style>/* 样式代码见下文 */</style>
</head>
<body>
    <!-- 主容器:卡片式布局,限制最大宽度,居中显示 -->
    <div class="container">
        <!-- 头部:标题+副标题,渐变背景 -->
        <header>
            <h1>我的未来我做主</h1>
            <p>与AI助手一起规划您的未来</p>
        </header>

        <!-- 聊天容器:包含消息展示区和正在输入提示 -->
        <div class="chat-container">
            <!-- 聊天消息展示区:滚动显示所有聊天记录 -->
            <div id="chat-messages" class="chat-messages">
                <!-- 初始消息:机器人欢迎语,页面加载时显示 -->
                <div class="message bot-message">
                    <p>您好!我是您的未来规划助手。请问您想了解什么关于未来规划的问题?</p>
                </div>
            </div>

            <!-- 正在输入提示:机器人处理消息时显示,默认隐藏 -->
            <div class="typing-indicator" id="typing-indicator">
                <div class="typing-dots">
                    <div class="typing-dot"></div>
                    <div class="typing-dot"></div>
                    <div class="typing-dot"></div>
                </div>
            </div>
        </div>

        <!-- 输入区域:输入框+发送按钮,底部固定 -->
        <div class="input-area">
            <input type="text" id="user-input" placeholder="输入您的问题...">
            <button id="send-button">发送</button>
        </div>
    </div>

    <!-- 内嵌JavaScript:交互逻辑,页面加载后执行 -->
    <script>/* 交互代码见下文 */</script>
</body>
</html>

核心要点

  1. 移动端适配<meta name="viewport" ...>是移动端适配的核心,保证页面在手机上不会缩放变形;
  2. ID 命名规范:所有需要 JavaScript 操作的元素都设置了唯一 ID(如chat-messagesuser-input),便于通过getElementById获取;
  3. 默认隐藏元素typing-indicator(正在输入提示)默认通过 CSS 隐藏,需要时通过 JavaScript 修改样式显示;
  4. 初始消息:页面加载时显示机器人欢迎语,提升用户体验,避免空白界面。

3.2 CSS 样式:美观的视觉设计与响应式布局

CSS 样式采用原生 CSS3开发,实现了渐变色背景卡片式布局消息气泡样式动画效果移动端适配,所有样式都添加了详细的注释,便于修改和定制。

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box; /* 盒模型:padding/border计入元素宽度,避免布局错乱 */
}

body {
    font-family: 'Arial', sans-serif; /* 全局字体:无衬线字体,适配所有设备 */
    /* 页面背景:线性渐变,提升视觉体验 */
    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
    color: #333;
    line-height: 1.6; /* 行高:提升文本可读性 */
    min-height: 100vh; /* 最小高度:占满整个视口高度 */
    padding: 20px; /* 页面内边距:避免内容贴边 */
}

.container {
    max-width: 900px; /* 最大宽度:桌面端限制宽度,居中显示 */
    margin: 0 auto; /* 水平居中:核心布局技巧 */
    background-color: white;
    border-radius: 15px; /* 圆角:卡片式布局,柔和视觉 */
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); /* 阴影:提升层次感 */
    overflow: hidden; /* 溢出隐藏:避免子元素超出圆角 */
}

header {
    /* 头部渐变背景:蓝黑渐变,符合AI产品的科技感 */
    background: linear-gradient(90deg, #3498db, #2c3e50);
    color: white;
    padding: 30px 20px;
    text-align: center; /* 文本居中 */
}

header h1 {
    font-size: 2.5rem; /* 大标题字体:rem单位,适配移动端 */
    margin-bottom: 10px;
    text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3); /* 文字阴影:提升辨识度 */
}

header p {
    font-size: 1.2rem;
    opacity: 0.9; /* 透明度:副标题弱化,突出主标题 */
}

.chat-container {
    display: flex;
    flex-direction: column; /* 弹性布局:垂直方向排列子元素 */
    height: 500px; /* 固定高度:聊天区域不会随消息增多无限增高 */
}

.chat-messages {
    flex: 1; /* 弹性占比:占满chat-container的剩余高度 */
    overflow-y: auto; /* 垂直滚动:消息过多时可滚动查看 */
    padding: 20px;
    background-color: #f9f9f9; /* 浅灰色背景:区分消息区域和输入区域 */
}

/* 消息气泡通用样式 */
.message {
    margin-bottom: 15px; /* 消息之间的间距 */
    padding: 12px 18px; /* 消息内边距:文本不贴边 */
    border-radius: 18px; /* 大圆角:气泡样式,符合聊天软件习惯 */
    max-width: 80%; /* 最大宽度:消息不会过宽,提升可读性 */
    animation: fadeIn 0.3s ease; /* 淡入动画:消息展示更流畅 */
}

/* 淡入动画:CSS3关键帧动画,消息从透明到不透明,轻微上移 */
@keyframes fadeIn {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
}

/* 用户消息样式:右侧蓝色背景,白色文字 */
.user-message {
    background-color: #3498db;
    color: white;
    margin-left: auto; /* 右对齐:核心技巧,将元素推到右侧 */
    border-bottom-right-radius: 5px; /* 右下角小圆角:符合聊天气泡的视觉习惯 */
}

/* 机器人消息样式:左侧浅灰色背景,黑色文字 */
.bot-message {
    background-color: #ecf0f1;
    color: #333;
    margin-right: auto; /* 左对齐:核心技巧,将元素推到左侧 */
    border-bottom-left-radius: 5px; /* 左下角小圆角:与用户消息呼应 */
}

.input-area {
    display: flex; /* 弹性布局:输入框和按钮横向排列 */
    padding: 15px;
    border-top: 1px solid #eee; /* 上边框:区分输入区域和消息区域 */
    background-color: white;
}

#user-input {
    flex: 1; /* 弹性占比:输入框占满剩余宽度 */
    padding: 12px 18px;
    border: 1px solid #ddd; /* 浅灰色边框 */
    border-radius: 25px; /* 大圆角:输入框样式更柔和 */
    outline: none; /* 去除默认焦点边框 */
    font-size: 1rem;
    transition: border-color 0.3s; /* 过渡效果:焦点时边框颜色平滑变化 */
}

/* 输入框焦点样式:蓝色边框+浅蓝阴影,提升交互反馈 */
#user-input:focus {
    border-color: #3498db;
    box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}

#send-button {
    margin-left: 10px; /* 按钮与输入框的间距 */
    padding: 12px 25px;
    background-color: #3498db; /* 主色调:与用户消息背景一致 */
    color: white;
    border: none; /* 去除默认边框 */
    border-radius: 25px; /* 大圆角:与输入框呼应 */
    cursor: pointer; /* 鼠标悬浮为手型:提示可点击 */
    font-size: 1rem;
    /* 过渡效果:悬停和点击时样式平滑变化 */
    transition: background-color 0.3s, transform 0.2s;
}

/* 按钮悬停样式:加深背景色,轻微放大 */
#send-button:hover {
    background-color: #2980b9;
    transform: scale(1.05);
}

/* 按钮点击样式:轻微缩小,模拟按压效果 */
#send-button:active {
    transform: scale(0.98);
}

/* 正在输入提示:默认隐藏 */
.typing-indicator {
    display: none;
    padding: 10px 15px;
    background-color: #ecf0f1; /* 与机器人消息背景一致 */
    border-radius: 18px;
    margin-bottom: 15px;
    width: fit-content; /* 宽度自适应内容:避免过宽 */
    border-bottom-left-radius: 5px; /* 与机器人消息呼应 */
}

.typing-dots {
    display: flex; /* 弹性布局:三个点横向排列 */
    align-items: center;
    height: 20px;
}

/* 打字点点样式:小圆点,模拟机器人思考 */
.typing-dot {
    width: 8px;
    height: 8px;
    background-color: #7f8c8d;
    border-radius: 50%; /* 圆形:核心技巧 */
    margin: 0 3px; /* 点之间的间距 */
    animation: typingAnimation 1.4s infinite ease-in-out; /* 无限动画:持续跳动 */
}

/* 三个点的动画延迟:依次延迟0/0.2/0.4秒,实现交替跳动效果 */
.typing-dot:nth-child(1) { animation-delay: 0s; }
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }

/* 打字动画:小圆点上下跳动,模拟机器人输入 */
@keyframes typingAnimation {
    0%, 60%, 100% { transform: translateY(0); }
    30% { transform: translateY(-5px); }
}

/* 媒体查询:移动端适配(屏幕宽度≤600px时生效) */
@media (max-width: 600px) {
    header h1 {
        font-size: 2rem; /* 缩小主标题字体 */
    }

    header p {
        font-size: 1rem; /* 缩小副标题字体 */
    }

    .message {
        max-width: 90%; /* 增大消息最大宽度,适配小屏幕 */
    }

    #send-button {
        padding: 12px 20px; /* 缩小按钮内边距,节省空间 */
    }
}

核心要点

  1. 盒模型box-sizing: border-box是前端布局的核心技巧,避免 padding/border 导致元素宽度超出预期;
  2. 弹性布局(Flex):全程使用display: flex实现布局,替代传统的 float,布局更简单、更灵活;
  3. 消息对齐margin-left: auto(右对齐)和margin-right: auto(左对齐)是实现消息气泡左右分布的核心技巧,无需浮动;
  4. CSS3 动画:通过@keyframes实现消息淡入和打字点点动画,提升用户体验,无额外 JavaScript 开销;
  5. 响应式布局:通过@media (max-width: 600px)实现移动端适配,针对小屏幕调整字体、间距等样式;
  6. rem 单位:字体使用rem单位,基于根元素(html)的字体大小,适配不同设备的屏幕分辨率。

3.3 JavaScript 交互:前后端通信与 DOM 操作

JavaScript 是前端的交互核心,实现了消息发送前后端异步通信DOM 动态更新正在输入提示等功能,所有代码都封装在DOMContentLoaded事件中,确保页面元素加载完成后再执行,避免获取不到元素的错误。

// 页面DOM元素加载完成后执行:避免获取不到元素的错误
document.addEventListener('DOMContentLoaded', function() {
    // 第一步:获取所有需要操作的DOM元素(全局变量,函数内复用)
    const chatMessages = document.getElementById('chat-messages'); // 消息展示区
    const userInput = document.getElementById('user-input'); // 输入框
    const sendButton = document.getElementById('send-button'); // 发送按钮
    const typingIndicator = document.getElementById('typing-indicator'); // 正在输入提示

    // 函数1:添加用户消息到聊天界面
    function addUserMessage(message) {
        const messageElement = document.createElement('div'); // 创建新的div元素
        messageElement.classList.add('message', 'user-message'); // 添加样式类:通用消息+用户消息
        messageElement.innerHTML = `<p>${message}</p>`; // 设置消息内容
        chatMessages.appendChild(messageElement); // 将消息元素添加到展示区
        scrollToBottom(); // 自动滚动到底部,查看最新消息
    }

    // 函数2:添加机器人消息到聊天界面(与用户消息逻辑一致,样式类不同)
    function addBotMessage(message) {
        const messageElement = document.createElement('div');
        messageElement.classList.add('message', 'bot-message'); // 添加样式类:通用消息+机器人消息
        messageElement.innerHTML = `<p>${message}</p>`;
        chatMessages.appendChild(messageElement);
        scrollToBottom(); // 自动滚动到底部
    }

    // 函数3:显示正在输入提示
    function showTypingIndicator() {
        typingIndicator.style.display = 'block'; // 修改样式为显示
        scrollToBottom(); // 滚动到底部,让用户看到提示
    }

    // 函数4:隐藏正在输入提示
    function hideTypingIndicator() {
        typingIndicator.style.display = 'none'; // 修改样式为隐藏
    }

    // 函数5:自动滚动聊天区域到底部(核心工具函数,多次复用)
    function scrollToBottom() {
        chatMessages.scrollTop = chatMessages.scrollHeight; // scrollTop:滚动条距离顶部的高度;scrollHeight:元素的总高度
    }

    // 函数6:核心函数——发送消息到后端,处理响应
    async function sendMessage() {
        // 第一步:获取并清理用户输入(去除首尾空格)
        const message = userInput.value.trim();
        // 非空校验:如果消息为空,直接返回,不执行后续操作
        if (!message) return;

        // 第二步:更新前端界面——添加用户消息,清空输入框
        addUserMessage(message);
        userInput.value = ''; // 清空输入框,方便下次输入

        // 第三步:显示正在输入提示,模拟机器人思考
        showTypingIndicator();

        try {
            // 第四步:异步请求后端接口(Fetch API,替代传统的AJAX)
            const response = await fetch('/chat', {
                method: 'POST', // 请求方法:与后端路由一致,必须为POST
                headers: {
                    // 请求头:告诉后端请求体是JSON格式,必须设置,否则后端request.json解析失败
                    'Content-Type': 'application/json'
                },
                // 请求体:将消息转换为JSON字符串,与后端约定的格式一致
                body: JSON.stringify({ message: message })
            });

            // 解析后端返回的JSON响应:与后端jsonify返回的格式一致
            const data = await response.json();

            // 第五步:隐藏正在输入提示,无论成功还是失败
            hideTypingIndicator();

            // 第六步:处理后端响应结果,更新前端界面
            if (data.status === 'success') {
                // 响应成功:添加机器人消息(显示AI回复)
                addBotMessage(data.content);
            } else {
                // 响应失败:显示友好的错误提示,同时在控制台打印详细错误(便于调试)
                addBotMessage('抱歉,我遇到了一些问题,请稍后再试。');
                console.error('API Error:', data.content);
            }
        } catch (error) {
            // 异常捕获:捕获网络错误、请求超时等问题(如后端服务未启动)
            hideTypingIndicator(); // 隐藏正在输入提示
            addBotMessage('网络错误,请检查您的连接。'); // 显示网络错误提示
            console.error('Fetch Error:', error); // 控制台打印详细错误
        }
    }

    // 事件1:发送按钮点击事件——触发sendMessage函数
    sendButton.addEventListener('click', sendMessage);

    // 事件2:输入框回车事件——触发sendMessage函数,支持回车发送
    userInput.addEventListener('keypress', function(e) {
        // 判断是否按下回车键(e.key === 'Enter')
        if (e.key === 'Enter') {
            sendMessage();
        }
    });

    // 页面加载后,输入框自动获取焦点,提升用户体验
    userInput.focus();
});

核心要点

  1. DOMContentLoaded 事件:所有代码都包裹在此事件中,确保页面元素加载完成后再执行,这是前端开发的最佳实践;
  2. Fetch API:使用现代的fetch替代传统的XMLHttpRequest,实现异步前后端通信,语法更简洁,支持 Promise 和 async/await;
  3. async/await:使用async/await处理异步请求,替代传统的 Promise 链式调用,代码更易读、更易维护;
  4. 请求头设置'Content-Type': 'application/json'必须设置,否则后端request.json无法解析请求体,会导致报错;
  5. JSON 序列化JSON.stringify({ message: message })将 JavaScript 对象转换为 JSON 字符串,符合后端的接收格式;
  6. 异常处理:使用try-catch捕获所有异常(网络错误、后端服务未启动、接口返回错误等),保证前端页面不会崩溃,同时给用户友好的提示;
  7. 自动焦点userInput.focus()让页面加载后输入框自动获取焦点,用户可直接输入,提升体验;
  8. 滚动到底部chatMessages.scrollTop = chatMessages.scrollHeight是实现聊天区域自动滚动的核心技巧,适用于所有滚动容器。

四、环境配置与依赖文件

4.1 .env 文件:敏感信息配置

.env文件用于存储敏感信息(Coze API 令牌、机器人 ID),避免硬编码在代码中,提高项目的安全性,文件内容如下:

# Coze AI API令牌(从Coze开放平台获取,替换为自己的)
COZE_API_TOKEN=your_coze_api_token
# Coze机器人ID(从Coze开放平台创建机器人后获取,替换为自己的)
BOT_ID=your_bot_id

获取方式

  1. 访问Coze 开放平台,注册并登录账号;
  2. 创建机器人,进入机器人设置页面,获取机器人 ID
  3. 进入个人中心 - API 密钥,创建并获取API 令牌

4.2 requirements.txt:Python 依赖清单

requirements.txt用于管理 Python 第三方库,避免手动安装时版本不一致的问题,文件内容如下:

# Flask框架:轻量级Web框架,核心依赖
flask>=2.0.0
# Coze AI官方SDK:与Coze AI交互的核心依赖
cozepy>=0.1.0
# python-dotenv:加载.env文件的环境变量
python-dotenv>=1.0.0

安装命令:在项目根目录执行以下命令,自动安装所有依赖:

pip install -r requirements.txt

五、项目完整运行步骤

5.1 前置准备

  1. 注册 Coze 开放平台账号,创建机器人,获取API 令牌机器人 ID,填入.env文件;
  2. 确保本地安装了 Python3.8+(推荐 3.9-3.11,兼容性最好);
  3. 新建项目文件夹,将app.pyindex.html.envrequirements.txt四个文件放入文件夹中。

5.2 运行步骤

  1. 安装依赖:打开命令行,进入项目根目录,执行:
    pip install -r requirements.txt
    
  2. 启动后端服务:在项目根目录执行:
    python app.py
    
    看到以下输出,说明服务启动成功:
    * Serving Flask app 'app'
    * Debug mode: on
    WARNING: This is a development server. Do not use it in a production deployment.
    * Running on http://127.0.0.1:5000
    Press CTRL+C to quit
    * Restarting with stat
    * Debugger is active!
    * Debugger PIN: 123-456-789
    
  3. 访问聊天界面:打开浏览器,输入http://127.0.0.1:5000,即可看到聊天界面;
  4. 代码验证:在输入框中输入 “未来职业规划有什么建议?”,点击发送或按回车,即可看到 AI 的回复,同时实现多轮对话(如继续问 “互联网行业怎么样?”,AI 能识别上下文)。

六、代码优化与扩展

现有代码实现了核心功能,基于现有代码结构,可快速实现以下优化和扩展,无需重构代码:

6.1 会话清理优化(添加时间阈值)

修改cleanup_expired_sessions函数,实现30 分钟无活动则清理会话

def cleanup_expired_sessions():
    expired_sessions = []
    # 定义过期时间:30分钟 = 30*60=1800秒
    EXPIRE_TIME = 1800
    for user_identifier, session_data in user_sessions.items():
        # 判断最后活动时间是否超过过期时间
        if time.time() - session_data["last_activity"] > EXPIRE_TIME:
            expired_sessions.append(user_identifier)
    # 删除过期会话
    for user_identifier in expired_sessions:
        del user_sessions[user_identifier]

6.2 前端添加清空聊天记录功能

在 HTML 的input-area添加清空按钮,再在 JavaScript 中添加点击事件:

<!-- 输入区域添加清空按钮 -->
<div class="input-area">
    <input type="text" id="user-input" placeholder="输入您的问题...">
    <button id="clear-button" style="margin-left:10px;padding:12px 20px;border-radius:25px;border:none;background:#95a5a6;color:white;cursor:pointer;">清空</button>
    <button id="send-button">发送</button>
</div>
// JavaScript中添加清空按钮的事件
const clearButton = document.getElementById('clear-button');
clearButton.addEventListener('click', function() {
    // 清空消息展示区
    chatMessages.innerHTML = '';
    // 重新添加欢迎语
    addBotMessage('您好!我是您的未来规划助手。请问您想了解什么关于未来规划的问题?');
    // 输入框获取焦点
    userInput.focus();
});

6.3 生产环境部署优化

修改app.py的启动代码,关闭调试模式,使用固定端口:

if __name__ == '__main__':
    # 生产环境:关闭debug,设置host=0.0.0.0(允许外网访问)
    app.run(debug=False, port=5000, host='0.0.0.0')

七、核心代码要点总结

本文的代码实战围绕Flask+Coze AI展开,所有代码都经过实际运行验证,核心要点可总结为 3 点:

  1. 后端会话保持的核心:复用 Coze AI 返回的conversation_id,并通过全局字典user_sessions存储会话信息,实现多轮对话的上下文关联;
  2. 前后端通信的核心:后端通过Flask+jsonify提供 JSON 接口,前端通过Fetch API+async/await实现异步请求,请求头必须设置Content-Type: application/json
  3. 前端交互的核心:通过原生 JavaScript 实现 DOM 动态更新,使用 CSS3 实现动画和响应式布局,无框架依赖,轻量且易维护。

通过本文的代码解析,你不仅能跑通一个完整的 AI 聊天机器人项目,更能理解每一行代码的设计思路和实现细节,在此基础上可快速扩展出更复杂的功能(如图片发送、多机器人切换、用户登录等),为后续的 AI 应用开发打下坚实的基础。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐