Files
EzVibe/docs/v2_design/DESIGN_DECISIONS.md
e2hang 96cb28fe08 feat: multi-layer eventFilter for window drag + text input fix + Qt platform xcb
- Fix window drag: install eventFilter on live2d_container, central, and
  _live2d_widget; fix super() call in dynamic class
- Fix text input: remove WA_TransparentForMouseEvents from _chat_container
- Force QT_QPA_PLATFORM=xcb on Linux (wayland has mouse event issues)
- Add HealthTracker module, update AgentBrain with health integration
- Update scheduler and memory modules
- Add v5_modify documentation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 23:36:58 +08:00

7.3 KiB
Raw Permalink Blame History

EzVibe 关键设计决策记录

本文档记录 EzVibe 项目中的关键设计决策及其理由。


1. 技术栈选择

1.1 为什么用 SQLite + NumPy 而非 ChromaDB

决策:使用 MemoryStore(SQLite) + VectorEngine(NumPy) 实现向量检索。

原因

  • 轻量ChromaDB 需要额外的服务端进程SQLite 是单文件零依赖
  • 隐私:所有数据存储在本地,不经过任何第三方服务
  • 可控:向量维度、索引策略完全可控,便于调试
  • 测试友好SQLite 路径可配置,单元测试使用内存数据库 :memory:

备选方案考虑

  • ChromaDB功能更完善但增加了运行时复杂度
  • FAISS大规模场景性能更好但当前规模< 1000 条记忆NumPy 足够

结论:当前阶段 SQLite + NumPy 是最优解FAISS 作为预留(vector_mode="faiss" 接口已预留)。


1.2 为什么用 TF-IDF DummyEmbedder 而非 Ollama

决策DummyEmbedder 使用 TF-IDF 实现,开发者/测试环境不依赖 Ollama 服务。

原因

  • 离线可用:开发者在没有运行 Ollama 的情况下也能跑测试
  • 启动快:不需要等待 Ollama 服务启动
  • 确定性TF-IDF 输出固定(给定相同语料库),有利于单元测试复现

