主题
4. Hook 方案详解
入口:
langfuse_trace/common/langfuse_transcript.py→main()
部署:langfuse_trace/hook/langfuse_hook_all_agents.py(薄封装)
1. 工作原理
你在 Claude Code 里发消息、AI 回复
↓
Claude Code 写入 session jsonl
↓
本轮结束(Stop 事件)→ 触发 Hook 脚本
↓
Hook 从 stdin 读取 {sessionId, transcriptPath}
↓
read_new_jsonl() 增量读取新增行
↓
build_turns() → emit_main_turn() → flush()
↓
Langfuse UI 出现新 Trace生活类比:每做完一道菜,厨师按一下铃,会计过来把刚才的流水账录入系统。
2. 触发时机
Hook 注册在 Claude Code 的 Stop 事件上。每次 AI 完成一轮回复(stop_reason=end_turn),Claude Code 会:
- 把 Hook 脚本路径和 payload 写入 stdin
- 等待 Hook 退出(必须快速返回,不能阻塞)
payload 结构
json
{
"sessionId": "b0dcb60c-5539-42a7-b71b-80ffc878ec0f",
"transcriptPath": "/home/user/.claude/projects/abc123/b0dcb60c....jsonl"
}子代理 Hook 跳过
python
if transcript_path.name.startswith("agent-"):
return 0 # 子代理由主 Hook 内联处理,不单独上报3. main() 执行流程
python
def main() -> int:
# ① 开关检查
if os.environ.get("TRACE_TO_LANGFUSE") != "true":
return 0
# ② 凭证检查(缺失则静默退出)
if not public_key or not secret_key:
return 0
# ③ 读 Hook payload
payload = read_hook_payload()
session_id, transcript_path = extract_session_and_transcript(payload)
# ④ 初始化 Langfuse + SDK 版本检查
langfuse = Langfuse(public_key=..., secret_key=..., host=...)
# ⑤ 加锁读状态 → 增量读 jsonl → 切 Turn
with FileLock(LOCK_FILE):
state = load_state()
ss = load_session_state(state, key)
new_msgs, ss_after = read_new_jsonl(transcript_path, ss)
turns = build_turns(new_msgs)
# ⑥ 逐 Turn 上报
for t in turns:
emit_main_turn(langfuse, session_id, turn_num, t, ...)
emitted_ok += 1
# ⑦ flush 成功才提交进度
if flush_ok and emitted_ok == len(turns):
ss_after.turn_count += emitted_ok
write_session_state(state, key, ss_after)
save_state(state)
return 0 # 永远返回 04. 状态管理
| 文件 | 用途 |
|---|---|
~/.claude/state/langfuse_hook_all_agents_state.json | 全局状态(按 session+path 分 key) |
~/.claude/state/langfuse_hook_all_agents_state.lock | 文件锁,防并发 Hook |
~/.claude/state/langfuse_hook.log | 运行日志 |
每个 session 的状态:
json
{
"a1b2c3...sha256": {
"offset": 45678,
"buffer": "",
"turn_count": 5,
"updated": "2026-06-05T10:00:00+00:00"
}
}可靠投递
上报 3 个 Turn,第 2 个失败 或 flush 失败
→ offset/turn_count 不推进
→ 下次 Stop 重读同一批新行
→ 从失败的 Turn 重试5. 安装与配置
5.1 安装依赖
bash
cd langfuse_trace
pip install -r requirements.txt # langfuse>=2,<35.2 部署 Hook 脚本
bash
cp hook/langfuse_hook_all_agents.py ~/.claude/hooks/5.3 环境变量
bash
export TRACE_TO_LANGFUSE=true
export CC_LANGFUSE_PUBLIC_KEY=pk-lf-xxx
export CC_LANGFUSE_SECRET_KEY=sk-lf-xxx
# 可选
export CC_LANGFUSE_BASE_URL=https://cloud.langfuse.com
export CC_LANGFUSE_DEBUG=true5.4 注册 Hook
在 ~/.claude/settings.json 的 hooks.Stop 中注册脚本路径。详见 langfuse_trace/方案实现说明.md。
6. Hook 方案的特点
| 优点 | 缺点 |
|---|---|
| 与 Claude Code 生命周期绑定 | 需改 settings.json |
| 每轮结束才触发,逻辑简单 | 非流式,Turn 结束前 UI 看不到 |
| fail-open,不影响 CC | Stop 时若 Langfuse 不可达,当轮延迟上报 |
| 共享 common 模块,易维护 | 与 Watcher 不可同时开 |
7. Hook vs Watcher 在 emit 上的差异
Hook 的 main() 不走流式模式,每次 Stop 触发时:
python
turns = build_turns(new_msgs) # 只处理新增消息
for t in turns:
emit_main_turn(...) # 批量一次性上报完整 TurnWatcher 默认走 process_turn_streaming()(见 6_streaming_mode.md)。
Hook 也可以启用流式——但目前 main() 未调用 process_turn_streaming(),这是两者最大的行为差异。
8. 调试
bash
# 开启详细日志
export CC_LANGFUSE_DEBUG=true
# 查看日志
tail -f ~/.claude/state/langfuse_hook.log
# 端到端测试
cd langfuse_trace/hook
export CC_LANGFUSE_PUBLIC_KEY=pk-lf-xxx
export CC_LANGFUSE_SECRET_KEY=sk-lf-xxx
./run_e2e_langfuse_test.sh