首页
/ graphify /graphify Skill 实战手册:把任意代码库构建成可查询知识图谱(Kiro Agent 运行规范与源码级解析)

graphify /graphify Skill 实战手册:把任意代码库构建成可查询知识图谱(Kiro Agent 运行规范与源码级解析)

2026-09-06 18:56:46作者:舒璇辛Bertina

把任何文件夹(代码、文档、论文、图片、音视频)交给 /graphify,它会在本地生成一个带社区检测、诚实审计轨迹、三种输出(交互式 HTML、GraphRAG 就绪的 JSON、自然语言 GRAPH_REPORT.md)的可查询知识图谱。本文以仓库内 Kiro 版 skill 手册为骨架,逐段拆解 skill 的完整运行规范(Steps 0–9、各子命令分派与诚实规则),并结合 graphify 包源码 说明每步背后真正执行的函数与状态文件,让读者既会照规范跑通全流程,也能读懂每一步的底层原理。

本文针对的场景是:作为 Agent(如 Kiro、Claude Code、Codex 等)在任意项目目录中调用 /graphify,把项目建构成持久知识图谱,然后通过 query / path / explain 回答代码库问题。你将掌握:全部 30+ 条命令行参数的含义与适用场景、从"检测文件 → 双通道抽取 → 构图 → 聚类 → 打标 → 导出 → 收尾汇报"的完整执行协议,以及诚实审计(EXTRACTED / INFERRED / AMBIGUOUS)等设计原则。

一、/graphify 是什么:定位与核心产出

/graphify 是 graphify 项目随各 AI 编码平台分发的 Agent 技能(skill)。仓库中同一份手册有 agents、claude、codexkiro 等多个平台副本,本文研究的 tools/skillgen/expected/graphify__skill-kiro.md 就是 skillgen 生成管线产出的 Kiro 版标准件,源文件为 graphify/skill-kiro.md

它的设计定位一句话概括:把任何文件夹变成可导航的知识图谱,并输出三类资产:

  • 交互式 HTMLgraph.html)—— 浏览器中浏览节点与边;
  • GraphRAG 就绪的 JSONgraph.json)—— 供下游程序/Agent 查询;
  • 纯语言 GRAPH_REPORT.md —— 人类可读的审计报告,内含 God Nodes、Surprising Connections、Suggested Questions。

图谱跨会话持久保存在 graphify-out/ 目录,天然带有诚实审计轨迹(每条边标注 EXTRACTED / INFERRED / AMBIGUOUS 三种置信来源),并通过社区检测浮现你原本不会想到去问的跨文档联系。

从 skill 的 frontmatter 看,它面向的使用场景是:任何关于代码库架构、文件关系或项目内容的问题——尤其是当 graphify-out/ 已存在时,这类问题应当优先当作一次 graphify 查询处理。

二、Usage 全命令族速查

skill 规定:当用户调用 /graphify --help/graphify -h(且无其他参数)时,Agent 必须逐字打印下方 Usage 区块并立即停止——不执行任何命令、不探测文件、不把路径默认成 .。下面是该区块的完整命令清单,并对每条给出了适用语义说明。

2.1 核心构建命令

