369 个 skill 上架,0 次调用:我拉出调用日志后重画的「盘活」三道闸

装了一个 286 skill 的 marketplace,开 6 天、每会话往上下文注入 30 KB 货架,hub skill 调用 0 次。用本机 27 个会话的日志拆出「盘活」三道闸:描述预算在字母 b 处用尽、写了中文触发语的只有 1/286、通用能力零增量。附可复现度量脚本。

我在自己的机器上装了一个有 286 个 skill 的 marketplace,开了 6 天。这 6 天里它每个会话都往上下文注入一份 30 KB、381 条的 skill 货架清单。然后我把调用日志拉了出来:这 286 个 skill,一次都没有被调用过。 同一批会话里,我自己手写的两个 skill 被调用了 5 次。

这篇不是「skill 没用」,恰恰相反——被调用的那两个是我最依赖的工具。这篇要回答的是那个更难的问题:一个 skill 从「在货架上」到「真的被用」,中间到底卡在哪几道闸上。三道闸我都测出了数字,其中第一道的失败原因荒谬得我不得不反复核了三遍:它取决于 skill 名字的首字母。

名词速查

术语一句话解释
skill一个 SKILL.md 文件,写清「什么时候用我」和「按什么步骤做」,Agent 按需加载;本站专文
skill hub / marketplace可一键安装的 skill 集合仓库,装一次进来几十到几百个
skill 货架(listing)每个会话开头注入上下文的那份「可用 skill 目录」,通常是 名字: 描述 的列表
渐进式加载只把名字和描述放进上下文,正文等真被调用时才读——不然几百个 skill 的正文根本装不下
自动触发模型自己根据 description 决定调用某 skill,区别于用户敲 /skill-name 显式调用
调用日志会话记录里的 Skill 工具调用条目,是「这个 skill 真被用了」的唯一硬证据

一、先给结论

我的结论是一句话:skill hub 的库存量和调用量之间几乎没有关系;一个 skill 要被盘活,得连过三道闸,而 hub 模式恰好在每一道上都有系统性劣势。

三道闸,以及我在本机测到的数字:

问的问题本机实测
一、进得去描述有没有真的进到模型的上下文里?381 条货架里只有 126 条带描述,其余 255 条只有一个名字。切断点在 ecc:benchmark-methodology——字母 b
二、对得上描述里的词,和我实际敲进去的词重合吗?286 个 hub skill 里,描述真正写了中文触发语的 1 个;我敲的是「发布一篇博客」
三、划得来相对模型的默认能力,这个 skill 有增量吗?article-writing 描述进了上下文(第 111 条)、写得也规范,依然 0 调用

第三道闸是真正的分水岭,也是本文最想留下的判断:skill 的硬通货不是知识,是模型猜不到的本地事实。 「怎么写好一篇文章」模型本来就会,写进 skill 是零增量;「这个仓库的校验命令是 pnpm validate:blog、frontmatter 必须有 pubDate、推 main 才会触发部署」模型永远猜不到,写进 skill 就是全部价值。hub 天生只能提供前者。

所以「盘活」的方向不是把 hub 逛得更勤,而是把 hub 当种子库,fork 进自己的仓库,注入只有你的仓库才知道的东西。具体六个动作在第七节。

二、数据从哪来:一次没设计过的自然实验

我不是为写这篇去做实验的。是我三周前装了一个 skill marketplace(ECC,通过 Claude Code 的 plugin 机制安装),后来关掉了,这中间刚好留下了一段带对照的日志。

Claude Code 把每个会话完整写进本地的 .jsonl 记录,其中两类记录正好是我要的:

  • attachment.type == "skill_listing":会话开头注入的 skill 货架,带 skillCountnames 和完整 content
  • Skill 工具调用:模型真的调用了某个 skill。

我把本机 27 个主会话(排除子 agent 侧链)全部解析了一遍,得到三个窗口:

窗口日期主会话数货架条目货架体积Skill 调用其中 hub skill
A08-12 → 08-2410176.9 KB70
B08-27 → 09-01838130.0 KB50
C09-02 → 09-08912–134.9–5.3 KB40

窗口 B 就是 hub 开着的 6 天。合计 27 个会话、149 条我本人敲进去的提问、16 次 skill 调用,落在两个 skill 上:

publish-blog          13 次
researcher-deepdive    3 次

