本部分作为博客使用Python结合大模型创建自己的人工智能助手(喵娘)-CSDN博客https://blog.csdn.net/Jay_Franklin/article/details/150589387?spm=1011.2124.3001.6209的补充内容。

本文将开发一个简易桌面应用程序,通过调用通义千问等大语言模型的API接口,实现基础聊天机器人功能。

功能简述

应用界面如图所示。

在设置面板中,你可以:

  1. 自定义密钥保存路径
  2. 更换个人喜欢的角色形象和背景图片
  3. 调整对话历史长度,防止超过上下文限制

点击保存后应用会保存你的个性化设置。

配置好后,就可以在输入框中聊天啦!

代码实现

首先需要在工作文件夹下建立一个子文件modules。在子文件目录下建立一个__init__.py空文件,以及alice_new.py。

# 爱丽丝,1.0.该版本具有记忆功能。
import os
import logging
from openai import OpenAI, OpenAIError
from requests.exceptions import RequestException
import json

class Alice:
    """
    Alice 智能助手类,支持与 DashScope 兼容的 API 进行对话。
    """

    def __init__(self,
                 key_path: str,
                 model: str = 'qwen-plus',
                 sys_content: str = "You are a helpful assistant.",
                 max_history: int = 10):
        """
        初始化 Alice 实例。

        :param key_path: API 密钥文件路径
        :param model: 使用的模型名称(默认 qwen-plus)
        :param sys_content: 系统消息内容(默认为助手角色)
        :param max_history: 最大保留对话历史长度(默认 10 轮对话)
        """
        self.key_path = key_path
        self.model = model
        self.sys_content = sys_content
        self.max_history = max_history
        self.messages = [
            {"role": "system", "content": sys_content}
        ]
        self._setup_logging()

    def _setup_logging(self):
        """配置日志记录"""
        logging.basicConfig(
            level=logging.INFO,
            format="%(asctime)s - %(levelname)s - %(message)s",
            filename="alice_debug.log"
        )

    def get_key(self) -> str:
        """
        从文件中读取 API 密钥

        :return: API 密钥字符串
        :raises: FileNotFoundError 如果密钥文件不存在
        """
        if not os.path.exists(self.key_path):
            raise FileNotFoundError(f"API key file not found: {self.key_path}")
        with open(self.key_path, 'r', encoding='utf-8') as file:
            key = file.read().strip()
        return key

    def build_client(self) -> OpenAI:
        """
        构建 OpenAI 兼容客户端

        :return: OpenAI 客户端实例
        """
        return OpenAI(
            api_key=self.get_key(),
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",  # 官方推荐地址
        )

    def _truncate_history(self):
        """
        截断消息历史,保留最多 max_history 轮对话(含系统消息)
        """
        if len(self.messages) > self.max_history + 1:
            self.messages = self.messages[:1] + self.messages[-self.max_history:]

    def response(self, content: str):
        """
        发送请求并获取响应

        :param content: 用户输入内容
        :return: API 响应对象
        :raises: RuntimeError 请求失败
        """
        try:
            client = self.build_client()
            completion = client.chat.completions.create(
                model=self.model,
                messages=self.messages,
            )
            return completion
        except OpenAIError as e:
            logging.error(f"OpenAI API 错误: {e}")
            raise RuntimeError(f"API 请求失败: {str(e)}")
        except RequestException as e:
            logging.error(f"网络请求错误: {e}")
            raise RuntimeError("网络连接失败,请检查网络设置。")
        except Exception as e:
            logging.exception("未知错误发生")
            raise RuntimeError(f"未知错误: {str(e)}")

    def chatbox(self, content: str) -> str:
        """
        处理用户输入并返回结果

        :param content: 用户输入内容
        :return: AI 生成的回复内容
        """
        self.messages.append({"role": "user", "content": content})
        self._truncate_history()  # 添加用户消息后截断

        try:
            completion = self.response(content)
            if completion.choices:
                assistant_output = completion.choices[0].message.content
                self.messages.append({"role": "assistant", "content": assistant_output})
                self._truncate_history()  # 添加助手回复后再次截断
                return assistant_output
            else:
                return "API 返回无结果。"
        except FileNotFoundError as e:
            return f"密钥文件未找到:{str(e)}"
        except RuntimeError as e:
            return f"请求失败:{str(e)}"
        except Exception as e:
            return f"未知错误:{str(e)}"

    def clear_history(self):
        """
        清空对话历史(仅保留系统消息)
        """
        self.messages = [self. Messages[0]]  # 保留系统消息

