<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Product-Engineering on Armstrong Yan</title><link>https://yanqian.github.io/zh/tags/product-engineering/</link><description>Recent content in Product-Engineering on Armstrong Yan</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Thu, 13 Aug 2026 14:08:41 +0800</lastBuildDate><atom:link href="https://yanqian.github.io/zh/tags/product-engineering/index.xml" rel="self" type="application/rss+xml"/><item><title>Hey Jarvis 的困难 II：从 Demo 到 Mac 产品</title><link>https://yanqian.github.io/zh/posts/publish/building-hey-jarvis-mac-product/</link><pubDate>Thu, 13 Aug 2026 14:08:41 +0800</pubDate><guid>https://yanqian.github.io/zh/posts/publish/building-hey-jarvis-mac-product/</guid><description>&lt;p&gt;在 &lt;a href="https://yanqian.github.io/zh/posts/publish/building-hey-jarvis-voice-interaction/" &gt;上一篇&lt;/a&gt; 里，我写了 Hey Jarvis 怎样处理确认音、回声、打断和麦克风交接。&lt;/p&gt;
&lt;p&gt;当这些能力在终端和浏览器里跑通时，我一度觉得产品最困难的部分已经完成了。&lt;/p&gt;
&lt;p&gt;后来我才意识到，那只是证明了一条语音流程可以工作。&lt;/p&gt;
&lt;p&gt;一个 Demo 只需要在我准备好的环境里成功一次。一个产品则必须面对第一次安装、权限拒绝、窗口切换、电脑锁屏、系统睡眠、子进程崩溃和应用退出。它不但要成功，还要在无法成功时停在一个安全、诚实、可以恢复的状态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Demo 证明一件事能发生。产品必须决定其他所有事情发生时怎么办。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="产品化不是给-python-加一个窗口"&gt;
 产品化不是给 Python 加一个窗口
 &lt;a class="heading-link" href="#%e4%ba%a7%e5%93%81%e5%8c%96%e4%b8%8d%e6%98%af%e7%bb%99-python-%e5%8a%a0%e4%b8%80%e4%b8%aa%e7%aa%97%e5%8f%a3"&gt;
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="链接到标题"&gt;&lt;/i&gt;
 &lt;span class="sr-only"&gt;链接到标题&lt;/span&gt;
 &lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;最早的 Hey Jarvis 由 Python 启动。Realtime 模式还需要打开一个 Chrome 页面，让浏览器负责 WebRTC 的麦克风和语音播放。&lt;/p&gt;
&lt;p&gt;这个结构适合开发，因为每一层都可以单独观察和调试。但如果把它直接包装成一个窗口，很多只存在于我电脑里的前提仍然没有消失：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系统里已经安装了正确版本的 Python 和依赖；&lt;/li&gt;
&lt;li&gt;项目目录和 &lt;code&gt;.env&lt;/code&gt; 文件位于预期位置；&lt;/li&gt;
&lt;li&gt;Chrome 页面没有被重复打开；&lt;/li&gt;
&lt;li&gt;API Key 可以由开发环境读取；&lt;/li&gt;
&lt;li&gt;进程异常时，我知道应该去哪里看日志和怎样重新启动。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都不是产品应该交给使用者理解的事情。&lt;/p&gt;
&lt;p&gt;在正式搭建 Mac App 之前，我先做了一个完全隔离的 Tauri 实验。它只回答一个问题：WKWebView 能不能在真实的 Apple Silicon Mac 上完成麦克风授权、Realtime 播放、自然打断、媒体释放，以及把麦克风还给 Python？&lt;/p&gt;
&lt;p&gt;实验成功以后，我没有继续在这份实验代码上追加功能，而是重新建立产品自己的应用外壳。&lt;/p&gt;
&lt;p&gt;这个选择看起来多走了一步，却帮我区分了两个很不一样的问题：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;实验：这条技术路线可不可行？
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;产品：它能否保护秘密、正确失败、恢复运行，并在另一台电脑上重现？
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="三种运行环境三种责任"&gt;
 三种运行环境，三种责任
 &lt;a class="heading-link" href="#%e4%b8%89%e7%a7%8d%e8%bf%90%e8%a1%8c%e7%8e%af%e5%a2%83%e4%b8%89%e7%a7%8d%e8%b4%a3%e4%bb%bb"&gt;
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="链接到标题"&gt;&lt;/i&gt;
 &lt;span class="sr-only"&gt;链接到标题&lt;/span&gt;
 &lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;最终的 Hey Jarvis Mac App 分成三部分：&lt;/p&gt;</description></item><item><title>我做了一个 Hey Jarvis：从睡前的一个问题开始</title><link>https://yanqian.github.io/zh/posts/publish/building-hey-jarvis/</link><pubDate>Thu, 13 Aug 2026 13:55:06 +0800</pubDate><guid>https://yanqian.github.io/zh/posts/publish/building-hey-jarvis/</guid><description>&lt;p&gt;我和老婆晚上睡觉前经常会聊天。&lt;/p&gt;
