首页 › tutorials › 构建日志根因分析Skill实战

构建日志根因分析 Skill 实战:脚本+参考+模板三层架构

概述

本教程带你从零构建一个名为 log-rca(Log Root Cause Analysis)的 Claude Code Skill。它能读取一份服务日志,自动提取异常事件、关联时间窗口、给出排序后的根因假设并产出结构化分析报告。整篇教程聚焦一个多数已有 Skill 教程都没碰过的应用场景——生产日志根因定位,并完整演示官方推荐的"带可执行代码的 Skill"四要素:工具脚本(utility scripts)、可验证中间输出(verifiable intermediate outputs)、渐进披露的参考知识库、模板化的最终产物。

适用人群:已跑通过至少一个 Claude Code 项目、了解 SKILL.md 基本概念、希望构建能真正在生产环境复用的复杂 Skill 的中高级开发者与 SRE。

前置条件

为什么用 Skill 而不是直接问 Claude

把同一段分析步骤反复粘进对话框,是创建 Skill 最典型的信号。根因分析尤其符合:每次都要"先抽异常、再按时间聚类、再查错误码含义、再套报告模板"。CLAUDE.md 适合写事实("我们的网关是 Nginx 1.24"),而 Skill 适合写流程——而且它的正文只有在被激活时才进入上下文,长篇参考知识库平时零成本。

三层架构设计

一个能复用的分析型 Skill,本质是把"确定性计算"交给脚本、"模糊判断"交给模型、"领域知识"交给参考文件、"格式规范"交给模板。四者各司其职:

层 文件 职责 由谁执行
控制层 SKILL.md 工作流编排、假设生成与排序 Claude
计算层 scripts/extract.py 正则抽取、时间聚类、统计 Python(确定性)
知识层 references/patterns.md 错误码含义、常见根因对照表 Claude 按需读取
表现层 templates/report.md 报告骨架与字段约束 Claude 填充

关键设计原则来自官方最佳实践:默认假设 Claude 已经很聪明——它知道什么是堆栈、什么是超时,所以参考文件只放它不可能猜到的内部知识(自家错误码、本集群拓扑约定),不要写它已经懂的常识。

flowchart TD
    U[用户: /log-rca app.log] --> SK[SKILL.md 控制层]
    SK -->|动态注入| EX[scripts/extract.py 计算层]
    EX -->|JSON 中间产物| SK
    SK -->|按需读取| REF[references/patterns.md 知识层]
    REF -->|错误码语义| SK
    SK -->|填充| TPL[templates/report.md 表现层]
    TPL --> R[结构化根因报告]
    EX -.可验证.-> CHK{中间产物可复算?}
    CHK -->|是| R

步骤 1:创建骨架与 frontmatter

个人级 Skill 放在 ~/.claude/skills/ 下,对所有项目生效;项目级放在 .claude/skills/ 下。这里做项目级,方便随仓库分发给团队。

mkdir -p .claude/skills/log-rca/{scripts,references,templates}
cd .claude/skills/log-rca

目录骨架:

log-rca/
├── SKILL.md              # 主控流程(必需)
├── scripts/
│   └── extract.py        # 日志解析引擎
├── references/
│   └── patterns.md       # 错误码知识库
└── templates/
    └── report.md         # 报告模板

预期结果:在当前会话内,Claude Code 的实时变更检测会立刻发现新 Skill,无需重启。若你是会话启动后首次创建顶层的 .claude/skills/ 目录(目录此前不存在),则需要重启一次 Claude Code 让它开始监听。

创建 SKILL.md 的 frontmatter。frontmatter 是 Claude Code 在加载正文前先读取的元数据,控制发现、调用、权限与执行位置:

---
name: log-rca
description: 对一份服务日志做根因分析,抽取异常事件、按时间窗口聚类、给出排序后的根因假设并产出报告。当用户贴入或指向一份 .log/.out 文件、说"分析这份日志""为什么挂了""排查一下报错"时使用。
allowed-tools:
  - Bash(log-rca:*)
  - Read
disable-model-invocation: false
---

逐字段说明:

常见问题:Skill 不触发?九成是 description 写得太泛(如"分析日志")或漏掉了用户实际会说的触发短语。把它写成包含动词 + 对象 + 场景的完整句。

步骤 2:编写 SKILL.md 主控流程(工作流模式)

frontmatter 之后就是正文。正文采用官方推荐的条件工作流模式(conditional workflow pattern)——用编号步骤告诉 Claude 在什么条件下做什么,而不是堆细节。

