从零实现一个AI桌面宠物:纯Python、离线运行、会学习你的习惯

你是否想过拥有一只生活在电脑桌面上的虚拟宠物?它会主动找你玩,会饿会困会开心,能记住你喂过它什么,甚至拥有独立的"性格"——而且这一切完全离线运行,不需要联网,不调用任何云API,全部用纯Python实现。


请添加图片描述

公众号文章链接

一、项目背景

市面上的桌面宠物要么是静态壁纸,要么需要联网调用AI接口。我想做一个不一样的:

  • 完全离线 —— 0 API 调用,所有AI逻辑在本地运行
  • 有"大脑" —— 能自主学习主人的互动习惯,拥有独立的性格和记忆
  • 轻量 —— 一个普通笔记本就能跑,RNN模型仅28K参数
  • 零图片资源 —— 宠物完全用代码绘制,不需要任何图片素材

最终成品是一个200×280像素的透明窗口,一只矢量风格的卡通猫会"住"在你的桌面上。


二、最终效果

功能 操作方式
左键单击宠物 随机互动,宠物切换对应表情
左键拖拽 移动宠物位置
右键单击宠物 弹出功能菜单,宠物即时反馈表情
系统托盘图标 置顶切换 / 菜单操作

右键菜单功能(每个操作宠物都会呈现不同的动画表情):

  • 打招呼 — 开心/好奇/活泼地回应
  • 喂食 — 开心/好奇地闻食物
  • 摸摸头 — 开心眯眼/好奇张望/甚至犯困
  • 玩耍 — 活泼蹦跳/开心追逐
  • 治疗 — 恢复健康,安逸舒适
  • 改名字 — 自定义宠物名称
  • 查看状态 — 在对话气泡中显示四维属性

宠物会随时间自动衰减属性,并自主学习在什么时候应该做什么。每个互动都会触发即时视觉反馈,让你立刻知道它的"心情"。


三、技术架构

整体设计

用户互动 → 性格系统 → 行为引擎 → 动作输出
                  ↘ 记忆系统 ↗
对话 ← 混合引擎(模板 + Char-RNN)

技术选型

组件 技术 选型原因
桌面窗口 PyQt5 原生透明窗口、系统托盘、事件处理
游戏渲染 pygame-ce 高效的2D图形API,支持SRCALPHA合成
神经网络 numpy 纯手工实现Char-RNN,零ML框架依赖
行为决策 Q-Learning 轻量级强化学习,可解释性强

四、核心实现详解

4.1 矢量风格宠物绘制(非像素)

这是项目的最大亮点之一。宠物不是像素画,而是用代码绘制的矢量风格卡通猫。

实现文件:renderer/sprites.py

核心思路:用一个 _draw_cat_base 函数绘制猫的全部身体,通过 modifiers 字典驱动所有动画状态:

def _draw_cat_base(surf, frame, modifiers):
    # modifiers 控制所有变形参数:
    #   bounce_y    — 垂直弹跳(开心时)
    #   head_tilt   — 头部倾斜(好奇时)
    #   tail_up     — 尾巴抬起(玩耍时)
    #   eye_open    — 眼睛睁开程度(0~1.4)
    #   smile       — 嘴角弧度(正=笑,负=哭)
    #   blush       — 腮红透明度
    #   body_color  — 身体颜色(生病时变灰)
    
    breathe = math.sin(frame * 0.3) * 1.5  # 呼吸动画
    ear_twitch = math.sin(frame * 0.5) * 2  # 耳朵抖动
    
    # 用 pygame 图元组合成一只猫:
    # 椭圆 → 身体/腿
    # 圆   → 头/尾巴尖
    # 多边形 → 耳朵/鼻子
    # 弧线 → 眼睛/嘴巴
    # 抗锯齿线 → 胡须/尾巴
    # 半透明面 → 腮红

8种情绪状态各自定义一组 modifiers 参数:

状态 关键动画 实现方式
idle 呼吸起伏 sin波驱动body垂直微动
happy 上下弹跳 bounce_y = abs(sin(frame*0.5))*6
sad 垂头丧气 head_tilt 负值 + 半闭眼
playful 活泼好动 高弹跳 + 尾巴竖起
hungry 左右张望 head_tilt 正弦摆动
sleepy 打瞌睡 eye_open=0.1 + 缓慢点头
sick 生病萎靡 body_color换为灰白色
curious 好奇歪头 head_tilt 摆动 + 睁大眼

