解读 193 · 读报错的艺术
解读对象:概念课 · 以
microduck-replica/构建日志.md「踩过的坑」一节为教材(全部真实踩坑记录)
需要的前置:解读 192(打印、断言与日志);第 186 篇(Python 环境:uv 入门)的uv run命令格式。
报错不是失败,是程序在用它的语言跟你说话。读得懂,每个报错都是一次免费的教学。构建日志的「踩过的坑」一节自评为"整份日志最有价值的部分——别人可以不重复踩",每条都按「现象 → 原因 → 解决」写。我们就精读其中三条,把"读报错"的方法论拆出来。
案例一:IndexError,报错在最上层,病根在最底层
现象:在一台没有 GPU 的 Mac 上跑 uv run train,程序直接崩,抛 IndexError: list index out of range(出自 mjlab 的 utils/gpu.py,函数 select_gpus)。
读法:IndexError 是"按编号取列表,编号超长了"。但先别急着在自己代码里找列表——先看崩在哪个文件的哪个函数:select_gpus,选 GPU 的。再问一句"为什么它会去选 GPU":训练器默认 gpu_ids=[0],即"用第 0 块 GPU";这台机器没有 CUDA,GPU 列表是空的,按 0 号取值就越界了。
解决:显式声明"我没有 GPU"——环境变量 CUDA_VISIBLE_DEVICES=""(这是 mjlab 官方约定的显式 CPU 模式,gpu.py 的文档字符串写明)。一条命令不改,环境先答话。
案例二:报错只说"出错了",答案在默认值里
现象:同一天的第二个坑——刚修好 GPU 报错,程序又卡在 wandb 上:默认的实验记录工具是 wandb,而本机没配它的 API key,直接报错。
读法:这类报错的根源往往不是你写错了什么,而是工具链替你做了一次你没同意的选择("默认用 wandb")。读法是反问:这个默认是谁定的?能不能换?
解决:换成不联网的 TensorBoard——--agent.logger tensorboard(与官方冒烟测试参数一致)。注意这条解决和案例一连起来才完整:报错经常是一个接一个出现的,修好第一个才会看见第二个。别指望一次跑通,也别因为"又报错了"而灰心——每次崩的位置都前进了。
案例三:程序不报错,但结果是错的——最贵的一类
现象:给评估脚本写输出路径时用了 ../rl-series/... 这样的相对路径,从 upstream 目录一运行,文件写到了错误的位置——日志原话:"本会话第二次栽在相对路径上"。
读法:相对路径的"参照物"是运行程序时所在的目录,不是脚本文件所在的目录。同一个脚本,换个目录启动,.. 就指去了别处。它难缠在不崩溃:文件照样生成,只是生成在错的地方,你事后找数据时才会发现不对。
解决:教训已固化成纪律——脚本内一律基于 __file__(脚本自身在磁盘上的位置)定位仓库相对路径,让"从哪运行"变得无关紧要。
你带走的收获
- 读报错三步:看哪一行哪个函数崩 → 问它为什么会走到这里 → 查哪个默认值或假设错了;案例一的病根是"默认要 GPU",案例二是"默认要 wandb"。
- 报错会连环出现:修好一个才轮到下一个,每个新报错都是进展的里程碑。
- 不崩溃的 bug 最贵(案例三):结果错了但不响,靠纪律(用
__file__定位)而非运气防御。 - 日志的「现象 → 原因 → 解决」三段式本身就是可复用的模板——没解决的也要诚实写"未解决"。
- 同一份日志里还有快速案例可自测:tyro 的布尔参数必须写
--agent.resume True;macOS 上launch_passive必须 mjpython;"localhost 打不开"先查lsof -i :8080端口有没有监听。
延伸
- 调试的基本功(打印、断言、日志):解读 192
- Python 环境与
uv run命令的由来:解读 186(uv 入门) - 本地:
microduck-replica/构建日志.md「踩过的坑」一节(2026-09-06 各条)