使用PySide6+Ollama开发一款本地AI题库复习软件
使用PySide6+Ollama开发一款本地AI题库复习软件
目录
一、引言
在备考过程中,刷题是必不可少的环节。然而,市面上现有的刷题软件往往存在一些痛点:要么依赖网络服务器,断网无法使用;要么充斥着各种广告和VIP收费陷阱;最重要的是,当我们做错题时,往往只能看到一个冷冰冰的标准答案,缺乏针对性的解析过程。
为了解决这些问题,我决定自己动手开发一款完全单机运行、无网络依赖、集成本地AI大模型解析的题库复习软件。本软件基于Python的PySide6框架构建GUI,通过内嵌WebEngine渲染答题界面,结合Ollama本地部署的大语言模型(如DeepSeek),实现了“导入题库->做题->错题统计->AI深度解析”的完整闭环。
本文将详细拆解该软件的核心技术点,包括PySide6与JS的交互、多线程防止UI卡顿、Ollama流式输出处理以及巧妙的错题统计算法,希望能为你的桌面应用开发提供灵感。
软件下载地址:https://download.csdn.net/download/qq616491978/92814944
使用PySide6+Ollama开发一款本地AI题库复习软件
二、软件功能展示与技术架构
在深入代码之前,我们先来看看这款软件的核心功能模块以及整体的系统架构设计。软件主要包含四大核心功能:
- 题库管理:支持直接导入Excel格式的题库,自动转化为JSON并在本地持久化存储,支持历史题库加载。
- 答题与记录:左侧Web页面渲染题目,支持单选、多选、判断等题型,每次提交自动生成带时间戳的答题记录快照。
- 错题智能分析:通过对比历次答题记录,利用算法精准定位“高频易错题”,并支持一键进入错题模式复习。
- 本地AI解析:调用本地Ollama模型,支持“思考模式”和“详细分析”切换,流式输出题目的解题思路。
为了平衡“复杂的答题交互”与“原生GUI的控制力”,系统采用了混合架构:
| 层级 | 技术栈 | 职责描述 |
|---|---|---|
| 展示层 (左) | HTML/JS + QWebEngineView | 负责题库渲染、答题交互、错题高亮等复杂前端UI |
| 展示层 (右) | PySide6 原生组件 | 负责AI解析结果展示、模型配置、状态控制 |
| 控制层 | QToolBar, QMenu, QAction | 负责软件全局操作路由、菜单响应 |
| 桥接层 | QWebChannel (Bridge) | 负责Python后端与JS前端的双向通信 |
| 业务逻辑层 | Python (Numpy, Base64) | 负责题库编解码、错题交集计算、记录持久化 |
| AI计算层 | QThread + Ollama API | 负责大模型推理,完全异步流式处理,不卡顿主线程 |

三、PySide6与WebEngine的混搭:前端与原生的完美融合
在桌面开发中,如果纯粹用Python代码画选择题、排版文本,工作量巨大且极其僵硬。因此,我选择用HTML/JS来处理答题页面,而用PySide6原生组件来做外框和控制面板。这就带来了一个核心问题:Python如何与网页里的JS代码通信?
PySide6提供了QWebChannel来解决这个问题。我们在Python端定义一个继承自QObject的桥接类,并将其注册到WebChannel中。
技术要点:
- 桥接类中的方法必须使用
@Slot(str)装饰器修饰,以暴露给JS调用。 - 网页加载完成后,通过
runJavaScript主动将数据推送给JS初始化。 - JS触发点击事件时,回调Python方法,Python再通过
runJavaScript调用JS的submit()函数获取数据,形成闭环。
核心代码:
from PySide6.QtCore import QObject, Slot
from PySide6.QtWebChannel import QWebChannel
class Bridge(QObject):
def __init__(self, m_win):
super().__init__()
self.m_win = m_win
# JS端触发的点击事件会进入这里
@Slot(str)
def handle_click(self, message):
self.m_win._save_from_js()
class MainWindow(QMainWindow):
def _init_web_channel(self):
self.channel = QWebChannel()
self.bridge = Bridge(self)
# 将bridge对象注册到前端,前端可通过 window.bridge 调用
self.channel.registerObject('bridge', self.bridge)
self.webEngineView.page().setWebChannel(self.channel)
def _delayed_init(self):
# Python主动调用JS函数,传递题库数据初始化页面
js_code = f"init({json.dumps(self.environment['cur_question_bank_id'])}, {json.dumps(self.ti_ku_nr)});"
self.webEngineView.page().runJavaScript(js_code, 0, js_callback)
原始数据:

使用PySide6本地web组件进行数据的展示与交互:

四、本地大模型接入:Ollama流式输出与多线程处理
这是本软件最核心的亮点。如果直接在主线程调用Ollama API,由于大模型推理耗时较长(几秒到几十秒),软件界面会直接“白屏”卡死。因此,必须引入QThread进行异步处理。
此外,为了提升用户体验,我接入了Ollama的stream=True流式输出,并适配了DeepSeek等模型的“思考模式”(即先输出思考过程,再输出正式答案)。
技术要点:
- 继承
QThread定义子线程:通过自定义信号update_signal将生成的文本片段实时发给主线程。 - 使用Python的
ollama库:开启think=True参数获取隐藏的思考链。 - 折叠AI思考过程:在流式遍历中,通过状态机判断当前输出的是
thinking还是content,动态拼接HTML的<details>标签,实现“点击展开思考过程”的折叠UI效果。 - 避免兼容性问题:完全限制使用CPU(设置环境变量),避免因显卡驱动问题导致程序崩溃。
核心代码:
import os
# 必须在导入PySide6前设置,强制禁用GPU,提升兼容性
os.environ['QTWEBENGINE_DISABLE_GPU'] = '1'
os.environ['OLLAMA_NUM_GPU'] = '0'
class OllamaDeepSeek():
def send_messages(self, chat_str):
# 根据是否开启思考模式,动态调整上下文长度和Token限制
if self.enable_think:
options = {'num_gpu': 0, 'num_ctx': 4096}
else:
options = {'num_gpu': 0, 'num_ctx': 1024, 'num_predict': answer_tokens}
try:
stream = chat(model=self.model_name, messages=messages, options=options, stream=True, think=self.enable_think)
is_thinking = False
for chunk in stream:
thinking = chunk.get('message', {}).get('thinking', '')
content = chunk.get('message', {}).get('content', '')
if thinking:
if not is_thinking:
self.update_signal.emit("<details><summary>AI 正在思考...</summary>\n\n")
is_thinking = True
self.update_signal.emit(thinking)
if content:
if is_thinking:
self.update_signal.emit("\n\n</details>\n\n---\n\n")
is_thinking = False
self.update_signal.emit(content)
self.finish_signal.emit(full_response)
except Exception as e:
self.update_signal.emit(f"发生错误: {e}")
class OllamaDeepSeekThread(QThread):
update_signal = Signal(str)
finish_signal = Signal(str)
def run(self):
self.olmds.send_messages(self.question_text)