技术要点:腮红效果通过单独创建带逐像素Alpha的Surface实现:

blush_surf = pygame.Surface((12, 8), pygame.SRCALPHA)
pygame.draw.ellipse(blush_surf, (255, 184, 184, 120), (0, 0, 12, 8))

4.2 右键交互菜单

实现文件:desktop/pet_window.py

右键菜单的完整流程:

  1. PyQt5检测到 Qt.RightButton 鼠标事件
  2. 调用 _show_context_menu(globalPos)
  3. 动态创建一个 QMenu,添加7个功能项 + 1个退出项
  4. 菜单采用深色主题样式:
    • 背景 #2D2D36,选中色 #FF9F43(橙色)
    • 圆角边框,完美融入桌面
  5. 点击菜单项触发对应操作:
    • greet / feed / pet / play / heal → 调用 brain.interact()
    • rename → 弹出 QInputDialog 改名字
    • status → 弹窗显示四维属性数值

关键代码片段:

def _show_context_menu(self, global_pos):
    menu = QMenu(self)
    menu.setStyleSheet("""
        QMenu {
            background-color: #2D2D36;
            color: white;
            border: 1px solid #555;
            border-radius: 6px;
            padding: 4px;
        }
        QMenu::item {
            padding: 6px 24px;
            border-radius: 4px;
        }
        QMenu::item:selected {
            background-color: #FF9F43;
        }
    """)
    
    actions = [
        ("打招呼", "greet"), ("喂食", "feed"), ("摸摸头", "pet"),
        ("玩耍", "play"), ("治疗", "heal"), ("改名字", "rename"),
        ("查看状态", "status"),
    ]
    for label, action_type in actions:
        action = QAction(label, self)
        action.triggered.connect(lambda checked, t=action_type: self.on_menu_action(t))
        menu.addAction(action)
    
    menu.addSeparator()
    exit_action = QAction("退出", self)
    exit_action.triggered.connect(self.quit_application)
    menu.addAction(exit_action)
    
    menu.exec_(global_pos)

4.3 PyGame嵌入Qt窗口

这是一个关键的架构决策。最终方案是PyGame只作渲染引擎,Qt负责窗口管理

