‹ 返回笔记 · Back to notes

现场手记

纪嫣然语音工作台:为什么语音入口要做三层解耦

Jiyanran Voice Workbench: Why a Voice Entry Point Needs Three Layers of Decoupling

一个本地语音助手看上去简单,但语音入口是最容易耦合的层。我把它拆成 OpenRoom 前台 / voice-bridge / avatar-bridge / OpenClaw 四件——三层独立服务、各自 mock 兜底、各自风险关卡——为什么这样做。

纪嫣然语音工作台架构解耦OpenClawpublic-safe
水彩手绘:圆桌前 8 个 Agent 协作的场景

纪嫣然语音工作台手记

语音入口是最容易让人误判难度的一层——用户说话,AI 回答,看上去就这点事;但真要它在第二个月还能站着,就不是"接一根线"那么轻。

纪嫣然是我在本地跑的一个语音工作台。干的事很直白:我对着 Mac 说一段话,系统识别出我在说什么,再决定这句话该交给 OpenClaw(我自己的 agent 工厂)里的谁来处理,处理完再把结果说回给我。听起来一根管子就够了——麦克风进、扬声器出,中间塞一个模型。

我一开始也是这么想的。第一版甚至草草写过一个直连版本:前台 UI 里直接调语音识别 SDK,识别完直接调大模型,大模型返回再直接读出来。能跑,演示也好看,但只跑了两三天,我就意识到这条路是个陷阱。任何一处要换、要升级、要加风控,整个链路都得动。

这才有了 v1.0:OpenRoom 前台 → voice-bridge(语音桥接层)→ avatar-bridge(分发桥接层)→ OpenClaw。三层独立服务,各自 mock(占位 / 假数据兜底)fallback(降级兜底方案),各自 risk-gate(风险关卡)。中间多写了两层 bridge(桥接层 / 中间层),看着冗余。用了一段时间,反而越来越笃定:这两层 bridge 不是冗余,是这系统能长寿的关键。

这篇文章要说的,就是这件事:一个本地语音入口,为什么不能图省事写成 monolith(单体架构)的直连,为什么必须拆成三层、为什么每一层都要 mock、为什么每一层都要 risk-gate。不是炫功能,是讲一种被踩出来的判断——关于"哪里该拆开、哪里该兜底、哪里该守门"。如果你也正在想做一个本地的语音助手或 agent 入口,这份判断也许能替你省一段我踩过的坑。

OpenRoom 前台、voice-bridge、avatar-bridge 与 OpenClaw 四层解耦示意

耦合的代价:图省事的版本,前两周很爽,第三周开始烂

第一版直连有多简单?前台一个按钮,按住说话,松开发请求。识别在前台跑,模型调用在前台拼 prompt,连风控都顺手写在前台里——三百多行 JavaScript,端到端能演示。当时我还挺得意,觉得这事没那么复杂。

问题是,第二周我就想换识别引擎。本地那个模型在中英混说时识别得不行,我想试试另一个。点开前台代码,发现识别的调用、错误处理、超时、重试、降采样、VAD(语音活动检测)全在前台里搅在一起。换一个引擎,不是改一个 import 的事,是要把这一坨重新拆一遍。

第三周更难。OpenClaw 那边的 agent 接口变了一次。是个很小的协议升级,但因为前台直接拼 OpenClaw 的请求,整个前台的请求构造逻辑都要跟着改。每改一次,前台就抖一次,UI 也容易跟着出事。

最后压垮我的是风控。我想给某些命令加一道确认(比如"删除某个工作区"这类操作要二次确认),结果发现这道关卡只能写在前台。但前台是用户那一端,理论上谁都能绕过——按一个对的请求就能直接打到 OpenClaw。这不是技术债,是真安全洞。

那一刻我看清楚一件事:语音入口看上去简单,但它把"展示层、感知层、调度层、执行层"四件事一开始就混在了一起。混在一起的代价不是写代码慢,而是后面任何一处变化都要重写整层。前两周很爽,第三周开始烂,第四周就推不动了。

