返回 2026-05-10
🛠 工具 / 开源

开源的误测The Mismeasure of Open Source

nesbitt.io·2026-05-09

文章探讨了开源项目健康度评估中的“路灯效应”(streetlight effect),即人们倾向于在容易观察的地方寻找数据,而忽略了真正重要的指标。它批评了当前许多基于提交频率、星标数量等表面数据的评分方法,这些方法无法准确反映项目的实际活力、可持续性或对社区的贡献。

Andrew Nesbitt

任何试图对开源项目进行关键性、风险或资金需求评分的尝试,最终都基于大致相同的十几个信号,因为这些信号是你在一天之内可以从注册中心 API 和 GitHub REST 端点获取到的。本周早些时候我写过关于 2015 年 CII 普查的内容,其公式给 xz-utils 打了 6 分(满分 13 分),导致它排在第 254 位,但总体而言,它的判断比人们通常认为的要准确得多。

十年过去了,如今已有多个由基金会、学术界和资助方推动的后续项目,我也为其中大部分贡献了数据。尽管拥有的数据和参与人员都大幅增加,但这些模型仍主要依赖相同的输入,因此继承了大部分原有的盲点,并新增了一些。我想把这些盲点集中整理出来,而不针对某个特定模型进行批评。

缺失即视为零

最严重的错误之一是将信号的缺失当作该信号的低值处理。Go 模块通过代理从版本控制系统中获取,而该代理并不发布每个模块的下载量统计。C 语言库则通过 apt、dnf、apk、vendoring 或静态链接等方式分发,这些方式均不向任何人报告数据。如果一个模型的筛选条件是“按下载量排名前 N”,或其重要性权重基于下载量,那么这两个生态系统在很大程度上就会被排除在评估之外,且输出结果中不会显示它们曾被纳入考虑。

我曾提到 Daniel Stenberg 发现 curl 的年下载量被记录为一万次,这实际上只是被监控渠道的准确计数,与 curl 实际安装量达二十亿次的事实毫无关系。

零个 GitHub issue 可能意味着没有用户使用该项目,也可能意味着问题跟踪在 Bugzilla、邮件列表、Launchpad 或 Debian BTS 中进行。缺少 FUNDING.yml 文件既可能表示无人付费,也可能表示有三位全职维护者受雇于 Red Hat,而缺少 OpenSSF Scorecard 的结果则无法说明项目的安全性,因为如果项目托管在 cgit 上,Scorecard 根本无法运行。

我所见过的几乎所有模型都是通过将单元格设为零,或静默删除整行来处理这些情况,而不是标记为“未知”并传播不确定性。一旦仪表盘渲染完成,空值和零值看起来完全相同,未能符合模式的项目与真正没有任何内容的项目也无法区分。

更早期的、更为严苛的同类错误体现在准入过滤器上——它决定了哪些项目会被纳入评分。虽然人们常讨论的是关键性模型中的评分公式,但实际上,候选集的选择往往已经做出了更重要的决策。2015 年的普查设定了 popcon 阈值来筛选 Debian 软件包,因此 sudo 和 polkit 并非被错误排名,而是根本未被纳入输入。

现代等效模型通常以“注册表下载量前 N%”或“具有可解析 GitHub URL 的软件包”作为起点,所有被这些过滤器排除的项目都会被静默剔除。输出结果中没有一行标明“未考虑”,读者也无法区分“被评为低风险”和“从未进入评估范围”。

易于收集,所以它一定有意义

可用性常被误认为相关性:人们使用某个指标,仅仅是因为 API 能提供它,而理由往往是事后才想出来的。这就像把 GitHub 和注册表 API 当作路灯,而大部分实际风险却藏在黑暗之中。下载量就是最典型的例子。npm 或 PyPI 提供的数字主要由 CI 运行器每次推送时重新安装所有依赖、镜像流量、机器人扫描以及偶尔的人类用户构成,它既不是用户数量,也不是安装次数,更无法清晰映射到“如果出问题会影响多少人”。该指标在分布的中段与这些因素有一定相关性,但在边缘区域完全失效,而这恰恰是我们最需要关注的地方。

GitHub 星标反映的是“拥有 GitHub 账号并点击按钮的人”与项目在该群体中的可见度之间的交集,因此明显偏向 Web 前端和开发者工具领域。ICU 被集成到每个浏览器、Android、JDK 和 Node 中,却只有约 3,500 颗星;c-ares 为 curl、Node 和 gRPC 提供异步 DNS 功能,也有约 2,100 颗星;libxml2 的 GitHub 镜像更是仅有 735 颗星。此外,星标还可以直接购买。He 等人发现,在 2019 至 2024 年间,GitHub 上存在约六百万个疑似虚假星标,且近一年增速迅猛,因此任何基于星标的模型实际上也在部分衡量花钱买推广的人。

