资讯动态

graphify `--update` 与 `--cluster-only`:知识图谱增量重建与社区重聚类的完整操作手册

发布时间:2026/9/7 2:26:44 来源:尧图企业网站定制
graphify--update与--cluster-only知识图谱增量重建与社区重聚类的完整操作手册【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify这篇指南面向 Trae 等 AI 编码 Agent 的 graphify 技能使用者围绕 Trae 平台的 update 参考文档 展开完整讲解两条非全量重建的执行路径--update增量重抽取只处理新增/变更文件省 token 省时间与--cluster-only在既有图之上仅重跑社区聚类。读完你会掌握如何在已有graphify-out/产物的项目上只重抽变更文件、如何区分纯代码变更快路径与文档/音视频语义路径、如何安全地把增量抽取结果合并回既有graph.json含删除剪枝、重抽取替换、超边保留与有向图保真以及只重聚类时哪些步骤绝不能重跑。何时才需要这份参考update.md在技能体系中的定位非常明确——只有用户显式传入--update或--cluster-only时才加载它首次全量构建永远不会读取该文件。原因在于首次构建由 graphify/skill-trae.md 主流程的 Steps 1–5 完成先检测语料再做 AST 结构抽取Step 3A与语义抽取Step 3B再构建、聚类、导出。而增量更新与仅重聚类都建立在上一次运行已经生成graph.json、GRAPH_REPORT.md、manifest 等中间产物的前提上。因此阅读本手册前请先确认工作目录中存在完整的上一轮产物尤其是graphify-out/graph.json—— 既有知识图谱本体节点的source_file通常是相对扫描根目录的路径graphify-out/.graphify_python—— 上一轮解析出的可用 Python 解释器路径uv tool / pipx / venv / 系统 Python 均可所有命令均用$(cat graphify-out/.graphify_python)调用以保证解释器一致.graphify_detect.json、.graphify_extract.json等.graphify_*中间状态文件注意全量构建的收尾步骤会清理它们这正是--cluster-only后不能重跑 Steps 5–9 的原因详见后文。--update只重抽新增/变更文件的增量流程当用户自上次运行以来新增或修改了文件时使用--update。核心思想是只重新抽取变更过的文件从而节省 token 与时间。整个流程在逻辑上分七个阶段下面逐一展开。阶段 1调用detect_incremental得到变更集合首先用 Python 调用 graphify/detect.py 中的detect_incremental把结果同时打印出来并落盘为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, indent2, ensure_asciiFalse)) Path(graphify-out/.graphify_incremental.json).write_text(json.dumps(result, ensure_asciiFalse), 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.) 从源码看detect_incremental的判定逻辑很有讲究见 detect.py#L2366-L2418它先跑一次完整detect()扫描再与上次保存的 manifest 比对。判断是否变更分两条 hash 轨道——kindast比较ast_hashgraphify update用kindsemantic比较semantic_hashgraphify extract用。为了不每次全量做磁盘 IO它还带快路径优化mtime 未变 hash 匹配即视为未变更仅一次 stat零磁盘 IOmtime 变动才走慢路径比对 MD5detect.py#L2386-L2388。detect_incremental返回的关键字段即上述脚本所消费的字段含义new_files按文件类型分组如document、video、image、代码各类的新增/变更文件清单new_total需要重抽取的文件总数deleted_files磁盘上已消失、需要从图中剪枝的真删除文件excluded_files仍存活于磁盘但已被 ignore 规则 /--exclude排除、不得当作删除的文件files/unchanged_files完整语料 / 未变更文件供需要全语料上下文的步骤使用脚本开头的提前退出很重要若new_total 0且无删除文件直接打印 Nothing to update 并以SystemExit(0)干净退出——这一步为零成本不会触发任何重抽取或合并。值得注意的健壮性细节源码注释可印证manifest 旧格式只存浮点 mtime 时用!而非比较因此git checkout旧提交、tarball 还原、rsync --times造成的 mtime 倒退仍会触发重抽取避免图与磁盘内容漂移detect.py#L2432-L2438manifest 键按 NFC 归一化防止 macOS NFD 文件名漏检。阶段 2回填.graphify_detect.json让下游步骤看到增量状态Steps 3A–6结构抽取、语义抽取、合并、聚类、分析、导出会无条件读取.graphify_detect.json因此必须把增量结果重写为它们期望的形态。这里的双字段设计是精髓files—— 只装变更子集驱动 Step 3AAST 结构抽取与 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_asciiFalse), encoding\utf-8\) 阶段 3判断纯代码变更并走零 LLM 快路径在跑任何语义抽取之前先检查所有变更文件是否都是代码文件。这一步决定了能否跳过最昂贵的语义子代理环节。脚本内维护了一份硬编码的代码扩展名集合涵盖 30 余种语言$(cat graphify-out/.graphify_python) -c import json from pathlib import Path result json.loads(open(graphify-out/.graphify_incremental.json, encodingutf-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) 分流的操作指令如下code_only为 True打印[graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed)只对变更文件跑 Step 3AAST 结构抽取完全跳过 Step 3B语义子代理不需要 LLM随后直接进入合并与 Steps 4–8。这正是代码改动零 token 成本刷图的机制AST 抽取是确定性的本地解析天然不需要大模型。code_only为 False任一变更文件是文档 / 论文 / 图片 / 视频进入完整 Steps 3A–3C 管线。在代码路径下还可对照 CLIgraphify update见 cli.py#L2251-L2309无参数时会从graphify-out/.graphify_root恢复上次全量构建保存的扫描根找不到才回退到.然后调用graphify.watch._rebuild_code做无 LLM 的代码重抽。CLI 打印的提示语与技能手册互为印证Code graph updated. For doc/paper/image changes run /graphify --update in your AI assistant.——即命令行只能覆盖代码文件文档/论文/图片的语义重抽必须回到 Agent 技能流程。阶段 4处理音视频变更Step 2.5 转录避免把媒体文件喂给语义子代理若code_only为 False且变更文件中存在new_files[video]则必须先对这些文件执行 transcribe 参考文档 中的 Step 2.5视频/音频转录然后重写.graphify_detect.json把转录产物路径并入files[document]并删除files[video]。文档注释明确给出了不这么做的后果否则原始.mp4/.mp3路径会被当作文档直接喂给语义子代理而子代理读不了媒体文件对应 issue 上下文 #1392。这保证了语义层永远只接触可读文本。阶段 5仅删除场景——构造空抽取让合并步骤执行剪枝如果没有任何新文件只有删除就不存在新抽取内容。此时合并步骤仍需一个可用的抽取文件才能执行删除剪枝因此需要显式构造一个空的 extractionif [ ! -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}), encodingutf-8) fi注意这里用if [ ! -f ... ]守卫只有当抽取文件不存在时才创建避免覆盖真实例如来自仅删除前的小规模重抽内容。阶段 6build_merge合并——剪枝只针对真删除、重抽取走替换、方向必须显式这是整个增量更新的核心。它读取新的抽取结果与增量状态调用 graphify/build.py 的build_merge把新内容并入既有graph.json并把合并结果写回.graphify_extract.json使 Step 4 能看到完整图$(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_sources is ONLY for genuinely DELETED files. Changed/re-extracted files are # handled by build_merges replace-on-re-extract (#1344): every source_file in # new_chunks is dropped from the base before merge, so old/stale nodes dont survive. # Do NOT add changed here: with root passed, prune_set relativizes to the same base # as the freshly merged nodes and would DELETE the re-extracted content (#1178 is moot # now that replace — not the dedup pass — reconciles changed files). prune list(deleted) or None # Use build_merge() — reads graph.json directly without NetworkX round-trip # so edge direction (calls, implements, imports) is always preserved (#801). # Pass root so prune_sources (absolute paths from detect_incremental) are # relativized to match the graphs relative source_file values; without it # nothing is pruned and stale nodes accumulate on every update (#1361). # directedIS_DIRECTED: replace IS_DIRECTED with True if --directed was given, else # False. Without it a --directed --update silently rebuilds undirected and collapses # reciprocal A-B edges (#1392). G build_merge( [new_extraction], graph_pathgraphify-out/graph.json, prune_sourcesprune, rootINPUT_PATH, directedIS_DIRECTED, ) print(f[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges) # Write merged result back to .graphify_extract.json so Step 4 sees the full graph merged_out { nodes: [{id: n, **d} for n, d in G.nodes(dataTrue)], edges: [ # Explicit source/target last so they win over any stale attrs in d. {**{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(dataTrue) ], # G.graph[\hyperedges\] holds hyperedges from both existing graph.json # and new_extraction (build_merge combines them). Falling back to # new_extraction only would silently drop prior-run hyperedges (#801). 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_asciiFalse), encoding\utf-8\) print(f[graphify update] Merged extraction written ({len(merged_out[\nodes\])} nodes, {len(merged_out[\edges\])} edges)) # Save manifest so next --update diffs against todays state, not the # prior runs baseline (prevents ghost-node reports on subsequent updates). # root matches the build_merge call above so the manifest keys stay relative to # the scan root — portable across clones/machines, so --update keeps matching # cached files instead of missing every one after a move (#1417). # # Only stamp semantic files (docs/papers/images) that ACTUALLY produced output # THIS run (new_extraction is this runs fresh extraction, read above before the # merge overwrote the file): a changed doc whose chunk failed must stay unstamped # so the next --update re-queues it, otherwise it is marked done and its content # is lost forever (#2015). Mirrors the library extract path # (cli._stamped_manifest_files clear_semantic scan_corpus). from graphify.cli import _stamped_manifest_files _manifest_files _stamped_manifest_files(incremental[files], new_extraction, Path(INPUT_PATH)) # Changed semantic files dispatched this run but NOT stamped had their chunk fail # or be omitted; clear any stale semantic_hash so they are re-queued (#1948). _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_corpus the RAW full corpus so in-root files newly excluded since last run # are dropped rather than masquerading as deletions; untouched rows preserved (#1908). _scan {f for fl in incremental[files].values() for f in fl} save_manifest(_manifest_files, rootINPUT_PATH, scan_corpus_scan, clear_semantic_cleared or None) print([graphify update] Manifest saved.) 这段脚本浓缩了增量合并最容易被忽视的四个语义陷阱值得逐个吃透陷阱一prune_sources只能装真删除文件绝不能装变更文件。变更重抽取文件由build_merge的 replace-on-re-extract 机制负责new_chunks中出现的每个source_file在合并前会先被从基线图里整体剔除build.py#L1685-L1734旧节点/旧边不会残留也不依赖 dedup 去兜底。若把changed混进prune在传入root的情况下剪枝集合与刚合并的新节点同基会把刚重抽出来的新内容也剪掉。源码里更细致替换是按 tier 拆分的AST tier 与 semantic tier 各自独立一次只重抽一个 tier 时另一个 tier 的既有贡献会被原样保留build.py#L1694-L1714避免仅语义重抽却把该文件的 AST 节点删光。陷阱二必须传rootINPUT_PATH。detect_incremental返回的删除路径是绝对路径而图内节点的source_file是相对扫描根的值root让剪枝路径与存储键同基。注释直白地警示不传rootnothing is pruned and stale nodes accumulate on every update剪枝全部落空陈旧节点每次更新都在堆积#1361。即使调用方省略rootbuild_merge也会回退用图上记录的 scan root 推断有效根绝对路径与相对键仍能对齐build.py#L1673-L1683当任何剪枝条目在存储键中零命中时还会尝试通过后缀匹配重新推导根build.py#L1776-L1796。陷阱三directedIS_DIRECTED必须与当初构建的图一致。如果不显式传入一个原本--directed的图在--update时会静默退化为无向重建A→B与B→A的互反边会被折叠丢失。源码的默认策略更稳妥directedNone时继承磁盘上既有图的 directed 标志build.py#L1650-L1671。脚本里要求在跑--update时手动替换IS_DIRECTED占位符本质上就是让 Agent 按用户本次是否给了--directed显式表态。陷阱四超边hyperedges必须从合并后的图对象上取。build_merge会同时保留既有graph.json与新抽取里的超边并做 id 去重若回退只取new_extraction的超边上一次运行沉淀下来的超边会整体消失#801。源码中未重抽未删除文件的超边会被显式carry进新图build.py#L1798-L1825重抽文件的旧超边随 replace 丢弃、其新版本已含于新块。随后是 manifest 落盘。关键点是只给本轮确实产出了输出的语义文件盖章_stamped_manifest_files见 cli.py#L88-L157只会把sem_result中出现过source_file的文档/论文/图片文件算作已提取节点与超边都算有效输出纯边结果不算AST 失败或零节点文件也排除——这些文件保持未盖章下一次--update才会重新入队否则一次失败的内容会被永久标记为 done 而丢失#2015/#2543。clear_semantic处理本轮已派发但未盖章的文件#1948这类文件上一轮残留的semantic_hash会被清空避免detect_incremental误判为未变更。scan_corpus传的是原始完整语料不是盖章过滤后的子集自上次运行以来新被 exclude 的 in-root 文件会从 manifest 中删除而不是永远伪装成删除文件#1908未触碰文件的旧行则完整保留。阶段 7在合并图上继续 Steps 4–8并展示图差异合并完成后按主流程在合并后的全量图上继续跑 Steps 4–8构建/聚类/分析/导出/report。Step 4 之后建议向用户展示一次图差异摘要。做法是在合并之前先备份旧图合并后再用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 # Load old graph (before update) from backup written before merge 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, directedIS_DIRECTED) if old_data: G_old json_graph.node_link_graph(old_data, edgeslinks) 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])) 配套的两条 shell 指令必须按序执行合并步骤前先cp graphify-out/graph.json graphify-out/.graphify_old.json保存旧图用完后清理rm -f graphify-out/.graphify_old.json避免残留下一次把旧快照误读为本次变更前状态。graph_diff的实现位于 analyze.py#L556-L637返回结构化差异new_nodes/removed_nodes含 label、new_edges/removed_edges含 relation 与 confidence以及人类可读的summary字符串如 3 new nodes, 5 new edges, 1 node removed无变化时为 no changes。注意它用带 relation 的键有向图用(u, v, relation)无向图对端点排序做差集因此语义上是边关系级别的差异而非单纯端点差异。--cluster-only在既有图上仅重聚类当用户只改了聚类相关诉求如想看不同的社区划分结果不需要动抽取层时使用--cluster-only。它跳过 Steps 1–3探测/抽取直接在既有图上重跑聚类graphify cluster-only .该命令是完全自包含的它会重新聚类、命名社区并基于既有图重新生成GRAPH_REPORT.md、graph.json与graph.html。实现层面cluster-only与label命令同源cli.py#L1843 一带label是总是重新生成社区名的cluster-only从测试看它覆盖了输出目录缺失时自动创建、graph.json在graphify-out/时相邻写入、保留 analysis sidecar、把新社区 id 映射回上一轮的 cid、写被拒时报错、以及非 git 仓库 cwd 下保留built_at_commit等大量边界见 test_cli_export.py 中test_cluster_only_*系列用例。务必遵守的禁令不要重跑 Steps 5–9。文档措辞非常严厉——这些步骤读取的是.graphify_extract.json、.graphify_detect.json、.graphify_analysis.json这类中间文件而上一轮全量构建的收尾Step 9已经把它们删掉了因此重跑会直接抛FileNotFoundError#1392。cluster-only命令本身已经处理完聚类、命名与三件套输出结束后只需像平时一样展示刷新后的GRAPH_REPORT.md摘要即可不需要也不应该再叠加任何后续步骤。与整份参考体系的关系这份update.md是 Trae 技能参考集的一员。它的兄弟参考文档彼此咬合构成了完整的图生命周期首次全量构建由 skill-trae.md 主流程Steps 1–9驱动其## Usage一节列出了全部子命令含--update、--cluster-only、--directed、--watch、query、path、explain等更新后的问答读 query 参考执行graphify query/path/explain并把 QA 用graphify save-result写回图形成下一次--update会把它抽取为图节点的自改进闭环变更文件含音视频时走 transcribe 参考 的 Step 2.5 转录。从主流程skill-trae.md 的--update分支同样指向 Seereferences/update.md可以看出无论运行在 Trae 还是其他 Agent 平台增量更新与仅重聚类这两条路径都遵循同一份运行手册——本文内容对任意平台的同构技能副本如graphify/skills/claude/references/update.md同样适用。小结与自查清单把两份参考与实现源码对照后落地一条安全的--update可以浓缩为如下检查清单detect_incremental无变更时提前退出零成本有变更则先判定code_only纯代码 → 只跑 Step 3AAST跳过所有 LLM 语义子代理含文档/论文/图片/视频 → 视频先转录并改写files[document]再走完整 Steps 3A–3C仅删除 → 造空抽取交给合并步骤剪枝合并前cp备份旧图build_merge时prune_sources只含真删除、务必传root与正确的directed超边从合并后的G.graph取manifest 只给本轮真正产出输出的语义文件盖章派发未盖章的文件清semantic_hashscan_corpus传原始全语料继续 Steps 4–8 跑全量图用graph_diff汇报差异用毕删除.graphify_old.json。而--cluster-only更简单也更挑剔一条graphify cluster-only .自包含完成聚类-命名-输出三件套此后不要再重跑任何依赖已被清理的中间文件的下游步骤。【免费下载链接】graphifyTurn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph. A /graphify skill for Claude Code, Cursor, Codex, and Gemini CLI: local deterministic AST parsing, every edge explained, no vector store.项目地址: https://gitcode.com/GitHub_Trending/graph/graphify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价