前言
最近开发 「Verbiverse」这个工具,学了些 QT pyside6 与 LangChain 相关的知识与使用技巧,这篇文章主要会以项目解析为主,同时总结下 pyside6 与 LangChain 使用技巧。
如果你想参与 Verbiverse 一起开发新功能或是打算利用 Pyside6 + LangChain 构建自己的大语言模型应用,这篇文章可以对你有所帮助。
Verbiverse 工具介绍:「利用大模型,我就是想一边刷美剧,一边把英语学好!「Verbiverse V1.0」」
环境搭建
主要使用 python 开发,我是用 poetry 作为包管理器,基本使用如下:
- 使用 poetry 创建:
poetry new <project name>
- 查看当前 poetry 环境:
poetry env info
- 切换 python 版本:
pyenv local <version>; poetry env use <python-path>
- 进入 poetry 虚拟环境 shell:
poetry shell
- 退出 poetry 虚拟环境,注意直接 deactivate 是不行的:
exit
会在项目根目录生成 pyproject.toml 文件,记录项目信息与依赖库,可以使用命令或者手动修改,修改完后记得 poetry lock 并 update 下更新依赖。
poetry 同样可以配合 conda 使用,进入 conda 虚拟环境后,查看 env info 如下:
Verbiverse 项目 环境搭建:
- clone 源码到本地:
git clone https://github.com/HATTER-LONG/Verbiverse.git
- 使用 conda 或 python (>=3.9, <=3.12) venv 创建虚拟环境,推荐使用 conda:
- 使用 conda:
conda create -n Verbiverse python=3.11
- 激活虚拟环境:
conda activate Verbiverse
;
- 激活虚拟环境:
- 使用 venv,进入源码目录后:
python3 -m venv ./.venv;source ./venv/bin/activate
;
- 使用 conda:
- 安装 poetry:
- 确认已正确启用虚拟环境;
pip install -U pip setuptools;pip install poetry
;
- 安装项目依赖环境:
poetry install
:- 需要代理则取消
pyproject.toml
中[[tool.poetry.source]]
相关注释,然后重新poetry lock --no-update
;
- 需要代理则取消
- 运行程序:
python3 main.py
项目结构
.
├── LICENSE
├── PDF_js # 开源 PDF js 源码,加了一个 qt web 的钩子用来订阅 pdf 页数变化
├── PIC
├── README.md
├── RoadMap.md
├── build.py # 辅助项目开发的脚本,支持 ui -> python、国际化、生成 qrc、优化 prompt 等功能
├── command_alias # alias 开发脚本命令
├── icons
├── main.py # 程序入口
├── main.spec # pyinstaller 打包脚本
├── poetry.lock
├── pyproject.toml # poetry 版本控制
├── tests # 测试代码
├── tools # 工具代码,主要用来优化 prompt
└── verbiverse # 项目主要源码
├── CustomWidgets # 自定义子控件目录
├── Functions # 通用的函数方法目录
├── LLM # 基于 LangChain 相关 LLM 实现目录
├── MainWindow.py # 主界面框架代码
├── UI # 主要 UI 界面目录
├── __init__.py
└── resources # 资源文件、翻译、图片、提示词等
Pyside6 + Fluent Widgets
看过 Verbiverse 界面稍微了解 QT 的同学就会发现这个 UI 和原生界面的区别,我这里用了 PyQt-Fluent-Widgets ,一个基于 Qt 的 Fluent 设计风格组件库,可以方便我们快速构建出一个 像模像样
的 UI 界面,而不用细扣 qss。
接下来以 Verbiverse 工具的视频播放界面举例如何做的:
添加子页面
verbiverse/MainWindow.py
是主界面框架代码,其控制如何添加子页面,我们先简单添加个测试页面:
测试页面代码,很简单,只是在页面正中间显示一个 SubtitleLabel
,这个 SubtitleLabel
看起来很陌生因为它是 qfluentwidgets
框架提供的:
class Widget(QFrame):
def __init__(self, text: str, parent=None):
super().__init__(parent=parent)
self.label = SubtitleLabel(text, self)
self.hBoxLayout = QHBoxLayout(self)
setFont(self.label, 24)
self.label.setAlignment(Qt.AlignCenter)
self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter)
self.setObjectName(text.replace(" ", "-"))
# !IMPORTANT: leave some space for title bar
self.hBoxLayout.setContentsMargins(0, 32, 0, 0)
如下图修改,FluentWindow 同样是 qfluentwidgets 提供的框架页面,因此想要添加子页面很简单:
效果如下图,可以看到我们成功添加了一个 Test 子页面:
接下来我们使用 Qt Designer 工具创建我们基本的页面结构:
- 打开 Qt Designer:安装 pyside6 后已经包含工具,使用命令打开,
pyside6-designer &
; - 按照设想设计控件布局,复杂的子控件使用 QWidget 暂时占位,最终效果如下图:
- 注意⚠️:我这里 subtitle_browser 忘记恢复成 QWidget 了,后续字幕区域你的效果可能和我示例的图片不一致,没有影响,到第四节将会处理这部分。
- 保存 ui 文件,后使用命令将其转为 python 代码:
pyside6-uic verbiverse/UI/test.ui -o verbiverse/UI/test_ui.py
- 增加 test.py 使用生成的 test_ui.py 编辑页面:
- 代码:
from PySide6.QtWidgets import QWidget
from test_ui import Ui_Form
class CTest(QWidget, Ui_Form):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
- `__init__.py` 中新增模块,允许外部访问;
- 在
verbiverse/MainWindow.py
中使用 CTest 新增子页面:
最终我们成功添加了自己设计的页面,虽说这个界面大部分还是空的 QWidget,但接下来会一步步完善:
视频播放区
视频播放区我们可以直接使用 qfluentwidgets 提供的 VideoWidget:
先选中 Video 部分占位的 QWidget 控件,鼠标右键提升为 qfluentwidgets.multimedia 的 VideoWidget:
再使用 build
或者 pyside6-uic
命令重新生成 python 代码,就可以看到正常使用 video_widget 控件了:
最终效果如下:
鼠标右键子菜单
子菜单很好加,如下代码即可:
from PySide6.QtCore import QPoint, Qt, Slot
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QWidget
from qfluentwidgets import FluentIcon as FIF
from qfluentwidgets import (
RoundMenu,
)
from test_ui import Ui_Form
class CTest(QWidget, Ui_Form):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
# 设置自定义菜单
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._onContextMenuRequested)
# 子菜单回调
@Slot(QPoint)
def _onContextMenuRequested(self, event: QPoint) -> None:
menu = RoundMenu(parent=self)
# 增加菜单项
menu.addAction(
QAction(
FIF.ADD_TO.icon(),
self.tr("test"),
self,
triggered=lambda: print("test"), # 回调函数
)
)
menu.exec(self.mapToGlobal(event))
最终效果如下:
字幕功能区域
字幕功能区域,是页面的核心功能区,这可就没有现成的插件可以供我们用了,这时就需要 Custom Widgets
:
在 verbiverse/CustomWidgets
目录中新建控件代码文件:
与前面章节类似,提升字幕区域占位的 QWidget 为我们自己创建的控件:
重新 build 将 ui 生成为 python 代码后,在我们的子页面可以测试控件是否正常:
最终效果如下:
字幕列表与视频列表
最终右侧的字幕列表与视频列表是通过 Tab 标签业实现,其实与自定义的字幕控件流程差别不大,这里就不再细说,简单说下用到的控件:
- 使用到的 qfluentwidgets 提供的 例子 修改而来:
from PySide6.QtWidgets import (
QStackedWidget,
QVBoxLayout,
QWidget,
)
from qfluentwidgets import ListWidget, SegmentedWidget
class CTabWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.pivot = SegmentedWidget(self)
self.stackedWidget = QStackedWidget(self)
self.vBoxLayout = QVBoxLayout(self)
self.subtitle = ListWidget(self)
self.file_list = ListWidget(self)
# add items to pivot
self.addSubInterface(self.subtitle, "SubTitleInterface", self.tr("SubTitle"))
self.addSubInterface(
self.file_list, "videoFileInterface", self.tr("Video List")
)
self.vBoxLayout.addWidget(self.pivot)
self.vBoxLayout.addWidget(self.stackedWidget)
self.vBoxLayout.setContentsMargins(15, 10, 15, 30)
self.stackedWidget.setCurrentWidget(self.subtitle)
self.pivot.setCurrentItem(self.subtitle.objectName())
self.pivot.currentItemChanged.connect(
lambda k: self.stackedWidget.setCurrentWidget(self.findChild(QWidget, k))
)
def addSubInterface(self, widget: QWidget, objectName, text):
widget.setObjectName(objectName)
self.stackedWidget.addWidget(widget)
self.pivot.addItem(routeKey=objectName, text=text)
依旧提升控件为我们自定义的即可,子页面中我们增加测试代码:
class CTest(QWidget, Ui_Form):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._onContextMenuRequested)
self.subtitel_browser.setText("test subtitle")
# 添加列表数据
widget = QListWidgetItem("test list")
self.tab_widget.subtitle.addItem(widget)
最终效果:
国际化
我们最开始添加子页面时,是固定写死了页面名 Test
,如果想要支持多语言翻译,则需要使用 self.tr
函数:
TL;DR 使用 self.tr
函数包裹翻译字符串后,项目配套的脚本直接使用 build 会自动生成对应 ts 文件,手动修改完翻译后,再次 build 即可生成最终的资源文件:
- 不太理解这个操作的话,可以看下文手动操作流程。
手动操作步骤如下:
verbiverse/MainWindow.py
中使用 self.tr
函数,然后生成 ts 翻译文件:
将分散的文件合并到统一的 ts 中,使用命令转为 qm 格式文件:
生成资源文件:
代码中加载对应文件翻译即可:
最终效果如下:
LangChain
LangChain 作为流行的开源框架,旨在帮助开发快速创建基于大语言模型的应用程序。
LangChain 基础
一个基于 LLM 模型的对话工具一般由以下几个部分:
- 对话模型:聊天机器人界面基于消息而不是原始文本,因此最适合 chat model 而不是纯文本 LLM。Chat models | 🦜️🔗 LangChain
- 提示词模板:这简化了组合默认消息、用户输入、聊天历史和(可选)额外检索上下文的提示的过程。
- 对话历史:帮助聊天机器人“记住”过去的互动记录,并在回答后续问题时将其考虑在内。Chat Messages | 🦜️🔗 LangChain
- 检索器:如果您想构建一个聊天机器人,它可以使用特定领域的最新知识作为上下文来增强其响应。Retrievers | 🦜️🔗 LangChain
基本的对话系统
基本的对话系统如下:
from langchain.schema import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
# 配置信息
api_key = "lm-studio"
api_url = "https://localhost:1234/v1"
model = "Qwen/Qwen1.5-7B-Chat-GGUF/qwen1_5-7b-chat-q6_k.gguf"
# 创建 OpenAI Chat
chat = ChatOpenAI(
model_name=model,
openai_api_key=api_key,
openai_api_base=api_url,
temperature=0.7,
)
# 创建一个对话 prompt 模板,标记了 system 的信息
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are a helpful assistant. Answer all questions to the best of your ability.",
),
# 用户信息占位符,后续用户输入的信息,可以通过 variable_name 进行链接
MessagesPlaceholder(variable_name="messages"),
]
)
msg = {
# 标记为 messages 信息,与 prompt 链接
"messages": [
HumanMessage(
content="Translate this sentence from English to Chinese: I love programming."
),
AIMessage(content="我喜欢编程。"),
HumanMessage(content="What did you just say?"),
]
}
# 通过 | 管道符号链接 prompt 与 chat
mchain = prompt | chat
for chunk in mchain.stream(msg):
print(chunk.content, end="", flush=True)
如何构建 RAG
官方教程已经很详细了:How to add chat history | 🦜️🔗 LangChain
主要流程如下图,当用户输入请求后会使用 LLM 对请求进行向量化,经由向量数据库检索最接近的内容,将这些内容与输入一起组合发给 LLM 得到最终的答案:
项目 LLM 源码结构
Verbiverse 有关 LLM 相关的代码均在 verbiverse/LLM
目录下,相关提示词在 verbiverse/resources/prompt/
资源目录下:
❯ tree ./LLM -L 1
./LLM
├── ChatChain.py # 基础对话类,支持记录最近 10 条对话记录,之前老版本用于阅读界面对话
├── ChatRAGChain.py # 支持传入 PDF 数据作为数据嵌入的 RAG 问答类,当前 V1.0 支持嵌入后使用此类
├── ChatWithCustomHistoryChain.py # 支持传入指定历史对话的对话类,用于输入检查功能
├── ChatWorkerThread.py # 对话多线程异步请求类
├── ExplainWorkerThread.py # 解释多线程异步请求类
├── LLMServerInfo.py # 根据配置 LLM 信息获取,包括提示词、使用的模型等
├── ModuleLogger.py
├── OpenAI.py # OpenAI 接口适配
├── TongYiQWen.py # 通义千问接口适配
└── __init__.py
结构图如下:
对话系统
创建一个基本的对话系统很简单,使用 tests/test_ChatChain.py
的代码即可,前提是先配置好了相关模型信息:
- 使用 ChatChain 基础对话类实现,会自动记录最近 10 条历史对话记录:
import verbiverse.resources.resources_rc # noqa: F401
from verbiverse.LLM.ChatChain import ChatChain
if __name__ == "__main__":
chat = ChatChain()
while True:
input_string = input("> ")
for chunk in chat.stream(input_string):
print(chunk.content, end="", flush=True)
print("\n")
总结
本来想要将 LangChain 写的详细些,但是发现文章有些太长了而且都是在复制官方的教程,实在没什么营养 🤣 !
最终决定主要还是围绕解析项目源码结构为主,也方便想要参与项目的同学快速上手。
觉得文章写的不错对 Verbiverse 工具感兴趣的话,快来给项目个 star 吧 🙏 🥺 !
项目源码路径:HATTER-LONG/Verbiverse: 利用 LLM 大模型辅助阅读 PDF 与观看视频,用以提升语言能力。 (github.com)
文章中的演示代码分支:HATTER-LONG/Verbiverse at train (github.com)