
在实际开发中桌宠通常被看作“娱乐项目”但如果把 AI 能力接入进去它就不再只是桌面上的一个小挂件。对于一个需要频繁切换任务、容易分心、又依赖外部提醒才能维持节奏的 ADHD 人群来说桌宠可以同时承担解压陪伴、任务提醒、AI 问答和专注计时四种角色。这篇文章会围绕“AI 情绪陪伴 效率提醒”这条主线拆解一个桌面 AI 桌宠的完整实现过程从需求设计、技术选型、环境准备到本地大模型接入、桌面悬浮窗口、语音交互、专注提醒再到常见的踩坑排查和生产环境建议。读者对象是已经掌握 Python 基础、了解 PySide6 或 Electron 基本用法想把 AI 功能做成桌面产品的开发者。读完这篇文章你能得到一个可以本地运行的 AI 桌宠最小闭环并且能够根据自己的需求扩展出更多功能模块。1. 先理解 ADHD 场景下 AI 桌宠到底在解决什么问题桌宠不是新鲜概念早在 Flash 时代就出现过桌面宠物。但和当年的“纯动画挂件”相比今天的 AI 桌宠多了一个核心能力它能理解上下文、能主动回复、能根据时间提醒任务。对于 ADHD 人群和使用场景而言这个差异是决定性的。1.1 ADHD 人群使用桌宠的核心需求ADHD 的执行功能障碍通常表现为启动困难、注意力保持短、时间感知偏差、容易沉迷某一件事而忘记其他安排。因此一个好的数字陪伴工具需要满足以下要求低启动门槛打开电脑就能看到不需要先打开某个应用再去操作。高容错交互用户可能中断对话、随时离开、过一会又回来系统要能接受这种不连续行为。主动提醒能力不能只等人来问它要根据设定的时间主动说话。情绪价值在用户焦躁、拖延、无法启动任务时给出安抚或拆解任务的提示而不是冷冰冰的指令。离线可用ADHD 用户在注意力脆弱的时候如果网络请求失败或者等待时间过长很容易放弃工具本身。这些需求决定了架构设计方向客户端必须在本地完成主要交互AI 能力需要优先考虑本地模型而不是完全依赖云端接口。1.2 “解压 效率”两条产品线的差异这个产品实际上有两条完全不同的功能线功能线代表功能核心体验要求技术关键词解压陪伴点击气泡、拖动桌宠、抚摸互动、随机小动作低延迟、高反馈、动画自然动画系统、事件响应、音频反馈效率辅助待办提醒、专注计时、AI 拆解任务、快捷问答准点、内容准确、上下文可追溯定时器、本地数据库、LLM 上下文解压功能要求反馈即时毫秒级响应效率功能要求逻辑严谨时间、内容、状态都不能丢失。两条功能线共享桌面窗口和交互入口但底层模块必须分离开。2. AI 桌宠整体架构和技术方案选型在写代码之前先确定整体架构。一个可维护的 AI 桌宠至少包含五个模块桌面窗口模块、交互输入模块、AI 对话模块、定时任务模块、持久化存储模块。2.1 桌面客户端技术选型对比桌面客户端的实现有多种选择需要根据目标场景做取舍。常见的方案对比见下表方案开发语言窗口透明与置顶能力AI 生态集成便捷度适用场景PySide6 / PyQt6Python支持无边框、透明、置顶高Python 生态丰富快速原型、中小型桌宠Electron WebJavaScript/TypeScript支持透明窗口但有性能开销中需要桥接 Node 层Web 前端团队、复杂 UITauri WebRust Web性能好包体积小中Rust 生态相对门槛高追求体积和性能的团队WPF / WinUIC#Windows 原生支持好中依赖 .NET 生态仅限 Windows 场景这里选择 PySide6 作为示例理由有三点一是 Python 接入本地大模型非常方便Ollama、Transformers、LangChain 都有现成接口二是 PySide6 的 QSystemTrayIcon、QPropertyAnimation、QtMultimedia 模块可以覆盖桌宠的托盘、动画和音效需求三是原型开发迭代速度快适合个人开发者先验证产品逻辑。2.2 AI 能力选择本地模型优先ADHD 场景对延迟非常敏感。如果每一句对话都要经过云端大模型网络抖动和排队都会让用户产生“这个工具不行”的负面感受。优先选择本地模型方案。目前比较成熟的本地大模型运行工具是 Ollama。它支持多种开源模型并提供 HTTP API默认端口是11434。对于普通对话和任务拆解需求qwen2.5:7b或llama3.1:8b都够用。计算机配置较低的可以选qwen2.5:3b。选择本地模型同时要接受一个事实本地模型的能力上限低于云端大模型尤其是复杂推理和长文本理解。因此架构上要做双通道设计默认走本地模型当检测到复杂任务时提示用户是否切换到云端模型。2.3 模块划分和消息流转链路整个系统的消息流转可以理解为用户操作点击、拖动、文本输入、语音输入 - 输入层PySide6 信号 / 语音识别结果 - 行为分发器判断是互动操作、命令操作还是对话操作 - 功能执行器动画模块 / 提醒模块 / AI 对话模块 / 计时模块 - 输出层气泡文本、窗口动画、系统提示音、任务状态变更 - 持久化层SQLite 保存对话记录、任务列表和用户配置以“用户输入一句话”为例行为分发器会把这句话送给意图识别模块。如果命中“提醒我 30 分钟后开会”则进入定时模块如果只是普通句子则进入 AI 对话模块。这个过程要控制在 100ms 内完成意图判断否则对话延迟会变明显。3. 环境准备与依赖配置在开始写代码之前先把环境和依赖准备好。这一步不要偷懒版本不一致会在后面产生大量隐性错误。3.1 Python 环境与虚拟环境建议使用 Python 3.10 及以上版本。桌面应用依赖较多必须使用虚拟环境隔离python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活虚拟环境后升级 pip 并安装核心依赖python -m pip install --upgrade pip pip install PySide6 requests openai这里openai库的作用不是必须连接 OpenAI 官方服务而是因为它提供了兼容的客户端接口。Ollama 的 API 与 OpenAI 格式兼容所以可以用OpenAI客户端指向本地地址省去手动构造 HTTP 请求的代码。如果计划加入语音交互还需要额外安装pip install SpeechRecognition pyttsx3 pyaudio注意pyaudio在 Windows 上直接安装经常失败可以从pipwin或预编译 wheel 中安装或者使用sounddevice替代。3.2 安装 Ollama 并下载本地模型Ollama 的安装方式官网有详细说明这里只说关键点。安装完成后先验证服务是否启动ollama list如果显示模型列表或没有报错说明服务正常。然后下载一个适合对话的基础模型ollama pull qwen2.5:7b下载完成后本地就能通过命令行测试模型ollama run qwen2.5:7b 用一句话鼓励我看到模型回复后再确认 HTTP API 是否可用curl http://localhost:11434/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5:7b, messages: [ {role: user, content: 你好} ] }这一步返回正常的 JSON 响应后Python 代码里的网络部分就不会有悬念了。3.3 项目目录结构设计一个清晰的目录结构对后续扩展非常重要。推荐按模块拆分而不是把所有逻辑写在一个文件里ai_desktop_pet/ ├── main.py # 应用入口 ├── config.py # 全局配置 ├── core/ │ ├── pet_window.py # 桌宠透明窗口 │ ├── bubble.py # 气泡对话框 │ ├── animator.py # 动画控制器 │ └── tray.py # 系统托盘 ├── services/ │ ├── ai_service.py # 大模型对话服务 │ ├── scheduler.py # 定时提醒服务 │ └── voice.py # 语音输入输出 ├── storage/ │ └── database.py # SQLite 封装 ├── assets/ │ ├── idle.png # 待机动画帧 │ ├── click.png # 点击动画帧 │ └── sound/ # 提示音效 └── requirements.txt这个结构把界面、业务、数据三层分离。后续替换动画资源、更换大模型后端、增加新的提醒规则都不会牵动全部代码。4. 第一阶段先让桌宠窗口在桌面上“活”起来不要一上来就接 AI先把桌宠的窗口工程跑通。透明的无边框窗口、拖拽、置顶、托盘这些基础能力是桌宠的地基。4.1 创建透明无边框窗口桌宠窗口不需要系统标题栏也不需要普通窗口背景。PySide6 中通过设置窗口标志和属性实现# core/pet_window.py from PySide6.QtWidgets import QWidget from PySide6.QtCore import Qt, QPoint from PySide6.QtGui import QPainter, QPixmap class PetWindow(QWidget): def __init__(self): super().__init__() # 无边框、窗口置顶、工具窗口不抢焦点 self.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool ) # 背景透明 self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setFixedSize(160, 160) self.image QPixmap(assets/idle.png) self.drag_position None def paintEvent(self, event): painter QPainter(self) painter.drawPixmap(self.rect(), self.image) def mousePressEvent(self, event): if event.button() Qt.MouseButton.LeftButton: self.drag_position event.globalPosition().toPoint() - self.frameGeometry().topLeft() event.accept() def mouseMoveEvent(self, event): if event.buttons() and Qt.MouseButton.LeftButton and self.drag_position is not None: self.move(event.globalPosition().toPoint() - self.drag_position) event.accept() def mouseReleaseEvent(self, event): self.drag_position None关键点有三个。FramelessWindowHint去掉标题栏WA_TranslucentBackground让窗口背景透明Tool标志让窗口不抢焦点这是桌宠不干扰用户打字的关键。4.2 添加气泡对话框和文字输出点击桌宠时需要弹出气泡显示文字。气泡可以做成同一个窗口内的子控件也可以单独做一个半透明圆角面板。这里做一个简单的 QLabel 子控件# core/bubble.py from PySide6.QtWidgets import QLabel from PySide6.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve class Bubble(QLabel): def __init__(self, parentNone): super().__init__(parent) self.setObjectName(bubble) self.setWordWrap(True) self.setMaximumWidth(240) self.setStyleSheet( #bubble { background: rgba(0, 0, 0, 200); color: white; border-radius: 12px; padding: 12px; font-size: 14px; } ) self.hide() self.timer QTimer(self) self.timer.timeout.connect(self.hide) def show_text(self, text, duration5000): self.setText(text) self.adjustSize() # 气泡位置在桌宠窗口上方 self.move(20, -self.height() - 10) self.show() self.timer.start(duration)这里用了adjustSize根据内容自动调整气泡大小。位置通过move放到桌宠上方保证不遮挡桌宠本体。4.3 使用动画切换待机与点击状态桌宠的“活”主要体现在动画上。PySide6 的QPropertyAnimation可以设置位置、透明度和大小动画。一个简单的做法是做多帧切换# core/animator.py from PySide6.QtCore import QObject, QTimer, QPropertyAnimation, QEasingCurve class PetAnimator(QObject): def __init__(self, pet_window): super().__init__() self.window pet_window self.frame_index 0 self.frames [assets/idle_1.png, assets/idle_2.png, assets/idle_3.png] self.timer QTimer() self.timer.timeout.connect(self.next_frame) self.timer.start(400) # 每 400ms 切换一帧 def next_frame(self): self.frame_index (self.frame_index 1) % len(self.frames) self.window.image.load(self.frames[self.frame_index]) self.window.update() def jump(self): # 点击后的弹跳动画 self.animation QPropertyAnimation(self.window, bpos) current self.window.pos() self.animation.setDuration(300) self.animation.setStartValue(current) self.animation.setEndValue(current QPoint(0, -30)) self.animation.setEasingCurve(QEasingCurve.Type.OutBounce) self.animation.finished.connect( lambda: self._back_to_origin(current) ) self.animation.start() def _back_to_origin(self, origin): self.back_animation QPropertyAnimation(self.window, bpos) self.back_animation.setDuration(300) self.back_animation.setStartValue(self.window.pos()) self.back_animation.setEndValue(origin) self.back_animation.setEasingCurve(QEasingCurve.Type.InBounce) self.back_animation.start()动画帧图片可以用简单的 PNG 序列。没有美术资源的可以直接用文字或不同颜色的圆角矩形占位关键是先把动画渲染链路跑通。5. 第二阶段把本地大模型接入桌宠对话窗口跑通后开始接入 AI 对话。这是整个项目从“普通桌宠”升级到“AI 桌宠”的关键一步。5.1 配置 Ollama 客户端使用 OpenAI 兼容接口连接本地 Ollama 服务。配置类单独放在config.py中# config.py import os class Config: # 本地 Ollama 服务地址 OLLAMA_BASE_URL os.getenv(OLLAMA_BASE_URL, http://localhost:11434/v1) OLLAMA_API_KEY os.getenv(OLLAMA_API_KEY, ollama) OLLAMA_MODEL os.getenv(OLLAMA_MODEL, qwen2.5:7b) # 对话参数 TEMPERATURE 0.7 MAX_TOKENS 1024 TIMEOUT 30 # 数据库路径 DB_PATH os.getenv(DB_PATH, pet_data.db)环境变量预留了配置外置的入口。即使现在直接写死也建议保留这个结构方便后面切换到云端模型时只修改环境变量。创建 AI 对话服务# services/ai_service.py from openai import OpenAI from config import Config class AIService: def __init__(self): self.client OpenAI( base_urlConfig.OLLAMA_BASE_URL, api_keyConfig.OLLAMA_API_KEY, timeoutConfig.TIMEOUT, ) self.system_prompt ( 你是一个生活助理桌宠名字叫小灵。 用户可能是 ADHD 人群需要在鼓励、提醒、拆解任务方面提供帮助。 回答要简洁最多 80 个字。语气友好耐心不要长篇大论。 ) def chat(self, user_message: str, history: list) - str: messages [{role: system, content: self.system_prompt}] messages.extend(history) messages.append({role: user, content: user_message}) response self.client.chat.completions.create( modelConfig.OLLAMA_MODEL, messagesmessages, temperatureConfig.TEMPERATURE, max_tokensConfig.MAX_TOKENS, ) return response.choices[0].message.contentOpenAI客户端的base_url指向本地地址api_key随便填一个非空值即可因为 Ollama 本地服务不校验 key。但要注意如果你用的是远程 Ollama 服务或兼容网关key 必须真实填写。5.2 处理对话上下文和超时问题桌宠对话必须是多轮的否则用户每说一句话它都无法理解上文。简单做法是把最近 6 条对话记录保存到列表里随请求一起发给模型class ConversationManager: def __init__(self, max_history6): self.max_history max_history self.history [] def add(self, role: str, content: str): self.history.append({role: role, content: content}) if len(self.history) self.max_history: self.history self.history[-self.max_history:] def clear(self): self.history []这里的坑在于如果直接拿用户输入和 AI 返回拼接历史用户消息和 AI 消息需要成对保存。顺序错误会导致模型上下文混乱。超时问题也需要处理。本地模型在低配置机器上可能要 10 到 30 秒才生成回复如果放在 UI 主线程里窗口会直接卡死。必须把 AI 调用放进线程池# services/ai_service.py from PySide6.QtCore import QThread, Signal class ChatWorker(QThread): finished Signal(str) failed Signal(str) def __init__(self, ai_service, user_message, history): super().__init__() self.ai_service ai_service self.user_message user_message self.history history def run(self): try: reply self.ai_service.chat(self.user_message, self.history) self.finished.emit(reply) except Exception as exc: self.failed.emit(str(exc))在窗口层点击发送后启动ChatWorker等到finished信号时再刷新气泡。这是桌宠交互流畅的最低要求。5.3 在气泡处理中识别“提醒意图”除了普通对话桌宠还需要识别用户是否在设置提醒。在把消息交给大模型之前先做一个轻量级规则判断# services/intent.py import re from datetime import datetime, timedelta def parse_remind(text: str): # 匹配 “N 分钟后……” match re.search(r(\d)\s*分钟后\s*(.*), text) if match: minutes int(match.group(1)) content match.group(2) or 未指定事项 remind_time datetime.now() timedelta(minutesminutes) return {type: remind, time: remind_time, content: content} return None这个规则很简单但非常直接。命中“提醒”意图后就走定时任务模块不再送进大模型。这样既省了模型调用延迟也让“提醒”这类关键动作不受模型输出稳定性影响。6. 第三阶段加入专注计时和定时提醒效率工具的核心是定时任务。这里使用 QTimer 做倒计时SQLite 做任务持久化。6.1 设计任务表结构提醒任务需要有开始时间、执行时间、内容、状态和创建时间。SQLite 表结构如下CREATE TABLE IF NOT EXISTS reminders ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, remind_at TEXT NOT NULL, status TEXT DEFAULT pending, created_at TEXT DEFAULT CURRENT_TIMESTAMP );针对 ADHD 场景还可以增加一个repeat_type字段用于后续扩展重复提醒每天、每周、工作日。第一版先不做重复提醒但要留字段位。6.2 使用 QTimer 周期性扫描到期任务桌宠不需要为每个任务单独创建一个定时器那样资源开销太大。更好的方案是每隔 10 秒查询一次数据库找到所有到期且未被处理的任务# services/scheduler.py from PySide6.QtCore import QObject, QTimer from datetime import datetime class ReminderScheduler(QObject): def __init__(self, db, on_remind): super().__init__() self.db db self.on_remind on_remind self.timer QTimer() self.timer.timeout.connect(self.check_due_reminders) self.timer.start(10000) def check_due_reminders(self): now datetime.now().strftime(%Y-%m-%d %H:%M:%S) reminders self.db.fetch_due(now) for item in reminders: self.on_remind(item[content]) self.db.mark_done(item[id])这种轮询方式简单可靠不会因为任务数量增加而产生大量线程。10 秒的扫描间隔对提醒场景足够因为桌宠不是秒级闹钟。6.3 专注计时模块专注计时是 ADHD 场景中非常有用的功能。实现一个简单的双状态计时器# services/focus_timer.py from PySide6.QtCore import QObject, QTimer, Signal class FocusTimer(QObject): tick Signal(int) completed Signal() def __init__(self, duration_minutes: int 25): super().__init__() self.duration duration_minutes * 60 self.remaining self.duration self.timer QTimer() self.timer.timeout.connect(self._on_tick) def start(self): self.remaining self.duration self.timer.start(1000) self.tick.emit(self.remaining) def pause(self): self.timer.stop() def reset(self): self.timer.stop() self.remaining self.duration self.tick.emit(self.remaining) def _on_tick(self): self.remaining - 1 if self.remaining 0: self.timer.stop() self.completed.emit() else: self.tick.emit(self.remaining)每次 tick 信号通知界面刷新剩余时间。倒计时结束后触发completed桌宠可以弹出气泡提示“专注结束休息一下”。这个模块后续可以和番茄钟算法、待办列表关联形成完整的效率闭环。7. 完整串联从点击桌宠到 AI 回应的主流程各模块已经就绪现在把它们串进主窗口。核心目标是让整个链路在用户视角下自然流畅点击桌宠 - 输入框出现 - 提交文本 - 气泡显示等待状态 - AI 回复出现在气泡中。7.1 主窗口的组件协作主窗口负责创建桌宠窗口、气泡、托盘、动画器、AI 服务和提醒调度器。一个简化版的main.py# main.py import sys from PySide6.QtWidgets import QApplication, QSystemTrayIcon, QMenu from PySide6.QtGui import QAction, QIcon from core.pet_window import PetWindow from core.bubble import Bubble from core.animator import PetAnimator from core.tray import TrayIcon from services.ai_service import AIService, ChatWorker from services.scheduler import ReminderScheduler from services.intent import parse_remind from storage.database import Database from config import Config class DesktopPetApp: def __init__(self): self.app QApplication(sys.argv) self.pet PetWindow() self.bubble Bubble(self.pet) self.animator PetAnimator(self.pet) self.ai_service AIService() self.db Database(Config.DB_PATH) self.scheduler ReminderScheduler(self.db, self.on_remind) self._setup_tray() self._setup_interactions() def _setup_interactions(self): self.pet.mousePressEvent self.pet.mousePressEvent # 保留原生拖拽 # 双击打开对话输入框 self.pet.mouseDoubleClickEvent lambda e: self.pet.show_text_input() def _setup_tray(self): self.tray TrayIcon(self.pet, self) def on_remind(self, content: str): self.bubble.show_text(f提醒{content}, duration8000) def handle_user_input(self, text: str): if not text.strip(): return # 先判断提醒意图 remind parse_remind(text) if remind: self.db.add_reminder(remind[content], remind[time].strftime(%Y-%m-%d %H:%M:%S)) self.bubble.show_text(f已设置提醒{remind[content]}, duration4000) return # 普通对话走 AI 线程 self.bubble.show_text(让我想想……, duration100000) self.worker ChatWorker(self.ai_service, text, []) self.worker.finished.connect(lambda reply: self.bubble.show_text(reply, duration8000)) self.worker.failed.connect(lambda err: self.bubble.show_text(f出错了{err}, duration8000)) self.worker.start() def run(self): self.pet.show() return self.app.exec() if __name__ __main__: app DesktopPetApp() sys.exit(app.run())这里没有把所有信号槽都展开但主流程已经完整。双击桌宠弹出输入框输入文本后先判断是否是提醒意图是则写入数据库否则交给 AI 线程处理完成后通过信号更新气泡。7.2 添加到系统托盘桌宠窗口可以被隐藏但应用不能消失。托盘图标提供了显示/隐藏、退出、清空历史记录等操作# core/tray.py from PySide6.QtWidgets import QSystemTrayIcon, QMenu from PySide6.QtGui import QAction, QIcon class TrayIcon: def __init__(self, pet_window, app): self.pet pet_window self.app app self.tray QSystemTrayIcon(QIcon(assets/icon.png)) self.tray.setToolTip(AI 桌宠) menu QMenu() show_action QAction(显示桌宠, None) show_action.triggered.connect(self.pet.show) menu.addAction(show_action) hide_action QAction(隐藏桌宠, None) hide_action.triggered.connect(self.pet.hide) menu.addAction(hide_action) menu.addSeparator() quit_action QAction(退出, None) quit_action.triggered.connect(self.app.app.quit) menu.addAction(quit_action) self.tray.setContextMenu(menu) self.tray.show()如果桌宠进程退出时没有隐藏托盘图标Windows 上会出现图标残留。退出前调用self.tray.hide()可以规避这个问题。8. 运行验证与调试方法代码写完不是结束必须验证各条主链路是否正常。8.1 最小验证清单按依赖顺序逐项验证验证项操作预期结果Ollama 服务curl http://localhost:11434/api/tags返回模型 JSON模型对话ollama run qwen2.5:7b 你好模型正常回复Python 导入python -c import PySide6; print(PySide6.__version__)输出版本号窗口启动python main.py桌宠悬浮显示、无边框、可拖动AI 对话双击桌宠输入“你好”气泡显示模型回复提醒任务输入“提醒我 1 分钟后喝水”气泡确认1 分钟后提醒出现问题时不要直接看界面先检查命令行输出和stderr日志。PySide6 的很多错误不会弹窗而是打印到标准错误流。8.2 常见报错排查路径现象 1窗口启动后闪退可能原因通常是资源路径错误。比如assets/idle.png不存在QPixmap加载空图片导致绘制异常。检查方式是在paintEvent里打印self.image.isNull()。解决方式是使用os.path.join(os.path.dirname(__file__), assets, idle.png)绝对路径加载。现象 2AI 对话很久不回复先测试模型本身是否正常。如果模型加载需要 10 秒以上可以考虑减少MAX_TOKENS或者换qwen2.5:3b。另外确认ChatWorker是否真的启动代码里容易把self.worker写成局部变量导致线程被垃圾回收从而丢失信号。现象 3定时提醒不触发先看数据库表是否正确创建再确认remind_at的时间格式和fetch_due查询是否匹配。最容易踩的坑是时区不一致datetime.now()生成的是本地时间如果之前手动插入过 UTC 时间到期判断就会出错。现象 4托盘图标消失但进程没退出正常情况下右键菜单选择退出应同时结束QApplication。如果是直接关闭桌宠窗口只隐藏不退出需要确认隐藏动作不会导致QSystemTrayIcon被回收。8.3 加入线程安全防护PySide6 的 UI 操作必须在主线程执行。ChatWorker中不能直接调用self.bubble.show_text()否则在高频操作下会出现段错误。上面的代码通过信号传递结果实际使用中如果要在工作线程里写数据库也要使用独立连接或在主线程里执行写入。给数据库操作加上锁也是一种防御方式。SQLite 默认串行写入但多个线程同时调用fetch_due和mark_done时可能会遇到database is locked。可以在Database类内部使用threading.Lock# storage/database.py import sqlite3 import threading class Database: def __init__(self, db_path): self.lock threading.Lock() self.conn sqlite3.connect(db_path, check_same_threadFalse) self._create_tables() def fetch_due(self, now): with self.lock: cur self.conn.execute( SELECT id, content FROM reminders WHERE statuspending AND remind_at ?, (now,), ) return [{id: r[0], content: r[1]} for r in cur.fetchall()] def mark_done(self, reminder_id): with self.lock: self.conn.execute( UPDATE reminders SET statusdone WHERE id?, (reminder_id,), ) self.conn.commit()check_same_threadFalse允许跨线程使用同一个连接但必须配合锁保护写入操作。实际生产环境也可以考虑每个线程独立连接但对桌宠这种轻量应用一个连接加锁已经足够。9. 落地阶段的排错与细节校准进入实际使用后还有很多从“能跑”到“好用”之间的问题需要处理。这些细节往往决定了用户是否愿意长期使用桌宠。9.1 AI 回复内容不可控怎么办本地模型没有云端模型的指令跟随能力稳定。即使设定了 system prompt在用户输入“你是什么模型”“讲个超长故事”等请求时它仍然可能输出过长内容。应对方案在代码层面强制截断如果回复超过 200 个字符气泡里只显示前 200 个字符加省略号。在 system prompt 里强调“每条回复最多 80 字”但不要完全依赖它。在界面上增加一个“完整回复”按钮点击才展开大模型原始输出。不要试图完全控制模型输出这不是桌宠应用应该做的事情。应用层负责展示边界模型负责内容生成职责分离最安全。9.2 桌宠窗口交互干扰正常桌面操作桌宠窗口虽然设置了Tool标志但拖动过程中仍然可能与正常窗口产生遮挡问题。针对 ADHD 用户桌宠必须比普通应用更安静不能突然跳动或抢焦点。设置策略如下self.pet.setWindowFlag(Qt.WindowType.WindowDoesNotAcceptFocus, True) self.pet.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)第一个标志让桌宠不接受键盘焦点用户打字时不会被桌宠打断。第二个属性让桌宠显示时不会激活窗口自身。这两个配置对保持桌面安静非常重要。9.3 开机自启动和资源占用作为桌宠产品开机自启几乎是刚需。Windows 上可以通过在注册表启动项中添加当前程序实现也可以更规范地做成安装器配置。手动注册表的写法HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run添加一个字符串值指向当前 Python 脚本或打包后的 exe。macOS 和 Linux 则分别使用 LaunchAgent 和 autostart desktop 文件。资源占用方面Python PySide6 桌宠启动后内存通常在 80MB 到 150MB 之间。如果不做优化多帧动画和频繁的本地模型调用会显著增加 CPU 占用。建议空闲状态使用低帧率动画例如每 800ms 一帧。用户未点击时不要频繁重绘。本地模型使用 Ollama 时模型默认加载到内存后会常驻对内存占用敏感的用户可以设置OLLAMA_KEEP_ALIVE环境变量调整释放时间。10. 缺陷与防护桌面应用最容易忽略的问题桌宠长期运行很多潜在问题不会在开发时暴露而是在开机后几天才出现。这里梳理实际使用中最值得关注的五类问题。10.1 长时间运行导致的动画卡顿多帧动画如果使用QPixmap频繁加载文件长时间运行后会累积内存碎片。建议启动时把所有动画帧加载进内存而不是每帧都读磁盘。10.2 本地大模型响应过长导致的线程堆积用户连续提问时如果前一个ChatWorker还没结束用户又发起新请求就会同时存在多个线程等待模型回复。内存和 CPU 都会被拖垮。解决方式是新请求发起前把之前的worker设置为不可执行或者使用队列机制同一时间只允许一个对话请求。10.3 提醒任务长时间不执行前面说过轮询机制本身可靠但如果用户把系统休眠、桌宠窗口被隐藏、或者 QTimer 回调中出现了未捕获异常定时器会静默停止。关键位置要加 try-except 并把异常写入日志文件。10.4 隐私数据持久化风险对话记录和任务提醒会包含用户的生活细节。数据库文件不能明文存放在普通目录中。至少要做数据库文件加密或存储到用户数据目录并设置文件权限。常见的 SQLite 加密方案有 SQLCipher但对桌宠项目来说第一步先把数据库文件放到系统用户目录下避免程序目录被第三方修改读取。10.5 未处理全局异常导致桌宠悄悄退出桌宠不像服务器可以随时查看控制台。一旦某个线程抛异常可能整个应用消失而用户毫不知情。在入口位置注册全局异常钩子并把错误写入日志文件# utils/logger.py import sys import traceback def init_global_exception_logger(): def hook(exc_type, exc_value, exc_tb): with open(pet_error.log, a, encodingutf-8) as f: f.write(.join(traceback.format_exception(exc_type, exc_value, exc_tb))) sys.excepthook hook这样即使应用崩溃也能留下排查线索。11. 生产化部署与后续扩展桌宠项目从原型到长期可用还有几条值得持续投入的扩展方向。11.1 从 Python 脚本到独立产品的路径本地直接运行python main.py依赖用户电脑安装 Python 环境不适合发给普通用户。至少要做到两步使用 PyInstaller 或 Nuitka 把应用打包成独立可执行文件。把 Ollama 作为可选安装项初始版本对话能力直接通过内置 API 接一个默认的轻量模型用户后续可以自行切换。打包时要注意 PySide6 的资源路径问题。脚本运行正常的代码在打包后经常因为找不到assets目录而崩。使用PyInstaller的--add-data参数把资源文件打进去并在代码中通过sys._MEIPASS兼容路径处理。11.2 多模态交互扩展当前示例只实现了文本交互。生产级桌宠还应支持语音输入通过麦克风识别用户说了什么适合 ADHD 用户在分心时快速记录。语音输出使用pyttsx3或本地 TTS 引擎朗读提醒内容。表情动画根据大模型输出情感标签切换桌宠表情。应用联动检测到用户长时间使用某个软件时桌宠主动弹窗提醒休息。其中应用联动是效率工具的关键升级。通过系统 API 获取当前前台窗口标题结合时间策略判断用户是否处于超长专注状态。11.3 开放接口和插件化设计如果桌宠想继续发展不能把功能写死在核心代码里。把“技能”做成插件例如“任务插件”负责从待办软件同步今日任务。“天气插件”在早上问候时附带天气提醒。“健康插件”根据久坐时间提醒用户喝水、伸展。每个插件通过简单的 Python 接口实现由桌宠主体动态加载。这样即使不增加模型能力桌宠也能通过规则引擎完成大量效率辅助工作。11.4 ADHD 场景的长期使用建议技术只是工具真正对用户有效的是让工具融入日常节奏。桌宠产品在 ADHD 场景落地时建议遵循三原则少即是多默认关闭非必要的弹窗提醒避免形成“提醒疲劳”。信息可回溯对话记录和任务记录要有历史页面帮助用户在状态不好时回顾自己做过什么。允许失败不要设计成“必须做到”的打卡工具而是提供“鼓励再试一次”的心理缓冲。这三点决定了产品定位桌宠不是监督者而是陪伴者和辅助者。开发时也应保持这种产品心态技术选型和交互设计都不应该给用户增加额外认知负担。12. 常见开发误区与最佳实践汇总12.1 六个高频误区误区后果正确做法把所有逻辑写进 main.py后期寸步难行拆分为窗口、服务、存储三层用全局变量保存对话历史状态混乱用 ConversationManager 管理AI 请求放主线程UI 卡死使用 QThread 或线程池给每个提醒创建 QTimer资源浪费统一轮询数据库忽略全局异常处理程序静默退出注册 sys.excepthook 和日志直接在程序目录写数据库权限和数据安全风险使用用户数据目录12.2 代码审查清单在提交代码或打包前自查以下项目启动时是否检查 Ollama 服务和模型可用性不可用时是否给出友好提示。是否限制对话历史长度防止 token 超限。所有耗时操作是否在非主线程执行。数据库写入是否有锁保护。拖拽窗口时是否频繁触发布局计算是否有必要降低重绘频率。退出逻辑是否完整包括隐藏托盘图标、停止定时器、关闭数据库连接。语音模块是否适配了无麦克风环境。错误日志是否包含时间和异常栈是否循环写入避免无限膨胀。12.3 进阶学习路径完成一个基础版 AI 桌宠后下一步建议按这个顺序深入学习 PySide6 的 Graphics View 框架实现更丰富的桌宠动画。理解 RAG 的基本概念给桌宠接入用户自己的知识库让它能回答“我上次说的那个问题”等需要记忆的请求。学习本地模型的微调方法针对 ADHD 场景定制更贴合的回复风格。使用 WHISPER 本地语音识别代替云端的语音接口。研究长期运行服务的稳定性设计包括内存监控、自动重启和性能日志分析。每一步都能独立成为一篇技术博客也是把 AI 桌宠从“玩具”做成“工具”的必然路径。