你们团队在用 Claude Code、Cursor 和 Copilot。它们动手之前都会先读一个文件 —— CLAUDE.md、.cursor/rules/*.mdc、.github/copilot-instructions.md。这些文件会被复制出去:复制进另一个仓库,复制到另一台笔记本,复制进某个人的临时目录。
然后有人改了其中一份。
不报错。没有警告,没有 diff,没有测试失败。那台机器上的智能体只是行为不一样了 —— 六周前有人删掉的一条规则,或者只有半个团队的智能体知道的一条约定。它以“在我机器上是好的”的形式浮出来,而且没有东西可以二分定位。
我写了个脚本,把这些文件找出来,告诉你哪些副本对不上。
curl -O https://raw.githubusercontent.com/untactit/agent-drift/main/agent_drift.py
python3 agent_drift.py ~/work
Scanned 47 instruction files.
DRIFT: 2 documents, 5 distinct versions between them.
claude-code:CLAUDE.md
6 copies, 3 versions
9b01aeaa204d 3 files, 406 lines
~/work/api/CLAUDE.md
~/work/web/CLAUDE.md
2dc3c616c279 2 files, 411 lines
~/work/infra/CLAUDE.md
单个文件,只用标准库,Python 3.8+。只读 —— 它从不写它扫过的文件。MIT。
最朴素的那版我花了十分钟写完,然后毫无用处。真正的内容是那三处修正。
第一版:把每一个 CLAUDE.md 收集起来,算哈希,把不一样的报出来。
在自己机器上跑了一遍。它报出21 个文件属于同一个文档,有 18 个版本。
当然会这样。互不相干的项目本来就有互不相干的指令。它们从来就不该一致。把这个报成漂移的工具就是噪音,而噪音工具比没有工具更糟 —— 跑到第三次你就不看它了。
两个文件是不是同一份文档,由内容说了算,不是由文件名说了算。
于是:归一化空白,分词,用 Jaccard 相似度比较,阈值 0.7。
还是一个大团。我去测了具体的文件对,看看为什么:
012Hki23QBMr ↔ 016ct6suZ14M : 0.000
012Hki23QBMr ↔ 016avoYh97Eu : 0.764
给同一个工具写的指令文件,用词几乎完全重合。claude、skill、agent、file、commit、never、always。两份毫无关系的文档,当成词集合看照样高度重叠。
换成 5 词 shingle —— 把文本里每一段连续 5 个词的短语收成一个集合。用词可以撞,措辞很少撞。重新测:不相干的文件对掉到 0.000,真正分叉出来的文档落在 0.53–0.76。这个分离度正是你要的,而它来自实测,不是来自推理。
还是一个大团。这个是经典问题。
并查集加上“和任意一个成员相似就算”,意味着 A 并进 B,B 并进 C,于是 A 和 C 明明毫无共同点却进了同一个簇。一条弱连接,整棵树就被这个簇吞掉。
现在每个簇都保留一个代表元 —— 簇里最大的那份 —— 一个文件只有直接和这个代表元相似才会加进来。不管扫描顺序如何,簇都保持内聚。
在我机器上的最终结果:15 个文件,其中 9 个与代表元的相似度在 0.53–0.65。这些确实是同一份模板分叉出来的。检测是对的,而且数量小到可以动手处理。
- run: python3 agent_drift.py . --fail-on-drift
存在漂移就以 1 退出。团队指令的两份副本对不上的那一天构建就变红,而不是半年后有人问“智能体为什么不遵守某条规则了”你才发现。
--json 会给你完整报告,你想自己做后处理就用它。
Claude Code、Codex/AGENTS.md、Cursor、GitHub Copilot、Gemini、Windsurf、Cline、Aider、Continue、Zed。这些路径模式就是文件顶部的一个常量 —— 少了你在用的那个,提 PR 或开 issue。
检测是这件事里难度低的那一半。副本的存在是有原因的,原因不消失,漂移就会回来。真正让它消失的,是一份评审过的源头,不靠任何人粘贴就到达每一台机器。
这就是我做 untactit 要解的问题 —— 尚未公开发布。这个脚本不依赖它,以后也不会。