首页 › tutorials › 造变更影响Skill秒圈波及面

造变更影响分析 Skill:改一处代码,秒圈全仓波及面

概述

本教程带你从零构建一个 change-impact(变更影响分析)Skill:当你在 Git 里改动了一个函数或类,它能自动反向追溯出所有调用方、必须重跑的测试、待核对的文档,并按风险等级(红/黄/绿)输出报告。

更重要的是,本教程借此讲解 Skill 最容易被忽视的核心机制——渐进式披露(Progressive Disclosure):为什么 SKILL.md 应该只放"流程步骤",而把繁重的确定性分析交给 scripts/、把可复用的规则知识放进 references/。学完你掌握的不是又一个孤立小工具,而是一套"让 Skill 经久耐用、不占上下文"的工程化方法。

适用人群:已跑通基础 Skill(写过单文件 SKILL.md)、想让 Skill 处理真实工程复杂度的开发者。

前置条件


一、为什么 90% 的 Skill 用一次就荒废:先搞懂渐进式披露

很多人写 Skill 的第一反应,是把所有知识、规则、示例代码一股脑塞进 SKILL.md。这恰恰是失败的根源。

根据 Agent Skills 官方规范,一个 Skill 的目录结构是这样的:

change-impact/
├── SKILL.md          # 必需:元数据 + 指令
├── scripts/          # 可选:可执行代码
├── references/       # 可选:参考文档
├── assets/           # 可选:图片等资源
└── skills/           # 可选:嵌套子 skill

关键在于 SKILL.md 的加载方式是分阶段的:

  1. 常驻阶段:只有 SKILL.md 的 YAML 元数据(name + description)被加载进上下文,通常只有几十个 token。Claude 据此判断"要不要激活这个 Skill"。
  2. 激活阶段:当判断相关时,才加载 SKILL.md 的正文指令(规范建议控制在 5000 token 以内)。
  3. 按需阶段:正文里若指引"详见 references/xxx.md"或"运行 scripts/xxx",那些文件只有真正需要时才被读取或执行。

这就像图书馆:书名和摘要(元数据)永远摆在检索系统里,但书的具体内容(references)只有你借阅时才占你书桌。如果你把整本书抄进检索卡(巨型 SKILL.md),每查询一次就占满书桌,很快系统就转不动了。

业界对此有一条被反复验证的结论:SKILL.md 应该只描述"流程步骤"(Process Steps)——什么时候做、按什么顺序做、调用哪个脚本/参考——而把详尽的规则、长篇示例、领域知识全部下沉到 references/ 文件里按需读取。

change-impact 正是这套理念的完美载体:它的核心是"对每个被改符号在全仓做反向追溯"——这种确定性、高重复的计算,交给 scripts/blast_radius.py 去跑,一个 token 都不占 Claude 的上下文;而风险评级细则、输出格式规范,则放进 references/,Claude 只在需要补充判断时才去读。


二、设计 change-impact:把"重活"交给脚本,把"判断"交给 Claude

先想清楚分工边界,这是写出好 Skill 的关键一步。

工作 谁来做 为什么
读 git diff、提取被改符号 脚本 blast_radius.py 确定性逻辑,用正则最稳,不消耗 token
全仓反向追溯调用点 脚本(rg/grep) 比让 Claude 一个个读文件快几个数量级
分类(源码/测试/文档) 脚本 规则固定,无需"思考"
评估"这次改动是否破坏契约" Claude 需要语义理解,这是 LLM 的主场
把报告翻译成给队友看的人话 Claude 需要表达力

这张表直接决定了我们的目录骨架:脚本负责前三行的"苦力",Claude 负责后两行的"判断"。


三、动手实现:四个文件,约 120 行代码

步骤 1:建立目录骨架

在项目的 .claude/skills/(个人级则放 ~/.claude/skills/)下创建:

mkdir -p .claude/skills/change-impact/{scripts,references}

最终结构:

.claude/skills/change-impact/
├── SKILL.md
├── scripts/
│   └── blast_radius.py
└── references/
    └── risk-rules.md

步骤 2:写 SKILL.md(只放流程,不放知识)

这是整个 Skill 的"调度大脑",必须精简。注意 description 字段——它是触发开关,要写清楚什么时候该用:

---
name: change-impact
description: 分析一次代码变更(git diff)的波及范围。当用户改了函数/类/接口后问"影响哪些地方""要重跑哪些测试""合并前风险",或在 PR 审查、重构、删函数前做影响评估时使用。先跑脚本做确定性溯源,再补充语义层面的契约影响。
---