更隐蔽的是另一层代价——心理负担。直连版本每改一处我都要先在脑子里把整条链路过一遍:识别会不会被影响?UI 状态会不会错位?请求构造会不会跟新的协议不对?这种"任何一处改动都要全链路担心"的感觉,会很快磨掉做事的兴致。一个本地工具如果让自己每次改它都要花十分钟"先想清楚副作用",它早晚会被自己放弃。

这事不是语音入口独有的。任何一个"前端 + 模型 + agent"三件事挤在一起的系统都会遇到。但语音入口尤其严重——因为它额外多了两个让事情复杂化的东西:实时音频流、用户预期的低延迟反馈。这两件事都会强烈地诱惑你"图省事写在一起",因为多一跳就多一点延迟,多一个进程就多一点不确定。诱惑很大,但代价更大。

三层解耦怎么分:OpenRoom / voice-bridge / avatar-bridge / OpenClaw 各管一件

所以 v1.0 干脆推倒重来,按"每一层只管一件事"重排了一遍。四件事,四层。

最外层是 OpenRoom 前台。它就是一个房间:里面有麦克风、有扬声器、有显示对话的界面、有按钮、有一些视觉上的反馈。它管的事极窄——把用户的声音收进来,把后端返回的内容播出去或显示出来。它不识别语音,不构造请求,不知道 OpenClaw 是谁,也不直接跟模型说话。它就是一个房间,房间里有人讲话,房间外的事它一概不管。

再往里是 voice-bridge,跑在 3962 端口。这一层只管一件事:把"声音"变成"结构化任务"。它接住前台送过来的音频流,调识别引擎,处理 VAD、断句、置信度、可选的语言判别,最后吐出一段"我相对确定用户说的是这件事"的结构化描述。这一层不知道下游谁会接、不知道接了之后会干什么,它的责任只到"识别出意图为止"。

再往里是 avatar-bridge,跑在 3961 端口。这层管"派活"。voice-bridge 把结构化任务交给它,它判断这个任务该归哪个 agent(智能体):是问知识就找信息线的 agent,是写东西就找内容线的,是执行命令就找执行线的。它对应的是"调度",不是"识别",也不是"执行"。

最里是 OpenClaw。真正干活的人都住在这里——苏晚、霍锐、申知行、纪嫣然自己也在。OpenClaw 不关心声音、不关心前端按钮、不关心是谁派的活,它只关心"我接到一个任务,按我的人设和能力把它做好"。

四件事,四层,每一层都只看自己的边界。前台不知道下游怎么调度,voice-bridge 不知道下游有哪些 agent,avatar-bridge 不知道每个 agent 内部怎么工作,OpenClaw 不知道这个任务是用嘴说出来的还是手敲出来的。每一层只看自己这一截。

这种"只看自己这一截"有个隐藏的好处:每一层都能换"入口形式"而不影响别的层。今天是语音入口,明天我想加一个键盘入口,只需要再写一层"键盘 bridge"接到 avatar-bridge 上,下游 OpenClaw 完全不用动。哪天又想加一个邮件入口、Telegram 入口、shortcut 入口,都是同样的套路。avatar-bridge 之下变成一个稳定的"任务执行后端",avatar-bridge 之上可以是任意多种入口形式。这在我开始把纪嫣然之外的其他 agent 接进来时变得特别重要——同一个 OpenClaw 后端能服务多种入口,不用每次都从零开始。

四层各自责任边界:前台收声、bridge 识别、bridge 派活、OpenClaw 干活

这种分法看起来很啰嗦——一段音频从麦克风到真正干活,要经过四个进程、两个端口、三次序列化。后来发现,正是这种"啰嗦"让每一层都能单独换。换识别引擎只动 voice-bridge;改派活规则只动 avatar-bridge;OpenClaw 那边升级 agent 协议,外面三层都不用动。

端口为什么是 3962 和 3961 这种相邻的数?纯属顺手——我把语音相关的服务集中在 3960 段,方便记忆和排障。这不是设计哲学,就是工程偏好。但"每一层都有独立端口"这件事是有意为之:它逼着我把每一层当成独立服务对待,不能在某个版本里偷偷把两层合到一个进程里。物理隔离强制了逻辑隔离。

