一行报错,三个坑,五层调用链

事情的起因很简单:我想让 OpenClaw 通过 ACP(Agent Client Protocol)调用 Claude 来执行任务。点了一下,终端吐出三行字:

1
2
3
[client] initialize (running)
[client] session/new (running)
[error] RUNTIME: Internal error

“Internal error”——最没用的报错信息,什么都没说。

如果是三年前,我大概会去论坛搜一圈、发个帖、等几天。但现在,我把这行报错扔给了 AI,说:“帮我查查怎么回事。”

接下来发生的事,让我对"用 AI 排查问题"这件事有了全新的认识——既看到了它的强大,也看到了它的局限。


第一步:拨开迷雾,找到真正的错误

AI 做的第一件事是去翻 gateway 日志:

1
2
3
[gateway/ws] ⇄ res ✗ agent errorCode=UNAVAILABLE
  errorMessage=AcpRuntimeError: acpx exited with code 1
  code=ACP_TURN_FAILED

“Internal error"变成了"acpx exited with code 1”。有了方向,但还不够。

AI 接着手动执行了 acpx claude exec "Say hello",这次 stderr 里终于吐出了真话:

1
2
Failed to authenticate. API Error: 401
"OAuth token has expired. Please obtain a new token."

OAuth token 过期? 但我们用的是 API key 啊,什么时候冒出来一个 OAuth token?

第二步:梳理调用链——6 层嵌套,每一层都可能出问题

在动手修之前,AI 先梳理了完整的调用链。这一步很关键,因为后面发现的三个坑分别藏在不同的层:

1
2
3
4
5
6
7
OpenClaw Gateway (LaunchAgent 进程)
  → acpx 插件
    → acpx CLI(子进程,继承 env)
      → npx @zed-industries/claude-agent-acp(ACP 适配器)
        → @anthropic-ai/claude-agent-sdk
          → spawn Claude Code CLI(又一个子进程)
            → 认证 → Anthropic API

6 层嵌套,环境变量通过 { ...process.env } 逐层继承。任何一层"加料"或"漏传",都会出问题。

第三步:第一个坑——macOS Keychain 里的幽灵

AI 在 12MB 的混淆 JS 里一个字节一个字节地翻(后面会说这其实不是好做法),找到了 Claude Code CLI 的认证优先级:

1
2
3
4
1. ANTHROPIC_AUTH_TOKEN 环境变量 → 当作 OAuth token(最高!)
2. macOS Keychain "Claude Code-credentials" → OAuth token
3. settings.json 的 env 配置
4. ANTHROPIC_API_KEY 环境变量(最低)

然后检查了 Keychain:

1
2
3
4
$ security dump-keychain | grep "Claude Code"
svce: "Claude Code-credentials"
acct: "yuanming"
cdat: "20250612"  # 2025年6月创建

找到了。 去年用过一次 Claude 官方 OAuth 登录,token 存进了 Keychain。之后一直用 API key,但这个过期的 token 一直静静地躺在那里,优先级比 ANTHROPIC_API_KEY 还高。

1
2
$ security delete-generic-password -a "yuanming" -s "Claude Code-credentials"
password has been deleted.

删掉。再试——错误变了,从"OAuth token expired"变成了"Authentication required"。进步了,但还没完。

第四步:第二个坑——一个变量名的歧义

.zshrc 里有两个看起来差不多的变量:

1
2
export ANTHROPIC_AUTH_TOKEN="sk-ss-v1-18ede..."  # ← 这个
export ANTHROPIC_API_KEY="sk-ss-v1-18ede..."      # ← 和这个

值一模一样,但 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 环境里实际上不存在——但在我们手动测试时会踩到,造成了排查的干扰。

第五步:第三个坑——“能跑"和"能跑"不是一回事

排除了前两个坑后,我们做了一个对比实验:

1
2
3
4
5
6
7
# 直接用 CLI 运行 → 成功!
ANTHROPIC_API_KEY="sk-ss-v1-..." node cli.js -p "Say hello"
# 输出: Hello! How can I help you today?

