乐于分享
好东西不私藏

做一个团队小龙虾,接入OpenClaw该处理的问题

做一个团队小龙虾,接入OpenClaw该处理的问题

一、为什么要把 OpenClaw 做成内网工具

现在像 OpenClaw 这样的命令行 AI Agent 越来越火:能读代码、能执行命令、能调一堆工具,干起活来像个不知疲倦的实习生。但它们大多有个共同的局限:很强,但太像个人工具了。

每次用都要开终端、配环境、敲命令。熟悉的人觉得顺手,不熟悉的人会被挡在门外。更麻烦的是,团队里真要把它用起来,光靠”每个人本地跑一个”是不够的:权限怎么管?会话怎么留?任务跑到一半人走了怎么办?不同人能不能用不同的配置?

所以我做了件事:把一个命令行里的 Agent,包装成了团队内网里可以直接打开的 Web 工具。

做完之后,它变成了这样:

  • 打开浏览器就能用
    登录后每个人有自己独立的空间,互不干扰;
  • 多会话
    像聊天软件一样,左边一列会话,这个在查代码、那个在做检查,互不打架;
  • 实时流式
    agent 的回答一个字一个字蹦出来,调用了什么工具、跑到哪一步,全都实时显示在界面上,而不是干等几分钟然后甩一坨结果;
  • 能贴图
    截个图直接发给它看;
  • 关了页面也不怕
    后台任务继续跑,下次打开还在。

这只是第一层好处:把一个”极客专属”的命令行工具,变成了”全团队点开就能用”的内网应用。

更有想象力的是第二层。只要它从本地命令行变成了一个平台,就可以开始做很多原来不好做的事:

  • 按 skill 配不同模型:写代码走一个模型,查配置走一个模型,做长文档总结又走另一个模型;
  • 按 使用者或角色 配不同权限:研发能让它读仓库、跑测试,运营只能让它查数据、生成报告;
  • 按 事件 触发不同任务:代码合并后自动检查配置,线上告警后自动拉上下文,日报时间到了自动整理当天变更;
  • 把团队常用流程沉淀成按钮或模板,让不会写 prompt 的同事也能稳定使用。

这些能力不一定要在第一版全部做完,但只要先把 OpenClaw 这类 Agent 接进内网,后面就不再是”某个人会不会用命令行”的问题,而是”团队要把哪些流程产品化”的问题。

技术栈其实不复杂:后端用 WebSocket 连到 agent 的 gateway,前端用 SSE(Server-Sent Events)把流式结果推给浏览器。架构画出来一张纸都用不完。

但——真正有意思的不是把它做出来,而是真实用户开始用之后。

接下来一周,我连收两个反馈。每一个,表面看是个小毛病,挖下去都是一层”看不见”的坑。这部分才是我想重点讲的。


二、那几个”看不见”的 Bug

开胃菜:流式输出是怎么实现的

先花一分钟讲清楚 SSE,后面的 bug 才好理解。

你肯定见过 ChatGPT 那种”打字机效果”——字是一个个蹦出来的。这背后不是前端在变魔术,而是服务端和浏览器之间开了一条单向的长连接,服务端每生成一小段内容,就顺着这条连接”推”一条过去,浏览器收一条、显示一条。这就是 SSE(Server-Sent Events)。

它和普通的”请求-响应”最大的区别是:一次请求,服务端可以持续往回推很多条消息,直到它说”我说完了”为止。

注意最后那句——”直到它说我说完了“。这个”说完了”的信号怎么判断,正是后面两个 bug 的核心。先记住它。

Bug 一:「结果出来了,但页面上不显示,我追问才发」

用户跑了个耗时的检查任务。agent 回复”正在处理,稍等几分钟”,然后……就没下文了。用户等不及,追问”结果呢?”,agent 这才把结果贴出来。

诡异的是,用户说他去后台看,结果其实早就生成了,只是没推到页面上。

第一步:对比上下游两边的历史记录。

我手上有两份记录:一份是 Web 端自己存的会话历史,一份是 agent gateway 那边的原始记录。我把两边的时间戳逐条对齐

结果一目了然:gateway 那边,结果在 15:19 就产出了。而 Web 端存的那条回复,时间戳冻结在 15:16——正好是它说”稍等几分钟”的那一刻。之后直接跳到了用户的追问。

结论很明确:结果在 gateway 侧确实产出了,但 Web 端没接住,凭空消失了。

第二步:翻 git,锁定可疑改动。

