graphify Windows 使用指南:在 PowerShell 中把任意代码库变成可查询的知识图谱

graphify Windows 使用指南:在 PowerShell 中把任意代码库变成可查询的知识图谱

【免费下载链接】graphify Turn 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. 【免费下载链接】graphify 项目地址: https://gitcode.com/GitHub_Trending/graph/graphify

graphify 是 GitHub Trending 精选仓库中一个面向 Agent 的「知识图谱生成器」技能(skill):它对代码、文档、论文、图片与视频执行本地确定性的 AST 抽取与可选的 LLM 语义抽取,产出一份带社区发现、可信审计线索与三种产物(交互式 HTML、GraphRAG 就绪 JSON、纯文本 GRAPH_REPORT.md)的可查询知识图谱。graphify/skill-windows.md 是 graphify 的 Windows 专属运行手册:它与 claudecodexgemini 等平台版本共享同一套 SKILL.md 协议,但所有命令都针对 PowerShell 重新实现,逐一解决了 Windows 特有的解释器发现、BOM 编码、ANSI 终端滚动与跨 shell 兼容问题。读完本文,你将能在 Windows(PowerShell 5.1 / 7+、Windows Terminal)上完整走通「安装检测 → 语料扫描 → 双通道抽取 → 建图聚类 → 社区标注 → 导出可视化 → 增量更新 → 图上问答」的全部环节,并理解每条 PowerShell 命令背后的源码级原理。

一、这份文档在讲什么:graphify 的 Windows 运行手册

仓库内 graphify/ 目录下有一组面向不同 Agent 宿主平台的 skill 文档(skill-agents.mdskill-codex.mdskill-vscode.mdskill-windows.md 等),它们都是同一份技能内容在不同宿主上的「安装与执行方言」。其中 skill-windows.md 的定位可以从其 YAML frontmatter 读出:

---
name: graphify
description: "Use for any question about a codebase, its architecture, file relationships, or project content — especially when graphify-out/ exists, where the question should be treated as a graphify query first. Turns any input (code, docs, papers, images, videos) into a persistent knowledge graph with god nodes, community detection, and query/path/explain tools."
---

即:任何关于代码库「架构、文件关系、项目内容」的问题都应优先走 graphify;它能把任意文件夹变成一份「可导航的知识图谱 + 社区发现 + 诚实审计线索」,并产出三类产物:

  • 交互式 graph.html(可视化,HTML 默认生成);
  • GraphRAG 就绪的 graph.json(可继续喂给图数据库或图查询工具);
  • 纯文本 GRAPH_REPORT.md(上帝节点、惊喜连接、建议问题等可读报告)。

在 Windows 上运行这套技能的全部规范性步骤,就是本文要讲解的主题。技能文档同时确认:graphify 的确定性 AST 抽取不需要任何 API Key,纯代码语料在无 Key 场景即可完成;语义抽取只有在语料含文档/论文/图片/视频时才需要 LLM,且只认 GEMINI_API_KEY / GOOGLE_API_KEY(详见下文第四节)。

二、快速上手:/graphify 的完整命令面(Usage 全量解读)

文档开头的 Usage 段定义了整套 CLI 命令面。下表是它的完整继承与参数化解析,覆盖从「一次性全量建图」到「图数据导出」到「图上问答」的全部入口:

/graphify                                             # 对当前目录跑完整流水线(HTML 可视化;加 --obsidian 生成 vault)
/graphify <path>                                      # 对指定路径跑完整流水线
/graphify https://github.com/<owner>/<repo>           # clone 仓库后对其跑完整流水线
/graphify https://github.com/<owner>/<repo> --branch <branch>  # clone 指定分支
/graphify <url1> <url2> ...                           # clone 多仓库、分别建图并合并成一张跨仓库图
/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
/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 server 供 Agent 访问
/graphify <path> --watch                              # 监听文件夹,代码变化时自动重建(无需 LLM)
/graphify <path> --wiki                               # 构建可被 Agent 爬取的 wiki(index.md + 每个社区一篇)
/graphify <path> --obsidian --obsidian-dir ~/vaults/my-project  # 把 vault 写入自定义路径(例如已有 vault)
/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 tokens
/graphify path "AuthModule" "Database"                # 两个概念间的最短路径
/graphify explain "SwinTransformer"                   # 对某节点给出通俗解释

这些命令与仓库中 CLI 的实际子命令一一对应。例如 graphify/__main__.py--help 输出里可以看到:pathexplainquery --dfs/--budget/--graphaffectedgod-nodesexport obsidian|html|svg|graphml|neo4j|falkordb|wikiadd --author/--contributor/--dirwatchupdate --force/--no-clustercluster-only --no-label/--backend/--modelmerge-graphs --branch/--outclone 等均有落地实现,/graphify 命令面就是这套 Python CLI 的 Agent 化封装。

