跳转至

设计:增量更新与实体去重

日期: 2026-05-04 Issue: #698(增量更新)、实体去重(无 issue,主动行为) 分支: v7


问题

  1. graphify extract 每次运行都会从头重建完整图——无论什么文件变化,都会将所有文件重新发送给 LLM。对于每天更新的 1000 个 Markdown 文件语料库来说,这代价高昂。
  2. LLM 提取是逐 chunk 进行的——同一个真实世界概念在不同 chunk 中可能得到不同标签(AuthManager、AuthenticationManager、auth_mgr)。除精确字符串归一化外,没有语义去重。

处理流程(每次 graphify extract 运行){#pipeline-every-graphify-extract-run}

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

功能 1:增量更新

自动检测

如果 graphify-out/manifest.json 和 graphify-out/graph.json 都存在 → 进入增量模式。无需指定 flag。首次运行始终为完整模式。

增量模式变更

  • 使用 detect_incremental(target) 替代 detect(target)——返回 new_files、unchanged_files、deleted_files
  • 只有 new_files 会经过 AST + LLM 提取
  • 使用 build_merge(new_chunks, prune_sources=deleted_files) 替代 build_from_json——合并到现有图,并剪除已删除文件对应的节点
  • 只有在成功完成后才写入 manifest.json(运行中途崩溃不会破坏下一次运行的 diff)

语义缓存(完整与增量模式){#semantic-cache-both-full-and-incremental-mode}

  • 调用 LLM 前:check_semantic_cache(files) 将文件分为 (cached_results, uncached_files)
  • 只有 uncached_files 被发送给 extract_corpus_parallel
  • LLM 调用后:save_semantic_cache(fresh_results)——以内容哈希作为键
  • 文件重命名时:缓存结果中的 source_file 会更新为新路径(与现有 AST 缓存相同模式)

输出摘要

[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__.py——仅 elif cmd == "extract": 代码块(约 5 处定向修改)

功能 2:实体去重

新模块:graphify/dedup.py

单一职责。在图构建完成后由 build.py 调用。返回去重后的 (nodes, edges)。

处理流程

步骤 1 — 精确归一化

启用 build.py 中休眠的 deduplicate_by_label。可捕获跨文件的大小写/标点变体。这是零成本的现成收益。

步骤 2 — 熵门控

将模糊匹配限制在熵 ≥ 2.5 bits/char 的标签。短而有歧义的名称("AI"、"DB"、"x")自动合并风险过高。只有高熵标签进入步骤 3-4。

步骤 3 — MinHash + LSH 分块(datasketch) 3-gram shingles,128 个 permutation,阈值 0.7。以 O(n) 而非 O(n²) 生成候选对。在 10k 节点上亚秒级完成。

步骤 4 — Jaro-Winkler 验证(rapidfuzz) 每个候选对需要 ≥ 0.92 才能通过。可捕获拼写错误、复数形式、空格变体。低于阈值的候选对被丢弃。

步骤 5 — 同社区加权 两个节点共享同一 Leiden 社区 ID 时,得分 +0.05。这是 Graphify 特有的优势——社区结构是 GraphRAG/LightRAG 未利用的强信号。

步骤 6 — 并查集合并 确认后的候选对送入并查集 → 连通分量 → 每个分量合并为一个节点。边重新连接到保留节点。丢弃自环。优先选择不带 chunk 后缀的较短 ID 作为保留节点。

步骤 7 — 可选 LLM 决胜(--dedup-llm flag) 模糊对(得分 0.75–0.85)按每批 30 个分组,每批一次 LLM 调用。10k 节点总计约 $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

新依赖

  • datasketch——始终必需(加入 [project.dependencies])
  • rapidfuzz——始终必需(加入 [project.dependencies])
  • 不引入 sentence-transformers / PyTorch 依赖

变更文件

  • graphify/dedup.py——新模块,完整处理流程
  • graphify/build.py——在图构建后调用 deduplicate_entities;启用休眠的 deduplicate_by_label
  • graphify/__main__.py——在 extract 块中增加 --dedup-llm flag 解析
  • pyproject.toml——将 datasketch、rapidfuzz 加入基础依赖

测试

  • 针对每个去重步骤的独立单元测试(tests/test_dedup.py)
  • 集成测试:两个 chunk 含重叠实体标签 → 输出图中合并为单一节点
  • 增量测试:运行两次 extract,断言第二次对未变更文件零 LLM 调用
  • 重命名测试:重命名文件,断言缓存命中且 source_file 正确更新
  • 删除测试:删除文件,断言其节点从图中剪除

非目标

  • --dedup embed(MiniLM 余弦相似度)——明确排除,不引入 PyTorch 依赖
  • 对 graphify update(仅 AST)的增量支持——已有现有 AST 缓存处理
  • 跨多个 graph.json 文件的去重(合并两个图)——独立功能

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 graph/graphify