# 通过 acpx 运行 → 失败!
ANTHROPIC_API_KEY="sk-ss-v1-..." acpx claude exec "Say hello"
# 输出: [error] RUNTIME: Authentication required

同样的环境变量,同一个 CLI,一个能跑一个不能。

差异在哪?直接运行用 -p 参数(print 模式),走的是一条简化的初始化路径。而通过 acpx 运行时,SDK 用 query() 函数启动 CLI,走的是完整的 stream-json 初始化路径——这条路径会读取 ~/.claude/settings.json

更关键的是,ACP 适配器在消息流里检测到了 CLI 输出的登录提示:

1
2
3
4
// 如果 CLI 说"请登录",适配器直接报认证失败
if (message.content[0].text.includes("Please run /login")) {
    throw RequestError.authRequired();
}

最终修复:~/.claude/settings.json 中配置 API 信息:

1
2
3
4
5
6
{
  "env": {
    "ANTHROPIC_API_KEY": "sk-ss-v1-...",
    "ANTHROPIC_BASE_URL": "https://zenmux.ai/api/anthropic"
  }
}
1
2
3
4
5
$ acpx claude exec "Say hello in one sentence."
[client] initialize (running)
[client] session/new (running)
Hello! How can I help you today?
[done] end_turn

终于。


三个坑的全景

#原因修复
1Keychain 幽灵 token去年 OAuth 登录存的 token 过期了,但优先级比 API key 高security delete-generic-password
2变量名歧义ANTHROPIC_AUTH_TOKEN 会被当 OAuth token,不是"另一种 API key”不要在 ACP 环境中设此变量
3SDK 有自己的配置通道query() 走完整初始化,需要 settings.json 中的 env 配置~/.claude/settings.json 添加 env

做对了的事

在排查这个问题之前的同一个晚上,我们已经用 AI 成功解决了好几个 OpenClaw 的问题。回头看,做得好的排查都有一个共同点:先实验,后分析。

acpx 包名 bug:20 分钟,从发现到社区修复

升级 OpenClaw 时报错 npm pack @openclaw/acpx 404。AI 没有猜答案,而是:

  1. 在打包产物里搜 @openclaw/acpx 字符串 → 定位到 5 个文件
  2. 对比两条代码路径:手动安装有 findBundledPluginByNpmSpec fallback,升级路径没有
  3. 找到根因:不是包名错了,而是升级路径缺少容错逻辑

带着完整的根因分析去 GitHub 提了 issue #32380。10 分钟内,两个开发者分别提交了修复——一个改路径解析,一个直接改包名。提 issue 的人用 AI,写 fix 的人也用 AI。

做对了什么:没有止步于"包名写错了"这个表面原因,而是追问"为什么手动装能成功",找到了两条路径的差异。

消息 11 分钟延迟:日志时间线一目了然

用户 1:35 发消息,1:46 才收到回复。AI 拉出日志按时间排列:

1
2
3
4
5
6
01:35:31  SIGTERM → 重启
01:35:50  SIGTERM → 重启
01:36:32  SIGTERM → 重启
01:37:18  SIGTERM → 重启
01:42:00  new model: contextWindow=16000 ← 这不对
01:46:29  compaction safeguard: cancelled

四次重启 + 模型参数异常(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 分钟就够了

实际走过的弯路

1
2
3
4
看日志 → 读 acpx 源码 → 读 adapter 源码 → 读 SDK 源码
→ 用 dd/grep 逐字节啃 12MB 混淆 CLI(找函数 Ix、SZ、mV、X1、Ik6、p_...)
→ 找到 Keychain 逻辑 → 删 Keychain → 继续啃源码找优先级
→ 尝试加 approved list → 不行 → 最后试了 settings.json → 成功

本该走的路

1
2
3
4
5
1. stderr 拿到具体错误(3秒)
2. "OAuth token expired" → 查 Keychain → 删除(2分钟)
3. 还报错 → 对比实验:CLI 直接跑 vs acpx 跑(5分钟)
4. CLI 能跑 → 差异在 SDK 路径 → 查文档 → settings.json(5分钟)
5. 搞定(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 费——其中一半是学费。