class PetWindow(QWidget):
    def _setup_pygame(self):
        pygame.font.init()
        self.pygame_surf = pygame.Surface((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.SRCALPHA)
    
    def paintEvent(self, event):
        painter = QPainter(self)
        pixels = pygame.image.tobytes(self.pygame_surf, "RGBA")
        image = QImage(pixels, w, h, QImage.Format_RGBA8888)
        painter.drawImage(0, 0, image)
    
    def _update(self):
        self.brain.tick()
        self.renderer.render(self.pygame_surf, self.brain.current_emotion, dialogue)
        self.update()  # 触发 paintEvent

每次tick:brain更新状态 → renderer绘制到pygame surface → paintEvent将surface转为QImage显示。这样既享受了pygame的绘图API,又获得了Qt的窗口管理能力。

4.4 Q-Learning 行为引擎

宠物拥有一个轻量级的强化学习"大脑":

  • 状态空间:饥饿 × 精力 × 心情 × 健康 × 时间段(5维,各3级离散)= 243种状态
  • 动作空间:7种行为(sleep / play / explore / cuddle / eat / wander / groom)
  • 奖励机制:每个动作根据当前状态获得reward,性格会影响reward权重
  • 探索率:初始20%,每次更新衰减(0.9995),最低5%
def get_reward(self, action, hunger, energy, mood, health):
    reward = 0.0
    if action == "eat":
        reward += 1.0 if hunger > 50 else -0.5
        reward += personality.get("affectionate") * 0.2
    elif action == "play":
        reward += 1.0 if energy > 40 else -1.0
        reward += personality.get("energetic") * 0.5
        reward += personality.get("curious") * 0.3
    # ... 其他动作
    return reward

性格系统是一个5维向量 [affectionate, energetic, curious, stubborn, clingy],初始值 [0.6, 0.5, 0.7, 0.3, 0.4]。每次互动会微调性格参数,且每日变化有上限(±0.1),保证性格变化平滑自然。

4.5 纯NumPy实现的Char-RNN

这是项目中"最硬核"的部分。一个字符级别的循环神经网络,完全用NumPy手动实现前向传播和反向传播。

class CharRNN:
    def __init__(self):
        self.W_embed = np.random.randn(vocab_size, embed_dim) * 0.01
        self.W_xh = np.random.randn(embed_dim, hidden_dim) * 0.01
        self.W_hh = np.random.randn(hidden_dim, hidden_dim) * 0.01
        self.W_hy = np.random.randn(hidden_dim, vocab_size) * 0.01
    
    def train_step(self, inputs, targets, lr):
        # 前向传播
        for t in range(T):
            xs[t] = self.W_embed[inputs[t]]
            hs[t+1] = np.tanh(xs[t] @ self.W_xh + hs[t] @ self.W_hh + self.b_h)
        
        # 反向传播(BPTT)
        for t in reversed(range(T)):
            dy = probs[t:t+1].copy()
            dy[0, targets[t]] -= 1
            # 计算各参数梯度
            dW_hy += hs[t+1].T @ dy
            dh = dy @ self.W_hy.T + dh_next
            # ...
        
        # SGD更新
        for param in ["W_embed", "W_xh", "W_hh", "b_h", "W_hy", "b_y"]:
            setattr(self, param, getattr(self, param) - lr * grad)

模型规格:

  • 词表:70字符(英文字母 + 数字 + 标点)
  • Embedding维度:16
  • 隐藏层维度:64
  • 参数量:~28K
  • 权重文件:~112KB

训练数据是322条带情绪标签的英文短句(<happy>, <sad>, <hungry>等8种标签)。首次启动自动训练60个epoch,loss从3.1降至0.59。

对话时80%概率使用中文模板(保证通顺),20%概率用RNN自由生成(增加惊喜感)。

4.6 记忆系统

记忆采用键值对存储,每条记忆有7天半衰期

def _weight(self, timestamp):
    days = (time.time() - timestamp) / 86400
    return 2.0 ** (-days / 7)  # 7天半衰期
  • 权重低于0.05且访问次数<3的记忆自动清理
  • 频繁访问的记忆即使时间久远也会保留
  • 记忆影响对话内容:“你上次喂我鸡肉好好吃~”

4.7 互动响应情绪系统

宠物在接收到用户操作时,立即切换画面表情,给用户即时的视觉反馈。

实现原理(pet/brain.py):

def interact(self, action_type: str) -> str:
    self.emotion_timer = FPS * 3  # 情绪保持3秒
    # 设置当前情绪和动作,驱动渲染层切换动画
    self.current_emotion, self.current_action = self._pick_emotion([
        ("happy", "happy", 5),    # 50% 概率开心
        ("curious", "curious", 3), # 30% 概率好奇
        ("playful", "playful", 2), # 20% 概率活泼
    ])

每个操作对应多种可能的情绪反馈,使用加权随机选择:

操作 可能情绪(权重)
摸摸头 happy(5) / curious(2) / sleepy(1)
喂食 happy(5) / curious(3) / playful(1)
玩耍 playful(5) / happy(3) / curious(2)
治疗 happy(5) / idle(2) / sleepy(1)
打招呼 happy(5) / curious(3) / playful(2)
责骂 sad(5) / sleepy(2) / idle(1)

情绪切换后持续约3秒(emotion_timer),然后自动恢复为由属性数值决定的基础情绪。这样既保证了操作的即时反馈,又不影响宠物自主行为的连贯性。

tick() 中的关键逻辑:

def tick(self):
    # ... 属性衰减 ...
    if self.emotion_timer > 0:
        self.emotion_timer -= 1  # 持续显示互动情绪
    else:
        self.current_emotion = self._determine_emotion()  # 恢复自动判断

此外,查看状态不再弹出独立窗口,而是显示在宠物的对话气泡中,停留6秒:

elif action_type == "status":
    text = "饥饿:30/100  精力:70/100  心情:60/100  健康:80/100"
    self._show_dialogue(text, duration=FPS * 6)  # 6秒停留

五、遇到的坑与解决方案

1. Pygame-ce字体初始化崩溃

问题:Python 3.14 + pygame-ce 2.5.7 下,调用 pygame.font.SysFont() 会触发Windows字体枚举,但是枚举返回的数据类型与Python 3.14不兼容,导致 splitext 报错 TypeError: expected str, not int

解决:放弃使用 SysFont,改为直接加载Windows字体目录下的具体字体文件:

def _find_chinese_font():
    paths = [
        "C:\\Windows\\Fonts\\simsun.ttc",
        "C:\\Windows\\Fonts\\msyh.ttc",
        "C:\\Windows\\Fonts\\simhei.ttf",
    ]
    for p in paths:
        if os.path.exists(p):
            return pygame.font.Font(p, 14)
    return pygame.font.Font(None, 16)  # 最后兜底

2. RNN训练速度过慢

问题:原始实现中,get_loss方法先调用forward进行前向传播,然后在反向传播中又重复计算了部分前向值,导致每个训练step做了2次前向传播。

解决:重写train_step方法,将前向传播和反向传播合并为一个函数,复用前向计算的结果,训练速度提升了约10倍(200 epoch从>2分钟缩短至约30秒)。

3. 透明窗口的事件穿透

问题:Qt透明窗口默认会让鼠标事件穿透到下层窗口,但我们需要在宠物身上捕获点击。

解决:不设置 WA_TransparentForMouseEvents,在 mousePressEvent 中通过 _is_on_pet() 判断点击位置是否在宠物的渲染矩形内,仅在矩形内处理事件。


六、如何使用

环境要求

  • Python 3.14+
  • 依赖:pygame-ce, PyQt5, numpy

安装运行

pip install pygame-ce PyQt5 numpy
cd ai_pet
python main.py

或双击 run.bat

首次启动会自动训练Char-RNN(约30秒),之后秒开。所有数据保存在 brain_models/data/ 目录,退出时自动保存。


七、项目结构

ai_pet/
├── main.py                      # 程序入口
├── config.py                    # 全局配置参数
│
├── pet/                         # AI 大脑模块
│   ├── brain.py                 # 大脑主控
│   ├── behavior.py              # Q-Learning 行为引擎
│   ├── personality.py           # 5维性格系统
│   ├── memory.py                # 记忆系统
│   └── dialogue.py              # 混合对话引擎
│
├── brain_models/                # 神经网络模块
│   ├── char_rnn.py              # 微型 Char-RNN
│   ├── train.py                 # 训练脚本
│   └── data/                    # 数据持久化
│       ├── corpus.txt           # 训练语料
│       ├── weights.npz          # 训练好的权重
│       ├── q_table.json         # Q-Learning 表
│       ├── memory.json          # 记忆数据
│       └── personality.json     # 性格数据
│
├── renderer/                    # 渲染模块
│   ├── sprites.py               # 矢量精灵绘制
│   └── renderer.py              # 帧动画 + 对话气泡
│
├── desktop/                     # 桌面集成模块
│   ├── pet_window.py            # 透明穿透窗口 + 右键菜单
│   └── tray.py                  # 系统托盘
│
└── assets/                      # 资源目录

八、技术栈总结

组件 技术 用途
游戏循环/渲染 pygame-ce 矢量绘图、帧动画
桌面窗口 PyQt5 透明窗口、系统托盘、右键菜单
神经网络 numpy Char-RNN 训练与推理
行为决策 Q-Learning 状态-动作价值学习
数据格式 JSON + NPZ 配置/状态持久化

九、总结与展望

这个项目用纯Python实现了一个完整的AI桌面宠物系统,核心亮点在于:

  1. 零依赖AI —— RNN完全用NumPy手写,不依赖PyTorch/TensorFlow
  2. 零图片资源 —— 矢量风格绘图,全部用代码生成
  3. 零联网 —— 所有功能离线运行
  4. 可成长 —— Q-Learning让宠物越养越"懂你"

未来可以扩展的方向:

  • 更多互动小游戏(接球、拼图)
  • 多宠物同屏互动
  • 声音反馈系统
  • 自定义外观编辑器

项目完全开源,代码放在 GitHub。如果你也对桌面宠物或轻量AI感兴趣,欢迎Star和PR!


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多人看到用纯Python也能做出这么有趣的AI应用!

更多推荐