命令 含义
/graphify 对当前目录跑完整流水线(含 HTML 可视化;需要构建 Obsidian 知识库时加 --obsidian
/graphify <path> 对指定路径跑完整流水线
/graphify https://github.com/<owner>/<repo> 先 clone 仓库,再对克隆结果跑完整流水线
/graphify https://github.com/<owner>/<repo> --branch <branch> 克隆指定分支后构建
/graphify <url1> <url2> ... 克隆多个仓库,各自构图后合并成一个跨仓库图谱

2.2 抽取与建图模式

命令 含义
/graphify <path> --mode deep 深度抽取模式,产出更丰富的 INFERRED 边
/graphify <path> --update 增量模式,只对新增/变更文件重新抽取
/graphify <path> --directed 构建有向图,保留 source→target 的边方向
/graphify <path> --whisper-model medium 用更大的 Whisper 模型提升音视频转写准确率
/graphify <path> --cluster-only 在既有图谱上只重跑聚类
/graphify <path> --no-viz 跳过可视化,只产出报告 + JSON

2.3 导出族

命令 含义
/graphify <path> --html HTML 默认已生成,此 flag 是 no-op
/graphify <path> --svg 额外导出 graph.svg(可嵌入 Notion、GitHub)
/graphify <path> --graphml 导出 graph.graphml(供 Gephi、yEd 使用)
/graphify <path> --neo4j 生成 graphify-out/cypher.txt 供 Neo4j 导入
/graphify <path> --neo4j-push bolt://localhost:7687 直接推送进 Neo4j
/graphify <path> --falkordb 生成 graphify-out/cypher.txt 供 FalkorDB 导入
/graphify <path> --falkordb-push falkordb://localhost:6379 直接推送进 FalkorDB
/graphify <path> --mcp 启动 MCP stdio 服务,供外部 Agent 访问
/graphify <path> --watch 监视文件夹,代码变更时自动重建(无需 LLM)
/graphify <path> --wiki 构建可被 Agent 爬取的 wiki(index.md + 每个社区一篇文章)
/graphify <path> --obsidian --obsidian-dir ~/vaults/my-project 把知识库写到自定义路径(如已存在的 vault)

2.4 增量摄取与查询族

命令 含义
/graphify add <url> 抓取 URL,存到 ./raw,并更新图谱
/graphify add <url> --author "Name" 标记内容作者
/graphify add <url> --contributor "Name" 标记语料贡献者
/graphify query "<question>" BFS 遍历 —— 提供宽泛上下文
/graphify query "<question>" --dfs DFS —— 追踪特定调用路径
/graphify query "<question>" --budget 1500 把回答控制在 N 个 token 以内
/graphify path "AuthModule" "Database" 两个概念间的最短路径
/graphify explain "SwinTransformer" 对某个节点的通俗语言解释

值得注意:--watch 是"无需 LLM"的自动重建模式,这与下一节要讲的"代码纯结构化抽取不需要 API key"设计一脉相承。

三、被调用时的决策协议(What You Must Do When Invoked)

skill 用一个有序协议约束 Agent 的启动行为,任何一次调用都必须先走完这个判断链:

  1. --help/-h 特判:只打印 Usage,不做事。
  2. Fast path(已有图谱):先检查当前工作目录(即执行命令的项目根)下是否存在 graphify-out/graph.json。若存在,且用户的请求是自然语言代码库问题("X 是怎么工作的?""谁调用了 Y?""追踪 Z 的数据流")而不是显式重建命令(--update--cluster-only、或隐含全新抽取的裸路径/URL),则跳过 Steps 1–5 直接跳到 query 章节,立即执行 graphify query "<question>"——不做 detect、不检查语料规模、不要求用户缩小范围。图谱已经建好,直接用。
  3. 路径默认值:未给路径时默认使用 .(当前目录),不得反问用户要路径。
  4. URL 识别:路径以 https://github.com/http://github.com/ 开头时按 GitHub URL 处理——先执行 Step 0,再继续处理解析后的本地路径。

这一节对应了 extract.pybuild.py 设计中的一个核心思想:构建是一次性的、查询是廉价的。图谱持久化在磁盘上(graph.jsonexport.py 的 to_json 写出),因此反复构建同一语料是浪费。

四、Step 0:GitHub 仓库克隆与多路径合并

只有当下述两种情形出现时才执行本步:路径是一个或多个 https://github.com/... URL,或是多个需要合并的本地子文件夹。纯本地单路径直接跳过本步。

克隆、跨仓库合并与 monorepo 流程的细节见仓库内随 skill 分发的 github-and-merge.md。完成克隆/合并解析后,用得到的本地路径继续后续步骤。多仓库场景最终会"各自建图再合并成一张跨仓库图",这与仓库里 merge 相关能力(dedup.pycross_repo_types.py)对应。

五、Step 1:确保 graphify 已安装(解释器探测协议)

graphify 的安装形态多样(uv tool、pipx、venv、系统级 pip),所以 skill 用一个多级解释器探测脚本确定当前可用的 Python 解释器,并把结果持久化到 graphify-out/.graphify_python,后续每个 bash 代码块统一用 $(cat graphify-out/.graphify_python) 代替 python3,保证跨调用一致。

# 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 → shebang → python3:uv tool 安装是当前 Mac/Linux 上最可靠的方式;其次从 graphify 可执行文件的首行 shebang 反推解释器;最后兜底 python3
  • 发行安装包名为 graphifyy(注意双 y),安装失败时还带 --break-system-packages 兜底,兼容 PEP 668 的现代系统 Python。
  • 判定安装成功的唯一标准是 python -c "import graphify" 能导入成功——成功后不打印任何东西,直接进入 Step 2。
  • 两个持久化状态文件各有用途:.graphify_python 记解释器路径;.graphify_root 记扫描根目录的绝对路径,供以后无参 graphify update 定位。

若中途解释器丢失(例如用户删掉了 graphify-out/),skill 的"子命令解释器守卫"(见第 13 节)会先重建 .graphify_python 再继续。

六、Step 2:检测文件并生成语料摘要

文件检测通过 graphify.detect.detect() 完成。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')
"

INPUT_PATH 换成用户实际提供的路径。产出后不要 cat 或打印 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 ...)