# 变更影响分析

## 何时触发
用户改动了代码,且表达出对"影响范围""回归风险""要跑哪些测试"的关心。

## 执行流程(严格按序)

1. 确认比对区间。若用户给了基准/目标 ref 就用它;否则脚本会自动回退到 main/master/HEAD~1。
2. 运行溯源脚本(在仓库根目录执行):
   ```bash
   python3 "$SKILL_DIR/scripts/blast_radius.py" [base] [head]
   ```
   把 `$SKILL_DIR` 替换为本 Skill 目录的实际绝对路径。
3. 脚本输出 Markdown 报告,含三档:🔴 高风险调用方 / 🟡 必跑测试 / 🟢 待核对文档。
4. 基于报告,由你(Claude)补充脚本无法判断的部分:
   - 被改符号若被导出为公共 API,提示对外契约可能破坏。
   - 若 🔴 清单里有"核心路径"文件,提升风险定级并说明理由。
   - 若 🟡 为空,明确提示"缺乏测试覆盖,建议补充回归用例"。
5. 输出给用户:一段结论 + 必跑测试清单 + 建议核对项。

## 风险评级细则
参见 references/risk-rules.md,仅在需要解释某条定级理由时读取。

注意三个设计细节:

步骤 3:写 scripts/blast_radius.py(确定性分析引擎)

这是 Skill 的"苦力"。完整可运行,覆盖 Python/JS/TS/Go/Rust/Java 等主流语言:

#!/usr/bin/env python3
"""blast_radius.py — 变更影响分析引擎
用法: python3 blast_radius.py [基准ref] [目标ref] [仓库根目录]
基准默认 main/master/HEAD~1(按可用性回退), 目标默认 HEAD。
输出 Markdown 报告: 🔴高风险调用方 / 🟡必跑测试 / 🟢待核对文档。
"""
import os, re, sys, subprocess

EXCLUDE_DIRS = {".git","node_modules","dist","build","vendor",
                "__pycache__",".venv",".next","target",".idea",".vscode"}
TEST_RES = [r"test_.+\.py$", r".+_test\.go$", r".+\.(test|spec)\.(ts|tsx|js|jsx)$",
            r"Test.+\.java$", r"tests?/.+\.(py|ts|js)$"]
# 多语言函数/类定义符号提取
DEF_RE = re.compile(
    r"\b(?:def|class|fn|func|function|interface|type|struct|enum|"
    r"export\s+(?:default\s+|async\s+)?function)\s+([A-Za-z_][A-Za-z0-9_]+)")
KEYWORDS = {"if","for","while","return","new","class","def","import","from",
            "const","let","var","function","func","fn","true","false","null",
            "void","this","self","async","await"}

def run(cmd):
    return subprocess.run(cmd, capture_output=True, text=True).stdout

def pick_base():
    for c in ("main","master","origin/main","origin/master","HEAD~1"):
        if subprocess.run(["git","rev-parse","--verify","--quiet",c],
                          capture_output=True).returncode == 0:
            return c
    return "HEAD~1"

def changed_symbols(base, head):
    diff = run(["git","diff","--unified=0",f"{base}..{head}"])
    syms = set()
    for line in diff.splitlines():
        if line.startswith("+") and not line.startswith("+++"):
            for s in DEF_RE.findall(line):
                if len(s) >= 2 and s not in KEYWORDS:
                    syms.add(s)
    return syms

def is_excluded(path):
    return any(p in EXCLUDE_DIRS for p in path.replace("\\","/").split("/"))

def search(symbol, repo):
    try:
        out = subprocess.run(["rg","-n","--no-heading","-w",symbol,repo],
                             capture_output=True, text=True, check=True).stdout
    except Exception:
        out = subprocess.run(["grep","-rnw",symbol,repo],
                             capture_output=True, text=True).stdout
    hits = []
    for l in out.splitlines():
        seg = l.split(":",2)
        if len(seg) < 3: continue
        path, ln, code = seg[0], seg[1], seg[2]
        if is_excluded(path): continue
        hits.append((symbol, path, ln, code.strip()))
    return hits

def kind(path):
    p = path.replace("\\","/")
    if any(re.search(r, p) for r in TEST_RES): return "test"
    if p.endswith(".md"): return "doc"
    return "src"