CVE 数量常被用作安全信号,但它衡量的恰恰是人们所假设的反面。OpenSSL 和 Linux 内核有数百个 CVE,是因为安全研究人员持续关注它们;而一个自 2014 年以来无人检查过的未模糊测试的 C 解析器则可能零 CVE,反而更危险。若将其视为风险输入,该指标反映的是审计关注度而非实际漏洞数量,从而奖励那些根本没人愿意去审查的项目。

此外,它仅统计那些走完 CVE 流程的已修复漏洞。许多维护者会自行发现并修复安全问题,并在常规发布中直接部署补丁,而不是花一周时间与 CNA 争论严重性评分——但这些历史记录在数据列中完全看不到。

代码复杂度指标常被当作项目难以替换程度的代理变量,但圈复杂度和 Halstead 分数只计算分支和操作符,这是静态分析器能看到的,与人类感知的难度仅有松散关联。Vlad-Stefan Harbuz 向我指出他为 Hare 标准库编写的正则表达式引擎:任何此类工具都会将其视为对数组的普通循环遍历。然而这些循环实际上是在执行一个用于模式匹配的虚拟机,所有难点都隐藏在它们编码的自动机逻辑中,而没有任何度量指标能捕捉到这一点。

提交频率和“最后活动时间”会惩罚已经完成维护的软件。TeX 是经典案例,bzip2、libogg 以及大量实现冻结规范的格式解析和加密代码也刻意保持低活跃度,因为规范稳定,改动本身即构成风险。相反,Dependabot 和 Renovate 等工具可以轻松反向操纵该指标,在被人类停止查看通知多年后仍让仓库的活动图保持绿色。至少日志中还能识别出机器人作者。

Claude Code 等类似代码代理现在支持计划任务和重复执行任务,因此一个仓库可以持续生成大量看似合理的维护提交记录,这些提交都署名于某位人类开发者名下,但实际上全程无人参与。提交频率已完全无法区分哪些是人工维护、哪些是自动化操作。我一直在尝试统计这种现象在依赖最广泛的包中出现的频率,而仅由机器人产生的类别已经足够庞大,以至于任何忽略作者身份的活跃度指标实际上都在衡量机器人的行为。

一个数字,多种单位

跨生态系统比较这些指标的数值毫无意义,因为即使列标题相同,其计量单位也不同。npm 的“下载量”主要是 CI 缓存未命中的计数;Homebrew 分析数据则来自 macOS 开发者笔记本发出的 ping 请求;Debian 的 popcon 安装数则是来自一个非代表性群体(且规模不断缩小)的自愿上报数据——该群体既不能代表服务器环境,也不能代表容器化部署。将这些不同本质的数据加总或拟合转换系数,只是在给一个根本不存在的量赋予精确的数字外观。

依赖数量也面临同样的问题:npm 文化推崇细粒度、单一职责的小包,一个字符串填充工具可能拥有数万声明依赖者。而像 C 语言压缩库这样被浏览器、数据库引擎乃至全球大多数游戏静态链接的基础库,可能仅有几十个依赖者。这是因为 C 语言的依赖关系通过 #include、内嵌源码文件、git submodule 或 CMake 脚本中的某行配置表达,这些方式都无法生成注册表爬虫能追踪的清单边。因此,按依赖数量对这两个项目进行排序,反映的是各自生态系统的打包约定,而非哪个项目更重要。

即使在那些确实产生清单边的场景中,包的粒度差异也会破坏可比性。一个 Rust workspace 可能从一个仓库和团队发布四十个 crate,而同等规模的 Python 项目只会发布一个包。这使得 Rust 项目在图谱中显示为四十倍节点,内部边会拉高其 PageRank 值,同时同一批维护者在 bus-factor 计算中被重复计数四十次。

GitHub 作为可见宇宙

大多数模型都依赖 GitHub API 获取除注册信息外的所有数据:贡献者、issue、star、安全策略、赞助者、Scorecard。按包数量算,大多数开源项目都在 GitHub 上,因此这是一个合理的起点。但那些托管在其他平台的项目,恰恰是模型旨在发现的重要底层基础设施——它们往往历史悠久且处于核心地位。

PostgreSQL、SQLite、GnuPG、glibc、FFmpeg 以及大多数 GNU 项目的主要开发活动发生在邮件列表、自托管 cgit、Gerrit 或 Savannah 上。有些项目虽有只读的 GitHub 镜像,但这本身就是一个陷阱。镜像有 star 数和贡献者图谱,API 会 happily 返回相关数字,但提交到那里的 pull request 毫无作用,贡献者图谱反映的也只是同步推送者,而非实际编写代码的人。API 响应中没有任何字段能区分镜像与主仓库,于是镜像被当作主项目来评分。

