首页 › tutorials › 为Skill加装可观测性看板

概述

本教程带你为自定义 Claude Code Skill 构建一套可观测性(observability)体系:通过原生 OpenTelemetry 与零侵入 Hook 双通路,把 Skill 的每一次激活、调用时长、Token 消耗、成功率和成本落盘成结构化数据,再聚合成 CLI 看板,最终用真实数据回答"哪个 Skill 值得用 Opus、哪个 Skill 在偷偷失败"。适合已经写过 1–2 个 Skill、想知道它们在真实会话里表现如何的开发者。

前置条件

为什么 Skill 需要可观测性

AI 编码 Agent 在一次会话里可能触发上百次工具调用,Skill 只是其中一种触发源。没有遥测时,你只能靠"感觉"判断 Skill 好不好用——它到底被激活了几次?平均跑多久?吃掉了多少上下文预算?哪几次悄悄失败了?这些问题,传统 Evals(在静态测试集上打分)回答不了,因为它衡量的是实验室质量,而非线上表现。

可观测性补齐的就是这一环:让 Skill 从一个"黑盒提示词"变成一个可以被度量、被诊断、被优化的服务。这与"Skill 失效诊断"(坏了才查)不同,可观测性是主动、持续、全程的;也与 Evals 不同,它度量的是真实运行时的每一次调用。

两条数据通路对比

Claude Code 提供两条互补的遥测通路,理解它们的分工是本教程的核心:

维度 原生 OpenTelemetry(OTel) 自定义 Hook 采集
数据粒度 官方预定义指标与事件 完全自定义字段
是否含 Skill 事件 ✅ Skill activated event ✅ 可自行增强
落地难度 改 3 个环境变量即可 需写 hook 脚本
适用场景 团队级成本/Token 统计 Skill 级质量分析与模型选型
存储后端 OTel Collector / Grafana / Datadog 本地 JSONL / SQLite

实战中两者并用最香:OTel 管宏观成本,Hook 管微观质量。

步骤一:开启原生 OpenTelemetry(5 分钟速赢)

Claude Code 把指标作为时间序列、事件作为日志/事件协议、(可选)分布式链路作为 trace 协议导出。在 shell 里配置环境变量即可:

# 1. 打开总开关
export CLAUDE_CODE_ENABLE_TELEMETRY=1

# 2. 选择导出器(metrics 与 logs 各自可选)
export OTEL_METRICS_EXPORTER=otlp      # 可选: otlp / prometheus / console / none
export OTEL_LOGS_EXPORTER=otlp         # 可选: otlp / console / none

# 3. 指定 OTLP 端点
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

# 4. 鉴权(如需)
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-token"

# 5. 调试期缩短导出间隔(生产请还原)
export OTEL_METRIC_EXPORT_INTERVAL=10000   # 10 秒,默认 60s
export OTEL_LOGS_EXPORT_INTERVAL=5000      # 5 秒,默认 5s

claude

管理员也可在 managed settings 的 env 块里集中下发同样的变量,实现全组织统一管控。

预期结果: Claude Code 会周期性吐出官方指标。其中与你 Skill 最相关的是这一组——

常见问题: 看不到数据?把 OTEL_METRICS_EXPORTER=console 切到控制台导出,先把 JSON 打到终端确认格式正确,再切回 otlp。企业代理环境下记得配 OTEL_EXPORTER_OTLP_HEADERS 做鉴权。

步骤二:用 Hook 零侵入捕获 Skill 调用

原生 OTel 给的是聚合指标,但要做 Skill 级的质量分析(每次调用单独成行),最干净的办法是挂 Hook。Hook 是独立进程,完全不需要改动 Skill 本身——这正是"零侵入"的含义。

在 .claude/settings.json 注册四类核心事件:

{
  "hooks": {
    "UserPromptSubmit": [{
      "hooks": [{
        "type": "command",
        "command": "node ~/.skill-otel/hooks/on-prompt.js",
        "timeout": 10
      }]
    }],
    "PreToolUse": [{
      "hooks": [{
        "type": "command",
        "command": "node ~/.skill-otel/hooks/pre-tool.js",
        "timeout": 10
      }]
    }],
    "PostToolUse": [{
      "hooks": [{
        "type": "command",
        "command": "node ~/.skill-otel/hooks/post-tool.js",
        "timeout": 10
      }]
    }],
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "node ~/.skill-otel/hooks/on-stop.js",
        "timeout": 10
      }]
    }]
  }
}

这套结构与社区项目 claude-code-telemetry 完全一致:UserPromptSubmit 开一条 trace,PreToolUse/PostToolUse 包住每一次工具调用的起止耗时,Stop 关闭会话 span。如果你想监控 Subagent 编排,再加 SubagentStart / SubagentStop 即可(官方支持的全部 12 类事件)。

