一个数字是怎么骗人的:盘点 234 个镜像与 7991 个 skill 之后

发布于 2026-10-04 06:20 更新于 2026-10-04 16:00 6272 字 32 min read

浅唱ヾ落雨殇 avatar

浅唱ヾ落雨殇

落赋韶华 —— 浅唱ヾ落雨殇的个人站点:AI / 理工科 / 开源折腾

给自己的机器做了一次 MCP / skills 资产盘点,想搞清楚一个问题:这机器上到底有多少能力在用? 四个小时之后我手里有四个数字。每一个看起来都像结论。 其中一个还被我当成"发现了一个真实泄露"发了出去。它不是。 这篇记录那几个数字是怎么错的,以及我为其中一个错误付了什么代价。 先说我不做什么 这次盘点有几件事是一开始就决定不做的,先划出来: 不公开那 4683 个查不到协议的 skill。...

给自己的机器做了一次 MCP / skills 资产盘点,想搞清楚一个问题:这机器上到底有多少能力在用?

四个小时之后我手里有四个数字。每一个看起来都像结论。

其中一个还被我当成”发现了一个真实泄露”发了出去。它不是。

这篇记录那几个数字是怎么错的,以及我为其中一个错误付了什么代价。


先说我不做什么

这次盘点有几件事是一开始就决定不做的,先划出来:

  • 不公开那 4683 个查不到协议的 skill。 无声明不等于可自由使用,默认仍受著作权保护[19];
  • 不把”我没查到它的许可证”记成”它没有许可证”——这两件事的差别就是能不能再分发[20];
  • 不用任何占位符去糊弄空值。 查不到就写”未声明”,不写”MIT”也不留空。

这三条划下来,后面能公开的就只有 3308 个。它们里面的绝大部分同样是厂商内容, 但至少每一条的授权状态是可查的。

起点:一句含糊的问题

问题里没说”多少”该以什么为单位。配置文件?实际能调用的?已下载待启用的?还是这个资产本身该不该存在?

我没问,直接写了脚本数。

files_scanned=927  servers=910

910。这个数字很干净,很具体,我可以拿它写标题。

它是错的。


第一个数字:分子对了,分母错了

910 个 server 定义里,自己手写的配置只有 25 个。

剩下 866 个是各种 AI 客户端安装插件时自动落盘到缓存目录的第三方模板:

归属数量
workbuddy-plugin276
workbuddy-connector260
workbuddy-marketplace243
codex-plugin44
workbuddy-ai-plugin22
其他21

WorkBuddy 三个市场合计占 90%。

25 + 866 = 891,和 910 差 19 条——那 19 条来自无法归入任何一类的路径 (散落在聊天客户端缓存、下载目录里的零散配置)。不补这个差,就又要用一个 “凑得上的数”代替真实的数。

我拿”扫到了多少条定义”当”有多少能力在用”,相差 36 倍。[9][10]

如果按 910 估算”这个环境的 MCP 暴露面有多大”,结论会离谱到没法用——而 910 本身看不出任何问题。

补记(发布后被人核对才发现):这篇文章的第一版里,上面那张表我填的是 codex-plugin 1922 / workbuddy-plugin 1416 / workbuddy-marketplace 672 / codebuddy-marketplace 372。 这四个数是 skill 的来源分布,不是 MCP 的——我从另一个清单里直接抄了过来。

方向恰好相反:那张表暗示 MCP 生态以 Codex 插件为主,真实主力是 WorkBuddy 的三个市场。

而且我在表下面加了一句”同一份配置有多个客户端副本,去重后 866”,这句话也是错的—— 那份 CSV 的 866 行已经是去重后的(server + config_path 的唯一组合数正好是 866), 不存在需要再去重的副本。

这张表是用来论证”分子对了分母错了”的。而它本身就是分子分母搞错了。

第二个数字:整个方向漏了

第一轮只扫配置文件。扫完给自己下了个结论:

Docker 那边没问题——compose 里的环境变量用的都是 ${VAR:-默认值} 形式,没有硬编码密钥。

这句话的代理指标是”我读了 compose 文件”,真正该测的是”Docker 里到底有什么”。[4]

