
现在基本都在用 AI 写代码但是实际上 AI 经常缺少全局意识甚至你就算写了规则它也不一定遵守比如一个昨天就看到一个网友发的这个感觉就挺典型了这次情况下你如果没有仔细 Review 有可能不知不觉里项目就已经整了一大批相似代码比如相同的 Loading、Error、Snackbar、日期格式化和 Repository 转换逻辑。然后 Deslop 就是用在这个方面它会解析 Dart 代码的语法结构然后通过规则找到完全相同、变量改名后相同、局部修改后相似的代码同时通过 MCP 在 Agent 写代码之前提供一次“仓库里有没有类似实现”的查询。是的最重要的就是这个提供查询。它能把重复检测从代码提交后的质量检查提前到了 Agent 的生成循环里比如类似代码实际上如果直接让 AI 判断这两个实现可能 AI 都不一定判断得出来是否重复因为函数名、参数类型、变量名都不同最终可能 出现两个实现开始独立演进修复空字符串、国际化或者姓名顺序时很容易只改到其中一个。特别是 Flutter 项目更容易出现这种情况因为声明式 Widget 树包含大量稳定结构AI 很容易重复生成以下内容AsyncValue、FutureBuilder或自定义状态中的 loading/error/data 分支Scaffold、SafeArea、空状态和错误卡片DTO 到 Entity 的字段映射Repository 中相似的重试、异常转换和分页逻辑日期、金额、文件大小等格式化函数Golden Test 的设备配置和 Provider overrideSnackbar、Dialog、BottomSheet 的包装函数这些代码有时候其实只有十几行但是经过几个月持续生成后可能每种基础能力会可能出现三四套版本。而 Deslop 的做法就有点意思了它会用用 Tree-sitter 解析 Dart 的构造语法树再经过标准化、指纹计算、近似匹配、聚类和排序比如官方给出的完整流程是discover → parse → normalize → fingerprint → cluster → LSH → embed → fuse → rank → render在这里第一层会先把代码还原成语法结构Tree-sitter 会把函数、调用、条件、属性访问、循环和表达式解析为 AST然后 Deslop 对语法树做标准化处理标识符统一替换为__ident__字符串、数字和字符常量替换为__literal__删除注释、空格和其他语法 trivia前面的formatUserLabel和buildAuthorName经过处理后都会变成“函数声明、一个参数、字符串插值、两次属性访问”的相同结构单纯改函数名和变量名在这种情况也可以被检测出来。所以 Deslop 的核心是关注代码的语法形状变量重命名和格式化不会破坏结果。然后第二层会用 Merkle Hash 找完全相同的子树默认情况下Deslop 会处理不少于 30 个 AST 节点的子树从叶子向上计算 BLAKE3 Merkle Hash节点类型和子节点顺序相同最终便会得到相同指纹这部分主要捕获 Type-1 和 Type-2 Clone类型典型情况Deslop 的识别方式Type-1代码、变量名、常量都相同AST Merkle HashType-2变量名、函数名或常量不同标准化 AST 后再计算 HashType-3增删了少量语句主体仍相似AST k-gram、MinHash 和 LSHType-4写法明显不同行为接近可选的代码 EmbeddingDeslop 还会对连续 2 到 8 个兄弟语句建立窗口指纹这样后面就算两段函数外围结构不同只要中间存在一组连续重复语句也有机会被发现。接着第三层会用 MinHash 和 LSH 找“近似复制”完全相同的 Hash 不能处理局部增加一条判断、调整部分语句之类的情况所以 Deslop 会把标准化后的 AST 节点类型组成宽度为 5 的 k-gram再计算包含 128 个值的 MinHash 签名并拆成 32 个 band每个 band 包含 4 行。这个设计的目的是快速召回可能相似的代码对发生 band collision 后Deslop 再根据完整签名估算 Jaccard 相似度单独依靠这一路信号时当前规则还要求token_jaccard ≥ 0.90两端都至少有 40 个 AST 节点这样的目的是减少普通 Flutter Widget 结构带来的噪音很多 Widget 都有build → Column → children只凭几个相同节点就判定重复报告很快会失去价值。最后是可选的语义 EmbeddingDeslop 还支持通过代码向量寻找“行为接近、语法差异较大”的实现例如命令式循环和函数式集合操作。不过这一层默认关闭目前只实现了 Ollama Provider默认模型为nomic-embed-text需要显式启用--embeddings auto或--embeddings required。最后三种分数会合并每一对候选代码都有三个独立信号structural完全结构匹配取值为 0 或 1token_jaccard近似语法序列相似度embedding_cos代码向量余弦相似度候选阶段的融合值采用三者中的最大值max(structural, token_jaccard, embedding_cos)达到FUSED_THRESHOLD 0.85才会保留然后就会通过传递闭包建立 Cluster如果 A 与 B 相似B 与 C 相似三者会进入同一组即使 A 与 C 没有直接达到阈值。这种聚类方式扩大了召回范围当然也带来一个需要人工留意的问题Cluster 内部其实不保证任意两个成员都同样相似规模较大的 Cluster 可能由中间代码串联起来不能看到一组结果便直接抽成公共基类。Deslop 还会对结构相同但内容证据不足的结果进行 content gate同时默认把structural_only和大块数据型重复降到 0.15 倍权重。然后还有排序Deslop 会用下面的公式给 Cluster 排序weight clone_node_count × (cluster_size − 1) × log2(1 spanned_bytes)它主要考虑了三个维度重复片段包含多少 AST 节点同一片段出现了多少份这些代码在源码中占用了多少字节比如 cluster_size − 1可以近似理解为“有多少份可以删掉”log2会压低超大文件的体积优势避免一份巨大的生成文件占据整个榜单最终报告优先展示收益最高的重复项然后 Deslop 目前还提供的几种形态形态之间共享同一个deslop-core分析引擎但使用场景差异很大CLI 适合首次审计、CI 和冷扫描执行deslop .后默认会在项目的.deslop/目录生成 JSON、TXT 和 HTML 报告JSON 面向 Agent 和自动化处理TXT 适合终端HTML 用于人工查看。VS Code 扩展会启动 LSP监听文件变化并增量更新结果每个文件以内容 Hash 为缓存键没有变化的文件可以跳过分析MCP 是给 Coding Agent 提供查询入口这里的架构细节很有意思deslop-mcp自身不重新分析整个仓库它通过本地 IPC 向正在运行的 LSP 查询最新结果macOS 和 Linux 使用 Unix SocketWindows 使用带 Token 的本地 TCP LoopbackAgent 调用一次find-similar通常不需要重新遍历所有 Dart 文件CI 负责长期趋势当重复率超过.deslop.toml中的阈值CLI 返回退出码 3报告还会生成方便从失败的 Pipeline 中查看具体问题name: deslop on: [push, pull_request] jobs: duplication-gate: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: Nimblesite/Deslopv0.32.0 with: fail-over: 5.0 # or omit to use [threshold] in .deslop.toml如果你只运行deslop .那它和其他静态分析器的差别主要体现在 Dart 支持、AST 结构检测和排序但是支持 MCP 就不一样了它可以带进 Agent 的写入循环比如Agent 准备写函数 ↓ 调用 find-similar ↓ 查询当前仓库的结构索引 ↓ 找到近似实现 ↓ 复用、扩展或放弃新建这相当于给 Agent 增加了一套本地代码检索LLM 负责理解需求Deslop 负责回答仓库里是否已有相似结构检索结果是确定性的 AST、Token 和可选向量分析不需要让模型先盲目grep一圈再猜哪个函数可能相关这个理论上其实更不容易幻觉。不过安装 MCP 不会保证 Agent 主动调用它所以官方专门提供了一段AGENTS.mdCLAUDE.md规则要求 Agent 在创建超过几行的新函数、类、Fixture、Parser、Route 或 ViewModel 前调用find-similar。官方当前建议的判断区间是fused ≥ 0.85优先复用或抽取0.6 ≤ fused 0.85读取候选代码后判断fused 0.6结构距离较远可以继续创建作者还把 Deslop 封装成了一套更谨慎的 Dart/Flutter Agent Skill先只读扫描再由用户确认随后判断每个重复项是否值得合并最后通过重构前后的测试进行验证kevmoo_skills/skills/deslop-duplication-audit/SKILL.md at main · kevmoo/kevmoo_skills · GitHub不过也不是重复代码就都应该消失比如两个函数可以拥有相同结构同时承担完全不同的领域语义比如价格格式化和重量格式化目前可能都是value.toStringAsFixed(2)这里如果将它们合并成formatNumber()其实会抹掉领域含义以后价格需要货币精度重量需要单位转换这个公共函数又会被拆开。实际上在 Flutter 中还有几类常见的合理重复iOS 与 Android 的平台实现需要保持隔离多个页面遵循相同 Widget 骨架但生命周期和交互正在分化不同 Domain 的 Repository 恰好采用相同流程测试用例刻意展开 Arrange/Act/Assert方便独立阅读性能敏感循环为了避免闭包、动态分发而保持专用实现不同 package 需要独立发布强行共享会制造反向依赖所以结构查重只是提供主要证据架构边界还是需要开发者自己判断。链接Deslop: Find Duplicate Code in Your Flutter App