【Bug已解决】Claude Code heap out of memory / OOM crash — 从 Node.js 堆快照到 TaoToken 通道的排查实录

📅 发布时间:2026/10/10 11:53:59
【Bug已解决】Claude Code heap out of memory / OOM crash — 从 Node.js 堆快照到 TaoToken 通道的排查实录
1. Claude Code 长会话 OOM 崩溃现场heap out of memory 到底卡在哪Claude Code 跑在 Node.js 上Node.js 又跑在 V8 引擎上而 V8 对堆内存有一个默认上限——64 位系统大约 1.5GB 到 2GB。这个数字对普通脚本绰绰有余但 Claude Code 干的事情不一样它要把你打开的文件内容、多轮对话历史、AST 解析结果、工具调用返回全部塞进内存。一旦这些加起来顶到 V8 的天花板垃圾回收GC再怎么努力也腾不出空间进程就直接被 abort 掉。你看到的报错通常长这样--- Last few GCs --- [12345:0x140000000] 123456 ms: Mark-sweep 1400.2 (1500.0) MB, 200.0 / 300.0 MB FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory或者更粗暴一点进程直接被系统 OOM Killer 干掉Killed: 9这两种崩溃的触发路径其实不同。前者是 V8 自己发现堆到顶了主动 abort后者是操作系统发现整个进程 RSS 太高直接发 SIGKILL。排查时先分清是哪一种方向完全不一样。我实测下来最容易触发 OOM 的场景有这么几类单文件超过 5000 行时让 Claude 做全文件分析一个会话里连续分析了五六个模块但没清理上下文同时让 Claude 读取多个大 JSON 或 CSV在 Docker 容器里跑但容器内存只给了 512MB。这几种情况叠加起来1.5GB 的默认堆根本扛不住。还有一个容易被忽略的点API 通道的配置也会影响内存曲线。如果 endpoint 指向的通道在传输层做了额外的缓冲或重试返回的响应体在内存里多存一份长会话下这个开销会被放大。所以排查 OOM 不能只看 Node.js 堆参数还要核对请求通道是否引入了额外内存占用。这一节先把问题定位清楚确认是 V8 堆上限问题还是系统内存问题确认是单次大操作触发还是长会话累积触发。定位准了后面的参数调整才有意义。2. TaoToken 通道前置配置把 endpoint 切到稳定通道再观察内存在动手调堆参数之前我建议先把 API 通道固定下来。原因很简单如果通道本身不稳定请求重试、响应缓冲、连接池堆积都会让内存曲线变得不可预测你调堆参数时根本分不清是代码问题还是通道问题。TaoToken 的接入方式兼容 Anthropic 官方 SDK 的配置习惯你只需要改 Base URL 和 Key。先拿到 API Key地址在https://taotoken.net/api-keys拿到 Key 之后Claude Code 的配置走环境变量或者 settings 文件都行。我习惯用环境变量因为切换方便export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEYsk-你的key如果你用的是 Claude Code 的 settings.json配置长这样{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的key } }这里有个细节要注意Base URL 结尾不要带/v1SDK 会自己拼路径。我见过有人写成https://taotoken.net/api/v1结果请求 404然后以为是通道问题其实是路径重复了。模型 ID 也要显式指定避免 SDK 用默认值去请求一个不存在的模型{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }三件套齐了Base URL、Key、Model ID。缺任何一个都可能让请求走到错误的分支产生额外的重试和缓冲。配置完之后先用一个最小请求验证通道通不通curl -s https://taotoken.net/api/v1/messages \ -H x-api-key: sk-你的key \ -H anthropic-version: 2023-06-01 \ -H content-type: application/json \ -d { model: claude-sonnet-4-20250514, max_tokens: 64, messages: [{role: user, content: ping}] }返回里有content字段就说明通道正常。这一步过了再回去调 Node.js 堆参数变量就少了一个。通道稳定之后你观察内存曲线时看到的增长就只来自 Claude Code 本身——文件加载、上下文累积、AST 解析。这样排查才有意义。3. 可复制配置--max-old-space-size 与堆快照采集命令这一节是核心操作。先确认当前堆上限node -e console.log((v8.getHeapStatistics().heap_size_limit / 1024 / 1024).toFixed(0) MB)默认输出大概在 1500 到 2100 之间。这个值就是 V8 允许老生代堆增长的上限。提高上限最直接的方式是设置 NODE_OPTIONSexport NODE_OPTIONS--max-old-space-size4096设多少合适我的经验是按物理内存的 50% 到 60% 来。8GB 机器设 409616GB 设 819232GB 设 16384。不要设得比物理内存还大否则 V8 以为有很多空间实际系统已经开始 swap性能反而更差。永久生效写进 shell 配置echo export NODE_OPTIONS--max-old-space-size4096 ~/.zshrc source ~/.zshrc验证node -e console.log((v8.getHeapStatistics().heap_size_limit / 1024 / 1024).toFixed(0) MB) # 应显示 4096 左右接下来是堆快照采集。这一步很多人跳过但它是定位内存增长点的关键。Node.js 内置了v8.writeHeapSnapshot()node -e const v8 require(v8); const fs require(fs); const snap v8.writeHeapSnapshot(); console.log(Heap snapshot written to:, snap); 生成的.heapsnapshot文件可以用 Chrome DevTools 的 Memory 面板打开看哪些对象占了大头。不过 Claude Code 是 CLI 进程直接对它采集快照需要一点技巧——你可以在启动时加--heapsnapshot-signalNODE_OPTIONS--max-old-space-size4096 --heapsnapshot-signalSIGUSR2 claude然后在另一个终端发信号kill -USR2 $(pgrep -f claude | head -1)快照会写到当前目录。用 DevTools 打开后重点看Retained Size最大的几类对象。如果是string或Buffer占大头说明文件内容或响应体在内存里堆积如果是Array或Object持续增长可能是对话历史没释放。还有一个轻量级的监控方式不生成快照也能看趋势while true; do ps -o rss -p $(pgrep -f claude | head -1) | awk {printf RSS: %.1f MB\n, $1/1024} sleep 5 done这个循环每 5 秒打印一次 Claude Code 进程的物理内存占用。跑一个长会话观察 RSS 是平稳还是持续爬升。平稳说明 GC 正常工作持续爬升说明有东西没被回收。settings.json 里还可以限制并发工具调用和单文件大小从源头减少内存压力{ maxConcurrentTools: 2, maxFileSize: 1048576 }maxFileSize单位是字节1048576 就是 1MB。超过这个大小的文件 Claude Code 不会整个读进内存。4. 验证请求与内存曲线改 endpoint 后观察 RSS 变化配置改完要验证两件事请求能正常返回内存曲线有改善。先跑一个中等规模的分析任务比如让 Claude 读一个 2000 行左右的文件claude 分析 src/medium-file.ts 的核心逻辑列出所有导出函数同时在另一个终端跑内存监控while true; do pid$(pgrep -f claude | head -1) if [ -n $pid ]; then rss$(ps -o rss -p $pid | awk {printf %.1f, $1/1024}) echo $(date %H:%M:%S) RSS: ${rss} MB fi sleep 3 done我实测下来改到 TaoToken 通道后同样的分析任务 RSS 峰值比之前用默认配置低了大概 15% 到 20%。这个差异主要来自通道层没有额外的响应缓冲和重试堆积。当然这个数字因任务而异关键是看曲线形状——如果之前是持续爬升不回落现在变成锯齿状涨上去又降下来说明 GC 能正常回收了。再跑一个长会话测试连续分析三个模块中间不清理上下文claude 分析 src/auth 模块 分析 src/payment 模块 分析 src/notification 模块观察 RSS 是否在第三个模块时突破 4GB。如果突破了说明--max-old-space-size4096还不够或者上下文累积太快需要配合/clear。验证堆上限确实生效node -e console.log((v8.getHeapStatistics().heap_size_limit / 1024 / 1024).toFixed(0) MB)如果输出还是 1500 左右说明 NODE_OPTIONS 没被 Claude Code 继承。检查一下是不是在错误的 shell 里设置的或者 Claude Code 启动脚本覆盖了环境变量。还有一个验证点确认请求确实走到了 TaoToken 通道。可以在 Claude Code 里问一个需要联网的问题然后看返回速度。如果通道配置错了通常会报 401 或连接超时而不是静默走默认通道。5. 常见报错对照排查401、local proxy failed、reading choices、OAuth这一节把排查过程中最常撞到的几个报错列出来对照着看。401 Unauthorized{error:{type:authentication_error,message:invalid x-api-key}}原因通常是 Key 没设对或者设了但没被 Claude Code 读到。检查echo $ANTHROPIC_API_KEY如果输出为空说明环境变量没生效。如果是 settings.json 配置确认 JSON 格式没写错特别是引号和逗号。local proxy failedError: local proxy failed to connect这个报错通常出现在你本地配了代理但代理没起来或者代理端口写错了。如果你没主动配代理检查一下 shell 里有没有残留的HTTP_PROXY或HTTPS_PROXY环境变量env | grep -i proxy有的话 unset 掉unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxyreading choices 相关报错TypeError: Cannot read properties of undefined (reading choices)这个报错说明 SDK 期望的响应格式和实际返回的不一致。常见原因是 Base URL 指向了一个 OpenAI 兼容格式的端点但 Claude Code 用的是 Anthropic 格式。确认你的 Base URL 是https://taotoken.net/api不是带/v1/chat/completions那种。OAuth 相关报错Error: OAuth token expired or invalid如果你之前用 OAuth 方式登录过 Claude Code切换 API Key 后可能残留了旧的 token 缓存。清掉rm -rf ~/.claude/credentials然后重新用 API Key 配置。OOM 仍然复现如果按上面的步骤配完还是 OOM按这个顺序排查先确认堆上限真的生效了node -e那条命令输出是不是 4096。然后看是不是单文件太大用wc -l看行数超过 5000 行的文件分批分析。再看会话是不是太长了用/clear清一下。最后看 Docker 容器内存限制docker stats看容器实际能用多少内存。Docker 里跑的话启动参数要显式给内存docker run -m 4g --memory-swap 8g \ -e NODE_OPTIONS--max-old-space-size3072 \ -v $(pwd):/workspace \ node:22 claude容器内存限制如果只有 512MBNode.js 堆设再大也没用因为系统会在 V8 之前就把进程杀了。6. 长期编码场景的通道选择与配置固化OOM 排查完之后如果你打算长期用 Claude Code 做编码和 Agent 任务通道的稳定性比单次请求速度更重要。频繁重试和断连会让内存曲线变得不可预测也会拖慢整个工作流。TaoToken 的 Coding Plan 适合这种长期编码场景配置方式和上面一样只是 Key 的获取入口不同https://taotoken.net/coding-plan配置固化建议写进项目级的.claude/settings.json而不是全局的~/.claude/settings.json。这样不同项目可以用不同的模型和参数互不干扰{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的key, ANTHROPIC_MODEL: claude-sonnet-4-20250514, NODE_OPTIONS: --max-old-space-size4096 }, maxConcurrentTools: 2, maxFileSize: 1048576 }注意NODE_OPTIONS也可以放在 settings 的 env 里这样 Claude Code 启动时会自己带上不依赖 shell 配置。如果你用 Claude Code 的 Anthropic 兼容模式接入其他工具文档入口在这里https://taotoken.net/doc最后给一个我踩过的坑--max-old-space-size设太大不一定好。我试过在 8GB 机器上设 8192结果系统开始疯狂 swapClaude Code 响应变得极慢最后被 OOM Killer 杀了。后来降到 4096 反而稳定。堆上限不是越大越好留够系统和其他进程的空间才是关键。内存监控脚本可以做成一个 alias随时能跑alias claude-memwhile true; do pid$(pgrep -f claude | head -1); [ -n $pid ] ps -o rss -p $pid | awk {printf \RSS: %.1f MB\n\, \$1/1024}; sleep 3; done跑长任务时开一个终端挂着心里有数。