首页
/ graphify 增量更新(--update)与仅重聚类(--cluster-only)实战指南

graphify 增量更新(--update)与仅重聚类(--cluster-only)实战指南

2026-09-07 18:37:39作者:裴锟轩Denise

本指南讲解 graphify 的两条高频增量管线:--update 增量重提取,以及 --cluster-only 在既有图上重跑社区发现。读完你将掌握如何只对变更文件做 AST 重提取与合并、正确处理删除文件与语义文件重试,以及如何用 graphify cluster-only . 一步刷新报告与图谱文件。文中全部命令与 API 调用均以仓库内实际实现为准,可直接复制运行。

适用场景定位:参考文档 update.md 源文件 是面向 Agent(如 Claude Code、Kiro、Codex、Gemini CLI 等)的操作手册,仅在用户传入 --update--cluster-only 时才需要加载;首次全量构建不读该文件。它默认已经存在由全量构建产生的 graphify-out/(含 graph.jsonmanifest.json),即“上一次运行”的基线。

一、两条命令与它们的定位

graphify 的全量流程通常分为多个步骤(文件扫描 → AST 提取 → 语义提取 → 聚类 → 报告)。在已经产出过一次 graphify-out/ 之后,有两类高频场景:

场景 命令/触发词 做什么 与全量流程的关系
增量更新 --update 只对新增/修改过的文件重做提取,删除的文件被裁剪,随后合并进既有图 跳过“未变文件”,仅重跑变更文件相关步骤
仅重聚类 --cluster-only(子命令) 忽略文件内容,在已有 graph.json 上重新聚类、命名社区并重新生成产物 跳过提取,只做图谱后处理

两种模式都围绕 graphify-out/ 下的中间文件协作:manifest.json(文件指纹清单)、.graphify_detect.json(本次扫描结果)、.graphify_incremental.json(增量差异)、.graphify_extract.json(提取结果)、graph.json(合并后的图)。理解这些文件关系是读懂后文脚本的前提。

二、--update 增量重提取:整体流程

增量更新的核心哲学是“能省则省”:只有内容真的变了,才为它付出提取成本。

  1. 调用 detect_incremental() 计算“相对上次清单”的变更集,写出 .graphify_incremental.json
  2. 把变更集翻译成后续步骤能直接消费的 .graphify_detect.jsonfiles 只含变更文件、all_files 仍保留全量语料);
  3. 根据“是否只改了代码文件”分流:
    • 全部是代码文件 → 只跑 Step 3A(AST),完全跳过 Step 3B(无需 LLM/子代理);
    • 存在文档/论文/图片/视频 → 视频先走 references/transcribe.md(Step 2.5),再全量跑 3A–3C;
    • 只有删除、没有新增 → 生成空提取结果,让合并步骤做裁剪;
  4. build_merge() 把新提取结果与磁盘上的 graph.json 合并(含删除文件裁剪与变更文件替换),合并结果写回 .graphify_extract.json
  5. 保存 manifest,供下一次 --update 作差异基线;
  6. 按常规流程继续执行 Step 4 之后的图谱处理,并在 Step 4 后展示图差异。

三、计算增量差异:detect_incremental 与结果落盘

增量差异的判定实现在 detect.pydetect_incremental(root, ...)。它每次都会先做一次完整 detect() 扫描,再把扫描结果与 manifest.json(路径默认 graphify-out/manifest.json)逐文件比对:

  • mtime 未变且指纹匹配 → 未变(免费快速路径,仅 stat);
  • mtime 变化 → 用 MD5 内容指纹二次确认,避免“只改时间戳不改内容”误报;
  • 出现在 manifest、磁盘上已不存在 → 判定为 deleted_files(真正的删除);磁盘上仍在但已不在扫描范围 → 归入 excluded_files(被忽略规则排除,不能当删除)。
  • 首次运行(无 manifest)会把全部文件当作新增。