# 日志根因分析

你是一位资深 SRE。下面是一份待分析日志的预处理结果,以及分析步骤。

## 预处理结果(脚本已抽取,可直接信任)

!`python3 "$SKILL_DIR/scripts/extract.py" "$ARGUMENTS" 2>/dev/null || echo "EXTRACT_FAILED: 请确认日志文件路径正确"`

## 分析步骤

1. **校验中间产物**:若上方出现 `EXTRACT_FAILED`,停止并向用户索要正确路径;否则继续。
2. **识别主异常**:在"异常事件"列表中,找出出现频次最高或最先爆发的那一类,作为主线索。
3. **关联时间窗口**:检查主异常前后 ±60 秒内的 WARNING/慢查询/资源指标,建立因果候选链。
4. **查证错误码**:对每个未知错误码,读取 `references/patterns.md` 获取其语义与常见根因。**只在这一步需要时才读**,不要预先加载。
5. **生成假设**:给出 1–3 条根因假设,按"证据充分度"从高到低排序,每条标注依据的事件编号。
6. **产出报告**:严格按 `templates/report.md` 的结构填充并输出。

## 约束

- 中间产物的数字是脚本算出来的,不要自己重新数日志行——那是确定性结果,直接引用。
- 若证据不足以支撑某假设,明确标注"证据不足",不要编造。
- 报告中所有时间戳必须能在原始日志中复核。

这里有两个关键技术点:

动态上下文注入:!command` 语法。Claude Code 在把 SKILL.md 交给模型之前,会先执行该命令并把整行替换为输出。所以模型看到的不是命令本身,而是已经算好的中间结果——它的推理因此"接地"于真实数据,而不是靠猜。变量$SKILL_DIR是 Skill 自身目录的绝对路径,$ARGUMENTS是/log-rca` 后面用户输入的全部参数。

为什么把抽取交给脚本:这正是"可验证中间输出"的精髓。日志里有多少条 ERROR、哪个时间点爆发、哪条堆栈出现最频繁——这些是确定性计算,让 Python 算远比让模型"数"可靠(模型数行数经常出错)。脚本输出 JSON,模型只负责"解读",分工明确,结果可复算、可审计。

常见问题:$SKILL_DIR 不展开?确认 Claude Code 版本 ≥ v2.1.145。旧版本可改用相对路径 python3 scripts/extract.py ... 并保证工作目录正确。

步骤 3:编写计算层 scripts/extract.py

这是整个 Skill 的"确定性引擎"。它的设计目标只有一个:把非结构化日志变成结构化、可复核的中间产物。

#!/usr/bin/env python3
"""log-rca 计算层:抽取异常、按时间聚类、统计堆栈签名。
输出一段 Markdown,供 SKILL.md 动态注入。刻意不输出原始敏感字段。"""
import re, sys, json
from collections import Counter, defaultdict
from datetime import datetime

if len(sys.argv) < 2:
    print("EXTRACT_FAILED: 缺少日志文件路径参数"); sys.exit(0)

path = sys.argv[1]
TS = re.compile(r'(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})')
LEVEL = re.compile(r'\b(ERROR|FATAL|WARN(?:ING)?)\b', re.I)

try:
    lines = open(path, encoding='utf-8', errors='replace').read().splitlines()
except OSError as e:
    print(f"EXTRACT_FAILED: {e}"); sys.exit(0)

events, signatures = [], Counter()
for ln in lines:
    m_ts, m_lv = TS.search(ln), LEVEL.search(ln)
    if not (m_ts and m_lv): continue
    level = m_lv.group(1).upper()
    if level.startswith("WARN"): continue          # 先聚焦 ERROR/FATAL
    ts = m_ts.group(1).replace("T", " ")
    # 堆栈签名:去掉数字/路径/变量,保留异常类型
    sig = re.sub(r'0x[0-9a-f]+|\d+|/[\w./\-]+|"[^"]*"', '', ln)
    sig = re.sub(r'\s+', ' ', sig).strip()[:80]
    signatures[sig] += 1
    events.append({"ts": ts, "level": level, "sig": sig})

# 时间聚类:按分钟桶统计 ERROR 数量,找爆发点
buckets = Counter()
for e in events:
    buckets[e["ts"][:16]] += 1                      # 截到分钟
burst = buckets.most_common(3)

