技能激活检查 是做什么的?
技能只有在模型在正确时机调用它时才有价值。这个决策几乎完全取决于技能 frontmatter 中的 description(描述)——所以“我的技能会触发吗?”是一个可测量的问题,而这就是你的测量方法:构建一组标记好的用户请求,对每个请求运行真正的 Agent,并评分技能是否被启用。
用户的请求是:$ARGUMENTS
步骤 1 — 定位目标技能
$ARGUMENTS 可能是 SKILL.md 的路径、技能 目录 的路径、技能名称或为空。
- 为空:使用 glob 查找
.claude/skills/*/SKILL.md和**/skills/*/SKILL.md。匹配到一个→使用它。多个→列出它们并询问使用哪个。没有→告知并停止。 - 目录:使用该目录内的
SKILL.md。 - 名称:查找匹配的技能目录。
裸技能名称即包含 SKILL.md 的目录名称。读取 frontmatter 中的 description 并将其放在面前:这个字符串是模型匹配的依据,因此它是数据行设计和文本编辑建议的主要输入。
在读取时测量其长度——两个独立的预算会截断它,任何一个都会产生一个看起来像是措辞不佳的低召回率结果。
- 单个技能截断。
description和when_to_use被拼接并在一个固定的字符预算处截断——1,536 个字符,可通过skillListingMaxDescChars设置配置。超过截断点的触发文本完全无法影响激活,因此它等同于不存在。 - 整个列表的预算,这在执行激活测试套件的仓库中更为重要。 列表总是包含每个技能的名称,但其总字符预算大约为 模型上下文窗口的 1%,由用户安装的 每个 技能共享。当溢出时,Claude Code 会从你调用最少的技能开始丢弃描述。
第二个后果值得直言:在一个多技能仓库中,一个技能可能拥有完美的描述但召回率接近零,因为它的描述从未被放入列表中。然后重写措辞也无济于事。而丢弃顺序是最少调用的优先,因此一个新创作的技能——它本身被调用次数极少,且正是有人对此运行测试套件的对象——是最有可能的受害者。这是对被测技能的系统性偏见。
如果问题出在列表上,可用的杠杆有:skillListingBudgetFraction(默认 1%)、SLASH_COMMAND_TOOL_CHAR_BUDGET 环境变量(固定字符数),以及将 skillOverrides 设置为 "name-only" 以从不需要匹配的技能中释放预算。
如果技能在其 frontmatter 中没有 description,则停止并报告该发现——没有描述的技能永远无法被模型调用,因此测试套件会构建出零召回率的结果,且无法提供任何有用信息。
步骤 2 — 确认已安装 coder-eval
运行 coder-eval --version。安装此插件并未安装 CLI,且没有它就无法验证或运行测试套件。
如果缺失,请按照 ${CLAUDE_PLUGIN_ROOT}/reference/cli-setup.md 的说明:提供安装选项,运行前询问,并在安装后使用 coder-eval --version 确认。切勿未经提示安装,如果用户拒绝则不要继续。
该参考文档还涵盖了版本检查的另一半——该项目是否固定了 coder-eval 版本,以及当已安装版本不匹配时该如何处理。
步骤 3 — 检查该技能已有的覆盖
在设计任何内容之前,先查看仓库中已有的内容。根据 ${CLAUDE_PLUGIN_ROOT}/reference/repo-layout.md 找到任务树,并使用 glob 查找带有 skill_triggered 标准的任务。
如果现有套件已命名此技能,报告其覆盖率——包含多少个正面用例和干扰行,及其依赖的 suite_thresholds——并提议扩展它,通过向其数据集追加行,而不是搭建一个并行的套件。一个技能的两个套件会逐渐偏离,用户每次运行都要为两者付费。只有在没有套件覆盖此技能时,或者用户在查看现有内容后选择这样做时,才进行搭建。
有两件事值得报告,而不是默默绕过:
- 一个没有干扰行的现有套件。 精度按定义为 1.0,因此其一半结果毫无意义。提议添加干扰项——这比搭建一个竞争套件或保持沉默是更实际的改进。
- 一个命中了不同技能的相邻套件。 这不算是覆盖。继续搭建,并说明为什么相邻套件不算。
然后检查 agent.plugins 应该来自哪里。 如果任务要解析的实验已经提供了该块,则继承它,不要编写一个新的——重新声明实验已提供内容的任务会逐渐偏离,且没有任何机制能捕获这种偏差。只有当没有实验向沙箱暴露该技能时,才编写模板自身的块。如果存在多个实验,且不清楚该任务解析的是哪一个,则询问而非猜测。
步骤 4 — 设计数据行
这一步是整个实验的核心。其余都是机械工作。
正面用例行——真实用户可能提出的、描述声称要覆盖的请求。进行释义;切勿复制描述中的措辞。从描述中直接提取的行测试的是字符串匹配,而非激活。变换词汇,并至少包含一个间接行,其中用户描述的是他们的问题而非技能执行的操作。
干扰行——技能不应处理的相邻请求,尤其是那些与描述共享词汇的请求。这些才是使精度有意义的关键。
兄弟技能所有行 (可选)——合理属于同一仓库中已命名的其他技能的请求,且 expected_skill 设置为该兄弟技能。在多技能仓库中,两个相邻技能之间的错误路由是常见故障,而一个普通的干扰行只能显示发生了误触发,却无法显示误触到了哪里。这些行指明了去向。
当两个技能具有重叠主题,且你想知道哪一个获胜时,添加这些行。它们会消耗额外的行,而每一行都是一次完整的 Agent 运行——因此将其视为有针对性的后续步骤,而非默认操作。
切勿在提示中命名技能。 那测试的是服从性,而非激活:
- 错误:“使用 pdf-forms 技能填写此申请表。”
- 正确:“我需要填写此 PDF 申请表上的字段并将其发回。”
此规则不受兄弟技能所有行的影响:expected_skill 是数据集中的标签,由标准读取,且从未向 Agent 展示。该行的 prompt 仍然不得命名任何技能,包括兄弟技能。
规模。 最少 3 个正面用例 + 3 个干扰行。目标是每种 8-12 个以获得可据此行动的信号——3 行上的召回率以 33 个百分点的步长移动,这太粗糙,无法区分真正的回归和噪声。提供的模板包含 6 行,因为那只是演示的最小值,而非目标。任何兄弟技能所有行都是额外的:8-12 个正面用例加 8-12 个干扰行已经是 16-24 次运行,因此在编写套件前说明最终总数。
拒绝生成没有干扰行的套件。 没有负面用例,精度按定义为 1.0,且一半结果毫无意义。说明原因并要求提供相邻案例。
步骤 5 — 编写套件
将两个模板文件复制到用户的任务树中,位置按照 ${CLAUDE_PLUGIN_ROOT}/reference/repo-layout.md 确定(如果仓库中没有,则提议一个位置并询问):
${CLAUDE_PLUGIN_ROOT}/reference/templates/activation.yaml${CLAUDE_PLUGIN_ROOT}/reference/templates/activation-rows.jsonl
然后进行替换,并保持 JSONL 与 YAML 相邻(dataset.paths 条目相对于任务文件解析):
task_id→<技能名称>-activationskill_name→ 裸技能名称- 每个正面用例行的
expected_skill→ 相同的裸名称;干扰行保持"";兄弟技能所有行的expected_skill→ 该兄弟技能的裸名称 - 每一行的
prompt→ 步骤 4 设计的请求,每行一个 JSON 对象
然后使技能可访问,这是决定套件是否有意义的步骤。 任务在一个全新的沙箱中运行,该沙箱不包含用户的任何文件,因此除非任务指明了技能的位置,否则 Agent 不会获得任何技能。这就是模板中的 agent.plugins 块——并且只有在没有其他东西暴露该技能时才应由模板承担:如果步骤 3 找到了提供该块的实验,则继承它并删除模板的副本,而不是编写第二个声明。
否则,填写它。path 必须是插件根目录:一个包含 skills/ 子目录的目录,这样技能位于 <路径>/skills/<技能名称>/SKILL.md。.claude-plugin/plugin.json 是可选的——没有它时命名空间默认为目录自身的名称。
对于 .claude/skills/pdf-forms/SKILL.md,其根目录是 .claude,而不是 .claude/skills。指向一个只包含技能目录的目录则什么也加载不到。
构建一个最小根目录,而不是直接指向 .claude 本身。 一个插件根目录会加载整个插件,而不仅仅是其技能:位于 skills/ 旁边的 agents/、commands/ 或 hooks/ 目录也会对被评估的 Agent 可见。指向仓库的 .claude 就等于将每个项目的子Agent 和命令都交给了 Agent——而一个可以回答请求的子Agent 是一个混杂因素,而非一个细节。如果在测量 pdf-forms 时 .claude/agents/pdf-expert.md 存在,Agent 可能会委托给它而不是调用技能;skill_triggered 记录为 no,并且召回率因与技能描述无关的原因而下降。这也使得数字依赖于仓库,因此两个套件不再可比。
因此,构建一个只包含被测单元的根目录,并指向它:
SKILL_ROOT="$(mktemp -d)"
mkdir -p "$SKILL_ROOT/skills"
ln -s "$(pwd)/.claude/skills/pdf-forms" "$SKILL_ROOT/skills/pdf-forms"
export SKILL_SOURCE_PATH="$SKILL_ROOT"
在符号链接不方便的地方(Windows、某些 CI 镜像),使用 cp -R 替代 ln -s。如果用户更喜欢单行命令,export SKILL_SOURCE_PATH="$(pwd)/.claude" 仍然有效——但需明确指出这也会暴露 .claude 下的所有其他内容,因此低召回率可能是兄弟技能而非技能本身的问题。
将其保留为环境变量,而不是将绝对路径硬编码到 YAML 中——套件会被提交并在其他机器上重新运行。如果变量未设置,技能就会缺失,每个正面用例行都得 0 分,且结果与一个永不触发的技能无法区分,因此在报告任何低召回率的发现之前,先确认它已被设置。
即使技能来自插件并调用为 plugin:skill,skill_name 也必须是裸名称——检查器在比较前会去除命名空间。这里使用带命名空间的值会悄悄地在每一行上得零召回率,这看起来完全像一个损坏的技能。
因为匹配基于裸名称,它无法处理名称冲突。 两个名为 init 的技能——一个来自插件,一个内置——对标准来说是同一个字符串,因此它会将任何触发的技能视为被测对象。在信任结果之前,检查该名称在所有已安装内容中是否唯一(/context 和 /doctor 会列出活动集)。冲突不会产生错误;它测量的是错误的技能。如果存在冲突,则直接说明,而不是报告数字——重命名技能,或在不存在冲突的环境中测量,才是唯一诚实的修复方法。
对于超出此模板的标准字段,请阅读 ${CLAUDE_PLUGIN_ROOT}/reference/criteria.md。
步骤 6 — 在运行前验证
coder-eval plan <activation.yaml的路径> 必须返回退出码 0。它仅进行架构检查——不会读取数据集文件——因此也请确认 JSONL 文件与 YAML 文件相邻且每行包含一个对象。
步骤 7 — 运行
每一行都是一次完整的 Agent 运行:N 行意味着 N 次运行和真实的令牌成本。说明行数和将要使用的 Agent/模型,然后在开始前询问。
coder-eval run <activation.yaml的路径>
该标准是 Agent 无关的——它通过 Skill 工具检测 Claude 是否使用了技能,并通过从磁盘读取技能文件来检测任何没有该工具的 Agent(例如 Codex)——因此无论任务解析到哪个 Agent,同一套件都有效。
步骤 8 — 报告与解读
呈现召回率、精度、F1 分数和混淆矩阵,然后说明它们的含义:
- 低召回率(遗漏了本应捕获的行):在断定描述覆盖面不足之前,先排除截断和列表驱逐问题。 两者都会产生与措辞不佳无法区分的低召回率结果,且检查成本都很低廉:
/doctor会估算列表的上下文成本及其最大贡献者,而/context中的 Skills 行会报告应用预算后的列表大小——这才是模型实际接收到的内容。只有在描述被确凿证明在列表内且未超过单个技能截断点时,措辞才是罪魁祸首:它没有提到应该触发它的情境、文件类型或措辞方式。 - 低精度(在干扰行上触发):描述覆盖过度,正在抢占相邻请求。缩小其范围,并明确说明技能不适用于什么。
- 误触集中在一个兄弟技能上(配合兄弟技能所有行):这是两个描述之间的边界争议,而非一个模糊的描述。单独修复被测技能往往会转移问题而非消除它——明确说明两个技能各自不适用于什么,然后重新运行。
- 两者都高:报告数字和行数,并指出小套件说明的问题有限——提议扩大套件。
将问题指向 frontmatter 中的 description 作为待修改的内容,引用导致问题的具体行,并在用户重写后提议重新运行套件。