关于 mtime 有一个值得注意的细节:源码在 detect.py 处理了“mtime 隐藏同一次重写”的竞态——若 manifest 写入与文件编辑落在同一时间戳刻度内,mtime 无法证明内容未变,此时仍会对该文件补算一次 MD5。manifest 中每行记录的字段为 mtimeast_hashsemantic_hashseen,分别由 graphify update(AST 通道)与 graphify extract(语义通道)盖戳。

参考文档给出的调用片段如下,结果同时打印并写入 graphify-out/.graphify_incremental.json

$(cat graphify-out/.graphify_python) -c "
import sys, json
from graphify.detect import detect_incremental, save_manifest
from pathlib import Path

result = detect_incremental(Path('INPUT_PATH'))
new_total = result.get('new_total', 0)
print(json.dumps(result, indent=2, ensure_ascii=False))
Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding='utf-8')
deleted = list(result.get('deleted_files', []))
if new_total == 0 and not deleted:
    print('No files changed since last run. Nothing to update.')
    raise SystemExit(0)
if deleted:
    print(f'{len(deleted)} deleted file(s) to prune.')
if new_total > 0:
    print(f'{new_total} new/changed file(s) to re-extract.')
"

两点说明:$(cat graphify-out/.graphify_python) 从 graphify 自身输出目录读取本次构建所用的解释器路径,保证与安装 graphify 的 Python 环境一致;INPUT_PATH 是占位符,需替换为实际扫描根目录。

detect_incremental 的返回字典字段(与 detect.py 的写入逻辑对应)如下:

字段 含义
files 本次全量扫描按类型分组的文件表(code/document/paper/image/video
new_files 仅新增/修改文件,按类型分组
unchanged_files 未变文件,按类型分组
new_total 新增/修改文件总数
deleted_files 磁盘上已删除(需裁剪)
excluded_files 仍存活但已移出扫描范围(不算删除)
skipped_sensitive 因疑似敏感内容被跳过的文件

四、把增量结果翻译给后续步骤:.graphify_detect.json

文档特别强调:后续“Step 3A–Step 6”读取 .graphify_detect.json 时不加条件,因此必须为增量运行写入正确的状态。要点是字段分工:

  • files 携带变更子集——驱动 Step 3A 的 AST 与 Step 3B0 的缓存检查只针对变了的部分;
  • all_files 携带全量语料——供需要“全语料上下文”的步骤使用。
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding='utf-8'))
Path('graphify-out/.graphify_detect.json').write_text(json.dumps({
    'files': r.get('new_files', {}),
    'all_files': r.get('files', {}),
    'total_files': r.get('new_total', 0),
    'total_words': r.get('total_words', 0),
    'skipped_sensitive': r.get('skipped_sensitive', []),
    'needs_graph': True,
}, ensure_ascii=False), encoding='utf-8')
"

注意 total_files 使用的是 new_total(本次变更数)而非全量数,这是有意为之:让下游“按文件数估算成本”的逻辑只看到本轮真正要做的工作。

五、分流决策:代码-only 与含语义文件

5.1 先判定是否全部为代码文件

$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path

result = json.loads(open('graphify-out/.graphify_incremental.json', encoding='utf-8').read()) if Path('graphify-out/.graphify_incremental.json').exists() else {}
code_exts = {'.py','.ts','.js','.go','.rs','.java','.cpp','.c','.rb','.swift','.kt','.cs','.scala','.php','.cc','.cxx','.hpp','.h','.kts','.lua','.toc','.f','.F','.f90','.F90','.f95','.F95','.f03','.F03','.f08','.F08'}
new_files = result.get('new_files', {})
all_changed = [f for files in new_files.values() for f in files]
code_only = all(Path(f).suffix.lower() in code_exts for f in all_changed)
print('code_only:', code_only)
"