print(f"## 预处理摘要\n- 解析行数: {len(lines)}")
print(f"- ERROR/FATAL 事件: {len(events)}")
print(f"- 堆栈签名种类: {len(signatures)}")
print("\n### Top 堆栈签名(出现次数)")
for sig, n in signatures.most_common(5):
    print(f"- ({n}x) `{sig}`")
print("\n### 异常爆发时间窗(按分钟)")
for minute, n in burst:
    print(f"- `{minute}` — {n} 条")
print("\n### 前 10 条异常事件")
for e in events[:10]:
    print(f"- [{e['ts']}] {e['level']} — {e['sig']}")

赋予执行权限(动态注入走的是 python3 调用,不强制可执行,但养成习惯):

chmod +x scripts/extract.py

手动验证中间产物(这是 Skill 开发的核心纪律——先确保脚本能独立跑通):

echo '2026-06-15T10:00:01 ERROR NullPointerException at UserService.java:42
2026-06-15T10:00:01 ERROR NullPointerException at UserService.java:42
2026-06-15T10:00:03 ERROR ConnectionRefused db.internal:5432' > /tmp/sample.log
python3 scripts/extract.py /tmp/sample.log

预期结果:输出一段 Markdown 摘要,包含事件数 3、签名种类 2、爆发窗 2026-06-15 10:00 共 3 条,以及前几条事件列表。脚本永远以 exit 0 结束并把错误写进正文(EXTRACT_FAILED: ...),这样 SKILL.md 的步骤 1 能稳定捕获——绝不要让脚本抛非零退出码,否则动态注入会中断整个 Skill 加载。

常见问题:日志时间戳格式千奇百怪?TS 正则只覆盖了最常见的 ISO 与空格分隔两种。针对自家日志格式扩展正则即可,这恰恰是"内部知识"该沉淀进脚本而非交给模型猜的地方。

步骤 4:编写知识层 references/patterns.md

参考文件体现渐进披露:它平时不进上下文,只有 SKILL.md 步骤 4 明确要求查错误码时才被读取。所以可以放心写得详尽——这正是放领域知识的正确位置。

# 错误码与常见根因对照表

> 本文件仅在被需要时读取。新增内部错误码时追加到此。

## 数据库相关
| 现象 | 常见根因 | 首要排查 |
|---|---|---|
| ConnectionRefused | DB 进程退出 / 端口被防火墙拦 | `systemctl status postgresql`、安全组规则 |
| ConnectionTimeout | 网络抖动 / 连接池耗尽 | 连接池监控、`netstat -an \| grep TIME_WAIT` |
| too many connections | 上游突发流量 / 连接泄漏 | 慢查询日志、连接数趋势图 |

## 应用层
| 现象 | 常见根因 | 首要排查 |
|---|---|---|
| NullPointerException | 空值未校验 / 上游返回结构变更 | 对应行的上游调用链、最近发布记录 |
| OutOfMemoryError | 堆内存泄漏 / 大对象未释放 | 堆 dump、GC 日志 |

## 基础设施
| 现象 | 常见根因 | 首要排查 |
|---|---|---|
| 502/504 网关 | 后端无响应 / 上游超时 | 上游健康检查、网关 access log |
| disk full | 日志/临时文件未轮转 | `df -h`、crontab 中的清理任务 |

官方最佳实践反复强调一条铁律:避免深层嵌套引用。参考文件最多一层,且文件较长时用目录(TOC)组织。本文件用分类表格替代长段落,模型扫一眼就能定位。

常见问题:把参考写太长会拖慢吗?不会——只要它不被无谓读取。SKILL.md 的步骤 4 写明"只在这一步需要时才读",模型就会按需加载。真正拖慢的是把大段参考直接塞进 SKILL.md 正文,因为它一旦加载就跨多轮常驻。

步骤 5:编写表现层 templates/report.md

模板模式(template pattern)让最终产物的格式稳定、可被下游脚本解析。模板里只放结构与约束,不放内容。

# 根因分析报告

**分析对象**:{{文件名}} **分析时间**:{{当前会话时间}} **事件总数**:{{N}}

## 1. 主异常
- 类型:{{最频/最早爆发的异常签名}}
- 爆发窗口:{{起止时间}},峰值 {{N}} 条/分钟

## 2. 因果候选链
{{按时间排列的关键事件序列,每条带事件编号与时间戳}}

## 3. 根因假设(按证据充分度排序)
### 假设 1:{{一句话根因}} — 置信度 高/中/低
- 依据:事件 #{{n}}, #{{m}}
- 反证(若有):{{...}}

