目录

一、项目背景与技术选型

1. 需求分析

2. 技术栈选择

二、系统架构与核心模块设计

1. 后端核心模块:游戏逻辑类(IdiomGame)

(1)初始成语生成(generate_initial_idiom)

(2)游戏状态管理

(3)API 接口封装

2. AI 交互层:容错与响应处理

3. 前端交互层:用户体验优化

(1)状态可视化

(2)输入校验

(3)历史记录展示

(4)完整代码

三、关键技术难点与解决方案

1. AI 响应异步处理问题

2. 跨域请求问题

3. 敏感配置管理

4. 用户体验与容错平衡

四、系统优化与扩展方向

1. 现有优化点

2. 扩展方向

(1)功能扩展

(2)技术优化

五、项目总结与思考

1. 技术价值

2. 产品思考

3. 开发感悟

六、快速上手指南

1. 环境准备

2. 启动服务

3. 访问游戏

结语


成语接龙作为中国传统文化的经典游戏,既考验词汇量,又锻炼思维敏捷度。当传统游戏遇上 AI 技术,会碰撞出怎样的火花?本文将详细拆解一个基于 Flask+Coze AI 打造的智能成语接龙游戏的实现过程,从技术架构、核心逻辑到用户体验优化,带你深入了解如何将 AI 能力落地到实际应用中。

一、项目背景与技术选型

1. 需求分析

我们希望打造一个兼具趣味性和智能化的成语接龙游戏,核心需求包括:

  • 自动生成初始接龙成语,支持游戏重置
  • 验证用户输入的成语有效性,实现 AI 自动接龙
  • 记录游戏历史,提供友好的交互界面
  • 具备容错机制,在 AI 服务异常时保证游戏可用

2. 技术栈选择

技术 / 工具 用途 选型理由
Flask 后端 Web 框架 轻量级、易上手,适合快速开发小型 API 服务
Coze AI 智能成语生成 字节跳动旗下 AI 平台,支持多轮对话,响应速度快,中文处理能力优秀
Flask-CORS 跨域支持 解决前端页面与后端 API 的跨域请求问题
HTML/CSS/JS 前端界面 原生技术栈,无需额外依赖,适配性强
dotenv 环境变量管理 安全管理敏感配置(如 API Token),便于环境切换

二、系统架构与核心模块设计

整个系统分为后端服务层AI 交互层前端交互层三个核心部分,架构如下:

前端页面(HTML/JS) → 后端API(Flask) → Coze AI SDK → 成语生成/验证
    ↑                      ↓                    ↓
游戏状态展示 ← 游戏逻辑处理 ← AI响应解析与容错 ← 成语结果返回

1. 后端核心模块:游戏逻辑类(IdiomGame)

游戏的核心逻辑封装在IdiomGame类中,主要承担三个核心职责:

(1)初始成语生成(generate_initial_idiom)
  • AI 调用流程:构造用户指令消息,调用 Coze Chat API 生成四字成语
  • 容错机制:设置 30 秒超时时间,若 AI 响应超时 / 返回无效结果,自动切换到默认成语列表
  • 数据清洗:过滤非中文字符,确保返回结果为标准四字成语
def generate_initial_idiom(self):
    try:
        # 构造AI请求消息
        messages = [Message(role="user", content="生成一个四字成语作为开头,仅返回成语本身")]
        chat = self.coze.chat.create(bot_id=self.bot_id, user_id=self.user_id, additional_messages=messages)
        
        # 等待AI响应(超时控制)
        timeout = 0
        while chat.status == ChatStatus.IN_PROGRESS and timeout < 30:
            chat = self.coze.chat.retrieve(conversation_id=chat.conversation_id, chat_id=chat.id)
            timeout += 1
            time.sleep(1)
        
        # 解析并清洗AI响应
        initial_idiom = msg.content.strip()
        initial_idiom = "".join(filter(lambda x: '\u4e00' <= x <= '\u9fff', initial_idiom))
        if initial_idiom and len(initial_idiom) == 4:
            return initial_idiom
        raise Exception("AI生成的初始成语无效")
    except Exception as e:
        # 降级策略:使用默认成语列表
        return random.choice(COMMON_IDIOMS)
(2)游戏状态管理
  • reset_game:重置游戏,生成新初始成语并清空历史记录
  • add_to_history:记录用户与 AI 的接龙记录,限制最多保存 20 条
  • get_sdk_response:处理用户输入,调用 AI 完成接龙,返回标准化响应
(3)API 接口封装

后端暴露三个核心 API 接口,实现前后端交互:

接口路径 请求方法 功能
/api/init GET 初始化游戏,返回当前成语和历史记录
/api/play POST 提交用户成语,返回 AI 接龙结果
/api/restart POST 重置游戏,生成新初始成语

2. AI 交互层:容错与响应处理

