本教程带你从零构建一个 change-impact(变更影响分析)Skill:当你在 Git 里改动了一个函数或类,它能自动反向追溯出所有调用方、必须重跑的测试、待核对的文档,并按风险等级(红/黄/绿)输出报告。
更重要的是,本教程借此讲解 Skill 最容易被忽视的核心机制——渐进式披露(Progressive Disclosure):为什么 SKILL.md 应该只放"流程步骤",而把繁重的确定性分析交给 scripts/、把可复用的规则知识放进 references/。学完你掌握的不是又一个孤立小工具,而是一套"让 Skill 经久耐用、不占上下文"的工程化方法。
适用人群:已跑通基础 Skill(写过单文件 SKILL.md)、想让 Skill 处理真实工程复杂度的开发者。
git 的机器;可选 ripgrep(rg),没有则脚本自动回退到 grep很多人写 Skill 的第一反应,是把所有知识、规则、示例代码一股脑塞进 SKILL.md。这恰恰是失败的根源。
根据 Agent Skills 官方规范,一个 Skill 的目录结构是这样的:
change-impact/
├── SKILL.md # 必需:元数据 + 指令
├── scripts/ # 可选:可执行代码
├── references/ # 可选:参考文档
├── assets/ # 可选:图片等资源
└── skills/ # 可选:嵌套子 skill
关键在于 SKILL.md 的加载方式是分阶段的:
SKILL.md 的 YAML 元数据(name + description)被加载进上下文,通常只有几十个 token。Claude 据此判断"要不要激活这个 Skill"。SKILL.md 的正文指令(规范建议控制在 5000 token 以内)。这就像图书馆:书名和摘要(元数据)永远摆在检索系统里,但书的具体内容(references)只有你借阅时才占你书桌。如果你把整本书抄进检索卡(巨型 SKILL.md),每查询一次就占满书桌,很快系统就转不动了。
业界对此有一条被反复验证的结论:SKILL.md 应该只描述"流程步骤"(Process Steps)——什么时候做、按什么顺序做、调用哪个脚本/参考——而把详尽的规则、长篇示例、领域知识全部下沉到 references/ 文件里按需读取。
change-impact 正是这套理念的完美载体:它的核心是"对每个被改符号在全仓做反向追溯"——这种确定性、高重复的计算,交给 scripts/blast_radius.py 去跑,一个 token 都不占 Claude 的上下文;而风险评级细则、输出格式规范,则放进 references/,Claude 只在需要补充判断时才去读。
先想清楚分工边界,这是写出好 Skill 的关键一步。
| 工作 | 谁来做 | 为什么 |
|---|---|---|
读 git diff、提取被改符号 |
脚本 blast_radius.py |
确定性逻辑,用正则最稳,不消耗 token |
| 全仓反向追溯调用点 | 脚本(rg/grep) |
比让 Claude 一个个读文件快几个数量级 |
| 分类(源码/测试/文档) | 脚本 | 规则固定,无需"思考" |
| 评估"这次改动是否破坏契约" | Claude | 需要语义理解,这是 LLM 的主场 |
| 把报告翻译成给队友看的人话 | Claude | 需要表达力 |
这张表直接决定了我们的目录骨架:脚本负责前三行的"苦力",Claude 负责后两行的"判断"。
在项目的 .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
这是整个 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,仅在需要解释某条定级理由时读取。
注意三个设计细节:
description 里密集铺设触发词("影响范围""回归风险""要跑哪些测试""重构""删函数"),这是 Skill 能否被自动激活的关键。$SKILL_DIR 用环境变量或绝对路径——Claude Code 激活 Skill 时会暴露相关路径,确保脚本可定位。这是 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
设计要点:
def/function/func/class 等,过滤关键字避免噪音。rg(快),没有则回退 grep,保证可移植。node_modules、.venv、dist 等目录,避免误报。这部分是"图书馆里的书",默认不加载,只有 Claude 需要解释某条定级理由时才读:
# 风险评级细则(change-impact 参考)
## 🔴 高风险(必须人工核对)
- 调用点位于 `src/api/`、`src/core/`、`internal/` 等核心路径。
- 被改符号出现在 ≥ 5 个文件的导入语句中(公共 API 特征)。
- 改动涉及函数签名(参数增删、返回类型变化)。
## 🟡 中风险(务必重跑测试)
- 存在直接覆盖的测试文件 → 必跑。
- 无覆盖但调用方 ≥ 3 → 必须先补用例再合并。
## 🟢 低风险(顺手核对)
- 仅文档/注释引用,行为不受影响。
- 调用点位于已废弃目录(`deprecated/`、`legacy/`)。
## 误报处理
- 短名符号(如 `get`、`id`)易误匹配,溯源时若某命中明显无关,可忽略并说明。
无需额外注册——放入 .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:当 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 接管: 语义判断 + 建议]
change-impact Skill,并掌握"脚本扛确定性、references 存知识、SKILL.md 只做调度"的可迁移方法论。[!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 等场景全覆盖。
