上周三晚上十点,我正准备睡觉,手机突然响了。是运维同事打来的,说线上AI眼镜的语音助手功能出问题了,用户反馈说唤醒词没反应,眼镜像聋了一样。

我心里咯噔一下。这个功能是我们团队花了三个月做的核心功能,上线才一周,怎么就出问题了?我打开电脑,连上VPN,开始了一夜的排查。

这篇文章就来记录这次排查经历,不是为了诉苦,而是想聊聊在AI眼镜这种新硬件平台上做开发,会遇到哪些传统软件开发中不会遇到的坑,以及排查复杂Bug的一些方法论。

问题初现:唤醒词失灵

先简单说一下背景。我们做的是一款AI眼镜,核心功能之一是语音助手。用户说出唤醒词"你好小镜",眼镜就会被唤醒,然后用户可以说指令,比如"今天天气怎么样""给张三打电话""导航到公司"。

语音唤醒的技术方案是这样的:眼镜上有一个低功耗的语音唤醒芯片,一直在监听声音,检测到唤醒词后就给主芯片发一个中断信号,主芯片启动语音识别和对话流程。这个方案的好处是功耗低,唤醒芯片的功耗只有几毫瓦,可以一直运行。

用户反馈的问题是:有时候说唤醒词没反应,需要说好几遍才能唤醒。而且这个问题不是必现的,有时候正常有时候不正常,完全没有规律。

这种非必现的Bug最让人头疼。因为你没法稳定复现,就没法确定问题出在哪里,也没法验证修复是否有效。

我首先看了一下监控数据。发现从晚上八点开始,唤醒失败率从平时的2%飙升到了35%。也就是说,超过三分之一的唤醒请求失败了。而且失败率还在缓慢上升。

这个时间点很关键。晚上八点之前一切正常,八点之后突然出问题。这说明不是代码的问题(代码没有变),而是环境或者状态的问题。是什么东西在晚上八点发生了变化呢?

第一轮排查:从最常见的原因开始

排查Bug的第一步,永远是从最常见的原因开始排除。

我首先怀疑是网络问题。AI眼镜的语音识别是在云端做的,唤醒之后要把音频传到云端。如果网络不好,可能会导致唤醒失败。但监控显示网络延迟和丢包率都正常,而且唤醒是在本地做的,不需要联网,所以网络应该不是问题。

然后我怀疑是唤醒芯片的固件问题。会不会是芯片跑了一段时间之后状态异常了?我查了一下芯片的日志,发现芯片一直在正常运行,没有崩溃也没有异常重启。而且如果是芯片问题,应该是所有用户都受影响,但实际上只有部分用户反馈有问题。

接着我怀疑是音频采集的问题。会不会是麦克风出问题了?或者是环境噪音太大导致唤醒率下降?我拉了一些失败案例的音频数据,发现音频本身是正常的,唤醒词说得也很清楚,用离线模型测试是可以识别的。这说明音频采集没问题,问题出在唤醒检测之后的流程。

这一轮排查花了大概一个小时,排除了网络、芯片、音频采集这几个最常见的原因。问题比我想象的要复杂。

第二轮排查:深入系统内部

排除了常见原因之后,我开始深入系统内部排查。

唤醒的流程是这样的:唤醒芯片检测到唤醒词 -> 通过GPIO中断通知主芯片 -> 主芯片的唤醒服务收到中断 -> 启动语音识别服务 -> 播放唤醒提示音 -> 开始录音并上传云端。

我在每一个环节都加了日志,然后找了一台有问题的测试眼镜来复现。

测试了几次之后,我发现了一个规律:唤醒失败的时候,唤醒芯片确实检测到了唤醒词(芯片日志里有记录),也确实发了中断信号(用示波器测了GPIO引脚,确实有电平变化)。但是主芯片的唤醒服务没有收到这个中断!

这就有意思了。中断信号发出来了,但接收端没收到。这说明问题出在中断信号的传递过程中。

在我们的系统中,GPIO中断是通过Linux内核的GPIO子系统处理的,然后通过input子系统上报给用户空间的唤醒服务。中间的链路是:硬件GPIO -> 内核GPIO驱动 -> input子系统 -> 用户空间服务。

我查了内核日志,发现了一个奇怪的现象:在唤醒失败的时候,内核里有GPIO中断的记录,但input子系统没有上报对应的事件。也就是说,中断到了内核,但没有被传递到用户空间。

为什么内核收到了中断但不上报呢?我仔细看了一下input子系统的代码,发现了一个可能的原因:input事件的去抖动机制。

为了防止按键抖动导致的误触发,input子系统有一个去抖动的配置。如果两个中断之间的时间间隔小于去抖动阈值,第二个中断就会被忽略。我们的唤醒芯片有时候会因为噪音而产生误触发,所以我们配置了一个比较大的去抖动阈值,是500毫秒。

但问题是,如果用户在500毫秒内说了两次唤醒词(第一次没反应,用户会马上再说一次),第二次就会被去抖动机制忽略掉!这就解释了为什么用户需要说好几遍才能唤醒——不是第一遍没检测到,而是第一遍触发了去抖动,后面的几遍都被忽略了,等500毫秒过去之后再说一遍才能成功。

但这还不能完全解释问题。因为去抖动机制一直都在,为什么之前没问题,晚上八点之后才出问题呢?

找到真凶:一个看似无关的配置变更