两个都是我自己手写的(一个在全局目录,一个在博客仓库里)。hub 提供的 369 条货架条目(286 个 skill + 83 个 command),调用次数 0

一个必须先挡掉的反驳:「是不是那些 skill 对应的任务根本没发生过?」 部分成立,但不足以解释。hub 里 33% 是 Vue/Rust/Swift 这类栈专属 skill,这 6 天我确实没写 Vue,它们不该被算进来。但同一个 hub 里也有 article-writingdeep-researchresearch-opsprompt-optimizercontent-engineskill-scout——而这 6 天我干的事情正是写博客、做深度检索、设计 skill。任务发生了,而且发生了不止一次。它们照样 0 调用。

(另外必须承认的一条:skillCount 只统计货架,我没有做 skill / no-skill 的 A/B。所以我能证明的是「从未被选中」,不能证明「用了会更差」。这条我放在第九节一起说。)

三、第一道闸「进得去」:描述预算在字母 b 处用尽

先算一笔账,解释为什么货架必须是「名字 + 描述」而不是全文。

hub 里 286 个 SKILL.md,正文平均 8,921 字符,合计 2.55 MB。按 3.6 字符/token 折算约 70 万 token——比很多模型的整个上下文窗口都大。所以渐进式加载不是优化,是唯一可行方案:上下文里只放名字和描述,谁被选中才读谁的正文。

于是「描述有没有进上下文」就成了生死线。我把窗口 B 那份 29,967 字符的货架清单逐行解析,发现它长这样:

# 前 117 条:名字 + 完整描述
- publish-blog: Use when the user wants to publish a new blog post to ...
- researcher-deepdive: Use when the user wants to check tracked AI ...
   ...
- ecc:article-writing: Write articles, guides, blog posts, tutorials, ...
- ecc:benchmark: Use this skill to measure performance baselines, ...

# 第 117 条起:只剩名字
- ecc:benchmark-methodology
- ecc:blueprint
- ecc:brand-voice
- ecc:bun-runtime
- ecc:canary-watch
   ...

统计结果:381 条里 126 条带描述,255 条(67%)只有一个光秃秃的名字。 而且清单是按字母序排的,描述在累计约 19.4 KB 处被切断,切断位置正好落在字母 b

我把窗口 B 的 8 个会话全跑了一遍,想确认这是不是偶发:

0be8dc45  total=381 described=126 bare=255  first_bare=ecc:benchmark-methodology
761e583a  total=381 described=126 bare=255  first_bare=ecc:benchmark-methodology
98b17748  total=381 described=126 bare=255  first_bare=ecc:benchmark-methodology
99596974  total=381 described=126 bare=255  first_bare=ecc:benchmark-methodology
d47fdc1d  total=381 described=126 bare=255  first_bare=ecc:benchmark-methodology
dfed5309  total=381 described=126 bare=255  first_bare=ecc:benchmark-methodology
f6885b4c  total=381 described=126 bare=255  first_bare=ecc:benchmark-methodology
d6686d25  total=380 described=126 bare=254  first_bare=ecc:benchmark-optimization-loop

八次完全一致,确定性的。于是本机这批数据推出了一个我没预料到的结论:

在一个 286+ 的 hub 里,一个 skill 能不能被自动触发,先取决于它名字的首字母。

对我这 6 天的实际影响,逐条查过:

skill货架位置描述进上下文了吗
ecc:agent-eval96
ecc:agentic-engineering102
ecc:article-writing111
ecc:content-engine142否,只有名字
ecc:deep-research164否,只有名字
ecc:prompt-optimizer298否,只有名字
ecc:research-ops318否,只有名字
ecc:skill-scout335否,只有名字

deep-research 的描述原文是「Multi-source deep research using firecrawl and exa MCPs… Use when the user wants thorough research on any topic with evidence and citations」——写得完全合格。它一次都没进过模型的视野。模型看到的只有 ecc:deep-research 六个字符。

观点:这就是 hub 模式最反直觉的规模效应。你往 hub 里多加一个 skill,不只是「多一个选项」,而是在跟所有字母序比你靠后的 skill 抢描述预算。库存越大,靠后的部分越接近隐形。加法在这里是负和的。

需要标注的两处不确定:(1)具体的预算算法我没有权威文档,19.4 KB 是我从一份清单反推的切断点,不同版本大概率不同;(2)第 123 条 ecc:browser-qa 是切断之后唯一还带描述的例外,我解释不了,如实记在这里。能确定的是切断确实发生、位置在字母 b、八个会话完全可复现。