GitHub 的 /contributors 端点仅统计能关联到 GitHub 账户的提交作者。curl 自身的 THANKS 文件列出了超过 3,600 名贡献者,但 API 只返回几百人,因为 curl 历史中大多数补丁是由 GitHub 从未见过的邮箱地址发送的。基于相同数据构建的“人员流失率”公式因此将 curl 报告为 1,因为 Daniel Stenberg 撰写了半数以上的提交,而该公式无法区分一位高产创始人与数十位活跃协作者共同工作,还是独自维护且无人协助的情况。

同一端点在相反方向上也造成误导,因为它返回的是终身累计值——例如一个项目在 2012 年有八十位贡献者,如今只剩一人仍在维护,API 却显示一个令人安心的总人数,且 API 或注册元数据中没有任何字段表明此人是否即将退出。

OpenSSF Scorecard 常被用作安全评分,尽管其多项检查(Branch-Protection、Token-Permissions、Dependency-Update-Tool、CI-Tests)检测的是 GitHub 功能而非实际安全属性。一个使用自建 Buildbot CI、邮件列表补丁审查并拥有二十年严谨安全流程的项目,得分反而低于启用默认 Actions 工作流的周末模板仓库。另一方面,某个项目若每项都勾选,得分接近十,下游系统便将其解读为“安全”,仿佛清单覆盖了全部安全范畴,实则仅反映仓库扫描可触及的那一部分。一旦此类分数成为资助决策的依据,Goodhart 定律便开始生效:项目开始只为提升数值而启用复选框,而非完成这些复选框原本旨在代表的实际工作。

这是哪个项目?

身份识别远比表面复杂,几乎所有模型至少在一个方向上都出错,首要问题在于同一段代码在不同平台以不同名称出现:libcurl 在 Homebrew 上叫 curl,Debian 上是 libcurl4,PyPI 上是 pycurl,crates.io 上是 curl-sys,GitHub 上则是 curl/curl。未能统一这些映射的模型会为同一风险面生成五个低分条目,导致任何由输出引导的资金或关注被分散五次,或指向封装层而非被封装的核心组件。

反之亦然:LLVM、GCC、coreutils、util-linux 和 BusyBox 等每个项目都从单一仓库发布数十个命名不同的工件。假设“一个包对应一个仓库”的模型要么只选取其中一个工件而忽略其余,要么将同一维护团队重复计数数十次。我曾考察的多数模型干脆排除这些项目,因为对如此庞大的仓库计算复杂度指标会超时——这意味着关键性评分恰好在这些最关键的项目上留下漏洞。

分叉进一步加剧混乱:当软件包的登记库中列出的仓库 URL 指向一个 fork,或原项目已归档而开发转移至一个登记信息未更新的新 fork 时,所有基于仓库的指标都在描述错误的树结构。

看不见的资金

项目健康状况和资金模型通常会寻找 GitHub Sponsors、FUNDING.yml、Open Collective 以及基金会成员名单,因为这些信息是公开且可被机器读取的。但对于关键基础设施而言,最常见的资金安排恰恰不属于上述任何一种。通常是维护者受雇于 Red Hat、Google、Intel、Canonical 或某家硬件厂商,并将该项目作为其工作的一部分(甚至全部),而这种安排不会在爬虫可获取的任何文件中留下痕迹。其次是围绕该项目提供咨询和支持服务的合同,同样难以追踪。

我和本·尼科尔兹在2025年FOSDEM上就此主题发表演讲:当我们试图描绘开源资金的实际来源与流向时,发现公众所依赖的“捐款箱”层面——即人人都在测量的那一层——其实只是覆盖在一个规模大得多、几乎完全不可见的资金池之上,其中包括企业薪资、基金会拨款(未公开披露)以及支持服务收入。若仅依据公开层面建模,系统会将拥有专职团队的项目标记为“无资助”,却将一个虽已启用但几乎无人使用的赞助按钮视为可持续发展的证据。

复合情况

单独来看,每种测量方式都会在某些方向上对部分项目进行误判;而对于大多数现代、托管在注册中心并通过GitHub发布的包来说,这些误差大致相互抵消。问题在于这些误差具有相关性——因为一个足够古老以至于早于GitHub出现的项目,更有可能用C语言编写、通过捆绑而非清单依赖分发、在邮件列表中开发、由某人薪水中资助,并且由于其所实现的格式多年未变而更新频率极低。

因此,同一个项目会同时在六个方面被低估或忽略:下载量统计偏低、从依赖图中消失、贡献者指标归零、Scorecard评分低下、被标记为“无资助”、并被判定为“不活跃”。这一切都源于同一个根本事实:它看起来不像一个npm包。那个只有一个疲惫维护者、没有仪表盘足迹的静默系统库,正是我们构建所有这些工具所要找的对象,而这类项目恰恰是现有工具体系最难以识别的。

需要完整排版与评论请前往来源站点阅读。