git log 一翻,出问题前一天刚好有个 commit 改了”对话何时算结束”的判断逻辑。它把判断依据简化成了只看一个字段。高度可疑,但还不能下结论。

第三步:写诊断脚本,让真实系统自己说话。

光看代码猜不准。我写了个几十行的脚本:连上 gateway,开个临时会话,订阅它推送的所有事件,让 agent 跑一个秒级的小任务,把每个事件的类型、状态、ID 全打印出来。

跑完,真相大白:

[+6.6s]  对话结束,结束原因=空        ← 主流程这一轮结束了[+9.1s]  一个新的 run 启动了          ← 子任务完成,gateway 起了个新"唤醒"流程[+11.3s] 结果在这个新流程里产出[+11.3s] 对话结束                     ← 这才是真正的终点

原来这个 agent 派生子任务是异步的。主流程说完”稍等”就自然结束了,但子任务会在几分钟后,在一个完全独立的新流程里把结果推回来。

而我们的代码,主流程一结束就以为整件事完了,把事件订阅给取消了。等几分钟后子任务的结果到达——已经没有人在听了。结果直接掉进了虚空。

还记得前面那句”直到它说我说完了”吗?问题就出在这:我们把”主流程这一轮停了”误当成了”它说完了”。

修复:让连接在派生子任务后保持订阅,一直等到子任务结果真正产出才收工。同时加了个后台兜底——万一用户中途关了页面,转后台继续接收、落盘,下次刷新还能看到。

Bug 二:「消息明明完成了,按钮却一直转」

修完上一个,我以为这块稳了。结果新反馈来了:消息内容完整显示出来了,但发送按钮一直卡在”运行中”,用户得手动点停止才恢复。

这次,是我自己上一个修复埋的雷。

上一个 bug 我是怎么修的?我用了个看似聪明的办法:数数。派生一个子任务计数 +1,每来一个”结束事件”就 -1,减到 0 才算真完。当时实测过,一个子任务流程”恰好”有两个结束事件,配得严丝合缝。

但”恰好“是工程里最危险的词。

我又写了诊断脚本,这次故意测了不同的组合,结果发现结束事件的个数根本不固定

场景
结束事件个数
派生子任务、不等待
2 个
派生子任务、紧接着等待
1 个
并发派生 2 个子任务
2 个,但状态各不相同

出问题的会话,正好是”派生 + 等待”——只有 1 个结束事件。我的代码 +1 之后只等到 1 个,永远减不到 0,于是流永远不结束,按钮永远转。

更打脸的是,我想换个更可靠的判断信号,结果实测把我另外两个想当然的判断也一并打掉了

  • ❌ 数事件个数——派生后跟不跟”等待”,个数就变;
  • ❌ 看某个 ID 后缀——我以为唤醒流程的 ID 有特定后缀,实测发现那是另一个字段,ID 里压根没有;
  • ❌ 看状态字段——并发时两个流程的状态字段一个这值、一个那值,不固定;
  • ✅ 只有一个靠谱:唤醒流程的 ID 有个稳定的前缀

于是改成:数子任务个数,但只认带这个前缀的结束事件来递减。四种场景全部实测通过,按钮终于乖乖复位了。


三、几个能带走的方法论

这两个 bug 有个共同点:都发生在”看不见”的地方——几分钟后的异步回调、第三方系统的内部状态。没有报错、没有崩溃,只有”行为不对”。这种 bug 最磨人。

我反复用的就两招,但极其管用:

  1. 对比上下游两边的记录。 时间戳一对齐,”丢在哪一段”立刻浮出来——是根本没产出,还是产出了没接住,一眼分辨。

  2. 写一次性诊断脚本,让真实系统自己说话。 与其盯着代码猜第三方会发什么事件,不如花二十分钟写个脚本连上去,把所有事件打出来。我后来甚至专门去测并发场景——就是不想再让”恰好”这个词出现在我的判断里。

还有个反直觉的决定:到这个阶段,我都没有急着给项目上一整套日志系统。不是反日志,也不是偷懒——而是上面两招已经够用,过早搭一套完整设施反而容易给自己加负担。我把这个决定也写进了项目的排查笔记,约定”等这两招都搞不定某个 bug,再上更完整的日志”。

工具要在真正需要时才加。在那之前,会对比、会写脚本实测,比什么都强。

这大概也是给 AI agent 做工程化最真实的一面:模型本身很强,但把它稳稳地、对所有人跑起来,坑全在那些”看不见”的接缝里。


(完)