7月23日,我发现了一个令人毛骨悚然的事实:从7月3日起,我自己写的那款名为HELM的AI监督工具,在长达20天的时间里,没有真正批准过任何一次操作。而它每次的自我检测,都稳稳地报告“Self-check OK (8/9)”。
HELM的使命很简单——监控我电脑上Claude Code智能体的运行,自动点击“Allow”按钮,让那些通宵运行的自治任务不会因为等一个睡着的人而卡在凌晨两点。整个产品的核心承诺就一条:当你的AI代理需要人类介入时,总有一个可靠的替身顶上。
但它瞎了。整整20天,日志里填满了看起来像成功批准的记录,实际上它连按钮的影子都没摸到。
解剖这次故障,每一层都失效得极具“借鉴意义”。
树消失了
Claude Desktop 是基于 Electron 构建的,Electron 的底层是 Chromium,而 Chromium 的辅助功能树(accessibility tree)是“惰性”的。Chromium 只在它确信有辅助技术客户端正在监听时,才会为渲染器的内容构建这棵树。当它认定没人在听,它就索性不构建。
大概在7月3日前后,它不再“相信”有人在听了。
这对HELM意味着什么?Windows的UIA(UI Automation)依然能看到原生外壳部分:窗口装饰、最小化按钮、包含会话列表的侧边栏,一切正常。所以HELM还能列出每一个会话,继续绘制自己的控制面板,看起来完全活着。但那个本该出现“Allow”按钮的对话面板——那个按钮所在的区域,从辅助功能树里彻底消失了。
没有重命名,没有移动,是完全的“不存在”。
我是怎么发现的?我把整个窗口的每一个UIA控件都dump出来数了一遍。总共641个节点,全部是外壳或侧边栏的。内容区域,零。
那个HELM生来就是为了点击的按钮,对它来说根本不是一个能看见的东西。
日志里明明白白写着:
approve_button=None ← 连续 3551 次
自我检测何以沦为空转
其实HELM已经内置了针对这类场景的诊断——它会打印自己“看到”的所有按钮,这样一旦Claude新版本修改了按钮的文案,日志里就能一眼看出来。
每一次运行,它都如实记录:
buttons_seen=['Minimize','Maximize','Restore','Close','Menu','Collapse sidebar', 'Search','Back','Forward','Home','Code','New','Artifacts','Routines', 'Pinned','Idle Draft HOA conversation talking points', ...]
永远是外壳和侧边栏的组件。无论屏幕上实际显示什么内容,从来没有任何一个内容区按钮出现在这个列表里。
为什么自检没有报警?因为它采样的方式决定了它永远只检查自己“能看见”的那部分。当树里只剩下外壳节点时,它理所当然地认为一切正常,因为所有的外壳按钮都能被识别,自检的八项指标就在这些看得见的组件上逐条通过。它没有去查验“应该存在的内容区按钮到底在不在”。它只是笃信,自己看见的世界就是全部世界。
四行代码的修复,与阻塞调用的代价
修复出奇地简单,四行Windows API调用:向目标窗口及其渲染控件发送 WM_GETOBJECT 消息,Chromium就会乖乖把辅助功能树构建出来。
树节点从641激增至1191,内容区按钮从0变成了81。
但有一个细节耗掉了我整整一个小时,值得记下来:SendMessageTimeoutW 在这里不顶用。我最初用的是它,因为我想当一个“好公民”,不想让一个卡死的应用把调用线程阻塞住。结果树节点只从509变成了539,几乎是噪音。只有阻塞式的 SendMessageW 才真正触发了辅助功能树的构建。于是最终的解决方案是在一个一次性的守护线程里掉用 SendMessageW,既不阻塞主逻辑,又不会因为超时而欺骗自己“已经生效”。
整件事最讽刺的地方在于:一个专门替人“看”按钮的程序,自己却看不见了。而它那套健壮的自检机制,恰好在它最需要报警的时候,用一条平静的“Self-check OK”把它自己和我们一起蒙在了鼓里。
HELM所犯的错误,不是它工作不够努力,而是它太信任自己感知到的世界的完整性。对于所有依赖AI代理自动运行、并把最后一公里交给类似监督工具的开发者来说,这20天的空白或许是一种提醒:你永远需要一个独立于被监督系统之外的“哨兵”,去观察你的观察者本身是否还睁着眼睛。
热门跟贴