四、第二道闸「对得上」:我敲的是「发布一篇博客」

假设描述进去了。下一道闸是:描述里的词,和用户实际敲进去的词,重合吗?

description 在这套机制里不是简介,是路由键。模型拿用户这句话去跟一百多条描述做语义匹配,匹配上才考虑调用。所以描述该写的不是「我是什么」,而是「用户会怎么开口」。

hub 这批描述的基本功其实不差——286 条里 233 条(81%)都用了 “Use when the user wants…” 的触发式写法。这不是懒。但我一测触发面的另一个维度,差距就出来了:

指标hub 286 个 skill我那两个被调用的 skill
描述里有引号包起来的用户原话示例14 个(4.9%)都有
描述里有中文触发语1 个都有

那 1 个中文的是 openclaw-persona-forge(正则先命中 5 条,我逐条看过,另外 4 条只是英文描述里混进了全角标点和破折号,不算)。

我平时敲的是「发布博客」「把这篇发到博客」「追踪大咖」。publish-blog 的描述里原样写着 (e.g. "发布博客", "publish this post", "把这篇发到博客", "add a blog post")——这不是语义匹配,几乎是字符串命中。researcher-deepdive 同理。

一个 hub 面向全世界发布,用英文写描述完全合理。但代价是它对任何一个非英语母语用户的触发面都被系统性削薄了——这不是 hub 作者的错,是 hub 这个形态的固有成本,而这个成本只能在本地被消掉。

灰度提醒:中文触发语是必要条件,不是充分条件。那 1 个有中文描述的 hub skill 同样 0 调用——它讲的是给 OpenClaw 生成人格设定,我这 6 天确实没干这事。第二道闸过了,还有第三道。

五、第三道闸「划得来」:article-writing 输在哪

这一节是全文的重点,因为它是唯一一个排除了前两道闸的干净样本。

ecc:article-writing

  • 描述进了上下文(第 111 条,前 117 名以内)✓
  • 描述是标准触发式写法:「Write articles, guides, blog posts, tutorials… Use when the user wants polished written content longer than a paragraph, especially when voice consistency, structure, and credibility matter.」✓
  • 我这 6 天写了好几篇博客长文 ✓

调用次数:0。同期 publish-blog 调用 4 次。

为什么?把两个描述并排放,差别一眼可见:

ecc:article-writing
  Write articles, guides, blog posts, tutorials, newsletter issues,
  and other long-form content in a distinctive voice derived from
  supplied examples or brand guidance.
  → 承诺的是一种「能力」:写得好

publish-blog
  Use when the user wants to publish a new blog post to the personal
  Astro blog (repo jokeuncle.github.io, served at ...) (e.g. "发布博客",
  "publish this post", "把这篇发到博客"). Guides creating the markdown
  file with correct frontmatter, validating content, building, and
  pushing to trigger the Cloudflare Pages deploy.
  → 承诺的是一串「事实」:哪个仓库、哪种 frontmatter、哪条校验命令、推哪个分支才部署

我的判断:skill 不是跟「什么都没有」竞争,是跟「模型的默认能力」竞争。 一个模型本来就会写文章。当它读到「我能帮你把文章写得有调性」,理性选择是不调用——多一次工具调用、多读一份文档,换来的是它自己本来就能做的事。这是纯成本。

publish-blog 里装的东西模型无论多聪明都推不出来:这个仓库的 posts 放在 src/content/blog/、frontmatter schema 长什么样、校验命令是 pnpm validate:blog、部署跟着 main 分支而不是 master。不读这份 skill 就一定做错。这时候调用不是成本,是省下一轮返工。

于是可以把三道闸收成一条可操作的自检:

写完一个 skill,问自己:删掉它,模型会不会做错? 答不出具体会错在哪一步,这个 skill 就注定 0 调用——不管描述写得多规范。

这也解释了 hub 的结构性困境。我按内容给 286 个 skill 分了一下类:59% 是通用方法论(coding-standardsapi-designtdd-workflowcontext-budget),33% 是栈专属(vue-patternsrust-testswift-concurrency-6-2),8% 是垂直业务。那 59% 是最容易被 hub 分发的,也恰恰是最容易输给模型默认能力的一档。 hub 能规模化交付的东西,正好是最不需要被交付的东西。