随后按如下规则行动:

  • 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 子目录及文件数,询问用户要在哪个子文件夹上运行,等用户回答后再继续
  • 其余情况:检测到视频文件则进入 Step 2.5,否则直接进入 Step 3。

从源码看,detect() 返回的 dict 结构包含 total_filestotal_wordsskipped_sensitivescan_root,以及 files 下按 code/document/paper/image/video 分类的文件列表(见 detect.py 的返回结构)。检测实现还尊重 .gitignore / .graphifyignore,支持 --exclude 模式锚定在扫描根并以最高优先级生效,且自 #873 起点目录默认参与索引——所以 .graphifyinclude 允许列表机制已经不再生效(若发现该文件会打印警告,detect.py)。词数统计走带 stat 签名的缓存(cached_word_count),避免未变更的 PDF/docx 每次被重复解析(#1656)。

七、Step 2.5:音视频转写(仅当检测到 video)

detect 返回的 video 文件数为 0,整步跳过。语料含视频/音频时,先按 transcribe.md 把音视频转成文本,之后把转录稿当作 doc 文件进入 Step 3。这也是前面 --whisper-model medium 参数的存在意义——通过换用更大的 Whisper 模型提升转写准确率。

八、Step 3:抽取实体与关系(双通道:AST + 语义)

Step 3 是整个流水线的核心,拆成两大部分:

  • 结构化抽取(structural):确定性 AST 解析,免费;
  • 语义抽取(semantic):LLM 参与,消耗 token。

两个硬性约定贯穿本节:

  1. 开始前必须追踪 --mode deep:若给定,必须向 Step B2 派发的每个子代理传 DEEP_MODE=true,全程不得丢失该状态。
  2. graphify 不需要 API key:绝不向用户索要 key,也绝不为缺 key 而阻塞。

8.1 "无需 API key" 的设计原则

这条约定值得单独展开,因为它决定了整条流水线的成本边界(skill 原文反复强调,甚至说"如果你发现自己正要因为缺 key 而提示、等待或停下,那是对本 skill 的误读"):

  • 纯代码语料走 AST,不用 LLM、不用任何 key——这也是最常见的 /graphify . 场景,会直接跳过语义抽取;
  • 语义抽取只针对文档、论文、图片:只有环境变量 GEMINI_API_KEY/GOOGLE_API_KEY 已经设置时才会用 Gemini;否则宿主 Agent 自己就是那个 LLM;
  • graphify 不读取 ANTHROPIC_API_KEYOPENAI_API_KEY 或任何其他厂商 key。若 Gemini key 缺失,最多打印一行提示后继续,不等待

    Tip: set GEMINI_API_KEY or GOOGLE_API_KEY to use Gemini for semantic extraction (pip install 'graphifyy[gemini]').

