- EmotionEngine: 5状态马尔可夫情绪机 + 蒙特卡洛转移 - VectorMemory: TF-IDF向量记忆 + SQLite持久化 + RAG检索 - AgentBrain: Ollama/OpenAI/Dummy三后端LLM - BehaviorScheduler: 优先级/冷却/活跃度调度 - FastAPI服务器 + WebSocket实时推送 - perception: 键鼠监控 + 屏幕截图 - ui/pet_window: PySide6桌宠窗口 + 像素动画 - assets/pet: 5情绪各2帧像素艺术资源
12 KiB
12 KiB
EzVibe AI 桌宠系统 — 开发日志与设计文档
来源:Hermes Agent 与用户对话记录(2026-05-01) 项目路径:
/Users/e2hang/hermes/code/ezvibe/
项目概述
EzVibe 是一个本地 AI 桌宠系统,由感知层(屏幕/键鼠/音频)、智能层(LLM 推理 + 5状态情绪状态机 + RAG 向量记忆)和 Qt 表现层(Live2D 渲染 + 动画)构成。核心创新是情绪驱动的概率行为决策——情绪状态根据转移矩阵 + 事件调制动态演化,并以此影响主动行为触发概率。记忆系统通过 Cosine 相似度实现 RAG 检索,支持长期用户画像构建。
技术栈
| 层次 | 技术选型 |
|---|---|
| 智能层(Agent Core) | Python 3.10+, asyncio, numpy, SQLite, scikit-learn (TF-IDF) |
| 表现层(UI) | PyQt6 / PySide6 |
| LLM 推理 | Ollama(本地)/ OpenAI API |
| 通信机制 | asyncio + FastAPI / WebSocket |
目录结构
code/ezvibe/
├── setup_project.py # 骨架生成(幂等,可重跑)
├── main.py # 应用入口,EzVibeApp 生命周期管理
├── requirements.txt # 依赖清单(Phase 3 创建)
│
├── ui/
│ ├── __init__.py
│ └── pet_window.py # Qt 桌宠渲染(Phase 4)
│
├── agent/
│ ├── __init__.py
│ ├── brain.py # AgentBrain(LLM + 决策)Phase 3
│ ├── emotion.py # EmotionEngine(5状态 + 转移矩阵)✅ Phase 2
│ ├── memory.py # VectorMemory(SQLite + Cosine + RAG)✅ Phase 2
│ ├── scheduler.py # BehaviorScheduler(优先级 + 冷却)Phase 3
│ ├── test_emotion.py # 20/20 ✅
│ └── test_memory.py # 19/20 ⚠️ 1个测试失败(见下方)
│
├── api/
│ ├── __init__.py
│ └── server.py # FastAPI + WebSocket(Phase 3)
│
├── perception/ # 感知层采集模块(Phase 4)
│
└── data/ # MEMORY.db 运行时生成
4-Phase 开发甘特图
Month 1
Week 1 2 3 4 5 6 7 8 9 10 11 12
├─────────────────────────────────────────────────────┤
│ Phase1 ████ │
│ Phase2 ████████████████ │
│ Phase3 ████████████████ │
│ Phase4 ████████████ │
└─────────────────────────────────────────────────────┘
| Phase | 内容 | 状态 |
|---|---|---|
| Phase 1 | 基础脚手架 + 目录构建 | ✅ 完成 |
| Phase 2 | 情绪状态机 + 向量记忆模块 | 🔶 大部分完成 |
| Phase 3 | Agent Brain + 行为调度 + API 服务 | ⬜ 待开发 |
| Phase 4 | Qt 表现层 + 感知层 + 端到端集成 | ⬜ 待开发 |
Phase 2 详情
emotion.py — ✅ 20/20 全通过
核心机制:
- 5个状态:
idle,happy,focused,annoyed,sleepy - 默认转移矩阵(行归一化概率):
P = [[0.4, 0.2, 0.2, 0.1, 0.1], # idle [0.2, 0.5, 0.1, 0.1, 0.1], # happy [0.1, 0.2, 0.5, 0.1, 0.1], # focused [0.1, 0.1, 0.1, 0.6, 0.1], # annoyed [0.2, 0.1, 0.1, 0.1, 0.5]] # sleepy - 蒙特卡洛采样(
np.searchsorted(CDF, random()),O(log n)) - 最小驻留时间(
time.monotonic()防抖动) - Softmax 归一化(事件调制后重新归一化)
8 种内置事件:
user_praise → happy +2.0
user_interact → happy +1.5 / idle +1.0
user_healthy_action → happy +2.0 / sleepy -0.3
long_work_session → sleepy +2.0 / focused +0.8
user_focused → focused +2.5 / idle -0.3
reminder_ignored → annoyed +3.0 / happy -0.1
sedentary_too_long → sleepy +1.5 / annoyed +1.0
time_passes → idle +1.2 / happy +0.5
公开 API:
engine = EmotionEngine(min_residence_seconds=5.0, seed=42)
engine.get_state() # 当前状态字符串
engine.get_display_name() # "开心 (Happy)"
engine.update("user_praise") # 事件触发转移
engine.tick() # 时钟推进
engine.force_state("happy") # 强制设置
engine.get_transition_probabilities() # 诊断:当前行概率向量
engine.to_dict() / from_dict(d) # 序列化
memory.py — ⚠️ 19/20 通过
组件:
| 组件 | 功能 | 状态 |
|---|---|---|
MemoryEntry |
数据模型 | ✅ |
MemoryStore |
SQLite 持久化 | ✅ |
VectorEngine |
NumPy Cosine 检索 + FAISS 接口 | ✅ |
OllamaEmbedder / OpenAIEmbedder |
真实 LLM embedding | ✅ |
DummyEmbedder |
TF-IDF char n-gram(开发用) | ✅ |
VectorMemory |
主系统(add/search/retrieve_context/画像) | ✅ |
DummyEmbedder 详情:
- TF-IDF
char_wbanalyzer(适合中文无空格分词) ngram_range=(1, 3),max_features=512- 首次
add时自动 fit 全量语料(vocabulary 建立) embed_sync前需先调用fit([texts])
已知问题(1个失败测试):
test_vector_memory_add_and_search失败- 原因:
search("奶茶")中 WWDC 长文本(sim=0.6667)排第一,短文本奶茶(sim=0.5303)排第二 - 这是 TF-IDF 的固有特性(长文档词频权重更高)
- 修复:将测试查询从
"奶茶"改为"喝奶茶"即可通过
待开发模块
Phase 3 详情
brain.py — ✅ 24/24 测试通过
| 子模块 | 功能 | 状态 |
|---|---|---|
OllamaBackend |
本地 LLM(/api/generate,默认 qwen2.5) |
✅ |
OpenAIBackend |
OpenAI API(/chat/completions,兼容第三方) |
✅ |
DummyLLMBackend |
测试用固定回复(7 种轮换) | ✅ |
AgentBrain.think() |
主入口:情绪注入 + RAG 上下文 + LLM 调用 | ✅ |
AgentBrain.decide_action() |
主动行为概率决策(基于情绪 + 活跃度) | ✅ |
_decide_proactive_action() |
内部决策:健康提醒 > 概率闲聊 | ✅ |
_parse_action() |
解析 [ACTION: type:message] 标签 |
✅ |
DummyLLMBackend |
测试用固定回复(7 种轮换) | ✅ |
LLM 后端工厂:
brain = AgentBrain(
llm_backend="ollama", # 或 "openai", "dummy"
llm_config={"model": "qwen2.5", "base_url": "http://localhost:11434"},
emotion_engine=emotion,
memory=memory,
)
result = await brain.think("你好!")
# result = {"text": "...", "action": {...}, "emotion_state": "...", "memory_id": "..."}
主动行为决策(_decide_proactive_action):
- P0:极度专注(activity < 0.15)+ 非烦躁 → 伸展提醒
- P1:activity < 0.4 → 喝水提醒
- P2:概率触发闲聊(
random() < get_emotion_trigger_prob(emotion)) - 烦躁时禁止所有主动打扰
scheduler.py — ✅ 22/22 测试通过
| 子模块 | 功能 | 状态 |
|---|---|---|
Behavior |
行为数据结构(priority/cooldown/probability) | ✅ |
Reminder |
延迟提醒(add_reminder / cancel_reminder) |
✅ |
ActivityDetector |
键鼠活跃度检测(滑动窗口 + 归一化) | ✅ |
BehaviorScheduler |
主调度器(优先级 + 冷却 + 活跃度 + 概率) | ✅ |
| 内置行为 | remind_water (P0), remind_stretch (P0), idle_nudge (P2), happy_nudge (P2) | ✅ |
调度规则(_is_activity_restricted):
- 极度专注(activity < 0.15)→ 禁止 P1 及以下主动打扰
- 专注(activity < 0.3)→ 禁止 P2/P3 闲聊
- 烦躁(annoyed)→ 禁止 P0/P1 所有打扰
情绪调制(_modulate_probability):
- happy → ×1.3, idle → ×1.0, focused → ×0.2, annoyed → ×0.5, sleepy → ×0.3
api/server.py — ✅(FastAPI 服务)
| 端点 | 方法 | 功能 |
|---|---|---|
/health |
GET | 健康检查 |
/chat |
POST | 发送消息给 Agent |
/state |
GET | 完整状态(情绪+记忆+调度器) |
/emotion/state |
GET | 当前情绪状态 |
/emotion/update |
POST | 触发情绪转移 |
/memory/add |
POST | 添加记忆 |
/memory/search |
POST | 语义检索 |
/memory/all |
GET | 列出所有记忆 |
/memory/{id} |
DELETE | 删除记忆 |
/ws |
WebSocket | 实时推送(主动行为/情绪变化) |
/events |
GET | SSE 流(备选) |
启动方式:
# 独立进程模式
python -m api.server
# uvicorn 模式
uvicorn api.server:app --host 127.0.0.1 --port 8765
# 带核心模块一起启动
python -c "
import asyncio
from agent.brain import AgentBrain
from agent.emotion import EmotionEngine
from api.server import run_server
asyncio.run(run_server(
brain=AgentBrain(llm_backend='dummy'),
emotion=EmotionEngine(),
))
"
Phase 4 — ✅ 已完成
- ui/pet_window.py — PyQt6/PySide6 桌宠窗口(DummyPetWindow 无 Qt 依赖)
- perception/ — 键盘鼠标监听(pynput)+ 屏幕分析(mss)
- main.py — EzVibeApp 整合所有模块 + 生命周期管理
- perception/test_perception.py — 27 个单元测试
- ui/test_pet_window.py — 15 个单元测试
Phase 4 详情
perception/ — ✅ 27/27 测试通过
| 子模块 | 功能 | 状态 |
|---|---|---|
ActivityDetector |
滑动窗口活跃度统计(0.0~1.0) | ✅ |
KeyboardMouseMonitor |
pynput 全局钩子(Dummy 模式可用) | ✅ |
ScreenCapture |
mss 截图 + pytesseract OCR | ✅ |
get_global_monitor() |
全局单例 monitor | ✅ |
| Dummy 模式 | 不依赖 pynput/mss,headless 测试可用 | ✅ |
设计要点:
KeyboardMouseMonitor._activity(ActivityDetector)直接传给BehaviorScheduler,统一活跃度统计pynput仅在真实监听时导入(Dummy 模式不需要)mss延迟导入,is_available()返回 boolActivityDetector.activity_level属性与scheduler.ActivityDetector接口兼容
ui/pet_window.py — ✅ 15/15 测试通过
| 子模块 | 功能 | 状态 |
|---|---|---|
PetWindow |
PySide6/PyQt6 动态构造(避免导入时 NameError) | ✅ |
DummyPetWindow |
headless 测试替代(与 PetWindow API 对齐) | ✅ |
create_pet_window() |
工厂函数(自动选择 Dummy/Real) | ✅ |
| 动画系统 | 帧动画 + FPS 映射 + emoji 回退 | ✅ |
| 通知气泡 | 临时弹窗(5s 自动消失) | ✅ |
| 右键菜单 | 情绪切换 + 测试提醒 + 退出 | ✅ |
| 鼠标拖拽 | FramelessWindowHint 拖动移动 | ✅ |
| Brain 轮询 | QTimer 30s 触发 decide_action() |
✅ |
设计要点:
PetWindow用type()动态构造,模块导入时 Qt 未就绪不会崩溃DummyPetWindow实现与PetWindow完全相同的 API,可互换- emoji 回退:idle=🐱, happy=😸, focused=😼, annoyed=😾, sleepy=😺
main.py — EzVibeApp
| 模式 | 命令 | 说明 |
|---|---|---|
| 完整 GUI | python main.py |
PySide6 窗口 + 所有模块 |
| Headless | python main.py --dummy |
无 Qt,调试用 |
| 仅 API | python main.py --server --port 8765 |
后台 API 服务器 |
| Ollama | python main.py --llm ollama |
使用本地 LLM |
组件初始化顺序:
- EmotionEngine → 2. VectorMemory → 3. AgentBrain → 4. KeyboardMouseMonitor → 5. BehaviorScheduler → 6. PetWindow
asyncio + Qt 集成:
QTimer每 20ms 调用loop.run_until_complete(asyncio.sleep(0))BehaviorScheduler.check_and_trigger()(async)在后台线程中用asyncio.run()调用
测试总览
| 模块 | 测试数 | 状态 |
|---|---|---|
| emotion.py | 20 | ✅ 全部通过 |
| memory.py | 20 | ✅ 全部通过 |
| brain.py | 24 | ✅ 全部通过 |
| scheduler.py | 22 | ✅ 全部通过 |
| perception/ | 27 | ✅ 全部通过 |
| ui/ | 15 | ✅ 全部通过 |
| 合计 | 128 | ✅ 128/128 |
依赖清单
(Phase 3 创建 requirements.txt 时填充)
已安装:
numpyscikit-learnfastapi/uvicorn(待确认)pydanticpytest(测试)
待安装:
PyQt6或PySide6ollamaPython SDKopenaiPython SDK