我和老婆晚上睡觉前经常会聊天。

有时候聊的是一天里发生的事情,有时候会从一件小事跳到历史、科技、健康或者某个我们突然想到的知识点。聊着聊着,总会遇到一些我们都不知道、但又很想马上知道答案的问题。

这时候,最自然的动作本来应该是直接问一句。

但现实里的动作通常是:伸手去找手机,解锁,打开一个应用,点进输入框,再把刚才的问题重新组织一遍。等答案出现时,原来的聊天已经被打断了。

语音助手本来应该最适合这个场景。可我真正想问的问题,Siri 经常回答不了;而更聪明的 AI,又往往住在一个需要主动打开的窗口里。

我想要的其实很简单:

躺在床上说一句 “Hey Jarvis”,它就能加入我们的对话,回答刚才的问题。

于是我开始做 Hey Jarvis。

功能演示:中文版:让 Siri 级别的唤醒接上 ChatGPT

项目与下载:

第一个版本,其实很快就能工作 链接到标题

Hey Jarvis 的第一版是一条很直接的 Pipeline,也就是一条串行语音处理流程:

本地等待 “Hey Jarvis”
→ 播放一句确认音
→ 录下问题
→ 把语音转成文字
→ 让模型生成答案
→ 把答案转成语音并播放
→ 回到唤醒状态

从功能定义上说,做到这里已经实现了最初的需求。

我不需要拿起手机。只要说出唤醒词,再问一个问题,它就会把答案讲出来。为了避免所有声音一直上传,唤醒词检测在本地完成;只有被唤醒之后录下的问题,才会进入后面的 AI 流程。

我还给它加了一些确定性的工具。时间和计算可以在本地完成;天气、汇率和股票价格由明确的数据提供方返回。对于这些会随时间变化的问题,我不希望模型凭记忆猜一个听起来很像真的答案。

如果目标只是做一段功能演示,项目大概已经可以停在这里。

但当我真的开始使用它,我发现:能够用语音问 AI,与拥有一个语音助手,并不是同一件事。

Pipeline 能回答问题,但它还不会对话 链接到标题

第一版更像一个由语音启动的命令行程序。

我问完一个问题,它录音、思考、播放答案,然后整段交互结束。想继续追问,就要再说一次 “Hey Jarvis”。如果回答说得太长,我不能像人与人聊天一样插话;我只能等它讲完。

人与人对话时,我们很少意识到自己在做这些事情:

  • 听到对方回应后,知道他已经准备好听了;
  • 在对方说话时插入一句,对方会停下来;
  • 用“那明天呢”继续追问,而不必重新交代上下文;
  • 聊完说一句“再见”,双方都知道对话结束了。

这些在人类交流中近乎本能的行为,到了软件里都会变成明确的状态、事件和边界。

比如,Hey Jarvis 播放“在呢”时,麦克风也会收录这句“在呢”。处理不当,它可能把自己的声音当成用户已经开口;如果简单丢掉这一段声音,用户提前说出的第一个字也可能一起消失。

再比如,界面显示“正在听”,不代表它真的已经准备好听我说话。网络连接、实时对话配置、确认音播放和麦克风输入开放,必须按正确顺序完成。否则我以为它在听,实际上说出的话根本没有进入系统。

这些问题让我意识到,模型反而是整个系统里比较省心的一部分。真正决定体验的,是模型周围那套不容易被看见的系统。

从一次问答,走向一次真正的谈话 链接到标题

于是我做了第二条后端:实时语音会话(Realtime)。

在新的流程里,本地唤醒仍然是入口。听到 “Hey Jarvis” 以后,麦克风从本地唤醒程序交给实时会话。接下来,我们可以连续追问,也可以在回答过程中直接插话。说完“再见”以后,会话关闭,麦克风重新回到本地唤醒程序手里。

本地唤醒
→ 交出麦克风
→ 建立实时语音会话
→ 确认输入已经可用
→ 连续对话 / 追问 / 打断
→ 结束会话
→ 释放浏览器音频
→ 恢复本地唤醒

这里最重要的变化,并不是把一个 API 换成另一个 API。

Pipeline 的基本单位是“一次问题”;实时语音会话的基本单位是“一段对话”。