(顺带一个我笑了一下的细节:这个 hub 里有 skill-health——「Show skill portfolio health dashboard with charts and analytics」,专门用来体检你的 skill 组合。它的位置在字母 s,描述没进上下文,调用次数 0。)

六、一个反直觉结果:全开没有淹掉原有 skill

我原本准备写「装 300 个 skill 会把你自己的 skill 淹掉」。数据不支持,所以改掉:

窗口货架条目会话数调用次数每会话调用
A171070.70
B381850.63
C12–13940.44

货架从 17 条涨到 381 条,publish-blog 的调用节奏基本没变。0.70 / 0.63 / 0.44 这个下降趋势在 27 个会话、16 次调用的样本量下说明不了任何事——窗口 C 我写的长文更多、单会话更长,工作负载本身就不一样。

所以这里只留一条能站住的话:hub 全开的代价不是「淹掉老 skill」,是「花了每会话 30 KB(约 7.5k–8.3k token,按 3.6–4 字符/token 折算,非 tokenizer 实测)的货架费,换来 0 次新增调用」。 它不伤害存量,只是纯支出。这比「淹没」更难被发现——因为一切照常工作,账单静悄悄地记在上下文预算里。

七、那到底怎么盘活:六个动作

按我会真的去做的顺序排。

1. 先量,别先加。 盘活的第一步不是逛 hub,是拉调用日志(脚本在第八节)。考核指标从「装了多少个」换成「被调用过的 skill 数 / 货架条目数」。我这个比值是 2/381 ≈ 0.5%。你至少要先知道自己的数。

2. 把货架砍到装得下描述的规模。 本机数据:约 117 条描述就把预算撑满了。超出的部分不是「优先级低」,是隐形。按场景做 profile 分批启用(写前端时开前端那组),而不是长期全开。plugin 的开关就是干这个的。

3. Fork,而不是浏览。 把 hub 当种子库:找到最接近你场景的那个 SKILL.md,拉进自己的仓库,然后把仓库私有的东西灌进去——真实命令、真实路径、真实门禁、真实 schema。hub 给你骨架省下从零起草的时间,价值全在你灌进去的那部分。

4. 描述里写你会真的敲出来的原话,包括母语。 把 description 当路由键写,不当简介写。带上 3–4 个引号包起来的原话示例。这是 5 分钟的改动,也是我这批数据里唯一和「被调用」强相关的特征(n=2,当观察看,别当定律)。

5. 每个 skill 至少携带一条模型猜不到的硬事实。 一条校验命令、一个 schema 约束、一条分支规则、一个门禁判据。没有这一条,它就是在跟模型的默认能力硬碰,必输。这条同时是很好的写作前筛子:想不出这条硬事实,这个 skill 别写。

6. 定期 GC。 30 天 0 调用就下架——腾出的是给还活着的 skill 的描述预算。我这台机器上的 enabledPlugins 现在是 {"ecc@ecc": false},货架从 30 KB 回落到 5 KB。

倒着说一遍会更清楚:如果要让一个 skill hub 彻底废掉,最有效的做法就是往里塞更多通用方法论 skill。 它们描述规范、听起来都对、每一个都在跟字母序靠后的邻居抢预算,而且没有一个能通过第三道闸。这正是我这 6 天实测到的状态。

八、把度量脚本带走

三十行以内,跑一次就知道自己的 skill 命中率。只读本地会话记录,不发网络请求:

# 统计本机所有 Claude Code 会话的 skill 货架与真实调用
import json, glob, os, collections

ROOT = os.path.expanduser('~/.claude/projects')
inv, shelf = collections.Counter(), {}

for f in glob.glob(os.path.join(ROOT, '**', '*.jsonl'), recursive=True):
    if os.path.basename(f).startswith('agent-'):      # 跳过子 agent 侧链
        continue
    for line in open(f, encoding='utf-8', errors='replace'):
        if '"skillCount"' in line:                    # 会话开头注入的货架
            a = json.loads(line).get('attachment', {})
            if a.get('isInitial'):
                entries = [l for l in a['content'].split('\n') if l.startswith('- ')]
                described = sum(1 for l in entries if ': ' in l[2:].replace('ecc:', '', 1))
                shelf[os.path.basename(f)] = (a['skillCount'], described, len(a['content']))
        if '"name":"Skill"' in line:                  # 真实调用
            for c in (json.loads(line).get('message', {}).get('content') or []):
                if isinstance(c, dict) and c.get('name') == 'Skill':
                    inv[c['input'].get('skill')] += 1

