graphify 增量更新(--update)与仅重聚类(--cluster-only)实战指南
导读
对同一代码库反复构建完整的知识图谱既浪费 token 也浪费时间。graphify 为此提供两条「后置运行」路径:--update 只对上次运行之后新增/修改过的文件做增量重新提取,再与磁盘上已有的 graph.json 合并;--cluster-only 则完全跳过提取,仅对既有图谱重新聚类与重命名社区、刷新 GRAPH_REPORT.md、graph.json 与 graph.html。本指南基于仓库内随 Skill 分发的参考文档 graphify/skills/vscode/references/update.md 展开,并对照其背后的源码实现进行逐段解读。读完本文,你将能亲手把一个上千文件的大仓库的第二次、第三次……构建成本压到「只处理变更文件」的量级,并在纯代码变更时做到零 LLM 调用。
一、背景:这份参考文档何时被加载
update.md 是一份 Skill 参考文档(reference),在仓库中按宿主平台分发,本指南以 VSCode 宿主版本为例,其兄弟副本存在于 graphify/skills/agents/、graphify/skills/claude/、graphify/skills/codex/ 等各平台的 references 目录下。它的主 Skill 入口是 graphify/skill-vscode.md,其中明确规定:
Load this only when the user passed
--updateor--cluster-only. A first-time full build never reads this file.
即:首次全量构建绝不读这份文档,只有用户在后续运行时显式带上 --update 或 --cluster-only,宿主 Agent 才加载它并按其流程执行。因此它描述的不是从零构建,而是对已有图谱的两种增量维护路径。
主 Skill 的默认管道由 Step 0–Step 9 组成(Step 1 解析解释器并写入 graphify-out/.graphify_python、Step 2 用 detect() 扫描语料、Step 3 做 AST 结构提取 + 语义提取、Step 4 建图/聚类/分析/导出、Step 5 社区命名、Step 9 保存 manifest 并清理中间文件)。update.md 假设这套管道骨架仍然存在,只在其上替换「变更检测」与「合并」两个环节;理解这一点是读懂下文 Steps 3A–6、Steps 4–8 这类引用的前提。
二、--update 的总体思路与变更检测原理
2.1 为什么只重提取变更文件
--update 的适用场景是:图谱已构建过一次,此后你在代码库里新增或修改了若干文件。全量重跑会重复处理未变更的绝大部分文件;而增量路径只把「上次运行之后有变化」的文件送入提取管道。文档原话是 "Only re-extracts changed files - saves tokens and time"。
2.2 底层实现:detect_incremental() 与 manifest
变更检测并不是目录间盲目比较时间戳,而是依赖一份 manifest。在 graphify/detect.py 中,manifest 的默认路径由 _MANIFEST_PATH = str(out_path("manifest.json")) 定义,即 graphify-out/manifest.json。增量检测函数 detect.py#L2366 的 detect_incremental(root, ...) 实现要点包括:
- 先做一次全量扫描:
detect_incremental内部先调用完整的detect(),拿到当前磁盘上的全部语料(full["files"]),再与 manifest 中记录的条目比对; - kind 双通道:
kind="semantic"(graphify extract默认)用semantic_hash判断是否变更;kind="ast"(graphify update使用)用ast_hash判断——这解释了为什么update触达的文件稍后还要被语义化提取重新处理; - 快慢两条路径:快速路径下 mtime 未变且 hash 匹配 → 直接判为 unchanged(免费,除 stat 外零磁盘 IO);慢速路径下 mtime 被改动 → 用 MD5(
_md5_file)与存储的 hash 比对后再决定是否重提取; - 兼容旧格式 manifest:既支持纯 float mtime 的旧条目,也支持
{mtime, hash}旧字典(会自动归一化为新 schema); - 区分删除与被排除:manifest 中有、当前扫描里没有的行,再依据磁盘上文件是否还存在分流——文件已从磁盘消失是真正的删除(
deleted_files),文件仍在但不在当前扫描范围则是被 ignore 规则/--exclude排除(excluded_files),后者绝不报告为删除。这一点对应源码中 #1908 的修复,注释为 "a row whose file is gone from DISK is a genuine deletion... a row whose file still exists but is out of the current scan was EXCLUDED"。
detect_incremental 的返回结构包含 new_files(按语料类型分组的变更子集)、unchanged_files、new_total、deleted_files、excluded_files 等键,其中 incremental 恒为 True。无任何历史 manifest(首次运行)时它会退化为把全量都当作 new,因此首次构建永远走全量路径,与本文开头「首次不读本文件」的原则一致。
2.3 第一段实操:跑增量检测并落盘 .graphify_incremental.json
update.md 给出的首个命令块从 graphify-out/.graphify_python 读取之前解析好的 Python 解释器(该文件由主 Skill Step 1 写入并跨调用持久化),随后调用 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, 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.')
"
注意三处状态机的关键分支:
new_total == 0且无删除 → 直接SystemExit(0),无事可做,不浪费任何后续 token;- 有删除 → 打印待剪枝(prune)数量;
- 有新/改文件 → 打印待重提取数量。
其中 INPUT_PATH 是用户在调用 /graphify ... --update 时给出的实际扫描根路径,graphify-out/.graphify_root 中记录了上次运行的扫描根,便于无参 graphify update 复用。
2.4 第二段实操:重建 .graphify_detect.json(files 与 all_files 的分工)
主管道的 Steps 3A–6 无条件读取 graphify-out/.graphify_detect.json。在增量运行里这份文件不能是 Step 2 全量检测的产物,而必须被增量结果覆盖,否则后续步骤会以为全量语料都要重处理。文档给出的映射逻辑是:
files←new_files:携带变更子集,驱动 Step 3A(AST)与 Step 3B0(缓存检查)只作用于真正变化的文件;all_files←files:携带全量语料,供任何需要语料级上下文的步骤使用;total_files/total_words/skipped_sensitive同步透传增量计数。
$(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\")
"
这套「files 是变更子集、all_files 是全语料」的双键约定在增量保存 manifest(见下文 2.8)时同样被复用,是增量路径正确性的地基。
三、按变更内容分流:code-only / 语义变更 / 纯删除
增量检测拿到 new_files 后,并不能直接进入统一的提取流程——变更文件的类型决定了成本。这是 update.md 中最具工程价值的分流逻辑。
3.1 code-only:纯代码变更 → 零 LLM 语义提取
如果所有变更文件都是代码文件,graphify 的根本设计决定了这一步不需要任何 LLM(代码由确定性 AST 提取,见主 Skill Step 3 Part A)。文档先给出一个探测脚本,用硬编码的代码扩展名集合判定 code_only:
$(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)
"
扩展名集合覆盖了 Python、TypeScript/JavaScript、Go、Rust、Java、C/C++ 家族(含 .cc/.cxx/.hpp/.h)、Ruby、Swift、Kotlin(含 .kts)、C#、Scala、PHP、Lua、Fortran 全后缀族(.f/.F/.f90/.F90/.f95/.F95/.f03/.F03/.f08/.F08)等 graphify 内置 AST 提取器支持的语言。
判定为 True 时执行路径被大幅缩短:
[graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed),只对变更文件运行 Step 3A(AST),完全跳过 Step 3B(不派发任何语义子代理),随后直接进入合并与 Steps 4–8。
这是 --update 在开发节奏中最常见的形态——改几行代码、加一个函数后刷新图谱,成本几乎为零。
3.2 混入文档/PDF/图片/视频:先转写视频再走完整语义管道
只要变更集合里有任何 doc/paper/image/video 文件(code_only 为 False),就必须走完整的 Steps 3A–3C 语义管道。但在此之前有一个硬性前置条件:若 new_files['video'] 非空,必须先执行 references/transcribe.md(Step 2.5)对视频/音频转写,再把产物路径从 files['video'] 移入 files['document']、删掉 files['video'] 分组,最后重写 .graphify_detect.json。
这条规则背后的动机在文档中被标注为 issue #1392:否则原始 .mp4/.mp3 路径会被当作不可读媒体直接喂给语义子代理,产生坏提取。转写文档的完整流程见同目录参考文档 graphify/skills/vscode/references/transcribe.md(Whisper 模型默认 tiny,可通过 --whisper-model medium 等调大以获得更好准确率)。
3.3 纯删除:构造空提取让合并步骤剪枝
如果 new_files 为空而只有删除(文件从磁盘消失),此时没有新的 AST/语义产出,但合并步骤(build_merge)需要一份「新提取」来与旧图合并、并按删除清单剪枝。文档的处理是:当 .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
注意 nodes/edges/hyperedges 三个列表全部为空、token 计数归零——它不贡献任何新内容,只为让下游合并器有一个合法的输入载体,从而把删除动作「传递」给剪枝逻辑。
四、核心合并:build_merge() 的替换式增量语义
增量提取(或空提取)就绪后,--update 的关键一步是把这份新结果与磁盘上的 graphify-out/graph.json 合并。这一步必须使用 graphify/build.py 的 build_merge()(源码见 build.py#L1626),文档与源码共同强调了几条必须遵守的语义约束,它们分别对应历史上踩过的真实 bug:
-
读 graph.json 直合并,不做 NetworkX 往返(#801):
build_merge直接读取磁盘上的 node-link 结构,避免先导出再导入造成的边方向丢失(calls/implements/imports 等有向关系在全量往返后可能坍塌),保证增量合并永远保留已有图的方向语义。 -
重新提取即替换,而非叠加(#1344、#2333/#2336):新提取结果里出现的每个
source_file,在合并前会先从旧图基数中按层级剔除其既有贡献(AST 层与语义层各自独立作用域),因此改动文件里已删除/改名的旧节点与旧边不会残留成幽灵。源码注释说得很直白:"a CHANGED file's stale nodes/edges don't accumulate across incremental updates. Without this, build() merges old+new for the same file and only exact-duplicate edges collapse"。新文件不在旧图基数中,这一步对它自然是无操作。 -
prune_sources只给真正删除的文件(#1361、#1178):变更/重提取文件绝不能放进prune——因为root=传入后,prune_set会按与刚合并节点相同的基做相对化,误删重新提取的内容。文档为此明确警告:"Do NOT addchangedhere",并解释"replace — not the dedup pass — reconciles changed files"。只有deleted_files需要剪枝。 -
root=必须传(#1361):detect_incremental返回的删除路径是绝对路径,而图谱节点里的source_file是相对扫描根的相对路径;不传root=做相对化则什么都剪不掉,陈旧节点会在每次更新中不断累积。 -
directed=必须与当初构建一致(#1392/#2342):不带--directed重建时若省略该参数,一个原本有向的图可能被静默重建为无向,双向互连的A↔B边会塌缩。源码层面build_merge在directed=None时会先读取磁盘图自身记录的directed标志再继承(见 build.py#L1670),但显式传参最稳妥。
合并命令块原文如下(IS_DIRECTED 需替换为 True/False,规则与主 Skill 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_merge's replace-on-re-extract (#1344): every source_file in
# new_chunks is dropped from the base before merge, so old/stale nodes don't 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 graph's relative source_file values; without it
# nothing is pruned and stale nodes accumulate on every update (#1361).
# directed=IS_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_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')
# 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(data=True)],
'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(data=True)
],
# 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_ascii=False), 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 today's state, not the
# prior run's 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 run's 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, root='INPUT_PATH', scan_corpus=_scan, clear_semantic=_cleared or None)
print('[graphify update] Manifest saved.')
"
4.1 合并结果回写的两个细节
回写 .graphify_extract.json 时,代码刻意把 source/target 放在字典展开的最后(Python dict 后者覆盖前者),确保显式键压过边属性里可能残留的陈旧 source/target;边的原始端点则存在 _src/_tgt 两个内部键中。而 hyperedges 必须取自 G.graph.get('hyperedges', []) 而非仅新提取——因为 build_merge 已把旧图与新提取的超边合并在一起(源码 docstring:"hyperedges ... combine them"),若只回写新提取的则会把此前所有运行累积的超边静默丢弃(#801)。
4.2 增量 manifest 保存的三个坑
合并完成后的 manifest 保存是整个增量体系闭环的关键,其正确性依赖三个精细机制:
-
只给「真产出」打戳(#2015/#933):
_stamped_manifest_files()(源码见 cli.py#L88)只把本轮实际产生了节点/超边的语义文件(docs/papers/images)写回semantic_hash。一个 chunk 失败的变更文档必须保持未打戳,否则下次--update会认为它已完成而永久丢内容。更精细的规则见源码 #2927:只有nodes与hyperedges算有效产出,仅含边的结果不构成打戳依据。代码文件因 AST 提取是确定性的,始终打戳。 -
清空陈旧语义哈希(#1948):本轮派发过但未打戳的语义文件,若仍携带上轮的
semantic_hash,会被detect_incremental误判为未变更。因此计算_dispatched − _stamped作为clear_semantic集合,显式清空这些行。 -
scan_corpus用原始全量语料(#1908):不能传打戳后的子集,否则「上轮在扫描范围内、本轮被 ignore 规则排除」的文件会被误当成删除。传原始全量语料后,这类文件作为 excluded 被丢弃,未动过的行则原样保留。
save_manifest 的定义位于 detect.py#L2112,它按 root= 把 manifest 键相对化到扫描根,使 manifest 跨克隆/跨机器可移植——这正是 #1417 要求的"--update 在目录迁移后仍能命中缓存文件,而不是错过每一个"。load_manifest(detect.py#L2085)读取时同样支持 root= 重锚定。
4.3 合并完成后的收尾
合并 + manifest 落盘后,文档要求接着按正常管道把合并结果当作全量图继续跑 Steps 4–8(Step 4 建图/聚类/分析/导出、Step 5 社区命名、可视化等)。换而言之,--update 的差异只体现在「提取什么」与「以什么为基数建图」,下游的聚类、命名、报告、导出逻辑全部复用全量管道,从而保证增量图与全量图拥有完全一致的结构契约。
五、增量后的图谱差异报告(graph diff)
为了让用户看到一次 --update 到底改变了什么,文档要求在 Step 4 之后展示图谱差异。其机制是:合并前先把旧图备份,随后用 graphify/analyze.py 的 graph_diff() 对比新旧快照。
先在合并前备份旧图:
cp graphify-out/graph.json graphify-out/.graphify_old.json
再读取旧图与合并后的新图并输出差异(IS_DIRECTED 同样按真实方向替换):
$(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, 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_diff() 的返回结构(源码见 analyze.py#L556)包含 new_nodes(携带 id 与 label)、removed_nodes、new_edges(携带 source/target/relation/confidence)、removed_edges 与人类可读的 summary 字符串(如 "3 new nodes, 5 new edges, 1 node removed")。新旧图的边比较在无向图中会先把端点归一化为有序对再判等,避免方向性误报。
用完后清理备份:
rm -f graphify-out/.graphify_old.json
六、--cluster-only:对既有图谱仅重聚类
6.1 适用场景与自包含性
--cluster-only 解决的是「提取没变,但想重新聚类」的需求。典型触发点包括:算法参数调整、社区标签需要重新生成、或者只是希望基于既有图谱刷新产物。它的核心主张在文档中被强调为自包含(self-contained):
graphify cluster-only .is self-contained: it re-clusters, names communities, and regeneratesGRAPH_REPORT.md,graph.json, andgraph.htmlfrom the existing graph.
一次调用同时完成三件事:重新聚类、社区命名、重新生成三类产物。命令形态即 graphify cluster-only <path>(路径通常为仓库根/扫描根)。
6.2 禁忌:绝不能再跑 Steps 5–9
--cluster-only 的内部流程在源码中对应于 cli.py 的 cluster-only/label 分支(cli.py#L1843 起,label 是强制重新命名社区的变体),它直接以磁盘上的 graph.json 为输入。文档对此有一条硬性警告:
Do not re-run Steps 5–9 — they read intermediate files (
.graphify_extract.json,.graphify_detect.json,.graphify_analysis.json) that a prior build's cleanup (Step 9) already deleted, so they raiseFileNotFoundError(#1392).
即主管道 Step 9 的清理动作会删除这些点号开头的中间 sidecar 文件,--cluster-only 之后这些文件早已不在磁盘上;若按惯性把完整管道的后续步骤重跑一遍,必然触发 FileNotFoundError。正确做法是:跑完 graphify cluster-only . 后,直接把刷新过的 GRAPH_REPORT.md 摘要照常呈现给用户即可。
6.3 从源码看可用的调节参数
cluster-only 分支在解析命令行参数时支持若干可选标志(可从 cli.py 源码确认):
--no-viz:跳过可视化,只更新报告与 graph.json;--no-label:不重新生成社区名;--missing-only:只补缺失的社区名;--label变体(对应graphify label):无论是否已有 labels 都强制重命名,且支持--backend=/--model=指定命名所用的 LLM 后端与模型;--min-community-size=N:控制社区最小规模,默认3;--resolution=与额外的 hub 处理参数:调节聚类分辨率等社区检测参数;--graph <path>:指定要重聚类的图文件,便于脱离默认graphify-out/graph.json工作。
这些参数意味着 --cluster-only 不只是「原样重跑一次聚类」,还可以承担"调聚类粒度 → 换命名后端 → 刷新产物"的实验循环。
七、两条路径的边界总结
把 update.md 与主 Skill 对照可得到一张清晰的职责边界表:
| 维度 | --update |
--cluster-only |
|---|---|---|
| 输入 | 变更文件 + 既有 graph.json |
仅既有 graph.json |
| 变更检测 | detect_incremental() 对比 manifest |
无 |
| 提取 | 只对 changed 文件(code-only 纯 AST / 含语义变更走全管道) | 不提取 |
| 合并 | build_merge() 替换式合并 + 剪枝删除 |
不合并 |
| 聚类/命名/导出 | 复用 Steps 4–8 | 内置一步完成,产物直接落盘 |
| 可重跑 Steps 5–9 | 可以(合并结果被当作全量图) | 绝对禁止(中间文件已被清理) |
在仓库的既有规划文档 docs/superpowers/plans/2026-05-04-incremental-updates-dedup.md 与设计规格 docs/superpowers/specs/2026-05-04-incremental-updates-dedup-design.md 中,可以看到增量更新与去重的设计脉络与演进历史;而 update.md 描述的这套 Agent 流程,就是把这些设计落到可执行命令上的最终形态。
结语:把二次构建的成本压到只差
对追求高效迭代的团队而言,--update 与 --cluster-only 是把 graphify 从"一次性分析工具"升级为"可持续维护的图谱系统"的关键闸门。前者用 manifest 驱动的 mtime+hash 双层检测只提取变更,用替换式 build_merge 保证不残留幽灵节点,并刻意只在删除时剪枝、只在真产出时打戳——每一个细节都对应源码中可考的历史缺陷修复;后者则提供一条不触碰提取、只刷新聚类与产物的廉价路径。建议实际使用时从一次全量构建开始,之后所有变更一律走 --update,并在需要调参或换标签语言时用 --cluster-only 收尾,即可在几乎零成本的条件下保持 graph.json、GRAPH_REPORT.md 与 graph.html 始终与代码库的真实状态一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00