本文基于 Sandeep Aggarwal 的 Jarvis 项目 整理为技术方案文档。
1. 概述与目标
1.1 项目背景
需要构建一个完全运行在本地 Mac 上的语音 AI 助手,具备持续聆听、唤醒词激活、任务执行和打断处理能力。核心约束条件为:
- 隐私:语音数据不离开本地设备
- 延迟:端到端响应延迟需支持自然对话节奏
- 成本:零云端 API 调用费用
1.2 运行环境
| 项目 | 规格 |
|---|---|
| 硬件 | MacBook Air M2, 24 GB 统一内存 |
| 操作系统 | macOS (Apple Silicon) |
| 推理框架 | MLX (Apple 机器学习框架) |
| 语言模型 | Qwen3.5-4B-MLX-4bit |
| 编程语言 | Python 3 (asyncio) |
2. 系统架构
2.1 架构总览
系统由 7 个核心组件构成,通过 Python asyncio 事件循环串联为流式管线:
架构图:0916-1365294-d3a110-arch.html(独立 HTML 文件,浏览器打开可查看完整架构图并导出 PNG/PDF)
2.2 组件职责
| 组件 | 职责 | 关键技术 |
|---|---|---|
| WakeWordDetector | 监听唤醒词 “Jarvis”,触发激活 | 本地关键词检测 |
| SpeechListener | 持续采集音频,执行 AEC 和 VAD | AEC: 内置音频运行时;VAD: 静音/噪声过滤 |
| STT Engine | 将语音实时转写为文字 | RealtimeSTT + faster-whisper |
| TaskManager | 管理任务队列、优先级、取消令牌 | asyncio 队列 + cancellation tokens |
| InterruptionHandler | 对新语音进行分类,决定打断策略 | 5 种动作:IGNORE / QUEUE / CANCEL_AND_RUN / MERGE / MODIFY_QUEUED |
| LLM Engine | 理解指令、选择工具、生成响应 | MLX + Qwen3.5-4B-MLX-4bit |
| TTS Worker | 将文本响应合成为语音输出 | 专用 TTS 工作线程 |
2.3 数据流
麦克风输入
→ SpeechListener(AEC 消除回声 → VAD 检测语音段)
→ STT Engine(语音 → 文本)
→ TaskManager(入队 / 打断判定)
→ LLM Engine(理解意图 → 工具调用 → 生成回复)
→ TTS Worker(文本 → 语音波形)
→ 扬声器输出
打断处理为并行路径:用户新语音被 SpeechListener 捕获后,InterruptionHandler 根据当前任务状态决定合并、取消、排队或忽略。
3. 核心组件设计
3.1 LLM 引擎
模型选型
在 24 GB M2 MacBook Air 上的测试结论:
| 候选模型 | 判定 | 原因 |
|---|---|---|
| Qwen3.5-4B-MLX-4bit | 选用 | 速度与能力的最佳平衡,约 30 token/s |
| 更大参数模型 | 不选用 | 显存不足,响应延迟不可接受 |
| 更小模型 | 不选用 | 理解能力不足,无法可靠完成任务 |
部署命令
mlx_lm.server --model "mlx-community/Qwen3.5-4B-MLX-4bit" \
--max-tokens 20000 \
--chat-template-args '{"enable_thinking":false}'
关键参数说明:
--max-tokens 20000:足够的上下文窗口以支持多轮对话--chat-template-args '{"enable_thinking":false}':禁用 Qwen 默认的推理链(Chain-of-Thought),该特性虽有助于复杂推理,但会显著增加响应延迟,不适合语音交互场景
性能指标
- 平均推理速度:约 30 token/s
- 感知延迟:对话自然流畅,无明显等待感
系统提示词设计
语音助手场景下,系统提示词应极致精简。每个多余 token 都会增加首 token 延迟。
You are Jarvis, a voice-controlled personal assistant.
Answer using tools. Call speak_async once with the final response.
资源约束
- 严禁多实例:
mlx_lm.server是资源密集型进程,同时运行多个实例会导致端口冲突、内存膨胀、响应错乱。切换模型时须先终止当前实例再启动新实例。
3.2 音频管线
声学回声消除(AEC)
问题:扬声器输出的助手语音被麦克风重新捕获,形成自激反馈循环。
方案:使用操作系统音频运行时内置的 AEC 模块。
配置:
| 参数 | 值 |
|---|---|
| 采样率 | 16,000 Hz |
| 帧大小 | 160 |
| 流延迟 | 10 ms |
语音活动检测(VAD)
目的:区分有效语音与静音/背景噪声,避免将噪声送入 STT 管线造成资源浪费和错误转录。
机制:检测语音段的起止边界,仅在检测到有效语音时触发 STT。
语音转文字(STT)
现状:不存在开箱即用、完美无缺的免费本地 STT 模型。
选型:RealtimeSTT(封装 faster-whisper),在准确性和推理速度之间取得了可接受的平衡。
实践建议:较长的自然语句转录准确率显著高于短促短语。例如:
- 避免:“孟买天气?”
- 推荐:“Jarvis,孟买今天的天气怎么样?”
3.3 任务管理与中断处理
中断处理模型
语音助手区别于"录音机"式逐条执行的关键能力。当用户在执行中发出新指令时,系统必须判定新指令的意图。
五种中断动作:
| 动作 | 触发条件 | 系统行为 |
|---|---|---|
| IGNORE | 识别为背景闲聊 | 丢弃,继续当前任务 |
| QUEUE | 独立的新请求 | 追加到待处理队列 |
| CANCEL_AND_RUN | 用户明确要求切换 | 取消当前任务,立即执行新任务 |
| MERGE | 修改当前请求的参数 | 更新当前任务上下文,继续执行 |
| MODIFY_QUEUED | 编辑队列中的等待任务 | 直接修改队列中指定任务的参数 |
实现机制:
- 基于 Python
asyncio的取消令牌(cancellation token)实现任务中断 - 优先级队列管理任务调度
- InterruptionHandler 作为独立组件,在任务运行期间持续对新语音进行分类
典型交互场景
用户: “帮我找几家德里的好餐厅。”
→ TaskManager: 创建任务 T1,状态 RUNNING
→ LLM: 开始搜索工具调用...
用户: “算了,改成孟买吧。”
→ SpeechListener: 捕获新语音
→ STT: 转写为文本
→ InterruptionHandler: 分类为 MERGE
→ TaskManager: 向 T1 发送取消信号,创建新任务 T2(孟买 + 好餐厅)
→ LLM: 重新执行
3.4 开发方法论
慢速 LLM 的开发价值
开发阶段使用较慢的模型反直觉地有利于系统质量:
- 响应延迟提供观察窗口,可检查提示词构建、工具选择、参数生成的正确性
- 迫使在提示词工程、工具设计、任务管理和打断逻辑上做更精细的设计
- 快速模型中容易被忽略的边界条件和竞态问题更易暴露
4. 关键设计决策与权衡
| 决策 | 选择 | 替代方案 | 权衡 |
|---|---|---|---|
| 推理框架 | MLX | llama.cpp, Ollama | Apple Silicon 原生优化,性能最优 |
| 模型 | Qwen3.5-4B-4bit | Llama 3, Mistral | 4B 在 24GB M2 上速度与能力的平衡点 |
| thinking 模式 | 禁用 | 启用 | 牺牲复杂推理能力换取低延迟 |
| STT 方案 | RealtimeSTT | WhisperKit, 云端 API | 实时性优先,接受准确率非最优 |
| 系统提示词 | 极简 2 句 | 详细角色描述 | 牺牲角色丰富度换取更低延迟 |
| 架构模式 | 单体 asyncio 进程 | 微服务 / 多进程 | 简化部署,但单点故障风险 |
5. 性能指标
| 指标 | 数值 | 备注 |
|---|---|---|
| LLM 推理速度 | ~30 token/s | M2, 24GB, Qwen3.5-4B-4bit |
| STT 实时性 | 实时 | 流式转录 |
| 端到端延迟 | < 2 秒 | 含语音采集到 TTS 输出 |
| 内存占用 | < 20 GB | 含 MLX 服务器进程 |
6. 依赖与参考资源
| 资源 | 用途 |
|---|---|
| SandeepAggarwal/Jarvis | 参考实现源码 |
| ml-explore/mlx | Apple Silicon 机器学习框架 |
| mlx-community/Qwen3.5-4B-MLX-4bit | 量化模型 |
| ml-explore/mlx-examples | mlx_lm.server 部署文档 |
| KoljaB/RealtimeSTT | 实时语音转文字库 |