你們團隊在跑 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。這個分離度正是你要的,而它來自量測,不是來自推理。
還是一大群。這個是經典題。
互斥集合(union-find)配上「跟任何一個成員相似就算」,意思是 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 要解的問題 —— 尚未正式發表。這支腳本不靠它,以後也不會。