故事是这样的。
今天下午 16 点 39 分,我把一条折腾了整整两天的通道彻底打通了,全链路实测通过的那一刻,我坐在电脑前说了句,成了。
这条通道叫 BFF,全称 Backend For Frontend,听起来特别唬人,其实干的事特别朴素,就是让 AI 直接操控浏览器。
我有个浏览器叫虾米,是个 Chrome 扩展,挂在浏览器里帮我干各种活,采集数据、投广告、管店铺。这玩意跟了我挺久,从 v5 一路迭代到 v6,功能越来越全,但我一直觉得哪里不对劲。
不对劲的地方在于,它跟我说话的方式太绕了。
以前虾米只能通过飞书跟我对话,我在飞书里发消息,它收到消息再执行,一来一回,中间隔着一层聊天软件。慢不说,还特别脆,飞书消息队列那种串行机制,一个活干完下一个才能上,中间要是断了还得重来,时间全耗在等上了。
我寻思,为啥不能搞一条直连通道,让 AI 直接把手伸进浏览器里呢。
然后就有了这个 BFF 直通渠道。
这条通道长什么样,说起来也简单,就三层。
最上面是 Agent 这端,装了一个叫 bff-client 的小插件,我这边调工具,它就往中间服务器发请求。中间是一台 Python 写的 WebSocket 中转服务器,叫 bff-server,跑在 19777 端口,负责认证、路由、排队。最下面是虾米扩展本身,它连上服务器,等着接活。
干活的时候流程是这样的,我在 Agent 里调一个 bffToolCall,说帮我打开某个网页,请求就到服务器,服务器验一下身份,然后丢给虾米扩展,扩展收到指令,在浏览器里真刀真枪地执行,最后把结果原路传回来。
整个过程走的是 WebSocket,实时双向,不经过任何聊天软件。
就为这个直连,我前后折腾了两天,踩的坑能写满一屏。
先说第一个坑,版本升级把 API 干没了。
bff-server 用的是 Python 的 websockets 库,一开始写的时候是按老版本写的,结果一跑就炸,报错说 send_json 不存在。
查了一下,17.0.1 版本把这个方法移除了,得改成 send(json.dumps(...))。
行,改。
改完又炸,process_request 的签名也变了,老版本返回三元组,新版本要返回 Response 对象,而且还得是 async 的。
行,继续改。
这种坑属于那种,你看着报错就知道是版本问题,但版本升级文档写得跟没写一样,全靠自己试。我一边改一边想,这大概就是维护开源依赖的宿命吧,它升级的时候可不会管你下游死活得。
然后是第二个坑,认证永远失败,查了半天发现是缺一个字段。
这个坑最阴。
两端都连上了,服务器也收到连接了,但客户端那边永远显示认证失败,试了各种姿势都不行。
我一度怀疑是 token 的问题,重新生成了好几个,都不行。最后把两端的代码摊开一行行对,才发现服务器返回的认证响应里少了一个 method 字段。
客户端是按 method 等于 auth 来识别认证响应的,服务器没返回这个字段,客户端就永远等不到认证结果。
就少一个字段,两边空转了半天。
这个事让我特别有感触,很多时候系统对不上,不是逻辑多复杂,就是接口契约两边没对齐,一个字段的差距,就是天壤之别。
基础链路通了之后,我以为差不多了,结果拉去真实 Chrome 一测,又爆出来五个 bug,一个比一个骚。
第一个是测试端口冲突,pytest 用固定端口跑并发,直接 Errno 98,改成自动随机端口才好。
第二个是幽灵连接,旧扩展连接失联了,但服务器不知道,新连接一注册,两条连接就打架。修复方式很暴力,注册新连接的时候强制把旧的关掉,简单粗暴,但管用。
第三个是队列丢消息,发送的时候一异常,消息就没了。修复是在发送异常时把消息放回队列,不丢。
第四个最有意思,保活 alarm 的周期设成了 0.4 分钟,结果 Chrome 扩展的 alarm 有最小合法值,0.4 分钟太短,直接被判非法。改成 0.5 分钟就好了。
第五个是 read_page 返回「标题未知」,原因是扩展刷新之后,旧标签页的 content script 没重新注入,而提取页面数据的函数没有重试机制。加了个 4 次乘以 500 毫秒的重试,跟执行其他动作对齐,就好了。
这五个 bug,每一个单拎出来都不算大,但合在一起就是一句话,真实环境永远是测试环境的爹。
你单测写得再漂亮,代码 review 再严格,拉到真实浏览器里一跑,该炸还是炸。这大概就是做浏览器自动化的宿命,你永远不知道用户的环境里藏着什么幺蛾子。
说到这我得讲一下我自己的一点点小坚持,就是这套东西的可靠性是拿测试堆出来的。
主仓库的 vitest 测试 171 个,全绿。服务器的 pytest 测试 17 个,全绿。bff-client 的测试 9 个,全绿。加起来快两百个用例,覆盖了认证、排队、超时、断线重连、并发、参数错误这些场景。
有人可能觉得,一个浏览器扩展搞两百个测试是不是有点过度了,但我的想法是,这种东西出一次 bug 的代价,比你写两百个测试的代价大得多。你想想,它是在真实浏览器里干活的,一旦在线上翻车,轻则数据采错,重则投放搞砸,那都是真金白银。
所以宁可测试写得多一点,也别让 bug 跑到线上去。
那为什么我觉得这条路值得走呢,说到底,BFF 直通渠道解决的是一个特别基础的问题,AI 怎么跟真实世界交互。
以前 AI 是隔着一层聊天软件跟浏览器对话,指令要翻译,结果要转述,中间任何一环断了,整个链路就废了。
现在直连了,AI 调工具,浏览器执行,结果原路返回,一气呵成。
我测了几个真实操作,打开网页、读取页面内容、获取属性、滚动页面,全部稳定通过。而且是在真实 Chrome 上跑的,不是模拟器,不是 mock,是真的把扩展装进浏览器里,一个指令一个指令地试。
那一刻我坐在电脑前,看着 AI 的指令在浏览器里变成真实的操作,页面自己打开了,内容自己读出来了,滚动自己滚了。
那个瞬间我就想,这才是 AI 该有的样子,不是停在对话框里跟你聊天,而是真的能伸手帮你干活。
说到这,我想到一个更大的东西。
我们一直在说 AI Agent,说智能体,但智能体的核心其实不是模型多聪明,而是它有多少只手可以伸出去,有多少条通道可以触达真实世界。
模型再聪明,如果只能输出文字,那它就是个聊天机器人。只有当你给它接上浏览器、接上代码环境、接上各种工具,它才真正开始干活。
BFF 这条通道,就是给虾米装的一只手。
而且这只手是双向的,AI 不仅能指挥浏览器,浏览器里的状态也能实时反馈给 AI,这就有意思了,AI 可以看着页面的变化做决策,像一个真的在操作电脑的人。
其实做这个事的过程中,我还想过一个更远的场景,就是多实例。
现在这套 BFF 是单浏览器跑的,但我的业务里其实有好几个浏览器要管,千川投放一个,抖店一个,直播一个,每个都是独立的账号体系。如果 BFF 能做成多实例的,一个中转服务器管多个浏览器,那 AI 就能在几个浏览器之间来回切换,像一个人同时盯着几台电脑干活一样。
这个方案我已经想好了,也写了评审,技术上完全可行,就等排期。
还有一个小细节我觉得值得说,就是这套通道的容错设计。
浏览器扩展有个头疼的问题,就是 Service Worker 会被 Chrome 回收,一回收,扩展就跟断了气一样。BFF 的解决方案是保活 alarm,每 0.5 分钟触发一次,让 Service Worker 一直活着。这个设计看着不起眼,但少了它,通道随时会断。
断线重连也是,指数退避,1 秒、2 秒、4 秒,一直退到 60 秒封顶,服务器一恢复,客户端自己就能爬回来,不用人工干预。
说实话我也不知道这套东西最后能长成什么样,但我知道一件事,今天下午 16 点 39 分,这条通道第一次全链路跑通的时候,我确定了一件事,方向是对的。
工具链的边界,就是 AI 能力的边界,而我在一点点把边界往外推。
可能很多朋友会问,这个东西我能不能用,能不能抄作业。我坦率的讲,这套代码就在我的仓库里,结构也清楚,三层架构,协议也简单,WebSocket 加 JSON,你只要有一个浏览器扩展,理论上都能照着搭一套。
但我得提醒一句,中间那些坑,版本兼容、字段对齐、真实环境翻车,一个都少不了,你踩的每一个坑,都会变成你代码里的一行注释。
这大概就是干这行的乐趣吧,永远有坑,永远在填坑,但每填完一个,系统就结实一分。
以上,既然看到这里了,如果觉得不错,随手点个赞、在看、转发三连吧,如果想第一时间收到推送,也可以给我个星标⭐~
谢谢你看我的文章,我们,下次再见。
本文作者:丘丘
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!