欢迎光临
我们一直在努力

AI应用开发04-对话+RAG管理桌面端GUI

说明

对AI应用开发所涉及到的流程、工具、技能进行系列介绍,全部文章收录于《AI应用开发》专栏。关注+收藏,不错过后续精彩。

前置文章

AI应用开发01-环境准备 AI应用开发02-从零构建AI聊天机器人 AI应用开发03-RAG增强知识问答

项目源码

gitee仓库:AI应用开发源码

一、项目目标

构建一个基于 Python 的桌面端 AI 聊天应用,具备以下核心能力:

  • 调用大模型 API 进行多轮对话(上下文记忆)
  • 流式/非流式输出切换
  • 对话记录本地保存
  • 内置 RAG 知识库(文档上传、向量检索、增强回答)
  • 支持嵌入模型与文本切分策略配置
  • 简洁实用的 GUI 界面

二、技术选型

组件技术方案说明
编程语言 Python 3.9+ 生态丰富,AI 开发首选
GUI 框架 tkinter Python 自带,轻量无额外依赖
大模型 API DeepSeek (兼容 OpenAI) 国内可直接访问,性价比高
API 客户端 openai 通用性最强,未来可无缝切换模型
RAG 引擎 自研模块 rag_engine 封装文档加载、分割、向量库操作
向量数据库 ChromaDB 轻量、支持本地持久化
嵌入模型 sentence-transformers 支持多种预训练模型,可本地运行
文档解析 PyPDF2 处理 PDF,后续可扩展 docx 等
环境变量管理 python-dotenv 安全存储 API Key

三、功能需求

3.1 V1 —— 基础对话功能

  • ✅ 多轮对话,携带完整上下文
  • ✅ 可配置系统提示词(角色设定)
  • ✅ 流式 / 非流式输出切换
  • ✅ 每次会话自动保存为带时间戳的日志文件
  • ✅ 设置面板:API Key、模型名、保存目录等

3.2 V2 —— RAG 知识增强与知识库构建

  • ✅ 本地 PDF / TXT 文档上传
  • ✅ 文本分割(简单切分 & 语义切分可选)
  • ✅ 向量化入库(ChromaDB)
  • ✅ 知识库文件管理界面:查看文件名、分块数、删除文件
  • ✅ 对话时自动检索相关片段,并显示参考资料来源
  • ✅ 嵌入模型可切换(下拉框 + 手动输入)
  • ✅ 向量库路径、切分策略等可配置

四、项目结构

F:\\ai-dev\\
├── rag_engine/ # 独立 RAG 模块包
│ ├── __init__.py
│ ├── config.py # 默认配置
│ ├── loader.py # 文档加载器
│ ├── splitter.py # 文本分割器 (simple / semantic)
│ └── vector_store.py # ChromaDB 封装
├── chatbot_gui_modular.py # 主 GUI 程序
├── chatbot_gui.py # 主GUI 程序(简单的对话界面)
├── .env # API 密钥 (不纳入版本控制)
├── config.json # 用户配置 (自动生成)
├── chat_logs/ # 对话记录保存目录
└── chroma_db/ # 向量数据库存储目录

五、关键模块实现

5.1 环境准备与依赖安装

构建虚拟环境agent-demo并激活(如果已经按照前置文章创建,可忽略该步骤)。

python m venv agent-demo
.\\agent-demo\\Scripts\\Activate.ps1
python m pip install openai python-dotenv chromadb PyPDF2 sentence-transformers # 安装必要的依赖包,已安装过忽略

对于向量化模型,推荐配置 HuggingFace 国内镜像(在脚本开头添加),避免因网络问题无法启动:

import os
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'

5.2 RAG 引擎包 (rag_engine/)

构建一个可独立复用的轻量级 RAG 工具包,采用模块化设计将文档处理全流程拆分为四个职责单一的组件:

  • 配置中心(config) 管理默认参数与可用模型列表。
  • 文档加载器(loader) 统一处理 PDF/TXT 等多格式文件读取。
  • 文本分割器(splitter) 提供简单切分与语义切分两种策略以适配不同场景。
  • 向量库管理器(vector_store) 封装 ChromaDB 操作实现文档的向量化存储、检索与统计。 该包通过 __init__.py 暴露统一接口,对外屏蔽内部实现细节,既可直接被桌面 GUI 调用,也可无缝集成到其他 Python 项目中,体现了“高内聚、低耦合”的工程原则,为后续扩展更多文档格式、切分算法或嵌入模型奠定了坚实基础。
