自动化项目跑久了你会发现一个反直觉的事实——写主流程其实不难,难的是出问题之后能不能复现。
界面识别错了一次,到底是模型识别失败,还是当时画面卡了?决策错了一次,是局面本身刁钻,还是上游传了脏数据?这些问题,单看一行日志根本答不上来。
最早遇到这种问题的时候,处理方式很无脑——让用户截图发群里、问”刚才操作过什么”、隔一段时间自己也忘了细节。一来二去,复现成功率约等于零。
后来痛定思痛,搞了一套”失败现场打包”机制。出错的时候自动把那一刻能拿到的所有上下文一起冻起来,事后能完整复盘。
一个失败包里该装什么
先看实际跑出来的目录长这样:
1 | |
这一套打包出来通常 1~5 MB。对比起原来”日志里一行 ERROR + 用户截图模糊得没法看”,信息量差出几个数量级。
代码上对应一个工具类,封装在 src/utils/task_failure_package.py,配合 task_dumper.py 做现场冻结。
为什么不光记日志
最常见的问题:日志能解决一切吗?
不能。原因有几个:
1. 日志只记”已知重要”的信息
你日志里记的是你写代码时认为重要的字段。但一旦遇到没预想到的失败,那些”没被记进日志”的字段往往才是关键——比如某个 buff 状态、某个隐藏 modal 的出现。临时加日志?等你下次复现已经是一周后了。
2. 日志没有”那一刻的画面”
视觉自动化最怕的事就是”日志说找到了一个按钮,但你不知道那是个什么按钮”。截图一截,立马明白——哦,是个广告 popup,YOLO 误识别成正常按钮了。
3. 日志是文本,结构化数据丢了
YOLO 返回的检测框是结构化的(坐标、类别、置信度),日志只能字符串化。事后想重新跑一遍 NMS 或者过一遍分类,原始数据已经不在了。
失败包补的就是这些”日志补不到”的部分。
打包的时机
最容易踩坑的设计点——什么时候打包?
最朴素的想法:在最外层 try-except 里包一下。问题是这时候很多上下文已经被 GC 掉了。比如那一帧的截图,任务往下走了几步可能已经被新截图覆盖。
我们的做法是在关键步骤进入时记一个”快照点”,异常发生的时候,从最近的快照点开始打包:
1 | |
调用方在每个有意义的步骤开始时调用 snapshot(),失败时由外层 try-except 调用 dump_on_failure()。snapshot 的开销是个浅拷贝 + 一次截图,可控。
截图的几个坑
视觉自动化里截图是失败包的灵魂,但坑也最多。
坑一:截图被覆盖
主线程持续抓屏,每次都写到同一个 numpy buffer。snapshot 的时候不复制一份,过一帧你拿到的就是新画面,不是失败那一刻的画面。
解决:snapshot 时立即 .copy(),强制内存隔离。
坑二:截图过大
高分辨率全屏 PNG 一张能到 3~5 MB。一天攒几十个失败包,磁盘吃不消。
解决:写盘时缩放到 1280px 宽(短边等比),用 JPG 而非 PNG,单张降到 200~400 KB。但要注意——用于复现的截图必须能再喂给模型推理,所以缩放比例要可逆,或者干脆同时存一份原图、一份压缩图(原图设短保留期,压缩图长期保留)。
坑三:含隐私信息
游戏画面可能含玩家 ID、昵称、好友列表。失败包要发给开发者排查时,这些都得脱敏。
解决:截图打包前过一道 mask——把已知的隐私区域用纯色覆盖。config_snapshot.json 也走脱敏函数,token、cookie、密码一律 [REDACTED]。
meta.json 是排查的入口
打包目录里第一个该看的就是 meta.json:
1 | |
snapshot_age_ms 这个字段特别有用——告诉你失败那一刻距离最近的 snapshot 隔了多久。隔得短,说明截图就是案发瞬间;隔得久(比如几秒),可能这中间发生了别的事,要看 recent_logs.txt 补完。
task_duration_ms 是另一个隐性指标——任务正常完成只要几秒,但这次跑了几十秒还失败,多半是某个等待环节卡住了。
一次真实的排查
举个真实例子说明这套东西怎么用的。
用户反馈”自动购买偶尔会失败”。打开他发来的 failure 包:
- meta.json:异常是
ElementNotFoundError,找不到”购买确认”按钮 - screenshot.png:屏幕上确实没有按钮——出现了一个”商品已售罄”的提示框
- yolo_detections.json:YOLO 正确检测到了 “SoldOutModal”,置信度 0.94
- task_context.json:任务状态显示”等待购买确认按钮出现”
- recent_logs.txt:日志里有一行 “Skipping unknown modal: SoldOutModal”——原来代码里见过这个 modal,但没写关闭逻辑
诊断结论清清楚楚——商品售罄的情况没被处理。修起来 5 分钟,写测试用例验证完事。
如果只有日志,光看 “ElementNotFoundError” 这个异常名根本指不到方向。
落地的几条规矩
写一套失败包系统不难,难的是用得起来——很多类似系统造出来之后没人用,最后失活。我们这套能持续运转,靠的是几条死规矩:
1. 默认开启,不能依赖用户手动启用。
加个开关让用户”如果遇到问题再打开”,那等于没做——用户遇到问题的时候开关是关的,错过现场。常态运行,磁盘吃得起。
2. 失败包目录要简单可压缩。
logs/failure_* 一个目录一个失败,用户一键打包成 zip 发上来。别搞数据库、别搞分布式存储,普通文件夹最香。
3. 大小要有兜底。
设个上限——比如最多保留 50 个失败包,超过的按时间删旧的。免得磁盘吃光。
4. 失败包本身打包失败不能影响主流程。
dump_on_failure 内部全部 try-except 包住,自己挂了写个 ERROR 日志就完事,绝不能往上抛——主任务已经在异常路径上了,再来一波异常就乱了。
5. 脱敏机制提前写好。
不是出问题之后再想”哦对截图里有手机号”。脱敏函数随 dump 一起写,每个失败包出去前都过一遍。
收个尾
自动化项目跑久了你会发现,复现能力比解决问题的能力更稀缺。
你能修任何 bug,前提是 bug 能复现。复现不了的 bug,再厉害的人也只能干瞪眼。失败包就是把”无法复现”这个最大的拦路虎搬走——出问题的那一刻发生了什么,全冻在那个目录里,你随时可以回去看。
写这套机制大概花了几天工夫,回头看是这个项目里 ROI 最高的几件事之一。每次有人在群里说”刚才出错了”,我都说”把 logs 目录里最新的那个失败包发我看看”,剩下的就好办了。
不打失败包的自动化项目,跟没装行车记录仪的车一样——平时没事,出事就抓瞎。