开源语音桌面助手的架构与踩坑
小蜻蜓将语音交互、3D角色、桌面工具和多代理整合进Electron客户端,以Fastify服务承载代理、授权和模型网关。项目已开源,主要功能在本地运行,但大语言模型仍依赖云端。
AI 深度解读
给桌面助手接上语音、工具和 3D 形象,难点很快就会从「模型会不会回答」变成「进程卡住怎么办、工具能碰什么、动画为什么把角色转过去」。Xiaoqingting 的作者分享了几个月开发中的架构取舍,以及几个耗掉整晚的故障。
这份经验值得读,因为它把成本放在了具体位置:本地语音、共享协议、权限边界、超时回收。不过,标题里的「完全本地」需要打个折扣:语音等组件在本机运行,LLM 仍然依赖云端。
本地优先,究竟把哪些东西留在电脑里
Xiaoqingting 用 Electron 做桌面端,整体拆成两个进程,通过 REST + WebSocket 通信。同一个 monorepo 里,@blm/desktop 负责录音、对话界面、three-vrm 角色,以及给网页输入语音的浏览器扩展;@blm/server 用 Fastify 封装 Claude Agent SDK,负责 agent、skills、工具审批、JWT 多用户认证,以及带用户配额的 LLM gateway。
语音链路尽量留在本地。语音识别(ASR)用 sherpa-onnx Paraformer,采用流式、int8 模型,体积约 79 MB。作者认为它的中文识别效果不错,但没有给出准确率测试。语音合成(TTS)使用 Windows SAPI,按块输出,让角色不必等整段音频准备好才开始说话。唤醒词则交给小型关键词检测模型,运行时从拼音生成,以支持同音词。
作者看重的是延迟、隐私和成本:语音处理不用等待云端往返,也不用按对话量持续支付语音 API 费用。但「语音不离开机器」不等于整个对话不出机器,LLM 仍在云端,原文没有展开它收到的具体数据范围。
模型分发也是架构的一部分。79 MB 二进制没有放进 git,用户需要自行从 sherpa-onnx releases 下载。少写这一步,安装问题就会进入 issues。原文列出的启动要求还有 Node 20+、npm install 和配置 .env,不能把它理解成装完 npm 依赖便能直接使用。
多 agent 的价值,先看它们各自能做什么
作者没有先引入 LangGraph 式编排图,而是让几个 agent 共用 SDK,分别配置提示词与工具白名单。下面这段配置解决的是不同角色的能力分配:
const agents = {
assistant: { tools: [...safeTools], prompt: PERSONA },
coder: { tools: [...devTools], prompt: CODER_PERSONA },
writer: { tools: [...readTools], prompt: WRITER_PERSONA },
};
要看的是每一行的 tools:assistant 处理聊天和语音,coder 获得 shell、文件工具,writer 负责长文且没有破坏性工具。角色名称只是入口,工具权限才决定实际影响范围。
多 agent 的第一笔复杂度,应该花在权限边界上。
服务端默认拒绝工具调用,只有白名单里的工具才放行;执行引擎也不接收服务端环境中的秘密信息。原文没有交代全部实现,但方向很清楚:带 shell 权限的助手,需要像公开 API 一样审视它的调用入口。
这也解释了为什么作者认为自己不需要编排框架。他展示的需求主要是角色分工,没有展示复杂的任务依赖、并发汇合或失败补偿,因此不能据此推导出所有多 agent 系统都只需一个配置对象。
用户一直等不到回复,比一次报错更难处理
作者遇到过 agent 一轮执行后完全沉默的故障:SDK 耗尽预算,CLI 进程没有继续输出,也没有抛出错误,界面便一直转圈。
他的处理方式是给每轮执行加 watchdog,也就是负责盯住超时的看门狗。启动时设定截止时间,到期仍无消息就强制结束引擎进程,并告诉用户发生了什么。用户反复点击「还在吗」,也不能延长原来的期限,否则只是把无限等待藏进一次次重置里。中止之后,如果连接仍未退出,10 秒后强制拆掉 socket,因为失效进程可能连正常关闭都无法完成。
这里需要区分作者定位过的故障和通用错误提示。他遇到的是预算耗尽;一般系统里,沉默也可能来自网络或工具阻塞。超时处理可以照搬思路,错误原因则要有证据才能写死。
3D 角色也暴露了类似的状态归属问题。VRM 0 模型按惯例背向摄像机,作者最初用 vrm.rotation.y = Math.PI 纠正朝向,动画播放后却又转了回去。原因是动画覆盖了变换。修正必须进入同一个更新循环,放在动画更新之后,不能只在加载时执行一次。
至于视觉效果,作者的经验是先补细微的待机、呼吸动画,再打磨口型同步。这是他的开发取舍,原文没有用户实验来比较两者的收益。
接口兼容,为什么还会报「model not found」
两个进程之间,作者把消息定义放进共享包 @blm/protocol,并用 Zod 校验。他认为这是最值得提前做的决定,因为协议变化能更早暴露,降低两端重构时的沟通成本。
这里有两层保障:共享类型帮助编译期检查,Zod 则检查运行时收到的数据。两者搭配有用,但不能保证所有协议漂移都在编译期被发现。
对外接入时,LLM gateway 支持 OpenAI-compatible 上游,却仍然碰到了迷惑性错误。模型名正确,上游却返回「model not found」。最后发现客户端给 URL 加了 ?beta=true,上游路由又把查询字符串算进模型注册表的路径匹配,导致查找失败。
修复办法是在路由匹配前去掉查询部分。若上游协议把 assistant 消息拆成独立的文本块和 tool_use 块,还需要按目标路由器的要求合并,有些路由器无法处理拆分形式。
排查这类问题时,只盯着模型名远远不够。实际 URL、查询参数、消息分块形式,都属于兼容性检查范围。
我的判断
这篇最有价值的是故障细节。权限白名单、共享协议、执行期限和连接回收,都直接关系到桌面助手能否长期使用,前端和 Node 开发者也容易理解它们解决的具体问题。
但作者没有提供延迟、识别率、语音成本的对照数据。本地方案更适合他的需求,仍属于经验判断;Windows SAPI 也让当前方案带有平台前提,原文没有说明跨平台替代方案。
我会优先采纳可靠性与权限设计,再评估语音和角色投入。作者自己也说,后来加入的网页语音输入,反而成了他最常用的功能。这个观察支持尽早验证高频入口,但还不足以证明它对所有用户都比桌面对话更重要。
能拿来做什么
- 做 Electron + Node 助手时,可以先整理进程间协议,把消息类型放进共享包,并在入口做运行时校验,减少两端分别修改后的隐性错配。
- 给 AI 编程助手开放 shell 或文件工具前,先按角色列出允许调用的工具,并给每轮执行设置固定截止时间、取消流程和连接回收逻辑。
- 接入 OpenAI-compatible 网关却遇到异常时,检查实际请求路径、查询字符串与
tool_use分块,不要仅靠反复修改模型名排错。 - 规划语音助手时,尽早验证网页听写这样的日常入口,同时准备模型下载说明、每轮成本展示、每用户配额,并用
CLAUDE.md记录架构决策,避免上线后补做。