docker images --format '{{.Repository}}' | grep -c mcp
  → 234 个 MCP 相关镜像

docker images --format '{{.Repository}}\t{{.Size}}' | grep -i mcp \
  | awk -F'\t' '{s+=$2} END {print s}'
  → 129.2 GB

docker ps -a --format '{{.Image}}' | grep -ci mcp
  → 0

215 个来自官方 mcp/ 目录,19 个来自第三方 registry,占着 129.2 GB, 没有容器在跑,没有任何配置引用它们。

差的不是精度,是整整一个维度。我以为在做全面审计,实际只做了一个方向, 然后把”我做完的那部分没问题”写成了”这件事没问题”。

第三个数字:工具报的数,得自己验

扫 skill 时 find 报了 1778 个 SKILL.md。

去修 find -maxdepth 的限制时才发现实际是 8003。

find ~ -maxdepth 7 -iname SKILL.md      →  1778
find ~           -iname SKILL.md        →  8003

同样是”我跑了个命令,报了数,我就信了”。这次不是代理指标选错, 是工具的默认行为被当成了事实——-maxdepth 静默截断,不会告诉你”还有更多”。

这类错误最难发现,因为它不产生错误输出。命令返回 0,结果是一个列表,看起来完全正常。[11][12]

写完初稿之后有人来复核,当场在同一台机器上撞见同一类错误的另一个变种:

gh api .../git/trees?recursive=1   →  { "truncated": true }

GitHub 同样静默截断文件树。第一次拿到 6280 个 SKILL.md,第二次 56301, 两次都不是全量,返回码都是 0。

这说明它不是”某个 find 用错了”,而是一整类陷阱:任何会省略掉一部分结果的工具, 默认都不会告诉你它省略了。 find -maxdepth、GitHub tree API、 甚至一个自己写的 [:40] 字符串截断,都是同一族。

顺带更正一个标题口径:标题里的 7991 是归档数量,8003 是扫描发现数量, 中间差的 12 个是噪声(散落在下载目录和聊天客户端缓存里的 SKILL.md, 其子目录恰好含有 skill)。两个数都对,但混用会让人以为文章算错了。

第四个数字:最安静的一个

有 4 个残留的 Docker 卷,名字都很像样:mcp-sqlite、mcp-task-data、claude-memory、 okta-mcp-server-keyring。

第一反应写进了给用户的回复:“顺手清掉能释放点空间。”

备份完、算完校验和之后才发现:

mcp-sqlite/db.sqlite              0 bytes
mcp-task-data/current-tasks.db    262144 bytes   (28 张表里 27 张是 0 行)
claude-memory/index.js            16673 bytes    (2025-04-30,一年多没动)
okta-mcp-server-keyring/          (空)

合计约 300 KB。

代理指标是”存在 4 个卷”,真正该测的是”这 4 个卷里有多少数据”。 前者是 4,后者接近 0。[5] 而这两个数字之间的差距,只有在真的去打开每个卷的时候才会出现。

这个形状和”PID 文件记的是包装进程”完全一致,方向还相反: PID 那条会删错东西,这条会让人以为省了 129 GB。


中场:四个形状并排

我报的我用的代理指标真正该测的差多少
910 个 MCP server扫到了 910 条定义其中多少是自己拥有的能力36×
Docker 没问题我读了 compose 文件Docker 里到底有什么129.2 GB
1778 个 skillfind 报了 1778命令有没有静默截断4.5×
删卷能释放空间存在 4 个卷这 4 个卷里有多少数据接近 0

共同形状:

用一个能拿到的表示,去代替一个同样能拿到的权威值;而”它俩差多少”这件事,一次都没量过。[4][5] 但这还不是 Goodhart[1][2][3][8]——本文的尺子从一开始就选错了,不是被当成目标之后才坏的。

注意”能拿到”这三个字。四个例子里权威值一直都可以取到: get_me 就在工具清单里没调,端口一条查询的事,镜像引用就写在启动参数里。 把这件事说成”环境不支持只好退而求其次”,是给自己一个台阶——而台阶会让人不去查。


后记:这篇文章自己被抓了三次

上面四节写完就发布了。发布之后有人没读文章、直接把引用的那份归档拉下来逐个数字重算, 找出两处硬伤:

一,第一张来源表整个数据集用错了(见第一节补记)。二,许可证表少了一整类, 五行相加对不上总数,而每一行的百分比单独看都自洽。

两处都是硬伤,两处我都当场核对确认了是自己错。

值得单独记下来的是它们为什么没被我自己发现:

  • 第一处:那张表的数字格式规整、量级合理、和上下文”看起来吻合”。没有任何局部特征会报警。
  • 第二处:每一行都自洽,只有求和才会暴露。而我在写的时候根本没有求和——我写的是 四个我记得的类别,不是从一份完整分类表里抄的。

所以这不属于”我会犯的错”,而属于**“这类错我的自查方式抓不到”**。[13][9] 自查是对着目标值逐项核,而这两种错的症状是”每项都像对的”。

对这两处,有效的做法是同一条:让总数参与判定。 表必须能加回去,命令必须能在另一套口径下重跑, 数字必须能被一个没读过上下文的人独立复核。

第三次是在修订这一版时被抓到的,对象换成了参考文献。补引用时,检索方交回 24 条, 格式规整、出处齐全——其中两条经独立核实是编造的:一条把 arXiv 编号指错了地方 (https://arxiv.org/abs/2009.03529 实际是 Haffert 等人的天体物理仪器论文,不是软件度量论文), 一条把官方规则的编号和原文都记错了(写成 ASD-STE100 的 Rule 9.4,实际是 Rule 5.2)。

为什么这又是同一个形状:引用是”一个名字指向一个来源”。一条编造的引用,就是一个 指向空处的名字——它格式正确、序号连续、在正文里读起来毫无破绽,和”910 个 server” 是同一种东西。

怎么发现的:不是靠读,是靠独立取一次原始出处——打开那个 arXiv 编号、翻官方规则原文。 这和”如果重来”里的第二条是同一个动作。


中场之二:我把自己当成了泄露发现者

同一个形状,在我自己身上复发了一次,而且这次有代价。

排查脱敏扫描器的告警时,其中一条是:一条 issue 正文里的 Authorization 头长度 72, 头两个字符是 Be。我把它当成了真实泄露,并且在对话里断言了两次:

“确认是真漏洞""多抓出 982 处以前漏掉的带前缀凭据”

然后去核对源文件。原文是:

export TAVILY_API_KEY="tvly-YOUR_API_KEY_HERE"

是占位符。 而且脱敏日志里那条记录压根不存在——它从头到尾没被我的规则匹配过。

两个错误叠在一起:

  1. 我从掩码后的长度和前缀推出”这是真的”,没有回去读原文;
  2. 真正的缺陷确实存在——\bapi_key\b 匹配不到 TAVILY_API_KEY 里的 API_KEY, 因为下划线是单词字符,所有带前缀的环境变量名都漏网了。

但这两件事没有关系。缺陷是真的,推断是假的,而我把推断当成了结论发出去。

要命的地方在于这条已经进了公开 commit,只能 amend 后 --force-with-lease 重推, 并把措辞改掉。代价不是技术上的,是”我说我发现了泄露,结果没有”这件事本身。

真正的教训不是”应该更谨慎”,而是:

代理指标不仅会出现在采集环节。它会出现在”我据此下结论”的那一步。 一个看起来像密钥的字符串,和一个真的是密钥的字符串,在掩码之后没有区别。[6][7]

中场之三:工具层的同一个错相

脱敏本身也反复栽在同一个地方。

第一次:66 处”密钥命中”,只有 1 处是真的。

原始:headers.Authorization = "Bearer ${VAR}"     ← 引用环境变量

正则看到 Bearer 加一串字符就报。逐条核实后 66 减到 1。

第二次:纠偏过头,第一版规则报了 3585 处。

process.env.API_KEY
this.value
expires_in

全是代码标识符,不是凭据。全部替换成 ${SECRET} 之后 skill 直接不能用了—— 归档的目的就是”后面还能直接用”,过度脱敏把这个目的消灭了。

收紧判定(要求值同时含字母和数字、且不是点号分隔的标识符链)之后落到 325 处真实凭据, 另有 79 处机器身份路径,合计 404 处。

第三次:校验器自己在喊狼来了。

泄漏校验脚本报了十几处”残留 token”,逐条看:

PublicKeyToken=b03f5f7f11d50a3a     ← .NET 程序集强名,微软的公钥标记
Token=test-token-12345               ← 示例代码里的测试固件

一个只会喊”有威胁”的检测器,比没有检测器更糟,因为你会开始忽略它。 最后给校验器加了一份带理由的白名单:每一条都写清楚为什么它不可能是凭据。

中场之四:14 处路径穿越里,13 处是噪音

安全工具报了 14 条 high,我逐条看了。

13 条是:

with open(args.output, "w", encoding="utf-8") as f:

args.output 是操作者自己在命令行传的参数。写到用户指定的位置是 CLI 的正常行为,不是漏洞。

真的那一条在这里:

safe_name = panel_info.replace("/", "_").replace(":", "_").replace(" ", "_")[:40]
filename = f"panel_{pi}_{safe_name}.json"
filepath = os.path.join(output_dir, filename)

panel_info 来自腾讯云 API 返回的面板名——是远程数据。过滤器处理了 /、: 和空格, 漏了 \。Windows 上反斜杠是路径分隔符,一个名为 ..\..\..\x 的面板就能写出 output_dir 之外。

向量窄(需要能命名面板的权限),但确实存在,而它就藏在那 13 条噪音中间。


再往后:查不到的东西也是数据

最后一块是许可证。把这 7991 个 skill 的授权状态逐条查清、列成一份完整清单——这正是软件物料清单(SBOM)关注的”里面有什么”[23]。7991 个归档的 skill 里:

状态数量占比
MIT228328.6%
Apache-2.04695.9%
上游明确标注 UNKNOWN3844.8%
LICENSE 正文无法归类1071.3%
明确专有560.7%
AGPL60.1%
CC30.0%
本地零证据468358.6%

有声明合计 3308,加零证据 4683,正好 7991。

这一张表的第一版只列了 MIT、Apache-2.0、专有、AGPL 四类,五类相加对不上总数—— 漏掉的正好是”上游明确写了 UNKNOWN”和”正文无法归类”这两类共 494 条。 而且每一行的百分比按 7991 反算都自洽,所以肉眼看每一行都没错,只有加起来才暴露。 这也是为什么它当时没被发现:错的不是某一格,是少了一行。

那个 4683 不是采集失败,是上游本身没有声明。最大的一个来源目录 (786 个连接器)里连 plugin.json 都没有,只有裸目录。24 个上游仓库里也有 6 个在 GitHub 上 没有 license 字段。

当时差一点就把这 4683 个算成”没问题的第三方内容”放过去了。 代理指标是”我没查到它的许可证”,真正该问的是”我有没有权力再分发它”。[21][22]


如果重来,我会先做哪一步

按今天撞出来的次数排序:

一,先划不做什么。 成本几乎为零,但它决定后面每一步的判断标准。 “不公开无声明内容”这一条如果在开头没写,后面就会在某个”反正大家都这么做”的时刻被绕过。

二,任何数字在写下来之前,先去看它到底在数什么。 不是复核,是去看。 910 要看归属、1778 要看命令的默认行为、4 个卷要打开、66 要读原文。

三,把”我据此下的结论”也当成采集环节来审。 这是这次最贵的一条—— 我漏掉的那次,不是采集错了,是从掩码长度推出结论那一步错了,而采集本身没错。

四,让总数参与判定。 一张表必须能加回去,一个数字必须能在另一套口径下重跑。[10] 自查是对着目标值逐项核,但本文第一节和倒数第二节这两种错,逐项核是抓不到的—— 它们的症状就是”每项都像对的”。

五,检测器会喊狼来了,要给它一份带理由的白名单。 一份白名单同时是文档: 它记录了”我们知道这些形状存在,它们不是威胁”,比多一份干净的报告有用。


语言这一层:一个名字承担了它不配承担的含义

上面四个错误,共同形状是”一个便宜、相关的指标,替代了真正想测的量”——而那个被替代的权威值,和这个指标一样是”能拿到”的[4][5]。但如果从语言的角度再看一遍,它们还有一层更统一的解释:

每一次,都是让一个名字,承担了它不配承担的含义。

  • “910 个 MCP server” 里,“server” 这个词同时指向了”自己手写的配置”和”厂商自动落盘的模板”;
  • “1778 个 skill” 里,那个数字指向的是”命令的输出”,而不是”磁盘上的事实”;
  • “4 个卷” 里,“卷” 这个词暗示了”里面有数据”。

这几个名字单独看都没错,错的是它们被拉去覆盖了一个自己并不精确对应的东西。数字只是名字的量化形式,名字含糊,数字就一定含糊。

航空业在四十年前就撞上了同一件事,只是后果严重得多——维修手册里一个词有歧义,会出人命。于是他们没有选择”更小心地读手册”,而是主动给语言加约束:ASD-STE100,53 条写作规则分 9 节,词典规模按公开资料整理约为 900 个批准词的量级、约 1200 个需避免的词,一个概念只能有一个名字,一句话只能有一个操作[14][16]。

它最核心的一条是 Rule 1.11:

“Do not use different technical nouns for the same item.”[15]

而本文需要的,是它的镜像面:

不要让同一个名字,指向不同的东西。

这一条其实比原文更难。Rule 1.11 管的是”同物异名”,而这次四个错误里出问题的,全都是”同名异物”——“server""skill""卷”这些词,各自被拉去承担了本不属于它们的含义。ISO 704 把这件事说成一条更朴素的原则:一个概念对应一个指称[17]。受控语言这一整类实践,本质都是在替这一步做减法[18]。

后来 Karpathy 把这套四十年前的航空维修手册英语规范直接丢给 LLM 用。他的说法很轻:让模型按这套规范改写输出,会更清楚;他甚至只要求做到大约 80% 的 ASD-STE100[24]。再往后他更进一步——能画图就别写字,能生成 HTML 就别给静态文本,他把这些叫 “large, custom, discardable software artifacts”(大型、定制、用完就可以丢掉的软件产物)。他判断,随着 LLM 变强,人的工作会向上移动到监督、检查和理解[24]。

“人的工作向上移动”这句,本文其实已经用自己的代价演示过一遍了。这次最贵的一次错误不是采集错了,是从掩码长度推出结论那一步——采集没错,推断错了。而”如果重来”那一节的第三条正是同一件事:把”我据此下的结论”也当成采集环节来审。两句话说的是同一件事,只是那句在说”往上移一层”,这句在说”多审一层”。

还有一层也已经在本文里出现过。“discardable artifacts” 就是那些只为一个具体问题临时造的脚本——为了验证归档里没有残留凭据,我从源码里 harvest 出真实的凭据字面量,再断言这些具体的值没有出现在归档副本里。那个脚本不值得维护,也不该进任何长期资产清单。但它值得生成——因为贵的是”不这么做”。


尾声

这四个错误在当天看起来都很严谨。每一个背后都真跑了一个脚本、真读了一个文件、 真拿到了一个可以写进文章的数字。

所以普通的”检查一下”抓不住它们——因为检查本身用的就是同一个代理指标。

能让它们现形的只有一件事:

在报出结论之前,去看它到底在数什么。

这句话现在可以再往语言那一层推一格:不只是去看它在数什么,也是去看那个用来指它的名字,究竟指向了什么。名字不精确,数得再准也没用。

具体到这次:去看那 910 个定义里有多少是自己的;去看 docker images; 去看 find 会不会截断;去打开那 4 个卷;去读那条被掩码的原文到底是什么。

每一步都很便宜。贵的是”不这么做”——尤其是,当不这么做还能让你当场 宣布一个”发现”的时候。


附:口径与复核

本文所有数字的测量口径都写在正文里——扫了什么、怎么去重、按什么分类、 哪一步是人工判定、哪一步没有覆盖到(第三节末尾标注了 8003 与 7991 的口径差, 许可证一节给出了能加回去的合计行)。按这些口径,任何人都可以直接复算,不需要额外权限。

原始材料——脱敏日志、溯源清单,以及每一处被替换掉的凭据记录(只存长度和前缀指纹, 不存原文)——在 mcp-configs 仓库。

它保持私有,是刻意的选择,不是疏漏。开篇”先说我不做什么”里那条——不公开无声明内容、 无声明不等于可自由使用——落到这个仓库上,结论就是它不能公开:里面装的就是那些我 没有权限再分发的东西。公开这个仓库本身,就构成一次再分发。它是本文结论的兑现, 不是对结论的背叛。

代价要如实说:读者因此无法直接点进去复核原始材料。这是为了不违反授权而付出的代价, 我不打算把它藏起来。能补上的那半,是把口径全部写进正文——即使没有仓库访问权, 一个没读过上下文的人也能照着口径,在另一台机器上把每个数字重跑一遍,并当场质疑它。

这里还藏着本文那个形状的又一次复发:当初写下”点进去就能复核”的时候, 我把**“公开”当成了”可分发”**——而这两个词不是一回事。这又是一次”一个名字承担了 它不配承担的含义”:公开只说明内容可见,不说明我有权把它交出去。

校验脚本不做模式匹配就下结论——它从源码里 harvest 出真实的凭据字面量, 再断言这些具体的值没有出现在归档副本里。模式匹配只能证明”形状”没了, 证明不了”这些值”没了。


参考文献

  1. Goodhart, C. (1975). Problems of Monetary Management: The U.K. Experience. Papers in Monetary Economics, Reserve Bank of Australia.
  2. Strathern, M. (1997). ‘Improving ratings’: audit in the British University system. European Review, 5(3), 305-321.
  3. Campbell, D. T. (1979). Assessing the impact of planned social change. Evaluation and Program Planning, 2(1), 67-90.
  4. Yankelovich, D. (1971). Interpreting Marketing Research. Sales Management.
  5. Korzybski, A. (1933). Science and Sanity: An Introduction to Non-Aristotelian Systems and General Semantics.
  6. Amodei, D., Olah, C., Steinhardt, J., et al. (2016). Concrete Problems in AI Safety. https://arxiv.org/abs/1606.06565.
  7. Krakovna, V. (2020). Specification gaming: the flip side of AI ingenuity. DeepMind blog.
  8. Manheim, D. & Garrabrant, S. (2018). Categorizing Variants of Goodhart’s Law. https://arxiv.org/abs/1803.04585.
  9. Inozemtseva, L. & Holmes, R. (2014). Coverage Is Not Strongly Correlated with Test Suite Effectiveness. ICSE 2014.
  10. Forsgren, N., et al. (2021). The SPACE of Developer Productivity. ACM Queue, 19(1).
  11. Shore, J. (2004). Fail Fast. IEEE Software, 21(5), 21-25.
  12. Leveson, N. G. & Turner, C. S. (1993). An Investigation of the Therac-25 Accidents. IEEE Computer, 26(7), 18-41.
  13. Dijkstra, E. W. (1972). The Humble Programmer. Communications of the ACM, 15(10), 859-866. (EWD340)
  14. ASD Simplified Technical English Maintenance Group. (2025). ASD-STE100 Simplified Technical English, Issue 9. https://www.asd-ste100.org/assets/files/ASD-STE100_ISSUE9.pdf
  15. ASD-STE100, Rule 1.11: “Do not use different technical nouns for the same item.” https://www.asd-ste100.org/
  16. ASD-STE100, Rule 5.2: “Write only one instruction in each sentence, unless two or more actions occur at the same time.” https://www.asd-ste100.org/
  17. ISO 704 (2022) Terminology work — Principles and methods.
  18. Kuhn, T. (2014). A Survey and Classification of Controlled Natural Languages. Computational Linguistics, 40(1), 121-170.
  19. Berne Convention for the Protection of Literary and Artistic Works (1886, 最新修订 1979).
  20. Choose a License. No License. https://choosealicense.com/no-permission/
  21. Open Source Initiative. The Open Source Definition. https://opensource.org/osd
  22. ISO/IEC 5962 (2021) —— SPDX 规范.
  23. NTIA (2021). The Minimum Elements For a Software Bill of Materials (SBOM).
  24. Karpathy, A. (2025). On applying ASD-STE100 to LLM output. X (Twitter). https://x.com/karpathy/status/2105819303471976479
© 2025 - 2026 浅唱ヾ落雨殇
Powered by theme astro-koharu · Inspired by Shoka