接下来在工作目录中创建主文件main.py。

# 请将此文件保存为 main_app.py (与 modules 文件夹同级)

import tkinter as tk
from tkinter import scrolledtext, messagebox, filedialog
from PIL import Image, ImageTk
import threading
import os
import logging
import json
from pathlib import Path
from modules.alice_new import Alice # 确保 alice_new.py 在 modules 文件夹中

class AliceChatApp:
    def __init__(self, master):
        self.master = master
        self.width = 400
        self.height = 800
        
        # 设置窗口居中
        screen_width = master.winfo_screenwidth()
        screen_height = master.winfo_screenheight()
        x = (screen_width - self.width) // 2
        y = (screen_height - self.height) // 2
        master.geometry(f"{self.width}x{self.height}+{x}+{y}")
        master.title("Alice-Jay's Assistant")
        master.resizable(False, False)
        master.configure(bg="#fff0f5")
        
        try:
            master.iconbitmap("alice_icon.ico")
        except:
            pass

        self.config_file = "alice_config.json"
        self.load_config()
        
        try:
            self.alice = Alice(
                key_path=self.config.get("key_path", ""),
                model= 'qwen-turbo',
                sys_content=self.config.get("sys_content", "你需要扮演一个可爱的喵娘,和你对话的是你的主人哦!"),
                max_history=self.config.get("max_history", 100)
            )
        except Exception as e:
            messagebox.showerror("初始化错误", f"无法初始化Alice: {str(e)}")
            self.alice = None

        self.create_widgets()
        self.load_image()
        self.master.protocol("WM_DELETE_WINDOW", self.on_closing)

    def load_config(self):
        """加载配置文件"""
        try:
            if os.path.exists(self.config_file):
                with open(self.config_file, 'r', encoding='utf-8') as f:
                    self.config = json.load(f)
            else:
                self.config = {
                    "key_path": "",
                    "sys_content": "你需要扮演一个可爱的喵娘,和你对话的是你的主人哦!",
                    "max_history": 100,
                    "image_path": "alice_image.jpg",
                    "theme": "pink"
                }
                self.save_config()
        except Exception as e:
            logging.error(f"加载配置失败: {e}")
            self.config = {
                "key_path": "", "sys_content": "你需要扮演一个可爱的喵娘,和你对话的是你的主人哦!",
                "max_history": 100, "image_path": "alice_image.jpg", "theme": "pink"
            }

    def save_config(self):
        """保存配置文件"""
        try:
            with open(self.config_file, 'w', encoding='utf-8') as f:
                json.dump(self.config, f, ensure_ascii=False, indent=2)
        except Exception as e:
            logging.error(f"保存配置失败: {e}")

    def create_widgets(self):
        # === 修正后的布局逻辑:统一使用 pack ===

        # 1. 顶部组件
        self.title_frame = tk.Frame(self.master, bg="#ffe6f0", height=60)
        self.title_frame.pack(fill=tk.X, side=tk.TOP)
        self.title_frame.pack_propagate(False)
        
        title_label = tk.Label(self.title_frame, text="🐾 Alice 的聊天室", font=("微软雅黑", 16, "bold"), bg="#ffe6f0")
        title_label.pack(pady=10)

        self.settings_btn = tk.Button(self.title_frame, text="⚙️", command=self.open_settings, font=("Arial", 12), bg="#ffe6f0", relief="flat")
        self.settings_btn.place(relx=0.95, rely=0.5, anchor="center")

        self.image_frame = tk.Frame(self.master, bg="#fff0f5", height=250)
        self.image_frame.pack(fill=tk.X, side=tk.TOP)
        self.image_frame.pack_propagate(False)
        
        self.image_label = tk.Label(self.image_frame, bg="#fff0f5")
        self.image_label.pack(expand=True)

        # 2. 底部组件 (代码上先 pack,使其固定在最底部)
        self.status_frame = tk.Frame(self.master, bg="#ffe6f0", height=25)
        self.status_frame.pack(fill=tk.X, side=tk.BOTTOM)
        
        self.status_label = tk.Label(self.status_frame, text="就绪", font=("微软雅黑", 9), bg="#ffe6f0", fg="#666666")
        self.status_label.pack(fill=tk.BOTH, expand=True)

        self.input_frame = tk.Frame(self.master, bg="#fff0f5", height=50)
        self.input_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=5)
        self.input_frame.pack_propagate(False)
        
        self.input_entry = tk.Entry(self.input_frame, font=("微软雅黑", 12), relief="flat", bd=1, highlightthickness=1, highlightbackground="#ffb6c1")
        self.input_entry.place(relx=0.02, rely=0.5, relwidth=0.78, relheight=0.6, anchor="w")
        self.input_entry.bind("<Return>", self.send_message)
        self.input_entry.bind("<KeyRelease-Up>", self.load_previous_message)
        self.input_entry.focus_set()

        self.send_btn = tk.Button(self.input_frame, text="发送", command=self.send_message, font=("微软雅黑", 10), bg="#ffb6c1", activebackground="#ff69b4", relief="flat", bd=1)
        self.send_btn.place(relx=0.82, rely=0.5, relwidth=0.16, relheight=0.6, anchor="w")

        # 3. 中间组件 (最后 pack,自动填充剩余空间)
        self.chat_frame = tk.Frame(self.master)
        self.chat_frame.pack(pady=5, fill=tk.BOTH, expand=True)

        self.chat_text = scrolledtext.ScrolledText(self.chat_frame, wrap=tk.WORD, state='disabled', font=("微软雅黑", 11), bg="#ffffff", relief="flat", bd=1)
        self.chat_text.pack(fill=tk.BOTH, expand=True)

    def load_previous_message(self, event):
        """加载上一条消息(方向键上)"""
        if hasattr(self, 'message_history') and len(self.message_history) > 0:
            if not hasattr(self, 'history_index') or self.history_index >= len(self.message_history):
                self.history_index = len(self.message_history) - 1
            else:
                self.history_index = max(0, self.history_index - 1)
            
            self.input_entry.delete(0, tk.END)
            self.input_entry.insert(0, self.message_history[self.history_index])
            self.input_entry.select_range(0, tk.END)

    def load_image(self):
        """加载并调整AI形象图片"""
        try:
            image_path = self.config.get("image_path", "alice_image.jpg")
            if not os.path.exists(image_path):
                raise FileNotFoundError("图片文件未找到")

            original_image = Image.open(image_path)
            
            width, height = original_image.size
            new_width = 150
            new_height = int((new_width / width) * height)
            if new_height > 200:
                new_height = 200
                new_width = int((new_height / height) * width)
            
            resized_image = original_image.resize((new_width, new_height), Image.LANCZOS)
            self.tk_image = ImageTk.PhotoImage(resized_image)
            self.image_label.config(image=self.tk_image)
        except Exception as e:
            logging.error(f"加载图片失败: {e}")
            self.image_label.config(text="⚠️ 无法加载图片\n请检查图片文件", font=("微软雅黑", 10), fg="#d63384")

    def send_message(self, event=None):
        """发送消息"""
        message = self.input_entry.get().strip()
        if not message:
            return

        if not hasattr(self, 'message_history'):
            self.message_history = []
        self.message_history.append(message)
        self.history_index = len(self.message_history)
        
        self.append_message("我", message, "user")
        self.input_entry.delete(0, tk.END)

        if self.alice:
            self.status_label.config(text="AI正在思考...")
            threading.Thread(target=self.get_ai_response, args=(message,), daemon=True).start()
        else:
            self.append_message("系统", "AI未初始化,请检查设置", "ai")

    def append_message(self, sender, message, msg_type):
        """在聊天框中添加消息"""
        self.chat_text.configure(state='normal')
        
        style_map = {
            "user": {"prefix": f"🧍 {sender}:\n", "tag": "user_tag"},
            "ai": {"prefix": f"🐱 Alice:\n", "tag": "ai_tag"},
            "system": {"prefix": f"⚙️ {sender}:\n", "tag": "system_tag"}
        }
        style = style_map.get(msg_type, style_map["system"])
        
        self.chat_text.insert(tk.END, style["prefix"], style["tag"])
        self.chat_text.insert(tk.END, f"{message}\n\n", f"{msg_type}_content")
        
        self.chat_text.configure(state='disabled')
        self.chat_text.see(tk.END)

    def get_ai_response(self, message):
        """获取AI响应"""
        try:
            response = self.alice.chatbox(message)
            self.master.after(0, self.append_message, "Alice", response, "ai")
            self.master.after(0, self.status_label.config, {"text": "就绪"})
        except Exception as e:
            error_msg = f"AI响应失败: {str(e)}"
            self.master.after(0, self.append_message, "系统", error_msg, "ai")
            self.master.after(0, self.status_label.config, {"text": "错误"})
            logging.error(f"AI响应失败: {e}")

    def open_settings(self):
        """打开设置对话框"""
        settings_window = tk.Toplevel(self.master)
        settings_window.title("设置")
        # <--- 修改:增加窗口高度,为所有组件提供充足空间
        settings_window.geometry("350x500") 
        settings_window.resizable(False, False)
        settings_window.transient(self.master)
        settings_window.grab_set()

        # 设置窗口居中
        # <--- 修改:同步修改居中定位的计算
        x = self.master.winfo_x() + (self.master.winfo_width() - 350) // 2
        y = self.master.winfo_y() + (self.master.winfo_height() - 500) // 2
        settings_window.geometry(f"+{x}+{y}")

        # 内部框架和组件布局 (这部分代码无需修改)
        frame = tk.Frame(settings_window, padx=20, pady=20)
        frame.pack(fill=tk.BOTH, expand=True)

        tk.Label(frame, text="API密钥文件路径:", font=("微软雅黑", 10)).pack(anchor="w", pady=(0, 5))
        key_frame = tk.Frame(frame)
        key_frame.pack(fill=tk.X, pady=(0, 15))
        key_entry = tk.Entry(key_frame, font=("微软雅黑", 10), width=30)
        key_entry.insert(0, self.config.get("key_path", ""))
        key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
        
        def browse_key():
            file_path = filedialog.askopenfilename(title="选择API密钥文件", filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")])
            if file_path:
                key_entry.delete(0, tk.END)
                key_entry.insert(0, file_path)
        browse_btn = tk.Button(key_frame, text="浏览", command=browse_key)
        browse_btn.pack(side=tk.RIGHT, padx=(5, 0))

        tk.Label(frame, text="AI角色设定:", font=("微软雅黑", 10)).pack(anchor="w", pady=(0, 5))
        sys_text = scrolledtext.ScrolledText(frame, font=("微软雅黑", 10), height=5, wrap=tk.WORD)
        sys_text.insert(tk.END, self.config.get("sys_content", "你需要扮演一个可爱的喵娘..."))
        sys_text.pack(fill=tk.X, pady=(0, 15))

        tk.Label(frame, text="对话历史长度:", font=("微软雅黑", 10)).pack(anchor="w", pady=(0, 5))
        history_var = tk.IntVar(value=self.config.get("max_history", 100))
        history_scale = tk.Scale(frame, from_=10, to=200, orient=tk.HORIZONTAL, variable=history_var, font=("微软雅黑", 10))
        history_scale.pack(fill=tk.X, pady=(0, 15))

        tk.Label(frame, text="形象图片路径:", font=("微软雅黑", 10)).pack(anchor="w", pady=(0, 5))
        image_frame = tk.Frame(frame)
        image_frame.pack(fill=tk.X, pady=(0, 15))
        image_entry = tk.Entry(image_frame, font=("微软雅黑", 10), width=30)
        image_entry.insert(0, self.config.get("image_path", "alice_image.jpg"))
        image_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
        
        def browse_image():
            file_path = filedialog.askopenfilename(title="选择形象图片", filetypes=[("图片文件", "*.jpg *.jpeg *.png *.gif"), ("所有文件", "*.*")])
            if file_path:
                image_entry.delete(0, tk.END)
                image_entry.insert(0, file_path)
        image_btn = tk.Button(image_frame, text="浏览", command=browse_image)
        image_btn.pack(side=tk.RIGHT, padx=(5, 0))

        btn_frame = tk.Frame(frame)
        btn_frame.pack(pady=20)

        def save_settings():
            self.config["key_path"] = key_entry.get()
            self.config["sys_content"] = sys_text.get("1.0", tk.END).strip()
            self.config["max_history"] = history_var.get()
            self.config["image_path"] = image_entry.get()
            
            self.save_config()
            
            try:
                self.alice = Alice(key_path=self.config["key_path"], sys_content=self.config["sys_content"], max_history=self.config["max_history"])
                messagebox.showinfo("成功", "设置已保存并应用")
                settings_window.destroy()
                self.load_image()
            except Exception as e:
                messagebox.showerror("错误", f"应用设置失败: {str(e)}")

        save_btn = tk.Button(btn_frame, text="保存", command=save_settings, font=("微软雅黑", 10), bg="#ffb6c1", width=10)
        save_btn.pack(side=tk.LEFT, padx=(0, 10))
        cancel_btn = tk.Button(btn_frame, text="取消", command=settings_window.destroy, font=("微软雅黑", 10), bg="#e0e0e0", width=10)
        cancel_btn.pack(side=tk.LEFT)

    def on_closing(self):
        """窗口关闭事件处理"""
        if messagebox.askokcancel("退出", "确定要退出吗?"):
            self.master.destroy()

def main():
    # 配置日志 (统一在此处配置)
    logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", filename="alice_app.log")

    root = tk.Tk()
    root.option_add("*Font", "微软雅黑 10")
    root.option_add("*Background", "#fff0f5")
    root.option_add("*Button.Background", "#ffb6c1")
    root.option_add("*Button.Foreground", "#333333")
    root.option_add("*Entry.Background", "white")
    root.option_add("*Text.Background", "white")

    app = AliceChatApp(root)

    app.chat_text.tag_configure("user_tag", foreground="#d63384", font=("微软雅黑", 11, "bold"))
    app.chat_text.tag_configure("ai_tag", foreground="#ff69b4", font=("微软雅黑", 11, "italic"))
    app.chat_text.tag_configure("system_tag", foreground="#666666", font=("微软雅黑", 11))
    app.chat_text.tag_configure("user_content", foreground="#333333", spacing1=5, spacing3=10)
    app.chat_text.tag_configure("ai_content", foreground="#333333", spacing1=5, spacing3=10) # 颜色可以自定义

    try:
        root.mainloop()
    except Exception as e:
        logging.error(f"应用运行异常: {e}")
        messagebox.showerror("错误", f"应用异常: {str(e)}")

if __name__ == "__main__":
    main()

本项目使用使用的库较少,通常只需要安装openai模块即可运行,如果还缺少其他模块,可以使用pip或conda(如果使用conda环境的话)安装它们。

pip install openai

关于密钥的获取和使用可以参考

https://blog.csdn.net/Jay_Franklin/article/details/150589387?spm=1011.2124.3001.6209https://blog.csdn.net/Jay_Franklin/article/details/150589387?spm=1011.2124.3001.6209

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