2.1 三个使用约束(何时触发哪个流程)

  1. /graphify --help/graphify -h(无其他参数):只原样打印上文 Usage 段并停止——不运行任何命令、不做文件检测、不把路径默认成 .
  2. 快速路径(fast path)——已有图时优先查询:动作前先检查当前工作目录下是否存在 graphify-out/graph.json。若存在,且用户的请求是关于代码库的自然语言问题(如 "How does X work?"、"What calls Y?"、"Trace the data flow through Z"),且不是显式重建命令(--update--cluster-only、或裸路径/URL 这类暗示全新抽取的参数),则跳过第 1–5 步直接进入 query 流程:立即执行 graphify query "<question>",不做 detect、不查语料规模、不要求用户收窄问题——图已经建好,直接用。
  3. 路径规则:未给路径时默认 .(当前目录),不要反问用户要路径;路径以 https://github.com/http://github.com/ 开头时视为 GitHub URL,先执行 Step 0 再以解析后的本地路径继续。

2.2 Step 0:GitHub 仓库与多路径合并

仅在路径是一个或多个 https://github.com/... URL、或若干本地子文件夹需要合并时执行。克隆、跨仓库合并与 monorepo 流程见 github-and-merge.md;普通本地路径直接跳过此步。仓库源码中对应实现了 _clone_repomerge-graphsclone 等命令(见 graphify/main.py)。

三、Step 1:确保 graphify 已安装(Windows 解释器发现全流程)

这是整份文档 Windows 特色最浓 的一步。在 PowerShell 中,问题从来不是「有没有 Python」,而是「哪个 Python 装了 graphify」——它可能是 uv tool 安装的隔离解释器、pipx 管理的 venv,也可能就是当前激活的 venv / conda 环境。文档给出的 Find-GraphifyPython 函数按权威优先级探测:

# Detect Python with graphify — uv/pipx-aware (fixes #831)
New-Item -ItemType Directory -Force -Path graphify-out | Out-Null
$GRAPHIFY_PYTHON = $null

function Find-GraphifyPython {
    # 1. uv tool install — 'uv tool dir' is authoritative, respects UV_TOOL_DIR automatically
    if (Get-Command uv -ErrorAction SilentlyContinue) {
        $uvDir = (uv tool dir 2>$null).Trim()
        if ($uvDir) {
            $py = Join-Path $uvDir "graphifyy\Scripts\python.exe"
            if (Test-Path $py) {
                & $py -c "import graphify" 2>$null
                if ($LASTEXITCODE -eq 0) { return $py }
            }
        }
    }
    # 2. pipx install — 'pipx environment' respects PIPX_HOME automatically
    if (Get-Command pipx -ErrorAction SilentlyContinue) {
        $venvs = (pipx environment --value PIPX_LOCAL_VENVS 2>$null).Trim()
        if ($venvs) {
            $py = Join-Path $venvs "graphifyy\Scripts\python.exe"
            if (Test-Path $py) {
                & $py -c "import graphify" 2>$null
                if ($LASTEXITCODE -eq 0) { return $py }
            }
        }
    }
    # 3. Active venv / conda / pip-into-current-env
    $pyCmd = Get-Command python -ErrorAction SilentlyContinue
    if ($pyCmd) {
        & $pyCmd.Source -c "import graphify" 2>$null
        if ($LASTEXITCODE -eq 0) {
            return (& $pyCmd.Source -c "import sys; print(sys.executable)").Trim()
        }
    }
    return $null
}

# Try to find the right Python (uv → pipx → active env)
$GRAPHIFY_PYTHON = Find-GraphifyPython

# Not found — install then re-detect
if (-not $GRAPHIFY_PYTHON) {
    if (Get-Command uv -ErrorAction SilentlyContinue) {
        uv tool install --upgrade graphifyy -q 2>&1 | Select-Object -Last 3
    } else {
        pip install graphifyy -q 2>&1 | Select-Object -Last 3
    }
    $GRAPHIFY_PYTHON = Find-GraphifyPython
}

# Save interpreter path — all subsequent steps read this.
# `Out-File -Encoding utf8` always writes a BOM on Windows PowerShell 5.1 (utf8NoBOM
# only exists from PowerShell 6), and that BOM rides into the saved path, so the hook
# rebuild fails with WinError 123 (#3028). WriteAllText with an explicit BOM-less
# encoding writes the bytes POSIX writes, and adds no trailing newline.
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_python'), [string]$GRAPHIFY_PYTHON, $Utf8NoBom)
# Save scan root so `graphify update` (no args) knows where to look next time
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_root'), (Resolve-Path INPUT_PATH).Path, $Utf8NoBom)

3.1 探测优先级:uv → pipx → 活动环境

从源码结构看,探测顺序是有意的:uv tool diruv tool install权威目录,且自动遵循 UV_TOOL_DIRpipx environment --value PIPX_LOCAL_VENVS 同理遵循 PIPX_HOME。两处路径都指向名为 graphifyy\Scripts\python.exe 的 venv 解释器——注意这里的包名是 graphifyy(双 y),这是它在 PyPI 上的发布名,源码里 from importlib.metadata import version("graphifyy")graphify/main.py)也能佐证这一命名。第三级才回退到 python 命令所在解释器。若都找不到,就现场安装:优先 uv tool install --upgrade graphifyy,否则 pip install graphifyy,装完再探测一次。

