博文视点
随着以ChatGPT为代表的AI模型不断发展,它逐渐能够处理复杂的对话场景,并生成连贯的回答。国内类似于ChatGPT的应用发展也较为迅速,如科大讯飞的星火认知大模型、百度的文心一言等。
本文以星火认知大模型为例,结合相关知识点,搭建一个AI问答小工具。
1.主要功能展示
(1)快捷提问:在输入问题后,按快捷键“Ctrl+Enter”即可发送问题进行提问。
(2)AI问答:回答问题,如写一个小红书文案,界面如下图所示。
(3)保存记录:将提问记录保存为HTML或TXT格式,界面如下图所示。
2.AI问答小工具开发前的准备
(1)申请星火认知大模型所需的文件。
在讯飞开放平台申请星火认知大模型所需的AppID、APISecret、APIKey,具体如何申请,详见星火认知大模型的开发文档。申请成功后,一般至少会提供500,000 token的使用量,有效期为一年。
Token通常是指文本处理过程中的最小单位,1 token约等于1.5个汉字或0.8个单词。在自然语言处理(Natural Language Processing,NLP)中,将文本拆分成token是常见的预处理步骤之一。
(2)安装必要模块。
星火认知大模型的调用分为两种方式:Web方式和SDK方式。本实战将采用Web方式,利用WebSocket技术实现客户端与服务器之间的连接和通信。
WebSocket是一种通信协议,允许在客户端和服务器之间建立双向的实时通信连接。一旦成功建立连接,客户端和服务器之间就可以通过发送消息进行实时交流。
因此,需要安装Python支持的websocket模块,代码如下:
pip install websocketpip install websocket-client
(3)学习开发文档。
星火认知大模型的开发文档提供了通用鉴权URL生成方式、错误代码原因、接口请求和接口响应等说明,以及Python调用示例代码。建议读者提前了解这些内容。
截至2024年10月,星火认知大模型已提供了Spark4.0 Ultra、Spark Max、Spark Pro、Spark Lite这4个版本的API调用方式。本书采用的是Spark Lite版本的调用方式。由于星火认知大模型后期可能会更新API的调用方式,这可能会影响本实战程序的成功运行,但其原理是不变的。拿这段话形容AI科技的日新月异,一点也不为过。
3.程序结构
整个程序可以分成两部分:
借鉴星火认知大模型提供的Python调用示例代码完成与模型的网络交互(其开发文档上已经有较为详细的说明)
完成问题的提交与结果的展示。
4.代码解析
整个程序大约有400行代码(不包含星火认知大模型提供的部分Python调用示例代码),这里仅展示核心代码。
(1)errorDialog类。
errorDialog类继承自QDialog类,主要用于查看错误日志,如下图所示。
核心代码如下:
self.setWindowFlags(Qt.WindowType.Window) # 窗体样式errorLog = QPlainTextEdit(self)font = QFont()font.setPointSize(11) # 字体大小with codecs.open(f”{currentdir}\error.log”, “r”, “utf-8”, errors=”ignore”) as file: content = file.read() # 读取文件 errorLog.setPlainText(content) errorLog.setFont(font)
虽然errorDialog类继承自QDialog类,但是它设置了Qt.WindowType.Window样式,使得窗体具有最大化和最小化按钮,便于查看日志。
(2)KeyDialog类。
KeyDialog类继承自QDialog类,主要用于配置WebSocket参数,如下图所示。图中的参数应以自己申请的参数为准。
在填写完参数后,会将相关信息写入Windows的注册表,如下图所示。
如果已经保存过配置信息,则打开对话框后会自动加载这些配置信息。核心代码如下:
class KeyDialog(QDialog): keySignal = pyqtSignal(list) # 参数配置的信号 def init(self, SparkDesk, Parent=None): super()._init(Parent) # 主窗体传递过来的QSettings(“SparkDesk”, “APIKey”)对象 self.sparkdesk = SparkDesk self.initUI() self.loadDate() def initUI(self): …… # 省略部分代码 # 一些控件的使用及布局 buttonBox.accepted.connect(self.settingKey) buttonBox.rejected.connect(self.reject)
def loadDate(self): “””载入配置信息””” # 检查注册表中是否已有配置,如果已有配置信息,则将其载入 if all([self.sparkdesk.contains(“APPID”), self.sparkdesk.contains(“API_Secret”), self.sparkdesk.contains(“API_Key”) , self.sparkdesk.contains(“GPT_Url”)]): self.appid.setText(self.sparkdesk.value(“APPID”)) self.api_secret.setText(self.sparkdesk.value(“API_Secret”)) self.api_key.setText(self.sparkdesk.value(“API_Key”)) self.gpt_url.setText(self.sparkdesk.value(“GPT_Url”)) def settingKey(self): “””设置鉴权密钥””” …… # 省略部分代码 if not all([APP_iD, API_Secret, API_Key, GPT_Url]): # 4个参数不能为空 QMessageBox.warning(self, “警告”, “请确保所有字段都填写完整。”) return # 将4项关键设置存入注册表 self.sparkdesk.setValue(“APPID”, APP_iD) …… # 省略类似代码 # 将配置发送到主窗体中 self.keySignal.emit([APP_iD, API_Secret, API_Key, GPT_Url]) self.accept()
(3)MyWidget类。
MyWidget类继承自QMainWindow类,可以方便地实现菜单功能。
【代码片段1】
self.SparkDesksettings = QSettings(“SparkDesk”, “APIKey”)# 设置APP_iD、API_Secret、API_Key、GPT_Url 默认值self.APP_iD = “”self.API_Secret = “”self.API_Key = “”self.GPT_Url = “”self.answer = “” # 答案默认为空
设置一些属性,以便程序使用。其中,QSettings主要用于将WebSocket的配置存入注册表。
【代码片段2】
def loadDate(self): “””载入数据””” # 如果注册表中没有相关配置,则提示无法使用 if not all([self.SparkDesksettings.contains(“APPID”), self.SparkDesksettings.contains(“API_Secret”), self.SparkDesksettings.contains(“API_Key”), self.SparkDesksettings.contains(“GPT_Url”)]): self.chatAnswer.append(“未配置模型鉴权参数,无法使用!
“) self.sendButton.setEnabled(False) # 如果无法使用,则禁止发送问题 else: # 如果注册表中有相关配置,则生成Ws_Param对象 self.APP_iD = self.SparkDesksettings.value(“APPID”) self.API_Secret = self.SparkDesksettings.value(“API_Secret”) self.API_Key = self.SparkDesksettings.value(“API_Key”) self.GPT_Url = self.SparkDesksettings.value(“GPT_Url”) self.wsParam = self.createWs_Param(self.APP_iD, self.API_Key, self.API_Secret, self.GPT_Url)
def createWs_Param(self, appid, api_key, api_secret, gpt_url): # 生成Ws_Param对象,以便为下一步鉴权URL做准备 # 具体信息请参阅星火认知大模型的开发文档 wsParam = SparkApi.Ws_Param(appid, api_key, api_secret, gpt_url) return wsParam
上面的程序首先会检查程序中是否有相关配置,若没有相关配置,则运行结果如下图所示,同时禁止发送问题。
【代码片段3】
以下代码用于实现整个界面。
def initUI(self): # ###########创建菜单############# settingMenu = self.menuBar().addMenu(“设置(&S)”) keyAct = QAction(“密钥(&K)”, self) keyAct.setShortcut(“ALt+K”) keyAct.setStatusTip(“密钥设置”) settingMenu.addAction(keyAct) …… # 其他菜单的创建与此相似,这里省略相关代码 # ###########创建聊天窗体############# spliter = QSplitter(self) spliter.setOrientation(Qt.Orientation.Vertical) widget= QWidget(spliter) # 使用QPlainTextEdit控件提出问题 self.chatQue = QPlainTextEdit(widget) fontQue = QFont() fontQue.setPointSize(11) self.chatQue.setFont(fontQue) # 占位文本 self.chatQue.setPlaceholderText(“可以在这里输入你的问题,按快捷键“Ctrl+Enter”即可发送问题”) # 使用QTextEdit控件回答问题 self.chatAnswer = QTextEdit() # 只读 self.chatAnswer.setReadOnly(True) self.chatAnswer.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.chatAnswer.customContextMenuRequested.connect(self.showContextMenu) # 上面的两行代码用于对self.chatAnswer右键菜单进行设置 self.cursor = self.chatAnswer.textCursor() # 获取当前文档中的光标 self.sendButton = QPushButton(“发送”, widget) …… # 省略布局代码 keyAct.triggered.connect(self.execKeyDialog) errorAct.triggered.connect(self.showLog) self.sendButton.clicked.connect(self.sendQuestion) zoomInAct.triggered.connect(self.chatAnswer.zoomIn) zoomOutAct.triggered.connect(self.chatAnswer.zoomOut)
【代码片段4】
以下代码用于设置对话框和查看错误日志。
def execKeyDialog(self): “””鉴权参数设置对话框””” keydialog = KeyDialog(self.SparkDesksettings, self) keydialog.keySignal.connect(self.readSparkDesksettings) keydialog.exec()
def showLog(self): “””查看错误日志””” errordialog = errorDialog(self) errordialog.exec()
【代码片段5】
以下代码用于发送问题并在聊天窗体中显示问题。
def sendQuestion(self): “””发送问题””” text = self.chatQue.toPlainText() if not text: return # 在发送问题后,提问处需要清屏 self.chatQue.clear() self.setRespondQue(text)
def setRespondQue(self, ques): “”” 将问题发送到聊天窗体中 ques表示问题 “”” self.setFormatText(“Q”) # 设置格式 # 将问题插入QTextEdit控件 self.cursor.insertText(ques + ‘\n’) self.setRespondAnswer(ques)
【代码片段6】
以下代码用于设置问题和答案的格式,以便进行区别。
def getCurrentDateTime(self): “””获取当前时间””” currentDateTime = QDateTime.currentDateTime().toString(“yyyy-MM-dd hhss”) return currentDateTime
def setFormatText(self, Q_A): “”” 格式化问答 Q_A表示识别问题和答案 “”” currentDateTime = ‘ ‘ + self.getCurrentDateTime() + ‘\n’ textFormat = QTextCharFormat() font = QFont() font.setItalic(True) # 斜体 font.setBold(True) # 加粗 textFormat.setFont(font) if Q_A == “Q”: # 给问题中的时间添加格式 self.cursor.insertImage(f”{current_dir}\images\heart.png”) # 心形图标 textFormat.setForeground(Qt.GlobalColor.red) # 红色 self.cursor.setCharFormat(textFormat) # 当前文档的光标格式 self.cursor.insertText(currentDateTime) # 插入时间 else: # 给答案中的时间添加格式 self.cursor.insertImage(f”{current_dir}\images\star.png”) # 星形图标 textFormat.setForeground(Qt.GlobalColor.darkRed) # 暗红色 self.cursor.setCharFormat(textFormat) self.cursor.insertText(currentDateTime) # 给除时间外的内容添加格式 font.setItalic(False) font.setBold(False) textFormat.setFont(font) textFormat.setForeground(Qt.GlobalColor.black) # 黑色 self.cursor.setCharFormat(textFormat)
【代码片段7】
以下代码将实现在提出问题后,聊天窗体会提示“AI思考中……”,用于提示用户需要等待回答,如下图所示。
对于一些较为复杂的问题,为了防止 AI问答小工具长时间思考,导致窗体卡死而无法正常工作,这里使用QCoreApplication.processEvents()方法来确保界面能够及时刷新和响应。
def setRespondAnswer(self, ques): “”” 得到问题的反馈 ques表示问题 “”” self.setFormatText(‘A’) # 提示正在等待回答中 self.cursor.insertText(“AI思考中……”) # 因为需要等待问题反馈,所以这里设置代码以防AI问答小工具卡死 QCoreApplication.processEvents() self.getAnswer(ques)
def getAnswer(self, ques): “”” 从星火认知大模型中获取答案 ques表示问题 这部分代码主要参考了星火认知大模型提供的Python调用示例代码 “”” wsUrl = self.wsParam.create_url() websocket.enableTrace(False) ws = websocket.WebSocketApp(wsUrl, on_message=self.on_message, on_error=self.on_error, on_close=SparkApi.on_close, on_open=SparkApi.on_open) ws.appid = self.APP_iD ws.question = ques ws.run_forever(sslopt={“cert_reqs”: ssl.CERT_NONE})
【代码片段8】
以下代码将实现接收回答和处理错误的方式,其中SparkApi.on_error表示从SparkApi.py文件中调用on_error()函数。SparkApi.py是星火认知大模型提供的Python示例文件。
def on_error(self, ws, error): “””错误处理””” SparkApi.on_error(ws, error) self.chatAnswer.append(“
连接错误,详细原因请查看日志!
“)
def on_message(self, ws, message): “””处理接收的WebSocket消息,详细信息请查看星火认知大模型的开发文档””” data = json.loads(message) code = data[‘header’][‘code’] if code != 0: SparkApi.error(code, data) self.chatAnswer.append(“
请求错误,详细原因请查找日志!
“) ws.close() else: choices = data[“payload”][“choices”] status = choices[“status”] content = choices[“text”][0][“content”] self.answer += content if status == 2: # 该代码表示已经回答完毕 self.cursor.movePosition(QTextCursor.MoveOperation.StartOfLine)self.cursor.movePosition(QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor) self.cursor.deleteChar() # 删除字符 # 以上3段代码用于删除提示“AI思考中……” self.cursor.insertMarkdown(self.answer) # 插入markdown self.chatAnswer.append(‘\n\n回答完毕!\n ————————————————————\n\n’) self.answer = “” # 清除暂存的答案 ws.close() # 关闭WebSocket连接
【代码片段9】
以下代码将实现通过QTextEdit类的右键菜单保存问答记录。问答记录可以被保存为HTML和TXT两种格式。
def showContextMenu(self, position): # 创建self.chatAnswer自定义菜单对象 context_menu = QMenu(self) # 创建动作并将其添加到自定义菜单中 copy_action = QAction(“复制”, self) copy_action.setShortcut(QKeySequence.StandardKey.Copy) context_menu.addAction(copy_action) # 连接动作的信号与槽方法 copy_action.triggered.connect(self.chatAnswer.copy) …… # 其他几个菜单的实现与此类似,这里省略相关代码 # 在指定位置显示自定义菜单 context_menu.exec(self.chatAnswer.mapToGlobal(position))
def saveAsChat(self): “””保存问答记录””” if not self.chatAnswer.toPlainText(): return saveFileName = QFileDialog.getSaveFileName(self, “保存文件”, “./“, “HTML files (.html);;Text files (.txt)”) absFileName = saveFileName[0] if absFileName: with codecs.open(absFileName, “w”, “utf-8”, errors=”ignore”) as f: if absFileName.split(‘.’)[1] == “html”: f.write(self.chatAnswer.toHtml()) else: f.write(self.chatAnswer.toPlainText())
【代码片段10】
以下代码将实现在按快捷键后,发送问题。
def keyPressEvent(self, event): “””实现在按快捷键“Ctrl+Enter”后,发送问题””” if event.modifiers() == Qt.KeyboardModifier.ControlModifier and event.key() == Qt.Key.Key_Return: self.sendButton.click() else: super().keyPressEvent(event)
整个AI问答小工具基本满足了日常提问的需求,但仍存在不足,如没有前置上下文,不能补充提问。虽然程序中使用了QCoreApplication.processEvents()方法来避免窗体无法正常工作,但窗体仍可能会出现“未响应”的情况。这个问题将在学完第14章中的内容后进行优化。
本文节选自《PyQt6 实战派》
图书介绍
(一)内容全面,从基础到进阶
1.初识篇
就像踏入一个新的世界,第一步总是至关重要的。在这部分,书里会详细地给你介绍 PyQt6 的基本概念和特点,让你对这个神奇的框架有一个初步的认识。同时,还会手把手地教你如何搭建开发环境,这就好比为你的编程之旅准备好交通工具。当你成功搭建好环境,就像拥有了一辆性能优良的汽车,随时可以出发探索 PyQt6 的精彩世界。
2入门篇
这是一个循序渐进的学习过程。从最基础的上手PyQt6 开始,通过一个个简单而有趣的示例,让你逐渐熟悉 PyQt6 的各种控件和使用方法。
比如,在 “上手 PyQt6” 这一章节中,还有 “【实战】更复杂一点 —— 四则运算小游戏”,通过开发这个小游戏,你能快速掌握如何运用 PyQt6 的基本功能,实现用户交互和简单的逻辑运算。
3进阶篇
进阶篇会深入讲解 PyQt6 中的各种高级特性,比如多线程编程、视图模型设计模式等。
以多线程编程为例,在实际开发中,很多时候我们需要程序能够同时处理多个任务,就像一个人同时做好几件事。多线程编程可以让你的应用程序更加高效地运行,而书中会用通俗易懂的语言和丰富的示例,教你如何在 PyQt6 中实现多线程编程,让你轻松驾驭这一强大的技术。
4.综合实例篇
这部分可谓是全书的精华所在,就像一场盛大的演出,把之前所学的知识全部串联起来,呈现出精彩的成果。
书中会通过多个实际项目案例,如开发一个功能完备的文本编辑器、实现一个简单的数据库管理系统等,全方位地展示 PyQt6 在不同场景下的应用。
每个项目都从需求分析开始,一步步引导你进行设计、编码和调试,让你在实践中真正掌握 PyQt6 的精髓,提升解决实际问题的能力。
(二)实战性强,学以致用
这本书最大的亮点就是实战性超强。它不像一些理论书籍,只是纸上谈兵,而是通过大量的实际案例,让你在实践中学习。每一个知识点后面,几乎都跟着一个实战项目,从简单的界面设计,到复杂的业务逻辑实现,一应俱全。
比如在学习文本输入类控件时,书中有
“【实战】文本输入、密码输入、自动补全网址”
“【实战】简单记事本”
“【实战】AI 问答小工具” 等多个项目。
以 “【实战】简单记事本” 为例,你将学习如何创建一个具备基本功能的记事本应用,包括打开文件、保存文件、编辑文本等。在这个过程中,你会深入了解文本输入控件的各种属性和方法,以及如何处理文件操作等相关知识。通过亲手完成这些项目,你能迅速将所学知识转化为实际技能,真正做到学以致用。
(三)讲解通俗易懂,易于上手
《PyQt6 实战派》作者在编写时,采用了通俗易懂的语言,把复杂的概念用形象生动的比喻进行解释。
比如,在讲解信号与槽机制时,把信号比作一个人发出的呼喊,槽则是另一个人听到呼喊后做出的反应。这样一来,原本抽象的概念变得简单易懂,即使是没有任何编程基础的小白,也能轻松理解。
书中的代码示例简洁明了,每一行代码都有详细的注释,就像一个贴心的导师在旁边为你讲解每一步的操作。无论是在学习过程中遇到问题,还是想要回顾某个知识点,这些注释都能帮助你快速理解代码的意图和作用。
四、适合人群
尊敬的博文视点用户您好: 欢迎您访问本站,您在本站点访问过程中遇到任何问题,均可以在本页留言,我们会根据您的意见和建议,对网站进行不断的优化和改进,给您带来更好的访问体验! 同时,您被采纳的意见和建议,管理员也会赠送您相应的积分...
时隔一周,让大家时刻挂念的《Unity3D实战核心技术详解》终于开放预售啦! 这本书不仅满足了很多年轻人的学习欲望,并且与实际开发相结合,能够解决工作中真实遇到的问题。预售期间优惠多多,实在不容错过! Unity 3D实战核心技术详解 ...
如题 ...
读者评论