GitHubのトレンドを眺めていたら、Graphify-Labs/graphify が7位に入っていました。説明には「Turn any codebase, with its docs, SQL schemas, configs, and PDFs, into a queryable knowledge graph.」とあります。確認した時点でスターは 129,524 です。
恥ずかしながら、僕は新しいコードベースに入るたびに、最初の数日を「どこに何があるか」を探すだけで使ってしまいます。grep で関数名を追いかけて、気づいたら同じファイルを何度も開いている、というやつです。コードの地図を作ってくれるなら欲しい。そう思って触ってみました。
README には「Code is parsed with tree-sitter AST: deterministic, no LLM, nothing leaves your machine.」とあり、Privacy の節には「A code-only corpus requires no API key: graphify extract runs fully offline.」とも書いてあります。なので今回は、本当にネットを切っても動くのか、そして README に載っている FastAPI の実例がそのまま再現するのか、の2点を確かめることにしました。
試した環境
- Docker の使い捨てコンテナ(
python:3.12-bookworm、Debian 12、メモリ4GB・CPU2に制限) - Python 3.12.13 / pip 25.0.1
- graphify 0.9.92(PyPI のパッケージ名は y が2つの
graphifyy) - 地図を作る対象: FastAPI の main(コミット
993899cc20、2026-10-20)。.pyが 1,176 ファイル、全体で 3,192 ファイル
FastAPI を選んだのは、README の「See it in action」がまさに FastAPI で graphify explain と graphify path を叩いた出力を載せていたからです。同じことをして同じ結果が出るかを見れば、ツールの癖も分かるかなと思いました。
最初の extract は1.6秒で止まった
インストールは pip install graphifyy==0.9.92 で 6.4 秒、30 パッケージでした。ここは拍子抜けするくらい軽かったです。
ところが、README を斜め読みしたまま FastAPI のディレクトリで graphify extract . を叩いたら、1.6 秒で終了コード 1 が返ってきました。
# インストール(コンテナ内)
$ pip install --no-cache-dir graphifyy==0.9.92 # real=6.446 / 30 パッケージ
$ graphify --version
graphify 0.9.92
# FastAPI のディレクトリで、キー無し・フラグ無し
$ graphify extract . # real=1.625 / 終了コード 1
error: no LLM API key found (1997 doc/paper/image file(s) need semantic extraction). Set GEMINI_API_KEY or GOOGLE_API_KEY (gemini), ...(中略)... A code-only corpus needs no key. Or pass --code-only to index just the code (local AST, no key) and skip the non-code files.
[graphify extract] scanning /work/fastapi
[graphify extract] found 1186 code, 1753 docs, 0 papers, 244 images
# ホスト側で docker network disconnect bridge してから
$ curl -sS -o /dev/null https://pypi.org
curl: (6) Could not resolve host: pypi.org
$ graphify extract . --code-only # real=15.475 / 終了コード 0
[graphify extract] --code-only: skipping 1997 non-code file(s) (1753 docs, 0 papers, 244 images) — no LLM extraction
[graphify extract] AST extraction on 1186 code files...
AST extraction: 1186/1186 uncached files (100%) [8 workers]
warning: 34 code file(s) yielded no symbols (0.02 MB) — a data file, or source this extractor models no symbols for: ...
[graphify] Deduplicated 15 node(s) (7 exact, 8 fuzzy).
[graphify extract] wrote /work/fastapi/graphify-out/graph.json: 9572 nodes, 20954 edges, 687 communities
# 3回作って graph.json の sha256 を比べる(先頭16桁)
1回目 0cc5df59f43dbe18
2回目(キャッシュあり) 0cc5df59f43dbe18 real=5.724
3回目(出力を消して) 0cc5df59f43dbe18 real=15.245
FastAPI のリポジトリはコード(1,186 件)より多くのドキュメントを抱えていて、graphify はコード以外を 1,753 件の docs と 244 件の画像として数えていました。コード以外は LLM で意味を抜き出す前提なので、キーが無いと止まる。エラー文にそのまま「--code-only を付ければキー無しでコードだけ索引できる」と書いてあったので、正直ここはエラー文に助けられました。
そこで、コンテナのネットワークを docker network disconnect で外して、pypi.org の名前解決もできない状態にしてから --code-only で作り直しました。15.5 秒で 9,572 ノード、20,954 本のエッジ、687 個のコミュニティができました。graph.json は 9,379,130 バイトです。ネットを切っていても、少なくとも extract は最後まで通りました。
ひとつ気になったのは [8 workers] という表示です。コンテナは CPU2 に制限していたのに、nproc が 8 を返す環境だったので、help にある既定値「cpu_count」がそのまま 8 になったようです。CI の小さいランナーで回すなら --max-workers を見ておいたほうがよさそうだと思いました(今回は試していません)。
同じ入力で3回作ったときの graph.json は、3回とも sha256 の先頭が 0cc5df59f43dbe18 で一致しました。キャッシュが効いた2回目は 5.7 秒、出力を消して作り直した3回目は 15.2 秒です。README の「deterministic」は、少なくともこの範囲では言葉どおりでした。
READMEの例をそのまま打ってみる
次に、README と同じコマンドを叩きました。先に結果を並べます。
| コマンド・項目 | README の出力例 | 手元(FastAPI 993899cc20) |
|---|---|---|
graphify explain “APIRouter” の場所 | routing.py L2210 | fastapi/routing.py L2327 |
| 同じく Degree(つながりの数) | 47 | 160 |
graphify path “FastAPI” “ModelField” | 3 hops(FastAPI → DefaultPlaceholder → get_request_handler() → ModelField。向きは省略) | 終了コード 3(Ambiguous: FastAPI が2つ) |
| 名前を絞って path を再実行 | ― | 3 hops(FastAPI → applications.py → openapi/utils.py → ModelField。向きは省略) |
$ graphify explain "APIRouter"
Node: APIRouter
ID: fastapi_routing_apirouter
Source: fastapi/routing.py L2327
Type: code
Community: 4
Degree: 160
Connections (160):
<-- fastapi/__init__.py [imports] [EXTRACTED] fastapi/__init__.py:L23
<-- routing.py [contains] [EXTRACTED] fastapi/routing.py:L2327
<-- test_frontend.py [imports] [EXTRACTED] tests/test_frontend.py:L11
...
--> FastAPIError [uses] [INFERRED] fastapi/routing.py:L3398
... and 140 more (--all lists all 160)
$ graphify path "FastAPI" "ModelField" # 終了コード 3
Ambiguous: 'FastAPI' matches 2 nodes in different files.
fastapi/applications.py
id: fastapi_applications_fastapi
pyproject.toml
id: pkg_fastapi
Retry with path::symbol using one of the paths above (e.g. <path>::FastAPI) or the full node id.
$ graphify path "fastapi/applications.py::FastAPI" "ModelField" # 終了コード 0
Shortest path (3 hops):
FastAPI <--contains [EXTRACTED]-- applications.py --imports_from [EXTRACTED]--> openapi/utils.py --imports [EXTRACTED]--> ModelField
$ graphify explain "NoSuchThing123" # 終了コード 4
No node matching 'NoSuchThing123' found.
explain のほうは、行番号もつながりの数も README と違いました。出力例の近くには、どの時点の FastAPI で取ったものかまでは書かれていなかったので、差の理由までは分かりません。手元の 160 本を見ると、tests/ 配下のテストファイルからの imports がずらっと並んでいました。
びっくりしたのは path のほうです。FastAPI という名前のノードが2つあって、ひとつは fastapi/applications.py のクラス、もうひとつは pyproject.toml から作られた pkg_fastapi というパッケージのノードでした。graphify は推測で片方を選ばず、候補を並べて終了コード 3 で止まります。help にも「query, path and explain exit 0 with an answer, 3 when a name is ambiguous」「4 when no node matches」とあって、存在しない名前を渡したときは本当に 4 が返りました。スクリプトから使うなら、この3つの値で分岐できるのはありがたいです。
fastapi/applications.py::FastAPI と書いて絞ると答えが返りましたが、経路は README と別物でした。ノード ID を渡して --undirected を付けると、今度はテストファイル test_compat.py を経由する経路になります。最初は全然分からなくて「README と違う、壊れてる?」と思ったんですが、先輩に聞いたら「README の出力例は、その時点のコードで撮ったスナップショットだと思って読むといいよ」と言われて腑に落ちました。最短経路は1本とは限らないし、コードが変われば変わる。言われてみればそうなんですよね。
日本語で聞くと0件だった
graphify query は、質問の文から関係するノードを集めてくれるコマンドです。日本語でも聞けたら嬉しいなと思って、同じ内容を4通りで聞いてみました。
| 質問 | 終了コード | 起点になったノード | 見つかった数 / 表示された数 |
|---|---|---|---|
| how are dependencies resolved | 0 | dependencies.py(docs_src のサンプル)、dependencies/utils.py、dependencies/models.py、_get_resolved_absolute_path() | 442 / 78 |
| 依存関係はどう解決されるか | 4 | なし(No matching nodes found.) | 0 |
| Depends はどう解決される | 0 | Depends、Depends() | 1,491 / 63 |
| solve_dependencies | 0 | solve_dependencies()、._solve_dependencies()、dependencies/utils.py | 462 / 82 |
日本語の文だけで聞くと、1件も当たりませんでした。英語の文でも、起点に選ばれたのは docs_src のサンプルファイルや、「resolved」という単語にひっかかったらしい _get_resolved_absolute_path() で、肝心の solve_dependencies() は起点に入っていません。結果を見た限りでは、意味で探しているというより、質問の単語とノード名の一致で起点を決めている動きに見えます(コードまでは追っていないので、ここは推測です)。
逆に「Depends はどう解決される」のように識別子を1つ混ぜると、日本語の部分は関係なく Depends から辿ってくれました。どれも既定の約2,000トークンに収まるよう切り詰められていて、「raise the token budget to 12160」のように全部出すのに必要な予算まで教えてくれます。日本語で質問を書くより、関数名やクラス名で聞くほうが確実、というのが今回の結論です。
INFERRED の線は一度は疑う
graphify cluster-only . でレポートも作ってみました(これもネットを切ったまま、5.0 秒)。GRAPH_REPORT.md の Summary には「95% EXTRACTED · 5% INFERRED」「Token cost: 0 input · 0 output」とあり、LLM を一度も呼ばずにここまで作れています。God Nodes の1位は FastAPI(1,025 本)で、2位の post()(278 本)を大きく引き離していました。
そのレポートの「Surprising Connections (you probably didn’t know these)」に、ライブラリ本体の _get_openapi_operation_parameters() からテストファイルの example() へ indirect_call の線が引かれていました。本体がテストの関数を呼ぶのは変だなと思って、graph.json の中身と元のコードを見比べました。
# GRAPH_REPORT.md の Surprising Connections から
- `_get_openapi_operation_parameters()` --indirect_call--> `example()` [INFERRED]
fastapi/openapi/utils.py → tests/test_schema_extra_examples.py
# graph.json の該当エッジ(必要なキーだけ抜き出し)
{'source': 'fastapi_openapi_utils_get_openapi_operation_parameters',
'target': 'tests_test_schema_extra_examples_create_app_example',
'relation': 'indirect_call', 'confidence': 'INFERRED',
'confidence_score': 0.85, 'context': 'getattr', 'source_location': 'L220'}
# fastapi/openapi/utils.py 219〜220 行目
openapi_examples = getattr(field_info, "openapi_examples", None)
example = getattr(field_info, "example", None)
# ↑ 属性を読んでいるだけの行が「呼び出し」扱いになっている。ここは改善の余地がありそう
# tests/test_schema_extra_examples.py 26 行目(create_app の中で定義された関数)
def example(item: Item = Body(example={"data": "Data in Body example"})):
220 行目は getattr(field_info, "example", None) で、属性を読んでいるだけです。それが、テストの中にある example という名前の関数への呼び出しとして解決されていました。確信度は 0.85 です。ちゃんと INFERRED と書いてあるのでツールが嘘をついているわけではないんですが、「意外なつながり」として真っ先に目に入る場所に出てくるので、根拠の行を開かずに信じるのは危ないなと思いました。ここはツール側に改善の余地がありそうですし、読む側の使い方次第でもあります。
クエリログは README と実装で既定が違った
README の Privacy の節には「every graphify query, graphify path, graphify explain, and MCP query_graph call is logged to ~/.cache/graphify-queries.log」とあります。ところが、ここまで何度も query や path を叩いたのに、そのファイルはどこにもありませんでした。
調べてみたところ、0.9.92 の querylog.py には「Opt-in only (#1797)」とコメントがあり、GRAPHIFY_QUERY_LOG_ENABLE=1 か GRAPHIFY_QUERY_LOG にパスを入れたときだけ記録する作りになっていました。有効にして叩くと、質問の文やグラフのパスが JSON Lines で残ります。
# 既定ではログファイルが作られない
$ ls -la ~/.cache/graphify-queries.log
ls: cannot access '/root/.cache/graphify-queries.log': No such file or directory
# querylog.py(0.9.92)のコメントから抜粋
# Opt-in only (#1797). The log records every query/path/explain question and
# ...
# GRAPHIFY_QUERY_LOG_ENABLE=1 log to ~/.cache/graphify-queries.log.
# GRAPHIFY_QUERY_LOG_ENABLE=1 で query と explain を1回ずつ叩いた後(1行目は途中まで)
$ cat ~/.cache/graphify-queries.log
{"ts": "2026-10-20T12:06:46.040075+00:00", "kind": "query", "question": "依存関係はどう解決されるか", "corpus": "/work/fastapi/graphify-out/graph.json", "nodes_returned": null, "result_chars": 24, "duration_ms": 379.33, ...
{"ts": "2026-10-20T12:06:46.559738+00:00", "kind": "explain", "question": "APIRouter", "corpus": "/work/fastapi/graphify-out/graph.json", "nodes_returned": 160}
# レポートと HTML(ネットは切ったまま)
$ graphify cluster-only . # real=5.007 / 終了コード 0
Graph has 9572 nodes (above 5000 limit). Building aggregated community view...
graph.html written (aggregated: 687 community nodes, 2701 cross-community edges)
Done - 687 communities. GRAPH_REPORT.md, graph.json and graph.html updated.
$ grep -n unpkg graphify-out/graph.html
6:<script src="https://unpkg.com/vis-network@9.1.6/standalone/umd/vis-network.min.js"
既定で記録しないほうが、社内のコードに対して使うときは安心です。README の記述が古いまま残っているだけだと思いますが、ドキュメントを信じて「ログが残るから消しておこう」と探しても見つからない、ということは起きます。
書き出された graph.html を開いてみた
ひとつ前のコードブロックの最後の grep のとおり、graph.html の6行目は unpkg から vis-network を読み込む script タグでした。グラフ作りはオフラインで完結しても、この HTML はブラウザで開くときに外へライブラリを取りに行く書き方です。
どんな見た目なのかはやっぱり気になったので、あとから同じ版(graphify 0.9.92、FastAPI 993899cc20)で使い捨てコンテナを作り直して、graph.html を書き出し直しました。graph.json の sha256 の先頭は今回も 0cc5df59f43dbe18、graph.html は 898,233 バイトです。それをホストのブラウザではなく、コンテナの中の headless Chromium(Playwright 1.64.0)で開いて、1920x1080 の画面をまるごと撮りました。このときはネットワークをつないだままで、vis-network は unpkg から HTTP 200 で読み込まれています。

開いてみると、9,572 個のノードがそのまま並ぶのではなく、687 個のコミュニティがそれぞれ1つの点になった図でした。cluster-only が出していた「aggregated: 687 community nodes, 2701 cross-community edges」のとおりで、右下にも「687 nodes · 2701 edges · 687 communities」と出ています。開き始めてから、配置の計算(物理演算)が止まったのを確認できるまで 1.1 秒ほどでした。
最初の表示では点と線だけで、名前は読めません。名前は右側の COMMUNITIES に、testclient.py(218)、fastapi/__init__.py(185)、.post(152)……と数の多い順に並んでいます。この数字はそのコミュニティに入っているノードの数で、687 個ぶん足すとちょうど 9,572 になりました。
いちばん目を引いたのは、中央の塊よりも、それをぐるっと囲む外周の点の輪です。何の輪なのか分からなかったので、HTML に埋め込まれたデータを数えてみると、687 個のうち 203 個は、ほかのコミュニティへの線が1本もないコミュニティでした。配置が止まった後の座標で中心からの距離を測ると、この 203 個はすべて、線のある 484 個のどれよりも外側にあります。輪の正体はこの 203 個でした。
オフラインの端末でこの HTML を開いたときの見え方は、今回も確かめていません。
今回学んだこと
- パッケージ名は
graphifyy(y が2つ)、コマンドはgraphify - ドキュメントが混ざったリポジトリは、キーが無いと
extractが止まる。--code-onlyを付ける - ネットを切っても
extract --code-onlyとcluster-onlyは動いた。出力は3回とも同じ - README の出力例は今のコードでは再現しなかった。名前が被ったら
path::symbolかノード ID で指定する queryは日本語の文だと0件。識別子で聞く- INFERRED の線は、根拠の行を開いてから信じる
- クエリログは 0.9.92 では既定で無効(README は既定で記録すると書いている)
試してみての所感
ぶっちゃけ、最初は「AI アシスタント向けの skill」という説明を見て、エディタに AI が入っていないと使えないのかと思っていました。実際には、コードだけならキーもネットも要らず、CLI だけで地図が作れて、explain と path とレポートの God Nodes だけでもかなり遊べます。新しいリポジトリに入った初日にこれを眺めるのは、自分の使い方としてはアリだと思っています。
一方で、日本語の質問と INFERRED の線は、今の時点では様子見です。今回は AI アシスタントに skill として入れる使い方(graphify install)は試していないので、そちらで質問がどう扱われるかは分かりません。
次は自分のチームのリポジトリで graphify update . を回して、コミットごとにグラフがどれくらい変わるかを見てみたいです。