我给 OpenClaw 杀了 47 次僵尸进程,终于想明白了一些事
我最近开始会下意识地数一件事:这套东西今天又让我杀了几次进程。
我不是突然迷上了 kill -9,只是这套链路确实常把人逼到这一步。
而是当 OpenClaw 真正被我接进 daily-digest、浏览器自动化、公众号草稿箱这些真实工作流之后,我发现最消耗人的,往往不是“模型回答得对不对”,而是失败之后那堆残局。
有些进程看起来已经结束了,其实还挂着;有些页面明明已经登录了,脚本却告诉你“未登录”;有些任务不是没做成,而是卡在最后一米,留下一个你不得不自己进去收拾的半完成状态。
前前后后,我给 OpenClaw 杀了 47 次僵尸进程。
47 次听起来像个标题党数字,但它对我来说更像一张工程账单:我开始真正意识到,agent 一旦从聊天框走向工作流,最先暴露的不是模型上限,而是系统下限。
真正难的,是它做砸之后,你能不能把现场收回来。
这 47 次“杀进程”,其实都落在 4 类失败上
我后来复盘,发现这些看似随机的崩法,最后都能归到四类。
第一类,是卡死但不退出。
任务表面上已经没反应了,实际上子进程还挂着,端口还占着,浏览器连接也没真正释放。你下一次再跑,同样的脚本、同样的参数,却会撞上完全不同的异常。很多人会把这种问题误解成“模型不稳定”,但根因通常跟模型没什么关系,就是资源没清干净。
第二类,是退出了,但状态没清干净。
这类最烦。因为它给你的错觉是“上一次已经结束了”。可实际上,临时页面还开着、旧 tab 还挂着、草稿态残留、缓存 DOM 还在。你以为自己是在跑一次全新任务,系统却在一个半旧半新的状态上继续往前滚,于是错误会越来越诡异。
第三类,是连到了错误页面,或者错误的 tab。
这几乎是 browser workflow 里最常见、也最容易被忽略的一类故障。脚本以为自己拿到的是目标页面,实际上只是“长得像目标页面”的另一个 tab。尤其在 CDP 9222 这种会复用现有浏览器上下文的环境里,只要多开几个相似页面,选择器、URL、等待时序一脆,自动化就很容易连错对象。
第四类,是报错信息把人带偏。
这是我觉得最坑的一类。因为它会把你从真正的根因旁边拉走。你明明遇到的是 tab 选择问题,报错却写成“未登录”;你遇到的是状态残留,报错却看起来像 selector 失效;你遇到的是进程没回收,表面症状却像接口抖动。
这时候你如果不先把系统状态拆开看,就会进入一个很熟悉的循环:重跑、报错、杀进程、再重跑。
<!-- diagram:workflow-framework -->
这也是我后来为什么开始把“杀进程”单独当成一个信号:它不是操作动作本身有多重要,而是它提醒我,这条工作流已经开始为失败支付额外成本了。
最典型的一次:公众号草稿注入,为什么会把“已登录”误报成“未登录”?
最近一次特别典型的故障,就发生在公众号草稿注入这一步。
这条链路原本很简单:文章写完,生成公众号版 Markdown 和 HTML,然后通过 9222 连接到已经登录的浏览器,把内容注入到微信公众号草稿箱里,最后只保存草稿,不公开发布。
按理说,这已经是非常保守的一种自动化了。对外动作只到草稿层,真正发布仍然留人工确认。
但它还是翻车了。
一开始脚本报错的口径非常直接:无法从 URL 提取 token,可能未登录。
如果只看这句提示,结论几乎是现成的:微信登录态失效了。
但这次我没有立刻接受这个结论,而是回过头去核查前置事实。结果很快发现,问题根本不是“没登录”。
当天的 9222 浏览器里,mp.weixin.qq.com 页面其实是存在的,而且已经登录。更关键的是,当前页面 URL 里明确带着 token,标题是“公众号”,账号也是正确的“随机比特”。换句话说,登录态、tab、账号,这几个最关键的事实都成立。
那脚本为什么还会说“未登录”?
后来把日志和页面状态对起来看,根因就出来了:不是用户没登录,而是脚本过早抓到了错误的公众号 tab。
更具体一点说,这类脚本通常会先扫一遍 http://localhost:9222/json 返回的目标列表,优先找 URL 里已经带 token= 的公众号页面;如果当下没找到,就会退回一个“任意 mp.weixin.qq.com 页”。这个降级逻辑听起来合理,但一旦你刚新开了一个公众号页,或者页面还在跳转途中,事情就会变得微妙。
因为这时候最先被抓到的,很可能是一个长得对,但还没完全准备好的 tab:域名对了,页面也像是公众号后台,但 URL 还没带 token,跳转还没结束。脚本如果在这个时间点做结论,就会把一种短暂的过渡态,误报成“未登录”。
这就是典型的 agent 工程问题。
问题不在模型不会写文章,也不在微信真的掉线,而在自动化脚本对“目标页面已就绪”这件事的判断太脆弱。它把“暂时没拿到状态”误判成了“状态根本不存在”。
这类问题如果放在聊天框里,其实不会出现。因为聊天框没有状态机,也没有外部页面要接。
但一旦你让 AI 去碰浏览器、草稿箱、登录页、已有 tab、复用会话,这些状态判断就会立刻变成主战场。
我后来开始强行区分三种问题:模型、脚本、环境
以前我调 agent 系统时,有个很常见的坏习惯:只要任务没跑通,就统称为“AI 出错了”。
后来我发现,这个说法几乎没什么诊断价值。
因为至少要区分三种完全不同的问题。
第一种是模型问题。
比如理解错了需求、漏读了上下文、生成结果偏题。这些确实属于“AI 本身没做好”。它们的修法通常是改上下文、改约束、改任务边界。
第二种是脚本问题。
比如某个等待条件写得太乐观,tab 选择逻辑过于粗糙,或者错误恢复路径根本没设计。这类问题跟模型无关,换十个模型都一样会翻车。它们本质上是流程设计缺陷。
第三种是环境问题。
浏览器上下文漂移、登录态变化、旧页面残留、端口被占、子进程没回收、外部站点响应变化——这些问题不但真实,而且常常最贵。因为它们会制造大量“看起来不像根因”的假症状。
一旦把这三类混在一起看,人就会很容易陷入一种假勤奋:不停重跑,不停换 prompt,不停怀疑模型,但真正该修的清理逻辑、等待逻辑、恢复逻辑,一直没人碰。
所以我现在会逼自己先问一句:
这次失败,到底是理解错了,流程错了,还是环境脏了?
这句分类问题,比继续重跑一遍有用得多。
后来我真正补的,不是 prompt,而是失败出口
也是从这些坑里出来之后,我开始重新定义一条工作流到底算不算“能用”。
以前我的标准比较乐观:能自动跑通一次,就算这条链路成立。
现在我会加一个更苛刻的判断:
它失败一次之后,能不能被顺手收拾干净?
如果失败后会留下孤儿 tab、残留进程、半成品草稿、无法判断真假的报错,那这条链路就还不算成熟。它只是勉强演示成功过。
所以后来我补的东西,很多都不酷。
我后来补的,不是什么更长的 prompt 或更花哨的 agent 编排,而是这些朴素但关键的东西:
- 失败时明确 stop,不要悄悄悬挂
- 重试前先 cleanup,不在脏状态上硬跑第二遍
- 浏览器自动化先验证目标页已经存在、已经登录、已经带 token
- 对外动作只到草稿层,别让失败直接穿透到发布层
- 每条链路都留人工接管点,别让系统在错误状态下继续自作主张
这也是我现在越来越在意的一件事:
agent 真正值钱的地方,从来不是“它会不会给你一个看起来聪明的回答”,而是“它在复杂、易脏、会漂移的真实环境里,能不能留下一个你接得住的现场”。
<!-- diagram:three-workflows -->
如果一个系统每次失败都得你重新理解一遍现场、手动扫尾一遍,那它就算某次成功了,也还谈不上真正省时间。
我最后想明白的一些事
杀了 47 次进程之后,我最后想明白的不是“以后要更小心”。
而是下面这几件更具体的事。
第一,Agent 不是更聪明的聊天框,而是一套会留下残局的系统。
只要它开始接浏览器、接文件、接脚本、接消息渠道,它就一定会进入状态管理问题。这个阶段里,模型智能当然重要,但不再是唯一主角。
第二,可靠性不是锦上添花,而是工作流能不能成立的地基。
你可以接受一篇草稿不够锋利,再改一版;但你很难接受一条链路每次到最后都要自己进去捞残局。因为后者消耗掉的,不只是几分钟,而是你对这条系统的信任。
第三,真正该留给人的,不只是“最终审批”,还有失败时的接管权。
这也是为什么我越来越坚持一个原则:对外发布、发消息、花钱、改线上状态,这些动作前面都必须有人工闸门。不是因为我不相信 AI,而是因为我越来越相信真实系统一定会出错。
最后,47 次不是战绩,也不值得浪漫化。
它只是很诚实地提醒我:当你把 AI 从聊天框里放出来,让它开始替你跑工作流时,你买到的不只是效率,还有一整套工程责任。
这套责任里最重要的一部分,不是成功路径,而是失败之后怎么收场。
这件事想明白之后,我反而对 agent 更有信心了。
因为我终于知道,真正该补的不是幻想,而是地基。
数据来源:OpenClaw 官方文档 https://docs.openclaw.ai;OpenClaw 官方仓库 https://github.com/openclaw/openclaw