在人工智能快速发展的今天,将大模型能力与桌面应用结合已成为开发热点。本文将带您从零开始,使用 PyQt6 开发一款功能完善的 AI 聊天助手,重点详解如何接入火山方舟平台的 Doubao-Seed-1.6-thinking 模型,并提供可直接运行的完整代码,即使是开发新手也能轻松上手。

一、项目概述

这款 AI 聊天助手是一个集美观界面与智能交互于一体的桌面应用,核心特性包括:

  • 采用 PyQt6 构建的现代化图形界面,区分用户与 AI 的消息气泡设计
  • 深度整合火山方舟平台的 Doubao-Seed-1.6-thinking 模型,支持自然语言对话
  • 实现流式响应技术,模拟真人打字效果,大幅提升交互体验
  • 内置 Markdown 格式自动清洗、网络异常重试等实用机制
  • 支持多轮对话上下文记忆,实现连贯的聊天逻辑

二、开发环境准备

1. 基础环境搭建

首先确保您的计算机已安装 Python 3.8 及以上版本,然后通过 pip 安装所需依赖库:

# 安装 PyQt6(界面框架)和 requests(网络请求)
pip install PyQt6 requests

安装完成后,可通过以下命令验证是否安装成功:

python -c "import PyQt6; import requests; print('安装成功')"

若输出 "安装成功",则基础环境准备完毕。

三、火山方舟平台配置全流程(获取 API 密钥与接入点)

要使用 Doubao-Seed-1.6-thinking 模型,必须先在火山方舟平台完成配置,以下是详细步骤:

1. 注册与登录火山方舟

  • 访问火山方舟官方控制台,点击右上角 "登录 / 注册"
  • 新用户需完成手机号注册及实名认证(个人用户可正常使用,无需企业资质)
  • 登录成功后,进入火山方舟主控制台

2. 创建 API 密钥(调用凭证)

API 密钥是访问火山方舟服务的身份凭证,获取步骤如下:

  1. 在控制台顶部导航栏,点击头像右侧的下拉菜单,选择【访问控制】
  2. 左侧菜单栏选择【API 密钥管理】,进入密钥配置页面
  3. 点击右上角【新建密钥】按钮,在弹窗中输入密钥名称(如 "chat-assistant-key")
  4. 点击【确定】后,系统会生成 "Access Key ID" 和 "Secret Access Key"
  5.  critical :点击 "下载 CSV" 保存密钥信息(此信息仅显示一次,丢失需重新创建)

⚠️ 安全提示:API 密钥具有账号权限,请勿分享给他人。若怀疑泄露,需立即在控制台吊销并重新创建。

3. 创建 Doubao-Seed-1.6-thinking 模型的推理接入点

推理接入点是模型的具体调用入口,每个接入点对应一个部署的模型实例:

① 返回火山方舟主控制台,左侧菜单选择【模型广场】

② 在搜索框输入 "Doubao-Seed-1.6-thinking",找到目标模型并点击进入详情页

③ 点击右上角【创建推理接入点】按钮,进入配置页面:                                                           

     接入点名称 :自定义(如 "doubao-chat-endpoint")                                                           

     计算规格 :个人测试选择 "轻量型" 即可(1 核 2G,足够日常使用)                                     

     其他参数 :保持默认配置,无需修改

④ 点击【确定】提交,等待 1-3 分钟,直到接入点状态变为 "运行中"

4. 获取接入点关键信息

接入点创建成功后,进入【我的接入点】页面,找到刚创建的接入点,记录以下信息:

Model ID :接入点列表中显示的 "模型 ID"(格式为ep-xxxxxxxxxxxxxxx)     

API 地址 :根据地域自动生成,华北 - 北京地域格式为:

https://ark.cn-beijing.volces.com/api/v3/chat/completions

四、完整代码(含占位符)

以下是可直接运行的完整代码,API配置部分需替换为你的火山方舟 API 密钥和模型接入点 ID:(大概162行到163行位置处)

import random
import time
import sys
import json
import requests
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                             QHBoxLayout, QLineEdit, QPushButton, QScrollArea,
                             QLabel, QSizePolicy)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QFont, QPalette, QColor