def main():
    repo = sys.argv[3] if len(sys.argv) > 3 else os.getcwd()
    base = sys.argv[1] if len(sys.argv) > 1 else pick_base()
    head = sys.argv[2] if len(sys.argv) > 2 else "HEAD"
    syms = changed_symbols(base, head)
    if not syms:
        print(f"## 变更影响分析 `{base}..{head}`\n\n未识别到函数/类级符号——"
              "可能仅改了函数内部实现或纯文本, 建议人工定位核心改动。")
        return
    buckets = {"src":[], "test":[], "doc":[]}
    for s in syms:
        for h in search(s, repo): buckets[kind(h[1])].append(h)
    print(f"# 变更影响分析报告\n")
    print(f"**比对** `{base}..{head}`  **改动的符号**: "
          + ", ".join(f"`{s}`" for s in sorted(syms)) + "\n")
    print(f"## 🔴 高风险调用方({len(buckets['src'])} 处)")
    for s,p,l,c in sorted(buckets['src'], key=lambda x:x[1])[:40]:
        print(f"- `{p}:{l}` 调用 `{s}` — `{c[:70]}`")
    print(f"\n## 🟡 必跑测试({len(set(x[1] for x in buckets['test']))} 个文件)")
    seen=set()
    for s,p,l,c in buckets['test']:
        if p not in seen: seen.add(p); print(f"- `{p}` 命中 `{s}`")
    if not seen: print("- 未找到直接覆盖, 建议补回归用例")
    print(f"\n## 🟢 待核对文档({len(buckets['doc'])} 处)")
    for s,p,l,c in buckets['doc'][:20]:
        print(f"- `{p}:{l}` 提到 `{s}`")
    print("\n---\n_确定性溯源由 blast_radius.py 完成; 契约/语义影响由 Claude 补充。_")

if __name__ == "__main__":
    main()

赋予执行权限:chmod +x .claude/skills/change-impact/scripts/blast_radius.py

设计要点:

步骤 4:写 references/risk-rules.md(按需加载的规则)

这部分是"图书馆里的书",默认不加载,只有 Claude 需要解释某条定级理由时才读:

# 风险评级细则(change-impact 参考)

## 🔴 高风险(必须人工核对)
- 调用点位于 `src/api/`、`src/core/`、`internal/` 等核心路径。
- 被改符号出现在 ≥ 5 个文件的导入语句中(公共 API 特征)。
- 改动涉及函数签名(参数增删、返回类型变化)。

## 🟡 中风险(务必重跑测试)
- 存在直接覆盖的测试文件 → 必跑。
- 无覆盖但调用方 ≥ 3 → 必须先补用例再合并。

## 🟢 低风险(顺手核对)
- 仅文档/注释引用,行为不受影响。
- 调用点位于已废弃目录(`deprecated/`、`legacy/`)。

## 误报处理
- 短名符号(如 `get`、`id`)易误匹配,溯源时若某命中明显无关,可忽略并说明。

步骤 5:安装与触发

无需额外注册——放入 .claude/skills/ 后,Claude Code 启动时会自动发现。触发方式有两种:

自动触发:直接用自然语言,description 里的关键词会命中。

我刚改了 parseConfig 函数,帮我看看影响哪些地方,要重跑哪些测试?

强制触发(调试时用):/change-impact 或明确点名。

预期结果:Claude 先跑脚本,拿到三档报告,再补一段语义化结论,例如"这个函数是 8 个模块的公共入口,签名变更属高风险,必须先跑 test_config.py 再合并"。


四、实战:在真实项目上跑一次

准备一个有多处调用的项目,制造一次签名变更:

git checkout -b feat/add-timeout
# 编辑 config.py,给 parse_config 加一个 timeout 参数
sed -i 's/def parse_config(path):/def parse_config(path, timeout=30):/' config.py

在 Claude Code 里输入:

这次改动会影响哪些地方?合并前我要跑哪些测试?

Claude 会执行脚本,你看到的报告大致长这样:

# 变更影响分析报告
**比对** `main..HEAD`  **改动的符号**: `parse_config`

## 🔴 高风险调用方(6 处)
- `src/cli/run.py:42` 调用 `parse_config` — `cfg = parse_config(args.file)`
- `src/api/server.py:118` 调用 `parse_config` — `conf = parse_config(p)`
- ... (共 6 处)

## 🟡 必跑测试(2 个文件)
- `tests/test_config.py` 命中 `parse_config`
- `tests/cli/test_run.py` 命中 `parse_config`

## 🟢 待核对文档(1 处)
- `docs/config.md:23` 提到 `parse_config`

随后 Claude 补充语义判断:

parse_config 被 6 处调用,且新增了带默认值的 timeout 参数——由于有默认值,现有调用方不会编译报错,但行为会变化(超时会触发)。属于中高风险。必跑 tests/test_config.py 和 tests/cli/test_run.py;建议核对 docs/config.md 补充新参数说明。