这让 Hey Jarvis 第一次接近了我最初想要的样子:它不是等我把每个问题都包装成一条新指令,而是短暂地加入我们正在进行的聊天。

能对话以后,它仍然只是一个开发项目 链接到标题

实时语音会话解决了交互方式,却没有解决日常使用。

那时的 Hey Jarvis 仍然需要从终端启动,还依赖浏览器承载 WebRTC。API Key、麦克风权限、进程退出和故障恢复都由开发者自己理解。对于我来说,这些操作可以接受;但如果我希望把它交给信任的朋友试用,就不能要求每个人先理解 Python 环境和启动命令。

所以我又开始把它做成一个真正的 Mac App。

最终,我用原生应用管理窗口、权限、凭证和进程,网页层承载实时语音,Python 后台进程保留已经验证过的本地唤醒与对话逻辑。重点不是堆技术栈,而是让每一种敏感能力都有明确的主人。

API Key 保存在 macOS Keychain;唤醒前的音频留在本地。应用退出、系统睡眠或者会话出错时,媒体和后台进程必须在限定时间内关闭。即使恢复失败,界面也应该诚实地显示“没有在监听”,而不是给出一个虚假的绿色状态。

做到这里,我才觉得它开始从一个能运行的程序,变成了一个产品。

今天的 Hey Jarvis 是什么 链接到标题

现在的 Hey Jarvis 是一个 local-first、BYOK 的 macOS 语音助手:唤醒检测优先在本地完成,使用者提供并掌握自己的 API Key。

它可以在本地等待 “Hey Jarvis”,被唤醒后进入连续的中英文语音对话,支持自然追问和打断,也可以查询时间、天气、汇率、股票以及完成安全的计算。应用提供了 Keychain 凭证管理、麦克风权限恢复、脱敏诊断、睡眠与唤醒恢复,以及一个面向 Apple Silicon Mac 的内部测试版本。

它也有非常明确的边界。

目前的 DMG 已经通过 GitHub Release 公开提供,方便知情的测试者下载和评估,但它没有 Developer ID 签名和 notarization,仍然不是面向普通消费者的正式版本。它需要使用者提供自己的 API Key,没有账号系统和自动更新。它不是一个已经准备商业化的 SaaS,也没有因为做出了这些功能就天然拥有护城河。

但对我来说,这个项目的价值本来就不只在于“再做一个语音助手”。

它来自一个真实的小需求,也让我完整经历了一个想法怎样变成 Pipeline,怎样因为实际使用暴露出新的问题,怎样演化成实时对话系统,最后又怎样面对原生应用、安全、打包和可靠性。

我原本以为难点会是 AI 链接到标题

刚开始时,我以为最难的问题会是:怎样让 AI 更聪明,怎样写出更好的 Prompt,怎样接入更多工具。

真正做下去以后,我遇到最多的问题却是:

  • 谁在这一刻拥有麦克风?
  • 用户什么时候可以开始说话?
  • 怎样既过滤助手自己的声音,又不丢掉用户的第一个字?
  • 怎样允许用户打断,同时不让扬声器回声触发假打断?
  • 电脑锁屏、睡眠或打开 Settings 后,系统还是否真的在监听?
  • 一个后台线程正在退出时,谁还能重新打开音频设备?
  • 怎样把 Python、模型和原生依赖交付给另一台没有开发环境的 Mac?
  • 出现问题时,怎样留下足够的诊断信息,又不记录用户说了什么?

模型决定 Hey Jarvis 能回答什么。

这些问题决定了我是否敢在每天晚上真正打开它。

我最初只是想解决一个很小的问题:睡前聊天时,不用拿起手机,也能随口问一个问题。Pipeline 很快证明这件事可以做到。但继续做下去之后,我才看到一个“能回答问题的程序”与一个“可以长期留在身边的助手”之间,还有多远。

下一篇,我会先写其中最直接的一部分:怎样让一段语音交互真正成立。


Building Hey Jarvis 系列:

  1. 我做了一个 Hey Jarvis:从睡前的一个问题开始
  2. Hey Jarvis 的困难 I:让语音交互真正成立
  3. Hey Jarvis 的困难 II:从 Demo 到 Mac 产品
  4. Hey Jarvis 的未来:当 AI 需要自己的入口