做完这套分层,还有一个意料之外的好处:每一层都能独立测试。我可以只起 voice-bridge,用一段录音文件喂进去,看它识别成什么;可以只起 avatar-bridge,用一段假的结构化任务喂进去,看它派给谁;可以只起 OpenClaw,用一段假的 agent 任务喂进去,看 agent 怎么响应。每一层都有自己的测试集、自己的回归用例。出问题时定位也快——四层各自的日志一比,错在哪一层一目了然。

为什么每一层都要 mock 兜底:用户那一端不能"白屏"

解耦只是第一步。真正让这套架构能在日常使用里站住的,是另一个看似不起眼的设计——每一层都自带 mock fallback

具体说——voice-bridge 启动的时候,如果发现 avatar-bridge 没起来或者打不通,它不会直接报错给前台。它会用 mock 接口兜住:返回一段事先准备的占位响应,告诉前台"调度层暂时不通,先用这段假数据"。avatar-bridge 也一样,如果 OpenClaw 那边没起来,它会用 mock agent 返回一段占位结果。前台也一样,如果 voice-bridge 都没起来,它至少能识别出用户按了按钮、显示一段"语音通道暂未连接",而不是黑屏。

这很重要。本地 AI 系统不是云服务,下游的不稳定是常态。OpenClaw 升级要重启,识别模型加载需要时间,agent 在跑长任务时可能不响应。如果每一层都把下游故障原样向上抛,用户那一端就会频繁见到"出错了,请重试"。一两次没事,连着十次,这个工作台就被废了。

但 mock 不是用来骗用户的。mock 兜住的时候,前台会明确显示"当前是占位响应,下游 X 未连接",而不是装作真的回答了。这点很关键——mock 是为了让用户那一端能继续操作(输入下一条、修改设置、看历史记录),不是为了假装一切正常。

这事真做起来才发现,mock 还有一个隐藏好处:它让每一层都能独立开发。voice-bridge 在开发时,avatar-bridge 可以直接跑 mock,不需要 OpenClaw 真在跑。avatar-bridge 在调派活规则时,OpenClaw 那一层可以全 mock,让开发不被下游卡住。否则四层联调,一层挂全链路停,开发节奏会被拖得很碎。

我设计 mock 的时候有一条原则:mock 必须可识别。它返回的内容会带一个明确的占位标记,前台看到这种标记会显式地告诉用户"当前为占位响应"。这条原则一开始我没想清,写过一版"假装一切正常"的 mock,结果有一次 voice-bridge 调不通 avatar-bridge,前台收到 mock 响应后正常地播了出来,我居然没发现下游断了一下午。从那以后,mock 必须显式可见——宁可显得粗糙,也不能让"系统其实没在工作"被假象遮住。

另一条经验:mock 不要追求"看起来很聪明"。我曾经想过让 mock 用一个小模型生成一段更像真实回答的占位文字。最后没做——原因很直接:mock 越聪明,用户越分不清是真的还是占位的,越容易把假数据当真。简单粗糙的 mock 反而是诚实的——它的存在本身就在说"下游这一段断了"。

v1.0 就这么干的,v2.0 没改这条。不是懒改,是被验证过了:mock 在线,整个系统就稳;mock 一拿掉,链路就脆。

为什么每一层都要风险关卡:不让 OpenClaw 被任意打穿

解耦解决了"换得动",mock 解决了"撑得住"。但还有一个问题没解决——安全

一个本地系统的安全比看上去更容易被忽视。很多人觉得"反正只有我自己用,本地没风险",但事实是:只要这套系统有端口、有 API、有能调用真东西的能力,它就有可能被绕过、被滥用、被无意中触发。哪怕是我自己说话说错了一句、识别错了一个字,也可能让 OpenClaw 去做一件不该做的事。

所以每一层都要有自己的 risk-gate。

前台这一层最朴素:白名单。哪些客户端可以连前台,哪些来源能注入消息,写死。没在名单里的,连页面都打不开。

voice-bridge 这一层管声音边界:哪些 prompt 模式是允许的,哪些指令模式要立刻拦截。比如用户口语里有些容易被识别错的关键词,voice-bridge 会先做一层意图清洗,把高危表达打个标记往下传。

