我分析了 Shadcn-UI/UI 中的 200 个 PR 以查找重复项:出乎意料地顺利。
Source: Dev.to
PR 冗余审计 – shadcn‑ui/ui
受 Pete 的一条推文启发,内容关于像 OpenClaw 这类高流量仓库的 PR 泛滥。AI 代理在编码方面很强大,但它们也在生成重复的逻辑和与维护者长期愿景不符的 PR。该项目审计 PR 并相应地标记它们。
目标
识别两个(或更多)贡献者在完全不同的方式下解决相同功能问题的情况,通常跨越不相连的文件。
系统并不寻找复制粘贴的代码行;它评估的是架构目标。当发现匹配时,系统会将 PR 分类到以下三类之一:
| Bucket | Description |
|---|---|
| SHADOW | 对同一回归的完全相同的修复。 |
| SUPERSET | 包含更小、特定修复的更广泛的架构修复。 |
| COMPETING | 两条不同的路径用于实现相同的功能结果。 |
下面的所有示例都旨在修复损坏的 /blocks 页面链接。
示例 PR(相同功能失效,不同文件)
| PR 编号 | 标题 | 策略 | 修改的文件 |
|---|---|---|---|
| #10156 | fix: update broken link on /blocks page | 简单 URL 替换 | apps/www/config/docs.ts |
| #10088 | fix(docs): absolute path for blocks link | 路径规范化 | apps/www/lib/utils.ts |
| #10096 | chore: rename internal block references | 重构引用键 | apps/www/registry/registry.json |
虽然这些更改涉及完全不同的文件(Config、Utils 与 Registry),系统仍然识别出它们都针对 相同的功能失效 —— 目标重复。
PR #10088 解决了根本原因(重命名文件),使得在合并前 #10156 和 #10096 中的文档修复变得多余。
审计结果
- 200 个最近的 PR 已扫描。
- 69 条有效冗余 已标记。
- 以下是一些最有趣的匹配。
| PR 标识 | 主要匹配 | 分类 | 为什么重要 |
|---|---|---|---|
| #10404 – ThemeHotkey 守卫 | #10401 | SHADOW | 对 event.key 的相同空值检查导致 Hotkeys 中的崩溃。 |
| #9895 – 文档复制按钮 | #9876 | SHADOW | 对 bash 命令/文本的相同拆分用于修复复制按钮。 |
| #10421 – DataTable 可访问性 | #10402 | SHADOW | 同时向数据表添加 aria-label。 |
#10403 – Drawer asChild 修复 | #10139 | SHADOW | 为 Drawer 文档添加 asChild 以修复嵌套破损。 |
| #10424 – Monorepo CLI 修复 | #10258 | SUPERSET | 对 monorepo CLI 的更广泛“一次性修复”策略。 |
| #10393 – Geist 字体不匹配 | #10273 | SUPERSET | 比 #10393 更健壮的字体映射。 |
| #10244 – 日历响应式 | #10235 | COMPETING | 对日历宽度响应性的不同 CSS 策略。 |
| #10386 – ThemeHotkey 错误 | #10404 | SHADOW | 对自动填充期间未定义键崩溃的相同逻辑层面修复。 |
| #10383 – FieldSeparator 修复 | #10201 | SHADOW | 两个 PR 都修改同一属性以修复分隔符继承问题。 |
| #10158 – iOS 日期输入 | #10133 | COMPETING | 对同一 iOS 渲染错误的全局 CSS 与组件级修复的竞争。 |
实现概览
1. 回填脚本(历史审计)
- 使用 Octokit 分页获取 PR,目标是关键分支(
main、master)。 - 压缩并过滤 大量 diff,以保持在免费额度范围内:
- 去除已知的大文件(SVG、lockfile、文档)。
- 删除注释和未改动的 import。
- 若仍超过 1500 字符,只保留修改的代码块(
+/-行)。
- 使用 Gemini 嵌入模型对每个清理后的 PR 进行 向量化,并存入 Upstash Vector。
2. 实时机器人(现场分流)
- 当有新 PR 到来时,查询 向量库,获取最相似的 8 条候选记录。
- 将这些候选记录传入 LLM 推理循环,判断意图并分配到相应的桶(SHADOW / SUPERSET / COMPETING)。
3. 处理速率限制
- 路由器会在不同提供商之间自动切换(Gemini、Llama、OpenRouter 等)。
- 对 503/429 错误采用 3 次重试,并使用指数退避策略。
经验教训
| Issue | Observation | Mitigation |
|---|---|---|
| Vector Gap | 同一问题的 PR 采用截然不同的实现方式时,往往在向量搜索中找不到对应项,导致它们从未被 LLM 检索到。 | 添加了一个回退的 “语义‑关键词” 索引(例如提取领域专有词),以扩大召回范围。 |
| Structural Bias | 早期模型把无关的 JSON 添加误判为重复项,因为它们的结构看起来相似。 | 在构建嵌入时优先考虑 字面值(如 ID、URL),而不是单纯的语法结构。 |
| Model Quota | 用尽了高阶 AI 配额,只好回退到较小的 8B 模型,导致偏差增大。 | 实现了 预算感知调度器,仅在相似度已经很高时才使用更便宜的提供商。 |
| False Positives | 仅靠向量相似度会产生大量误匹配。 | LLM 推理阶段现在会在最终分类前执行 目标对齐检查。 |
要点
- 历史聚类 冗余 PR 是对任何检测引擎的极佳压力测试。
- 两阶段流水线(向量相似度 → LLM 推理)在速度和准确性之间取得平衡。
- 考虑速率限制的设计(免费额度层)对开源工具至关重要。
- 即使使用了高级工具,人工审查 仍是最终裁决者——尤其是针对边缘案例的 “COMPETING” 修复。
如果您想查看代码或在其他仓库上运行审计,欢迎提交 Issue 或 PR!
Problem
它开始将全新的注册表条目标记为重复,仅仅因为它们在 JSON 结构层面看起来相似。系统实际上忽略了实际的 URL 值,因为它不够智能,无法在内容与结构之间进行权衡。
对宽范围扫描的弱点
在审视大型 PR 时,系统有时会看到并不存在的关系。如果两个大型 PR 恰好都涉及同一个包,系统可能会产生它们之间的关联,即使从逻辑上看,它们毫无关联。
解决方案
backfill engine 现在成为下一步的分析核心:实时 GitHub Bot。通过利用我们构建的历史记忆,Bot 能在新 PR 刚打开的瞬间进行分析,并在已有冗余修复时提醒 maintainers。
未来工作
我也在探索一个 Maintainer Dashboard 来可视化这些语义聚类,为项目维护者提供一个高层次的视图,了解他们的贡献者在哪里意外地重叠。
行动号召
如果你是想在自己的仓库上尝试的维护者,或是想要贡献的开发者,联系我——我很乐意聊聊。