前言

最近开发 「Verbiverse」这个工具,学了些 QT pyside6 与 LangChain 相关的知识与使用技巧,这篇文章主要会以项目解析为主,同时总结下 pyside6 与 LangChain 使用技巧。

如果你想参与 Verbiverse 一起开发新功能或是打算利用 Pyside6 + LangChain 构建自己的大语言模型应用,这篇文章可以对你有所帮助。

Verbiverse 工具介绍:「利用大模型,我就是想一边刷美剧,一边把英语学好!「Verbiverse V1.0」

环境搭建

主要使用 python 开发,我是用 poetry 作为包管理器,基本使用如下:

  1. 使用 poetry 创建:poetry new <project name>
  2. 查看当前 poetry 环境:poetry env info
  3. 切换 python 版本: pyenv local <version>; poetry env use <python-path>
  4. 进入 poetry 虚拟环境 shell:poetry shell
  5. 退出 poetry 虚拟环境,注意直接 deactivate 是不行的:exit

会在项目根目录生成 pyproject.toml 文件,记录项目信息与依赖库,可以使用命令或者手动修改,修改完后记得 poetry lock 并 update 下更新依赖。

poetry 同样可以配合 conda 使用,进入 conda 虚拟环境后,查看 env info 如下:

Verbiverse 项目 环境搭建:

  1. clone 源码到本地:git clone https://github.com/HATTER-LONG/Verbiverse.git
  2. 使用 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;
  3. 安装 poetry:
    • 确认已正确启用虚拟环境;
    • pip install -U pip setuptools;pip install poetry;
  4. 安装项目依赖环境:poetry install
    • 需要代理则取消 pyproject.toml[[tool.poetry.source]] 相关注释,然后重新 poetry lock --no-update;
  5. 运行程序: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 工具创建我们基本的页面结构:

  1. 打开 Qt Designer:安装 pyside6 后已经包含工具,使用命令打开,pyside6-designer &
  2. 按照设想设计控件布局,复杂的子控件使用 QWidget 暂时占位,最终效果如下图:
    • 注意⚠️:我这里 subtitle_browser 忘记恢复成 QWidget 了,后续字幕区域你的效果可能和我示例的图片不一致,没有影响,到第四节将会处理这部分。
  1. 保存 ui 文件,后使用命令将其转为 python 代码:
    • pyside6-uic verbiverse/UI/test.ui -o verbiverse/UI/test_ui.py
  1. 增加 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 标签业实现,其实与自定义的字幕控件流程差别不大,这里就不再细说,简单说下用到的控件:

  1. 使用到的 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 基础

How-to guides | 🦜️🔗 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)