一行报错,三个坑,五层调用链
事情的起因很简单:我想让 OpenClaw 通过 ACP(Agent Client Protocol)调用 Claude 来执行任务。点了一下,终端吐出三行字:
| |
“Internal error”——最没用的报错信息,什么都没说。
如果是三年前,我大概会去论坛搜一圈、发个帖、等几天。但现在,我把这行报错扔给了 AI,说:“帮我查查怎么回事。”
接下来发生的事,让我对"用 AI 排查问题"这件事有了全新的认识——既看到了它的强大,也看到了它的局限。
第一步:拨开迷雾,找到真正的错误
AI 做的第一件事是去翻 gateway 日志:
| |
“Internal error"变成了"acpx exited with code 1”。有了方向,但还不够。
AI 接着手动执行了 acpx claude exec "Say hello",这次 stderr 里终于吐出了真话:
| |
OAuth token 过期? 但我们用的是 API key 啊,什么时候冒出来一个 OAuth token?
第二步:梳理调用链——6 层嵌套,每一层都可能出问题
在动手修之前,AI 先梳理了完整的调用链。这一步很关键,因为后面发现的三个坑分别藏在不同的层:
| |
6 层嵌套,环境变量通过 { ...process.env } 逐层继承。任何一层"加料"或"漏传",都会出问题。
第三步:第一个坑——macOS Keychain 里的幽灵
AI 在 12MB 的混淆 JS 里一个字节一个字节地翻(后面会说这其实不是好做法),找到了 Claude Code CLI 的认证优先级:
| |
然后检查了 Keychain:
| |
找到了。 去年用过一次 Claude 官方 OAuth 登录,token 存进了 Keychain。之后一直用 API key,但这个过期的 token 一直静静地躺在那里,优先级比 ANTHROPIC_API_KEY 还高。
| |
删掉。再试——错误变了,从"OAuth token expired"变成了"Authentication required"。进步了,但还没完。
第四步:第二个坑——一个变量名的歧义
.zshrc 里有两个看起来差不多的变量:
| |
值一模一样,但 Claude Code 对它们的理解完全不同:
ANTHROPIC_API_KEY→ 当作 API key 使用ANTHROPIC_AUTH_TOKEN→ 当作 OAuth token 使用(最高优先级!)
我们的 sk-ss-v1-... 是 zenmux 代理的 API key,不是 OAuth token。Claude Code 拿着这个"API key"去做 OAuth 认证,自然被拒。
好在 gateway 是通过 LaunchAgent 运行的,LaunchAgent 不读 .zshrc。所以这个坑在 gateway 环境里实际上不存在——但在我们手动测试时会踩到,造成了排查的干扰。
第五步:第三个坑——“能跑"和"能跑"不是一回事
排除了前两个坑后,我们做了一个对比实验:
| |
同样的环境变量,同一个 CLI,一个能跑一个不能。
差异在哪?直接运行用 -p 参数(print 模式),走的是一条简化的初始化路径。而通过 acpx 运行时,SDK 用 query() 函数启动 CLI,走的是完整的 stream-json 初始化路径——这条路径会读取 ~/.claude/settings.json。
更关键的是,ACP 适配器在消息流里检测到了 CLI 输出的登录提示:
| |
最终修复: 在 ~/.claude/settings.json 中配置 API 信息:
| |
| |
终于。
三个坑的全景
| # | 坑 | 原因 | 修复 |
|---|---|---|---|
| 1 | Keychain 幽灵 token | 去年 OAuth 登录存的 token 过期了,但优先级比 API key 高 | security delete-generic-password |
| 2 | 变量名歧义 | ANTHROPIC_AUTH_TOKEN 会被当 OAuth token,不是"另一种 API key” | 不要在 ACP 环境中设此变量 |
| 3 | SDK 有自己的配置通道 | query() 走完整初始化,需要 settings.json 中的 env 配置 | 在 ~/.claude/settings.json 添加 env |
做对了的事
在排查这个问题之前的同一个晚上,我们已经用 AI 成功解决了好几个 OpenClaw 的问题。回头看,做得好的排查都有一个共同点:先实验,后分析。
acpx 包名 bug:20 分钟,从发现到社区修复
升级 OpenClaw 时报错 npm pack @openclaw/acpx 404。AI 没有猜答案,而是:
- 在打包产物里搜
@openclaw/acpx字符串 → 定位到 5 个文件 - 对比两条代码路径:手动安装有
findBundledPluginByNpmSpecfallback,升级路径没有 - 找到根因:不是包名错了,而是升级路径缺少容错逻辑
带着完整的根因分析去 GitHub 提了 issue #32380。10 分钟内,两个开发者分别提交了修复——一个改路径解析,一个直接改包名。提 issue 的人用 AI,写 fix 的人也用 AI。
做对了什么:没有止步于"包名写错了"这个表面原因,而是追问"为什么手动装能成功",找到了两条路径的差异。
消息 11 分钟延迟:日志时间线一目了然
用户 1:35 发消息,1:46 才收到回复。AI 拉出日志按时间排列:
| |
四次重启 + 模型参数异常(16K 上下文窗口触发反复压缩)= 11 分钟延迟。修正参数后恢复正常。
做对了什么:性能问题先看时间线,不猜原因。
Gateway 状态警告:读判定逻辑再动手
Gateway 报 “Other gateway-like services detected”。AI 没有猜着改文件名,而是先读了检测逻辑——detectMarker() 扫描 plist 文件内容(不是文件名),匹配关键词列表 ["openclaw", "clawdbot", "moltbot"]。
知道了规则,就能精准修改:把 cloudflare tunnel plist 中所有含 “openclaw” 的内容替换掉(label、tunnel 引用改 UUID、日志路径),文件名也一并改了。一次搞定。
做对了什么:理解规则再行动,不是试错。
做错了的事
ACP 认证这次排查,AI 花了 2 小时,烧了大约 $50-70(约 200-500 元)的 token。但如果思路对,15 分钟就够了。
实际走过的弯路
| |
本该走的路
| |
核心问题: AI 陷入了"读源码找根因"的惯性。在混淆代码里用 dd 按字节偏移量读函数定义,一个函数接一个函数地追调用链——技术上很厉害,但方向上是浪费。一次对比实验的信息量,顶得上读 1000 行混淆代码。
复盘之后做的事
排查结束后,我们把这些经验写进了项目的 CLAUDE.md 文件——这是 Claude Code 的持久化记忆,下次对话会自动读取。
把内容拆成了两层:
全局规则(~/.claude/CLAUDE.md,所有项目生效):
- 先实验后读码、先文档后源码、设止损线、验证要干净、注意花费
- 7 种常见场景的"第一步"速查表
项目专属(~/.openclaw/CLAUDE.md,只在 OpenClaw 目录生效):
- ACP 调用链和认证配置
- Gateway 检测逻辑和关键文件路径
- 历史排查案例的简要记录
这样做的好处是:下次遇到类似问题,AI 不会从零开始,而是带着上次的经验来。而通用的排查原则跟着我去到任何项目。
这件事说明了什么
一个晚上下来,我们用 AI 排查了 5 个不同的问题。最快的 3 分钟搞定,最慢的 2 小时。差异不在问题的难度,而在排查的方法。
AI 擅长的事:
- 在几万行代码里搜索关键字符串,秒级定位
- 梳理 6 层调用链,理清每层的环境继承
- 按时间线整理日志,一眼看出异常
- 把根因分析组织成开发者看得懂的 issue
AI 容易犯的错:
- 沉迷于"从源码找答案",忘了做实验是更快的路
- 没有止损意识,一条路走到黑
- 技术上能做到的事(逐字节读混淆代码)不等于应该做的事
而人的角色是:
- 判断方向:该深入还是该换思路
- 设置约束:这个问题值得花多少时间和钱
- 提供上下文:昨天装过 OAuth、用了代理、改过 plist
AI 不会替代你思考。但如果你能提出正确的问题、在正确的时机喊停、在经验中持续修正方法——它就是一个极其强大的放大器。
而经验本身,也可以被写进 CLAUDE.md,变成下一次对话的起点。
本文基于 2026 年 3 月 4 日凌晨的真实排查过程。5 个问题,3 个小时,烧了大概 500 块 token 费——其中一半是学费。