智能体技能为重复代码检测带来了什么
Kevin Moore(Google 的 Dart 与 Flutter 产品经理)发布了一个名为 deslop-duplication-audit 的智能体技能。它不是我们编写、委托或在发布前审阅过的。他还写了一篇介绍文章:Dart、Flutter、重复代码与 Deslop 技能。
如果你在做开发者工具,这个技能值得一读,因为它做到了我们在检测器内部做不到的事。
检测器产出证据,而不是决定
Deslop 用 tree-sitter 解析代码,归一化 AST,对子树做指纹,聚类,然后对结果排序。我们的准确性标准是绝对的:每一个上报的簇都是真实的重复,每一处真实的重复都会被上报。 我们死守这条线,因为误报会让人学会忽略报告,而漏报则根本不会被发现。
但"这确实是结构性重复"和"你应该合并它们"是两个不同的论断,只有前者由我们负责。后者取决于类型系统、热点循环、模块边界、发布风险以及谁拥有这个文件——这些上下文静态分析器并不掌握,也不应该假装掌握。
因此分工是这样的:工具提供证据,智能体则去梳理这些证据,并在据此做出任何决定之前先加以核实。两半单独都不成立。无人质疑的证据会变成工单;没有证据的智能体只是在猜。
智能体恰恰在这个缝隙里出错。把一份排好序的重复簇清单丢给编码智能体,不附带任何流程,它就会把清单当成工单,重构所有内容,然后交回一份没人要的 diff。报告是准确的,结果依然很糟。
这个技能用流程补上了这道缝隙。
判定关卡才是重点
技能的核心是一节叫做 "Actionable vs. Necessary"(可执行 vs. 必要)的内容,而它开篇就要求智能体与工具唱反调:
不要把每一处重复发现都当成缺陷或必须重构的目标。
接着它做了让这条指令真正可用的事——具体列举拒绝的类别,而不是抽象地要求"运用判断力":
- 类型不安全的多态目标。 形状相似但在被操作的属性上没有共同接口的类。用
dynamic或强制转换来统一它们,是拿编译期安全换来寥寥几行代码。 - 性能敏感的专用循环。 求解器中的对称遍历,共享抽象意味着要在紧凑路径里分配闭包或做虚派发。
- 对独立入口点的投机性包装。 散落在互不相关的
bin/脚本里的四到六行try/catch。合并它反而损害可读性,且毫无收益。
当一个簇落入上述类别时,技能要求智能体记录一条 Rejected(已拒绝) 判定,在报告中写明技术理由,并且完全不改动代码。
"运用判断力"不是智能体能执行的指令。"这里有三种看起来像重复、但应当保持重复的具体形态,你必须写下适用哪一种"才是。这两句话的差别,构成了该技能的大部分价值。
先只读,然后硬性暂停
第一阶段只扫描,不写入。附带的辅助脚本运行 CLI、解析 JSON 报告,并输出带源码链接的紧凑 Markdown 摘要——工作树里没有缓存目录,git 状态不变脏,也不做任何编辑。
第二阶段是完全停下来。智能体呈现排名靠前的簇,然后必须先问两个问题才能动手:你是否要进行整改,以及改动落在哪里——当前分支、特性分支,还是隔离的 worktree。
"先发现、后授权"听起来理所当然,实际却经常被跳过。它决定了你拿到的是一份可以边喝咖啡边读的报告,还是一份没人要求过的四十文件 diff。
把关的是项目的验证工具,不是智能体的自信
第四阶段是经验性的,而且按唯一能证明问题的顺序执行:
- 在编辑之前跑测试。基线是红的就停下并上报,而不是四十分钟后把锅甩给这次重构。
- 做外科手术式的修改。只碰去重所必需的部分,不动相邻代码。
- 重新运行分析器和测试套件——
dart analyze --fatal-infos、dart test——要求零错误、零 info、全部通过。 git add,报告行数变化和每个簇的判定,然后停下。在人类点头之前,不提交、不推送、不开 PR。
在整个流程中,智能体"觉得这次重构是安全的"这一点在任何环节都不作数。只有项目自己的工具链才能放行一次改动。
限定到 diff——以及它暴露出的缺口
在既有仓库上跑重复检测器,会把里面所有历史遗留的簇都翻出来。在代码评审场景下这就是噪音,因为待审的只有这次改动触及的部分。
技能的做法是把报告与 diff 求交集:
dart run skills/deslop-duplication-audit/bin/deslop_report.dart \
--dir {repo_dir} \
--diff-cmd "git diff main...HEAD" \
--only-changed
它同样支持 Jujutsu——--diff-cmd "jj diff"——这在 Google 内部很重要。
这里是该技能直接回馈我们的地方。在外部做这种过滤,意味着要解析 unified diff、计算行区间交集,并对我们的 JSON 做后处理。Kevin 提交了 issue #364,请求在 CLI 中原生支持 --diff 和 --only-changed,把评审真正关心的两个问题分开:这次改动是否引入了重复,以及被改动的代码是否克隆了别处已有的辅助函数。
这个能力我们没做。一个由真正在用这把工具的人写出来的包装脚本,本身就是一份带可运行参考实现的可用性规格说明,它比我们内部任何路线图讨论都更有说服力。
把出处写进 Pull Request
最后一个阶段会在 commit 或 PR 正文中追加一段可复现信息——运行时通过 deslop --version 解析出的 Deslop 版本,以及产出这些发现的确切命令行。
功能很小,效果不成比例。面对一份去重 diff 的评审者,通常没有办法核实背后的论断。锁定的版本加上可运行的命令,把"工具说的"变成了评审者可以重新执行的东西。无法复现的工具输出只是主张;附带命令的工具输出才是证据。
告诉智能体这个工具会在哪里出错
任何工具都有未修复的缺陷。把工具输出当作绝对真相的智能体,迟早会依据一条维护者早已知道是错的结论去动手。
所以我们把自己的缺陷公开出来。议题关系图展示所有未关闭的议题以及它们之间的关联;规划页展示工作跑道与排序队列,用透明的默认工作量估算,而不是承诺的日期;整份数据以机器可读的形式发布在 /assets/data/issues.json——这意味着技能可以把实时的缺陷清单交给智能体,而不是一份在技能写完那一周就已经过期的快照。#364 就在里面。任何当前影响准确性的问题也在里面,与准确性与透明度页面上公布的实测数字并列。
这一点并不只适用于我们。如果你的工具有公开的问题追踪器,就在技能里点名它,并说清楚智能体该拿它做什么:当某个结果看起来不对或出乎意料时,先与已知缺陷比对再决定是否照做;如果对不上,就提交一个新 issue。知道哪些证据本身已存疑,是核实证据的一部分。
为你自己的工具写一个同类技能
智能体技能就是一个带 front matter、说明何时使用的 Markdown 文件。格式就这么多,价值完全在正文提出的要求里。我们会从这一个里带走这些:
从只读开始。 第一阶段应当在结构上就无法改动工作树。发现与整改是两件事,应当分别授权。
明确写出暂停点。 写清"硬性暂停关卡",并指明必须回答什么才能继续。智能体不会从一句委婉的建议里推断出要停下来。
给智能体拒绝的权力,并且要具体。 列出你的工具输出应当被拒绝的具体形态,并要求为每一次拒绝写下理由。这是你能写下的价值最高的一节,而且只有当你用自己的工具在真实代码上跑到被它烦到,才可能写对。
把判定交给项目的验证工具。 先基线,再编辑,然后分析和测试。任何一步都不应依赖智能体自认为没问题。
把 token 预算当作设计约束。 该技能附带的辅助脚本会把 JSON 报告转成带链接的简短 Markdown 摘要。原始分析器 JSON 会淹没上下文窗口,并劣化其后的每一个决策。要在边界处做压缩。
只暂存,不提交。 把版本控制留给人类。git add 加上 git diff --cached --stat 就能给他们做决定所需的一切。
输出出处信息。 版本和命令,写进 PR 正文。
指向工具的已知缺陷。 链接到追踪器,并要求智能体在照做之前先把可疑结论与之比对,对不上就提 issue。一个把自身工具当作绝对可靠的技能,只会自信地把该工具的 bug 自动化。
说明何时不该用它。 Kevin 的 front matter 以这句结尾:"不要用于非 Dart 项目、非 Git 检出,或简单的单文件语法检查。" 一个宣称到处都适用的技能,会在不该被调用时被调用,并在第一次误伤时耗尽信任。
把安装步骤指向你真正支持的渠道。 该技能先探测 $PATH,再走回退安装。如果你为自己的工具写技能,请把回退指向你真实的分发渠道——对 Deslop 来说是 brew install nimblesite/tap/deslop、scoop install deslop,或者 VS Code 扩展,它把 CLI、LSP 和 MCP 服务器打包在一起并锁定版本。
审计负责清理,预防负责阻止下一次复制
审计是回溯性的,它找出已经被写了两遍的东西。
另一半是我们的 MCP 与 LSP 服务器,它们暴露 find-similar,让智能体在动笔写函数之前先查一遍仓库。命中强匹配时,智能体会复用既有实现,而不是产出一份下周会被审计标记出来的近似副本。Claude Code、Cursor 及其他客户端的配置见 AI 集成文档。
这两半对我们的要求是同一件事,而且不是更多功能。而是:我们上报的每一个簇都是真的,每一处真实的重复都会被上报——这正是我们在迈向 100% 准确率中给自己定下、并在准确性与透明度页面上公开衡量的标准。建立在不准确检测器之上的技能,只会更快地自动化糟糕的重构。只有底下的证据站得住,流程才有意义。
如果你发现 CLI、MCP 服务器或 LSP 给出了错误、过时或缺失的结果,请提交 issue。#364 就是这么来的,这也是我们通往一个值得被封装的检测器的最快路径。