Skip to content

Claude Code Transcript → Langfuse 方案实现说明

目录:langfuse_trace/
样例数据:transcript_demo/b0dcb60c-5539-42a7-b71b-80ffc878ec0f.jsonl


一、方案概览

Claude Code 将会话过程以 JSONL 形式写入本地 transcript。本方案在不改 Claude Code 核心的前提下,将 transcript 增量解析为 Turn(用户轮次),并上报到 Langfuse,实现:

  • 主会话每轮对话 → 独立 Trace
  • 模型调用 → Generation(含 token、真实时间戳)
  • 工具调用 → Tool Span
  • 子代理(Agent/Task)→ 嵌套在 Tool: Agent 下的 Subagent Turn

提供 两种上报方式,共享 common/langfuse_hook_all_agents.py 中的解析与 emit 逻辑:

维度方案 A:Hook方案 B:Watcher
入口Claude Code Stop 事件触发独立守护进程轮询 jsonl
实时性每轮结束上报默认每 1s 轮询 + 流式折中(trace 即时建、tool 实时 span)
依赖需配置 settings.json hooks无需 Hook,后台运行即可
典型场景与 Claude Code 深度集成CI/旁路监听/不便改 Hook 时

二者勿同时启用,否则可能重复上报同一 Turn。


二、目录结构

langfuse_trace/
├── 方案实现说明.md              # 本文档
├── README.md                    # 快速入门
├── requirements.txt             # langfuse>=2,<3
├── common/
│   └── langfuse_hook_all_agents.py   # 共享核心:解析 transcript + 上报 Langfuse
├── hook/                        # 方案 A:Stop Hook
│   ├── langfuse_hook_all_agents.py   # Hook 入口(薄封装 → common)
│   ├── e2e_langfuse_test.py
│   └── run_e2e_langfuse_test.sh
├── watcher/                     # 方案 B:jsonl 轮询 Watcher
│   ├── claude_langfuse_watcher.py
│   ├── run_watcher.sh           # 常驻监听入口(推荐)
│   ├── e2e_langfuse_watcher_test.py
│   └── run_e2e_langfuse_watcher_test.sh
├── transcript_demo/             # 脱敏样例(主 jsonl + subagents)
├── tests/
│   └── test_usage_transcript.py
└── docs/                        # 历史文档

三、Transcript 简介

3.1 存储位置

官方 Claude Code:

~/.claude/projects/<项目哈希>/
├── <sessionId>.jsonl                    # 主会话
└── <sessionId>/subagents/               # 或同级 agent-*.jsonl
    ├── agent-<id>.jsonl
    └── agent-<id>.meta.json

trpc-claudecode(本环境常用):

~/.trpc-claudecode/projects/<编码项目路径>/
├── <sessionId>.jsonl                    # 主会话
└── <sessionId>/subagents/
    ├── agent-<id>.jsonl
    └── agent-<id>.meta.json

Watcher 默认监听 ~/.trpc-claudecode/projects(见 run_watcher.sh),也可通过 --projects-root 指向 ~/.claude/projects

3.2 每行事件类型

type含义
user用户输入,或 tool_result 回灌
assistant模型文本 / tool_use
attachmentSkill 列表等上下文注入
system系统元数据

trpc-claudecode 拆行:同一 API 响应可能占多行 jsonl,共享 message.id——一行 text、每个 tool_use 各占一行。解析时按 message.id 分组,不能简单按 id 去重。

3.3 Turn 划分规则

user(真人,非 synthetic)  → 新 Turn
assistant                   → 追加当前 Turn
user + tool_result          → 归档工具结果,不新开 Turn

Synthetic user(如 Skill 注入的 Base directory for this skill:)不拆 Turn。


四、核心架构(共享逻辑)

4.0 数据流总览

批量模式CC_LANGFUSE_STREAMING=false,Hook 默认;Watcher 可关闭):

┌──────────────────────────────────────────────────────────────┐
│  输入:主 session jsonl(增量 offset + 半行 buffer)           │
└────────────────────────────┬─────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│  read_new_jsonl() → build_turns() → completed_turns()        │
│  discover_subagent_files() → match_subagent()                │
└────────────────────────────┬─────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│  emit_main_turn():Turn 全部完成后一次性上报                   │
│  Langfuse v2 API: trace → generation + tool span + 嵌套      │
└────────────────────────────┬─────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│  flush() 成功 → 推进 turn_count / offset;失败则下次重试        │
└──────────────────────────────────────────────────────────────┘

流式折中模式CC_LANGFUSE_STREAMING=true,Watcher 默认):

