其实一开始我没打算写这个项目,直到有一次我用 Claude Code 跑一个重构任务,跑了十几分钟都没动静,日志刷得飞快,我却完全不知道它在干什么:是卡在思考?是在反复改文件?还是陷入了工具调用的死循环?那感觉像极了当年线上服务挂起,手上却只有一个 top 输出的场景。排查 Linux 进程挂起,我有 pstack 可以看调用栈;排查这种 Agent 的“卡点”,我却找不到趁手的工具。于是我自己写了一个小工具,叫 pstack-claude,思路就是把 Claude Code 的会话记录解析成一棵清晰的工具调用树,你想看哪一层都有。
这篇文章不是只讲安装命令,我会把环境准备、核心实现原理、真实排障经验一起倒出来。无论是装 Claude Code 时卡在权限问题,还是想接自定义模型,又或者想搞清楚 Agent 到底在执行什么,都应该能从里面找到答案。适合对 Claude Code 有一定好奇心,又不想停留在“能跑起来就行”层面的朋友。
1. 我为什么写 pstack-claude:从进程栈到 Agent 栈
1.1 pstack 的思路为什么适合 Claude Code
老 Unix 玩家对 pstack 应该不陌生,它本质上是一个向目标进程发起诊断请求的命令行工具,用法很简单:pstack <pid>,输出就是当前进程的线程调用栈。它解决的是“进程活着但你不知道它在哪一行代码里卡住”的问题,尤其适合排查死锁、互等、长耗时的性能热点。
Claude Code 这类 Agent 运行时也有类似的黑盒问题。它会执行一个很长的任务,中间可能要读多个文件、改多个文件、跑若干次命令,甚至派生子任务。如果只盯着标准输出看,你会发现信息量巨大但毫无结构,你不知道当前栈顶在哪里,不知道哪个工具调用是瓶颈,更不知道子任务之间是怎么嵌套的。
pstack-claude 要做的就是把 pstack 的思路搬到 Agent 世界:从 Claude Code 的会话记录中提取事件流,用父子引用关系还原出一棵“Agent 调用栈”。栈顶就是当前正在执行的动作,栈的深度就是嵌套关系,每一层的耗时就是优化的切入点。做到这一点不需要改 Claude Code 源码,只需要把落盘的会话文件当作数据源,以只读方式解析,安全也干净。
1.2 pstack-claude 能做什么
工具第一版做出来以后,我给自己定了三个核心场景。
第一个场景是“判断卡点”。当 Claude Code 长时间没有输出时,用实时模式盯住调用栈,如果栈顶长时间停在某个工具调用,比如一个耗时的Bash命令,那就说明它在等外部命令返回,而不是真的死循环。如果栈顶反复在同一个节点上循环,而且时间戳在快速更新,那就更接近死循环,需要人工介入。
第二个场景是“理解嵌套”。Claude Code 在跑复杂任务时会派生子 Agent,每个子 Agent 又有自己的工具链。日志里它们的事件是交错写入的,人眼很难跟踪。pstack-claude 用 parentUuid 重建父子关系之后,子任务会被清晰地缩进在父任务下面,一眼就能看出谁是谁的下游。
第三个场景是“成本优化”。--top模式会聚合统计每个工具调用的次数和总耗时。你会很直观地发现,有些任务明明只需要一次Read,结果读了几十次;有些Bash命令一次能搞定,却被拆成了十几次。这些数字比任何代码审查都更能说明问题。
1.3 设计上我坚持的几个原则
做这个工具时我给自己定了三条铁律,后来被验证非常关键。
第一,只读日志,绝不注入。Claude Code 的进程模型是黑盒,尝试 attach 或者 hook 它的运行时可能随时被版本更新破坏。会话日志是官方主动写出来的,结构相对稳定,解析它是成本最低的方案。
第二,父子关系优先于时间顺序。Agent 的日志是并发写入的,如果简单按时间线性展示,你会发现父子调用穿插在一起,根本无法形成“栈”。用 uuid 和 parentUuid 构建森林,再对每棵树做按时间排序的深度优先渲染,才是真正可读的调用栈。
第三,允许数据不全。第三方模型服务、旧版本日志偶尔会缺少 parentUuid,如果因此就报错或者输出空白,工具就废了。我做了宽松模式,找不到父节点的节点会作为孤立树展示,至少你能看到有哪些事件发生。
2. 先把 Claude Code 环境装好
2.1 三种安装方式怎么选
pstack-claude 依赖 Claude Code 产生的会话文件,所以第一步是把 Claude Code 装好。官方提供的方式主要有两种:npm 全局安装和官方安装脚本。
npm install -g @anthropic-ai/claude-codecurl -fsSL https://claude.ai/install.sh | bash我推荐大多数用户用 npm 方式,因为升级方便,和 Node.js 生态天然统一。安装脚本的好处是自带运行时,适合没有 Node 环境的机器,但后续升级还是要走它的自动更新机制。无论哪种方式,装完先验证一下:
claude --version如果提示找不到命令,在 Windows 上多半是 npm 全局目录没进 PATH,在 Linux 上则可能是当前用户目录下的 bin 路径没配好。Node 版本建议用 18 以上,最好 20 或更高,太老的版本在处理复杂工具调用时容易触发内存问题。
2.2 Windows、WSL、虚拟机平台那点事
Windows 上跑 Claude Code 有两种主流方式:原生 Windows 和 WSL。我个人的习惯是 WSL,因为 Claude Code 经常要执行 shell 命令,很多原生的 Unix 工具链在 WSL 里是现成的,而在原生 Windows 环境里,脚本路径、命令解释器、换行符都会带来额外摩擦。
如果你在 Windows 上启动 Claude Code 的 workspace 相关功能,遇到提示说需要开启虚拟机平台,不要慌,这是 WSL 的基础依赖问题。需要开启两个 Windows 功能:“适用于 Linux 的 Windows 子系统”和“虚拟机平台”。在管理员 PowerShell 里执行:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart执行完以后重启机器,再运行wsl --set-default-version 2。注意一个问题:如果只是开了功能但没设置 WSL 默认版本为 2,某些情况下还是会报错。另外 BIOS 里的虚拟化开关如果被关闭,也会导致这种提示,需要进固件设置打开。
Ubuntu 22 这类 Linux 发行版安装就简单多了,直接 npm 全局装完就能用。唯一要注意的是如果你以后要编译原生模块,比如 node-gyp 相关的扩展,提前装好 build-essential 可以少踩坑。
注意:不要在没重启的情况下反复尝试开启功能,Windows 功能状态的切换必须重启才会完全生效,这个坑我至少踩了两次。
2.3 登录方式和自定义模型端点
Claude Code 支持几种身份配置方式。最简单的是交互式登录,在claude会话内执行/login,按提示用浏览器完成授权。这种方式适合直接用 Anthropic 官方服务的用户。
第二种是设置 API Key 环境变量:
export ANTHROPIC_API_KEY=sk-ant-...第三种对于想用其他 Anthropic 协议兼容服务的用户很关键:通过环境变量指定自定义端点,不需要登录 Claude 账号也能启动会话。
export ANTHROPIC_BASE_URL=https://api.example.com/anthropic export ANTHROPIC_AUTH_TOKEN=your-token这里有一个容易踩的细节:有些兼容服务要求 base_url 不能以/结尾,有些则要求必须带上/v1,你要以目标服务提供方的文档为准。更常见的问题是模型名映射。Claude Code 默认会发送一个官方模型别名,如果兼容服务不认识,就会在会话初始化阶段报 404 或者模型不存在。此时可以显式指定模型名:
export ANTHROPIC_MODEL=deepseek-chat值得注意的是,即便你能用自定义模型跑 Claude Code,也不是所有能力都等价。工具调用依赖服务端对 tool_use 的支持,如果服务端只实现了普通消息补全、不支持结构化工具调用,你会发现 Agent 在第一步规划完后没有任何动作,直接卡死。pstack-claude 在这种场景下很有用,因为它能帮你快速确认是不是卡在了工具调用的初始化阶段。
2.4 VSCode 和 MCP Server 配置
VSCode 本身不需要安装什么特殊插件才能用 Claude Code,直接在项目根目录打开终端,设置好环境变量,运行claude就行。如果你用的是 Claude 桌面版或者需要在 Claude Code 里扩展外部工具能力,就会遇到 MCP Server 配置。
MCP Server 最常见的启动方式是用 npx 运行,比如在.mcp.json里:
{ "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] } } }如果你把它写在 Claude Desktop 的配置文件里,字段结构一样,只不过外层统一叫mcpServers,具体路径在claude_desktop_config.json。Windows 用户要注意一个坑:配置里的 command 用npx有时会报找不到命令,因为在 Windows 上可执行文件名往往是npx.cmd。遇到这种情况把 command 改成npx.cmd,或者从绝对路径指定。
提示:MCP 配置里的命令路径、参数列表会在每次 Agent 调用工具时被重新解析,改完配置文件后最好重启 Claude Code 会话,避免旧的进程还持有过期的配置。
3. pstack-claude 的核心实现:如何把 Agent 调用栈抽出来
3.1 数据来源:会话 JSONL 与 OTEL
Claude Code 会把每个项目的会话事件落盘成一个 JSONL 文件,默认位置在~/.claude/projects/下面,文件名是根据项目路径转义来的。每一行是一个 JSON 对象,包含时间戳、事件类型、uuid、parentUuid,以及具体消息内容。
一个典型的事件行大概长这样:
{ "type": "assistant", "uuid": "a1b2c3", "parentUuid": "u0", "timestamp": "2025-06-18T10:00:01.000Z", "message": { "content": [ { "type": "tool_use", "name": "Edit", "input": { "file": "auth.js" } } ] } }如果你在启动 Claude Code 时开启了更详细的日志,比如--log参数,它会写出额外的日志文件。pstack-claude 默认只读~/.claude/projects下的会话 JSONL,因为这些文件结构统一,且每个项目都会自动生成,适合做默认数据源。
除了 JSONL,Claude Code 也支持 OTEL 导出,--otel-export配合一个采集端点可以把 spans 发出去。OTEL 的优势是标准统一,未来无论是看分布式追踪还是接可观测平台都很方便。但 JSONL 方案对本地诊断来说还是最简单直接的,不需要额外起服务,所以我优先实现了它。
3.2 解析逻辑:用 parentUuid 重建调用树
核心逻辑并不复杂,本质上是一次流式建树的过程。维护两个映射表:一个用 uuid 存节点,一个用 parentUuid 存父子关系。每读入一行事件,解析出基础字段后,创建一个树节点;如果它的 parentUuid 已经存在于表中,就挂到对应的父节点下面。
伪代码大概是这样:
function buildTree(lines) { const nodes = new Map(); for (const line of lines) { const evt = JSON.parse(line); const node = { id: evt.uuid, parentId: evt.parentUuid, type: evt.type, toolName: evt.message?.content?.[0]?.tool_use?.name ?? null, ts: Date.parse(evt.timestamp), children: [], }; nodes.set(node.id, node); } // 二次遍历,挂接父子关系 for (const node of nodes.values()) { const parent = nodes.get(node.parentId); if (parent) { parent.children.push(node); } } return nodes; }真正让我花了更多时间的是边角情况。比如某些类型的系统事件没有 parentUuid,它们不应该作为用户请求的子节点出现;再比如日志文件里可能出现未知 type,直接跳过会丢失信息。我的处理方式是:未知节点保留,但不会挂到调用树上,输出时可以用--include-system控制是否展示。
实时模式是另一个关键点。Claude Code 写日志是追加式的,pstack-claude 的--live模式不是简单重复读取整个文件,而是记录当前文件偏移量,用类似tail -F的方式只增量读取新增行,然后重建整棵树。这里有个性能取舍:如果会话特别长,每次都全量重建会让刷新越来越慢。我后来的做法是只保留最近 N 分钟内的节点,默认窗口是 10 分钟,这样实时刷新可以保持稳定。
3.3 命令行设计与输出效果
pstack-claude 的核心命令是:
pstack-claude --project /path/to/project运行以后它会在终端渲染一棵树,缩进代表调用层级,栈顶就是当前最近的活跃节点。一个典型的输出长这样:
[0] user: 重构 auth 模块 └─ [1] Agent: 分析请求 ts=10:00:01 ├─ [2] Read auth.js took=205ms │ └─ [3] Edit auth.js took=3.2s └─ [2] Bash npm test took=12.8s └─ [3] Error exitCode=1其中每一行的took是这个节点从开始到结束的耗时,如果没有结束,--live模式会显示running。看到running且长时间不变化时,说明 Agent 卡在了这个节点;如果running频繁变化,说明它在快速切换动作,距离死循环也不远了。
--top模式输出一张聚合表,例如:
| 工具 | 调用次数 | 总耗时 | 平均耗时 |
|---|---|---|---|
| Edit | 34 | 89.2s | 2.6s |
| Bash | 12 | 54.3s | 4.5s |
| Read | 8 | 1.6s | 0.2s |
这个表对优化 Prompt、合并操作特别有用。我在一个真实项目里看到Edit被调用了 34 次,就是因为每次修改的内容太少,后来把需求改成整块重写,调用次数直接降到 8 次。
3.4 运行流程与参数建议
安装 pstack-claude 只需要一条命令:
npm install -g pstack-claude常见的参数有这么几个:
--project:指定项目目录,用来定位~/.claude/projects下对应的会话文件--session-id:指定某个具体会话文件,适合历史会话复盘--live:进入实时跟踪模式,每秒刷新一次调用栈--interval:设置实时刷新间隔,默认 2000 毫秒--top:输出聚合统计,替代树形输出--json:把树结构以 JSON 形式输出,方便接其他工具
第一次使用我的建议是先跑--json,把原始树结构导出来,用 jq 之类工具看一眼字段结构。文本树输出有时候会掩盖父子关系不完整的情况,JSON 则能把孤立节点暴露得更清楚。
4. 真实使用中遇到的坑与排查技巧
4.1 升级失败与权限问题
Claude Code 的自动升级机制设计得比较激进,每次启动都会检查新版本。如果你以普通用户身份安装到系统级 npm 目录,很容易遇到这个报错:
auto-update failed: no write permission to npm prefix这不是 Claude Code 本身的 bug,而是 npm 全局目录的写入权限问题。最简单的根治方案是换用 Node 版本管理器,比如 volta 或者 nvm,把 Node 装到用户目录,npm 全局目录自然就在用户手里,不需要 sudo。
如果你不想换 Node 管理方式,也可以手动调整 npm prefix:
mkdir -p ~/npm-global npm config set prefix ~/npm-global export PATH=$HOME/npm-global/bin:$PATH改完记得把 PATH 加进 shell 配置文件,否则下次开机又找不到了。这里有个实测经验:不要直接去 chmod 系统目录来绕过权限,下次升级或者系统更新时很容易被重置,而且会遗留安全风险。
4.2 Windows 虚拟机平台报错处理
开头说过提示 “requires the virtual machine platform on Windows” 基本是 WSL 依赖缺失。但还有一种情况是功能已经开启,仍然报错,那就要依次排查三个地方:是否真的重启过了,BIOS 虚拟化开关是否开启,以及本次 WSL 内核是否过旧。
如果确认功能都开了还是有问题,可以在管理员 PowerShell 里更新 WSL 内核:
wsl --update wsl --set-default-version 2我自己的经验是,很多 Windows 相关报错在重启 + 更新 WSL 之后就消失了。注意区分“在原生 Windows 跑 Claude Code”和“在 WSL 里跑 Claude Code”这两个场景,pstack-claude 建议安装在 WSL 内部,因为它的日志解析依赖 Unix 的文件路径习惯。
4.3 自定义模型接入后连接失败或卡住
接入自定义模型时最常见的报错是 401 鉴权失败和 404 模型不存在。前者基本是ANTHROPIC_AUTH_TOKEN变量没有正确导出,或者服务端要求的不是 token 而是其他认证头;后者则多半是模型别名不匹配,需要显式设置ANTHROPIC_MODEL。
另外有个隐蔽的问题:某些兼容服务没有实现流式工具调用。Claude Code 在启动时如果发送了 tool_use 请求而服务端直接忽略,客户端不会立刻报错,而是表现为“一直在等待”。这种卡点用 pstack-claude 看非常清楚:会话树里出现了一个running状态的节点,但它的子节点永远是空的。
遇到这类问题,建议先把模型换成最简单的文本问答,确认对话正常,再开启需要工具能力的任务。同时把响应输出上限调低,比如设置一个较小的CLAUDE_CODE_MAX_OUTPUT_TOKENS,避免因超长响应导致连接中断。
4.4 pstack-claude 输出为空或树不完整
这个工具本身也会遇到问题,最常见的是输出为空。先检查~/.claude/projects目录里有没有会话文件,如果你用--log指定了自定义日志路径,默认的会话目录里可能真的什么都没有。其次检查会话文件的权限,普通用户是否可读。
树不完整的表现是明明有几十个事件,但渲染出来只有几棵孤立的小树。这通常是因为部分事件缺少 parentUuid。在旧版本 Claude Code 或某些第三方兼容服务产生的日志里,这不是罕见现象。此时可以用宽松模式渲染,把所有孤立节点按发生时间平铺展示,虽然层级信息丢了,但至少能看到全貌。
我建议先跑一行命令看看原始文件的头部结构:
head -n 20 ~/.claude/projects/xxx/xxx.jsonl | jq '.[0:2]'确认字段名和预期一致,再排查解析逻辑。基于实际经验,90% 的输出异常都出在字段名差异上,比如有的服务的字段叫message.thinking而不是message.content。
4.5 速查表:常见问题与对应解法
| 问题现象 | 常见原因 | 推荐解法 |
|---|---|---|
| claude 命令找不到 | npm 全局目录未进 PATH | 手动添加 PATH 或重装 Node 管理器 |
| auto-update failed 无权限 | npm prefix 指向系统目录 | 把 prefix 改为用户目录 |
| Windows 提示需要虚拟机平台 | WSL 依赖未开启 | 开启 Windows 功能后重启,更新 WSL |
| 自定义端点返回 404 | 模型别名不匹配 | 显式设置 ANTHROPIC_MODEL |
| 自定义端点返回 401 | token 未传递 | 检查 ANTHROPIC_AUTH_TOKEN |
| 启动后无动作卡住 | 服务端不支持工具调用 | 换支持 tool_use 的服务或降级为文本任务 |
| pstack-claude 输出为空 | 会话目录找错 | 使用--session-id指定文件 |
| pstack-claude 树不完整 | 事件缺 parentUuid | 使用宽松模式查看全量事件 |
| Windows MCP 启动报 ENOENT | npx 命令名不匹配 | 把 command 改成 npx.cmd |
5. 个人使用体会
实际用下来的体会是,pstack-claude 解决的最大问题不是“看不懂日志”,而是“不知道应该看哪里”。Agent 的日志量比传统程序日志大得多,而且夹杂着大量上下文内容和思考过程,你要在一堆文本里找出当前真正执行的动作,靠肉眼是很难的。有了调用栈以后,相当于给问题定位加了一个导航。
我还养成了一个习惯:跑长任务时开一个 tmux 分屏,左边窗口跑 pstack-claude 的--live模式,右边窗口正常观察 Claude Code 的会话输出。右边看到的是“它在说什么”,左边看到的是“它在做什么”。两者一对照,很多看起来像是卡死的现象,其实只是它在等待某个耗时命令返回,根本不需要人工干预,耐心等几秒就好。
另外一个很有价值的用法是复盘历史会话。任务跑完以后,用--top看看哪个工具耗时最多,哪类操作最多,然后针对性地把描述改得更细。我这个工具第一版发布后,对自己项目的帮助比对 Claude Code 本身的帮助还大,因为它逼着我理解了会话日志的数据结构,也让我对 Agent 的执行模型有了更具体的认知。如果你也被 Agent 的“黑盒”折磨过,不妨从读一次自己的会话 JSONL 开始。