Edge Runtime 架构(iOS 端)
PracticeMate 的核心感知与教练循环运行在用户设备上,保证离线可用、低延迟反馈。
核心角色
Edge Runtime 负责三件事:
- 实时感知:摄像头画面 → VL 模型推理 → 结构化信号
- 教练反馈:rubric 模板文案驱动的即时语音提示
- 会话管理:阶段切换、仪式层、即时复盘
所有推理在设备端完成,不依赖网络。
模型配置
| 模型 | 用途 | 大小 | 加载策略 |
|---|---|---|---|
| Qwen2.5-VL-2B | 实时视觉感知(唯一常驻模型) | ~1.2 GB | 会话开始时加载,全程常驻 |
| Qwen3-1.7B | 按需加载(Recap 润色) | ~1.0 GB | 仅在 Instant Recap 需要自然语言润色时按需加载,否则不加载 |
单模型优先原则(TD-012):实时检测循环只加载 Qwen2.5-VL-2B。Coach Voice 的文案来自 Rubric YAML 的模板字符串(alert_message / confirm_message),不经过 LLM 生成。Qwen3-1.7B 仅在用户结束练习后、需要对 Instant Recap 做自然语言润色时才按需加载;如果设备内存不足或模型不可用,Recap 退回规则聚合——用户无感知差异。
MNN 集成
使用 MNN 框架做端侧推理:
- 量化:INT4 量化,平衡精度与内存占用
- 线程:推理线程独立于 UI 主线程,通过 GCD 调度
- 帧采样:不逐帧推理,按 rubric 定义的
detection_interval(通常 2-5 秒)采样关键帧 - 预热:会话开始前预热模型,首帧推理延迟 < 500ms
Camera Quality Gate(摄像头质量门控)
把 ML 难题变成 onboarding 体验。
Camera Quality Gate 是独立可交付价值——即使 VL-2B spike 失败,CQG 仍然完整可用。它使用 Swift + 轻量级 CV(亮度/角度/遮挡检测),不依赖 VL 模型。
检测项
| 检测项 | 方法 | 阈值 |
|---|---|---|
| 亮度 | CIAreaAverage 取平均亮度 | > 0.3(0-1 归一化) |
| 角度 | VNDetectHumanBodyPoseRequest 关键点对称性 | 偏转 < 15° |
| 遮挡 | 关键点可见性置信度 | 核心关键点 > 0.6 |
| 距离 | 人体边界框占画面比例 | 20%-80% 画面高度 |
流程
摄像头预览 → [亮度检测] → [角度检测] → [遮挡检测] → [距离检测]
↓ 任一不通过
语音引导 + 视觉提示("请往后退一步")
↓ 全部通过
✅ 进入练习仪式层CQG 独立可演示:打开 App → 对准摄像头 → 实时看到检测反馈 → 调整到合格。这本身就是一个有价值的 onboarding 体验。
感知栈三层分离(Perception Stack)(TD-015)
端侧感知不是"一个模型做所有事",而是三个独立层级,各自有不同的技术栈和延迟特征:
| 层级 | 技术栈 | 延迟 | 用途 | 降级行为 |
|---|---|---|---|---|
| L0 Camera Quality Gate | Swift + Core Image / Vision | < 30ms | 亮度/角度/遮挡/距离 | 始终可用 |
| L1 Specialized CV | Swift Vision + 轻量规则 | < 50ms | 高频姿态信号(双手位置/按压姿态检测) | L2 不可用时兜底 |
| L2 VL Model | Qwen2.5-VL-2B + MNN | 500ms-2s | 语义理解类信号(按压深度/回弹/通气量) | 不可用时退回 L1 |
层级协作流程
摄像头帧
│
▼
L0: Camera Quality Gate(始终运行)
│ 画面合格
▼
L1: Specialized CV(高频姿态信号)
│ 双手位置/按压姿态等 < 50ms 快速检测
│ 如果 L2 不可用,L1 结果直接写入 events.jsonl
▼
L2: VL Model(语义理解信号)
│ 按 detection_interval 采样(2-5s)
│ 仅处理 L1 无法覆盖的信号
▼
events.jsonl每个 edge_signal 在 rubric YAML 中标注 perception_level: L0 | L1 | L2,Perception Planner 据此决定用哪层处理。
练习仪式层
仪式层为练习提供心理节奏感,让用户从"打开 App"过渡到"进入专注状态"。
进入仪式(Camera Quality Gate 通过后)
- 3 秒聚焦提示:haptic 震动 + Coach Voice 播报
rubric.ritual.focus_prompt(如"深呼吸,准备进入急救状态") - 倒计时动画(3-2-1),结束后正式开始第一阶段
- 目的:给用户一个明确的"开始"信号,区分准备和练习
阶段过渡仪式
- 每次阶段切换时:haptic 震动 + Coach Voice 播报当前阶段的
rubric.phases[].transition_feedback - 短暂过渡动画(0.5 秒),让用户意识到阶段已切换
- 见下方"混合阶段切换"详细流程
结束仪式
- 练习结束时:Instant Recap 优先展示亮点(
rubric.ritual.recap_priority = highlights_first) - 先正向反馈,再改进建议,保持积极收尾
Coach Persona 参数化(TD-019)
Coach Voice 的"人格"不再硬编码,而是由 rubric YAML 中的 coach_persona 配置驱动:
| 参数 | 范围 | 说明 | 默认值 |
|---|---|---|---|
| warmth | 0.0–1.0 | 语气温暖程度 | 0.8 |
| interruptiveness | 0.0–1.0 | 打断频率 | 0.5 |
| praise_frequency | 0.0–1.0 | 正向反馈频率 | 0.7 |
Persona 路由逻辑
信号产出 → 查 coach_persona 参数
│
├─ interruptiveness < 阈值 → 静默记录(不播报)
├─ praise_frequency < 阈值 → 跳过正向确认播报
└─ warmth 影响模板选择 → 高 warmth 选鼓励性模板,低 warmth 选事实性模板Ritual 成熟度(TD-019)
仪式层根据 maturity_level 自动调整深度:
| 级别 | 专注引导 | Coach 语音 | 操作提示 |
|---|---|---|---|
first_time | 3 秒完整引导 | 高频(每个信号) | 显示 |
daily | 1 秒精简 | 中频(仅 high/medium) | 隐藏 |
exam | 无 | 静默(纯记录) | 隐藏 |
混合阶段切换(三路并行)(TD-013)
阶段切换支持三条路径并行,确保不论 AI 检测是否准确,用户始终能顺畅推进练习。
三条路径
路径 A — AI 自动检测(主路径)
- VL-2B 从画面中检测到 rubric 定义的
transition_trigger条件 - 自动触发阶段切换 + haptic 震动 + Coach Voice 播报
transition_feedback transition_source: ai_detected
路径 B — 用户手动推进(常驻兜底)
- 屏幕上始终显示大号"进入下一阶段"按钮
- 用户随时可以点击手动推进,不需要等 AI 判断
transition_source: user_manual
路径 C — 超时提醒(时间兜底)
- 如果当前阶段已超过 rubric 定义的
estimated_minutes - Coach Voice 播报 rubric 中的
timeout_prompt(如"这个阶段已经练习了 5 分钟,要继续还是进入下一步?") - 用户确认后切换,或选择继续当前阶段
transition_source: timeout
流程图
┌─────────────────────────────────────────────────────┐
│ 当前阶段进行中 │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 路径 A │ │ 路径 B │ │ 路径 C │ │
│ │ AI 检测 │ │ 手动按钮 │ │ 超时提醒 │ │
│ │ trigger │ │ 用户点击 │ │ estimated │ │
│ │ 命中 │ │ "下一阶段" │ │ _minutes │ │
│ └────┬─────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └───────────────┼────────────────┘ │
│ ▼ │
│ phase_transition 事件 │
│ { phase_id, transition_source } │
│ │ │
│ ▼ │
│ haptic + transition_feedback 语音 │
│ │ │
│ ▼ │
│ 进入下一阶段 │
└─────────────────────────────────────────────────────┘phase_transition 事件统一包含 transition_source 字段(ai_detected / user_manual / timeout),用于后续数据分析和 rubric 优化。
Perception → Planner 循环
实时检测循环只运行 Qwen2.5-VL-2B:
摄像头帧(按 detection_interval 采样)
│
▼
Qwen2.5-VL-2B 推理
│
▼
结构化输出:{ signal, confidence, detail }
│
├─ confidence ≥ 0.8 → signal_confirmed 事件 → Coach Voice: rubric 模板文案(alert_message / confirm_message)
├─ 0.5 ≤ confidence < 0.8 → medium_confidence 事件 → 静默记录,Recap 中标记 △
└─ confidence < 0.5 → low_confidence 事件 → 静默记录,标记"待考官确认"Coach Voice 文案流程:
- VL-2B 输出结构化信号(如
{ signal: "eye_contact_lost", confidence: 0.85 }) - 用 signal 名查找 rubric YAML 中对应的
alert_message(如 "注意保持眼神交流") - 直接播报模板文案,不经过 LLM
这样做的好处:延迟低(省去 LLM 推理时间)、内存省(不需要加载第二个模型)、文案可控(教练语气在 rubric 中统一管理)。
置信度分层 UX(诚实哲学 TD-011)
这是信任设计,不是技术妥协。用户值得知道 AI 什么时候确定、什么时候不确定。
| 置信度 | UI 表现 | Coach Voice 示例 | 设计意图 |
|---|---|---|---|
| ≥ 0.8(高) | 实时标注 + 即时语音反馈 | "注意保持眼神交流"(rubric alert_message) | 确信时果断提醒 |
| 0.5-0.8(中) | 静默记录,Recap 中以 △ 标记 | (不触发实时语音) | 不确定时不干扰练习 |
| < 0.5(低) | 静默记录 + "待考官确认"徽章 | "这里我不太确定,先记下来,练完再看" | 坦诚不确定,邀请人工复核 |
设计哲学:与其假装全知全能然后翻车,不如坦诚告诉用户"这个我拿不准"。低置信度检测标记为"待考官确认",既保留了信息,又不会误导用户。这种诚实反而建立信任——用户知道当 AI 说"确定"的时候,是真的确定。
会话结束:教练即时复盘(Instant Recap)
Instant Recap 采用规则聚合生成,不依赖 LLM,确保飞行模式下 100% 可用。
生成流程
会话事件流
│
▼
按阶段分组聚合
│
├─ signal_confirmed 事件 → 计数 ✓
├─ medium_confidence 事件 → 计数 △
└─ low_confidence / violation 事件 → 计数 ✗
│
▼
按 rubric.ritual.recap_priority 排序
│
├─ highlights_first: 先展示 ✓(positive_notes),再展示改进建议
│
▼
生成 per-phase 摘要输出格式示例
📋 练习复盘
阶段 1:开场白(2分30秒)
✓ 眼神交流保持良好(8/10 次检测确认)
△ 手势幅度待确认(3 次中置信度)
✗ 语速偏快(2 次检测到)
阶段 2:主体论述(5分10秒)
✓ 站姿稳定(全程确认)
✓ 与听众互动自然
△ 有 1 处停顿较长(待考官确认)
总体:✓ 12 项确认 | △ 4 项待确认 | ✗ 2 项需改进可选:自然语言润色
如果设备内存允许(6GB+)且用户未处于飞行模式,可按需加载 Qwen3-1.7B 对规则聚合结果做自然语言润色:
- 将结构化摘要转化为更自然的教练语气段落
- 添加鼓励性开头和具体改进建议
- 这是锦上添花,不是必需品——规则聚合版本已经完整可用
如果 Qwen3-1.7B 不可用,用户看到的就是规则聚合版本,无降级感知。
Uncertainty Ledger(不确定性账本)(TD-017)
Instant Recap 底部新增"不确定项"折叠区,将散落在 events.jsonl 中的低置信事件产品化为一个清晰的列表。
展示逻辑
events.jsonl 中 alert_tier == "low" 的事件
│
▼
按时间排序,提取:时间戳 + 信号描述 + 教练备注
│
▼
Instant Recap 底部折叠区:
"不确定项(3 项)"
├─ 0:42 天平可能未归零 — "这个角度我看不清"
├─ 1:15 滴速可能偏快 — "不太确定,先记下来"
└─ 2:30 废液处理待确认 — "这里需要考官看一下"
│
[一键请考官裁定] ← 选中项随 events.jsonl 上传Uncertainty Ledger 不是"隐藏不确定",而是"展示诚实"——这是 PracticeMate 与竞品的核心差异化。
电源管理
| 策略 | 实现 |
|---|---|
| 帧采样而非逐帧 | 按 detection_interval 采样,通常 2-5 秒/帧 |
| 推理间歇休眠 | 非采样期间 MNN 线程挂起 |
| 屏幕亮度 | 练习中自动降低非关键区域亮度 |
| 热管理 | 监控 ProcessInfo.thermalState,过热时拉长采样间隔 |
目标:30 分钟练习消耗 < 15% 电量(iPhone 13 基准)。
内存管理
iPhone 13(4GB RAM)
| 占用项 | 内存 |
|---|---|
| Qwen2.5-VL-2B(INT4) | ~1.2 GB |
| Coach Voice 文案 | ~0 MB(rubric 模板字符串,可忽略) |
| Instant Recap | ~0 MB(规则聚合,无模型开销) |
| App + 摄像头 + UI | ~0.8 GB |
| 合计 | ~2.0 GB |
系统保留 ~2.0 GB,舒适运行。单模型架构让 4GB 设备无压力。
iPhone 13 Pro+(6GB RAM)
| 占用项 | 内存 |
|---|---|
| Qwen2.5-VL-2B(INT4,常驻) | ~1.2 GB |
| Qwen3-1.7B(按需加载,仅 Recap 润色) | ~1.0 GB |
| App + 摄像头 + UI | ~0.8 GB |
| 合计 | ~3.0 GB(峰值,Recap 润色时) |
Qwen3-1.7B 仅在会话结束、生成 Instant Recap 润色时按需加载,练习过程中不占内存。
降级策略
| 故障场景 | 降级行为 | 用户感知 |
|---|---|---|
| VL-2B 推理超时(> 3s) | 跳过当前帧,下一采样点重试 | 无感知(采样间隔本身就是几秒) |
| VL-2B 加载失败 | 仅运行 Camera Quality Gate + 手动阶段切换 + 计时器 | 提示"AI 教练暂不可用,手动模式练习" |
| L1 CV 检测降级 | L2 不可用时,L1 独立覆盖安全类信号 | 安全检测可用,语义类信号暂停 |
| Qwen3-1.7B 不可用 | Instant Recap 用规则聚合(默认行为) | 无降级感知——规则聚合就是默认输出 |
| 热节流 | 拉长 detection_interval(如 2s → 5s) | 反馈频率略降,但不中断 |
| 内存压力 | 释放非关键缓存;极端情况暂停推理 | 提示"内存不足,暂停 AI 检测" |
| 摄像头被遮挡 | 暂停检测 + 语音提示"摄像头被遮挡了" | 明确提示 |
核心原则:VL-2B 是唯一关键模型。Qwen3-1.7B 不可用 = 默认行为(规则聚合),不算降级。Camera Quality Gate 独立于模型,始终可用。
Spike 验证计划
| Spike | 验证目标 | 成功标准 |
|---|---|---|
| S-1: CQG 独立演示 | Swift + Vision 框架实现四项检测 | 亮度/角度/遮挡/距离实时反馈,无模型依赖 |
| S-2: MNN + VL-2B 端侧推理 | Qwen2.5-VL-2B 在 iPhone 13 上推理 | 单帧推理 < 2s,内存 < 1.5 GB |
| S-3: 结构化输出解析 | VL-2B 输出 → JSON → rubric 信号匹配 | 解析成功率 > 95% |
| S-4: Coach Voice 模板播报 | rubric 模板文案 → AVSpeechSynthesizer | 延迟 < 200ms(信号到语音开始) |
| S-5: 阶段切换三路并行 | AI 检测 + 手动按钮 + 超时提醒 | 三路均能触发 phase_transition 事件 |
| S-6: Instant Recap 规则聚合 | 事件流 → 按阶段分组 → 格式化输出 | 飞行模式下完整生成,< 500ms |
| S-7: Qwen3-1.7B 按需加载 | 会话结束时加载 → 润色 → 卸载 | 加载 < 5s,润色 < 3s,卸载释放内存 |