我在过去 4,000 次提交中犯的所有错误
作为 Browser Use 首位工程师的一年错误,按时间顺序排列。
我于2025年4月加入 Browser Use,成为第一位工程师。
在接下来的一年里,我提交了超过4,000次代码,构建了支撑数百万次智能体运行的云基础设施。我也犯了所有单兵作战、推进过快的工程师都会犯的错误。有些让我们损失了金钱,有些让我们流失了用户,而所有这些都教会了我一些本可以轻松学到的东西——如果我当初能慢下五分钟。
以下是按时间顺序排列的这些错误。
早期(2025年4月 - 6月)
没有部署成功的首次部署、错误的 AWS 区域,以及安装了根本不需要的浏览器。
我在 Browser Use 推送的第一次部署没有成功。Lambda 位于没有 NAT 网关的私有子网中,而且数据库安全组没有包含 Lambda。一次部署中出现了两个互不相关的问题。没有预发布环境来捕获它,因为我还没有搭建。
我还设法将 us-west-1 硬编码到了 Lambda 日志 ARN、SNS 策略和 ALB 证书中。我们在 us-east-2 中运行。Lambda 无法写入日志,我花了令人尴尬的大量时间进行调试,直到我逐个字符阅读 ARN 并注意到了区域。跨预发布和生产环境共三次提交才修复它。几个月后,同样的错误又在另一组实例的 SSM ARN 上重演了。
我花了3个小时和六次提交,试图在 Lambda 容器内安装 Chromium。系统依赖、手动安装、不同的 Playwright 模式。然后我意识到我们是通过 CDP 连接到远程浏览器的。从来没有任何理由需要安装本地浏览器。
计费漏洞(2025年7月 - 8月)
用户免费获得金钱、付款在任何人付款之前就已通过,以及让 LLM 调用免费的舍入错误。
我们的“领取免费额度”端点给了用户 $10。用户模型上存在 free_credits_claimed 布尔值。我们在授予额度后将其设置为 True。我们之前从未检查过它。一条 if 语句修复了它。然后一分钟后,第二次提交将错误改为静默返回,因为用户可能会双击该按钮。
额度扣除使用 Python 字典中按用户 ID 键控的 asyncio.Lock。在单个进程上有效,但在多个 Pod 之间不起作用。不同 Pod 上的并发请求可能会在两个进程都不知情的情况下重复花费额度。我将其替换为 SELECT ... FOR UPDATE,这是我一开始就应该使用的。但这也反过来咬了我们一口:在规模上,每个计费事件都试图独占锁定同一用户行,后端在等待行锁时陷入停顿。我们没有正式的账本,只有用户模型上的就地额度字段,每个并发请求都在争夺它。
计费还有其他问题。额度转换使用了 round(),而 round(0.4) 是 0,所以任何花费不到半个额度的 LLM 调用都是免费的。我们的 Stripe webhook 在 customer.subscription.created 上授予额度,而 Stripe 在付款实际成功之前就会发送它,所以用户可以注册、获得每月额度,然后从不完成付款。这些中的每一个都是一行代码的修复,本应在发布前被发现。
同一个错误三次(8月19日)
一天之内,三个文件,每次都是同样的错误。外加一个阻止付费用户的 datetime 错误。
8月19日是一天内三次 UnboundLocalError 修复。全部都是相同的模式:在 try 块内定义的变量在 except 块中被引用。如果 try 在赋值之前失败,该变量就不存在。那天我在三个不同的文件中修复了同一个错误,这正是那种让你怀疑自己还有什么没看到的事情。
同一天,数据库存储的是朴素时间戳,而代码使用的是 datetime.now(timezone.utc)。Python 拒绝比较朴素时间和带时区时间。这导致订阅检查崩溃,挡住了付费用户,还导致任务过滤器崩溃,破坏了 API。我用一个 .replace(tzinfo=None) 的临时手段修复了它。真正的修复是一个月后对 62 个文件的重构。在我们解决根本原因之前,这类 bug 出现了三次。
同一周:我把字符串常量重构为枚举,并更新了所有引用,唯独漏掉了那个真正将模型名称传递给 LLM SDK 的函数。Anthropic、Azure、Google、Groq 收到的都是 Python 枚举对象而不是字符串。每次智能体运行都坏了,而错误信息毫无用处。
没人要求的功能(2025 年 8 月 - 10 月)
一个没人想要的 GIF 生成器、一个只存在了六周的计费服务,还有一只动画仓鼠。
我花了一个下午构建一个基于 Lambda 的 GIF 生成器,用于智能体运行预览,然后又花了一个下午与内存作斗争:批处理、截图上限、ImageMagick 限制、垃圾回收,最后放弃 ImageMagick 改用 ffmpeg。2 小时内提交了 4 次。一周后我把它全删了,602 行基础设施。没人需要 GIF 生成,也没人注意到它消失了。
在这段时间里,我还构建了一个专门的计费服务。第一天队列类型就搞错了,然后是内存分配,然后是超时。我还是把它部署到了生产环境,六周后用 Autumn 替换它时又把整个东西移除了。在此期间,我在调试 S3 访问时设法把 AWS 凭证打到了 stdout。我把它发布到了生产环境,它在日志里待了 2 天我才注意到。
这段时期我最自豪的提交信息是:“Remove the fucking hamster.”
重构(2025 年 11 月 - 12 月)
移动了一个文件,弄坏了一切。12 个修复提交,其中五个在午夜,然后回滚了整个改动。
12 月下旬,我决定通过将 main.py 移动到 app/ 来清理项目结构。这弄坏了 ECS,因为 uvicorn 目标不对;弄坏了 Docker,因为 COPY 路径不对;还弄坏了配置,因为它造成了循环导入。
接下来是 12 个以上的修复提交,其中五个是在 12 月 29 日午夜提交的。我尝试添加 __init__.py,然后更改 uvicorn 目标,最后放弃并回滚了整个改动。然后我发现配置提取破坏了 SSM 参数注入,因为 pydantic-settings 在构造时会快照 os.environ。构造后再注入就太晚了,所以我也把它反转了。
在重构破坏部署流水线之后,我需要一种触发测试部署的方法。我的做法是:注释掉 Stripe 依赖,推送,看看部署是否成功,然后回滚。有五条提交信息类似“Test a fucked deployment”和“Test a fucked deployment again.”
同一时期:设置了 WAF 速率限制,然后立刻屏蔽了我们自己的 Stripe webhook。在一次 YC 黑客马拉松之前,我把 WAF 限制完全移除了。我就那样放了好几周。
配置地狱(2026 年 1 月)
一次配置更改搞垮了三个服务。一个自动充值 bug 让客户被无限扣费。
将配置加载从 extra='allow' 改为 extra='ignore',并移除了独立的配置类。第二天加密功能坏了,因为加密配置类不复存在。44 分钟后认证坏了。30 分钟后支付坏了。一次配置变更,三个服务宕机,两天的修复。每个服务都有自己的配置模式,以各自的方式崩溃。
当 Stripe 要求 3D Secure 验证时,我们的自动充值不断重试,每次重试都是一次扣款。没有退避,也没有上限。我的修复方式是在需要用户操作时禁用自动充值标志,然后添加每小时一次的幂等键作为第二道防线。本不该在两者都缺失的情况下发布。
还有更隐蔽的 bug。我在 Lambda 处理程序中使用 asyncio.run(),冷启动时没问题,但热启动时事件循环已经关闭,第一次调用的所有异步客户端都挂在一个死掉的循环上。这种 bug 只在真实流量模式下才会显现。
内存泄漏马拉松(2026 年 2 月)
一天内七个提交,追查连接泄漏、看似存活的死 TCP 套接字,以及四个月后卷土重来的同一个 bug。
2 月 18 日有七个提交用于修复内存泄漏。每次 LLM 调用都会创建一个新的 httpx 客户端,每个客户端都分配一个永不关闭的连接池。我把网关改成单例,然后预构建 provider 实例,然后把计费客户端改成单例,然后缓存 HTTP 客户端,然后尝试向 Google SDK 注入一个有界的 httpx 客户端,然后因为 SDK 不是为此设计的而回退了它,最后清理了泄露内部细节的错误响应。
每个修复都暴露出下一个泄漏。我还发现 ssl.create_default_context() 耗费约 1.8 秒 CPU,因此每请求创建客户端在负载下会阻塞事件循环。花了一整天才明白 HTTP 客户端应该是单例。
我们的 agent 运行在查询之间会挂起的微型 VM 中。当 VM 唤醒时,每个 TCP 套接字都已死亡,但 HTTP 客户端并不知情。我的第一个修复使用了一个不存在的 API,导致沙箱崩溃。第二个修复使用了正确的 API,但单调时钟在挂起期间会冻结,所以计时器认为零秒已过,每个死连接看起来都是活的。我的第三个修复是在每次查询时销毁并重建客户端。我花了好几天想耍聪明,最后还是暴力方案赢了。
7 月的数据库连接 bug 也回来了。不同的端点,同样的模式:在进行 HTTP 调用时持有数据库连接,导致连接池在负载下耗尽。这次是我们的前端工程师在后端写的代码,我在评审时没发现,因为我几个月前已经“修复”过这个问题,不再留意它了。
规模问题(2026 年 3 月)
我用负载测试弄死了自己的生产机器。然后一个第三方 SDK 让整个平台冻结了 4 分钟。
全年最大的事故是自找的。我在对 Unikraft 裸金属机器做负载测试,结果把它们弄死了。45 分钟的停机时间,我手忙脚乱地把它们恢复起来。运行我们 agent 沙箱的机器没了,因为我把负载测试指向了生产基础设施,没有想清楚在极限情况下会发生什么。
另一起重大事故:一个第三方代理 SDK 在内部进行同步 HTTP 调用,而我们是在异步处理器中调用它的。当他们的 API 变慢时,事件循环被冻结,并拖垮了所有端点。我们在 4 分钟内出现了 262 次网关超时。APM 让它看起来像是网络问题,因为所有 HTTP 调用同时变慢了,但实际上只是事件循环被冻结了,所以什么都无法完成。我们其实已经在 21 个调用点中的 1 个上应用了修复。那里的注释明确写着"将同步 SDK 调用卸载到线程以避免阻塞事件循环"。我们只是没有把它应用到所有地方。
我真正学到的东西
我的大多数错误都归结为几种模式。
我假设而不是去验证。我假设 SDK 是异步的,假设保活定时器能在虚拟机挂起后存活,假设 round() 对计费来说没问题。每一个假设最终都在生产环境中崩溃了,而每一次我都感到惊讶。
我没有测试完整路径就发布了。Stripe SDK、S3 事件处理器、后端重构、配置迁移。每一个在我脑子里都是好的,但一部署就坏了。然后我在线上生产环境中,一个提交一个提交地修复它。
我选择了聪明的方案而不是简单的方案。保活定时器 vs 重新创建客户端。带 asyncio.shield() 的 Singleflight vs 每个关注者的 future。聪明的方案总是那个需要更多修复的方案。
我没有完成我开始的事情。21 个调用点中只有 1 个做了同步转异步的修复。从未进行的数据库连接审计。每一个不完整的修复都给了我虚假的信心。
我构建了没人要求的东西。GIF 生成功能。一个独立的计费服务。602 行基础设施,用于没人使用也没人想念的功能。
这就是快速发布的真实样子。你搞砸了,你学到了,你又搞砸了,你又学到了。这个循环永不停止,但你在学习这件事上会变得更快。我们能增长得这么快,是因为我们能快速适应,而不是因为我们避免了错误。
"在演示中能用"和"在规模下能用"之间的差距大约是 4,000 次提交。这就是其中大多数提交的样子。