iFakeSiri自制语音助手:三条实现路线与工程避坑指南 第一次被问到能不能让手机里的语音助手说一段我们自己的词时我下意识回了一句改不了那是系统级服务。结果对方掏出一台手机当场放了一段视频屏幕上的语音助手在跟人一问一答台词严丝合缝节奏卡得比真人还准。后来才知道那玩意儿压根不是真助手在说话——声音是提前录好的界面是套壳的唤醒动作是旁边一台机器按的。那一刻我才意识到iFakeSiri 这类自制语音助手对话的价值根本不在骗过谁而在于把一个封闭的黑盒换成一套自己完全掌控的对话流水线。你要它说什么它就说什么你要它什么时候停顿它就什么时候停顿你要它答错它也绝不会多嘴。这篇文章就聊这件事一个假的语音助手到底怎么搭、有哪几条路可走、每条路会踩什么坑以及那些只在真正跑起来之后才会暴露的细节问题。适合做短视频剧情的、做展厅导览的、做对话产品原型的以及纯粹想给自己家小孩做一个会说话的小伙伴的人。1. 为什么要把语音助手做假1.1 真助手动不了的那几处先把话说透为什么非得假因为真的那套东西从设计之初就没打算让你改。它解决的问题是通用场景下的大概率可用而你要解决的问题是特定场景下的百分之百可控这两个目标天然冲突。最直接的障碍是回答库封闭。无论哪家的语音助手你都没法往里塞一条自定义问答让它在你问今天展厅有什么活动的时候,照你写好的文案念出来。顶多能做到打开某个应用查天气这类动作转发一旦涉及具体话术就只能靠事先录好的音频播放——那就不叫对话了叫广播。第二个障碍是离线不可用。语音助手基本都依赖云端识别和云端推理网络抖动一下回答就卡三秒网络断了直接哑巴。做展厅演示、做线下活动、做需要长时间稳定运行的陪伴设备这种不确定性是致命的。我见过一次现场演示讲解员问了三次同一个问题前两次助手答得挺好第三次网络抖了一下屏幕上转圈转了十几秒,观众的注意力全跑光了,后面再讲什么效果都打了对折。第三个障碍是业务数据进不去。你想让助手回答我这个订单到哪了我这张卡还剩多少它要么答不了要么把你的问题转到一个网页搜索上给你一堆不相干的链接。而假助手可以直接对接你自己的接口甚至直接读本地的一个文本文件。第四个障碍是语气不可控。通用助手的音色、语速、说话风格都是调好的你没法让它用更活泼一点的语气跟小孩说话也没法让它在你设定的剧情节点故意停顿半秒。而这些恰恰是内容创作里最要命的东西。1.2 假对话能覆盖的四类场景把假字拆开看其实是自主可控四个字。它能干的事比想象中多。第一类内容创作里的精确台词。拍一段人机对话的短视频或者微电影最怕的就是助手答非所问然后你得重拍二十遍。用自制方案台词是你写的节奏是你剪的想让它说什么就说什么一条过。这类需求对实时性要求最低对可控性要求最高。第二类展厅、门店、展会的定点导览。固定几十个问题固定几十条回答需要品牌人设统一、需要断网也能跑、需要连续开机十几个小时不崩。这类场景里识别准确率要求没那么高因为观众问的问题基本都在预设范围内但稳定性要求极高。第三类对话产品的原型验证。这是我自己用得最多的。你在设计一套多轮对话流程想知道用户说这句话的时候助手这么接会不会让对话死掉最快的办法不是等整个产品做出来而是先用一个脚本引擎把对话逻辑跑通把话术改到顺了再去接真正的语音链路。原型阶段甚至可以只做文字版把语音这一层先摘掉。第四类陪伴与教学类的小玩具。给小孩做一个会讲故事的角色给老人做一个会提醒吃药的闹钟这类东西不需要什么通用智能只需要几十条精心设计的对话加上稳定的唤醒。用通用助手做不了因为通用助手不认你是谁它只认你问了什么。顺带说一句这类方案还有一个隐藏用途给对话系统做回归测试。你把历史线上日志里的用户问句批量灌进去看新版本的话术引擎会不会把原本答对的问题答错这比人工点一百遍高效得多。2. 三条实现路线从纯文本气泡到能听会说的活体助手2.1 路线A只做界面的纸片人方案如果你的目标只是拍视频、做演示、给别人看一眼效果那最省事的路子是——压根不做语音只做一个会动嘴的界面。这套方案的核心是把对话当成一段动画来播。你在一个网页里做出聊天气泡的样式左边是助手的头像右边是用户的输入框然后写一个简单的脚本用户气泡延迟 300 毫秒出现助手气泡延迟 800 毫秒出现助手气泡里的文字用打字机效果一个字一个字往外吐中间遇到逗号停 120 毫秒、遇到句号停 260 毫秒。// 打字机效果按标点控制节奏 const paceMap { : 120, 。: 260, : 260, : 200, 、: 80 }; async function typeText(el, text) { for (const ch of text) { el.textContent ch; const wait paceMap[ch] ?? 45; await new Promise(r setTimeout(r, wait)); } }别小看这个 45 毫秒的基础间隔。默认快一点点看起来就像机器在刷屏默认慢一点点观众会觉得网卡了。我试过很多次40 到 60 毫秒之间是最像真人打字的区间而且这个值要跟句子的长短挂钩——短句可以快一点长句要略慢否则节奏会显得很赶。配上录音时的波形动画、助手思考中的三点跳动、以及一句嗯我想想的过渡语效果就相当唬人了。整套东西一个下午能搭完成本接近于零而且因为没有真正的语音识别环节永远不会出现识别错误导致的翻车。缺点也很明显它不能真交互。观众一旦伸手去按它就露馅了。所以这类方案适合录屏和由主持人代按的播放场景不适合开放给陌生人自己上手玩。2.2 路线BASR 脚本引擎 TTS 的完整闭环要真能听会说就得把一条完整链路串起来。这条链路有六个环节任何一个环节慢一点整体体验就崩。第一环是唤醒。两条路一是按键触发最简单可靠一个蓝牙遥控器或者一块大按钮就够二是唤醒词检测需要在本机跑一个轻量的唤醒词模型。按键的优点是零误唤醒、零延迟缺点是用户得动手唤醒词的优点是自然缺点是必然会有误唤醒而且误唤醒之后你得有个退出机制。我的建议是只要场景允许优先用按键把唤醒词留到实在腾不出手的时候再用。第二环是录音与端点检测。录音本身不难难在判断用户说完了。做法是算音频的能量和过零率连续 400 到 600 毫秒没有有效语音就认为说完了。这个阈值是个经验值设太短用户说到一半换气就被切断设太长用户说完之后还得干等一秒。中文对话里 500 毫秒是个不错的起点语速慢的场景可以放宽到 700 毫秒。第三环是语音识别。现在可选的方案不少本地跑的开源识别模型优点是断网可用、隐私可控缺点是首次加载慢、对硬件有要求调用云端识别接口优点是准确率高、本地压力小缺点是必须有网络。做展厅这类场景我倾向本地做内容创作云端更省事。第四环是意图匹配与回复生成。这是整个系统的灵魂后面单独开一章讲。第五环是语音合成。本地合成引擎比如一些开源的轻量 TTS优点是延迟低、离线可用缺点是音色偏机械神经网络合成的音色自然得多但要么吃算力要么得联网。折中方案是常用的一批回答预先合成好存成音频文件只有没命中缓存的部分才临时合成。第六环是播放与回声处理。这一环最容易被忽略。播放的时候麦克风如果还在收音就会把自己说的话又收进去形成自问自答的死循环。解决办法只有两个一是播放时硬性关闭收音简单粗暴但不能打断二是做回声消除也就是从麦克风信号里减掉已知的播放信号。后者效果更好但实现复杂业余项目建议先用前者。2.3 路线C接管系统助手入口的寄生方案还有一条路不自己造轮子而是想办法让自己写的东西寄生在系统语音助手的入口上。具体做法因平台而异但思路是共通的利用系统提供的快捷指令、无障碍服务、外接按键或者音频路由在用户触发助手的那个瞬间把你的服务抢先拉起来。比如通过蓝牙按键让系统弹出一个自定义的快捷面板面板里就是你自己写的那套对话界面或者通过音频路由把本该进系统的麦克风数据引到自己的应用里。这条路的优点是入口自然用户按的还是那个熟悉的键不需要学习新操作。缺点是极不稳定——系统版本一升级权限一收紧原来的路子可能就废了。而且做得越像系统原生越容易引起误解这一点后面讲合规的时候会提。我个人对这条路的态度是做技术验证可以做长期产品要慎重。真要长期跑老老实实用自己的物理按键更省心。2.4 三条路线的选型对照表维度路线A 纸片人路线B 完整闭环路线C 寄生入口开发周期半天到一天一到两周两周以上且不可控是否需要硬件不需要需要麦克风与音箱需要目标设备断网可用完全可用视组件而定视组件而定真实交互能力无完整完整稳定性极高中高低适合场景视频、演示、预览展厅、陪伴、原型技术验证选哪条取决于你要的是给别人看还是给别人用。看选 A用选 B只想证明某个入口能被接管选 C 然后把成果扔进抽屉。3. 对话引擎的核心让假对话不露馅的话术设计3.1 意图匹配的三层漏斗语音识别的结果永远带着噪声——错字、漏字、多字、口语化填充词。你不能指望用户说出跟脚本一字不差的话所以匹配必须分层。第一层精确命中。把用户文本做标准化处理去标点、去空格、全半角统一、繁简统一然后在一张哈希表里查。这一层负责那些高频、固定、一字不能错的问句比如你好再见你能做什么。命中速度是微秒级。第二层关键词与正则。当精确匹配落空就看这句话里有没有触发词。比如几点加上开门就命中营业时间多少钱加上任意一个商品名就命中价格。这一层用一组正则表达式加一个关键词标注表就够了不需要任何模型。第三层语义相似度。前两层都落空才动用向量相似度。把用户问句编码成一个向量跟脚本库里每个意图的标准问句向量算余弦相似度超过阈值我一般设 0.72 到 0.78就认为是同一个意图。这一层能兜住大量换说法的表达代价是需要一个轻量的句向量模型并且首次加载要花几百毫秒。最后是兜底层三层的分数都低于阈值就走兜底话术。# 三层漏斗的骨架示意 def match_intent(text, exact_map, rules, vec_index, threshold0.75): norm normalize(text) # 去噪、统一格式 if norm in exact_map: return exact_map[norm], 1.0, exact # 第一层 for rule in rules: # 第二层 if rule.pattern.search(norm): return rule.intent, 0.95, rule intent, score vec_index.search(norm) # 第三层 if score threshold: return intent, score, vector return fallback, score, fallback这里有个细节值得强调三层漏斗的阈值不能拍脑袋定一定要拿真实录音去调。你写的测试集是打字用户说的是口语两者差得很远。我自己踩过的坑是测试集里全是你们几点开门真实场景里用户说的是哎那个你们早上几点开啊——第三层相似度只有 0.68被兜底了。加上几点开门这两个触发词之后第二层就兜住了。3.2 上下文与打断多轮对话的最小状态机单轮问答谁都会写多轮才是真功夫。而多轮对话的最小实现其实就是一个带槽位的状态机。举个具体例子用户问明天天气怎么样助手要先知道哪个城市。如果城市在开场时已经收集过就直接答如果没有就反问一句你要问哪个城市的然后把状态切到等待城市下一个输入不做意图识别直接当成城市槽位来填。# 话术配置的一种写法 - intent: weather_query slots: [city, date] ask: city: 你想问哪个城市的天气呀 date: 你是想问今天还是明天 answer: {date}{city}的天气是{result}。 fallback_answer: 这个我得查一下你先告诉我城市 next_state: awaiting_city这套东西用 YAML 描述好处是改话术不用改代码。做内容的场景里你在剪辑的时候发现某句话别扭直接改配置文件重启就行不用重新编译。打断barge-in是另一个必须处理的问题。用户在你说话说到一半的时候插嘴理想行为是立刻闭嘴听用户说。实现上要有两件事一是播放器能被随时叫停二是麦克风在播放期间保持收音但做回声抑制否则自己的声音会持续触发。退而求其次的做法是——播放期间不收音但检测到一个明显的音量突增说明有人在喊就先停播等 200 毫秒再开麦确认。这个降级方案在嘈杂环境下误触发会多一些但总比完全不能打断强。3.3 兜底话术比答对更重要的是答错时的样子这是我最想说的一点。绝大多数自制助手翻车不是因为答得不准而是因为答错的时候那种嗯我不明白你在说什么的机器人腔调瞬间就把整个幻觉击碎了。真人聊天的时候遇到听不懂的话反应是很丰富的啊你刚才说什么你说的那个是……哪一款哦我知道了你说的是那个吧。这些反应里包含了澄清、猜测、转移话题而且都不尴尬。所以兜底话术不要只写一条要写一组并且根据场景轮换。我一般会准备这么几类兜底类型示例话术适用时机澄清式你再说一遍刚才有点吵我没听清。识别置信度低猜测式你是在问那个新出的那个吗有部分关键词命中转移式这个我可能帮不上不过我可以给你讲讲别的。完全没命中求助式等下我反应慢了点你换个说法试试连续两次没命中而且兜底话术一定要和人设一致。如果你的助手人设是个毒舌兜底可以写你这话说得我CPU都跟不上如果是儿童陪伴就要写嗯……我没听懂你能再讲一遍吗我们一起想办法。人设的一致性比准确率的绝对值重要得多。还有个小技巧连续兜底两次之后就别硬撑了主动把话题引向你能答的领域。这个我真不知道不过你要是想听我讲讲今天展厅有什么好玩的我倒是能说上两句。这句话一出对话就活了。4. 链路延迟把听完到出声压进一秒4.1 延迟拆解六个环节各花多少毫秒延迟是这种项目里最容易被低估的敌人。你在开发机上测的时候感觉挺快的啊一放到真实场景用户说完话到助手出声中间空了两秒半那个沉默立刻让整个东西变假。先把延迟拆开看。一次完整交互大致花在这些地方环节典型耗时说明端点检测等待400–700 ms判断说完了需要的静音时长音频封包与上传50–300 ms本地识别可忽略云端识别主要看网络语音识别150 ms–1.5 s本地小模型快云端准但慢意图匹配5–80 ms精确匹配几乎为零向量检索稍慢语音合成100 ms–1.2 s视是否缓存、是否神经网络合成播放启动30–100 ms音频缓冲把最坏情况加起来超过三秒太正常了。而人的对话节奏里超过 800 毫秒的沉默就已经显得不自在了。所以优化的目标不是把每个环节都做到极致而是把用户感知到的那段空白填满。最有效的手段是填充语在端点检测一结束、识别还没出结果的时候立刻播一句嗯我看看这个……这 300 毫秒的声音一出来用户的感知延迟立刻减掉一半。填充语要在本地预先加载好不能等。4.2 流式化改造边识别边合成第二个手段是把串行改并行。传统做法是录完 → 识别完 → 匹配完 → 合成完 → 播放。每一步都要等上一步彻底结束。流式改造之后录音的时候就在往外吐识别结果识别出前半句的时候就能开始匹配意图回复的第一句文本一确定就开始合成合成出第一段音频就开始播。整条链路像水管一样流动起来总耗时从各环节之和变成最慢那个环节。代价是复杂度上去了你要处理识别结果还在变意图就变了的情况所以匹配要做防抖——同一个意图连续稳定 200 毫秒才确认你还要处理合成的第一段刚播完第二段还没好的断流所以要有缓冲队列。对于内容创作这类不追求极低延迟的场景其实可以跳过流式改造。但如果你要做的是真人上前搭话的展项这一步省不掉。4.3 预生成与缓存把可预测的回答提前做掉第三个手段最土也最有效预先合成。你的回答库是有限的那就在启动的时候把所有回答音频全合成一遍存到本地。整个合成过程可能在后台跑两分钟但用户永远感知不到因为启动完了。之后每次命中直接从磁盘读文件播放延迟就是几十毫秒的读盘时间。缓存要设计淘汰机制否则回答库一多就撑爆磁盘。我的做法是按最近 30 天命中次数排序保留前 200 条的热缓存其余的走实时合成。命中了冷门回答就顺手加进队列下次就有了。还有一个更细的技巧把长回答按句切分每句一个音频文件播放时按顺序拼接。好处有两个——用户中途打断时能精确停在一句话的边界上而不是啪一下从半句话里切断以及改稿的时候只需要重新合成改过的那一句不用整段重做。5. 上线前的坑回声、抢占、断句与边界5.1 回声消除与麦克风抢占这一节讲的都是跑起来之后才会发现的问题。回声是第一大坑。音箱在放麦克风在收助手听到自己的声音以为是用户在说话于是又回答了一遍——死循环。表现上就是助手自言自语停不下来。我最早的那个版本就是在我离开房间十分钟之后开始无限循环自问自答回来一看录音文件里有三百多条重复对话。彻底解决要靠回声消除也就是把正在播放的音频信号作为参考从麦克风输入里减掉它。开源方案有但调起来不容易尤其是当扬声器音量大、房间有混响的时候。实用主义的做法是分层防御播放时把录音增益调低同时把端点检测的能量阈值调高再加上播放结束后的 300 毫秒内不触发任何新识别的冷却期。这三招叠加能把 95% 的自激情况挡住。麦克风抢占是第二大坑尤其在多应用环境下。你的程序在录音系统的语音助手被误唤醒两边同时抢麦克风结果两边都拿到半截音频。表现是识别结果莫名其妙少了前半句。这个问题没有优雅解法只能靠独占模式打开麦克风加上检测到其他应用占用就重试。5.2 TTS 断句、多音字和语气语音合成的坑集中在文本处理上。断句问题。合成引擎一般会自己断句但它对中文的标点和语义理解有限经常在奇怪的地方停顿。比如我想问问你明天有什么安排它可能在问问你后面断一下听起来就像结巴。解法是人为插入控制符或者干脆把长句手动切成短句逐句合成。多音字问题。行读 háng 还是 xíng重读 zhòng 还是 chóng还读 hái 还是 huán——这些在固定话术里其实可以规避写文案的时候就把容易读错的字换掉。还行改成还不错重做改成再做一遍改动成本极低效果立竿见影。语气问题。合成引擎默认的语气是平的所有句子一个调。真人说话的时候问句尾音上扬感叹句重音靠前这些细节合成引擎做不好只能靠标点和小词来弥补。句尾加个呀呢吧语气立刻就软下来了要说重话的时候把句子切短不行。这个真不行。比这个恐怕不行听起来有力量得多。我还发现一个反直觉的点语速调快一点点反而显得更自然。合成音色的默认语速往往偏慢慢就显得呆。把语速提到 1.05 到 1.1 倍机械感会明显下降。5.3 使用边界与合规自查做这类项目有几条边界得自己守住。别做误导性的伪装。自制助手可以做得像系统助手但如果拿去做公开传播的内容一定要有清晰的标识让人知道这是构造的对话而非真实记录。这不只是合规问题也是对自己信誉的保护——一旦被指出是伪造的前面所有内容的可信度都会归零。录音要有明确的知情同意。如果设备会采集现场用户的声音必须在显眼位置告知并且要有不录音的选项。展厅场景尤其要注意观众的对话内容可能涉及个人信息存储和传输都要谨慎。个人信息别往本地存。如果对话里出现了姓名、地址、号码这类内容处理完就该丢弃不要为了优化模型而把原始音频攒起来。音量别做太绝。展厅里循环播放的设备音量调到能听清就够强行提高音量只会让环境更吵识别效果更差形成恶性循环。6. 给不同目标的落地路径6.1 只想拍一条视频两小时的活如果你的目标只是产出一条人机对话的内容别碰语音链路直接走路线 A。具体流程做一个网页左边助手头像右边用户输入区中间气泡列表写一个数组里面按顺序放好每一轮的说话人、文本、延迟时间写个循环按顺序播录屏的时候用鼠标点发送让用户气泡出现剩下的交给脚本。整套东西加起来不到两百行代码两小时能出片。要注意的是节奏。视频里的对话节奏比真实对话要快因为观众没有耐心等。用户气泡出现到助手开始回应间隔控制在 600 到 900 毫秒助手回答内部句与句之间留 200 到 400 毫秒。太慢了观众会以为卡了太快了显得不真实。6.2 想摆一台能用的机器一个周末的活要做能真交互的原型按这个顺序搭每一步都能独立验证。第一步先把录音和播放跑通。按下按键开始录松手停止把录到的音频存成文件再播出来。这一步没跑通之前别碰识别。第二步接识别把文字打到屏幕上。只做识别不做匹配不做合成。拿二十句话测一遍看哪些识别错了。这一步会暴露麦克风质量、环境噪声、端点检测阈值的所有问题。第三步接脚本引擎先把回复打在屏幕上。用 YAML 写三十个意图覆盖你最常用的问答。这一步调的是话术跟语音无关改起来快。第四步接合成把回复念出来。先不做缓存实时合成感受一下延迟。如果延迟不能忍再回过头做预生成。第五步闭环。把前面四步串起来加上按键触发和播放时静音录音。这个顺序的好处是每一步都可以停下来交付一个半成品而且每一步的问题都是独立的不会互相干扰。我见过太多人一上来就把六个环节全接上然后一个 bug 都定位不出来。6.3 想做长期产品先把内容管起来如果这东西要长期维护最大的成本不在技术在内容。我的经验是把话术、意图、槽位、兜底全部外置成配置文件代码只负责执行。配一套简单的审核流程改话术前先在本地跑一遍回归集看有没有把原来能答对的问题答错改完之后打个版本号线上出问题能一键回滚。回归集怎么攒每次真实使用中出现一次没答好就把那句原话加进去。三个月下来你会有一份几百条的、完全贴合真实使用的测试集。这东西比任何现成的测试数据都有价值。最后分享一个我个人在操作里最受用的体会这类项目的真从来不是靠技术堆出来的是靠细节堆出来的。一句恰到好处的嗯一次自然的停顿一个答错时像人一样的反应比把识别准确率从 92% 提到 96% 有用得多。用户判断面前这个东西是真是假靠的不是逻辑是感觉。而感觉这东西是可以用一行文案、一百毫秒延迟调出来的。