┌──────────────────────────────────────────────────────────────┐
│  read_new_jsonl() → build_turns(include_pending=True)        │
│  仅处理 all_turns[turn_count](当前进行中的 Turn)            │
└────────────────────────────┬─────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│  process_turn_streaming()                                    │
│  · user 消息 → 立即建 trace                                   │
│  · tool_use  → 立即建 tool span                               │
│  · Agent span 打开期间 → 增量读 subagent jsonl 并嵌套上报      │
│  · end_turn  → 收尾 generation + usage,推进 turn_count       │
└────────────────────────────┬─────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│  所有 observation 必须带 trace_id(避免 SDK resume 建空 trace)│
│  flush() 成功 → 推进 offset;end_turn 后推进 turn_count        │
└──────────────────────────────────────────────────────────────┘

4.1 Langfuse 层级结构

Trace: Claude Code - Turn N
├── Generation: Claude Response 1     [start_time/end_time + usage]
├── Tool: Agent
│   ├── Subagent[Explore] Turn 1
│   │   ├── Generation: ...
│   │   └── Tool: ...
│   └── ...
└── Generation: Claude Response 2

4.2 子代理匹配与流式嵌套

主 Turn 遇到 tool_use(name ∈ {Task, Agent}) 时:

  • 扫描 agent-*.jsonl + .meta.json把 subagent jsonl 当作独立根 session 跟踪)
  • subagent_type == agentType + 时间戳就近打分
  • 批量模式tool_result 到达后全量解析子 jsonl,嵌套进 Tool: Agent span
  • 流式模式Agent span 打开期间,_streaming_sync_subagent_open_tools() 对匹配的 agent-*.jsonl 做增量 read_new_jsonl,子 Turn 的 generation / tool span 实时出现在 Tool: Agent

4.3 可靠提交

  • turn_count 仅在 langfuse.flush() 成功后推进
  • 失败时保留 offset,下次 at-least-once 重试(可能重复,优于静默丢失)

4.4 SDK 版本要求(重要)

本方案使用 Langfuse Python SDK v2.x 有状态 API:trace() / span() / generation()

bash
pip install -r requirements.txt
# 等价于: pip install 'langfuse>=2.0.0,<3.0.0'

若安装了 langfuse 3.x / 4.x,会报错:

'Langfuse' object has no attribute 'trace'

启动时会检测 SDK 并给出提示。langfuse_hook.py(v3 OTel API)与本目录方案 不兼容,请勿混用。


五、方案 A:Hook

5.1 安装

bash
pip install -r requirements.txt
cp hook/langfuse_hook_all_agents.py ~/.claude/hooks/
chmod +x ~/.claude/hooks/langfuse_hook_all_agents.py

5.2 环境变量

变量必填说明
TRACE_TO_LANGFUSE必须为 true
CC_LANGFUSE_PUBLIC_KEY / LANGFUSE_PUBLIC_KEY公钥
CC_LANGFUSE_SECRET_KEY / LANGFUSE_SECRET_KEY私钥
CC_LANGFUSE_BASE_URL默认 https://cloud.langfuse.com
CC_LANGFUSE_INCLUDE_SUBAGENTS默认 true
CC_LANGFUSE_REDACT默认 true(密钥脱敏)
CC_LANGFUSE_DEBUG详细日志

5.3 注册 Hook

~/.claude/settings.json

json
{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/langfuse_hook_all_agents.py"
          }
        ]
      }
    ]
  }
}

仅挂 Stop不要SubagentStop(子代理由主 Hook 内联处理,避免扁平重复 trace)。

5.4 状态文件

~/.claude/state/langfuse_hook_all_agents_state.json(offset / buffer / turn_count)

日志:~/.claude/state/langfuse_hook.log


六、方案 B:Watcher(实时轮询)

6.1 原理

独立守护进程按 poll_interval(默认 1s)轮询,对每个已跟踪的主 session jsonl 独立处理:

  1. rescan():发现目录下新增的 UUID 主 session jsonl,加入跟踪列表
  2. read_new_jsonl(offset):按文件 offset 读增量行(支持半行 buffer)
  3. build_turns() 重建 Turn 列表
  4. 按模式上报:
    • 流式(默认):处理 all_turns[turn_count],user → 建 trace,tool → span,end_turn → 收尾并推进 turn_count
    • 批量:仅上报 completed_turns()turn_count 之后的已完成 Turn
  5. flush 成功后才推进 state;失败保留 offset,下次 at-least-once 重试

tool_use + tool_result 之后模型仍可能继续输出最终回答。批量模式必须等 end_turn;流式模式在 Turn 进行中即可上报中间 observation,但 turn_count 仍只在 end_turn 后推进。

6.2 启动方式

bash
cd watcher

# 推荐:常驻监听(已配置凭证 + idle_timeout=0 + 流式默认开启)
./run_watcher.sh

# 监听单个 session(开发/调试)
python3 claude_langfuse_watcher.py \
  --transcript ~/.trpc-claudecode/projects/.../abc-session.jsonl