&lt;p&gt;有时候聊的是一天里发生的事情，有时候会从一件小事跳到历史、科技、健康或者某个我们突然想到的知识点。聊着聊着，总会遇到一些我们都不知道、但又很想马上知道答案的问题。&lt;/p&gt;
&lt;p&gt;这时候，最自然的动作本来应该是直接问一句。&lt;/p&gt;
&lt;p&gt;但现实里的动作通常是：伸手去找手机，解锁，打开一个应用，点进输入框，再把刚才的问题重新组织一遍。等答案出现时，原来的聊天已经被打断了。&lt;/p&gt;
&lt;p&gt;语音助手本来应该最适合这个场景。可我真正想问的问题，Siri 经常回答不了；而更聪明的 AI，又往往住在一个需要主动打开的窗口里。&lt;/p&gt;
&lt;p&gt;我想要的其实很简单：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;躺在床上说一句 “Hey Jarvis”，它就能加入我们的对话，回答刚才的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;于是我开始做 Hey Jarvis。&lt;/p&gt;
&lt;p&gt;功能演示：&lt;a href="https://www.youtube.com/watch?v=PDHQiYzFAXQ&amp;amp;t=9s" class="external-link" target="_blank" rel="noopener"&gt;&lt;strong&gt;中文版：让 Siri 级别的唤醒接上 ChatGPT&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;项目与下载：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/yanqian/hey-jarvis" class="external-link" target="_blank" rel="noopener"&gt;源代码：yanqian/hey-jarvis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/yanqian/hey-jarvis/releases/tag/v0.1.0-internal" class="external-link" target="_blank" rel="noopener"&gt;v0.1.0 INTERNAL-UNSIGNED 内部评估版&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="第一个版本其实很快就能工作"&gt;
 第一个版本，其实很快就能工作
 &lt;a class="heading-link" href="#%e7%ac%ac%e4%b8%80%e4%b8%aa%e7%89%88%e6%9c%ac%e5%85%b6%e5%ae%9e%e5%be%88%e5%bf%ab%e5%b0%b1%e8%83%bd%e5%b7%a5%e4%bd%9c"&gt;
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="链接到标题"&gt;&lt;/i&gt;
 &lt;span class="sr-only"&gt;链接到标题&lt;/span&gt;
 &lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Hey Jarvis 的第一版是一条很直接的 Pipeline，也就是一条串行语音处理流程：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;本地等待 “Hey Jarvis”
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 播放一句确认音
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 录下问题
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 把语音转成文字
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 让模型生成答案
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 把答案转成语音并播放
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 回到唤醒状态
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从功能定义上说，做到这里已经实现了最初的需求。&lt;/p&gt;
&lt;p&gt;我不需要拿起手机。只要说出唤醒词，再问一个问题，它就会把答案讲出来。为了避免所有声音一直上传，唤醒词检测在本地完成；只有被唤醒之后录下的问题，才会进入后面的 AI 流程。&lt;/p&gt;
&lt;p&gt;我还给它加了一些确定性的工具。时间和计算可以在本地完成；天气、汇率和股票价格由明确的数据提供方返回。对于这些会随时间变化的问题，我不希望模型凭记忆猜一个听起来很像真的答案。&lt;/p&gt;
&lt;p&gt;如果目标只是做一段功能演示，项目大概已经可以停在这里。&lt;/p&gt;
&lt;p&gt;但当我真的开始使用它，我发现：&lt;strong&gt;能够用语音问 AI，与拥有一个语音助手，并不是同一件事。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="pipeline-能回答问题但它还不会对话"&gt;
 Pipeline 能回答问题，但它还不会对话
 &lt;a class="heading-link" href="#pipeline-%e8%83%bd%e5%9b%9e%e7%ad%94%e9%97%ae%e9%a2%98%e4%bd%86%e5%ae%83%e8%bf%98%e4%b8%8d%e4%bc%9a%e5%af%b9%e8%af%9d"&gt;
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="链接到标题"&gt;&lt;/i&gt;
 &lt;span class="sr-only"&gt;链接到标题&lt;/span&gt;
 &lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;第一版更像一个由语音启动的命令行程序。&lt;/p&gt;
&lt;p&gt;我问完一个问题，它录音、思考、播放答案，然后整段交互结束。想继续追问，就要再说一次 “Hey Jarvis”。如果回答说得太长，我不能像人与人聊天一样插话；我只能等它讲完。&lt;/p&gt;
&lt;p&gt;人与人对话时，我们很少意识到自己在做这些事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;听到对方回应后，知道他已经准备好听了；&lt;/li&gt;
&lt;li&gt;在对方说话时插入一句，对方会停下来；&lt;/li&gt;
&lt;li&gt;用“那明天呢”继续追问，而不必重新交代上下文；&lt;/li&gt;
&lt;li&gt;聊完说一句“再见”，双方都知道对话结束了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些在人类交流中近乎本能的行为，到了软件里都会变成明确的状态、事件和边界。&lt;/p&gt;</description></item></channel></rss>