AI 交互是整个系统的关键环节,我们做了多层容错设计:

  1. 超时保护:所有 AI 请求设置 30 秒超时,避免服务挂起
  2. 响应验证:检查返回结果是否为 4 个中文字符,无效则触发降级
  3. 异常捕获:捕获网络错误、API 调用错误等,统一返回错误信息
  4. 历史兜底:即使 AI 服务完全不可用,默认成语列表仍能保证游戏基础功能

3. 前端交互层:用户体验优化

前端页面聚焦于流畅的交互体验清晰的状态反馈,核心设计点包括:

(1)状态可视化
  • 加载状态:初始成语获取时显示 “加载中...”
  • 操作反馈:提交按钮显示 “提交中...”,禁用重复点击
  • 结果提示:不同类型的消息使用不同样式(成功 - 绿色、错误 - 红色、信息 - 蓝色)
(2)输入校验

前端提前做输入验证,减少无效请求:

if (!/^[\u4e00-\u9fa5]+$/.test(userInput)) {
    showMessage('请输入中文成语', 'error');
    return;
}
if (userInput.length !== 4) {
    showMessage('请输入四字成语', 'error');
    return;
}
(3)历史记录展示

按时间倒序展示用户与 AI 的接龙记录,清晰呈现游戏进程:

<li>
    <span class="user">你: 一心一意</span>
    <span class="ai">AI: 意气风发</span>
    <span class="time">14:25:30</span>
</li>
(4)完整代码

后端:

# 导入必要的库和模块
import os  # 用于操作系统环境变量
from flask import Flask, request, jsonify, send_file  # Flask web框架相关功能
from cozepy import Coze, TokenAuth, ChatStatus, COZE_CN_BASE_URL, Message  # Coze AI服务SDK
from dotenv import load_dotenv  # 用于加载环境变量文件
import uuid  # 用于生成唯一的用户ID
import time  # 用于时间相关操作

# 加载.env文件中的环境变量
load_dotenv()

# 创建Flask应用实例
app = Flask(__name__)

# 用于存储会话状态的字典,key为客户端IP或自定义标识,value为会话信息
user_sessions = {}


# 定义Coze服务类,用于封装与Coze AI服务的交互
class CozeService:
    def __init__(self):
        # 从环境变量获取Coze API令牌,如果没有则返回None
        self.api_token = os.getenv("COZE_API_TOKEN")
        # 从环境变量获取机器人ID,如果没有则使用默认值
        self.bot_id = os.getenv("BOT_ID", "7552823978826694671")
        # 初始化Coze客户端,使用令牌认证和中国区基础URL
        self.coze = Coze(
            auth=TokenAuth(token=self.api_token),
            base_url=COZE_CN_BASE_URL
        )

    def get_sdk_response(self, user_message, user_identifier):
        """获取智能体响应,基于用户标识保持会话"""
        try:
            # 检查是否已有该用户的会话
            if user_identifier in user_sessions:
                session_data = user_sessions[user_identifier]
                conversation_id = session_data["conversation_id"]
                user_id = session_data["user_id"]

                # 构建用户消息
                messages = [
                    Message(
                        role="user",  # 消息角色为用户
                        content=user_message,  # 消息内容
                        content_type="text",  # 内容类型为文本
                        type="question"  # 消息类型为问题
                    )
                ]

                # 在现有会话中继续聊天
                chat = self.coze.chat.create(
                    bot_id=self.bot_id,  # 指定机器人ID
                    user_id=user_id,  # 使用相同的用户ID
                    conversation_id=conversation_id,  # 使用相同的会话ID
                    additional_messages=messages,  # 添加用户消息
                    auto_save_history=True  # 自动保存聊天历史
                )
            else:
                # 创建新的会话
                # 生成唯一的用户ID
                user_id = str(uuid.uuid4())

                # 构建用户消息
                messages = [
                    Message(
                        role="user",  # 消息角色为用户
                        content=user_message,  # 消息内容
                        content_type="text",  # 内容类型为文本
                        type="question"  # 消息类型为问题
                    )
                ]

                # 创建新的聊天会话
                chat = self.coze.chat.create(
                    bot_id=self.bot_id,  # 指定机器人ID
                    user_id=user_id,  # 指定用户ID
                    additional_messages=messages,  # 添加用户消息
                    auto_save_history=True  # 自动保存聊天历史
                )

                # 保存会话信息到user_sessions字典中
                user_sessions[user_identifier] = {
                    "conversation_id": chat.conversation_id,  # Coze的会话ID
                    "user_id": user_id,  # 用户ID
                    "chat_id": chat.id,  # 聊天ID
                    "created_at": time.time(),  # 会话创建时间戳
                    "last_activity": time.time()  # 最后活动时间
                }

            # 更新最后活动时间
            user_sessions[user_identifier]["last_activity"] = time.time()
            # 更新chat_id(每次新的聊天都会生成新的chat_id)
            user_sessions[user_identifier]["chat_id"] = chat.id

            # 轮询等待聊天完成,当聊天状态为"进行中"时继续查询
            while chat.status == ChatStatus.IN_PROGRESS:
                chat = self.coze.chat.retrieve(
                    conversation_id=chat.conversation_id,  # 会话ID
                    chat_id=chat.id  # 聊天ID
                )

            # 如果聊天状态为"已完成",获取聊天消息
            if chat.status == ChatStatus.COMPLETED:
                messages = self.coze.chat.messages.list(
                    conversation_id=chat.conversation_id,  # 会话ID
                    chat_id=chat.id  # 聊天ID
                )
                # 遍历消息,找到助手的回复
                for msg in messages:
                    if msg.role == "assistant":
                        return {
                            "status": "success",
                            "content": msg.content
                        }
            # 如果对话未完成,返回失败状态
            return {"status": "failed", "content": "对话未完成"}

        except Exception as e:
            # 如果发生异常,返回错误状态和异常信息
            return {"status": "error", "content": str(e)}


