市场存在特别多关于大模型本地部署的教程、视频、案例卖课的同样存在很多。各种工具花样百出。

在ollma的基础上,我们使用python创建窗口用来调用,可以进行切换模型,如果由新下载的模型直接刷新就好。

# local_llm_gui_stream.py
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import openai
import threading
import queue
import json
from datetime import datetime
import platform


class LLMGUI:
    def __init__(self, root):
        self.root = root
        self.setup_system_font()
        self.setup_ui_config()
        root.title("一个黑客创业者 - 本地大模型交互工具 v2.2")
        root.geometry("1200x800")
        root.configure(bg=self.background_color)

        # 初始化配置
        self.base_url = "http://localhost:11434/v1"
        self.current_model = "qwen2:7b"
        self.history = []
        self.response_queue = queue.Queue()

        # 创建界面组件
        self.create_widgets()
        self.setup_styles()

        # 初始化客户端
        self.client = openai.OpenAI(
            base_url=self.base_url,
            api_key="sk-no-key-required"
        )

        # 启动队列处理器
        self.process_queue()
        self.load_models()

    def setup_system_font(self):
        """设置跨平台系统字体"""
        system = platform.system()
        if system == 'Windows':
            self.font_family = '微软雅黑'
        elif system == 'Darwin':
            self.font_family = 'PingFang SC'
        else:
            self.font_family = 'Noto Sans'

        self.base_font = (self.font_family, 11)
        self.title_font = (self.font_family, 12, 'bold')
        self.status_font = (self.font_family, 9)

    def setup_ui_config(self):
        """初始化UI全局配置"""
        self.primary_color = '#2C3E50'
        self.secondary_color = '#3498DB'
        self.background_color = '#F5F5F5'
        self.text_color = '#333333'
        self.history_bg = '#FFFFFF'

    def setup_styles(self):
        """配置UI样式"""
        style = ttk.Style()
        style.theme_use('clam')

        # 通用样式
        style.configure('.',
                        background=self.background_color,
                        font=self.base_font)

        # 按钮样式
        style.configure('TButton',
                        borderwidth=0,
                        relief='flat',
                        background='#ECF0F1',
                        foreground=self.text_color)
        style.map('TButton',
                  background=[('active', self.secondary_color), ('!active', '#ECF0F1')],
                  foreground=[('active', 'white'), ('!active', self.text_color)])

        # 标签框样式
        style.configure('TLabelframe',
                        borderwidth=0,
                        background=self.background_color)
        style.configure('TLabelframe.Label',
                        background=self.background_color,
                        foreground=self.primary_color,
                        font=self.title_font)

        # 状态栏样式
        style.configure('Status.TLabel',
                        background='#E0E0E0',
                        foreground=self.text_color,
                        font=self.status_font,
                        padding=5)

    def create_widgets(self):
        """创建界面组件"""
        # 主框架
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)

        # 控制面板
        control_frame = ttk.LabelFrame(main_frame, text="模型设置")
        control_frame.pack(fill=tk.X, pady=10, ipadx=10, ipady=5)

        # 模型选择
        model_row = ttk.Frame(control_frame)
        model_row.pack(fill=tk.X, pady=5)
        ttk.Label(model_row, text="当前模型:").pack(side=tk.LEFT, padx=5)
        self.model_combobox = ttk.Combobox(model_row, width=35)
        self.model_combobox.pack(side=tk.LEFT, padx=5)
        ttk.Button(model_row, text="刷新列表", command=self.load_models).pack(side=tk.LEFT)

        # 参数设置
        param_row = ttk.Frame(control_frame)
        param_row.pack(fill=tk.X, pady=5)
        ttk.Label(param_row, text="Temperature:").pack(side=tk.LEFT, padx=5)
        self.temp_slider = ttk.Scale(param_row, from_=0.0, to=1.0, value=0.7)
        self.temp_slider.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
        self.temp_value = ttk.Label(param_row, text="0.7")
        self.temp_value.pack(side=tk.LEFT, padx=5)
        self.temp_slider.configure(command=lambda v: self.temp_value.config(text=f"{float(v):.1f}"))

        # 对话区域
        dialog_frame = ttk.LabelFrame(main_frame, text="对话")
        dialog_frame.pack(fill=tk.BOTH, expand=True, pady=10)

        # 历史记录
        self.history_text = scrolledtext.ScrolledText(
            dialog_frame,
            wrap=tk.WORD,
            font=self.base_font,
            padx=15,
            pady=15,
            bg=self.history_bg,
            borderwidth=1,
            relief='solid'
        )
        self.history_text.pack(fill=tk.BOTH, expand=True)
        self.history_text.tag_config('user', foreground='#2980B9', spacing2=5)
        self.history_text.tag_config('assistant', foreground='#27AE60', spacing2=5)
        self.history_text.configure(state='disabled')

        # 输入区域
        input_frame = ttk.Frame(dialog_frame)
        input_frame.pack(fill=tk.X, pady=10)

        self.input_text = scrolledtext.ScrolledText(
            input_frame,
            height=3,
            wrap=tk.WORD,
            font=self.base_font,
            padx=10,
            pady=10,
            bg='white',
            borderwidth=1,
            relief='solid'
        )
        self.input_text.pack(fill=tk.X, expand=True)
        self.input_text.bind("<Control-Return>", lambda e: self.start_query())

        # 按钮面板
        btn_frame = ttk.Frame(input_frame)
        btn_frame.pack(pady=5)

        self.send_btn = ttk.Button(btn_frame, text="发送 (Ctrl+Enter)", command=self.start_query)
        self.send_btn.pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="清空历史", command=self.clear_history).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="保存记录", command=self.save_chat_dialog).pack(side=tk.LEFT, padx=5)

        # 状态栏
        self.status = ttk.Label(main_frame, text="就绪", style='Status.TLabel')
        self.status.pack(fill=tk.X)

    def save_chat_dialog(self):
        """保存聊天记录对话框"""
        top = tk.Toplevel(self.root)
        top.title("保存选项")
        top.geometry("300x150")
        top.configure(bg=self.background_color)

        ttk.Label(top, text="选择保存格式:").pack(pady=10)
        btn_frame = ttk.Frame(top)
        btn_frame.pack(pady=5)

        ttk.Button(btn_frame, text="文本文件 (.txt)",
                   command=lambda: [self.save_chat('txt'), top.destroy()]).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="JSON文件 (.json)",
                   command=lambda: [self.save_chat('json'), top.destroy()]).pack(side=tk.LEFT, padx=5)

    def save_chat(self, format='txt'):
        """保存聊天记录到文件"""
        file_path = filedialog.asksaveasfilename(
            defaultextension=f".{format}",
            filetypes=[(f"{format.upper()}文件", f"*.{format}")],
            title="保存聊天记录"
        )
        if not file_path:
            return

        try:
            if format == 'txt':
                self._save_as_txt(file_path)
            elif format == 'json':
                self._save_as_json(file_path)
            self.update_status(f"聊天记录已保存至: {file_path}")
        except Exception as e:
            messagebox.showerror("保存错误", f"保存失败: {str(e)}")

    def _save_as_txt(self, path):
        """保存为文本格式"""
        with open(path, 'w', encoding='utf-8') as f:
            f.write(f"=== 对话记录 ({datetime.now().strftime('%Y-%m-%d %H:%M')}) ===\n\n")
            for msg in self.history:
                role = msg['role'].capitalize()
                content = msg['content'].strip()
                f.write(f"[{role}]\n{content}\n\n")

    def _save_as_json(self, path):
        """保存为JSON格式"""
        with open(path, 'w', encoding='utf-8') as f:
            json.dump({
                "metadata": {
                    "save_time": datetime.now().isoformat(),
                    "model": self.current_model,
                    "temperature": float(self.temp_slider.get())
                },
                "history": self.history
            }, f, ensure_ascii=False, indent=2)

    def load_models(self):
        """加载可用模型列表"""
        try:
            models = [m.id for m in self.client.models.list().data]
            self.model_combobox['values'] = models
            if models:
                self.current_model = models[0]
                self.model_combobox.set(models[0])
            self.update_status(f"已加载 {len(models)} 个模型")
        except Exception as e:
            messagebox.showerror("错误", f"无法获取模型列表: {str(e)}")
            self.model_combobox['values'] = ['qwen2:7b', 'llama3.1']

    def process_queue(self):
        """处理响应队列"""
        try:
            while True:
                msg = self.response_queue.get_nowait()
                if msg == 'EOS':
                    self.finalize_response()
                elif isinstance(msg, dict):
                    self.append_stream(msg['content'])
                elif isinstance(msg, Exception):
                    self.show_error(str(msg))
        except queue.Empty:
            pass
        finally:
            self.root.after(100, self.process_queue)

    def start_query(self):
        """启动查询线程"""
        prompt = self.input_text.get("1.0", tk.END).strip()
        if not prompt:
            return

        self.send_btn.configure(state='disabled')
        self.input_text.delete("1.0", tk.END)
        self.update_status("生成中...")
        self.append_history("user", prompt)

        thread = threading.Thread(target=self.stream_query, args=(prompt,))
        thread.daemon = True
        thread.start()

    def stream_query(self, prompt):
        """流式查询处理"""
        try:
            messages = self.history + [{"role": "user", "content": prompt}]

            response = self.client.chat.completions.create(
                model=self.current_model,
                messages=messages,
                temperature=float(self.temp_slider.get()),
                max_tokens=2048,
                stream=True
            )

            full_response = ""
            for chunk in response:
                content = chunk.choices[0].delta.content or ""
                full_response += content
                self.response_queue.put({'content': content})

            self.history.append({"role": "assistant", "content": full_response})
            self.response_queue.put('EOS')
        except Exception as e:
            self.response_queue.put(e)

    def append_stream(self, content):
        """追加流式内容"""
        self.history_text.configure(state='normal')
        self.history_text.insert(tk.END, content, 'assistant')
        self.history_text.see(tk.END)
        self.history_text.configure(state='disabled')

    def append_history(self, role, content):
        """添加历史记录"""
        self.history_text.configure(state='normal')
        self.history_text.insert(tk.END, f"\n[{role.capitalize()}] ", role)
        self.history_text.insert(tk.END, f"{content}\n")
        self.history_text.see(tk.END)
        self.history_text.configure(state='disabled')

    def finalize_response(self):
        """完成响应处理"""
        self.send_btn.configure(state='normal')
        self.update_status("就绪")

    def show_error(self, message):
        """显示错误信息"""
        self.history_text.configure(state='normal')
        self.history_text.insert(tk.END, f"\n[系统] 错误: {message}\n", 'error')
        self.history_text.see(tk.END)
        self.history_text.configure(state='disabled')
        self.send_btn.configure(state='normal')
        self.update_status("错误发生")

    def clear_history(self):
        """清空历史记录"""
        self.history = []
        self.history_text.configure(state='normal')
        self.history_text.delete("1.0", tk.END)
        self.history_text.configure(state='disabled')
        self.update_status("历史已清空")

    def update_status(self, message):
        """更新状态栏"""
        self.status.config(text=f"状态: {message}")


if __name__ == "__main__":
    root = tk.Tk()
    app = LLMGUI(root)
    root.mainloop()

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