增量更新 + 實體去重

「每次只處理變更的檔案」+「把同一個概念的不同名字併成一個節點」
來源:docs/superpowers/ design + plan(2026-05-04,分支 v7,issue #698)
這是兩份設計文件specs/…-design.md(設計,本文主體)與 plans/…-plan.md(實作計劃,checkbox 逐步)。上游用 superpowers 的 subagent-driven-development 流程落地。

要解決的兩個問題

  1. 每次重跑都全部重做graphify extract 每次執行都從零重建整張圖——不管改了什麼都把全部檔案重新送 LLM。對一個每天更新的 1000 檔 markdown 語料,這很貴。
  2. 同實體不同名字:LLM 抽取是分塊進行,同一個真實概念在不同 chunk 可能得到不同 label(AuthManagerAuthenticationManagerauth_mgr)。除了精確字串正規化外,沒有語意去重。

新 Pipeline(每次 graphify extract

detect (full or incremental, auto-detected)
    ↓
AST extract (code files, AST cache-aware)
    ↓
Semantic LLM extract (doc/paper/image files, semantic cache-aware)
    ↓
build_merge (merge into existing graph, prune deleted nodes)
    ↓
deduplicate_entities (normalize → entropy gate → MinHash/LSH → Jaro-Winkler → community boost → optional LLM)
    ↓
cluster (full graph, always re-run)
    ↓
score_all + god_nodes + surprising_connections
    ↓
write graph.json + .graphify_analysis.json + manifest.json

功能一:增量更新(Incremental Updates)

自動偵測

graphify-out/manifest.json + graphify-out/graph.json 都存在 → 進入增量模式。不需要任何 flag。第一次跑永遠是全量。

增量模式做了什麼

Semantic cache(全量與增量都適用)

展開英文原文:輸出摘要範例
[graphify extract] incremental: 20 changed, 980 cached, 2 deleted
[graphify extract] graph: 4,821 nodes, 12,304 edges, 43 communities
[graphify extract] tokens: 18,432 in / 6,201 out, est. cost: $0.08

改動範圍:graphify/__main__.pyelif cmd == "extract": 區塊約 5 處。

功能二:實體去重(Entity Deduplication)

新模組 graphify/dedup.py,單一職責,從 build.py 在建圖後呼叫,回傳去重後的 (nodes, edges)。七個步驟:

Step做法說明
1. 精確正規化接上 build.py 的既有 deduplicate_by_label抓跨檔案的 case/標點變體。免費,本來就寫好了。
2. 熵門檻entropy < 2.5 bits/char 的 label 跳過模糊比對短歧義名(AIDBx)太危險,不自動合併。只有高熵 label 進下一步。
3. MinHash + LSH blockingdatasketch,3-gram、128 排列、threshold 0.7候選對用 O(n) 產生(而非 O(n²))。1 萬節點 < 1 秒。
4. Jaro-Winkler 驗證rapidfuzz,≥ 0.92抓錯字、複數、空格變體。低於門檻的對丟棄。
5. 同社群加分兩個節點共享 Leiden community ID 加 +0.05Graphify 特有優勢——社群結構是 GraphRAG/LightRAG 沒用的強訊號。
6. Union-find 合併確認的對餵進 union-find → 連通分量 → 每個分量合併成一節點邊重新指向生還者;自環丟棄;偏好較短、非 chunk 後綴的 ID 當生還者。
7. 可選 LLM 仲裁--dedup-llm flag歧義對(0.75–0.85)每 30 個一批、每批一次 LLM 呼叫;1 萬節點約 $0.01。預設關閉。

整合點

去重在 build_merge/build_from_json 之後、cluster 之前執行。順序很重要:圖越乾淨,社群偵測越好

# in build.py
G = build_merge(...)          # or build_from_json
G = deduplicate_entities(G)   # new step
communities = cluster(G)      # unchanged

新相依套件

測試計劃

Non-goals(刻意不做)

教學價值:這份設計是「大型開源專案怎麼把工程計劃寫成可稽核文件」的範例——問題 → pipeline → 每步參數與理由 → 整合點 → 測試 → non-goals。而且它已經落地:cache.py(semantic cache)、dedup.py(七步去重)、build_merge 都在 程式碼對照 · ops 裡能對到實作。

教學解說

增量更新解決「每次重跑都全部重做」的問題。核心機制是 manifest.json:記錄上次的檔案狀態,下次跑時只處理新增或修改的檔案。自動偵測,不需要 flag。

實體去重解決「同實體不同名字」的問題。七個步驟從精確正規化到可選 LLM 仲裁,形成一個完整的去重管線。關鍵設計:用 MinHash/LSH 產生候選對(O(n) 而非 O(n²)),再用 Jaro-Winkler 驗證。

整合點很重要:去重在 build_merge 之後、cluster 之前執行。圖越乾淨,社群偵測越好。

Worked Example:觀察增量更新的行為

  1. 首次全量:在一個有 10 個檔案的目錄執行 graphify extract .,觀察輸出:所有檔案都被處理。
  2. 修改一個檔案:修改其中一個 Python 檔案,再次執行 graphify extract .。觀察輸出:只有 1 個檔案被重新處理,其他 9 個從快取讀取。
  3. 新增一個檔案
  4. 刪除一個檔案:刪除一個 Python 檔案,再次執行。觀察輸出:被刪除檔案的節點從圖中移除。
關鍵觀察:manifest.json 只在成功完成後寫入。中途當機不會弄壞下次的 diff。

常見錯誤與診斷

錯誤訊息原因解決方式
manifest.json: file not found首次執行或 manifest 被刪除這是正常的,首次執行永遠是全量
dedup: entropy too lowlabel 熵值 < 2.5 bits/char短歧義名(如 AI、DB)不自動合併,這是設計如此
build_merge: graph conflictmerge 時遇到衝突檢查是否有重複的節點 ID
semantic cache: stale entry快取的 source_file 路徑已改變快取會自動更新 source_file 到新路徑

練習與驗收清單

  • [ ] 能解釋增量更新的自動偵測機制
  • [ ] 能說明 manifest.json 的作用與寫入時機
  • [ ] 能說出七步去重的名稱與順序
  • [ ] 能解釋 MinHash/LSH 為什麼比 O(n²) 更快
  • [ ] 能說明同社群加分(+0.05)的設計理由
  • [ ] 能測試增量更新:修改檔案後只重新處理變更部分

① 進階真實情境 Worked Example:每天自動更新的 1000 檔文件語料 + 增量排程

前面示範單機手動操作;這次是維運情境:你有一個團隊 wiki,每晚同步外部 repo + 筆記,1000 份 markdown 每天變動約 20 份,你要讓「建圖」變成無人值守的 nightly job,並控制成本。

  1. 首次全量建圖graphify extract corpus/ 產生 manifest + graph.json(費用較高的一次,可接受)。
  2. 夜間同步:cron 先 rsync/git pull 更新 corpus,再 graphify extract corpus/——自動偵測到 manifest + graph.json 存在 → 進入增量模式,只對 new_files(約 20 份)跑語意抽取,其餘 980 份走 semantic cache 命中。
  3. 觀察輸出:看「incremental: 20 changed, 980 cached, 2 deleted」這行——deleted 的檔案由 build_merge(prune_sources=deleted_files) 從圖中剪除。
  4. 改名處理:如果檔案只是改名,內容 hash 不變 → cache hit,且 source_file 自動更新到新路徑(AST/semantic cache 同模式)。
  5. 防錯設計:manifest 只在成功完成後寫入——凌晨當機也不弄壞下一次的 diff;設 --max-spend 防月結爆表。
為什麼選這條路徑:全量重建每天 1000 份 markdown 的 LLM 成本會吃掉預算;增量 + 語意快取把「每天成本」壓到「只算變動的 20 份」。自動偵測(無 flag)讓 cron 腳本極簡,manifest 的「成功才寫入」語義讓排程可重啟、可當機、不會腐敗。這條路徑示範了「建圖系統」要長期維運所需的工程細節。

② 深入原理擴充:manifest 契約、build_merge、與「圖比檔案更舊」的一致性保證

增量不是「少做一點」的優化,而是一套一致性契約

  • manifest.json 是 diff 的真相來源:它記錄上次成功執行後「哪些檔案算數」。只有成功完成才寫入,所以「圖 + manifest」永遠是同一份快照;中途當機時,下次以舊 manifest 為基準,頂多多抽幾個檔,不會漏。
  • build_merge 比 build 複雜在有「存活邊」:changed 檔案指到 unchanged target 的跨檔邊必須保留;被 prune 的 source 產生的節點與邊要剪掉;還要跟 shrink guard(新圖不得更小)與 manifest 的一致性校驗互動。
  • Semantic cache 的 key 是「內容 hash + prompt fingerprint」:prompt 一改(比如 extraction-spec 調整),fingerprint 變,快取自動失效——這防止「舊 prompt 抽的結果在新 prompt 下被當新資料」。這是很多人忽略的失效條件。
  • 去重的順序依賴deduplicate_entitiesbuild_merge 之後、cluster 之前——圖越乾淨,社群偵測越好;但這也意味著「社群會因為去重結果而漂移」,跨版比較社群數時要小心。
大家以為建圖正確、但其實有誤的案例:以為「增量模式 = 只處理變更檔,所以社群不變」。實際上 cluster每次全圖重跑(設計文件明寫 "cluster (full graph, always re-run)")。加上去重可能把節點合併,社群數和社群成員每次都可能微調。若你把「社群穩定」當成「系統沒壞」的指標,會誤判——社群漂移是正常行為,社群 ID 跨跑漂移才該查(那是確定性失效)。

③ 診斷式疑難排解表

症狀可能原因解決方案
每次執行都顯示全量重建manifest.json 或 graph.json 被刪/被清(CI 工作目錄沒持久化)graphify-out/ 放進持久化 volume;確認不跑 --no-cache
改 prompt 後快取仍命中semantic cache key 沒含 prompt fingerprint檢查 prompt_fingerprint 是否納入 key;版本化你的 extraction-spec
檔案改名後出現「ghost 節點」改名但內容變了 → cache miss,舊節點未 pruned確認 manifest 正確偵測 deleted + new;檢查 build_merge 的 prune_sources
圖節點數不減反增(刪了一堆檔)deleted_files 偵測失敗(檔案被移到 ignore 目錄?)或 prune 沒生效看 detect_incremental 的 deleted 清單;確認沒被 ignore 規則吃掉
合併後社群「亂成一團」去重把不同實體誤併(Jaro-Winkler 閾值太鬆、缺社群加分護欄)檢查去重步驟 4/5 的門檻;考慮 --dedup-llm 仲裁 0.75–0.85 歧義對

④ 進階挑戰題

  1. manifest 的「成功才寫入」確保 diff 一致性。請設計一個失敗注入實驗(在 build_merge、cluster、write 三點各掛一次),證明不管在哪裡當機,下次跑都能安全恢復。
  2. 七步去重的 Step 5「同社群加分 +0.05」仰賴「先有社群、再去重」;但 pipeline 是「先去重、再 cluster」。這個看似矛盾的依賴怎麼被解決的?它對第一次建圖(無既有社群)的影響是什麼?
  3. 「社群永遠重跑」與「增量建圖」同時存在。若要讓社群在只改 1 個檔案時盡可能不漂移,你會採用哪種策略:固定 seed 重跑、接續舊社群標籤、還是凍結社群只在季度重建?各有哪些代價?

① 專案級端到端 Worked Example:CI/CD 管線中的增量建圖 + 去重 + 成本控制

前面的 Worked Example 是維運情境的 nightly job。這次是CI/CD 整合:你們的 monorepo(500 檔 Python + 200 檔 TypeScript)用 GitHub Actions,每次 PR 要跑圖分析、每次 merge 要更新圖、預算控制在每月 $20 以內。

  1. PR 階段(增量 + --check):PR 觸發 graphify extract . --check-drift——自動偵測到 manifest.json → 增量模式 → 只重抽變更檔案 → 如果圖跟程式碼不同步就 fail check。這防止「圖過期」的 PR 被合併。
  2. Merge 階段(全量更新):merge 到 main 後跑 graphify extract .——增量更新圖 + 用 --max-spend 5 限制每次 merge 的 token 預算。Manifest 只在成功完成後寫入,即使 CI 當機也不弄壞下一次 diff。
  3. 去重品質追蹤:每週跑一次 graphify extract --dedup-llm(啟用 LLM 仲裁),比較啟用前後的節點數——如果差異 >5%,代表七步去重的 Jaro-Winkler 閾值可能太鬆。
  4. 成本儀表板:從 spend ledger 聚合每月的 token 消耗——拆成 Pass 1(免費)vs Pass 3(花 token)vs dedup-llm(可選),確保 Pass 3 佔比 <80%。
  5. 季度全量重建graphify extract . --no-cache——消除增量累積的去重偏差,比較重建前後的圖差異(社群數、god nodes、節點數)。
預期成本:500+200 檔的 monorepo,每次 PR 增量約 $0.01-0.05(只抽變更檔案),每月 ~20 次 merge ≈ $0.2-1.0。Pass 3 的 LLM 成本集中在首次建圖,之後增量的 LLM 成本趨近於零(快取命中)。

② 效能/品質/安全深度

面向考量實務建議
效能MinHash/LSH 去重在 1 萬節點 <1 秒;但 union-find 合併後的邊重指向可能改變圖結構監控去重前後的節點數/邊數比;如果邊數下降 >10%,代表去重把不同實體誤併
品質manifest.json 是 diff 的真相來源;中途當機時下次以舊 manifest 為基準確認 manifest 只在成功完成後寫入(設計如此);定期用 graphify extract --check-drift 驗證圖與程式碼同步
安全semantic cache 的 key 含 prompt fingerprint;prompt 一改快取自動失效版本化你的 extraction-spec;不要手動改 cache 檔案;--no-cache 會清空整個快取目錄

③ 文件間比較對照表

面向本文(增量更新)相關文差異說明
增量機制manifest.json 自動偵測 + detect_incremental + build_merge架構總覽架構頁只描述靜態 pipeline(detect→export),不涉及增量模式
去重七步去重管線(精確正規化→LLM 仲裁)運作原理運作原理頁講信任標籤(EXTRACTED/INFERRED),不涉及去重邏輯
快取semantic cache 含 prompt fingerprint 失效條件程式碼對照 · ops程式碼對照頁逐函數講 check_semantic_cachesave_semantic_cache
測試五種測試類型(單元/整合/增量/改名/刪除)實作案例實作案例頁展示真實產出的 GRAPH_REPORT,但不展示增量測試過程

④ 互動式檢核清單

  • - [ ] 能在一個有 10 個檔案的目錄上跑兩次 graphify extract .,第二次觀察到「只有 1 個 changed」的增量行為
  • - [ ] 能解釋 manifest.json 的「成功才寫入」語義,並設計一個失敗注入實驗證明中途當機的安全性
  • - [ ] 能手動計算兩個 label 的 MinHash Jaccard 相似度(給定 3-gram 候選),並判斷是否通過 LSH threshold
  • - [ ] 能解釋為什麼「同社群加分 +0.05」在首次建圖時無效(因為尚無社群),以及這對第一次去重的影響
  • - [ ] 能設定一個 cron job,在每晚增量建圖後用 graphify benchmark 驗證圖的品質指標
  • - [ ] 能解釋 semantic cache 的 key 為什麼要含 prompt fingerprint,以及改 extraction-spec 後快取自動失效的機制