老周的工具笔记

老周的工具笔记

已发布 24 篇文章

技能调试指南:日志、最小复现与常见报错定位

技能调试指南:日志、最小复现与常见报错定位

技能写不顺,十次有九次不是逻辑问题,是「你没看清它到底干了什么」。OpenClaw 的技能本质上是一段被 agent 在合适时机加载、执行的指令加脚本组合,出错时栈信息不会像编译型程序那样直接拍你脸上。调试技能,拼的是顺序和读日志的本事。很多人排错喜欢凭直觉改两行,改完发现好了,过两天又坏,因为他从来没真正看清错误发生在哪一层。

下面这套流程我自己在排坑时反复用,按它走,基本不会绕远路。它不依赖任何花哨工具,只依赖你愿意慢下来、按顺序看。

一、调试的正确顺序

不要一上来就改代码。固定一个顺序,每一步都有明确的交付物:

  1. 复现:能不能稳定触发?偶发的问题先别动,先抓到必现路径。连复现都做不到,后面的所有修改都是在赌博。
  2. 看日志:错误到底报在哪一层——agent 调度层、技能加载层,还是脚本执行层。不同层的错误,修法完全相反。
  3. 隔离变量:是 SKILL.md 的指令问题,还是 scripts/ 里的脚本问题,还是外部工具(MCP/HTTP)的问题。先把锅分清楚。
  4. 最小复现:把无关内容删掉,只留能触发错误的最小集。这是调试里最被低估的一步。
  5. 定位并修复:改一个点,验证一个点,别一次性改五个地方。多变量同时改,你永远不知道哪处生效,也永远不知道哪处埋了新雷。

核心心法:一次只动一个变量。这是所有调试的金线,谁破谁后悔。

二、日志在哪、怎么读

技能运行产生三类信号,必须分层看,别混在一起:

层级来源看什么
agent 调度日志运行终端 / openclaw 的会话输出技能是否被加载、是否匹配到触发条件、工具调用顺序
技能自身输出SKILL.md 引用的脚本 stdout/stderr脚本内部逻辑错、返回值、打印的上下文
系统/环境日志沙箱、MCP server 日志权限不足、网络不通、进程崩溃

读日志有个习惯:先找 ERROR / WARN,再找前后相邻的 INFO。大多数报错不是孤立一行,错误行前面的「最后一次成功动作」才是根因线索。比如你看到脚本报错 command not found,往上翻两行往往是「正在进入 scripts/build.sh」——说明路径或执行环境不对,而不是脚本内容错。再比如看到 Permission denied,别急着加权限,先看它到底在访问哪个路径,那个路径本就不该被访问的情况也很多。

日志级别通常分 DEBUG / INFO / WARN / ERROR。排错时把级别调到 DEBUG(具体开关名以官方文档当前版本为准),能看到工具入参和中间状态,定位快很多。但要注意,DEBUG 会刷屏,定位完记得调回去,不然正常使用时日志噪声会淹没真正的异常。

三、怎么建最小复现

最小复现的目标是:删到不能再删,错误还在。它强迫你把问题从「这个技能好像偶尔会炸」变成「执行这条指令必炸」,难度直线下降。

步骤:

  1. 复制一份技能目录到临时位置,原目录先备份,避免越改越乱。改坏副本不影响线上。
  2. 把 SKILL.md 里和报错无关的步骤整段删掉,只留触发报错的那一条指令。
  3. 把 scripts/ 里无关脚本移走,只保留被调用的那个。
  4. 用固定输入反复跑——不要每次换说法,否则你分不清是输入变了还是技能变了。把输入写死在一个测试文件里。
  5. 如果删到空壳还能报错,说明问题在加载/配置层;如果删着删着不报了,最后删掉的那块就是雷区。
# 建一个隔离副本,原地改副本
cp -r ~/.openclaw/skills/my-skill /tmp/my-skill-debug
cd /tmp/my-skill-debug
# 之后只在这个副本里做删减实验,原目录保持干净

最小复现最大的价值不是「找到 bug」,是「让 bug 可被讨论」。你能把一个 200 行的技能砍成 15 行还能复现,发给同事一眼就能看出问题,而不用对着两千行聊天记录猜。

四、几类高频报错与排查路径

4.1 技能装了但 agent 当不存在

先看是否真的被加载。触发条件(如 frontmatter 里的匹配描述)写得不清晰,agent 就不会在合适时机调它。排查:

  • 用列表/校验类命令确认技能在已装清单里(具体命令以官方文档当前版本为准)。
  • 检查 SKILL.md 的 frontmatter 必填字段是否齐全。
  • 检查是否有同名技能冲突——后加载的覆盖先加载的,或反之,导致你以为装的是新版本其实是旧版。

4.2 脚本执行失败

最常见三类:

  • 路径错:脚本里写死绝对路径,换环境就炸。正确做法是用 {baseDir} 之类的变量指代技能根目录,或在脚本里基于自身位置算相对路径。
  • 权限错:scripts/ 下的脚本没加可执行位(chmod +x),或沙箱默认不允许执行。
  • 依赖错:脚本里 import / require 的包环境里没有。把依赖写进技能自带的说明或锁定文件,别假定宿主机有。
