- 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>
225 lines
7.3 KiB
Markdown
225 lines
7.3 KiB
Markdown
# 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()` 接口 | |