电话接通了,Bender没有回应。用户在那头说"我听到了你,但你没有回应"。这句话开启了一周的排查,最后挖出六个底层管道Bug,外加一次服务商卡顿——那次卡顿看起来和这六个Bug一模一样。没有一个是模型变笨了。它们形状相同:某个管道环节对"通话是否正在进行"给出了一个自信的错误答案,而代码信了。
如果你在做任何需要判断"人类是否在另一端"的东西,这些坑你都会踩到。六个Bug全部在一台Mac上撞齐。
通话接通的瞬间,监听进程死了
监听进程通过一条硬编码的解释器路径拉起语音循环,指向.venv。这个目录不知何时被改名成了venv,旧路径已死,于是Popen在监听进程内部抛出FileNotFoundError,恰好在来电那一刻杀死了监听进程。电话被接起,什么都没拉起来,也没有进程留下来记录原因。
修法刻意做得很无聊:从候选列表里解析解释器,逐个用import探测,回退到sys.executable,并把拉起动作包起来,失败必须写日志——负责监视失败的进程,不该是那个先死的进程。
检测是瞎的,而看门狗把瞎当成了死亡
这台Mac没有摄像头,FaceTime把通话中界面画在辅助功能树够不到的地方:零个AX节点,窗口本身只写着"连接一个摄像头"。辅助探测在一次448秒的通话里全程返回idle。在这台无摄像头的机器上,两个检测器在整通电话里都这么干。看门狗读到idle,判定通话结束,在接通约14秒时关掉了一个活着的会话。
替代信号来自音频侧。通话期间循环持有两个虚拟音频设备,通话在时两者都读作IsRunningSomewhere = True,不在时读作False。这成了现在的检测器。看门狗规则随之改变:明确的ended立即关闭,连续四次坏读数才挂断,未确定的读数保持会话存活。
再往下一层,同样的Bug以小字号重演。监听进程过去遇到单个unknown就关闭,而检测在历史数据库拷贝竞争时会闪烁为unknown一两个轮询周期,一次闪烁就能杀死通话。现在的门槛是连续六次unknown读数,约三秒。群组通话也需要自己的路径,因为它们根本不发ringing,直接进入connected。
挂断信号是反的
通话进行中,CallHistory没有行。行在通话完成时出现,携带最终时长。旧检查把duration == 0读作"仍在通话",两个方向都错,因为一次失败的拨号会让0.0永远留在那里。
活下来的信号是一个快照:会话开始时记录最新的Z_PK,任何更新的行都意味着通话已结束。这是这台机器上唯一诚实的挂断来源,因为持有音频设备的进程永远无法检测到自己这通电话的结束。
进程还活着,音频已经挂死
sounddevice在OpenAndSetupOneAudioUnit内部死锁,卡在mach_msg等待。sample抓到了确切的栈帧,一个裸的InputStream也以同样方式挂住。sudo killall coreaudiod能解开,因为该守护进程按需重启,设备打开恢复瞬时。
几天后它又回来了,而coreaudiod是无辜的。真正的持有者是audiomxd,自启动起就处于R状态,29天、约94% CPU,负载均值在48到88之间,空闲为零。杀掉这个守护进程,音频立刻解开。现在的规则是:音频打开挂死时,先读进程状态,再决定重启哪个显而易见的守护进程。
循环把故障养活了
这一条让故障看起来像永久的。卡住的循环持有虚拟音频设备,检测因此读到"已连接",看门狗于是再拉起一个循环,两个循环争抢一个设备。这个楔子能扛过coreaudiod重启,所以它读起来像硬件问题。
证据是两次运行:停掉看门狗、杀掉残留进程,音频瞬时打开;让它们跑着,每次打开都挂。由此得出的规则:每通电话一个循环,空闲时零个循环,两侧同时强制,外加一个看门狗在通话中杀重复、无通话时杀残留。
那通从未响起的电话
在这一切之上,还有一个Agent代码碰不到的故障。一个屏幕共享会话开着,macOS在来电响铃之前就把它抑制了。日志写得毫不含糊:outcome: suppressed; reason: display shared。一个通知设置——镜像或共享显示器时的那个——是关的。打开它之后,接下来五分钟里48次允许解析、48次成功,零次抑制。Agent没有漏接电话,是操作系统从未让这通电话存在过。
服务商加了四分钟死寂
管道修好后,一通实时通话仍然静了四分钟。日志有它的形状:20:19:03派发,20:23:07第一个工具输出,而工具轮本身只花了3.1秒。下一轮重演:20:23:18派发,20:27:31完成,即约11.6秒的工作被4分13秒的死寂包裹。客户端任何地方都没有超时,于是一个拥堵的服务商队列看起来和挂死一模一样。
修法是给每次大脑调用加35秒的socket读超时。流式增量会持续重置它,因此它限制死寂,又不会切断一个真正在思考的模型。它背后是一个兜底模型,接手同一段对话,实测一条调用路径1.9秒、另一条1.8秒。重连时另有一个限制咬了我:问候语携带了2500字符的通话尾部,三次触发Context append text must not exceed 500 tokens,每次都静默死掉。上限压到900字符。
由此得出的规则
- 失败要开放。未确定的读数保持会话存活,只有明确的结束才关闭。把"我判断不了"当成"已经结束"的看门狗,会在任何检测天生失明的机器上杀死活通话,而大多数机器都是这样。
- 单一持有者。每通电话只允许一个进程持有音频设备,空闲时零个。每个重复都是未来的反馈回路。
- 进程活着不等于在工作。卡死的守护进程保留PID、内存和状态,然后在第一行真正的工作前停住。测那个东西本身,别测进程表。健康脚本现在每次运行都打开音频设备,它的沉默就是一切正常的信号。
自愈器每五分钟跑一次,有五件事:通话中杀重复、杀卡死的循环让看门狗干净重启、无通话时杀残留循环、设备打开挂死时重启音频守护进程、看门狗倒了就踢它一脚。它不行动就不出声。
我会先查什么
如果你的Agent静音了,别从模型查起。从最后一个报告"正在发生什么"的地方查起,然后问它:当它不知道的时候,它打印什么?
热门跟贴