上表所举的扩展名只是示例集合;仓库实际维护了更完整的分类型扩展表(代码、文档、PDF、图片、Office、音视频),见 detect.pyCODE_EXTENSIONS / DOC_EXTENSIONS / PAPER_EXTENSIONS / IMAGE_EXTENSIONS / OFFICE_EXTENSIONS / VIDEO_EXTENSIONS。文件类型最终由 classify_file()detect.py)统一裁决,其中还包含包清单走 AST、.blade.php 复合扩展、shebang 识别扩展名缺失脚本、PDF 位于 Xcode 资源目录内时降级等细节。

5.2 两个分支

分支 A:code_only 为 True(纯代码变更) 这是最省钱的路径:打印提示 [graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed),只对变更文件运行 Step 3A(AST 确定性提取),完全跳过 Step 3B(无子代理、无 LLM 调用),随后直接进入合并与 Step 4–8。由于代码符号提取是确定性的本地 AST 解析,图在秒级内即可完成刷新。

分支 B:code_only 为 False(含文档/论文/图片/视频)

  • new_files['video'] 非空,必须对这批文件执行 references/transcribe.md(Step 2.5)完成转写,再把转写产出的文本路径改写进 .graphify_detect.jsonfiles['document'] 并删除 files['video']——否则裸 .mp4/.mp3 路径会被当成不可读媒体喂给语义子代理;
  • 随后按正常流程执行完整的 Step 3A–3C。

5.3 只有删除:写空提取结果

new_total == 0 且存在删除,.graphify_extract.json 可能尚未创建,而合并步骤需要它才能执行裁剪,因此要补一个空提取:

if [ ! -f graphify-out/.graphify_extract.json ]; then
    echo '[graphify update] Only deletions -- creating empty extraction for merge.'
    $(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8')
"
fi

六、合并:build_merge 的替换式合并与删除裁剪

增量更新正确性的关键在合并步骤。参考文档的合并脚本较长,但每一段都有对应源码依据。先看完整脚本,再分段讲解:

$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
from graphify.build import build_merge
from graphify.detect import save_manifest

# Load new extraction and incremental state
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding='utf-8'))
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding='utf-8'))
deleted = list(incremental.get('deleted_files', []))
prune = list(deleted) or None

G = build_merge(
    [new_extraction],
    graph_path='graphify-out/graph.json',
    prune_sources=prune,
    root='INPUT_PATH',
    directed=IS_DIRECTED,
)
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')

merged_out = {
    'nodes': [{'id': n, **d} for n, d in G.nodes(data=True)],
    'edges': [
        {**{k: val for k, val in d.items() if k not in ('_src', '_tgt', 'source', 'target')},
         'source': d.get('_src', u), 'target': d.get('_tgt', v)}
        for u, v, d in G.edges(data=True)
    ],
    'hyperedges': list(G.graph.get('hyperedges', [])),
    'input_tokens': new_extraction.get('input_tokens', 0),
    'output_tokens': new_extraction.get('output_tokens', 0),
}
Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged_out, ensure_ascii=False), encoding='utf-8')
print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"])} nodes, {len(merged_out[\"edges\"])} edges)')

from graphify.cli import _stamped_manifest_files
_manifest_files = _stamped_manifest_files(incremental['files'], new_extraction, Path('INPUT_PATH'))
_sem_types = ('document', 'paper', 'image')
_dispatched = {f for t, fl in incremental.get('new_files', {}).items() if t in _sem_types for f in fl}
_stamped = {f for fl in _manifest_files.values() for f in fl}
_cleared = _dispatched - _stamped
_scan = {f for fl in incremental['files'].values() for f in fl}
save_manifest(_manifest_files, root='INPUT_PATH', scan_corpus=_scan, clear_semantic=_cleared or None)
print('[graphify update] Manifest saved.')
"

IS_DIRECTED 需要按实际调用替换:传入 --directed 时为 True,否则为 False。)

6.1 prune_sources 只收“真删除”