5.2.1 config.py – 默认配置

GUI启动时加载默认配置,用户可修改并保存。配置属性主要包含:

  • 向量库地址
  • 默认向量化模型
  • 分块设置
  • 可选择的向量化模型列表

DEFAULT_RAG_CONFIG = {
"persist_dir": "chroma_db",
"embedding_model": "shibing624/text2vec-base-chinese",
"chunk_size": 500,
"chunk_overlap": 50,
"split_strategy": "simple",
"available_models": [
"shibing624/text2vec-base-chinese",
"moka-ai/m3e-base",
"BAAI/bge-small-zh-v1.5",
"all-MiniLM-L6-v2",
"intfloat/multilingual-e5-large"
]
}

5.2.2 loader.py – 文档加载器

import os
from PyPDF2 import PdfReader

class DocumentLoader:
@staticmethod
def load(file_path):
ext = os.path.splitext(file_path)[1].lower()
if ext == '.pdf':
return DocumentLoader._load_pdf(file_path)
elif ext == '.txt':
return DocumentLoader._load_txt(file_path)
else:
raise ValueError(f"暂不支持的格式: {ext}")

@staticmethod
def _load_pdf(path):
reader = PdfReader(path)
full_text = ""
for page in reader.pages:
text = page.extract_text()
if text:
full_text += text + "\\n"
return full_text

@staticmethod
def _load_txt(path):
with open(path, 'r', encoding='utf-8') as f:
return f.read()

5.2.3 splitter.py – 文本分割器(支持 simple 和 semantic)

TextSplitter 是整个 RAG 引擎中的文本预处理核心,负责将原始长文本切分成长度适中、语义连贯的文本块(chunk),以便后续进行向量嵌入和检索。其设计遵循“策略可插拔、参数可调节”的原则,对外仅暴露 split(text) 方法,内部则根据初始化时指定的 strategy 参数自动分派到不同的切分逻辑。