3.2 两个关键细节:BOM 与 sidecar 文件

  • 为什么不能 Out-File:PowerShell 5.1 的 Out-File -Encoding utf8 总会写入 BOM(无 BOM 的 utf8NoBOM 要到 PowerShell 6 才有)。BOM 会混进保存的路径里,导致后续 hook 重建时以 WinError 123(文件名/目录名/卷标语法错误)失败(issue #3028)。因此文档用 New-Object System.Text.UTF8Encoding $false 构造无 BOM 编码,再经 [System.IO.File]::WriteAllText 写出与 POSIX 完全一致的字节,且不加尾部换行
  • .graphify_python.graphify_root 两个 sidecar:前者保存「装有 graphify 的解释器绝对路径」,后续所有代码块统一用 & (Get-Content graphify-out\.graphify_python) 来驱动 Python,确保每个步骤都走那个真正装了 graphify 的解释器(不能裸写 python3);后者保存扫描根目录,让无参的 graphify update 下次知道去哪找。

运行约定:导入成功则什么都不打印,直接进入 Step 2。

3.3 子命令的解释器保护(Interpreter guard)

--update--cluster-onlyquerypathexplainadd 等子命令执行前都要先检查 graphify-out\.graphify_python 是否存在;若缺失(例如用户删了 graphify-out/),先按下面逻辑重新解析解释器再继续:

if (-not (Test-Path graphify-out\.graphify_python)) {
    $GRAPHIFY_PYTHON = $null
    $graphifyCmd = Get-Command graphify -ErrorAction SilentlyContinue
    if ($graphifyCmd) {
        # The interpreter that owns the graphify entry point sits next to it
        # (<env>\Scripts\python.exe for uv tool, pipx, and venv installs).
        $py = Join-Path (Split-Path $graphifyCmd.Source) "python.exe"
        if (Test-Path $py) { $GRAPHIFY_PYTHON = $py }
    }
    if (-not $GRAPHIFY_PYTHON) { $GRAPHIFY_PYTHON = "python" }
    New-Item -ItemType Directory -Force -Path graphify-out | Out-Null
    & $GRAPHIFY_PYTHON -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
}

这里的推论是:graphify 命令入口(console script)所在的 Scripts 目录旁必然有同一个 venv 的 python.exe——对 uv tool、pipx、venv 三种安装方式都成立,因此可借 graphify 可执行文件反推出解释器。这与 graphify/install.py 中安装子系统按平台解析 skill 目的地的思路一致。

四、Step 2:检测语料(detect)与规模守门

安装确认后进入扫描。Step 2 的 PowerShell 实现刻意用 here-string 喂给 Python,而不是 shell 重定向——这样同一段脚本在任何 PowerShell 宿主上渲染都不会出现控制台编码漂移(issue #2528):

@'
import json
from graphify.detect import detect
from pathlib import Path
result = detect(Path('INPUT_PATH'))
# Write the sidecar from Python, not a shell redirect, so the same block renders
# on PowerShell hosts without console-encoding drift (#2528).
Path('graphify-out/.graphify_detect.json').write_text(json.dumps(result, ensure_ascii=False), encoding="utf-8")
print(f'Detected {result["total_files"]} files')
'@ | & (Get-Content graphify-out\.graphify_python) -

执行时把 INPUT_PATH 替换为用户实际提供的路径。不要 cat 或打印 JSON——静默读取后向用户呈现一份干净摘要(某类为 0 则整行省略):

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 ...)

4.1 detect 的实现事实与三种分支

源码 graphify/detect.pydetect() 会遍历文件、按扩展名分类到 code/document/paper/image/video,返回 filestotal_filestotal_wordsneeds_graphwarningskipped_sensitiveunclassifiedignored 等字段。几个可验证的实现事实:

  • skipped_sensitive 记录被安全策略挡掉的文件,包括 symlink 目标越出扫描根非普通文件(命名管道/FIFO/socket/设备节点——这类文件 open() 会无限阻塞)、命中敏感名单的文件、Google Workspace 导出失败或 Office 转换失败的文件(graphify/detect.py);
  • 超大规模阈值时置位 needs_graph/warning

检测后的行动分支(完整继承):

  • total_files 为 0:停止并提示 "No supported files found in [path].";
  • skipped_sensitive 非空:报告数量并逐个列出被跳过文件名,让被误判的源文件/文档能被发现并改名或移走(issue #2106);
  • total_words > 2,000,000 total_files > 500:显示大语料警告,然后按文件数计算前 5 个一级子目录
    • 从 detect JSON 读 scan_root(永远是解析后 INPUT_PATH 的绝对路径);
    • code/document/paper/image/video 各类型文件列表全部拼起来;
    • 过滤掉以 scan_root + "/graphify-out/" 开头的路径(排除转换产生的 sidecar);
    • 对每个文件去掉 scan_root 前缀,取第一个路径组件;直接位于 scan_root 下无子目录的文件计为 (root)
    • 若所有文件都在 (root) 且没有子目录——不要建议收窄(根本没有子文件夹可选),改为建议 --no-cluster 跳过昂贵的聚类步骤并继续;
    • 否则按数量排序,展示前 5 名及文件数,询问用户要对哪个子文件夹运行,等待用户回答后再继续
  • 其余情况:若检测到视频文件则进入 Step 2.5,否则直接进入 Step 3。

4.2 Step 2.5:视频与音频(仅当检测到 video 时)

detect 返回的 video 文件数为 0,整步跳过。语料含视频/音频时,先按 transcribe.md 把它们转写成文本(对应 --whisper-model medium 等参数),再在 Step 3 中把转写稿当作文档文件处理。

五、Step 3:抽取实体与关系——「确定性 AST + 可选语义」双通道

动手前先记录一件事:若传入 --mode deep,则 Step B2 给每个子代理都必须传 DEEP_MODE=true,不能在中途丢掉该状态。

graphify 不需要 API Key——绝不向用户索要,也绝不因缺 Key 而阻塞。 代码用 AST 结构化抽取(无 LLM、无 Key);纯代码语料(最常见的 /graphify .)会整体跳过语义抽取,直接走 Part A 并跳过 Part B。语义抽取(只针对文档、论文、图片)仅在已设置 GEMINI_API_KEY / GOOGLE_API_KEY 时使用 Gemini;否则由宿主 Agent 自身充当 LLM。graphify 不读 ANTHROPIC_API_KEYOPENAI_API_KEY 或任何其他厂商 Key。若你正要因为缺 Key 而提示、等待或停下,那就是对这份技能的错误理解——继续执行即可。

语义抽取前检查环境:若 GEMINI_API_KEYGOOGLE_API_KEY 都未设置,向用户打印下面这行提示一次,然后继续(不要等用户提供 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 模型是 gemini-3-flash-preview,可用 GRAPHIFY_GEMINI_MODEL 环境变量或 headless CLI 的 --model 覆盖。这三点与源码完全吻合:LLM 后端配置中 default_model: "gemini-3-flash-preview"env_keys: ["GEMINI_API_KEY", "GOOGLE_API_KEY"]model_env_key: "GRAPHIFY_GEMINI_MODEL"graphify/llm.py)。

Part A(AST)与 Part B(语义)应并行启动:在同一轮消息里既派发全部语义子代理、又启动 AST 抽取——两者操作不同文件类型,可同时运行,最后在 Part C 合并。理由是大语料上并行能省 5–15 秒:AST 确定且快,趁子代理处理文档/论文时正好跑完。

5.1 Part A:代码文件的结构化抽取(AST)

@'
import sys, json
from graphify.extract import collect_files, extract
from pathlib import Path
import json

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')
'@ | & (Get-Content graphify-out\.graphify_python) -

源码侧 collect_files 负责把目录展开成文件清单(默认不跟随 symlink,见 graphify/extract.py),extract 执行真正的多文件抽取(graphify/extract.py),cache_root=Path('INPUT_PATH') 让 AST 结果也能享受缓存位置约定。抽取结果含 nodes/edges/token 计数,写入 sidecar .graphify_ast.json

5.2 Part B:语义抽取(并行子代理)

Fast path(纯代码语料):若检测到 0 个文档、论文、图片,直接跳过 Part B 进入 Part C——AST 已覆盖代码,没有语义工作可做。但必须先写一个空语义文件,因为 Part C 的合并无条件读取 .graphify_semantic.json,缺了它在纯代码运行时直接 FileNotFoundError

@'
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')
'@ | & (Get-Content graphify-out\.graphify_python) -

强制要求:必须使用 Agent 工具(子代理)并行处理,逐文件自己读是被禁止的——慢 5–10 倍。 派发前先打印耗时预估:从 .graphify_detect.jsontotal_words 与文件数;预计代理数 ceil(uncached_non_code_files / 22)(每块 20–25 个文件);预计时长约每个代理批次 45 秒(并行,所以总耗时 ≈ 45s × ceil(代理数/并行上限)),打印形如 "Semantic extraction: ~N files → X agents, estimated ~Ys"。

Step B0 — 先查抽取缓存。派发任何子代理前,先确认哪些文件已有缓存结果。SPEC_PATH 是本 SKILL.md 同目录的 references/extraction-spec.md绝对路径——它与 Step B2 交给每个子代理的是同一个文件,它就是抽取提示词。缓存条目被归属到该提示词:graphify 升级改动提示词时,旧提示词产出的缓存条目会被重新抽取而非回放;提示词未变则命中缓存(issue #1939)。Step B0 与 Step B3 必须传入同一个 SPEC_PATH,不可遗漏:

@'
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"))
# Only content files go to semantic extraction. Code is already covered structurally
# by the AST pass (Part A); flattening every category here makes subagents re-read
# every source file (#1392). Video is transcribed to a document in Step 2.5 first.
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')

# Always (re)write the cache file: write hits, else DELETE any leftover from a prior
# run so Part C never merges a stale .graphify_cached.json (#1392).
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')
'@ | & (Get-Content graphify-out\.graphify_python) -

只对 .graphify_uncached.txt 里列出的文件派发子代理;若全部命中缓存,直接跳到 Part C。源码 check_semantic_cache / save_semantic_cache 位于 graphify/cache.pygraphify/cache.py。注意代码文件(Part A 的 AST 已覆盖)不进入语义缓存检查,否则每个子代理都得重读全部源码(issue #1392)。

Step B1 — 分块。从 .graphify_uncached.txt 读取文件,按 20–25 个文件一组切块;每张图片独占一块(视觉需要独立上下文);切块时尽量把同一目录的文件放同一块,让跨文件关系更可能被抽出来。

Step B2 — 在单条消息里派发全部子代理。在同一次响应里多次调用 Agent 工具——每块一次调用,这是唯一能并行的方式。逐一等待再逐个调用就是串行,违背初衷。

  • 子代理类型必须subagent_type="general-purpose"绝不能用只读的 Explore——它无法写块文件到磁盘,会静默丢弃抽取结果;general-purpose 具备子代理写文件与执行命令所需的 Write/Bash 权限。
  • 3 个块的示例形态:
[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 = (Get-Location).Path(Part C 正是在 cwd 下 glob graphify-out\,注意不是 .graphify_root/scan 目录,issue #1392),第 N 块即 $CHUNK_PATH = Join-Path $PROJECT_ROOT "graphify-out\.graphify_chunk_0N.json"
  • 子代理提示词模板以 extraction-spec.md 为准(JSON schema、节点 ID 规则、置信度评分、frontmatter、超边与视觉规则)。仅在至少一个块含文档/论文/图片时加载它——纯代码语料已跳过 Part B、永不读取。把 FILE_LISTCHUNK_NUMTOTAL_CHUNKSDEEP_MODECHUNK_PATH 代入后逐字传给每个子代理,令其把结果写入 CHUNK_PATH

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.",不静默跳过;
  • 子代理失败或返回非法 JSON → 打印警告并跳过该块,不中止;
  • 超过一半块失败或缺失 → 停下,让用户重跑并确保用 subagent_type="general-purpose"

把全部块文件合并进 .graphify_semantic_new.json每个 Agent 调用完成后,从 Agent 结果的 usage 字段读出真实 token 数、写回块 JSON 再合并(块 JSON 内始终是占位零值)。随后执行合并脚本:

@'
import json, glob
from pathlib import Path

chunks = sorted(glob.glob('graphify-out/.graphify_chunk_*.json'))
all_nodes, all_edges, all_hyperedges = [], [], []
total_in, total_out = 0, 0
for c in chunks:
    d = json.loads(Path(c).read_text(encoding="utf-8"))
    all_nodes += d.get('nodes', [])
    all_edges += d.get('edges', [])
    all_hyperedges += d.get('hyperedges', [])
    total_in += d.get('input_tokens', 0)
    total_out += d.get('output_tokens', 0)
Path('graphify-out/.graphify_semantic_new.json').write_text(json.dumps({
    'nodes': all_nodes, 'edges': all_edges, 'hyperedges': all_hyperedges,
    'input_tokens': total_in, 'output_tokens': total_out,
}, indent=2, ensure_ascii=False), encoding="utf-8")
print(f'Merged {len(chunks)} chunks: {total_in:,} in / {total_out:,} out tokens')
'@ | & (Get-Content graphify-out\.graphify_python) -

新结果写入缓存,传与 Step B0 相同的 SPEC_PATH——它给每条缓存打上「产出它的提示词」戳,用不同提示词写入的位置下次 run 不会去找(issue #1939):

@'
import json
from graphify.cache import save_semantic_cache
from pathlib import Path

new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding="utf-8")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding="utf-8").splitlines() if line]
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached, prompt_file='SPEC_PATH')
print(f'Cached {saved} files')
'@ | & (Get-Content graphify-out\.graphify_python) -

把缓存 + 新结果合并进最终 .graphify_semantic.json(按节点 id 去重,超边取并集):

@'
import json
from pathlib import Path

cached = json.loads(Path('graphify-out/.graphify_cached.json').read_text(encoding="utf-8")) if Path('graphify-out/.graphify_cached.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding="utf-8")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}

all_nodes = cached['nodes'] + new.get('nodes', [])
all_edges = cached['edges'] + new.get('edges', [])
all_hyperedges = cached.get('hyperedges', []) + new.get('hyperedges', [])
seen = set()
deduped = []
for n in all_nodes:
    if n['id'] not in seen:
        seen.add(n['id'])
        deduped.append(n)

merged = {
    'nodes': deduped,
    'edges': all_edges,
    'hyperedges': all_hyperedges,
    'input_tokens': new.get('input_tokens', 0),
    'output_tokens': new.get('output_tokens', 0),
}
Path('graphify-out/.graphify_semantic.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding="utf-8")
print(f'Extraction complete - {len(deduped)} nodes, {len(all_edges)} edges ({len(cached["nodes"])} from cache, {len(new.get("nodes",[]))} new)')
'@ | & (Get-Content graphify-out\.graphify_python) -

清理临时文件:Remove-Item -Force -ErrorAction SilentlyContinue graphify-out\.graphify_cached.json, graphify-out\.graphify_uncached.txt, graphify-out\.graphify_semantic_new.json

5.3 Part C:合并 AST + 语义为最终抽取结果

@'
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)')
'@ | & (Get-Content graphify-out\.graphify_python) -

六、Step 4:建图、聚类、分析与产出

动手前:下方代码块把 directed=IS_DIRECTED 传给 build_from_json()。若给了 --directed,把 IS_DIRECTED 替换为 True(构建保留 source→target 方向的 DiGraph),否则 False(默认无向 Graph)。替换方式与替换 INPUT_PATH 相同——不要把字面量 IS_DIRECTED 留在代码里。

New-Item -ItemType Directory -Force -Path graphify-out | Out-Null
@'
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')
'@ | & (Get-Content graphify-out\.graphify_python) -

6.1 这一段调用的源码组件与它们各自的责任

从源码结构看,Step 4 汇聚了 graphify 的核心分析管线,每个函数都在仓库中有独立实现、可单独复用:

函数源码位置职责
build_from_json(extraction, root=..., directed=...)graphify/build.py从抽取 dict 构建 NetworkX 图;directed=True 时建 DiGraph
cluster(G)graphify/cluster.py社区检测:有 graspologic(Leiden)用 Leiden,否则回退 NetworkX Louvain
score_all(G, communities)graphify/cluster.py计算每个社区的 cohesion(内聚)分数
god_nodes(G)graphify/analyze.py找连接度最高的「上帝节点」(架构枢纽),默认 top 10
surprising_connections(G, communities)graphify/analyze.py发现跨越社区边界的「惊喜连接」
suggest_questions(G, communities, labels)graphify/analyze.py依据图结构与社区标签生成建议问题
generate(...)graphify/report.py生成 GRAPH_REPORT.md
to_json(G, communities, path, community_labels=...)graphify/export.py导出 graph.json,含 #479 收缩保护

6.2 两道守护:空图守护与 #479 收缩守护

代码里有两道写入前的保护,是理解运行结果的关键:

  1. 空图守护build_from_json 之后立刻检查 G.number_of_nodes() == 0。空的抽取结果绝不能覆盖掉一个好的 graph.json / GRAPH_REPORT.md / analysis sidecar(issue #1392)。触发时打印 "ERROR: Graph is empty - extraction produced no nodes." 及可能原因(全部文件被跳过、纯二进制语料、抽取失败),并 raise SystemExit(1)——不要继续到标注或可视化
  2. #479 收缩守护to_json 返回 False(什么都不写)当新图比已存在的 graph.json 更小。只有当图真的写出去了,才写 GRAPH_REPORT.md 和 analysis sidecar——否则报告描述的是一个 graph.json 里根本不存在的图(issue #1392)。触发时提示:"refused to shrink graphify-out/graph.json (existing graph has more nodes; #479)",若要删除文件后的有意的收缩,请用 --force 重跑全量构建。源码见 graphify/export.py 的 shrink 安全检查。

6.3 Step 4.5:图健康检查(只读完整性闸门)

这是建图后、标注前的一道非破坏性诊断:它暴露「边坍缩、悬空/缺失端点、自环」——正是增量更新与 AST/LLM 的 id 不一致引发的静默损坏模式。只读,永不中止流水线:

@'
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).')
'@ | & (Get-Content graphify-out\.graphify_python) -

IS_DIRECTEDINPUT_PATH 的替换规则同 Step 4。diagnose_extraction / format_diagnostic_report 的实现位于 graphify/diagnostics.pygraphify/diagnostics.py。若打印出 GRAPH HEALTH WARNING,必须把它写进最终摘要向用户展示(不中止——图仍可用,但按 Honesty Rules 完整性隐患必须可见)。

七、Step 5:社区标注(让 "Community 3" 变成 "Attention Mechanism")

读取 graphify-out/.graphify_analysis.json:对每个 community key,观察其节点标签,写一个 2–5 个词的通俗名称(如 "Attention Mechanism"、"Training Pipeline"、"Data Loading")。随后用真实标签重新生成报告并把标签交给可视化器:

@'
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"))

# root= as in Step 4 / the --update runbook (#1361) — same base for node-key parity.
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).
# Same extraction as Step 4, so the #479 shrink-guard passes on node count;
# if it still refuses, surface the guard message - do not force past it.
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')
'@ | & (Get-Content graphify-out\.graphify_python) -
  • LABELS_DICT 换成你构造的真实字典(例如 {0: "Attention Mechanism", 1: "Training Pipeline"}),INPUT_PATH 换成实际路径。
  • 标注后重新导出,使 graph.json 的节点携带精选的 community_name(issue #2490)。它基于与 Step 4 相同的抽取结果,因此 #479 收缩守护在节点数上必然通过;若仍拒绝,把守护消息展示出来即可,不要强行越过它
  • 注意一个易错点:communities = {int(k): v ...} 把 analysis JSON 里的字符串 key 转回 int(因为 JSON 对象键必须是字符串),与 cluster 返回的 int key 保持同构。

八、Step 6 / 6b–8:Obsidian、HTML 与按 flag 触发的导出

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;图超过 5000 节点时自动聚合到社区视图):

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

Steps 6b–8(wiki、Neo4j、FalkorDB、SVG、GraphML、MCP、benchmark)只在对应 flag 出现时运行--wiki--neo4j/--neo4j-push--falkordb/--falkordb-push--svg--graphml--mcp;token 削减 benchmark 则在 total_words 超过 5,000 时运行。默认无导出 flag 的 run 全部跳过。每个导出都对应 CLI 的 export ... 子命令(graphify/main.py),详细说明见 exports.md任何 --wiki 导出都要在 Step 9 清理之前跑,这样 .graphify_labels.json 仍然可用。

九、Step 9:保存 manifest、更新成本追踪、清理与汇报

@'
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"))
# In --update mode, 'all_files' carries the full corpus; 'files' is the changed
# subset. Full-rebuild mode populates only 'files', so the fallback handles that.
# root= relativizes the manifest keys to the scan root (same base as the build),
# so the on-disk manifest is portable across clones/machines and a later --update
# matches cached files instead of missing every one (#1417).
#
# Only stamp semantic files (docs/papers/images) that ACTUALLY produced output:
# a detected file whose chunk failed or was omitted must stay unstamped so the
# next --update re-queues it, otherwise it is marked done and its content is lost
# forever (#2015). This mirrors the library extract path exactly
# (cli._stamped_manifest_files + clear_semantic + scan_corpus); do not stamp the
# raw corpus. Code files are always stamped (AST is deterministic); only semantic
# types are gated on output.
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'))
# Files dispatched this run (the changed subset) but NOT stamped above still carry
# a stale semantic_hash from a prior run; clear it so detect_incremental re-queues
# them instead of reading them as unchanged (#1948).
_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_corpus = the RAW full corpus (not the stamp-filtered subset) so in-root
# files newly excluded since last run are dropped rather than masquerading as
# deletions; untouched files' prior rows are still preserved (#1908).
_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)')
'@ | & (Get-Content graphify-out\.graphify_python) -
Remove-Item -Force -ErrorAction SilentlyContinue 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
Get-ChildItem graphify-out -Filter '.graphify_chunk_*.json' -File -ErrorAction SilentlyContinue | Remove-Item -Force
Remove-Item -Force -ErrorAction SilentlyContinue graphify-out\.needs_update

9.1 manifest 的三条关键语义

Step 9 里 manifest 维护逻辑最为精密,其每条规则在源码 graphify/cli.py_stamped_manifest_filesgraphify/detect.pysave_manifest / detect_incrementalgraphify/detect.py)中有对应实现:

  • 可移植的 keyroot= 把 manifest 键相对化到扫描根(与建图同一 base),让磁盘上的 manifest 跨 clone / 跨机器可用,后续 --update 能命中缓存文件而不是全部 miss(issue #1417)。
  • 只给真正产出输出的语义文件盖章:被检测到但所在 chunk 失败/被跳过的文件必须保持未盖章,这样下次 --update 会重新排队它;否则它被标记为已完成、内容永久丢失(issue #2015)。代码文件总是盖章(AST 是确定性的),只有语义类型按输出把关。相应地,本轮派发但未被盖章的文件还携带上一次运行残留的 semantic_hash,要清掉它,否则 detect_incremental 会当作未变而跳过(issue #1948)。
  • scan_corpus 用原始全量语料:让本轮新被排除的文件被丢弃而不是伪装成删除;未动过的文件上一轮的记录仍被保留(issue #1908)。

9.2 向用户汇报

INPUT_PATH 替换为实际路径(与 Step 4–5 相同的值),让 manifest 相对化到扫描根。汇报模板如下(未给 --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)

PATH_TO_DIR 换成被处理目录的绝对路径。随后把 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]" 在图上跑一遍,借助图结构讲解答案——哪些节点相连、跨越了哪些社区边界、路径揭示了什么。每答完一题都以自然的后续问题收尾("this connects to X - want to go deeper?"),让会话像在导航地图而非交付一次性报告。文档的总结句是整份技能的哲学:The graph is the map. Your job after the pipeline is to be the guide.(图是地图,流水线之后你的职责是做向导。)

十、子命令流程:query / update / cluster-only / add / watch / hook

10.1 For --update 与 --cluster-only

两者都是非默认子命令。--update 只重抽取新增或变更文件(省 token、省时间);--cluster-only 在既有图上重跑聚类。完整流程见 update.md--update 的先导逻辑是调用 detect_incremental 得到 new_files/deleted_files 集合,再把变更子集写入 .graphify_detect.jsonfiles、全量语料写入 all_files,之后走与全量一致的 Step 3A–6——这是文档「同一份抽取脚本服务两种模式」的关键约定(issue #1361)。

10.2 For /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。该参考文档强调了一个值得注意的边界事实:graphify 的 query CLI 只做大小写折叠的子串 + IDF 匹配——没有词干化、同义词或跨语言匹配,所以「用户说中文词、图标签用英文」这类措辞鸿沟必须靠遍历前的词表扩充来解决。

10.3 For /graphify add 与 --watch

两者都不是默认构建的组成部分。/graphify add <url> 把 URL 抓取进语料,--watch 在文件变化时自动重建;见 add-watch.md

10.4 For commit hook 与原生 CLAUDE.md 集成

用户请求安装 post-commit 自动重建 hook、或把 graphify 接进项目的 CLAUDE.md 时,见 hooks.md

十一、疑难排查:PowerShell 5.1 垂直滚动失效

在 PowerShell 里运行 graphify 后若垂直滚动失灵,元凶是 graspologic 库输出的 ANSI 转义序列。graphify v0.3.10+ 已抑制这类输出,但若仍遇到,按顺序处理:

  1. 升级 graphifypip install --upgrade graphifyy
  2. 改用 Windows Terminal 替代传统 PowerShell 控制台——Windows Terminal 能正确处理 ANSI 码;
  3. 重置终端:关闭并重开 PowerShell;
  4. 跳过 graspologic:卸载它(pip uninstall graspologic),graphify 会回退到 NetworkX 内建 Louvain 算法,该算法不产生 ANSI 输出。

源码侧印证了问题根源与修复方向:graphify/cluster.py 的模块 docstring 说明社区检测「有 graspologic(Leiden)用 Leiden,否则回退 networkx Louvain」,并在 import 时用 _suppress_stdout 上下文管理器重定向 stdout/stderr 到 devnull——因为 graspologic 的 leiden() 会输出进度条与彩色警告等 ANSI 转义序列,污染 Windows 上 PowerShell 5.1 的滚动缓冲(issue #19);同时 _native_leiden 直接调用 graspologic_native(Rust 扩展)而绕开 graspologic 包整体 import——后者会连带 import umap/pynndescent 并触发 numba JIT 编译,带来显著的冷启动开销。

十二、Honesty Rules:诚实规则(不可逾越的红线)

流水线的每一次执行都必须遵守:

  • 永不编造边。不确定就标 AMBIGUOUS
  • 永不跳过语料规模检查警告;
  • 报告中永远展示 token 成本;
  • 永不把 cohesion 分数藏在符号后面——展示原始数字;
  • 图超过 5,000 节点时,未经警告用户绝不运行 HTML 可视化。

这些规则与 Step 4.5 的健康检查闸门一起,构成了 graphify「诚实审计线索」的工程化保障:抽取阶段有 EXTRACTED/INFERRED/AMBIGUOUS 三态标注,建图阶段有健康诊断,导出阶段有收缩守护,汇报阶段强制展示成本与完整性警告。

十三、Windows 平台要点速查与源码对照

Windows 要点文档位置仓库佐证
解释器发现顺序 uv → pipx → 活动环境Step 1 Find-GraphifyPython包发布名 graphifyy,见 graphify/__main__.py
无 BOM 写 sidecar(WinError 123 #3028)Step 1 UTF8Encoding $false注释 #3028
统一用 & (Get-Content .graphify_python) 驱动 Python全流程约定
here-string 防控制台编码漂移(#2528)Step 2/3 脚本形态注释 #2528
空图守护(#1392)Step 4graphify/build.py 后置检查
收缩守护(#479)Step 4/5graphify/export.py
默认语义后端 Gemini、GRAPHIFY_GEMINI_MODELStep 3 前置说明graphify/llm.py
graspologic ANSI 破坏 PowerShell 5.1 滚动Troubleshootinggraphify/cluster.py
语义缓存按提示词归属(#1939)Step B0/B3graphify/cache.py

需要进一步探索 Windows 专属参考细节时,可按需加载 graphify/skills/windows/references/ 下的八个参考文件:抽取协议 extraction-spec.md、增量与重聚类 update.md、图上问答 query.md、URL 抓取与监听 add-watch.md、仓库克隆与跨仓合并 github-and-merge.md、导出矩阵 exports.md、音视频转写 transcribe.md 与 git hook 集成 hooks.md。整体流程的全平台变体还可对照 skill-agents.md 等姊妹文档,理解同一技能在 bash 与 PowerShell 宿主下的差异边界。

【免费下载链接】graphify Turn 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. 【免费下载链接】graphify 项目地址: https://gitcode.com/GitHub_Trending/graph/graphify

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值