步骤三:读懂 Hook 的 stdin 契约

Claude Code 把一个 JSON 对象通过 stdin 喂给 hook 脚本。这是整个采集链路的命脉,字段在不同事件类型下略有不同:

// pre-tool.js:从 stdin 读入 Claude Code 推送的事件
const fs = require('fs');
let raw = '';
process.stdin.on('data', c => raw += c);
process.stdin.on('end', () => {
  const evt = JSON.parse(raw);
  // 核心字段(按事件类型出现):
  // evt.hook_event_name   → "PreToolUse" | "PostToolUse" | ...
  // evt.session_id        → 会话唯一 ID(trace 根)
  // evt.transcript_path   → 本次会话的 JSONL 转写文件
  // evt.cwd               → 工作目录
  // evt.tool_name         → 工具名(Skill 触发多经 Task/MCP,需结合 Skill 事件判定)
  // evt.tool_input        → 工具入参(脱敏后落盘!)
  // evt.tool_use_id       → 工具调用唯一 ID(span 关联键)
  // evt.tool_response     → 仅 PostToolUse 有,工具返回
  // evt.agent_id / evt.agent_type → Subagent 相关
  // evt.model             → 当前模型
  appendSpan(evt);            // 你的落盘逻辑
});

关键约束: hook 的 stdout 可以输出一个 JSON 来反控 Claude Code(如 {"decision":"block","reason":"..."} 阻断危险命令);但纯采集用途下,务必让脚本在 timeout(这里 10 秒)内干净退出,否则会拖慢每一次工具调用。采集脚本里不要做网络阻塞调用,先落本地 JSONL,再异步搬运。

步骤四:构建 append-only JSONL 采集器

落盘格式选 append-only JSONL + 按日期轮转,好处是无需数据库、可 tail -f 实时看、损坏只丢一行。核心写入器:

// lib/jsonl-exporter.js
const fs = require('fs');
const path = require('path');

class JsonlExporter {
  constructor(logDir = '~/.skill-otel/logs', maxSizeMB = 50) {
    this.logDir = logDir.replace('~', process.env.HOME);
    this.maxBytes = maxSizeMB * 1024 * 1024;
    if (!fs.existsSync(this.logDir)) fs.mkdirSync(this.logDir, { recursive: true });
  }
  file() {
    const d = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
    return path.join(this.logDir, `telemetry-${d}.jsonl`);
  }
  write(rec) {
    const f = this.file();
    // 敏感字段先脱敏:password/token/secret/key
    const safe = this.redact(rec);
    fs.appendFileSync(f, JSON.stringify(safe) + '\n');
    if (fs.statSync(f).size > this.maxBytes) this.rotate(f);
  }
  redact(o) { /* 递归把 key 匹配 /password|token|secret|key/i 的值替换为 *** */ }
  rotate(f) { fs.renameSync(f, f.replace(/\.jsonl$/, `-${Date.now()}.jsonl`)); }
}
module.exports = { JsonlExporter };

PreToolUse 写一条 start 记录并记下 tool_use_id;PostToolUse 用同一个 tool_use_id 回填 end 时间与返回摘要。跨进程的 span 拼接,社区方案是给每个未闭合 span 写一个 .pending-spans/<spanId>.json,收到配对事件后删除——这避免了任何内存状态。

步骤五:计算 Skill 质量指标

有了 JSONL,聚合就是几行流式统计。围绕 Skill 定义四类核心质量指标:

// dashboard/metrics.js
function summarize(events) {
  const bySkill = {};
  for (const e of events) {
    const s = bySkill[e.skill] ||= { calls: 0, ok: 0, dur: 0, tokens: 0, cost: 0 };
    if (e.kind === 'start') { s.calls++; s._start = e.ts; }
    if (e.kind === 'end') {
      if (!e.error) s.ok++;
      s.dur += e.ts - s._start;
      s.tokens += e.tokens; s.cost += e.cost;
    }
  }
  return Object.entries(bySkill).map(([skill, s]) => ({
    skill,
    calls: s.calls,
    successRate: s.calls ? (s.ok / s.calls * 100).toFixed(1) + '%' : '—',
    avgMs: s.calls ? Math.round(s.dur / s.calls) : 0,
    tokensPerCall: s.calls ? Math.round(s.tokens / s.calls) : 0,
    costPerCall: s.calls ? (s.cost / s.calls).toFixed(4) : 0,
  }));
}

输出形如下表,一眼看出哪个 Skill 在亏 Token、哪个在失败:

skill calls successRate avgMs tokensPerCall costPerCall
architect 42 97.6% 8120 6200 $0.031
sql-optimizer 118 84.7% 2310 850 $0.004
log-rca 9 100% 14500 9800 $0.052