avatar-bridge 这一层最关键。它是真正决定"这件事派给谁"的层,所以也是最严的一层。每个 agent 都有自己能做什么、不能做什么的边界,avatar-bridge 在派活之前要校验一遍:这个任务匹配这个 agent 的能力吗?需要的权限有吗?是不是属于需要二次确认的高风险动作?不通过就不派。

OpenClaw 那一层自己也有一层 risk-gate。这是"防守的最后一道"——哪怕前面三层都被绕过,OpenClaw 内部还有自己的人设、自己的边界、自己的 audit log(审计日志)。任何 agent 都不能在没有 owner 放行的情况下做出超出自己能力范围的事。

四层 risk-gate 听起来重复,其实不是。叠在一起的逻辑是:任何一层都不能被假设是可信的。前台可能被绕,voice-bridge 可能识别错,avatar-bridge 可能派错。所以每一层都自己守自己的门,不能指望外面那层把脏东西挡掉。

这套做下来还有一个附带好处:audit log 每天轮转,每一层都写自己的。哪里出了事,哪一层的日志就最先看到。要复盘一次误识别,不是去翻一坨混在一起的全链路日志,是先看 voice-bridge 那天的识别记录,再看 avatar-bridge 的派活记录,最后看 OpenClaw 的执行记录。每一层日志各管一段,复盘速度反而快。

我后来还总结出一个心得:每一层的 risk-gate 越严,下游能简化的逻辑就越多。如果 avatar-bridge 在派活之前已经把不合法的请求都挡掉了,OpenClaw 内部就不用再为"输入是不是恶意的"做太多防御性写法。它可以专心做自己擅长的事——执行任务。反过来如果上游 risk-gate 形同虚设,下游就要自己写各种边界检查,整个系统的代码会越来越臃肿。所以分层 risk-gate 不仅是安全设计,也是把责任摆清楚——每一层只需要做好自己那一层的检查,不用替别人兜底。

四层 risk-gate 叠加:白名单、意图清洗、派活校验、agent 边界

v1.0 vs v2.0 的产品判断:核心架构不动,增量只在边边角角

现在系统跑在 v1.0。这个版本已经稳定运转了一段时间,端口固定(voice-bridge 3962、avatar-bridge 3961),白名单和 risk-gate 都在工作,audit log 每天轮转,mock fallback 兜着,识别、派活、执行各司其职。它不完美,但它是个"在跑的真东西"。

我也开始做 v2.0。v2.0 的骨架已经落地,正在收口;v2.0 GA(General Availability,正式发布)还在等审计。但有一件事我从一开始就定下来了:v2.0 只做增量,不动核心架构

这是一个产品判断,不是一个技术判断。

技术上 v2.0 完全可以"借这次机会把架构改一改"——比如把 voice-bridge 和 avatar-bridge 合并成一个进程少一次序列化,比如换一种更现代的通信协议,比如把 mock 兜底改成更智能的"假装继续聊天"。每一项单独看都说得通。

但产品判断告诉我不要动。v1.0 的核心架构已经被几个月的真实使用验证过了:四层解耦、各自 mock、各自 risk-gate。这套结构不是凭空想出来的,是踩坑踩出来的。任何一处"借机会改一改",都可能把已经验证过的稳态推回到不稳态。增量是安全的,重写是冒险的。

v2.0 只做边边角角的增强。比如更细的意图识别、更友好的占位响应、对长对话的更好支持、对某些 agent 的派活规则做更精细的拆分。这些都是"加一点",不是"重写"。原本的四层、四端口、四 mock、四 risk-gate 一个都没动。

这种判断在自己一个人开发的项目里特别重要。一个人开发最容易掉的坑就是"每次升级都顺手重写"——因为没人拦着你,因为你昨天才写的代码今天看着不顺眼,因为新版本的 SDK 看着更性感。但真要它长寿,第一件事就是把验证过的部分锁住,把没验证过的部分留作增量探索。这两件事不能搅在一起。

所以 v2.0 的设计原则被我写得很死:能不动核心架构就不动;新功能优先做在边缘;老功能除非有明确的"在线证据"说它出问题,不然不重写;任何"看上去更好的设计",先在 mock 路径上验证,不直接上真路径。这些规则不是为了限制创造力,是为了让"v1.0 已经能跑这件事"这个事实不被新一轮兴奋冲掉。