限制

  • TF-IDF 是词袋模型,不理解语义("奶茶"和"咖啡"在向量空间可能很远)
  • 不适用于生产环境(请使用 OllamaEmbedder 或 OpenAIEmbedder

结论:开发测试用 Dummy生产部署用 Ollama/OpenAI。接口已统一为 EmbedderBase


1.3 为什么延迟构造 PetWindow 类

决策ui/pet_window.py 使用 type() 动态构造 PetWindow 类,而非模块级导入。

原因

  • Dummy 模式兼容headless 测试不导入 Qt避免 ImportError
  • Qt 版本兼容PySide6 和 PyQt6 API 略有差异,延迟构造可以在运行时检测
  • 避免循环导入:模块级导入 from PySide6 import QtWidgets 会触发 Qt 初始化

代码示例

# 旧方案(有问题)
from PySide6 import QtWidgets  # 模块级导入PySide6 不可用时直接崩溃

# 新方案(当前)
def _make_pet_window_class():
    if _load_qt() is None:
        raise ImportError("需要 PySide6 或 PyQt6")
    # 动态构造...

PetWindow = None  # 占位符

def create_pet_window(...):
    global PetWindow
    if force_dummy or _load_qt() is None:
        return DummyPetWindow(...)
    if PetWindow is None:
        PetWindow = _make_pet_window_class()
    return PetWindow(...)

2. 架构设计

2.1 为什么分离感知层和调度器中的 ActivityDetector

现状:感知层(KeyboardMouseMonitor._activity)和调度器(BehaviorScheduler._activity)各有一个 ActivityDetector 实例。

原因

  • 解耦:感知层负责数据采集,调度器负责行为决策,职责分离
  • 独立测试:两个组件可以独立单元测试

问题

  • 两处逻辑略有差异(窗口大小、基准计算)
  • 可能导致数据不一致

改进方向(见 EVOLUTION_ROADMAP.md

  • 统一使用感知层的单例 get_global_monitor()._activity
  • 调度器引用而非独立实例

2.2 为什么 brain 和 scheduler 都有主动行为决策

现状

  • AgentBrain.decide_action():对话中触发的主动行为
  • BehaviorScheduler.check_and_trigger():定时调度触发的主动行为

原因

  • 触发源不同brain 响应用户输入scheduler 响应时间/活跃度
  • 优先级不同scheduler 的 P0 提醒需要打断brain 的行为是 P3

问题

  • 边界模糊,可能导致行为重复触发
  • decide_action()check_and_trigger() 可能同时返回同一个行为

改进方向

  • 明确分工:brain 负责对话触发的闲聊行为P3scheduler 负责定时触发的健康提醒P0/P1
  • 共享冷却时间:两个模块的行为共享 _action_cooldown

2.3 为什么不使用消息队列中间件

决策:使用 Python 内置 queue.Queue 而非 Redis/RabbitMQ。

原因

  • 轻量:不需要额外的进程和服务
  • 本地优先:桌宠是本地应用,不需要分布式架构
  • 延迟可接受:事件传递延迟 < 10ms完全满足需求

结论:当前规模不需要消息队列中间件。如果未来需要跨进程通信,可以引入 asyncio.Queue


3. 行为设计

3.1 为什么用蒙特卡洛采样而非确定性转移

决策EmotionEngine 使用蒙特卡洛随机采样选择下一状态。

原因

  • 自然感:桌宠不会每次都以相同方式反应,增加变化和惊喜
  • 符合文档:设计文档明确要求"概率转移"
  • 可调节:随机种子可配置,单元测试可复现

代价

  • 行为不完全可预测
  • 可能导致情绪频繁切换(已通过 min_residence_seconds 缓解)

3.2 为什么 annoyed 状态禁止 P0 打扰

现状emotion == "annoyed" and behavior.priority <= 1 → 禁止打扰

问题

  • 用户连续工作 3 小时不休息,情绪可能变为 annoyed
  • 此时屏蔽久坐提醒,失去健康监测意义

改进方向:见 ARCHITECTURE_REVIEW.md 的"柔性降级"方案。


3.3 为什么用 [ACTION: type:description] 标签

决策:从 LLM 回复中解析 [ACTION: type:description] 标签来触发主动行为。

原因

  • 灵活性LLM 可以根据上下文决定是否触发行为、触发什么行为
  • 可读性:标签格式人类可读,便于调试
  • 解耦:行为决策在 LLM 端,行为执行在本地端

限制

  • 依赖 LLM 按格式输出(可能不听话)
  • 正则解析可能匹配到用户输入中的类似文本

改进方向

  • 严格模式LLM 必须按格式输出,否则忽略
  • 容错模式:优先匹配 [ACTION: 开头的内容

4. 性能与资源

4.1 为什么 OCR 使用 pytesseract 而非云服务

决策OCR 使用本地 pytesseract,不依赖云服务。

原因

  • 隐私:屏幕截图不经过任何第三方
  • 离线可用:不需要网络连接
  • 成本:无 API 调用费用

代价

  • CPU 密集,电池消耗
  • 准确率不如云端服务

改进方向:用焦点窗口提取替代全屏 OCR见 ARCHITECTURE_REVIEW.md


4.2 为什么用 pynput 而非其他监听方案

决策:使用 pynput 实现全局键鼠监听。

原因

  • 跨平台:支持 Windows/macOS/Linux
  • 活跃维护pynput 是当前最活跃的全局监听库
  • 简单 API:回调式设计,易于使用

替代方案

  • pyhookWindows only
  • keyboard(仅键盘,无鼠标)
  • appnopemacOS only

结论pynput 是当前最优解。


5. 未来决策预留

以下决策暂未落地,但在代码中已预留接口:

决策 预留方式
FAISS 向量检索 VectorEngine(mode="faiss") 接口已预留
多后端 LLM LLMBackend 策略模式,支持 Ollama/OpenAI/Dummy
多语言 System Prompt 支持中文,当前固定 zh_CN
番茄钟模式 EmotionState 预留 TOMATO 状态(未启用)
系统通知 show_reminder() 预留 show_system_notification() 接口