news 2026/10/9 3:44:03

pstack-claude:用工具调用树透视Claude Code Agent执行过程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pstack-claude:用工具调用树透视Claude Code Agent执行过程

其实一开始我没打算写这个项目,直到有一次我用 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-code
curl -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模式输出一张聚合表,例如:

工具调用次数总耗时平均耗时
Edit3489.2s2.6s
Bash1254.3s4.5s
Read81.6s0.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
自定义端点返回 401token 未传递检查 ANTHROPIC_AUTH_TOKEN
启动后无动作卡住服务端不支持工具调用换支持 tool_use 的服务或降级为文本任务
pstack-claude 输出为空会话目录找错使用--session-id指定文件
pstack-claude 树不完整事件缺 parentUuid使用宽松模式查看全量事件
Windows MCP 启动报 ENOENTnpx 命令名不匹配把 command 改成 npx.cmd

5. 个人使用体会

实际用下来的体会是,pstack-claude 解决的最大问题不是“看不懂日志”,而是“不知道应该看哪里”。Agent 的日志量比传统程序日志大得多,而且夹杂着大量上下文内容和思考过程,你要在一堆文本里找出当前真正执行的动作,靠肉眼是很难的。有了调用栈以后,相当于给问题定位加了一个导航。

我还养成了一个习惯:跑长任务时开一个 tmux 分屏,左边窗口跑 pstack-claude 的--live模式,右边窗口正常观察 Claude Code 的会话输出。右边看到的是“它在说什么”,左边看到的是“它在做什么”。两者一对照,很多看起来像是卡死的现象,其实只是它在等待某个耗时命令返回,根本不需要人工干预,耐心等几秒就好。

另外一个很有价值的用法是复盘历史会话。任务跑完以后,用--top看看哪个工具耗时最多,哪类操作最多,然后针对性地把描述改得更细。我这个工具第一版发布后,对自己项目的帮助比对 Claude Code 本身的帮助还大,因为它逼着我理解了会话日志的数据结构,也让我对 Agent 的执行模型有了更具体的认知。如果你也被 Agent 的“黑盒”折磨过,不妨从读一次自己的会话 JSONL 开始。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 3:43:39

OPM建模:用一张图统一系统结构与行为的系统设计方法

这几年在做架构设计的时候&#xff0c;我越来越觉得最折磨人的一件事不是画图&#xff0c;而是画了四五套图&#xff0c;却没法回答一个看起来最简单的问题&#xff1a;这个系统到底是干什么的、由什么组成、关键动作是什么&#xff1f;用例图画了需求、类图画了结构、时序图画…

作者头像 李华
网站建设 2026/10/9 3:42:45

Android系统定制:OTA升级链路全解析(Data分区目录、接口与SELinux)

做系统定制开发的同仁应该都遇到过这种需求&#xff1a;系统要支持OTA升级。乍一听这是个常见功能&#xff0c;但真要把这条链路完整打通——从数据分区的upgrade目录规划&#xff0c;到系统端接口的暴露&#xff0c;再到SELinux策略的放行——这里面的细节远比想象中多。我一接…

作者头像 李华
网站建设 2026/10/9 3:42:31

Linux DPM设备电源管理框架:系统睡眠挂起恢复调度机制与调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 3:42:11

DOI号里藏着什么?一套从编号拆解到文献精读的高效检索流程

拿到一串看不懂的文献编号&#xff0c;很多人第一反应是直接复制进浏览器看能不能打开&#xff0c;打不开就丢给导师或者扔进收藏夹吃灰。我之前帮学生做文献检索梳理的时候&#xff0c;收到过一条只写了几个字样和一个DOI号的信息&#xff1a;[TDSC]DOI: 10.1109/JIOT.2024.33…

作者头像 李华
网站建设 2026/10/9 3:42:09

Claude Opus 5.5 与 Claude Code 实战:Sub-agent 编排与 CLAUDE.md 避坑指南

1. 这次“焚诀”到底更新了什么&#xff1a;从标题拆解到真实能力边界先把话说在前头&#xff0c;标题里那个“焚诀”是圈内人的戏称&#xff0c;指的是模型在长链路推理、代码生成、复杂任务编排上的一次集中能力释放。我第一时间拿到 Claude Opus 5.5 的访问权限后&#xff0…

作者头像 李华
网站建设 2026/10/9 3:41:55

计算机发展史怎么读?从系统结构视角梳理四大阶段与核心概念

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华