注意这处分工之美:脚本用 0 token 完成了 6 处调用 + 2 个测试 + 1 篇文档的定位;Claude 只贡献了"有默认值所以不破坏编译,但行为变了"这一条脚本永远说不出的人话判断。


五、进阶:嵌套 Skill 与验证反馈循环

嵌套 Skill:当 change-impact 长大到包含多个子能力时,可在 skills/ 子目录里放嵌套 Skill。例如把"仅分析测试影响"拆成子 Skill change-impact/skills/test-only/,各自有独立 SKILL.md。复杂项目这样能进一步压缩主 Skill 的体积。

验证反馈循环:进阶实践是为 Skill 建一组"黄金案例"——准备 3-5 个已知影响范围的小改动(含期望报告),写成校验脚本,在 Skill 改动后跑一遍,确保溯源结果不回归。这是把 Skill 当"软件"来维护的关键习惯。


六、常见问题排查

问题:脚本识别不到任何符号。 原因:改动可能只在函数体内(如改一行赋值),没有新增 def/function。 对策:脚本已对这种情况给出提示。若想覆盖"改函数体也追溯调用方",可在 changed_symbols 里额外提取被改文件中的所有顶层符号(用 AST 或更宽松的正则)。

问题:误报太多,短名如 get、run 满屏。 对策:在 KEYWORDS 集合里加上这些高频词;或要求符号长度 ≥ 4;或在 search 里要求符号后跟 ((函数调用特征)。

问题:大仓库跑得慢。 原因:每个符号都全仓 rg 一遍,符号多时线性放大。 对策:把多个符号合并成一条正则 rg -w "sym1|sym2|sym3" 一次性搜索,减少进程启动开销;或先用 git diff --name-only 限定只在被改模块的"依赖方"目录里搜。

问题:Skill 没被自动触发。 原因:description 里缺触发词,或措辞太泛。 对策:审视 description,补上用户真实会说的短语("影响哪些""要跑哪些测试""能不能合并"),并避免与其它 Skill 的描述撞车。

问题:$SKILL_DIR 找不到路径。 对策:在 SKILL.md 流程里改用相对仓库根的明确路径,例如 python3 .claude/skills/change-impact/scripts/blast_radius.py;个人级 Skill 用 ~/.claude/skills/...。


工作流全景图

下图展示 Skill 的渐进式披露架构——元数据常驻、正文按需、脚本零 token、参考最末:

flowchart TD
    A[用户: 这次改动影响哪些地方?] --> B[匹配 description 触发 Skill]
    B --> C[仅加载 SKILL.md 元数据<br/>name+description ~几十 token]
    C --> D{需要执行分析?}
    D -->|是| E[加载 SKILL.md 正文流程 ~300 token]
    E --> F[调用 scripts/blast_radius.py]
    F --> G[确定性溯源<br/>git diff + ripgrep<br/>不占 Claude 上下文]
    D -->|需解释定级| H[按需读 references/risk-rules.md]
    G --> I[Claude 解读报告<br/>补充语义/契约影响]
    H --> I
    I --> J[输出红黄绿三档影响清单]

下图是 blast_radius.py 内部的溯源流水线:

flowchart LR
    A[git diff base..head] --> B[正则提取被改符号<br/>def/class/function...]
    B --> C[对每个符号 ripgrep 全仓溯源]
    C --> D{按文件类型分类}
    D --> E1[🔴 源码调用方]
    D --> E2[🟡 测试文件]
    D --> E3[🟢 文档引用]
    E1 --> F[结构化 Markdown 报告]
    E2 --> F
    E3 --> F
    F --> G[Claude 接管: 语义判断 + 建议]

延伸阅读


训练营适用性评估


参考来源

  1. Agent Skills Specification — Agent Skills 完整格式规范:目录结构、frontmatter、body token 预算(<5000)与加载机制
  2. Extend Claude with skills — Claude Code Docs — Claude Code 官方 Skill 创建、管理与分享指南
  3. Claude Code Skills Architecture: Why Your skill.md Should Only Contain Process Steps — 论证 SKILL.md 只放流程、细节下沉 references 的渐进式披露架构
  4. How to Create Agent Skills with Scripts and Reference Files — 用 scripts 与 references 构建高级 Skill 的中级实战,含验证反馈循环
  5. Agent Skills — Claude Platform Docs — Claude 平台对 Agent Skills 模块化能力的官方总览

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