# 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 初始化 **代码示例**: ```python # 旧方案(有问题) 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` 负责对话触发的闲聊行为(P3),`scheduler` 负责定时触发的健康提醒(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**:回调式设计,易于使用 **替代方案**: - `pyhook`(Windows only) - `keyboard`(仅键盘,无鼠标) - `appnope`(macOS only) **结论**:pynput 是当前最优解。 --- ## 5. 未来决策预留 以下决策暂未落地,但在代码中已预留接口: | 决策 | 预留方式 | |------|----------| | FAISS 向量检索 | `VectorEngine(mode="faiss")` 接口已预留 | | 多后端 LLM | `LLMBackend` 策略模式,支持 Ollama/OpenAI/Dummy | | 多语言 | System Prompt 支持中文,当前固定 `zh_CN` | | 番茄钟模式 | `EmotionState` 预留 `TOMATO` 状态(未启用) | | 系统通知 | `show_reminder()` 预留 `show_system_notification()` 接口 |