class MessageBubble(QWidget):
    def __init__(self, text, is_user=False, parent=None):
        super().__init__(parent)
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
        layout = QHBoxLayout(self)
        layout.setContentsMargins(5, 5, 5, 5)

        # 深度清洗所有 markdown 符号(重点处理 # 标题)
        cleaned_text = self.clean_all_markdown(text)

        self.label = QLabel(cleaned_text)
        self.label.setWordWrap(True)
        self.label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
        self.label.setContentsMargins(10, 8, 10, 8)

        # 强制中文字体,解决符号显示问题
        label_font = QFont("Microsoft YaHei, SimSun, sans-serif")
        label_font.setPointSize(12)
        self.label.setFont(label_font)

        # 气泡样式区分用户/AI
        if is_user:
            self.label.setStyleSheet("""
                background-color: #DCF8C6; 
                border-radius: 15px; 
                border-bottom-right-radius: 2px;
            """)
            layout.addStretch()
            layout.addWidget(self.label)
        else:
            self.label.setStyleSheet("""
                background-color: #F1F0F0; 
                border-radius: 15px; 
                border-bottom-left-radius: 2px;
            """)
            layout.addWidget(self.label)
            layout.addStretch()

    def clean_all_markdown(self, text):
        """彻底清洗所有 markdown 格式符号(#、##、*、- 等)"""
        # 1. 处理所有 # 标题(包括 #四、技术特点 格式)
        lines = text.split("\n")
        cleaned_lines = []
        for line in lines:
            # 匹配以 # 开头的标题(支持 #四、技术特点 格式)
            if line.strip().startswith("#"):
                # 移除 # 及后面的空格(保留标题内容)
                cleaned_line = line.split("#", 1)[-1].strip()
                cleaned_lines.append(cleaned_line)
            else:
                cleaned_lines.append(line)
        text = "\n".join(cleaned_lines)

        # 2. 保留原有清洗逻辑(不删除)
        text = text.replace("## ", "").replace("### ", "")  # 处理多级标题残留
        text = text.replace("**", "").replace("*", "")  # 处理加粗/斜体
        text = text.replace("- ", "  • ")  # 处理列表符号

        # 3. 清理多余空行
        text = "\n".join([line for line in text.split("\n") if line.strip()])
        return text

    def append_text(self, text):
        """追加文本到现有内容"""
        current_text = self.label.text()
        self.label.setText(current_text + text)


class ApiThread(QThread):
    partial_response = pyqtSignal(str)
    finished = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, api_url, api_key, model_id, messages):
        super().__init__()
        self.api_url = api_url
        self.api_key = api_key
        self.model_id = model_id
        self.messages = messages
        self.session = requests.Session()  # 复用连接

    def run(self):
        try:
            headers = {
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.api_key}"
            }

            data = {
                "model": self.model_id,
                "messages": self.messages,
                "temperature": 0.8,  # 稍微提高温度以获得更快响应
                "max_tokens": 1024,  # 减少最大生成长度
                "stream": True  # 启用流式响应
            }

            # 指数退避重试机制
            max_retries = 3
            for attempt in range(max_retries):
                try:
                    # 发送请求(不设置超时)
                    response = self.session.post(
                        self.api_url,
                        headers=headers,
                        json=data,
                        stream=True
                    )

                    if response.status_code == 200:
                        full_response = ""
                        # 流式解析响应
                        for line in response.iter_lines():
                            if line:
                                line = line.decode('utf-8')
                                if line.startswith('data: '):
                                    data_line = line[6:]
                                    if data_line.strip() == '[DONE]':
                                        continue
                                    data_obj = json.loads(data_line)
                                    if 'choices' in data_obj and len(data_obj['choices']) > 0:
                                        if 'delta' in data_obj['choices'][0] and 'content' in data_obj['choices'][0][
                                            'delta']:
                                            content = data_obj['choices'][0]['delta']['content']
                                            full_response += content
                                            self.partial_response.emit(content)
                        self.finished.emit(full_response)
                        return
                    else:
                        self.error.emit(f"API 错误: {response.status_code}\n{response.text}")
                        return
                except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
                    if attempt == max_retries - 1:
                        raise e
                    # 指数退避:2^attempt + 随机小数秒
                    wait_time = (2 ** attempt) + random.random()
                    time.sleep(wait_time)

        except Exception as e:
            self.error.emit(f"网络异常: {str(e)}")


class ChatApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("豆包 AI 聊天助手")
        self.setMinimumSize(600, 700)

        # API 配置(替换为你的实际参数)
        self.api_url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
        self.api_key = "***替换为你的Secret Access Key***"  # 从API密钥管理获取
        self.model_id = "***替换为你的模型ID(ep-xxxxxx)***"  # 从我的接入点获取

        self.init_ui()
        self.messages = []
        self.current_ai_bubble = None  # 当前AI回复气泡

    def init_ui(self):
        main_widget = QWidget()
        main_layout = QVBoxLayout(main_widget)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(0)

        # 聊天显示区域
        self.chat_scroll = QScrollArea()
        self.chat_scroll.setWidgetResizable(True)
        self.chat_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        self.chat_scroll.setStyleSheet("QScrollArea {background-color: #FFFFFF; border: none;}")

        self.chat_container = QWidget()
        self.chat_layout = QVBoxLayout(self.chat_container)
        self.chat_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.chat_layout.setSpacing(8)
        self.chat_layout.setContentsMargins(10, 10, 10, 10)

        self.chat_scroll.setWidget(self.chat_container)

        # 输入区域
        input_widget = QWidget()
        input_layout = QHBoxLayout(input_widget)
        input_layout.setContentsMargins(10, 10, 10, 10)
        input_layout.setSpacing(10)

        self.message_input = QLineEdit()
        self.message_input.setPlaceholderText("输入消息...")
        self.message_input.setMinimumHeight(40)
        self.message_input.setStyleSheet("""
            QLineEdit {
                border: 1px solid #CCCCCC;
                border-radius: 20px;
                padding: 0 15px;
                font-size: 14px;
            }
        """)
        self.message_input.returnPressed.connect(self.send_message)

        self.send_button = QPushButton("发送")
        self.send_button.setMinimumHeight(40)
        self.send_button.setMinimumWidth(80)
        self.send_button.setStyleSheet("""
            QPushButton {
                background-color: #07C160;
                color: white;
                border-radius: 20px;
                font-size: 14px;
                font-weight: bold;
            }
            QPushButton:hover {
                background-color: #06B156;
            }
        """)
        self.send_button.clicked.connect(self.send_message)

        input_layout.addWidget(self.message_input)
        input_layout.addWidget(self.send_button)

        main_layout.addWidget(self.chat_scroll, 9)
        main_layout.addWidget(input_widget, 1)

        self.setCentralWidget(main_widget)

    def add_message(self, text, is_user=False):
        """添加消息到聊天窗口,自动处理格式"""
        self.messages.append({"role": "user" if is_user else "assistant", "content": text})
        bubble = MessageBubble(text, is_user)
        self.chat_layout.addWidget(bubble)
        # 滚动到底部
        self.chat_scroll.verticalScrollBar().setValue(
            self.chat_scroll.verticalScrollBar().maximum()
        )

        if not is_user:
            self.current_ai_bubble = bubble

    def send_message(self):
        text = self.message_input.text().strip()
        if not text:
            return

        self.message_input.clear()
        self.add_message(text, True)

        # 显示AI正在输入的指示器
        self.add_message("豆包正在思考...", False)
        self.current_ai_bubble = self.chat_layout.itemAt(self.chat_layout.count() - 1).widget()
        self.current_ai_bubble.label.setText("")  # 清空"正在思考"文本

        # 启动 API 线程
        self.api_thread = ApiThread(self.api_url, self.api_key, self.model_id, self.messages)
        self.api_thread.partial_response.connect(self.on_partial_response)
        self.api_thread.finished.connect(self.on_api_finished)
        self.api_thread.error.connect(self.on_api_fail)
        self.api_thread.start()

    def on_partial_response(self, content):
        """处理流式响应的部分内容"""
        if self.current_ai_bubble:
            self.current_ai_bubble.append_text(content)
            # 滚动到底部
            self.chat_scroll.verticalScrollBar().setValue(
                self.chat_scroll.verticalScrollBar().maximum()
            )

    def on_api_finished(self, full_response):
        """API 响应完成"""
        # 更新消息历史
        if self.current_ai_bubble and self.messages:
            self.messages[-1]["content"] = full_response

    def on_api_fail(self, error_msg):
        """API 失败响应"""
        if self.current_ai_bubble:
            self.current_ai_bubble.append_text(f"Error: {error_msg}")


if __name__ == "__main__":
    app = QApplication(sys.argv)

    # 全局字体设置(强制中文字体,解决符号显示问题)
    font = QFont("Microsoft YaHei, SimSun, sans-serif")
    font.setPointSize(12)
    app.setFont(font)

    # 全局样式
    app.setStyle("Fusion")
    palette = QPalette()
    palette.setColor(QPalette.ColorRole.Window, QColor(245, 245, 245))
    app.setPalette(palette)

    window = ChatApp()
    window.show()
    sys.exit(app.exec())

五、参数配置与运行指南

1. 参数替换说明

将代码中以下三个参数替换为您从火山方舟获取的实际信息:

# 替换为你的火山方舟配置信息
self.api_url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"  # 接入点API地址
self.api_key = "***替换为你的Secret Access Key***"  # API密钥管理中的Secret
self.model_id = "***替换为你的模型ID(ep-xxxxxx)***"  # 我的接入点中的模型ID

api_url :必须与接入点地域一致(如北京地域为cn-beijing)       

api_key :使用 "Secret Access Key"(非 Access Key ID) 

model_id :格式以ep-开头,从 "我的接入点" 列表获取

2. 运行程序

程序启动后,会显示聊天窗口:

  • 左侧灰色气泡:AI 回复
  • 右侧绿色气泡:用户消息
  • 支持回车或点击 "发送" 按钮提交消息
  • AI 回复会逐字显示(流式响应效果)

运行效果展示——(多轮对话+流式回复)

六、功能扩展建议

基于此基础框架,您可以进一步扩展以下功能:

1.对话历史管理 :使用 SQLite 数据库存储聊天记录,支持保存 / 加载对话                           

2. 模型切换功能 :在界面添加下拉框,支持火山方舟多个模型快速切换             

3. 主题定制 :实现明暗主题切换,允许用户自定义气泡颜色和字体       

4. 输入增强 :添加表情选择器、代码块格式化、图片发送等功能     

5. 快捷键支持 :添加 Ctrl+Enter 发送、Ctrl+Up 查看历史消息等快捷操作

通过本文的详细教程,您不仅完成了一款可用的 AI 聊天助手开发,还掌握了 PyQt6 界面设计与火山方舟 API 接入的核心技能。这个项目既可以作为个人学习练手的案例,也能扩展为团队内部的智能工具。如果在开发过程中遇到问题,欢迎参考火山方舟官方文档或在评论区交流讨论。

Logo

更多推荐