print(f'会话数 {len(shelf)}   调用总数 {sum(inv.values())}   被调用过的 skill {len(inv)}')
for s, (n, d, ch) in sorted(shelf.items())[-5:]:      # 最近 5 个会话的货架
    print(f'  {s[:8]}  货架 {n:4} 条,其中带描述 {d:4} 条,{ch/1024:.1f} KB')
for k, v in inv.most_common():
    print(f'  {v:3}  {k}')

两个数看:「带描述 / 货架总数」告诉你有多少 skill 是隐形的;「被调用过的 skill / 货架总数」告诉你货架费花得值不值。我的是 126/381 和 2/381。

如果你想再进一步,最低成本的下一步实验:挑一个第三道闸存疑的 skill,敲 / 显式调用一次,跟不用它做同一件事对比返工次数。这一步我没做,见下节。

九、诚实的提醒

  • 样本很小,且只有一台机器。 27 个会话、149 条我本人的提问、16 次调用。窗口 B 只有 6 天 8 个会话。三个窗口的每会话调用率差异我明确说了不足以支撑结论。
  • 我没有做 skill / no-skill 的 A/B。 本文能证明的是「hub skill 从未被选中」和「为什么没被选中的机制」,不能证明「用了它们会更差」。第三道闸那一节是我的判断(标注为观点),依据是两条描述的对比和调用日志,不是对照实验。
  • token 数是折算的。 30 KB → 7.5k–8.3k token 用 3.6–4 字符/token 估算,没有跑 tokenizer;70 万 token 同理。字符数(29,967 / 2.55 MB)是精确的。
  • 描述预算的切断算法没有权威来源。 19.4 KB 是我从一份清单反推的,browser-qa 那个例外我解释不了。切断本身在 8 个会话可复现。
  • 我的工作负载严重偏向写博客和读论文。 这直接决定了 hub 里 33% 的栈专属 skill 本来就不该被触发。我在第二节做了收窄,只用任务确实发生过的那批(article-writingdeep-research 等)来支撑论点。
  • 最低成本的复现路径:把第八节脚本在你自己机器上跑一遍。如果你的「被调用过的 skill / 货架条目」也远低于 5%,那本文的三道闸对你成立;如果显著更高,我想知道你的 skill 是怎么写的。

相关旧文

  • Agent 的工具调用机制:从 Function Calling、MCP 到 Skill——skill 怎么进入模型视野的机制底座,本文第三节是它的一次实测。
  • Agent Skills 与 MCP 的研究前线——三个月前我在那篇里提出「skill 系统不该只记录被调用了,还要记录调用后局部状态有没有变好」,并列了一个 skill/no-skill A/B 的实验计划。这次真去拉日志才发现,我卡在更低一层:286 个 skill 连一条调用记录都没有,信用分配还没轮到成为瓶颈。 那篇的实验清单我仍然认可,只是要排在三道闸之后。
  • 当提示词成为依赖:读 Warp 的 skills-lock.json——skill 作为共享工件的分发与锁定;本文补的是分发之后没人提的那一半:装进来了,然后呢。
  • Parametric Skills 学习笔记——那篇讲文本 skill 在长上下文里的失效,用 LoRA 把技能编译进参数。本文的第一道闸给它添了一个更朴素的证据:文本 skill 有时根本没进上下文,连「找说明书」的机会都没有。
  • 面向 Agent 封装业务流程交付门禁 vs 评测系统——第五节说的「模型猜不到的本地事实」,在这两篇里是 schema 和门禁判据;skill 是它更轻的一种载体。

参考来源

工程实践(本机一手数据)

  • 本机 Claude Code 会话记录 ~/.claude/projects/**/*.jsonl,27 个主会话,2026-08-12 → 2026-09-08。所有数字由第八节脚本及其变体解析得出。
  • ECC marketplace,plugin 版本 2.2.0(git 源),skills/ 目录 286 个 SKILL.mdcommands/ 目录 94 条,货架中出现 369 条 hub 条目。
  • 描述文本引用自该 plugin 各 SKILL.md 的 frontmatter 原文;publish-blog / researcher-deepdive 引用自我自己的 skill 文件。

说明:本文没有引用论文。三道闸全部来自本机日志与文件内容,属于第一档(亲手测出)与第三档(明确标注的推测),我刻意没有去找论文给它背书——这批数字够硬,不需要。