# 监听整个 projects 目录
python3 claude_langfuse_watcher.py \
  --projects-root ~/.trpc-claudecode/projects \
  --idle-timeout 0

# 后台
nohup ./run_watcher.sh &

空目录启动--projects-root / --watch-dir 模式下,启动时没有任何 jsonl 也会继续运行,按 poll_interval 定期 rescan 发现新 session。适用于「先启 watcher、后开 Claude Code 对话」的 daemon 场景。--once 仍要求启动时已有文件。

--transcript 等待:指定的 jsonl 尚未创建时,watcher 会等待该文件出现后再跟踪(不会立即退出)。

修改代码后需重启 watcher 才能加载最新逻辑。

6.3 Watcher 专用环境变量

变量默认说明
CC_LANGFUSE_WATCH_POLL_S1.0轮询间隔(秒)
CC_LANGFUSE_WATCH_IDLE_S300文件空闲多久停止跟踪(run_watcher.sh 覆盖为 0
CC_LANGFUSE_STREAMINGtrue(Watcher)流式折中上报;false 回退批量模式
PROJECTS_ROOT~/.trpc-claudecode/projectsrun_watcher.sh 监听根目录

6.4 状态与日志

  • State:~/.claude/state/langfuse_watcher_state.json(与 Hook state 隔离
  • 上报日志:~/.claude/state/langfuse_hook.log(复用 hook 的 info()
  • 进程日志:~/.claude/state/langfuse_watcher.log(启停信号等)

6.5 流式上报(默认 CC_LANGFUSE_STREAMING=true

事件上报时机
user 消息写入立即创建 trace(Langfuse 可见)
tool_use立即创建 tool span
tool_result 到达结束 tool span
Agent/Task span 打开期间增量读 subagents/agent-*.jsonl,嵌套上报子 Turn
assistant end_turn收尾 generation + usage,推进 turn_count

关闭流式(回退旧行为,仅 end_turn 一次性上报):CC_LANGFUSE_STREAMING=false

阶段延迟来源
jsonl 追加 → watcher 感知poll_interval(默认 1s)
Langfuse UI 可见额外 ingestion 延迟(数秒~数十秒)

6.6 主 session 文件过滤(is_main_session_jsonl

Watcher 不会扫描目录下所有 jsonl,只跟踪「主 session」:

文件是否跟踪说明
<UUID>.jsonl主 session,文件名 stem 须匹配 UUID 格式
agent-*.jsonl子代理 transcript,由主 session 内联处理
subagents/ 目录下文件同上
非 UUID 文件名跳过(--transcript 强制指定时例外,仍会跟踪)

子代理 jsonl 从不作为独立根 trace 上报,避免与主 session 重复。

6.7 多 jsonl / 多 session / 多进程

常见疑问:目录里有很多 jsonl(旧的 + 正在写的),Watcher 怎么知道上报哪个?

答案:不是「选一个」,而是「全部纳入跟踪,谁有新增量谁就上报」。

projects/-data-workspace-demo-project/
├── d6671986-....jsonl   ← 旧 session,仍在跟踪列表
├── 93ba7d55-....jsonl   ← 旧 session,仍在跟踪列表
└── a0e702c2-....jsonl   ← 当前正在写的 session

每轮 poll 对 _tracked每个文件分别执行 process_file()

文件状态Watcher 行为
无新行写入read_new_jsonl 为空 → 基本 no-op,不上报
有增量写入解析新内容 → 流式更新 Langfuse
所有 Turn 已上报(turn_count 已追上)继续跟踪,但不再上报
首次发现已写完的旧文件从 Turn 1 起逐轮补报(每 poll 处理当前 Turn)

不会按 mtime「只选最新文件」;区分新旧靠的是 per-file 的 offset + turn_count,不是全局「当前活跃 session」选择器。

常见疑问:多个 Claude 进程写多个 jsonl,能分别上报 trace 吗?

可以。 每个主 session jsonl 完全独立:

Claude 进程 A  →  session-aaa.jsonl  →  Langfuse sessionId=aaa  →  Turn 1, 2, ...
Claude 进程 B  →  session-bbb.jsonl  →  Langfuse sessionId=bbb  →  Turn 1, 2, ...
  • 一文件一 session:初始 session_id = 文件名 stem(UUID);读到 jsonl 内 sessionId 字段后会校正
  • 一文件一 statestate_key = SHA256(session_id + 文件绝对路径),互不干扰
  • Langfuse UI:按 sessionId 过滤即可看到各会话各自的 trace 列表
场景说明
两个进程各开新 session两个 UUID jsonl,各自独立 trace 序列
同一 session 不应有两个主 jsonl正常只有一个 <sessionId>.jsonl
子 Agent嵌套在父 session 的 Tool: Agent 下,不单独占根 trace

6.8 状态隔离与 idle 行为

每个跟踪文件在 langfuse_watcher_state.json 中独立存储:

json
{
  "<state_key>": {
    "offset": 12345,
    "buffer": "",
    "turn_count": 3,
    "streaming": { "trace_id": "...", "turn_num": 4, "tools": {...} }
  }
}
  • offset / buffer:jsonl 增量读取进度
  • turn_count:已成功上报并 finalize 的 Turn 数
  • streaming:当前进行中 Turn 的 trace/span 句柄(仅流式模式,end_turn 后清空)

idle_timeoutCC_LANGFUSE_WATCH_IDLE_Srun_watcher.sh 默认 0):

行为
0永不停止跟踪;旧 session 留在列表,无新写入时开销极小
>0(如 300)文件超过 N 秒无新写入 → 从 _tracked 移除;下次 rescan 发现时会 _bootstrap_messages_if_needed() 从磁盘恢复全量消息

七、测试

7.1 单元测试

bash
python3 -m unittest tests/test_usage_transcript.py

7.2 端到端测试

需配置 Langfuse 凭证。

bash
export CC_LANGFUSE_PUBLIC_KEY=pk-lf-xxx
export CC_LANGFUSE_SECRET_KEY=sk-lf-xxx
export CC_LANGFUSE_BASE_URL=https://your-langfuse-host

./hook/run_e2e_langfuse_test.sh            # Hook:完整 transcript 一次上报
./watcher/run_e2e_langfuse_watcher_test.sh  # Watcher:--once 批量回放

八、与原版 langfuse_hook.py 的差异

维度原版(hooks/langfuse_hook.py)本方案
Langfuse SDKv3 start_as_current_observationv2 trace/span/generation
子代理黑盒嵌套上报
时间戳常显示 0.00stranscript 真实时间
Token缺失usage_details
失败重试易丢数据flush 失败不推进 state
上报方式仅 HookHook + Watcher

九、已知限制

  1. 子代理 jsonl 内不再递归 discover 更深层 Task。
  2. 子代理匹配为启发式(type + 时间戳),meta 缺失时可能误匹配。
  3. at-least-once 语义:极端情况下可能重复上报同一 Turn。
  4. 仅支持 Langfuse SDK 2.x;4.x 需降级或重写 emit 层。
  5. Watcher 无「只跟踪最新 session」选项;历史 jsonl 会一直留在跟踪列表(idle_timeout=0 时)。
  6. 首次发现大量历史 session 时,补报按 Turn 逐轮进行,非瞬时完成。

十、故障排查

现象排查
'Langfuse' object has no attribute 'trace'pip install 'langfuse>=2,<3'
日志有 emitted 但 UI 无 traceingestion 延迟;查 API GET /api/public/traces?sessionId=...
只有 signal 15, stoppinglangfuse_hook.logemitted 写在该文件
Watcher 一直 turn_count=0确认 turn 是否已到 end_turn;开 CC_LANGFUSE_DEBUG=true
重复 traceHook 与 Watcher 同时启用,或 at-least-once 重试
Langfuse 里大量空白 trace(仅 name、无内容)流式 resume 时未传 trace_id 的历史脏数据;升级代码后重启 watcher;可手动清理旧 trace
Tool: Agent span 为空确认 CC_LANGFUSE_STREAMING=true 且子 agent jsonl 路径可被 discover_subagent_files 发现
旧 session 也被轮询正常行为;无新写入时不上报;若需释放可设 --idle-timeout 300

十一、常见问题(FAQ)

Q:Watcher 会重复上报已经上报过的 Turn 吗?

一般不会。turn_count 仅在 end_turnflush() 成功后推进;流式模式下进行中的 Turn 通过 streaming 状态 resume 同一 trace,不会每 poll 新建 trace。极端网络失败时可能 at-least-once 重试。

Q:为什么不用子 agent jsonl 的文件 mtime 来判断「当前活跃 session」?

主 session 的活跃度应看主 jsonl 是否有新行。子 agent jsonl 只在 Agent/Task tool 执行期间写入,且由主 session 驱动匹配,不适合作为「选哪个主 session」的依据。

Q:Hook 和 Watcher 的 state 文件能共用吗?

不能。Hook 用 langfuse_hook_all_agents_state.json,Watcher 用 langfuse_watcher_state.json,避免互相覆盖 offset / turn_count。

Q:Langfuse 里同一 sessionId 下 Turn 编号怎么对应?

每个完成的 Turn 对应一条根 trace,命名形如 Claude Code - Turn NN = turn_count + 1(从 1 递增)。


十二、参考

  • 历史详细文档:docs/langfuse_hook_all_agents_README.md
  • 原版 Hook 参考:../hooks/langfuse_hook.py
  • 通用 Hook 机制:../hooks/hook.md