在支持派发子代理的主机上(如 Claude Code),无 Gemini key 时按 Part B 的写法派发子代理完成语义抽取;在不支持派发子代理、直接跑 CLI 的主机上,纯代码语料没有语义工作——直接写空语义文件(Part B 的 Fast path)继续到 Part C;含文档/论文/图片的语料则要么配 Gemini key,要么 Agent 亲自内联抽取。任何情况下都不要提示 ANTHROPIC_API_KEY

GEMINI_API_KEYGOOGLE_API_KEY 已设置,则调用 graphify.llm.extract_corpus_parallel(files, backend="gemini") 做语义抽取(见 llm.py),而非派发子代理。默认 Gemini 模型为 gemini-3-flash-preview,可通过 GRAPHIFY_GEMINI_MODEL 环境变量或 headless CLI 流程的 --model 覆盖。

8.2 Part A —— 代码文件的结构化 AST 抽取

AST 抽取与 Part B 的语义子代理并行运行(同一消息内同时启动),因为它们处理的是不同文件类型;在大型语料上并行能省下 5–15 秒,因为 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')
"

collect_files(把目录展开成文件列表)与 extract(对文件集合执行多语言 AST 解析)分别定义在 extract.py 的 collect_filesextract.py 的 extractcache_root=Path('INPUT_PATH') 会把抽取缓存定位到语料根,避免缓存进入被扫描语料。支持的语言解析器集中在 extractors/ 目录,覆盖 TS/JS、Python、Go、Rust、C/C++、C#、Java、Kotlin、Swift、PHP、Ruby、Pascal、SQL、Terraform 等数十种。

8.3 Part B —— 语义抽取(并行子代理)

Fast path(纯代码语料):若 detect 发现 docs、papers、images 全为 0,则跳过 Part B 直奔 Part C——AST 已覆盖代码,语义子代理无事可做。但必须先写一个空语义文件,因为 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')
"

非纯代码语料则严格按 B0→B3 执行:

B0 先查抽取缓存。 缓存键必须绑定"抽取提示词"(references/extraction-spec.md 的绝对路径,即与 SKILL.md 同目录分发的抽取提示,也就是 Step B2 交给每个子代理的那份 prompt)。这样当 graphify 升级改变了提示词时,旧提示产出的缓存条目会被重新抽取而非直接重放;提示词没变的则保留命中(#1939):

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

detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
all_files = [f for cat in ('document', 'paper', 'image') for f in detect['files'].get(cat, [])]
cached_nodes, cached_edges, cached_hyperedges, uncached = check_semantic_cache(all_files, root='INPUT_PATH', prompt_file='SPEC_PATH')
if cached_nodes or cached_edges or cached_hyperedges:
    Path('graphify-out/.graphify_cached.json').write_text(json.dumps({'nodes': cached_nodes, 'edges': cached_edges, 'hyperedges': cached_hyperedges}, ensure_ascii=False), encoding=\"utf-8\")
else:
    Path('graphify-out/.graphify_cached.json').unlink(missing_ok=True)
Path('graphify-out/.graphify_uncached.txt').write_text('\n'.join(uncached), encoding=\"utf-8\")
print(f'Cache: {len(all_files)-len(uncached)} files hit, {len(uncached)} files need extraction')
"

注意:只有 content 类文件(document/paper/image)才进语义抽取。代码已被 Part A 结构化覆盖,把每个类别都铺平会让子代理把每个源文件重读一遍(#1392);视频则已在 Step 2.5 转成文档。若 cached_nodes/cached_edges/cached_hyperedges 任一非空则(重新)写缓存文件,否则删除旧缓存 sidecar,避免 Part C 合并到过期的 .graphify_cached.json(#1392)。只对 .graphify_uncached.txt 中列出的文件派发子代理;若全部命中缓存则直接跳到 Part C。

B1 分块。.graphify_uncached.txt 按每块 20–25 个文件切分;每张图片独占一块(视觉需要独立上下文);切分时尽量把同一目录的文件归到同一块,让相关产物落进同一 chunk,提高跨文件关系被抽出的概率。派发前先打印耗时预估:所需代理数 ceil(未缓存非代码文件数 / 22),预计耗时约每个代理批次 45 秒(并行时总耗时 ≈ 45s × ceil(代理数/并行上限))。

B2 在单条消息里并发派发全部子代理。 关键约束:一个 chunk 对应一次 Agent tool 调用,所有调用必须写在同一响应里——若调用一个、等结果、再调用下一个,就变成了串行,违背了并行初衷。子代理类型必须是 subagent_type="general-purpose",绝不能用 Explore——它是只读的,无法把 chunk 结果写到磁盘,会导致抽取结果被静默丢弃。三个 chunk 的示例形态:

[Agent tool call 1: files 1-15, subagent_type="general-purpose"]
[Agent tool call 2: files 16-30, subagent_type="general-purpose"]
[Agent tool call 3: files 31-45, subagent_type="general-purpose"]

CHUNK_PATH 必须是绝对路径,推导方式为 ${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json,其中 PROJECT_ROOT=$(pwd) 是当前工作目录(Part C 正是在 cwd 下 glob graphify-out/,注意不是 .graphify_root 里记录的扫描目录,#1392)。子代理提示词模板由 extraction-spec.md 给出(JSON schema、node-ID 规则、置信度评定量表、frontmatter、超边与视觉规则),只有某块确实含 doc/paper/image 时才加载该文件;纯代码语料已跳过 Part B,永远不会读它。

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.不要静默跳过
  • 子代理失败或返回非法 JSON → 打印警告并跳过该 chunk,不中断流程。

超过一半 chunk 失败或缺失时,停下让用户重跑并确认使用了 general-purpose。合并前注意:每个 Agent 调用返回后,要读取其真实 token 数(usage 字段)写回 chunk JSON(chunk 文件里的 token 数永远是占位零)。随后把各 chunk 合并成 .graphify_semantic_new.json,调用 save_semantic_cache 存入缓存(再次传入与 B0 相同的 SPEC_PATH,保证读写同一 prompt 指纹,见 cache.py 的 save_semantic_cache),再把缓存命中与新增结果按节点 id 去重合并成最终 .graphify_semantic.json,最后清理临时文件(.graphify_cached.json.graphify_uncached.txt.graphify_semantic_new.json)。缓存读写两侧的核心函数分别位于 cache.py 的 check_semantic_cachesave_semantic_cache

8.4 Part C —— 合并 AST 与语义结果

AST 节点在前、语义节点按 id 去重追加,边与超边直接拼接,最终落盘为 .graphify_extract.json

$(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\"))
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\")
print(f'Merged: {len(merged_nodes)} nodes, {len(merged_edges)} edges ({len(ast[\"nodes\"])} AST + {len(sem[\"nodes\"])} semantic)')
"

九、Step 4:建图、聚类、分析与导出

本步把抽取产物构建成 NetworkX 图,跑社区检测、打分、找 God Nodes / Surprising Connections / 候选问题,并先导出 JSON 再写报告。执行前先把 IS_DIRECTED 替换为:给了 --directed 则为 True(构建保留 source→target 方向的 DiGraph),否则 False(默认无向 Graph)。

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\"))
G = build_from_json(extraction, root='INPUT_PATH', directed=IS_DIRECTED)
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}
questions = suggest_questions(G, communities, labels)
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:签名 (extraction, *, directed=False, root=None),见 build.pyroot=--update 的运行规范(#1361)对齐:把语义子代理产出的绝对 source_file 相对化到同一基准,保证全量构建与增量 --update 在重新抽取时不会漂移。构建过程还会做多项兼容规范化:旧版 linksedgessourcesource_filenamelabel 等字段别名折叠、数字 id 强制转 str(#2326)、缺失 file_type 默认补 concept,以及确定性语义 re-key(#1504/#1509):非 AST 节点 id 按其 source_file 由代码重推导,让 LLM 漂移的旧式短 id 在物理上与 AST 节点对齐,杜绝"ghost 节点"与重复计费。
  • 空图守卫:任何写操作之前若 number_of_nodes()==0,立即报错退出,防止空抽取覆盖掉好的 graph.json / GRAPH_REPORT.md / analysis sidecar(#1392)。
  • cluster:使用 Leiden 社区检测(有向图先转无向,cluster.py)。社区 id 跨运行稳定(0 = 拆分后最大社区);超大社区(超过全图节点 25%、最少 10 个)会在子图上做第二次 Leiden 拆分;孤立点各成单节点社区;可选 exclude_hubs_percentile 把超度节点从分区中排除、再按多数邻居社区回贴(防止"脚手架超级枢纽"抬高 God Node 排名,对应 issue #919);低内聚社区还会做二次拆分。score_all 基于 cohesion_score:社区内实际边数除以最大可能边数。
  • god_nodes:返回度最高的真实实体(见 analyze.py),并排除文件级枢纽节点(它们机械地累积 import/contains 边)、concept 节点与 JSON 噪声键节点——因为它们不代表有意义的架构抽象。
  • surprising_connections:多文件语料取真实实体间的跨文件边(按 AMBIGUOUS → INFERRED → EXTRACTED 排序);单文件语料退化为跨社区桥接边(基于边的介数中心性),见 analyze.py。概念节点被排除,因为它们是有意注入而非被发现的。
  • to_json收缩守卫(#479):新图节点数小于既有 graph.json 时返回 False 且什么都不写。因此先导出、后写报告——GRAPH_REPORT.md 与 analysis sidecar 只在图真正落盘后才写,保证它们永远描述 graph.json 实际包含的图(#1392)。缩容是故意为之(删了文件)时,用 --force 重跑全量构建。
  • generatereport.py)产出的 GRAPH_REPORT.md 是纯语言审计报告。分析 sidecar .graphify_analysis.json 保存 communities、cohesion、gods、surprises、questions,供 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).')
"

IS_DIRECTEDINPUT_PATH 的替换同 Step 4。诊断函数 diagnose_extraction 定义在 diagnostics.py。若打印出 GRAPH HEALTH WARNING必须在最终汇报中带上该警告(不中断——图仍可用,但按诚实规则必须让完整性问题可见)。

十一、Step 5:给社区打标

读取 .graphify_analysis.json,对每个社区 key,看它的节点标签,手写一个 2–5 个词的通俗名字(如 "Attention Mechanism"、"Training Pipeline"、"Data Loading"),随后用真实标签重建报告并把标签存给可视化器:

$(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 = LABELS_DICT
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\")
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_DICT 换成真实构造的字典(如 {0: "Attention Mechanism", 1: "Training Pipeline"})。为什么要重跑一遍 suggest_questions?因为标签影响提问措辞suggest_questionsanalyze.py)。to_json 这次带着 community_labels 再导出一次,让 graph.json 的节点携带整理过的 community_name(#2490)。若收缩守卫仍拒绝(本次与 Step 4 用的是同一份 extraction,理论上节点数一致可通过;真拒绝则把守卫消息展示给用户,不要强行绕过)。

十二、Step 6:Obsidian vault(opt-in)与 HTML

HTML 无条件生成(除非 --no-viz);Obsidian vault 仅在显式给了 --obsidian 时生成,否则跳过——因为它会为每个节点生成一个文件,代价不小。

Obsidian(给定了 --obsidian-dir <path> 就通过 --dir 传入,否则默认 graphify-out/obsidian):

graphify export obsidian
# or with custom dir: graphify export obsidian --dir ~/vaults/my-project

HTML(无条件,除非 --no-viz):

graphify export html  # auto-aggregates to community view if graph > 5000 nodes
# or: graphify export html --no-viz

注意 HTML 导出超过 5000 节点时自动聚合成社区视图——这也呼应了诚实规则里"绝不在 5000 节点以上的图上跑 HTML 可视化而不警告用户"。Obsidian/HTML 导出的底层实现在 export.pyexporters/html.pygraphdb.pybase.py)中。

十三、Steps 6b–8:按 flag 触发的可选导出

以下步骤只在对应 flag 出现时执行--wiki--neo4j/--neo4j-push--falkordb/--falkordb-push--svg--graphml--mcp),或当 total_words 超过 5,000 时跑 token 削减基准。默认无导出 flag 的运行会全部跳过。每个导出细节见随 skill 分发的 exports.md。任何 --wiki 导出都要赶在 Step 9 清理之前执行,以保证 .graphify_labels.json 仍可用。

十四、Step 9:manifest、成本跟踪、清理与汇报

收尾阶段做四件事:保存增量 manifest、更新累计成本跟踪、清理中间文件、向用户汇报。

保存 manifest(为后续 --update 服务)。其正确性逻辑相当讲究(skill 原文有详尽注释):

  • --update 模式下 all_files 携带全量语料、files 是变更子集;全量重建模式只填充 files,因此用 fallback 兼容。
  • root= 把 manifest 键相对化到扫描根(与构建同一基准),使磁盘上的 manifest 跨克隆/跨机器可移植,后续 --update 能命中缓存而非全部 miss(#1417)。
  • 只为真实产出语义输出的文件盖章(docs/papers/images):某个文件若其 chunk 失败或被遗漏,必须保持"未盖章",这样下次 --update 会重新排队它;否则它会被标记完成、内容永远丢失(#2015)。代码文件永远盖章(AST 是确定性的);只有语义类型按输出把关。这一步精确镜像库内抽取路径:cli._stamped_manifest_files(定义于 cli.py)+ clear_semantic + scan_corpus
  • 本次派发但未盖章的语义文件仍携带上次的陈旧 semantic_hash,必须清掉,让 detect_incremental 重新排队而非当作未变更(#1948)。
  • scan_corpus 使用原始全量语料(不是盖章过滤后的子集),使根目录内自上次运行起被排除的文件按"删除"处理而非伪装成新增;未触碰文件的历史行仍被保留(#1908)。
$(cat graphify-out/.graphify_python) -c "
import json
from pathlib import Path
from datetime import datetime, timezone
from graphify.detect import save_manifest
from graphify.cli import _stamped_manifest_files

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\"))
_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)

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

save_manifest 与增量探测 detect_incremental 的实现分别在 detect.pydetect.pycost.json 用 UTC ISO 时间戳累计每次运行的 input/output token 与文件数——诚实规则里"始终在报告中展示 token 成本"在实现层面的落点。

向用户汇报的固定格式(没给 --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——不要贴整份报告。紧接着主动引导探索:从报告里挑出最有趣的建议问题(跨越最多社区边界、或含最惊奇桥节点的那个),向用户提问 "The most interesting question this graph can answer: [question]. Want me to trace it?"。用户同意后执行 /graphify query "[question]",用图结构带用户走答案——哪些节点相连、跨越了哪些社区边界、路径揭示了什么——并让每次回答以自然的后续问题收尾,让会话像"导航"而非一次性报告。skill 原文对此的概括值得记住:图谱是地图,流水线结束之后你的角色是向导

十五、子命令分派:解释器守卫与各专项流程

在执行任何子命令(--update--cluster-onlyquerypathexplainadd)之前,先检查 .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

各专项流程统一委托给随 skill 分发的 reference 文档,仓库内对应文件位于 graphify/skills/kiro/references/

命令族 行为 参考文档
--update / --cluster-only 均为非默认子命令:前者只对新增/变更文件重抽取,后者在既有图上重跑聚类 update.md
/graphify query 图谱已存在时回答语料问题,从图查而非重建 query.md
/graphify add / --watch 抓 URL 入语料 / 监视变更自动重建 add-watch.md
commit hook / CLAUDE.md 集成 安装 post-commit 自动重建钩子、把 graphify 接进项目 CLAUDE.md hooks.md

关于 /graphify query 的展开:当 graphify-out/graph.json 已存在、用户提出语料相关问题时,直接回答而非重建:

graphify query "<question>"

遍历之前,要先用图自身的词汇表展开问题,避免措辞不一致把答案压成噪声。若 graphify query CLI 不可用,退化为对 graphify-out/graph.json 的内联 NetworkX 遍历。回答只依据图输出所包含的内容,引用具体事实时标注 source_location。词汇展开步骤、BFS/DFS 遍历模式、--budget 上限、NetworkX 兜底、save-result 反馈,以及 /graphify path/graphify explain 的流程,全部细节见 query.md

十六、诚实规则(Honesty Rules)

这是 graphify 全链路最核心的行为底线,skill 要求每条都严格执行:

  • 绝不编造边。不确定就用 AMBIGUOUS 置信度标注。
  • 绝不跳过语料过大警告
  • 始终在报告中展示 token 成本
  • 绝不用符号掩盖内聚分数——展示原始数字。
  • 绝不在超过 5000 节点的图上跑 HTML 可视化而不警告用户

这条规则与其实现一一对应:节点与边的置信度标注来自抽取协议(EXTRACTED / INFERRED / AMBIGUOUS 三档),token 成本由 Step 9 的 cost.json 持续累计,内聚分数由 cluster.py 的 cohesion_score 直接给出原始比值,超大图聚合由 HTML 导出的社区视图自动承担。

十七、graphify-out/ 状态文件速查

把整条流水线串起来看,graphify-out/ 目录中的文件就是 pipeline 各阶段的"可见状态"。以仓库根目录为基准,全流程产生的文件与生命周期如下表:

文件 作用 产生/消费
.graphify_python 解析出的 Python 解释器路径,跨调用持久 Step 1 写,所有步骤读
.graphify_root 扫描根绝对路径,供无参 update 定位 Step 1 写
.graphify_detect.json detect 结果(total_files/total_words/skipped_sensitive/scan_root/files) Step 2 写,Step 3 读,Step 9 清理
.graphify_ast.json Part A AST 抽取结果 Step 3 写
.graphify_semantic.json 语义抽取合并终稿 Step 3 写,Part C 读
.graphify_semantic_new.json / .graphify_cached.json / .graphify_uncached.txt / .graphify_chunk_NN.json 语义管线中间件 Step 3 内产生并清理
.graphify_extract.json AST + 语义最终合并稿 Step 3 写,Step 4/4.5/5 读,Step 9 清理
.graphify_analysis.json communities/cohesion/gods/surprises/questions Step 4 写,Step 5 读,Step 9 清理
.graphify_labels.json 社区人工标签 Step 5 写,供可视化/wiki
graph.json 最终图(GraphRAG 就绪) Step 4 首写、Step 5 带标签重写,持久保留
GRAPH_REPORT.md 纯语言审计报告 Step 4 写、Step 5 重写,持久保留
cost.json token 成本累计 Step 9 更新,持久保留
manifest(由 save_manifest 落盘) 增量更新清单 Step 9 写,update 读
graph.html / graph.svg / graph.graphml / cypher.txt / obsidian/ / wiki 各导出产物 按 flag 产生

结语:从 skill 到源码的一条主线

通读这份 Kiro 版 skill 手册,一条清晰的主线浮现出来:构建贵、查询廉、重建要增量、输出必须诚实。每一步操作规范都能在仓库源码中找到对应实现——确定性 AST 抽取的 extract.py、提示词绑定的语义缓存 cache.py、Leiden 聚类与内聚打分的 cluster.py、带 #479 收缩守卫的导出 export.py、只读完整性诊断 diagnostics.py、以及把增量正确性落实到"盖章"粒度的 detect.pycli.py。若要亲手复现一次完整运行,直接对照本文第 4 至第 14 节的 Steps 0–9 执行即可;若要深入了解语义抽取的 JSON schema、节点 ID 规则或查询的遍历策略,则继续读仓库内随 skill 分发的 extraction-spec.mdquery.md

登录后查看全文
热门项目推荐
相关项目推荐