一、整体实现思路
  • 参数化配置:构造时接收 chunk_size(目标块长度)、overlap(相邻块重叠长度)和 strategy(切分策略)三个关键参数,使切分行为灵活可控。
  • 策略路由:通过 split 方法统一入口,根据 self.strategy 的值判断调用 _simple_split 还是 _semantic_split,便于后续扩展新策略(例如基于 embedding 相似度的动态切分)。
  • 输出规范:无论采用哪种内部算法,最终均返回一个字符串列表,每个元素代表一个独立的文本块,供向量库直接使用。
  • 二、简单切分(Simple Split)

    原理:以自然段落边界(双换行)为第一优先级,段落内部若超出 chunk_size 则按固定窗口滑动截取,确保每个块长度大致均匀。

    实现步骤:

  • 段落提取:先将文本按 \\n\\n 拆分为段落,过滤掉长度不足 20 字符的空白段落;如果拆分后无有效段落,则降级为按单换行 \\n 拆分。
  • 逐段处理:遍历每个段落,若段落长度未超过 chunk_size 则直接作为一个块;否则以 chunk_size – overlap 为步长滑动窗口,逐段截取子字符串形成块。
  • 输出:将所有生成的块合并为列表返回。
  • 适用场景:通用性最强,适合绝大多数格式工整的文档(如报告、手册),计算开销低、速度快。

    三、语义切分(Semantic Split)

    原理:以句子为最小单位,通过贪婪合并策略将语义上连续的句子聚合成不超过 chunk_size 的块,避免在句子中间截断,从而保持每个块的语义完整性。

    实现步骤:

  • 分句:利用正则表达式 (?<=[。!?.!?])\\s* 按中英文标点将文本拆分为句子列表,并剔除空白句。
  • 贪婪合并:初始化一个空字符串 current_chunk,遍历句子列表:
    • 若将当前句子追加到 current_chunk 后总长度不超过 chunk_size,则执行追加;
    • 否则,先将已有的 current_chunk 作为一个完整块输出,然后处理当前句子:
      • 如果当前句子本身长度就超过 chunk_size(极长句),则对该句子按 _simple_split 的窗口滑动方式强制切分;
      • 若未超过,则将当前句子作为新 current_chunk 的起始。
  • 收尾:遍历结束后,若 current_chunk 非空,则作为最后一个块输出。
  • 输出:返回所有块组成的列表。
  • 适用场景:对话记录、法律条款、学术论文等对语义连续性要求较高的文本,能够显著提升检索结果的连贯性和答案准确性。

    代码实现:

    import re

    class TextSplitter:
    def __init__(self, chunk_size=500, overlap=50, strategy="simple"):
    self.chunk_size = chunk_size
    self.overlap = overlap
    self.strategy = strategy

    def split(self, text):
    if self.strategy == "semantic":
    return self._semantic_split(text)
    return self._simple_split(text)

    def _simple_split(self, text):
    paragraphs = [p.strip() for p in text.split("\\n\\n") if len(p.strip()) > 20]
    if not paragraphs:
    paragraphs = [p.strip() for p in text.split("\\n") if len(p.strip()) > 20]
    chunks = []
    for para in paragraphs:
    if len(para) <= self.chunk_size:
    chunks.append(para)
    else:
    for i in range(0, len(para), self.chunk_size self.overlap):
    chunk = para[i:i + self.chunk_size]
    if chunk:
    chunks.append(chunk)
    return chunks

    def _semantic_split(self, text):
    sentences = re.split(r'(?<=[。!?.!?])\\s*', text)
    sentences = [s.strip() for s in sentences if s.strip()]
    chunks = []
    current_chunk = ""
    for sent in sentences:
    if len(current_chunk) + len(sent) <= self.chunk_size:
    current_chunk += sent
    else:
    if current_chunk:
    chunks.append(current_chunk)
    if len(sent) > self.chunk_size:
    for i in range(0, len(sent), self.chunk_size self.overlap):
    chunks.append(sent[i:i + self.chunk_size])
    current_chunk = ""
    else:
    current_chunk = sent
    if current_chunk:
    chunks.append(current_chunk)
    return chunks

    5.2.4 vector_store.py – 向量库管理

    VectorStore 是 RAG 引擎的存储与检索核心,基于 ChromaDB 封装了文档向量的持久化管理和语义搜索能力,向上层提供简洁的操作接口。

    核心职责:

  • 初始化与连接:自动创建或加载指定路径下的 ChromaDB 持久化实例,并配置指定的嵌入模型(如 sentence-transformers 模型)作为向量化函数。
  • 文档入库:接收已切分的文本块列表和来源文件名,生成带元数据(来源、块序号)的 ID,支持对同一文件的旧数据先删后增,实现可重复上传的幂等操作。
  • 语义检索:将查询文本向量化后,在集合中执行相似度搜索,返回最相关的 top_k 个文本块,同时附带来源文件名和块序号等结构化信息,方便溯源。
  • 状态统计:通过读取集合中全部元数据,快速统计出每个源文件的分块数量,为知识库管理界面提供数据支撑。
  • 文件删除:支持按来源文件名批量删除其所有块,实现知识库的灵活清理。
  • 设计特点:

    • 持久化本地存储:所有向量数据均保存在指定目录,重启不丢失。
    • 模型可插拔:嵌入模型通过参数注入,更换模型只需修改初始化参数,无需改动内部逻辑。
    • 异常隔离:通过统一的接口屏蔽底层 ChromaDB 细节,外部调用者无需关心数据库操作,降低了耦合度。

    代码实现:

    import chromadb
    from chromadb.utils import embedding_functions
    from .config import DEFAULT_RAG_CONFIG

    class VectorStore:
    def __init__(self, persist_dir=None, embedding_model=None):
    self.persist_dir = persist_dir or DEFAULT_RAG_CONFIG["persist_dir"]
    self.embedding_model = embedding_model or DEFAULT_RAG_CONFIG["embedding_model"]
    self.embed_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name=self.embedding_model
    )
    self.client = chromadb.PersistentClient(path=self.persist_dir)
    self.collection = self.client.get_or_create_collection(
    name="rag_collection",
    embedding_function=self.embed_fn
    )

    def add_document(self, chunks, source_name):
    metadatas = [{"source": source_name, "chunk_index": i} for i in range(len(chunks))]
    ids = [f"{source_name}_{i}" for i in range(len(chunks))]
    existing = self.collection.get(where={"source": source_name})
    if existing["ids"]:
    self.collection.delete(ids=existing["ids"])
    self.collection.add(documents=chunks, metadatas=metadatas, ids=ids)

    def search(self, query, top_k=3):
    results = self.collection.query(
    query_texts=[query],
    n_results=top_k,
    include=['documents', 'metadatas']
    )
    if results["documents"] and results["metadatas"]:
    docs = results["documents"][0]
    metas = results["metadatas"][0]
    return [
    {"content": doc, "source": meta.get("source", ""), "chunk_index": meta.get("chunk_index", 0)}
    for doc, meta in zip(docs, metas)
    ]
    return []

    def get_file_stats(self):
    all_data = self.collection.get()
    stats = {}
    if all_data["metadatas"]:
    for meta in all_data["metadatas"]:
    src = meta["source"]
    stats[src] = stats.get(src, 0) + 1
    return stats

    def remove_file(self, source_name):
    self.collection.delete(where={"source": source_name})

    5.2.5 __init__.py – 包导出

    对外暴露可引用的模块

    from .loader import DocumentLoader
    from .splitter import TextSplitter
    from .vector_store import VectorStore
    from .config import DEFAULT_RAG_CONFIG

    __all__ = ["DocumentLoader", "TextSplitter", "VectorStore", "DEFAULT_RAG_CONFIG"]

    5.3 主 GUI 程序 chatbot_gui_modular.py

    完整代码因篇幅原因在此不再展示,可通过gitee仓库获取。核心特点:

    • 使用 PanedWindow 实现左右分栏:聊天区 + RAG 面板
    • 菜单栏提供“设置”入口,支持动态修改模型、切分策略等
    • 发送消息时自动根据 RAG 开关检索上下文,并将参考资料显示在对话框
    • 所有耗时操作(上传、API 调用)放入后台线程,避免 UI 冻结

    gitee仓库:AI应用开发源码

    六、效果演示

    6.1 简单对话示例

    启动窗口: 简单版对话框 设置界面,可修改保存,即时生效。 全局设置

    6.2 RAG+对话示例

    在这里插入图片描述 在这里插入图片描述

    七、设计权衡与优化方向

    关键设计权衡(TOP3)

    1. 轻量GUI vs 现代体验

    • 权衡:选择Python自带的tkinter,牺牲界面美观度,换取零额外依赖、极低上手成本。
    • 指导:初期学习重心应在AI逻辑而非前端。后续可无缝迁移至customtkinter或全栈Web框架,核心业务代码不受影响。

    2. 通用API接口 vs 专用SDK

    • 权衡:用openai库调用DeepSeek,而不用其官方SDK,保持与OpenAI协议兼容。
    • 指导:实现模型无关性,未来切换任何兼容OpenAI的模型只需改base_url和key,降低锁定风险,便于学习和对比不同模型。

    3. 简单切分 vs 语义切分

    • 权衡:默认采用快速、通用的简单切分,同时提供基于正则分句的语义切分作为选项,不引入沉重模型依赖。
    • 指导:理解“够用即可”的工程思维;当需要更高检索质量时,再升级为基于embedding相似度的动态切分,避免过早优化。

    #关键优化方向(TOP3)

    1. 混合检索(向量 + 关键词)

    • 问题:纯向量检索可能遗漏精确关键词匹配。
    • 行动:引入BM25等稀疏检索,与向量检索结果融合(混合召回),显著提升RAG回答的准确率和覆盖面。

    2. Agent化与工具调用(MCP集成)

    • 问题:当前仅是被动问答,无法主动执行操作。
    • 行动:接入MCP(Model Context Protocol),让模型能调用搜索、计算器、文件操作等外部工具,从“聊天助手”升级为“自主代理”。

    3. 产品化部署与多模态扩展

    • 问题:桌面应用局限于单机,无法协作。
    • 行动:将后端改造为FastAPI,前端用现代Web技术,实现多用户、云端部署;同时拓展图片、语音等多模态交互能力。
    赞(0)
    未经允许不得转载:171主机测评 » AI应用开发04-对话+RAG管理桌面端GUI
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址