# 本地先单跑脚本,确认不是技能包装的问题
bash scripts/step.sh
# 如果这里就失败,问题在脚本本身,和 agent 无关
# 如果这里成功但技能里失败,问题在加载/传参层

4.3 工具调用报错

工具(含 MCP 工具)调用失败,先区分是「调用格式错」还是「工具端错」:

  • 入参字段名/类型不对 → 回 SKILL.md 校正指令,明确字段。
  • 工具返回错误码 → 看工具侧日志,多半是鉴权或网络。
  • 超时 → 指令里没约束输出规模,工具吐了超大结果把上下文撑爆。

4.4 上下文超长 / token 超限

症状通常是「跑到一半开始胡说」或「中途截断」。根因往往是技能让 agent 反复读大文件、反复贴长输出。修法:让脚本只输出结论和关键行,别把整文件 dump 给模型;大结果落地成文件,SKILL.md 只传路径;长任务考虑拆子代理(见本系列第 04 篇)。

4.5 钩子报错导致任务中断

钩子(见本系列第 02 篇)在非预期情况下返回非零退出码,会直接阻断后续步骤。排这类问题先把钩子临时摘掉跑一遍:如果摘掉就正常,问题在钩子;如果还炸,问题在别处。这个排除法非常快,建议优先做。

4.6 环境差异导致的「我这边好好的」

这是协作排错里最磨人的一类。你本地能跑,别人机器上炸,或反过来。差异通常来自:操作系统(路径分隔符、换行符)、Python/Node 版本、是否装了某个全局包、沙箱是否开启。修法:在技能里显式声明依赖与环境假设,能容器化就容器化,至少用一份 requirements 或 package.json 锁住。

五、一个完整调试走查

假设现象是「技能跑一半,生成报告时报错,但重试有时能成功」。按上面顺序走:

  • 复现:固定同一份输入连跑五次,发现三次成功两次失败 → 非纯随机,怀疑外部抖动或竞态。
  • 看日志:失败的那次,错误行前是「正在写入 tmp/report.md」,错误是 EACCES → 怀疑写文件的权限或目录。
  • 隔离:单独跑写文件那步脚本,成功。说明不是脚本本身,而是 agent 调度时的工作目录和手动跑时不同。
  • 最小复现:在技能里只保留「写文件」一步,必现失败。确认根因是工作目录不对,脚本用了相对路径 tmp/,而 agent 实际 cwd 不在技能目录。
  • 修复:把路径改成基于 {baseDir} 的绝对路径,再跑,连续十次成功。

你看,整个过程没有靠猜,全靠日志指路、最小复现收敛。

六、一张排错速查表

下面这张表是我每次排错时先在脑子里过一遍的清单。它的价值不在于「覆盖所有情况」,而在于帮你建立排查的优先级——很多人排错慢,是因为在同一现象上乱试,而不是按「最可能的原因」从前到后依次排除。请记住:表里的顺序是按「发生概率 × 排查成本」排的,越靠前的越该先查。

现象优先怀疑第一步动作
技能完全不触发触发条件/加载失败查已装清单与 frontmatter
加载后第一句就错SKILL.md 指令/路径单跑脚本,隔离指令
偶发失败输入不稳定/外部抖动固定输入,抓必现
越跑越乱上下文污染砍输出规模,落盘
中途卡死钩子/超时摘钩子、加超时
结果对但慢多余步骤最小复现后删废指令
本地行远端炸环境差异锁依赖、查沙箱

七、调试工具箱:几个顺手的小技巧

除了上面的大流程,日常还有几样东西能大幅提速:

  • 准备一份「脏输入样本集」:把历史上触发过 bug 的输入存下来,每次改完技能拿这套样本跑一遍回归。比临场想输入稳得多。
  • 给脚本加 -x 追踪:bash 脚本开头加 set -x,执行时把每一行展开打印出来,哪一步没按预期走一眼可见。这是定位「脚本自己悄悄失败」的利器。
  • 二分删减法:不确定哪段指令出问题,就把 SKILL.md 从中间劈两半,先删后半跑,再删前半跑,快速锁定问题区间。比逐行读快。
  • 对比法:同一份输入,在「能跑的旧版本」和「跑挂的新版本」各跑一次,diff 两者的日志输出,差异处就是雷。改代码最怕没有对照,对照一立,方向就清。

八、什么时候该放弃当前路径、重写而非硬修

调试不是无限投入。出现下面信号,就该停手重写了,而不是继续打补丁:

  • 为了修一个 bug,你已经加了三个特判分支,且彼此开始冲突。
  • 最小复现怎么都建不起来,说明你对它的执行链路理解本身就模糊——这时候该回去重读 SKILL.md 和官方文档,而不是猜。
  • 同一个症状换了三种修法都只是「暂时好了」,根因始终没浮出来。

补丁越打越厚的技能,最后会塌。宁可花半小时重写一个干净的版本,也别在泥潭里再补第四块补丁。重写不是浪费,是把你调试过程中已经搞清楚的逻辑,重新组织一遍。

调试的本质是把不确定性变成可观测。日志给你眼睛,最小复现给你手术刀。先把这两样用熟,后面钩子、触发器、子代理的坑,排起来都是同一个套路,你不会再被「它好像偶尔会坏」这类模糊描述困住。