从代码到可查询知识图谱:graphify Agent Skill 完整执行规范与流水线深度解析
导读:本指南以 graphify 仓库为 Claude Code / Cursor / Codex / Gemini CLI 等宿主分发的 skill-agents 技能规范为蓝本,逐节拆解 /graphify 命令从“输入任意目录/URL”到“产出交互式 HTML、GraphRAG 就绪 JSON、可读报告 GRAPH_REPORT.md”的完整 Agent 流水线。读者读完将掌握:命令级用法与开关全表、九步执行协议的每个阶段(文件检测 → AST 抽取 → 语义抽取 → 建图聚类 → 标签与导出 → manifest 维护)、缓存与诚实审计规则的设计意图,以及如何把已有 graphify-out/graph.json 直接当知识库来 query / path / explain。
一、graphify Skill 是做什么的
把任意一个装满代码、文档、论文、图片甚至视频的目录丢给 graphify,就能得到一张可查询的知识图谱。它的核心价值有三个:
- 持久化(Persistent across sessions):图构建一次即可长期复用,后续提问无需重建;
- 诚实的审计轨迹(Honest audit trail):每条边都带有
EXTRACTED / INFERRED / AMBIGUOUS的证据标记,不会把猜测伪装成事实; - 社区发现(Community detection):自动把散落在不同文件里的相关节点聚成“社区”,帮助你发现根本想不到去问的跨文档关联。
整条流水线最终产出三种面向不同消费端的产物:
| 产物 | 形态 | 用途 |
|---|---|---|
| 交互式 HTML | graph.html |
浏览器中直接拖拽浏览的图可视化 |
| GraphRAG 就绪 JSON | graph.json |
交给下游 RAG / Agent / 图算法消费的结构化图数据 |
| 白话报告 | GRAPH_REPORT.md |
God Nodes、Surprising Connections、Suggested Questions 等人话摘要 |
源码层面对应关系为:抽取阶段在 graphify/extract.py(AST)与 graphify/llm.py(语义)完成,聚类在 graphify/cluster.py,社区分析与问题生成在 graphify/analyze.py,报告在 graphify/report.py,JSON 导出在 graphify/export.py。
二、命令速查:/graphify 的全部用法
Skill 规范第一优先要求是:当用户调用 /graphify --help(或 -h)且没有其它参数时,逐字打印下面这段 Usage 区块并停止,不执行任何命令、不做任何检测、也不要把路径默认成 .:
/graphify # full pipeline on current directory (HTML viz; add --obsidian for a vault)
/graphify <path> # full pipeline on specific path
/graphify https://github.com/<owner>/<repo> # clone repo then run full pipeline on it
/graphify https://github.com/<owner>/<repo> --branch <branch> # clone a specific branch
/graphify <url1> <url2> ... # clone multiple repos, build each, merge into one cross-repo graph
/graphify <path> --mode deep # thorough extraction, richer INFERRED edges
/graphify <path> --update # incremental - re-extract only new/changed files
/graphify <path> --directed # build directed graph (preserves edge direction: source→target)
/graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy
/graphify <path> --cluster-only # rerun clustering on existing graph
/graphify <path> --no-viz # skip visualization, just report + JSON
/graphify <path> --html # (HTML is generated by default - this flag is a no-op)
/graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub)
/graphify <path> --graphml # export graph.graphml (Gephi, yEd)
/graphify <path> --neo4j # generate graphify-out/cypher.txt for Neo4j
/graphify <path> --neo4j-push bolt://localhost:7687 # push directly to Neo4j
/graphify <path> --falkordb # generate graphify-out/cypher.txt for FalkorDB
/graphify <path> --falkordb-push falkordb://localhost:6379 # push directly to FalkorDB
/graphify <path> --mcp # start MCP stdio server for agent access
/graphify <path> --watch # watch folder, auto-rebuild on code changes (no LLM needed)
/graphify <path> --wiki # build agent-crawlable wiki (index.md + one article per community)
/graphify <path> --obsidian --obsidian-dir ~/vaults/my-project # write vault to custom path (e.g. existing vault)
/graphify add <url> # fetch URL, save to ./raw, update graph
/graphify add <url> --author "Name" # tag who wrote it
/graphify add <url> --contributor "Name" # tag who added it to the corpus
/graphify query "<question>" # BFS traversal - broad context
/graphify query "<question>" --dfs # DFS - trace a specific path
/graphify query "<question>" --budget 1500 # cap answer at N tokens
/graphify path "AuthModule" "Database" # shortest path between two concepts
/graphify explain "SwinTransformer" # plain-language explanation of a node
把上面这段命令逐项归类,可以整理出六组能力:
| 能力分组 | 命令 | 一句话说明 |
|---|---|---|
| 全量流水线 | 无参 / <path> / GitHub URL / 多 URL |
在当前目录、指定路径、clone 仓库或跨仓库 merge 上跑完整流程 |
| 模式开关 | --mode deep --update --directed --cluster-only |
深度抽取、增量更新、有向图、仅重聚类 |
| 转写 | --whisper-model medium |
指定更大的 Whisper 模型提升视频/音频转写精度 |
| 导出开关 | --no-viz --svg --graphml --neo4j --neo4j-push --falkordb --falkordb-push --mcp --wiki --obsidian --obsidian-dir |
控制可视化与各类图数据库/格式导出 |
| 语料管理 | add <url> [--author] [--contributor] |
把 URL 抓取进 ./raw 并增量更新图 |
| 图查询 | query [--dfs] [--budget] path explain |
在已建好的图上做问答、最短路径、节点解释 |
提示:
--html是一个空操作(no-op)——HTML 默认就会生成,加上它只是为了兼容旧习惯。而--update、--cluster-only、query、path、explain、add均属于非默认子命令,各自的完整流程分别沉淀在 skill 的references/手册中。
三、被调用时的行为协议:Fast Path 与九步执行顺序
Skill 对“什么时候该重建、什么时候该直接查”做了强约束,避免 Agent 浪费 token 重复建图:
- Fast path(快速通道):动手前先检查当前工作目录(即运行命令的项目根)下是否存在
graphify-out/graph.json。如果存在且用户请求是对代码库的自然语言提问(如 “How does X work?”、“What calls Y?”、“Trace the data flow through Z”),且不是显式重建命令(--update、--cluster-only,或隐含全新抽取的裸路径/URL)——则完全跳过 Steps 1–5,直接执行graphify query "<question>"。不运行 detect、不检查语料规模、不让用户收窄问题。 - 未给路径时默认用
.(当前目录),不得反问用户要路径。 - 路径以
https://github.com/或http://github.com/开头时先执行 Step 0(clone),随后按解析出的本地路径继续。
随后必须按序、不跳步地执行下面这张九步协议表:
| 步骤 | 名称 | 触发条件 | 产出 |
|---|---|---|---|
| Step 0 | GitHub 仓库与多路径合并 | 仅当路径是 GitHub URL 或多个本地子目录 | 解析后的本地路径 |
| Step 1 | 确认 graphify 已安装 | 总是 | graphify-out/.graphify_python + .graphify_root |
| Step 2 | 文件检测 | 总是 | graphify-out/.graphify_detect.json 及语料摘要 |
| Step 2.5 | 视频/音频转写 | 仅当检测到 video | 转写文本(当作文档处理) |
| Step 3 | 抽取实体与关系(AST + 语义) | 总是 | graphify-out/.graphify_extract.json |
| Step 4 | 建图、聚类、分析、输出 | 总是 | graph.json、GRAPH_REPORT.md、.graphify_analysis.json |
| Step 4.5 | 图健康检查(只读) | 总是 | 诊断报告 / GRAPH HEALTH WARNING |
| Step 5 | 社区打标签 | 总是 | GRAPH_REPORT.md 更新、.graphify_labels.json |
| Step 6 | Obsidian + HTML | HTML 总是(除非 --no-viz);Obsidian 仅当 --obsidian |
graph.html、可选 obsidian/ vault |
| Steps 6b–8 | wiki / Neo4j / FalkorDB / SVG / GraphML / MCP / benchmark | 仅当对应 flag | 各自导出物 |
| Step 9 | manifest、成本记录、清理、汇报 | 总是 | manifest、cost.json、会话总结 |
四、Step 1:安装检查与解释器探测
graphify 可以通过 uv tool、pipx、venv 或系统 Python 安装。由于不同宿主环境里 python3 可能指代不同解释器(装了 graphify 的那个不一定叫 python3),Skill 规定用一个探测脚本先锁定正确的 Python 解释器,并把结果持久化,供后续所有步骤复用:
# Detect the correct Python interpreter (handles uv tool, pipx, venv, system installs)
PYTHON=""
GRAPHIFY_BIN=$(which graphify 2>/dev/null)
# 1. uv tool installs — most reliable on modern Mac/Linux
if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then
_UV_PY=$(uv tool run --from graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null)
if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi
fi
# 2. Read shebang from graphify binary (pipx and direct pip installs)
if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then
_SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!')
case "$_SHEBANG" in
*[!a-zA-Z0-9/_.@-]*) ;;
*) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;;
esac
fi
# 3. Fall back to python3
if [ -z "$PYTHON" ]; then PYTHON="python3"; fi
if ! "$PYTHON" -c "import graphify" 2>/dev/null; then
if command -v uv >/dev/null 2>&1; then
uv tool install --upgrade graphifyy -q 2>&1 | tail -3
_UV_PY=$(uv tool run --from graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null)
if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi
else
"$PYTHON" -m pip install graphifyy -q 2>/dev/null \
|| "$PYTHON" -m pip install graphifyy -q --break-system-packages 2>&1 | tail -3
fi
fi
# Write interpreter path for all subsequent steps (persists across invocations)
mkdir -p graphify-out
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
# Save scan root so `graphify update` (no args) knows where to look next time
echo "$(cd INPUT_PATH && pwd)" > graphify-out/.graphify_root
三个关键设计点:
- 探测优先级:先试
uv tool(最可靠)→ 读graphify可执行文件 shebang → 退回python3。安装失败时依次尝试uv tool install --upgrade graphifyy、pip install graphifyy,再到pip install --break-system-packages。 - 持久化两个 sidecar:
graphify-out/.graphify_python记录解释器绝对路径(供每个后续 bash 块用$(cat graphify-out/.graphify_python)替换裸python3);graphify-out/.graphify_root记录扫描根目录,让无参数的graphify update下次知道去哪找。 - 注意安装包名为
graphifyy(双 y)——这与仓库内 graphify/cache.py 通过importlib.metadata.version("graphifyy")读取包版本号完全一致;Python 侧导入模块名才是graphify。
五、Step 2:文件检测与语料摘要
检测阶段调用 graphify/detect.py 的 detect()(定义于 detect.py)。Skill 规定必须用 Python 写 sidecar JSON 而不是 shell 重定向——原因在规范中有明确注释:同样的渲染块在 PowerShell 宿主上会因控制台编码漂移而出错(issue #2528):
$(cat graphify-out/.graphify_python) -c "
import json
from graphify.detect import detect
from pathlib import Path
result = detect(Path('INPUT_PATH'))
Path('graphify-out/.graphify_detect.json').write_text(json.dumps(result, ensure_ascii=False), encoding='utf-8')
print(f'Detected {result[\"total_files\"]} files')
"
检测结果按五种文件类别聚合,Agent 应向用户给出干净的人类可读摘要而非倾倒 JSON:
Corpus: X files · ~Y words
code: N files (.py .ts .go ...)
docs: N files (.md .txt ...)
papers: N files (.pdf ...)
images: N files
video: N files (.mp4 .mp3 ...)
某个类别为 0 文件时该行省略。随后按规则分流:
total_files == 0:停止并提示 “No supported files found in [path].”;skipped_sensitive非空:报告数量并逐个列出被跳过的敏感文件名,让被误判的源码/文档可见、可改名或移动(#2106);- 大规模语料分流:当
total_words > 2,000,000或total_files > 500时显示警告,并计算文件数 Top 5 的一级子目录:- 从 detect JSON 读
scan_root(解析后 INPUT_PATH 的绝对路径); - 把
code / document / paper / image / video各类文件列表拼接,剔除以scan_root + "/graphify-out/"开头(被转换的 sidecar)的路径; - 剥掉
scan_root前缀取第一段路径组件,直接位于根目录的记为(root); - 若所有文件都在
(root)、根本没有子目录,则不要求收窄,改为建议--no-cluster(跳过昂贵的聚类)后继续; - 否则按数量排序展示 Top 5 并询问用户跑哪个子文件夹,等待答复再继续。
- 从 detect JSON 读
实现细节上,detect() 尊重 .gitignore / .graphifyignore 规则(可参考 detect.py 中 _load_graphifyignore 与 CLI --exclude 追加逻辑),并默认索引点号目录;.graphifyinclude 已被废弃(#2112)。文档、PDF、docx 的词数统计走按 stat 签名的缓存(cached_word_count),避免每次都重解析。
六、Step 2.5:视频与音频转写
仅当 detect 返回了非零的 video 文件数时执行。此时按 graphify/skills/agents/references/transcribe.md 先把音视频转写成文本,再把转写文本当作 Step 3 的文档文件处理。--whisper-model medium 等参数即用于提升这一环节的转写准确度;零 video 的语料直接跳过本步。
七、Step 3:实体与关系抽取(核心,两部分并行)
7.1 先立规矩:graphify 不需要 API key
这是整个 Skill 最容易误读、也因此被反复强调的一条:
graphify 不需要 API key。永远不要向用户索要 key,也不要因缺失 key 而阻塞。
- 代码抽取是结构化(AST)的,不用 LLM、不需要任何 key;纯代码语料(最常见的
/graphify .)会完全跳过语义抽取,直接走 Part A → Part C。 - 语义抽取(仅针对文档、论文、图片)只有在
GEMINI_API_KEY或GOOGLE_API_KEY已设置时才用 Gemini;否则宿主 Agent 本身就是 LLM。 - graphify 不读取
ANTHROPIC_API_KEY、OPENAI_API_KEY或任何其它供应商 key。谁要是因为缺 key 而去提示/等待/停摆,就是对这份 Skill 的误读。 - 在 Part B 开始前若两个 Gemini key 都未设置,只打印一次提示即可继续:
Tip: set GEMINI_API_KEY or GOOGLE_API_KEY to use Gemini for semantic extraction (pip install 'graphifyy[gemini]')。 - 若设置了 key,则改走
graphify.llm.extract_corpus_parallel(files, backend="gemini"),默认模型为gemini-3-flash-preview,可用环境变量GRAPHIFY_GEMINI_MODEL或 CLI--model覆盖(模型表与默认值可分别在 graphify/llm.py 和 graphify/prs.py 中看到,如llm.py中"default_model": "gemini-3-flash-preview"、"model_env_key": "GRAPHIFY_GEMINI_MODEL")。
并行化要求:Part A(AST)与 Part B(语义子代理)要在同一条消息里同时发出,二者操作不同的文件类型、互不冲突,最终在 Part C 汇合。规范指出在大语料上并行可省 5–15 秒。
7.2 Part A:代码文件的结构化抽取(AST)
$(cat graphify-out/.graphify_python) -c "
import sys, json
from graphify.extract import collect_files, extract
from pathlib import Path
code_files = []
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding='utf-8'))
for f in detect.get('files', {}).get('code', []):
code_files.extend(collect_files(Path(f)) if Path(f).is_dir() else [Path(f)])
if code_files:
result = extract(code_files, cache_root=Path('INPUT_PATH'))
Path('graphify-out/.graphify_ast.json').write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding='utf-8')
print(f'AST: {len(result[\"nodes\"])} nodes, {len(result[\"edges\"])} edges')
else:
Path('graphify-out/.graphify_ast.json').write_text(json.dumps({'nodes':[],'edges':[],'input_tokens':0,'output_tokens':0}, ensure_ascii=False), encoding='utf-8')
print('No code files - skipping AST extraction')
"
这里调用的是 graphify/extract.py 的 extract()(定义于 extract.py)与 collect_files()。结合源码可知其底层是两趟式管线:
- 逐文件结构抽取:类、函数、导入、方法等节点与边(
extract_python、extract_js、extract_cpp等大量 per-language 抽取器,全部确定性的 tree-sitter 解析,见 graphify/extractors/ 各文件); - 跨文件导入解析:把文件级 import 升级为类级
INFERRED边(例如DigestAuth --uses--> Response)。
值得展开的实现事实(来自 extract() docstring 与代码):
- 抽取缓存优先:逐文件先查
load_cached,命中即跳过;未命中才进入并行池。AST 缓存按包版本与缓存 schema 命名空间隔离(cache/ast/v{version}-s{schema}/),因为 AST 缓存是 graphify 自身抽取器代码的输出,抽取器修复必须作废旧结果(见 graphify/cache.py 头部注释);而语义缓存刻意不版本化,否则每次发布都让未变化文件重新付费。 parallel=True且未缓存文件数达到阈值时走ProcessPoolExecutor多核抽取,max_workers默认取 CPU 数或GRAPHIFY_MAX_WORKERS。root参数锚定source_file相对化、节点 id 与符号解析;cache_root只决定graphify-out/cache/放哪(默认当前工作目录,绝不丢进只读或被扫描语料内部,#1774)。- 开始时会做环境自检(
_check_tree_sitter_version()、_raise_recursion_limit()),并清空 tsconfig alias、XAML 类表、Markdown 链接索引等进程内状态缓存,保证 watch/MCP 长驻进程内多次extract()不读到陈旧别名映射。
7.3 Part B:文档/论文/图片的语义抽取(并行子代理)
纯代码语料 Fast path:若检测结果里没有 docs/papers/images,直接跳过 Part B。此时必须先写一个空的语义文件,因为 Part C 的合并会无条件读取 .graphify_semantic.json——不写空文件,纯代码运行会撞上 FileNotFoundError:
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
Path('graphify-out/.graphify_semantic.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8')
"
强约束:Agent 必须使用 Agent/Task 工具并行派发,禁止自己逐文件阅读——那是 5–10 倍慢。派发前先打印耗时估算:ceil(uncached_non_code_files / 22) 个代理、每批约 45 秒(块大小 20–25 个文件),输出形如 Semantic extraction: ~N files → X agents, estimated ~Ys。
Step B0 — 先查语义缓存:调用 graphify/cache.py 的 check_semantic_cache()(cache.py),只对 document / paper / image 三类内容文件做检查(代码已由 AST 覆盖,扁平化所有类别会让子代理重读每个源文件,#1392)。关键点:
- 传入的
SPEC_PATH是随 Skill 分发的references/extraction-spec.md的绝对路径,它同时是抽取提示词本身——因此缓存条目会归属到该提示词版本:graphify 升级改了提示词,旧提示词产出的条目会被重新抽取而不是直接重放;未变的提示词则继续命中(#1939)。 - 命中就重写
.graphify_cached.json;一个都没有则删除该文件,避免 Part C 合并到陈旧缓存(#1392)。 - 需要抽取的文件名单写入
.graphify_uncached.txt,只对这些文件派发子代理;全部命中就直接跳到 Part C。
Step B1 — 切块:把未缓存文件按 20–25 个一组切块;每张图片独占一块(视觉需要独立上下文);切块时尽量把同一目录的文件归在一起,提高跨文件关系被抽出的概率。
Step B2 — 同一条消息内派发所有子代理:每个块一次 Task 调用、全部并行。任务描述里嵌入完整抽取提示词,并把 FILE_LIST、CHUNK_NUM、TOTAL_CHUNKS、DEEP_MODE、CHUNK_PATH 替换进去。子代理把结果写到各自的 graphify-out/.graphify_chunk_NN.json。CHUNK_PATH 必须是绝对路径(由 PROJECT_ROOT=$(pwd) 推导,cwd 即 Part C 用 glob 搜 graphify-out/ 的位置,注意不是 .graphify_root 指向的扫描目录,#1392)。规范明确:只有在至少一个块含 doc/paper/image 时才加载 graphify/skills/agents/references/extraction-spec.md——纯代码语料已跳 Part B,根本不会读它。注意区分 --mode deep:一旦给出,DEEP_MODE=true 必须传给每一个子代理,不能弄丢。
Step B3 — 收集、缓存、合并:
- 成功信号是
graphify-out/.graphify_chunk_NN.json落盘且为含nodes/edges的合法 JSON; - 文件缺失通常意味着子代理被以只读(Explore 类型)派发——打印警告 “chunk N missing from disk — subagent may have been read-only. Re-run with general-purpose agent.”,不要静默跳过;超过一半的块失败/缺失则停下并提示改用
subagent_type="general-purpose"; - 失败或返回非法 JSON 的块打印警告后跳过,不中止整轮;
- 每个 Agent 调用完成后,把结果
usage字段里的真实 token 数回写到块 JSON(块 JSON 里永远是占位 0),再合并进.graphify_semantic_new.json; - 随后用
save_semantic_cache()(cache.py)把新结果存缓存,allowed_source_files传.graphify_uncached.txt内容做写白名单,且必须传与 B0 相同的SPEC_PATH——不同提示词下的写入会落在下次读取不到的地方(#1939); - 最后把缓存命中 + 新结果合并去重(按节点
id)写入.graphify_semantic.json,并清理临时文件。
7.4 Part C:AST 与语义结果合并
$(cat graphify-out/.graphify_python) -c "
import sys, json
from pathlib import Path
ast = json.loads(Path('graphify-out/.graphify_ast.json').read_text(encoding='utf-8'))
sem = json.loads(Path('graphify-out/.graphify_semantic.json').read_text(encoding='utf-8'))
# Merge: AST nodes first, semantic nodes deduplicated by id
seen = {n['id'] for n in ast['nodes']}
merged_nodes = list(ast['nodes'])
for n in sem['nodes']:
if n['id'] not in seen:
merged_nodes.append(n)
seen.add(n['id'])
merged_edges = ast['edges'] + sem['edges']
merged_hyperedges = sem.get('hyperedges', [])
merged = {
'nodes': merged_nodes,
'edges': merged_edges,
'hyperedges': merged_hyperedges,
'input_tokens': sem.get('input_tokens', 0),
'output_tokens': sem.get('output_tokens', 0),
}
Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding='utf-8')
total = len(merged_nodes)
edges = len(merged_edges)
print(f'Merged: {total} nodes, {edges} edges ({len(ast[\"nodes\"])} AST + {len(sem[\"nodes\"])} semantic)')
"
合并策略很直白:AST 节点在前,语义节点按 id 去重追加;边做简单拼接;token 统计来自语义结果。最终得到统一抽取文件 .graphify_extract.json。
八、Step 4:建图、聚类、分析、输出
mkdir -p graphify-out
$(cat graphify-out/.graphify_python) -c "
import sys, json
from graphify.build import build_from_json
from graphify.cluster import cluster, score_all
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
from graphify.report import generate
from graphify.export import to_json
from pathlib import Path
extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding='utf-8'))
detection = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding='utf-8'))
# root= mirrors the --update runbook (#1361): relativize source_file to the same
# base so the full build and incremental --update never drift apart on re-extract.
G = build_from_json(extraction, root='INPUT_PATH', directed=IS_DIRECTED)
# Guard BEFORE any write: an empty extraction must not clobber a good graph.json /
# GRAPH_REPORT.md / analysis sidecar. Check immediately after build (#1392).
if G.number_of_nodes() == 0:
print('ERROR: Graph is empty - extraction produced no nodes.')
print('Possible causes: all files were skipped, binary-only corpus, or extraction failed.')
raise SystemExit(1)
communities = cluster(G)
cohesion = score_all(G, communities)
tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
gods = god_nodes(G)
surprises = surprising_connections(G, communities)
labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)
# Export FIRST and honor the #479 shrink-guard: to_json returns False (writing
# nothing) when the new graph is smaller than the existing graph.json. Only write
# GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so
# they never describe a graph that graph.json doesn't contain (#1392).
wrote = to_json(G, communities, 'graphify-out/graph.json')
if not wrote:
print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).')
print('If this shrink is intentional (you deleted files), re-run a full build with --force.')
raise SystemExit(1)
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions)
Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding='utf-8')
analysis = {
'communities': {str(k): v for k, v in communities.items()},
'cohesion': {str(k): v for k, v in cohesion.items()},
'gods': gods,
'surprises': surprises,
'questions': questions,
}
Path('graphify-out/.graphify_analysis.json').write_text(json.dumps(analysis, indent=2, ensure_ascii=False), encoding='utf-8')
print(f'Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges, {len(communities)} communities')
"
这一段值得逐点展开其背后设计(均有源码对应):
build_from_json(graphify/build.py) 负责从抽取结果构建 NetworkX 图。directed=IS_DIRECTED:--directed时传True建保留 source→target 方向的有向图DiGraph,否则默认无向Graph。Skill 要求把IS_DIRECTED当成和INPUT_PATH一样的占位符统一替换,不许把字面量留在代码里。root='INPUT_PATH'语义(#1361):把source_file相对化到与--updaterunbook 完全相同的基准,保证全量构建与增量--update在重抽取时不会漂移、node key 一致。- 空图守卫(#1392):
G.number_of_nodes() == 0时必须在任何写入之前报错退出——防止空抽取覆盖掉一份完好的graph.json/GRAPH_REPORT.md/ analysis sidecar。可能原因包括:所有文件被跳过、纯二进制语料、抽取失败。 - 先导出、后出报告(#479 shrink-guard):
to_json(graphify/export.py)在新图比现有graph.json更小(节点更少)时拒绝写入并返回 False——这是防缩水护栏,避免一次不完整构建悄悄砍掉已有的大图。只有当图真正写入后,才写GRAPH_REPORT.md与分析 sidecar,让报告永远与graph.json描述同一张图。如果缩水是有意的(确实删了文件),需要--force重跑全量构建。 - 分析项:
cluster(G)与score_all(graphify/cluster.py)做社区发现与凝聚度打分;god_nodes(graphify/analyze.py)、surprising_connections(analyze.py)、suggest_questions(analyze.py)分别找出枢纽“神节点”、跨越社区的意外连接、建议问题。labels 先用占位Community N,真实标签在 Step 5 生成后再回填并重生成问题(标签措辞会影响问题表述)。
若打印了 ERROR: Graph is empty,停下来向用户解释原因——不得继续进入打标签或可视化。
九、Step 4.5:图健康检查(只读完整性闸门)
对抽取结果做一次非破坏性诊断,用于暴露增量更新与 AST/LLM id 错配造成的静默损坏模式:边坍缩、悬空/缺失端点、自环。它只读、永不中止:
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
from graphify.diagnostics import diagnose_extraction, format_diagnostic_report
extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding='utf-8'))
summary = diagnose_extraction(extraction, directed=IS_DIRECTED, root='INPUT_PATH')
print(format_diagnostic_report(summary))
flags = [f'{summary[k]} {label}' for k, label in (
('dangling_endpoint_edges', 'dangling-endpoint edges'),
('missing_endpoint_edges', 'missing-endpoint edges'),
('self_loop_edges', 'self-loop edges'),
('directed_same_endpoint_collapsed_edges', 'collapsed (directed) edges'),
('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'),
) if summary.get(k, 0)]
print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).')
"
对应实现为 graphify/diagnostics.py 的 diagnose_extraction()(diagnostics.py)。若出现 GRAPH HEALTH WARNING,按 Honesty Rules 必须把它呈现在最终总结里(不中止——图仍可用,但完整性问题必须可见)。
十、Step 5:给社区打标签
读取 .graphify_analysis.json,为每个 community key 依据其节点标签写一个 2–5 词的平实名称(例如 “Attention Mechanism”、“Training Pipeline”、“Data Loading”),然后用 LABELS_DICT(如 {0: "Attention Mechanism", 1: "Training Pipeline"})回填并重建报告:
$(cat graphify-out/.graphify_python) -c "
import sys, json
from graphify.build import build_from_json
from graphify.cluster import score_all
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
from graphify.report import generate
from graphify.export import to_json
from pathlib import Path
extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding='utf-8'))
detection = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding='utf-8'))
analysis = json.loads(Path('graphify-out/.graphify_analysis.json').read_text(encoding='utf-8'))
G = build_from_json(extraction, root='INPUT_PATH', directed=IS_DIRECTED)
communities = {int(k): v for k, v in analysis['communities'].items()}
cohesion = {int(k): v for k, v in analysis['cohesion'].items()}
tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
# LABELS - replace these with the names you chose above
labels = LABELS_DICT
# Regenerate questions with real community labels (labels affect question phrasing)
questions = suggest_questions(G, communities, labels)
report = generate(G, communities, cohesion, labels, analysis['gods'], analysis['surprises'], detection, tokens, 'INPUT_PATH', suggested_questions=questions)
Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding='utf-8')
Path('graphify-out/.graphify_labels.json').write_text(json.dumps({str(k): v for k, v in labels.items()}, ensure_ascii=False), encoding='utf-8')
# Re-export so graph.json nodes carry the curated community_name (#2490).
wrote = to_json(G, communities, 'graphify-out/graph.json', community_labels=labels)
if not wrote:
print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).')
print('If this shrink is intentional (you deleted files), re-run a full build with --force.')
print('Report updated with community labels')
"
要点:labels 同时落盘到 .graphify_labels.json(Step 6 的可视化与 wiki 导出要用,所以 wiki 导出必须赶在 Step 9 清理之前);to_json 带 community_labels 重导出,让 graph.json 的节点携带策展后的 community_name(#2490)。因为与 Step 4 用的是同一份抽取,节点数不变,#479 shrink-guard 必然通过;若仍拒绝,把守卫消息亮给用户,不要强行越过。
十一、Step 6:Obsidian Vault(可选)与 HTML(默认)
HTML 总是生成(除非 --no-viz);Obsidian vault 只在显式给出 --obsidian 时生成——否则跳过,因为 vault 每个节点一个文件,很占空间。若同时给了 --obsidian-dir <path>,则通过 --dir 传入,否则默认 graphify-out/obsidian:
graphify export obsidian
# or with custom dir: graphify export obsidian --dir ~/vaults/my-project
graphify export html # auto-aggregates to community view if graph > 5000 nodes
# or: graphify export html --no-viz
注意 export html 的注释:当图超过 5000 个节点时会自动降级为社区聚合视图——这个阈值在整个代码库里是反复出现的常量,例如 graphify/cli.py 中 node_limit = 5000 及“falling back to community-aggregation view (node_limit=5000)”的路径,以及 graphify/analyze.py 的节点数分支。可视化大图的本地渲染压力是 graphify 从构建到导出一路设防的核心约束之一(__main__.py 的帮助文本也专门提示 --no-viz 适合 >5000 节点或 CI 场景)。
十二、Steps 6b–8:按 flag 触发的导出与基准
这一批只在对应 flag 出现时运行:--wiki、--neo4j / --neo4j-push、--falkordb / --falkordb-push、--svg、--graphml、--mcp;此外当语料 total_words 超过 5,000 时自动触发 token 缩减基准(benchmark)。默认无导出 flag 的运行会全部跳过。每个导出的细节见 graphify/skills/agents/references/exports.md。
各开关的职责与产物:
| Flag | 产物 / 行为 |
|---|---|
--wiki |
生成 Agent 可爬取的 wiki:index.md + 每个社区一篇文章 |
--neo4j |
生成 graphify-out/cypher.txt(Neo4j 导入脚本) |
--neo4j-push <bolt://host:7687> |
直接把图推送到运行中的 Neo4j |
--falkordb |
生成 graphify-out/cypher.txt(FalkorDB 导入脚本) |
--falkordb-push <falkordb://host:6379> |
直接推送到 FalkorDB |
--svg |
额外导出 graph.svg(可内嵌 Notion / GitHub) |
--graphml |
导出 graph.graphml(供 Gephi、yEd 使用) |
--mcp |
启动 MCP stdio server 供 Agent 访问 |
顺序要求:任何 --wiki 导出都要赶在 Step 9 清理之前跑,因为此时 .graphify_labels.json 还在。仓库内另有 docker 化的 MCP 落地说明可参考 docs/docker-mcp-sqlite.md。
十三、Step 9:manifest、成本追踪、清理与会话汇报
13.1 写 manifest + 更新成本追踪 + 清理
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
from datetime import datetime, timezone
from graphify.detect import save_manifest
# Save manifest for --update
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding='utf-8'))
extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding='utf-8'))
from graphify.cli import _stamped_manifest_files
_corpus = detect.get('all_files') or detect['files']
_manifest_files = _stamped_manifest_files(_corpus, extract, Path('INPUT_PATH'))
_sem_types = ('document', 'paper', 'image')
_dispatched = {f for t, fl in detect['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 _corpus.values() for f in fl}
save_manifest(_manifest_files, root='INPUT_PATH', scan_corpus=_scan, clear_semantic=_cleared or None)
# Update cumulative cost tracker
input_tok = extract.get('input_tokens', 0)
output_tok = extract.get('output_tokens', 0)
cost_path = Path('graphify-out/cost.json')
if cost_path.exists():
cost = json.loads(cost_path.read_text(encoding='utf-8'))
else:
cost = {'runs': [], 'total_input_tokens': 0, 'total_output_tokens': 0}
cost['runs'].append({
'date': datetime.now(timezone.utc).isoformat(),
'input_tokens': input_tok,
'output_tokens': output_tok,
'files': detect.get('total_files', 0),
})
cost['total_input_tokens'] += input_tok
cost['total_output_tokens'] += output_tok
cost_path.write_text(json.dumps(cost, indent=2, ensure_ascii=False), encoding='utf-8')
print(f'This run: {input_tok:,} input tokens, {output_tok:,} output tokens')
print(f'All time: {cost[\"total_input_tokens\"]:,} input, {cost[\"total_output_tokens\"]:,} output ({len(cost[\"runs\"])} runs)')
"
rm -f graphify-out/.graphify_detect.json graphify-out/.graphify_extract.json graphify-out/.graphify_ast.json graphify-out/.graphify_semantic.json graphify-out/.graphify_analysis.json
find graphify-out -maxdepth 1 -name '.graphify_chunk_*.json' -delete 2>/dev/null
rm -f graphify-out/.needs_update 2>/dev/null || true
这个看似繁琐的收尾脚本背后是整套增量更新一致性设计,规范里的注释逐条对应 issue:
--update模式下,detect 的all_files携带全量语料、files只带变化子集;全量重建模式只填充files,fallback 覆盖它。root=把 manifest key 相对化到扫描根(与 build 同一基准),使落盘 manifest 跨 clone/跨机器可移植,后续--update能命中缓存而不是全部 miss(#1417)。- 只给真正产出输出的语义文件打 stamp:检测到但块失败/被遗漏的文件必须保持未 stamp,否则它会被误标为“已完成”,内容永久丢失,下次
--update也不会重排它(#2015)。代码文件总是打 stamp(AST 确定性);只有语义类型按输出门控。 - 本轮派发但未打 stamp 的文件仍残留旧
semantic_hash,必须清除(clear_semantic)让detect_incremental把它们当“已变更”重新入队(#1948)。 scan_corpus传原始全量语料(非 stamp 过滤子集),保证本轮起新被排除的根内文件被当作删除丢弃,而不是伪装成删除;未动文件的旧行仍保留(#1908)。- 成本追踪:
graphify-out/cost.json按 UTC 时间戳累积每次运行的 input/output token 与文件数,跨会话给出累计统计——这正好呼应 Honesty Rules 里“报告中永远显示 token 成本”。
13.2 会话汇报与引导式探索
向用户汇报时(未给 --obsidian 就不提 obsidian 行):
Graph complete. Outputs in PATH_TO_DIR/graphify-out/
graph.html - interactive graph, open in browser
GRAPH_REPORT.md - audit report
graph.json - raw graph data
obsidian/ - Obsidian vault (only if --obsidian was given)
然后把 GRAPH_REPORT.md 中的三段直接贴进对话(不全贴报告):God Nodes、Surprising Connections、Suggested Questions。接着主动提供探索:从报告里挑出最值得一问的 suggested question——通常是跨越最多社区边界、桥节点最出乎意料的那一个——并询问用户是否要追踪。
用户同意后,用 graphify query "[question]" 走图并带用户沿图结构理解答案:哪些节点相连、跨了哪些社区边界、路径揭示了什么。每段回答都要以自然的追问收尾(“this connects to X - want to go deeper?”),让整场会话像在导航一张地图,而不是一次性报告。Skill 的最后一句话定义了角色分工:图是地图,流水线跑完后,Agent 的角色是向导。
十四、已建图之后的子命令世界
14.1 子命令解释器守卫
运行 --update、--cluster-only、query、path、explain、add 之前,先确认 .graphify_python 存在;若缺失(例如用户删了 graphify-out/),先重新解析解释器:
if [ ! -f graphify-out/.graphify_python ]; then
GRAPHIFY_BIN=$(which graphify 2>/dev/null)
if [ -n "$GRAPHIFY_BIN" ]; then
PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!')
case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac
else
PYTHON="python3"
fi
mkdir -p graphify-out
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
fi
14.2 --update 与 --cluster-only
二者都是非默认子命令:--update 只重抽取新增/变更文件(增量),--cluster-only 在既有图上重跑聚类。完整流程见 graphify/skills/agents/references/update.md。增量语义由 manifest 支撑——graphify/detect.py 的 detect_incremental() 通过比对文件内容 hash(semantic_hash / AST hash)判断哪些需要重抽。仓库内测试 test_incremental.py、test_incremental_mtime_collision.py、test_office_incremental.py 对这一机制做了覆盖。
14.3 基于已有图的查询:/graphify query
当 graphify-out/graph.json 已存在且用户提出关于语料的问题时,从图上回答而不是重建:
graphify query "<question>"
执行 query 前需要先对问题做词表扩充:把问题映射到图自己使用的词汇,避免措辞不匹配把答案塌缩成噪音。如果 graphify query CLI 不可用,则退化为对 graphify-out/graph.json 的内联 NetworkX 遍历。回答只能依据图输出本身包含的内容,引用具体事实时给出 source_location。关于词表扩充步骤、BFS/DFS 遍历模式、--budget token 上限、NetworkX fallback、save-result 反馈回路,以及 /graphify path(两点最短路径)和 /graphify explain(节点白话解释)的流程,统一见 graphify/skills/agents/references/query.md。
14.4 add 与 --watch
/graphify add <url> 把 URL 抓取进语料(存到 ./raw)并更新图,可用 --author、--contributor 标记来源归属;--watch 监听目录变化、在代码变更时自动重建(无需 LLM)。两者都不属于默认构建,详见 graphify/skills/agents/references/add-watch.md。extract() 源码中每次运行清空进程内 tsconfig alias / XAML / Markdown 索引等缓存的状态,正是为了支撑 watch 与 MCP server 在单个长驻进程里反复调 extract()。
14.5 提交钩子与原生 AGENTS.md 集成
用户希望安装 post-commit 自动重建钩子、或把 graphify 接入项目的 AGENTS.md 时,见 graphify/skills/agents/references/hooks.md。
十五、Honesty Rules:诚实审计规则
这是 graphify 整个产品哲学的最终落点,Skill 用五条铁律约束 Agent 的输出边界:
- 永不编造一条边。 不确定就标
AMBIGUOUS。 - 永不跳过语料规模检查警告。
- 报告里永远展示 token 成本。
- 永不把凝聚度分数藏在符号后面——显示原始数字。
- 超过 5,000 节点的图,未经警告不跑 HTML 可视化。
对照实现可以发现这套规则不是口号:EXTRACTED / INFERRED / AMBIGUOUS 三类证据标记贯穿抽取 schema(见 graphify/skills/agents/references/extraction-spec.md),Step 9 强制写 cost.json,Step 4.5 的健康检查把 dangling/missing/collapsed 边如实呈现,HTML 的 5000 节点社区聚合视图在 graphify/cli.py 里有硬编码护栏。
十六、延伸阅读:把 Skill 规范映射回源码
想让“规范怎么规定”与“代码怎么实现”互相印证,可以从下面几条路径继续深挖:
| 规范主题 | 仓库源码 / 手册 |
|---|---|
| 文件检测与语料分类 | graphify/detect.py |
| AST 两趟式抽取与跨文件解析 | graphify/extract.py、graphify/extractors/ |
| 语义缓存(提示词指纹、版本语义) | graphify/cache.py |
| 建图(root 锚定、directed) | graphify/build.py |
| 社区发现与凝聚度 | graphify/cluster.py |
| God Nodes / Surprising Connections / 问题生成 | graphify/analyze.py |
| 图健康诊断 | graphify/diagnostics.py |
| 报告生成与 JSON 导出(含 shrink-guard) | graphify/report.py、graphify/export.py |
| 语义抽取 Gemini backend | graphify/llm.py |
| CLI 与 manifest 门控逻辑 | graphify/cli.py |
| 各子命令专项手册 | graphify/skills/agents/references/ 下的 github-and-merge.md、update.md、query.md、add-watch.md、exports.md、extraction-spec.md、transcribe.md、hooks.md |
| 概念文档 | docs/how-it-works.md、docs/node-summaries-rfc.md、graphify/ARCHITECTURE.md |
结语
把这份 Skill 规范当成 graphify 的“操作说明书 + 契约书”来读,能看到一套高度工程化的 Agent 编排:Fast Path 避免无谓重建、解释器探测解决多宿主 Python 歧义、AST 与语义两路并行且免 key 起步、缓存全部带提示词指纹防止升级污染、#479 shrink-guard 与 #1392 空图守卫保护已有产物、manifest 语义保证增量更新不丢内容,最后用五条 Honesty Rules 约束输出边界。掌握了这套协议,你既能在任意 Agent 宿主上稳定跑通一次 /graphify 全量构建,也能把已有的 graph.json 当作长期可查询的知识资产来导航与追问。
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 StartedRust0623
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