但 v2.0 也不是不做事。骨架已经落地,正在收口阶段。GA 还在等审计——是的,等审计,不是等代码。因为这一层涉及到 OpenClaw 内部 agent 的能力边界,必须有一次外部 review 看过、确认风险关卡没有被新功能绕过,才能正式开放。这个等待值得。本地系统一旦上线就很难再"全量回滚",所以宁可在 GA 前多等一段时间。

真正的取舍:简单调用 vs 长期演进

回头看整件事,最大的一次抉择其实在最开始:要不要把一根直连写得更复杂一点。

一开始多写两层 bridge 是有代价的。多一倍代码,多一份部署,多一份监控,多一份心智负担。如果只是想"我做一个能跑的语音助手",三百行直连就够了,省下来的时间可以拿来做别的。

但如果你想做的是"一个能跟我用半年、一年、两年的语音工作台",多这两层就完全不一样了。它把"如何识别"和"派给谁"从前台彻底剥开,于是识别引擎可以换,调度规则可以演化,agent 可以增减,前台 UI 可以重做,每一件事都不会牵连别的事。

这是一次很典型的"短期复杂换长期简单"。短期看,多两层 bridge 是负担。长期看,它把这套系统从"一坨容易烂的胶水"变成了"四个能各自演进的小服务"。

我自己的经验是:本地 AI 系统里,凡是涉及到"用户入口 + 模型调用 + agent 执行"三件事同时存在的地方,都值得做这种解耦。不是因为它一开始就值,是因为它会避免一种最痛的烂——那种你明知道某一层该换、但因为耦合得太深所以不敢换,于是只能在一个越来越糟糕的状态下凑合用的烂。

本地 AI 系统跟云服务最大的差别就是:你没有一个团队帮你撑着、没有 SLA 兜着、没有半年一次的大重构窗口。你只有你自己。所以稳定性不是靠"我会去修",是靠"它本来就不容易坏"。三层解耦就是让它本来不容易坏的设计。

我把这套判断浓缩成几条原则放在这里,给后面要做类似系统的人参考:

  • 语音入口要拆成"前台 / 识别 / 调度 / 执行"四层,每层独立服务、独立端口;
  • 每一层都自带 mock fallback,下游不通时保证用户那一端不"白屏";
  • mock 必须可识别,不能让占位响应被当成真回答;
  • 每一层都自带 risk-gate,不假设上下游可信;
  • 核心架构一旦验证过就锁住,所有新功能优先做增量;
  • audit log 每层各写一份,按层切片复盘比全链路混在一起快得多。

这些原则没有一条高深。它们都是"踩过坑之后才觉得理所当然"的那种。但理所当然这件事,往往要等到自己写过一版烂的、然后被自己折磨过一阵子,才会真正接受。

v2.0 GA 还在等审计,mock 还在兜底,真 Claw 接入还没完成。

纪嫣然不是一个"做完了的语音助手",它更像一个"在跑、在改、在长"的工作台。v1.0 已经够稳——稳到我每天可以靠它做事,稳到我可以放心给它加新东西。但它远不是终态。语音入口的边界会继续模糊(多模态、多设备、长对话),下游 agent 的能力会继续长,风险关卡的颗粒度也会继续细。

但有一件事我现在比一开始更确定:不管前台 UI 怎么变、识别引擎换成什么、OpenClaw 里有多少个 agent,这套四层、四端口、四 mock、四 risk-gate 的骨架不会动。它不是终点,它是让这个工作台能一直走下去的地基。地基不漂亮,但地基要稳。

一句话收尾——本地 AI 系统不缺会写代码的人,缺的是愿意一开始就多写两层 bridge 的人。短期里这是负担,长期里这是寿命。语音入口看上去简单,但它是最容易让人误判难度的一层;正因为如此,它也最值得在第一次就把骨架做对。

v1.0 已经稳了,v2.0 在路上。下一篇可能写真 Claw 接入完成之后看到的事——但那是下一篇的事了。

把这篇记录接到下一步

读完以后,可以继续追问这篇文章,也可以回到策展目录,或通过标签追同一条线索。

追问这篇 回到目录 浏览标签