build_merge 的签名见 build.py。文档给出两条铁律:

  • prune_sources 只能放真正从磁盘删除的文件。变更/重提取文件的旧节点不靠它清理;
  • 绝不把 changed 放进 prune:一旦传入 root=,prune 路径会相对化到与刚合并节点相同的基准,会把刚重提取的内容一并删掉。变更文件的“旧节点替换”由 build_merge 自身的 replace-on-re-extract 机制负责(见下节),而不是去重(dedup)通道。

这背后的源码逻辑位于 build.py:凡在 new_chunks 中出现的 source_file,会先从既有图基准中按“AST 层/语义层”剔除旧节点/边再合入新数据,从而保证变更文件不会累积陈旧节点;真正删除的文件才通过 prune_sources 做不分层级的裁剪,且裁剪同时匹配原始绝对路径与相对化形式。

6.2 为什么用 build_merge 而不是 NetworkX 往返

build_merge 直接读 graph.json,不经 NetworkX 序列化往返,因此边方向(calls / implements / imports)始终被保留(仓库注释 #801)。同时需要传 root=,使来自 detect_incremental 的绝对路径删除项被相对化,与图中相对 source_file 对齐;缺失 root 会导致删除项永远匹配不上、陈旧节点每次更新都累积(#1361)。

directed=IS_DIRECTED 同样关键:--directed --update 若不显式传方向,合并会静默退化为无向图并折叠互反的 A↔B 边。源码对 directed=None 的兜底是继承磁盘上既有图的 directed 标志(见 build.py),但显式传值永远最安全。

6.3 合并结果写回与超边保留

合并得到的图(NetworkX G)被序列化回 .graphify_extract.json,供后续 Step 4 读取完整图:

  • 边属性显式取 _src/_tgt 并以 source/target 字段覆盖输出,防止陈旧属性残留在 d 中;
  • 超边(hyperedges)取自 G.graph['hyperedges'],因为 build_merge 已把旧图与新提取两边的超边合并到一起;若回退只取 new_extraction,会静默丢掉此前运行的超边(#801)。

6.4 写回 manifest:为下一次 diff 立基线

增量自洽的最后一步是保存 manifest。若不更新基线,下一次 --update 会拿“上一次”的旧状态做差异,产生幽灵节点报告。此段三个要点:

  1. root= 必须与 build_merge 一致:manifest 键以扫描根为基准的相对路径落盘,跨克隆/跨机器可移植(#1417)。这也与 save_manifest 的实现对应——传 root 时键先 NFC 归一化再转相对路径存储。
  2. 只盖戳真正产出语义输出的文件_stamped_manifest_filescli.py)会筛掉“分块失败/被省略”的语义文件——只有 nodes/hyperedges 中存在其 source_file 的文件才算有效输出。一个 chunk 失败的文档必须保持未盖戳,否则会被标记为完成、内容永久丢失(#2015/#933)。
  3. clear_semantic 清掉陈旧指纹:本次派发但未盖戳的语义文件(chunk 失败或被省略)要清空其历史 semantic_hash,强制下次重新排队(#1948);scan_corpus 传的是原始全量语料,使自上次运行起新被排除的 in-root 文件被正确丢弃而不是伪装成删除,未触碰的行则完整保留(#1908)。

save_manifest 的行记录由 mtimeseenast_hashsemantic_hash 组成(detect.py),并以原子写落盘避免截断(detect.py)。若序列化结果与磁盘一致还会跳过重写,减少无效 IO。

七、合并之后:展示图差异

合并完成后按常规执行 Step 4–8,并在 Step 4 后展示本轮图差异。做法是:合并前先把旧图备份.graphify_old.jsoncp graphify-out/graph.json graphify-out/.graphify_old.json),合并完成后用 graph_diff 对比新旧两图:

$(cat graphify-out/.graphify_python) -c "
import json
from graphify.analyze import graph_diff
from graphify.build import build_from_json
from networkx.readwrite import json_graph
import networkx as nx
from pathlib import Path

old_data = json.loads(Path('graphify-out/.graphify_old.json').read_text(encoding='utf-8')) if Path('graphify-out/.graphify_old.json').exists() else None
new_extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding='utf-8'))
G_new = build_from_json(new_extract, directed=IS_DIRECTED)

if old_data:
    G_old = json_graph.node_link_graph(old_data, edges='links')
    diff = graph_diff(G_old, G_new)
    print(diff['summary'])
    if diff['new_nodes']:
        print('New nodes:', ', '.join(n['label'] for n in diff['new_nodes'][:5]))
    if diff['new_edges']:
        print('New edges:', len(diff['new_edges']))
"

graph_diffanalyze.py)返回结构化的新增/删除节点与边,以及一句 summary(如 "3 new nodes, 5 new edges, 1 node removed");边比较时会区分有向与无向(无向图对 (u,v) 归一化),有向关系保留方向。.graphify_old.json 用完后清理:rm -f graphify-out/.graphify_old.json