# 创建CozeService实例
coze_service = CozeService()


@app.route('/')
def index():
    # 在返回页面之前清理过期会话
    cleanup_expired_sessions()

    # 返回index.html文件
    return send_file('index.html')


# 定义聊天路由,处理POST请求
@app.route('/chat', methods=['POST'])
def chat():
    # 从请求的JSON数据中获取用户消息
    data = request.json
    user_message = data.get('message')

    # 如果消息为空,返回错误
    if not user_message:
        return jsonify({"status": "error", "content": "消息不能为空"})

    # 使用客户端IP作为用户标识(也可以使用cookie或其他方式)
    user_identifier = request.remote_addr

    # 获取AI响应,基于用户标识保持会话
    response = coze_service.get_sdk_response(user_message, user_identifier)
    # 将响应转换为JSON格式返回
    return jsonify(response)


# 清理过期会话的辅助函数
def cleanup_expired_sessions():
    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]


# 如果是直接运行此脚本,启动Flask应用
if __name__ == '__main__':
    # 启动应用,开启调试模式,端口为5000
    app.run(debug=True, port=5000)

前端:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的人生我做主</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        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 {
            background: linear-gradient(90deg, #3498db, #2c3e50);
            color: white;
            padding: 30px 20px;
            text-align: center;
        }

        header h1 {
            font-size: 2.5rem;
            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;
            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;
        }

        @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;
        }

        .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); }
        }

        @media (max-width: 600px) {
            header h1 {
                font-size: 2rem;
            }

            header p {
                font-size: 1rem;
            }

            .message {
                max-width: 90%;
            }

            #send-button {
                padding: 12px 20px;
            }
        }
    </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 class="input-area">
                <input type="text" id="user-input" placeholder="输入您的问题...">
                <button id="send-button">发送</button>
            </div>
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const chatMessages = document.getElementById('chat-messages');
            const userInput = document.getElementById('user-input');
            const sendButton = document.getElementById('send-button');
            const typingIndicator = document.getElementById('typing-indicator');

            // 添加用户消息到聊天界面
            function addUserMessage(message) {
                const messageElement = document.createElement('div');
                messageElement.classList.add('message', 'user-message');
                messageElement.innerHTML = `<p>${message}</p>`;
                chatMessages.appendChild(messageElement);
                scrollToBottom();
            }

            // 添加AI消息到聊天界面
            function addBotMessage(message) {
                const messageElement = document.createElement('div');
                messageElement.classList.add('message', 'bot-message');
                messageElement.innerHTML = `<p>${message}</p>`;
                chatMessages.appendChild(messageElement);
                scrollToBottom();
            }

            // 显示正在输入指示器
            function showTypingIndicator() {
                typingIndicator.style.display = 'block';
                scrollToBottom();
            }

            // 隐藏正在输入指示器
            function hideTypingIndicator() {
                typingIndicator.style.display = 'none';
            }

            // 滚动到底部
            function scrollToBottom() {
                chatMessages.scrollTop = chatMessages.scrollHeight;
            }

            // 发送消息到后端
            async function sendMessage() {
                const message = userInput.value.trim();
                if (!message) return;

                // 添加用户消息到界面
                addUserMessage(message);
                userInput.value = '';

                // 显示正在输入指示器
                showTypingIndicator();

                try {
                    // 发送请求到后端
                    const response = await fetch('/chat', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({ message: message })
                    });

                    const data = await response.json();

                    // 隐藏正在输入指示器
                    hideTypingIndicator();

                    if (data.status === 'success') {
                        addBotMessage(data.content);
                    } else {
                        addBotMessage('抱歉,我遇到了一些问题,请稍后再试。');
                        console.error('API Error:', data.content);
                    }
                } catch (error) {
                    // 隐藏正在输入指示器
                    hideTypingIndicator();
                    addBotMessage('网络错误,请检查您的连接。');
                    console.error('Fetch Error:', error);
                }
            }

            // 发送按钮点击事件
            sendButton.addEventListener('click', sendMessage);

            // 输入框回车事件
            userInput.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') {
                    sendMessage();
                }
            });

            // 页面加载后自动聚焦到输入框
            userInput.focus();
        });
    </script>