洞察示例: sql-optimizer 调用最多但成功率最低——这是 Evals 看不出的"线上退化",该回去补案例;log-rca 成功率满分却吃掉最多 Token,候选降级到 Sonnet。

步骤六:渲染 CLI 看板

无需前端,一个终端看板就能驱动决策:

# Skill 用量报告
node dashboard.js view --report skills
# 按日期 + Skill 过滤
node dashboard.js view --report skills --date 2026-06-17 --skill architect
# 单条 trace 的瀑布视图(Jaeger 风格)
node dashboard.js view --report trace --trace-id <id>
# JSON 输出,方便接 CI
node dashboard.js view --report skills --format json

进阶可像 claude-code-telemetry 那样起 docker compose up -d,拿到 Grafana 三张预置看板:Skill Performance(调用数/耗时/成功率,成功率按红黄绿阈值染色)、Token Analysis(Token 时序 + 上下文预算剩余 gauge)、Model Comparison(Skill → 推荐模型),外加一个 /trace/:traceId 的瀑布回溯。

步骤七:数据驱动的模型选型

这是可观测性的终极回报:不再凭感觉,而是用观测数据为每个 Skill 挑模型。社区项目用一套规则评分做推荐,阈值可直接借鉴:

推荐模型 单次 Token 上下文占比 其他特征
Opus ≥ 5000 ≥ 30% 复杂度高
Sonnet 1000–5000 10–30% 成功率 90–98%
Haiku ≤ 1000 ≤ 10% 成功率 ≥ 98%
Gemini ≥ 3000 ≥ 40% 成功率 ≥ 95%

把步骤五的 tokensPerCall、successRate 喂进这张表,每个 Skill 自动得到一个"该用哪档模型"的建议。Token 占比低、成功率高的 Skill(如 sql-optimizer)从 Opus 降到 Haiku,单次成本可砍掉一个数量级——这就是数据替你省钱。

整体架构(Mermaid)

graph TB
    subgraph 采集["数据采集(零侵入)"]
      A[Claude Code 会话] -->|Skill activated event| OT[原生 OTel]
      A -->|stdin JSON| H[Hook 脚本]
      H -->|append-only| L[telemetry-YYYY-MM-DD.jsonl]
    end
    subgraph 存储["存储"]
      OT -->|OTLP gRPC| COL[OTel Collector]
      L -->|按日轮转| DIS[本地磁盘]
    end
    subgraph 分析["分析与可视化"]
      DIS --> M[质量指标聚合]
      M --> CLI[CLI 看板]
      DIS --> TR[Jaeger 式 Trace 瀑布]
      M --> REC[模型选型推荐]
      COL --> G[Grafana 成本/Token 看板]
    end
    REC -.->|反馈| A

实战案例:最小可观测性套件

把上述步骤拼成一个可跑的端到端示例:

  1. 建目录:mkdir -p ~/.skill-otel/{hooks,lib,dashboard},放入 pre-tool.js、post-tool.js、jsonl-exporter.js、metrics.js、dashboard.js。
  2. 注册 Hook:把步骤二的 JSON 写进 ~/.claude/settings.json,重启 Claude Code。
  3. 触发 Skill:在会话里运行你的 Skill 一次(如 /your-skill ...)。
  4. 看数据:cat ~/.skill-otel/logs/telemetry-$(date +%F).jsonl | jq . 应看到 Skill 的 start/end 成对记录。
  5. 跑看板:node ~/.skill-otel/dashboard/dashboard.js view --report skills,确认成功率/耗时/Token 出表。
  6. 决策:把成功率 < 90% 的 Skill 回灌 Evals 补案例;把 Token 偏低、成功率高的 Skill 在 frontmatter 里切到更便宜的模型。

整套实现控制在 ~200 行,零数据库依赖,落地一个工作日内完成。

常见问题

延伸阅读

训练营适用性评估

参考来源

  1. Monitoring — Claude Code Docs — 官方 OpenTelemetry 配置、可用指标与事件清单(含 Skill activated event、Hook execution 事件、Token/Cost counter)
  2. claude-code-telemetry — GitHub — 本地优先的 Skill 调用遥测系统,提供 JSONL 采集、Jaeger 瀑布看板与规则式模型推荐(Opus/Sonnet/Haiku 阈值表来源)
  3. claude-code-hooks-multi-agent-observability — GitHub — 全量 12 类 Hook 事件的实时可观测性实现,含 send_event.py 的 stdin 契约与 settings.json 配置范例
  4. Claude Code Metrics Dashboard: Grafana Setup (2026) — Sealos — 用 Docker Compose + Grafana 搭建成本/Token/会话看板的实操指南

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入