用 Litefuse 追踪 Claude Code
Claude Code 是 Anthropic 的终端编码 Agent。本集成使用 Claude Code 的 Stop hook 把每一轮对话发送到 Litefuse —— 无需修改 Claude Code 源码。
该 hook 会解析 Claude Code 的会话 transcript(位于 ~/.claude/projects/ 下的 JSONL 文件),并为每个用户回合生成一条 Litefuse trace,每个 LLM 调用(thinking / text / tool decision)和每次工具执行都会作为单独的 observation。
给 AI —— 自动安装
如果你正在和 Claude Code 对话,把下面这段 prompt 粘贴过去,Agent 会端到端地完成整个安装:
Read https://litefuse.ai/SKILL.md and follow the instructions to install and configure Litefuse for Claude Code.
skill 会向你索取 Litefuse 的 API Key(如果还没有账号,会引导你注册),然后在本地完成全部配置。如果你想手工一步步配置,请继续往下看。
会捕获哪些数据
| 数据 | 捕获方式 | 备注 |
|---|---|---|
| 用户 prompt | trace input | 文本 + 图片 block 摘要 |
助手发出的每个 thinking / text / tool_use block | generation observation | 每个 JSONL 行一个 observation,使用真实时间戳 |
| 工具执行(输入 + 输出) | tool observation | 按工具类型给出结构化的 toolUseResult 摘要 |
token 用量(input_tokens、output_tokens、cache_read_input_tokens、cache_creation_input_tokens) | generation 上的 usage_details | 仅附加到每条 Anthropic 消息的最后一行,避免重复累加 |
| 模型名称 | generation 上的 model | Litefuse 用它来计算成本 |
| API 错误 / 重试 / 限流 | event observation,level=ERROR | 来自 Claude Code 的 system 行 |
工具错误(is_error=true) | tool observation,level=ERROR | 附带 status_message 预览 |
| 会话分组 | trace 的 session_id | Claude Code 会话 UUID |
| 用户身份 | trace 的 user_id | $LITEFUSE_USER_ID,回退到 getpass.getuser() |
| Claude Code 版本 | trace 的 release | 来自 JSONL 的 version 字段 |
| 环境上下文 | trace metadata | cwd、git 分支、entrypoint、permission mode、sidechain 标志 |
| 单个 observation 的身份 | metadata | uuid、parentUuid、requestId、message.id —— JSONL 字段名原样保留 |
Trace 结构
一个典型的多工具回合产生的 trace 形如:
Claude Code - Turn 7 (root span, AS_ROOT, trace headers)
├── user message (event — carries the user prompt)
├── Thinking (#1) (generation)
├── Text response (#2) (generation)
├── Decision to call tool: Bash (#3) (generation, usage_details here)
├── Tool call: Bash (#3) (tool)
├── Thinking (#4) (generation)
├── Decision to call tool: Edit (#5) (generation, usage_details here)
├── Tool call: Edit (#5) (tool)
└── Final response (#6) (generation)设计说明:
- 每个回合都以
user messageevent 开头。 这是一个瞬时的 event 类型 observation,把用户的 prompt 作为 input 携带。它视觉上和回合最后的Final response (#N)generation 形成开闭对应,让 trace 时间线从上到下读起来像一个清晰的“开-闭”结构。 - 逐行保真:每个 JSONL 行变成自己的 observation。同一条 Anthropic 消息中的
thinking、text和tool_useblock 不会 被合并。 - 唯一的 span 名称:
(#N)步骤后缀让一个回合内的 observation 名称保持唯一,即便同一个工具被多次调用,Litefuse 的图视图也能保持线性。 - 真实时间戳:span 的 start/end 来自 JSONL 的
timestamp字段,而不是 hook 的本地时钟 —— Litefuse 的时间线反映的是事情真正发生的时刻,包括冷启动和排队延迟。 - 不重复计算 token:多行消息的每个 JSONL 行都会带上同一份
usageblock;hook 只把usage_details附加到最后一行。 - 1ms 间隔:相邻的同级 span 之间留 1ms 间隔,这样 Litefuse 的图视图就不会把首尾相接的 observation 当成并行分支。
- 命名空间化的 metadata,统一放在
claude_code.*下。每个 Hermes 或 JSONL 专属字段都放在这一个 key 下面(例如claude_code.uuid、claude_code.requestId、claude_code.service_tier)。Litefuse 的标准字段(sessionId、userId、tags、release、usageDetails)保持在顶层。稀疏存储:源 JSONL 行中不存在的字段,metadata 中也完全不会出现,不会用null占位。 - 未完成的回合会被延迟。 Claude Code 的 Stop hook 可能在 Agent 仍在循环中(在两次工具派发之间)时触发。如果某个回合最后一个助手行不是
textblock(比如还在等下一个工具结果的tool_use),hook 会 延迟 发送 —— 它会回退自己的字节偏移量,等下一次 Stop 触发,那时候收尾的text“Final response”通常已经到达。每个会话维护一个emitted_user_uuids集合,确保已发送过的回合不会因为回退而被重复发送。代价:如果 Agent 真的在没有最终文字回复的情况下结束了一个回合(比如在工具循环中被强制中止),那么这一回合不会产生 trace。
快速开始
前置条件
- Python ≥ 3.10(Langfuse SDK v4 的要求)。可以用
python3 --version检查。macOS 自带的python3通常是 3.9 —— 通过 Homebrew 安装更新版本(brew install python@3.13,或任意 3.10+ 的小版本),并确保它作为python3出现在PATH最前面。 - 在 https://litefuse.cloud 创建一个 Litefuse 项目,拿到 public 与 secret key。
创建 virtualenv 并安装 Langfuse v4
把 hook 的依赖隔离在专用的 venv 里:
python3 -m venv ~/.claude/hooks/.venv
~/.claude/hooks/.venv/bin/pip install --upgrade pip
~/.claude/hooks/.venv/bin/pip install 'langfuse>=4,<5'下载 hook 脚本
mkdir -p ~/.claude/hooks
curl -fsSL https://litefuse.ai/integrations/claude-code/litefuse_hook.py \
-o ~/.claude/hooks/litefuse_hook.py
chmod +x ~/.claude/hooks/litefuse_hook.py源码也可以在同一 URL 直接浏览 —— 部署前可以先看一遍。
配置 ~/.claude/settings.json
加入 Stop hook 与 Litefuse 凭据:
{
"env": {
"TRACE_TO_LANGFUSE": "true",
"LANGFUSE_PUBLIC_KEY": "pk-lf-xxx",
"LANGFUSE_SECRET_KEY": "sk-lf-xxx",
"LANGFUSE_BASE_URL": "https://litefuse.cloud"
},
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$HOME\"/.claude/hooks/.venv/bin/python3 \"$HOME\"/.claude/hooks/litefuse_hook.py"
}
]
}
]
}
}如果想按项目使用,把同样的 env 块放进 <project>/.claude/settings.local.json 即可。
验证
在 Claude Code 中发送一条消息,然后查看 hook 的日志:
tail -f ~/.claude/state/litefuse_hook.log
# Expected: "Processed N turns in X.XXs (session=...)"在 Litefuse 中打开对应项目 —— 每条用户消息会变成一个具有上面所示结构的 trace。
环境变量
| 变量 | 必填 | 说明 |
|---|---|---|
TRACE_TO_LANGFUSE | 是 | 设为 "true" 启用 hook。任何其他值都会让脚本短路退出。 |
LANGFUSE_PUBLIC_KEY | 是 | Litefuse 项目 public key(pk-lf-...)。 |
LANGFUSE_SECRET_KEY | 是 | Litefuse 项目 secret key(sk-lf-...)。 |
LANGFUSE_BASE_URL | 否 | 默认 https://cloud.langfuse.com。设为 https://litefuse.cloud(或你自托管的 URL)。 |
LITEFUSE_USER_ID | 否 | 覆盖 trace 的 user_id。回退到 getpass.getuser(),再回退到 hostname。 |
CC_LANGFUSE_DEBUG | 否 | 设为 "true" 启用详细的 hook 日志。 |
CC_LANGFUSE_MAX_CHARS | 否 | span 输入/输出的截断阈值(字符数)。默认 1000000(约 1MB 文本)。 |
Trace metadata 参考
所有 Claude Code 专属字段在 observation metadata 中都放在 claude_code.* 命名空间下。JSONL 字段名原样保留(按原 transcript 中的 camelCase / snake_case 写法)。Litefuse 的标准字段(sessionId、userId、tags、release、usageDetails)通过 OTel trace 属性保持在顶层 —— 不会重复出现在 claude_code 下。
Trace root span —— claude_code.*(来自用户消息行):
uuid、parentUuid、promptId、isMeta、origin—— 行身份cwd、gitBranch、version、entrypoint、userType、isSidechain、permissionMode—— Claude Code 环境上下文sessionId、turn_number、transcript_path、models_used、source—— 衍生字段user_text_truncated、user_text_orig_len—— 在用户 prompt 被截断时设置user_content_text_blocks、user_content_image_blocks、user_content_image_media_types—— 在用户发送了图片时设置
user message event —— claude_code.*:
uuid、parentUuid、promptId—— 与 trace root 相同的身份字段user_content_*、user_text_*—— 与 trace root 相同的内容 / 截断摘要
Generation observation —— claude_code.*(来自助手行):
uuid、parentUuid、requestId—— JSONL 行身份id(Anthropic message id)、stop_reason、stop_sequence、stop_details—— Anthropic 消息字段service_tier、speed、inference_geo、iterations—— Anthropic usage 元数据cache_creation(ephemeral_1h / ephemeral_5m 拆分)、server_tool_use—— 仅信息性字段;不计入usage_detailsdiagnostics—— Anthropic 的cache_miss_reason等;当cache_creation_input_tokens异常大时很有用context_management、container—— Anthropic 一侧的字段,存在时才出现step_index、is_last_in_message—— 衍生字段assistant_text_truncated、assistant_text_orig_len、… —— 仅在文本被截断时出现
Tool observation —— claude_code.*:
name、tool_use_id—— 来自tool_useblockuuid、parentUuid、sourceToolAssistantUUID—— 来自携带tool_result的user行is_error—— 来自tool_resultblocktoolUseResult—— 扁平摘要;字段因工具类型而异:- Bash:
command、stdout_len、stderr_len、has_stderr、interrupted - WebFetch / WebSearch:
code、codeText、bytes - Task(子 agent):
agentId、agentType - BashOutput:
backgroundTaskId、assistantAutoBackgrounded - Glob / Grep / Read:
appliedLimit、appliedOffset、itemCount
- Bash:
input_truncated、input_orig_len、output_truncated、output_orig_len—— 截断信息
Event observation —— claude_code.*(来自 system 行 —— API 错误、重试、hook 结果):
- 所有 system 行字段原样保留:
subtype、level、cause、error、uuid、parentUuid、stopReason、toolUseID、hookCount、hookErrors、hookInfos、retryAttempt、maxRetries、retryInMs、preventedContinuation、hasOutput stop_hook_summary子类型被有意过滤掉了(它记录的是这个脚本自身的执行)。
工作原理
每次 Stop hook 触发时,脚本会:
- 从会话 JSONL transcript 中读取自上次
offset以来的新字节(状态缓存在~/.claude/state/langfuse_state.json,以sha256(session_id::transcript_path)为 key)。 - 解析行;按
uuid去重(transcript 在会话恢复时偶尔会重复行)。 - 把行分组成回合:每个真正的
user消息开启一个回合;后续的助手行 +tool_result行 + 非 summary 类型的system行附加到该回合。 - 拿每个回合的
user_msg.uuid与该会话的emitted_user_uuids集合比对(也缓存在 state 中)。已发送的回合直接跳过 —— 防止偏移量回退(见第 6 步)时的重复发送。 - 判断最后一个回合是否完整:如果最后一个助手行包含
textblock(即 Claude Code 的“Final response”收尾标志),该回合即视为完整并发送。否则 —— 通常因为 Agent 仍在循环中、最新的助手行是等待下一轮的tool_use—— hook 延迟 处理。 - 延迟时:把
offset回退到读取前的值,下一次 Stop 触发会重新读取相同字节。那时缺失的text行通常已经到达,回合也就可以发送了。emitted_user_uuids集合保证回退期间不会重发之前已完成的回合。 - 每个完整回合通过 Langfuse SDK v4 的 OTel 层向 Litefuse 发送一条 trace,使用 JSONL 中显式的 start/end 时间戳,让时间线反映每个 block 实际到达的时刻。
- 保存新的
offset+emitted_user_uuids,下一次 hook 调用从这里继续。
该 hook 是 fail-open 的:任何意外错误都会写入 ~/.claude/state/litefuse_hook.log,脚本以 0 退出,因此永远不会阻塞 Claude Code 的主循环。
故障排查
Litefuse 中没有出现 trace。 用 tail -f ~/.claude/state/litefuse_hook.log 看看日志。空日志意味着 hook 没在运行 —— 确认 TRACE_TO_LANGFUSE=true 已设置,并且 settings.json 的 command 路径正确(venv 的 python3 加 hook 脚本)。
hook 日志显示 Langfuse client init failed。 venv 里的 Python 没装好 langfuse。重新执行:
~/.claude/hooks/.venv/bin/pip install --upgrade 'langfuse>=4,<5'hook 日志显示 Missing session_id or transcript_path; exiting. Claude Code 没有在 stdin 上传入预期的 payload。确认你的 Claude Code 版本会发出 Stop hook payload(见 Claude Code hooks)。
token 总数看起来不对。 hook 只把 usage_details 附加到每条 Anthropic 消息的最后一行 —— 在 Litefuse 中查看单个 generation observation 即可验证:只有 Decision to call tool: X (#N) / Final response (#N) 行带有 token 计数;同一条消息的 Thinking (#N) / Text response (#N) 不会带。
有一个回合从未出现为 trace。 看 hook 日志 —— 如果你看到 Processed N turns, deferred 1 (in-progress),最近的回合因为最后一个助手行不是 text block 而被暂时挂起。下一次 Stop 触发时会重新评估它。如果该回合在工具循环中被强制中止(永远不会有 text 收尾行),延迟就变成永久的,不会发送 trace。在 Claude Code 中再发送一条后续消息,被延迟的回合会被重新检查:如果它后面已经出现了更新的用户消息,followup 去重路径就会无视完整性条件直接发送(一个旧回合后面跟着新的用户消息,已经“尽可能完整”了)。
手动测试 hook:
TRACE_TO_LANGFUSE=true \
LANGFUSE_PUBLIC_KEY="pk-lf-..." \
LANGFUSE_SECRET_KEY="sk-lf-..." \
LANGFUSE_BASE_URL="https://litefuse.cloud" \
~/.claude/hooks/.venv/bin/python3 ~/.claude/hooks/litefuse_hook.py
# Errors appear in the log file.启用调试日志:
# Add to settings.json env block:
"CC_LANGFUSE_DEBUG": "true"