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

225 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()` 接口 |