B 站最近冒出好几个 Agent Skills 的系列课,时长都在十小时以上,讲的多半是技能怎么组织、什么时候被触发,很少有人把它的成本结构摊开量一遍。我把 GitHub 上那份官方技能包整份拉下来(20 个 SKILL.md,仓库 17.9 万 star),量了三级加载的真实开销,又验了一遍只靠目录能不能选对技能。结论是目录层 9785 字节 / 2203 个 token,全量正文 274364 字节 / 74395 个 token,另外还挂着 405 个附件共 10.8 MB;而只凭 name+description 路由,19 条任务句的 top1 命中是 58%,把整篇正文喂进去不归一化反而掉到 42%。
三级加载的口径与实测
Agent Skills 规范把技能内容分成三档,各档进上下文的时机完全不同:元数据(name + description)在会话开始时就全部加载,指令(SKILL.md 主体)要等技能被激活,资源(scripts/、references/、assets/)被引用时才读。规范给的预算是元数据约 100 token、指令建议 5000 token 以内,并建议 SKILL.md 别超过 500 行。
| 层级 | 什么时候进上下文 | 规范口径 | 20 个技能包实测 |
|---|---|---|---|
| 元数据 name + description | 会话开始,所有技能常驻 | 约 100 token/技能 | 9785 字节 / 2203 token,平均 110 |
| 指令 SKILL.md 主体 | 技能被激活时 | 建议 5000 token 以内 | 274364 字节 / 74395 token,平均 3720、中位 2217 |
| 资源 scripts/ references/ assets/ | 被引用时才读 | 访问前不计成本 | 405 个文件 / 10.8 MB |
表里的 token 数不是估的。本机不能装分词库,我按 gpt2 的 vocab 与 merges 表重写了一份字节级 BPE(预切分规则照原正则手写,Python 的 re 没有\p{L},改用 unicodedata 判类别),拿三个已知切分校验,再做全文往返:
'Hello world' ids=[15496, 995] 期望=[15496, 995] OK 'hello world' ids=[31373, 995] 期望=[31373, 995] OK " don't stop" ids=[836, 470, 2245] 期望=[836, 470, 2245] OK 往返校验: 通过校验过了就拿它当尺子,把 20 个技能包的 frontmatter 和正文分别过一遍:
importredefparse_skill(path):raw=open(path,encoding="utf-8").read()m=re.match(r"^---\r?\n(.*?)\r?\n---\r?\n?(.*)$",raw,re.S)fm,body=m.group(1),m.group(2)meta,key={},Noneforlineinfm.splitlines():ifre.match(r"^[A-Za-z_][\w-]*:",line):key,val=line.split(":",1)meta[key.strip()]=val.strip()elifkeyandline.strip():# description 允许折行meta[key]+=" "+line.strip()returnmeta,body# 目录层就是这两行拼起来cat_txt=f"{meta['name']}:{meta['description']}"=== 第 1 层 目录(name + description) === 技能数 20 | 目录合计 9785 字节 / 2203 token | 平均 110 token/技能 description 字符数 中位数 322 最大 1071 === 第 2 层 正文(SKILL.md 主体) === 合计 274364 字节 / 74395 token | 平均 3720 token/技能 正文/目录 字节比 28.0x token 比 33.8x 正文行数 最大 595 行,中位数 126 行110 和规范说的“约 100 token”对得上,误差来自 description 写多长。这一档是三级里唯一常驻的部分:20 个技能目录合起来占 20 万窗口的 1.10%,而把正文全读进来要占 37.2%,差 33.8 倍。
还有个容易忽略的点,description 用什么语言写不一样。同一句请求,中文写“把这份 PDF 里的表格提出来,转成 markdown。”28 个字符要 34 个 token(1.21 token/字符),英文写“Extract tables from this PDF and convert to markdown.”53 个字符只要 12 个 token(0.23 token/字符)。gpt2 词表对中文本来就苛刻,你实际用哪个模型要看它的词表,但方向很清楚:同样一句话,中文 description 的常驻成本不会比英文低。20 个技能如果都用中文写描述,这一层就不是 2203 token 了。
第三层有 10.8 MB,没人管
资源层不进上下文,所以很容易被当成不算成本。这 20 个包里除了 SKILL.md 还有 405 个文件、11330746 字节,分布是:
| 资源类别 | 体积 | 占比 | 说明 |
|---|---|---|---|
| 字体 canvas-fonts | 5530719 B | 48.8% | 全在 canvas-design 一个技能里,82 个文件 |
| 脚本 scripts | 3477727 B | 30.7% | 含三份 ISO 表格 schema |
| 共享参考 shared | 1408422 B | 12.4% | claude-api 的迁移文档 |
| 其余根目录文件 | 316449 B | 2.8% | 许可证、模板 |
| markdown 参考资料 | 99 个文件 | — | references 类,真正会被读的 |
| 可执行代码脚本 | 75 个文件 | — | .py / .js / .ts / .sh |
单个最大的 markdown 是 claude-api 里的迁移文档,316954 字节,比它自己的 SKILL.md 大 3 倍多。更浪费的是重复:同一份 ISO/IEC 29500 的表格 schema(242277 字节)在 xlsx、pptx、docx 三个技能目录里各存了一份,光这三份就占了资源总量的 6.4%。
真正会烧上下文的不是这些大文件,而是“一次误读”——脚本按目录扫一遍、或者把整个参考目录读进来做检索,10.8 MB 里 48.8% 是字体,一半的量白读。我自己的教训是统计时先按后缀过滤,只认 .md 和代码文件。
只靠目录选技能,比全量正文更准
目录层够不够用来做路由?我拿 19 条自然语言任务句(每条对应一个应该被触发的技能)和 20 个技能做了个词覆盖打分的基线——注意这只是基线,不是模型行为,但它能给出“只凭目录”的下限。打分按 IDF 加权,再除以查询里所有词的权重和,另加一个按候选文本长度开方的归一化项:
deftoks(text):return{wforwinre.findall(r"[a-z0-9]+",text.lower())ifwnotinSTOPandlen(w)>1}defbuild_idf(docs):df=Counter()fordindocs:df.update(d)n=len(docs)return{w:math.log((n+1)/(c+0.5))forw,cindf.items()}defscore(query,target,idf,norm=None):qt,tt=toks(query),toks(target)raw=sum(idf.get(w,0.0)forwinqt&tt)denom=sum(idf.get(w,0.1)forwinqt)or1ifnorm=="len":# 长度归一化returnraw/denom/math.sqrt(max(len(tt),1))returnraw/denom五种条件跑同一批查询:
条件 top1 top3 目录:name+description 58% 84% 目录 + 长度归一化 58% 84% 完整正文 42% 84% 完整正文 + 长度归一化 74% 89% 完整正文只取前 1500 字符 74% 84%全量正文不归一化是五个里最差的。原因很直白:claude-api 的正文有 29180 个 token,词汇量最大,19 条查询里它抢走 8 条 top1,从“抽取 PDF 表格”到“做一份发布会 PPT”都能在它里面找到词。丢进上下文的字越多,越容易把路由器带偏——长文本天然占便宜,这是长度偏置,不是它真的更相关。按长度归一化,或者干脆截到前 1500 字符,命中率都能回到 74%,说明对路由有用的信号集中在前三分之一。
目录层 58% 这个数字不该直接当准确率看:中文任务句配英文描述、纯关键词匹配,本来就不占便宜。它的用处是提醒你 description 里的动作词要写全,写漏了连基线都过不了。我拿一条不该触发任何技能的句子探了一下阈值:目录条件下它最高只拿到 0.372 分,而 19 条正例的最高分中位数是 0.637,0.5 上下就能把“没有对应技能”的情况挡掉,比多装几个技能重要。
我怎么用它
description 是路由信号,不是简介。pdf 那一档写得很实在,437 个字符里塞进了读表、合并、拆分、旋转、水印、表单、加密、OCR 这些动作词,路由靠的就是它们。反过来 internal-comms 只有 329 个字符的描述、27 行正文,是这 20 个里最省的技能,因为它的边界清楚:写面向全公司的通知,不干别的。
正文超 500 行就拆。20 个包里 8 个正文超过 200 行,claude-api 是 595 行、29180 token,激活一次吃掉 20 万窗口的 14.6%。规范建议 SKILL.md 控制 500 行以内,办法是把过程性内容挪进 references/,主文件只留“怎么做”和“什么时候去读哪个文件”。
别把正文拼起来喂路由器。我一开始的想法是技能不多,全塞进去让模型自己挑,实测这条路线在长正文上会被带偏(42%)。要读正文做路由,至少加长度归一化,或者截断。
附件目录别整目录读。48.8% 的体积是字体,6.4% 是三份重复的表格 schema,整目录扫描是纯浪费;要读的是 99 个 markdown 和 75 个脚本。
规范是建议不是硬校验。claude-api 的 description 是 1071 字符,超了 1024 的上限,正文 595 行也超了 500 行的建议值。官方自己的包都这么破,说明这两条是拿来提醒你的,不是拿来卡人的——但得知道自己在破哪一条。
今天就能做的三件事:一是把自家技能目录的 name+description 全加起来算一次 token 总量,超过 2000 就开始砍描述;二是正文超过 500 行或 5000 token 的,把步骤性内容挪进 references/,主文件只留流程和引用清单;三是路由只喂目录,并留一个无匹配阈值,需要读正文时先做长度归一化或截断,再跟目录的结果对一次。