## 4. 建议排查动作
1. {{可立即执行的第一步}}
2. {{...}}

## 5. 复核清单
- [ ] 所列时间戳均可在原日志中检索到
- [ ] 每条假设标注了依据事件
- [ ] 证据不足处已明确标注

设计要点:第 5 节"复核清单"是反馈循环的体现——它强迫模型在交付前自检,把"可验证"从中间产物延伸到最终报告。

步骤 6:用真实日志跑通端到端

构造一份贴近真实的故障日志(连接池耗尽导致服务雪崩):

cat > /tmp/incident.log <<'EOF'
2026-06-15T14:00:00 INFO  starting worker pool size=50
2026-06-15T14:02:11 WARN  slow query 3200ms on orders
2026-06-15T14:02:12 WARN  slow query 4100ms on orders
2026-06-15T14:02:30 ERROR ConnectionTimeout db.internal:5432
2026-06-15T14:02:30 ERROR ConnectionTimeout db.internal:5432
2026-06-15T14:02:31 ERROR ConnectionTimeout db.internal:5432
2026-06-15T14:02:45 ERROR NullPointerException at OrderService.java:118
2026-06-15T14:03:01 FATAL OutOfMemoryError Java heap space
EOF

在 Claude Code 中输入:

/log-rca /tmp/incident.log

预期结果:Claude 先在内部执行 extract.py(你会在工具调用区看到一次带 log-rca 注释的 Bash 调用,因 allowed-tools 预批准而无需确认),随后基于注入的摘要推理。报告应将 ConnectionTimeout 在 14:02 的爆发 列为主异常,关联紧随其后的 NullPointerException 与 OutOfMemoryError,给出类似"数据库连接超时 → 请求堆积 → 堆内存耗尽雪崩"的因果链,并建议先查数据库连接池与慢查询。

步骤 7:调试、触发调优与团队分发

调试中间产物:临时把动态注入行改成 !cat /tmp/last_extract.md``,手动跑一次脚本把结果存进去,就能反复调试推理逻辑而不必每次重跑解析。

触发调优:若 Skill 触发太频繁(用户只是提了"日志"二字就被加载),在 description 里收紧条件、加入更具体的宾语;若触发不够,补充用户口语("为什么挂了""5xx 飙升""服务起了吗")。description 被 Claude Code 自动截断到约 1024 字符,超长部分不参与匹配,把触发短语放在前部。

分发:把整个 .claude/skills/log-rca/ 目录提交到仓库即可被团队共享;若想跨项目复用,移到 ~/.claude/skills/。要进一步打包成可独立分发的插件(同时捆绑 agents/hooks/MCP),在 Skill 目录加 .claude-plugin/plugin.json,即可作为 <name>@skills-dir 插件加载。

训练营适用性评估

维度 评估
适合的训练营类型 SRE/运维训练营、AI 工程实战营、企业内部研发提效培训
学员基础要求 熟悉 Python 基础、读过至少一份 SKILL.md、理解正则与 JSON
预计教学时长 授课 45 分钟 + 动手 60 分钟 + 复盘 15 分钟,共约 2 课时
可拓展练习 ① 把 extract.py 改为支持 JSON 结构化日志;② 增加 references/metrics.md 关联 Prometheus 指标;③ 用 context: fork 让分析在隔离子 agent 中跑,避免污染主上下文
评估方式 给定一份隐藏答案的故障日志,以报告是否锁定正确根因 + 复核清单全部可验证为评分标准

延伸阅读

参考来源

  1. Extend Claude with skills — Claude Code Docs — Skills 目录结构、frontmatter、动态上下文注入(!cmd``)与实时变更检测的官方权威说明
  2. Skill authoring best practices — Claude API Docs — 简洁原则、自由度匹配、工作流/反馈循环模式、可验证中间输出、渐进披露与反模式的官方指南
  3. Claude Code Skill Frontmatter: Every YAML Option Explained — 15 个 frontmatter 字段(name、description、allowed-tools、disable-model-invocation、context、paths、hooks 等)的默认值与示例速查
  4. Claude Code Skills: How to Set Them Up and Use Them — SKILL.md 文件夹结构、支持文件(scripts/references/templates)组织与上手示例
  5. The Complete .claude Directory Guide for Claude Code [2026] — .claude 目录下 skills/commands/agents/hooks 的层级覆盖优先级与团队分发实践

[!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学通信知识星球海报-扫码加入