上下文省了 857 倍,路由却瞎掉 72%:50 个 Agent Skill 渐进式加载的实测复盘

📅 2026/8/2 3:47:06 👁️ 阅读次数 📝 编程学习
上下文省了 857 倍,路由却瞎掉 72%:50 个 Agent Skill 渐进式加载的实测复盘

背景:技能越装越多,账单先撑不住

给 Agent 装技能这件事,现在几乎没人再讨论"要不要渐进式加载"了——答案显然是要。但很少有人真的去称一称:省下来的到底是多少,以及省完之后,还剩多少信息够 Agent 做出正确的路由判断。

我把本机装着的 50 个 Skill 全量扫了一遍,用cl100k_base逐字节数 token。结论有两半,一半符合预期,一半不符合:全量塞进上下文要6,768,295 token,只常驻描述层要7,890 token,压缩比857.8 倍;但代价是,Agent 判断"该不该用这个技能"的全部依据,被压进了平均 158 token 的一行字里——而这一行还有72% 的技能会被截断

Skill / MCP / Agent Tool 这类扩展机制的共同形态是:一个目录,一份说明书,外加若干脚本和参考资料。装十个还好,装到五十个,问题就变成了一道很朴素的算术题——这些说明书要不要全部塞进模型的上下文?

塞,那每一轮请求都要为五十份说明书付一次 token。不塞,Agent 又怎么知道自己有这个能力?

行业给出的答案叫渐进式加载(progressive disclosure):启动时只注入技能的名字和一句描述,命中了再读正文,正文里再指路去读参考资料。听起来很合理。我想知道的是这套设计在真实语料上的量化收益,以及它把风险转移到了哪里。

方法:把"一个技能有多贵"拆成三层来称

我按加载时机把每个技能的内容切成三层:

内容加载时机
L1 描述层frontmatter 里的name+description会话启动,全量常驻,每轮重发
L2 正文层SKILL.md去掉 frontmatter 的正文路由命中后,一次性读入
L3 引用层references/ templates/ scripts/ assets/下的文本文件正文里再指路才读

称重脚本大约 200 行,核心就是分层遍历加 tokenize:

import re, tiktoken from pathlib import Path ENC = tiktoken.get_encoding("cl100k_base") FM_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n?", re.S) BUNDLE_DIRS = ("references", "templates", "scripts", "assets", "examples") def ntok(s: str) -> int: return len(ENC.encode(s, disallowed_special=())) def scan_skill(skill_md: Path) -> dict: raw = skill_md.read_text(encoding="utf-8", errors="replace") m = FM_RE.match(raw) fm, body = (m.group(1), raw[m.end():]) if m else ("", raw) name, desc = scalar_field(fm, "name"), scalar_field(fm, "description") l1 = ntok(f"- {name}: {desc}") # 真正常驻的只有这一行 l2 = ntok(body) # SKILL.md 正文 l3 = sum( # 技能自带的参考资料 ntok(p.read_text(encoding="utf-8", errors="replace")) for sub in BUNDLE_DIRS for p in (skill_md.parent / sub).rglob("*") if p.is_file() and p.suffix.lower() in TEXT_EXT ) return {"name": name, "desc_chars": len(desc), "l1_tokens": l1, "l2_tokens": l2, "l3_tokens": l3}

有两个细节值得说明。一是 L1 我按- name: description的清单行格式计算,而不是只数描述本身,因为实际注入系统提示时这些结构字符也要付费。二是 tokenizer 固定用cl100k_base,不同模型的分词器会有几个百分点差异,但不影响量级结论。

扫描范围是三个真实目录:用户级技能、连接器技能、内置技能,共 50 个SKILL.md、486 个引用层文件。

python measure_skill_context.py \ --roots ~/.workbuddy/skills \ ~/.workbuddy/connectors/skills \ <app>/resources/builtin-skills \ --desc-limit 150 --turns 20 \ --out data/skill-context-budget.json

图1:三层加载模型的实测体量。常驻的 L1 只占三层合计的 0.12%,但它承担了 100% 的路由决策。

实证一:857 倍压缩比,和一条被忽略的幂律

第一批数字如下:

token 合计每技能均值占比
L1 描述层7,890157.80.12%
L2 正文层174,6233,492.52.58%
L3 引用层6,585,782中位 6,11197.30%
三层合计6,768,295——100%

857.8 倍的压缩比在意料之中。真正值得停一下的是 L3 的分布形态:均值 131,716,中位数只有 6,111,差了 21 倍

排序之后原因很清楚:两个金融数据类技能(westock-data3,058,438 token、westock-tool2,225,202 token)合计吃掉了引用层的80.2%,Top5 占89.5%。剩下 45 个技能挤在长尾里,中位数只有六千出头。

这条幂律对工程决策的意义是:引用层的容量风险不是均摊的,而是集中在少数几个"数据型技能"上。如果你的 Agent 一次性读入了westock-data的某个参考文件,单次读取就可能顶掉半个上下文窗口。给引用层做分层加载和单文件体积上限,收益远大于给那 45 个长尾技能做优化。

图2:引用层呈明显幂律分布,Top2 技能占 80.2%。均值被两个数据型技能拉高到中位数的 21 倍。

实证二:唯一常驻的路由信号,72% 撞上了 150 字符的墙