八、--cluster-only:在既有图上重聚类

--cluster-only 是图构建完成后的独立后处理。命令形态如下(也可用子命令形态 graphify cluster-only .):

graphify cluster-only .

该命令自包含:它直接基于现有图重新聚类、命名社区,并从既有图重新生成 GRAPH_REPORT.mdgraph.jsongraph.html。在 CLI 层面对应的实现位于 cli.pycluster-onlylabel 命令共用入口,区别在于 label 总是重生成社区名),它会基于 .graphify_labels.json 等持久化数据在已有图之上工作(见 cluster.py 附近对“供后续 cluster-only 判读”的说明)。

使用上的关键纪律是:不要在 --cluster-only 后重跑 Step 5–9。这些步骤会读取 .graphify_extract.json.graphify_detect.json.graphify_analysis.json 等中间文件,而上次构建的 Step 9 清理早已删除它们,重跑必然抛出 FileNotFoundError。命令完成后,照常向用户展示刷新后的 GRAPH_REPORT.md 摘要即可。

九、何时该用增量管线:决策速查

情形 推荐做法 原因
上次全量构建后改了几个代码文件 --update(纯代码分支) 只跑 AST,零 LLM 开销,秒级刷新
新加入/修改了文档、PDF、图片 --update(语义分支) 自动按需触发语义提取,视频先转写
删除了一批文件 --update 空提取 + prune_sources 裁剪幽灵节点
只改了聚类/命名参数,想重看社区划分 graphify cluster-only . 自包含,不触碰提取阶段
从未构建过 全量构建 增量依赖既有 manifest.jsongraph.json 基线

一条贯穿始终的提醒:增量管线依赖“上次的 manifest 基线”,而 manifest 必须相对扫描根存储(root=)才具备跨机器可移植性。若基线缺失或格式不兼容(如纯 float mtime 的旧格式),detect_incremental 会按“全部视为新增”处理——宁可多做,不可漏做。

十、源码速查

  • update.md 参考文档:本文的权威来源,Kiro 平台技能同款(各平台副本位于 graphify/skills 下的 kiro/claude/codex/opencode/ 等目录,以及 skill-agents.md 等以 .md 形式嵌入的技能)
  • detect.py:文件分类、敏感文件过滤、manifest 读写与 detect_incremental 增量判定(save_manifest 见第 2112 行附近,detect_incremental 见第 2366 行附近)
  • build.pybuild_merge 替换式合并与删除裁剪(第 1626 行附近)、build_from_json(第 798 行附近)
  • cli.py_stamped_manifest_files 语义盖戳过滤(第 88 行附近)、cluster-only/label 命令入口(第 1843 行附近)
  • analyze.pygraph_diff 新旧图差异计算(第 556 行附近)
  • tests:增量行为有专门的回归覆盖,例如 test_incremental.pytest_incremental_mtime_collision.py,以及 test_dedup_remaps_hyperedges.pytest_hyperedge_roundtrip.py 等对合并/超边保持的验证
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388