目录
前言
随着深度学习和自然语言处理技术的不断进步,问答系统进入了一个新的发展阶段,能够更加精准地理解复杂问题,支持多种知识形式的表达与多轮对话,从而实现更智能的问答体验。传统的问答方法主要依赖规则库、信息检索技术和浅层机器学习模型,尽管在特定领域中表现较为出色且系统具有较好的解释性,但在处理复杂语义和多轮对话时却显得力不从心。近年来,随着人工智能技术的快速发展,特别是大规模模型的出现,如 chatgpt、DeepSeek 等,迅速火爆整个 AI 圈。本文将使用 PySide6 搭建 AI 聊天界面,模仿 chatgpt 聊天,能实现与 AI 对话,使用 DeepSeek 大模型接口,功能:实现实时聊天,支持流式输出,下文也会教你怎么使用源码,界面如下:
一、DeepSeek注册与使用
官网地址:
DeepSeek | 深度求索
api接口改到这里了,点击开发平台
下图应该是旧版本的网页,官网应该更新了
自行注册
登录后的样子,有免费的额度,演示应该够用了
参考官网文档,python 代码感觉没啥用,又不是流式输出,对于新手不是很友好,后面我通过这个接口搭建一个界面:
二、安装环境
安装 openai 库,命令如下:
pip install openai
安装 pyside6,在自己创建的虚拟环境安装即可,命令如下
pip install pyside6==6.4.2
三、界面设计
搭建界面前需要完成 QtDesigner 配置,参考下面的教程:
设计好的界面如下:
之后转成 python 文件即可
四、后端实现
接下来,将重点介绍如何实现后端逻辑,包括如何处理用户输入、发送 API 请求并响应、以及如何更新 UI 界面。本文的代码实现了一个多轮对话的聊天界面,并结合 API 实现了智能回复功能。
1.QTextEdit 输入控件实现
先说一下用户输入的控件,使用的是 QTextEdit ,原本 QTextEdit 控件键盘回车键是换行,我将其修改成发送信息了,如果按下的是 Shift+Enter,则是换行;如果仅按下 Enter,提交用户输入的消息。
核心代码如下:
def eventFilter(self, obj, event):
if obj == self.textEdit_input and event.type() == QEvent.KeyPress:
if event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return:
if event.modifiers() & Qt.ShiftModifier:
# 按下 Shift+Enter 插入换行
cursor = self.textEdit_input.textCursor()
cursor.insertText("\n")
return True # 事件被处理,避免传递到其他控件
else:
# 按下 Enter 键发送消息
self.on_pushButton_Submit_clicked()
return True
return super().eventFilter(obj, event)
2.API 请求与响应处理
将通过一个自定义的线程类 ApiThread 来实现与后端 API 的交互。用户输入的消息会通过这个线程发送请求,获取响应后再更新到界面上。on_pushButton_Submit_clicked 方法是获取用户输入的消息并启动 API 请求线程。on_api_response 方法会将响应数据显示在聊天窗口中,并根据时间戳处理消息的时间显示。核心代码如下:
def on_pushButton_Submit_clicked(self):
message = self.textEdit_input.toPlainText()
self.textEdit_input.clear()
time = str(int(QDateTime.currentDateTime().toSecsSinceEpoch())) # 获取时间戳
if message != "": # 确保消息不为空
self.updateMessageTimeDisplay(time)
user_window = AIChatMessageWindow(self.listWidget_out.parentWidget())
user_item = QListWidgetItem(self.listWidget_out)
self.updateMessageDisplay(user_window, user_item, message, time, RoleType.user)
self.current_message_window = AIChatMessageWindow(self.listWidget_out.parentWidget())
self.current_item = QListWidgetItem(self.listWidget_out)
# 启动API请求线程
self.api_thread = ApiThread(message)
self.api_thread.response_signal.connect(self.on_api_response)
self.api_thread.start()
self.listWidget_out.setCurrentRow(self.listWidget_out.count() - 1)
def on_api_response(self, response):
time = str(int(QDateTime.currentDateTime().toSecsSinceEpoch())) # 获取时间戳
if hasattr(self, 'current_item') and self.current_item:
current_message_window = self.current_message_window
current_text = current_message_window.message_text # 获取当前显示的文本
current_text += response
# 更新消息显示
self.updateMessageDisplay(current_message_window, self.current_item, current_text, time, RoleType.system)
# 将消息列表滚动到最新的消息项
self.listWidget_out.setCurrentRow(self.listWidget_out.count() - 1)
3.消息显示与时间显示
动态地显示消息,并根据消息的时间间隔决定是否显示时间戳。每当一条消息发送或接收到响应时,都会更新消息显示,并在合适的时机显示时间戳。updateMessageDisplay 方法根据消息的文本内容调整消息项的高度,并将消息显示在聊天窗口中。updateMessageTimeDisplay 方法用来显示时间戳,当两条消息的时间间隔超过 60 秒时,会显示时间戳。核心代码如下:
def updateMessageDisplay(self, message_window, current_item, text, time, userType):
message_window.setFixedWidth(self.width()) # 设置消息窗口的宽度为主窗口的宽度
size = message_window.font_rect(text) # 获取文本的矩形区域
current_item.setSizeHint(QSize(self.width(), size.height())) # 设置列表项的高度为文本高度
message_window.setText(text, time, size, userType)
self.listWidget_out.setItemWidget(current_item, message_window) # 将消息添加到消息列表中
# 处理消息的时间显示
def updateMessageTimeDisplay(self, curMsgTime):
if self.listWidget_out.count() > 0:
lastItem = self.listWidget_out.item(self.listWidget_out.count() - 1)
message_window = self.listWidget_out.itemWidget(lastItem)
lastTime = int(message_window.message_time) # 获取最后一条消息的时间戳
curTime = int(curMsgTime) # 获取当前时间戳
show_time = (curTime - lastTime) > 60 # 如果两条消息相差超过60秒,显示时间
else:
show_time = True
if show_time:
messageTime = AIChatMessageWindow(self.listWidget_out.parentWidget())
itemTime = QListWidgetItem(self.listWidget_out)
size = QSize(self.width(), 40)
messageTime.resize(size)
itemTime.setSizeHint(size)
messageTime.setText(curMsgTime, curMsgTime, size, RoleType.current_time)
self.listWidget_out.setItemWidget(itemTime, messageTime)
4.实现头像绘制和文本的绘制
重点是这些代码实现,每个聊天作为一个QWidget窗口,简单来说 QListWidget 可以加载多个QWidget窗口,不过也可以通过其他组件实现,讲一下绘制代码 paintEvent ,paintEvent 是 Qt 框架中窗口和控件的自带方法,它是 QWidget 类的一个事件处理函数。所有继承自 QWidget 的控件(如 QMainWindow、QDialog、QLabel 等)都具有 paintEvent 方法,并且当需要重绘时,Qt 会自动调用这个方法。
在代码中每次窗口重绘时,会根据消息类型绘制不同的内容:
- RoleType.system:绘制系统消息,包含左侧头像、消息框和文本。
- RoleType.user:绘制用户消息,包含右侧头像、消息框和文本。
- RoleType.current_time:绘制时间消息,显示当前时间。
绘制过程中,QPainter 用于绘制图形、文本等,QRect 用于定义矩形区域,QPen 设置画笔颜色。绘制步骤:
- 绘制头像:首先根据消息类型选择左侧或右侧的头像,确保其大小与设备的像素比例一致。
- 绘制消息框:为每条消息绘制一个背景框,左侧系统消息框背景为浅灰色,右侧用户消息框为蓝色。
- 绘制文本:根据消息内容绘制文本,如果有换行,文本会自动换行并适应消息框的宽度。
- 绘制时间消息:时间消息居中显示,字体较小且灰色。
paintEvent 方法全部代码如下:
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
# 获取 QWidget 的设备像素比(DPI 比例)
device_pixel_ratio = self.devicePixelRatio()
if self.message_userType == RoleType.system: # openai信息
# 确保头像与设备像素比一致
pixmap = self.leftPixmap
if pixmap.devicePixelRatio() != device_pixel_ratio:
# 将 QPixmap 缩放为正确的 DPI 比例
pixmap = self.leftPixmap.scaled(self.left_icon_rect.size() * device_pixel_ratio,
Qt.KeepAspectRatio, Qt.SmoothTransformation)
pixmap.setDevicePixelRatio(device_pixel_ratio)
# 绘制左侧头像
painter.drawPixmap(self.left_icon_rect, pixmap)
# 绘制消息框
col_KuangB = QColor(234, 234, 234)
painter.setBrush(col_KuangB)
painter.drawRoundedRect(self.left_frame_rect.adjusted(-1, -1, 1, 1), 4, 4)
# 绘制消息框的实际背景
col_Kuang = QColor(255, 255, 255)
painter.setBrush(col_Kuang)
painter.drawRoundedRect(self.left_frame_rect, 4, 4)
# 绘制文本
penText = QPen(QColor(51, 51, 51)) # 设置文本颜色
painter.setPen(penText)
option = QTextOption(Qt.AlignLeft | Qt.AlignVCenter)
option.setWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) # 设置自动换行
painter.drawText(self.left_text_rect, self.message_text, option)
elif self.message_userType == RoleType.user: # 用户的消息,右侧头像
# 确保头像与设备像素比一致
pixmap = self.rightPixmap
if pixmap.devicePixelRatio() != device_pixel_ratio:
# 将 QPixmap 缩放为正确的 DPI 比例
pixmap = self.rightPixmap.scaled(self.right_icon_rect.size() * device_pixel_ratio,
Qt.KeepAspectRatio, Qt.SmoothTransformation)
pixmap.setDevicePixelRatio(device_pixel_ratio) # 设置设备像素比
# 绘制右侧头像
painter.drawPixmap(self.right_icon_rect, pixmap)
# 绘制消息框
col_Kuang = QColor(75, 164, 242)
painter.setBrush(col_Kuang)
painter.drawRoundedRect(self.right_frame_rect, 4, 4)
# 绘制文本
penText = QPen(Qt.white) # 设置文本颜色为白色
painter.setPen(penText)
option = QTextOption(Qt.AlignLeft | Qt.AlignVCenter)
option.setWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
painter.drawText(self.right_text_rect, self.message_text, option)
elif self.message_userType == RoleType.current_time: # 时间消息
penText = QPen(QColor(153, 153, 153)) # 设置时间文本的颜色
painter.setPen(penText)
option = QTextOption(Qt.AlignCenter) # 时间居中显示
option.setWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
# 设置时间文本的字体
te_font = self.font()
te_font.setFamily("Microsoft YaHei")
te_font.setPointSize(10)
painter.setFont(te_font)
painter.drawText(self.rect(), self.current_time, option)
在代码实现过程出现绘制的头像模糊,我电脑设备高分辨率会出现模糊,为了确保在不同显示设备上,使用 devicePixelRatio 方法获取当前显示设备的像素比(DPI比例)之后通过 pixmap.scaled 方法调整图像的大小,这样就可以使图像能够按照正确的设备像素比(DPI)进行缩放和显示,因此能在高分辨率显示器上显示清晰,且不会模糊。核心代码如下:
# 获取 QWidget 的设备像素比(DPI 比例)
device_pixel_ratio = self.devicePixelRatio()
if self.message_userType == RoleType.system: # openai信息
# 确保头像与设备像素比一致
pixmap = self.leftPixmap
if pixmap.devicePixelRatio() != device_pixel_ratio:
# 将 QPixmap 缩放为正确的 DPI 比例
pixmap = self.leftPixmap.scaled(self.left_icon_rect.size() * device_pixel_ratio,
Qt.KeepAspectRatio, Qt.SmoothTransformation)
pixmap.setDevicePixelRatio(device_pixel_ratio)
# 绘制左侧头像
painter.drawPixmap(self.left_icon_rect, pixmap)
5.更换头像
如需更换自己头像也是很简单的,在 leftPixmap 和 rightPixmap 填入图片路径即可。
五、完整源码下载和使用方法
完整源码发送 AI聊天界面 关键字即可获取,自己配置一下环境即可运行起来,代码需要更换自己的 API keys,在 DeepSeek 平台创建即可,创建时候记住 keys 值,它只会出现一次
之后在 deepseek.py 文件中的 api_key 处的单引号里面填入你的 keys 就行
总结
本文到此结束,对你有帮助帮忙点个小爱心呗,完整源码发送 AI聊天界面 关键字即可获取
参考文章: Qt 学习之路】Qt5气泡式聊天框——QListWidget+QPainter实现
标签:AI,DeepSeek,current,PySide6,消息,message,绘制,self,painter From: https://blog.csdn.net/weixin_44779079/article/details/145226517