我盯着监控数据看了很久,突然注意到一个细节:晚上八点,正好是我们的运营团队推送了一个新的配置文件的时间。

这个配置文件是用来调整语音助手的各种参数的,比如唤醒灵敏度、语音识别的语言模型、对话的prompt等等。运营团队每天晚上八点会推送当天的配置更新,这是正常的操作,已经做了很久了。

但这次的配置更新有什么不同呢?我对比了一下新旧配置,发现了一个改动:唤醒灵敏度从0.7调到了0.5。

唤醒灵敏度是唤醒芯片的一个参数,值越低越灵敏(更容易被唤醒),但也越容易误触发。运营团队说,因为有用户反馈唤醒不灵敏,所以他们把灵敏度调低了,想提高唤醒率。

这个改动看起来很合理,但它就是问题的根源!

灵敏度调低之后,唤醒芯片的误触发率大幅上升。以前误触发很少,去抖动机制几乎不起作用。现在误触发多了,经常会在用户说话之前就产生一个误触发,然后去抖动机制启动,接下来500毫秒内的真正唤醒词就被忽略了。用户发现没反应,会马上再说一遍,但还是在500毫秒的去抖动窗口内,又被忽略了。直到用户停顿一下,超过500毫秒之后再说,才能成功唤醒。

这就完美解释了所有现象:为什么问题从晚上八点开始(配置推送的时间),为什么是非必现的(取决于误触发和用户说话的时机),为什么需要说好几遍(去抖动窗口内的唤醒都被忽略)。

找到原因的时候,已经是凌晨三点了。我盯着屏幕,又困又兴奋。困是因为熬了一夜,兴奋是因为终于找到了真凶——一个看似无关的配置变更,通过一连串的因果链,最终导致了用户端的诡异Bug。

修复和反思

找到原因之后,修复就简单了。我做了三件事。

第一,把唤醒灵敏度调回0.7,先恢复线上服务。运营团队想提高唤醒率的初衷是好的,但这个参数不能随便调,需要经过充分测试。

第二,修改了去抖动机制。原来的去抖动是简单的时间窗口,我改成了更智能的方式:如果连续两个中断都是有效的唤醒词(而不是噪音导致的误触发),就不去抖。这样既保留了去抖动的功能,又不会影响正常的连续唤醒。

第三,加了监控和告警。以后任何配置变更,如果导致唤醒失败率超过阈值,就自动告警,并且可以自动回滚。不能再让一个配置变更悄无声息地搞垮线上服务。

修复上线之后,唤醒失败率很快降回了2%以下。用户的反馈也正常了。

这次排查经历让我有很多反思。

首先,在AI眼镜这种新硬件平台上做开发,和传统的软件开发有很大不同。硬件、固件、驱动、系统、应用,每一层都可能出问题,而且层与层之间的交互非常复杂。一个应用层的Bug,根源可能在硬件的GPIO信号上;一个配置参数的改动,可能通过内核的去抖动机制影响到用户体验。做这种开发,需要对整个技术栈都有了解,不能只懂自己那一层。

其次,非必现的Bug虽然难排查,但只要有耐心、有方法,最终都能找到原因。关键是要系统性地排查,从最常见的原因开始,一个一个排除,然后逐步深入。不要一上来就怀疑最复杂的地方,也不要忽略看似无关的细节。很多时候,真凶就藏在那个你觉得"不可能有问题"的地方。

第三,配置变更的风险被严重低估了。我们通常觉得代码变更需要严格的测试和评审,而配置变更比较随意,改个参数就上线了。但实际上,配置变更的风险一点也不比代码变更小,特别是当配置参数影响到底层系统行为的时候。任何可能影响线上服务的变更,都应该经过测试和评审,都应该有监控和回滚机制。

第四,日志和监控是排查Bug的生命线。这次排查之所以能找到原因,很大程度上是因为我们在各个环节都有日志,有比较完善的监控数据。如果没有这些数据,这种非必现的、跨层的Bug根本不可能在一夜之内找到原因。做开发的时候,一定要重视日志和监控,它们是你线上出问题时的救命稻草。

写在最后

天亮的时候,我关上电脑,走到窗边,看到外面已经有人开始晨跑了。熬了一夜,身体很疲惫,但心里有一种解决问题之后的踏实感。

做开发就是这样,大部分时间是平淡的写代码、改需求、开会,但偶尔会遇到这样的夜晚——一个诡异的Bug,一场通宵的排查,一次豁然开朗的顿悟。这些夜晚虽然辛苦,但也是这份工作最有魅力的地方。你在和一个看不见的对手博弈,你用逻辑、经验和耐心一步步逼近真相,最终找到那个隐藏在系统深处的错误。

AI眼镜是一个很新的领域,很多技术还不成熟,很多坑还没有人踩过。我们作为这个领域的早期开发者,注定要经历很多这样的夜晚。但每解决一个问题,我们就对这个平台多了一分理解,产品也就成熟了一分。

这一夜的排查,让我对AI眼镜的整个技术栈有了更深的理解,也让我对配置管理、监控告警、跨层调试有了新的认识。这些经验,比写多少行代码都宝贵。

最后想说一句:线上出Bug不可怕,可怕的是出了Bug之后找不到原因,或者找到了原因但不去反思。每一个Bug都是一次学习的机会,每一次通宵排查都是一次成长。只要我们能从每次故障中吸取教训,系统就会越来越稳定,我们也会越来越强。