从零实现一个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
右键菜单的完整流程:
- PyQt5检测到
Qt.RightButton鼠标事件 - 调用
_show_context_menu(globalPos) - 动态创建一个
QMenu,添加7个功能项 + 1个退出项 - 菜单采用深色主题样式:
- 背景
#2D2D36,选中色#FF9F43(橙色) - 圆角边框,完美融入桌面
- 背景
- 点击菜单项触发对应操作:
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"),]forlabel,action_typeinactions:action=QAction(label,self)action.triggered.connect(lambdachecked,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负责窗口管理:
classPetWindow(QWidget):def_setup_pygame(self):pygame.font.init()self.pygame_surf=pygame.Surface((WINDOW_WIDTH,WINDOW_HEIGHT),pygame.SRCALPHA)defpaintEvent(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%
defget_reward(self,action,hunger,energy,mood,health):reward=0.0ifaction=="eat":reward+=1.0ifhunger>50else-0.5reward+=personality.get("affectionate")*0.2elifaction=="play":reward+=1.0ifenergy>40else-1.0reward+=personality.get("energetic")*0.5reward+=personality.get("curious")*0.3# ... 其他动作returnreward性格系统是一个5维向量[affectionate, energetic, curious, stubborn, clingy],初始值[0.6, 0.5, 0.7, 0.3, 0.4]。每次互动会微调性格参数,且每日变化有上限(±0.1),保证性格变化平滑自然。
4.5 纯NumPy实现的Char-RNN
这是项目中"最硬核"的部分。一个字符级别的循环神经网络,完全用NumPy手动实现前向传播和反向传播。
classCharRNN:def__init__(self):self.W_embed=np.random.randn(vocab_size,embed_dim)*0.01self.W_xh=np.random.randn(embed_dim,hidden_dim)*0.01self.W_hh=np.random.randn(hidden_dim,hidden_dim)*0.01self.W_hy=np.random.randn(hidden_dim,vocab_size)*0.01deftrain_step(self,inputs,targets,lr):# 前向传播fortinrange(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)fortinreversed(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更新forparamin["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)/86400return2.0**(-days/7)# 7天半衰期- 权重低于0.05且访问次数<3的记忆自动清理
- 频繁访问的记忆即使时间久远也会保留
- 记忆影响对话内容:“你上次喂我鸡肉好好吃~”
4.7 互动响应情绪系统
宠物在接收到用户操作时,立即切换画面表情,给用户即时的视觉反馈。
实现原理(pet/brain.py):
definteract(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()中的关键逻辑:
deftick(self):# ... 属性衰减 ...ifself.emotion_timer>0:self.emotion_timer-=1# 持续显示互动情绪else:self.current_emotion=self._determine_emotion()# 恢复自动判断此外,查看状态不再弹出独立窗口,而是显示在宠物的对话气泡中,停留6秒:
elifaction_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",]forpinpaths:ifos.path.exists(p):returnpygame.font.Font(p,14)returnpygame.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
安装运行
pipinstallpygame-ce PyQt5 numpycdai_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桌面宠物系统,核心亮点在于:
- 零依赖AI—— RNN完全用NumPy手写,不依赖PyTorch/TensorFlow
- 零图片资源—— 矢量风格绘图,全部用代码生成
- 零联网—— 所有功能离线运行
- 可成长—— Q-Learning让宠物越养越"懂你"
未来可以扩展的方向:
- 更多互动小游戏(接球、拼图)
- 多宠物同屏互动
- 声音反馈系统
- 自定义外观编辑器
项目完全开源,代码放在 GitHub。如果你也对桌面宠物或轻量AI感兴趣,欢迎Star和PR!
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多人看到用纯Python也能做出这么有趣的AI应用!