</body>
</html>

三、关键技术难点与解决方案

1. AI 响应异步处理问题

Coze Chat API 采用异步响应模式,直接调用后无法立即获取结果。解决方案:

  • 轮询机制:调用chat.create后,循环调用chat.retrieve获取最新状态
  • 超时控制:设置最大轮询次数,避免无限等待
  • 状态判断:通过ChatStatus枚举值判断 AI 处理状态(IN_PROGRESS/COMPLETED)

2. 跨域请求问题

前端页面与后端 API 部署在同一域名但不同端口,导致跨域请求被浏览器拦截。解决方案:

  • 引入 Flask-CORS 扩展,全局启用 CORS:CORS(app)
  • 生产环境可配置指定域名白名单,提升安全性

3. 敏感配置管理

API Token、Bot ID 等敏感信息直接写死代码存在安全风险。解决方案:

  • 使用.env文件存储环境变量:COZE_API_TOKEN=xxx
  • 通过dotenv加载配置:load_dotenv()
  • 代码中通过os.environ.get()获取,避免硬编码

4. 用户体验与容错平衡

AI 响应存在延迟,若直接等待会导致用户体验下降。解决方案:

  • 前端禁用操作按钮并显示加载状态,明确告知用户处理中
  • 后端设置合理的超时时间(30 秒),兼顾响应速度和成功率
  • AI 服务异常时自动切换到本地成语列表,保证游戏不中断

四、系统优化与扩展方向

1. 现有优化点

  • 性能优化:历史记录限制最多 20 条,减少数据传输量
  • 资源复用:全局唯一的IdiomGame实例,避免重复初始化 AI 客户端
  • 错误处理:全局异常处理器,统一返回 JSON 格式错误信息

2. 扩展方向

(1)功能扩展
  • 成语验证:增加成语合法性校验(接入成语词典 API)
  • 难度分级:根据用户水平调整 AI 接龙难度(如生僻成语 / 常用成语)
  • 计分系统:记录接龙成功次数,增加游戏竞技性
  • 多语言支持:适配繁体中文,面向海外用户
(2)技术优化
  • 缓存机制:缓存 AI 生成的成语列表,减少 API 调用次数
  • 异步处理:使用 Flask-Async 扩展,将 AI 请求改为异步任务,提升并发能力
  • 前端框架重构:使用 Vue/React 重构前端,提升代码可维护性
  • 部署优化:使用 Docker 容器化部署,支持多环境一键部署

五、项目总结与思考

1. 技术价值

这个小项目展示了 AI 技术与传统应用结合的可能性:

  • 轻量化 AI 集成:无需复杂的模型部署,通过 API 即可快速接入 AI 能力
  • 容错设计的重要性:任何依赖外部服务的应用,都需要做好降级策略
  • 前后端分离的简化实现:原生技术栈也能打造流畅的交互体验

2. 产品思考

从产品角度,这个游戏的核心价值在于 “轻量化” 和 “趣味性”:

  • 无需下载安装,网页端直接体验
  • AI 接龙替代人工,随时随地可玩
  • 传统文化与现代技术结合,兼具教育意义和娱乐性

3. 开发感悟

  • 小项目也需要注重架构设计:模块化封装让代码更易维护
  • 容错机制是生产级应用的必备:用户不会关心技术细节,只在意是否可用
  • 快速迭代与验证:先实现核心功能,再逐步优化体验和扩展功能

六、快速上手指南

1. 环境准备

# 安装依赖
pip install flask flask-cors cozepy python-dotenv

# 创建.env文件
COZE_API_TOKEN=你的Coze API Token
BOT_ID=你的Coze Bot ID
USER_ID=自定义用户ID

2. 启动服务

python app.py

3. 访问游戏

打开浏览器,访问http://localhost:5000即可开始游戏。

结语

这个智能成语接龙游戏看似简单,却涵盖了前后端交互、AI 集成、容错设计、用户体验优化等多个开发维度。在实际开发中,我们不需要一开始就追求完美,而是先实现核心功能,再通过持续优化提升系统的稳定性和用户体验。

AI 技术的普及让小型应用也能具备智能能力,关键在于找到合适的应用场景,并用简洁的技术方案解决核心问题。希望本文的拆解能为你带来启发,也欢迎你基于这个项目继续扩展,打造更有趣的 AI 应用。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