省下 99.88% 的上下文之后,Agent 判断"这个任务该不该用某技能"的全部依据,就只剩 L1 那一行。所以下一个问题是:这一行够用吗?

先要确定它有多长。我没有直接采信文档,而是拿技能清单里实际渲染出来的、末尾带省略号的四条描述反推:

技能清单里的描述字符数
csdn-auto-flow148
nano-tools-matrix-audit149
lark-apps149
tencent-docs-routing149

四个独立样本全部落在 148–149,加上省略号正好 150。截断阈值是 150 字符,这是一个硬上限,不是建议值。

然后拿这个实测阈值去量 50 个技能的描述长度:

  • 最短 18 字符,中位232 字符,最长915 字符
  • 36 / 50 = 72%的技能描述超过 150 字符,会被截断;
  • 累计被截掉7,791 个字符

被截掉的是什么内容?我用一组触发语模式(当用户时使用适用于Use when遇到场景等)扫了一遍:有 6 个技能,它的"什么时候该用我"整句只出现在 150 字符之后——也就是说,在 Agent 实际看到的清单里,这句话根本不存在。

这 6 个技能是csdn-auto-flownano-tools-matrix-auditlark-appslark-driveneodata-financial-searchwestock-data。它们的描述前半段都在详细罗列"我能做什么",把"什么时候用我"留到了最后——而恰恰是后者才是路由需要的信号。

这就是渐进式加载真正的代价所在:它没有消灭成本,它把成本从算力转移到了描述的信息密度上。而 72% 的超限率说明,绝大多数技能作者是按"写文档"的习惯在写 description,不是按"写路由索引"的习惯。

图3:描述层长度分布对照 150 字符实测截断线。中位数 232 字符已越线,6 个技能的触发条件整句落在线外。

复算:盈亏平衡点根本够不着

有人会问:渐进式加载多了一次"读正文"的往返,会不会得不偿失?把常驻层每轮重发这件事算进去就清楚了。

设一次会话 20 轮。对照组是"半急切式"——把所有SKILL.md正文也塞进系统提示(不含引用层,这是不少项目的实际做法):

方案单轮常驻20 轮累计
渐进式(只驻 L1)7,890157,800
半急切(L1 + L2 常驻)182,5133,650,260
差额——3,492,460

差额 349 万 token。按 ¥2 / 百万输入 token 这个假设单价折算约 ¥6.98 —— 单价只是为了给量级一个直观参照,不代表任何厂商实际报价。

关键在盈亏平衡点:渐进式要为命中的每个技能额外付一次 L2(平均 3,492.5 token,且只付一次,不随轮次重发)。要追平半急切式,需要3,492,460 / 3,492.5 ≈ 1,000个技能正文被加载。语料里总共才 50 个技能。

结论很干脆:在 20 轮量级的会话里,渐进式加载不存在被反超的可能。争论点从来不在"要不要渐进式",而在"描述层要怎么写"。

局限:这次测量没能证明什么

得把边界说清楚,否则上面的数字容易被过度引用。

  1. 没有测路由准确率。我测的是"触发条件被截断了多少",这是路由失败的必要条件,不是充分条件。模型完全可能靠前 150 字符里的功能描述猜对。要证明因果,得跑一组带标注意图的 A/B 评测。
  2. 只有一台机器的一份语料。50 个技能里有 27 个来自同一个连接器家族(lark-*),写作风格高度同质,描述偏长可能是这批技能的团队习惯,不能推广成行业普遍现象。
  3. 150 字符是黑盒反推。四个样本一致落在 148–149,但这是从渲染结果反推的经验值。不同宿主、不同版本的实现阈值可能不同,换环境需要重测。
  4. tokenizer 不等价。cl100k_base对中文的切分与实际使用的模型可能有几个百分点偏差,量级结论不受影响,精确账单会有出入。
  5. L3 只统计了文本文件。二进制资源、图片没有计入,实际的引用层落盘体积比 token 数反映的更大。

结论与下一步

一句话方法论:渐进式加载省的是算力,花的是描述的信息密度;省下 857 倍之后,唯一该被反复打磨的就是那 150 个字符。

落到可执行的三条:

  • 把 description 当路由索引写,不当摘要写。触发条件("当用户…时使用"、"遇到 X 格式文件时")放前 80 字符,能力清单放后面——反正后面大概率会被切掉。
  • 给引用层设单文件体积上限。幂律分布意味着风险集中在少数几个数据型技能上,对这几个做拆分和索引化,比优化长尾 45 个更划算。
  • 把描述长度检查加进 CI。一行断言就够了:assert len(description) <= 150。这次 72% 的超限率,本质上是没人在提交时量过。

称重脚本已经放进下面这套单文件工具矩阵里,可以直接对自己的技能目录跑一遍。

开源地址

  • 矩阵门户:GitHub - wangzifan396-wzf/WB: 一个标签页,收纳你全部的开发者工具。24 个单文件、零依赖、本地优先的开源工具,下载即用。 · GitHub
  • 单文件工具聚合器:GitHub - wangzifan396-wzf/nano-workbench: Single-file tabbed launcher for the nano-tools matrix - one tab, all 28 tools, instant switch. Zero-dep. Part of nano-tools. · GitHub
  • GitHub 组织主页:wangzifan396-wzf (WangZi) · GitHub