五、题库导入与错题分析:Numpy在业务逻辑中的巧妙运用
传统的错题本只是简单地记录做错的题目,但本题库软件实现了跨次错题频率统计。用户可能做了5套卷子,某道题第1、3、5次都做错了,这道题就是“高频易错题”。
题库导入方面,为了防止乱码和兼容性问题的复杂处理,我采用了一个巧妙的设计:将Excel读取为JSON字符串后,直接进行Base64编码存储为.js文件。前端拿到Base64解码后即可直接作为对象使用。
技术要点:
- 错题判定逻辑:
fen_shu数组记录得分(0表示错),if_answer数组记录是否作答(1表示作了)。 - 跨文件错题统计:读取某题库下的所有历史答题记录JS文件,解析出错题索引数组。
- 引入
Numpy进行数组交集与频次统计:将复杂的循环比对简化为一行代码。
核心代码:
import numpy as np
from collections import Counter
import base64
def import_question_bank(self):
excel_data = excel2json.read_excel_to_json(file_name)
# 核心技巧:将整个JSON字符串进行Base64编码再存为js文件
encoded_data = base64.b64encode(excel_data.encode("utf-8")).decode("utf-8")
with open(json_save_path, 'w', encoding='utf-8') as f:
f.write(encoded_data)
def icr_analyze(self):
file_contents = []
# 遍历读取所有历史记录文件
for item in os.listdir(folder_path):
with open(full_path, 'r', encoding='utf-8') as f:
file_contents.append(f.read())
icr_dicts = []
for content in file_contents:
data = json.loads(content)
# Numpy数组运算:找出 (得分为0) 且 (已作答为1) 的题目索引
arr1, arr2 = np.array(data["fen_shu"]), np.array(data["if_answer"])
error_indices = np.where((arr1 == 0) & (arr2 == 1))[0].tolist()
icr_dicts.append(error_indices)
# 统计所有记录中,各个错题索引出现的次数
counter = Counter()
for arr in icr_dicts:
counter.update(arr)
dpc_counters = counter.most_common() # 按错误频率降序排列


六、现代化UI定制与单文件打包部署
一个优秀的桌面软件,颜值同样重要。PySide6默认的控件样式比较复古,但通过QSS(类似前端的CSS)可以实现高度自定义的现代化扁平化UI。此外,为了方便非技术用户使用,软件需要打包成独立的.exe文件。
技术要点:
- 全局QSS注入:在
QApplication创建后,直接setStyleSheet应用全局风格,统一控件的圆角、悬停变色、字体颜色。 - 动态图片绘制:对于复选框的勾选图标,不依赖外部图片,而是使用
QPainter在内存中动态绘制一个矢量对勾,转为Base64嵌入到QSS的image: url()中,做到了真正的“零外部图片依赖”。 - PyInstaller路径兼容:打包成单文件后,资源文件会被解压到系统的临时目录(
_MEIPASS)。通过编写resource_path()函数,实现开发环境与打包环境路径的自动适配。
核心代码:
def resource_path(relative_path):
"""兼容PyInstaller单文件打包的路径获取函数"""
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
# 使用QPainter动态生成对勾图标的Base64编码
img = QImage(16, 16, QImage.Format.Format_ARGB32)
img.fill(QColor(0, 0, 0, 0))
painter = QPainter(img)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
pen = QPen(QColor(255, 255, 255), 2.5, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin)
painter.setPen(pen)
painter.drawLine(3, 8, 7, 13)
painter.drawLine(7, 13, 13, 4)
painter.end()
buffer = QBuffer()
buffer.open(QBuffer.OpenModeFlag.ReadWrite)
img.save(buffer, "PNG")
check_img_url = f"data:image/png;base64,{base64.b64encode(buffer.data()).decode('utf-8')}"
# 将生成的图标直接应用到QSS中
settings_widget.setStyleSheet(f"""
QCheckBox::indicator:checked {{
background-color: #1890ff;
border-color: #1890ff;
image: url({check_img_url});
}}
""")
七、总结
这款题库复习软件从实际备考需求出发,巧妙地将PySide6原生控件与Web前端技术结合起来,既保证了复杂交互的灵活性,又拥有了桌面软件的原生体验。通过引入Ollama本地大模型,不仅彻底摆脱了网络限制,更通过流式输出和思考链处理,让AI解析变得可控且直观。
在开发过程中,我也总结出了一些宝贵的经验:
- 桌面应用的UI容错:强制禁用WebEngine的GPU加速(
QTWEBENGINE_DISABLE_GPU=1)能解决90%以上在不同Windows机器上的白屏和崩溃问题。 - 数据格式选型:对于结构不固定、包含复杂文本(如题干中的公式、图片)的数据,直接使用Base64编码整个JSON字符串,虽然稍占空间,但彻底杜绝了转义符导致的解析错误,极大提升了系统的鲁棒性。
软件下载地址:https://download.csdn.net/download/qq616491978/92814944
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)