← ブログに戻る

コード用Knowledge Graphを7ステップで作った実測ノート

「Knowledge Graph はコードレビューに効く」と何度も書いたのですが、そういえば「どう作ったか」の実測ノートを一度も出していませんでした。私が自分のリポジトリで最初に KG を組んだとき、Python の環境構築を含めて 35 分かかりました。そのうち 5 分は tree-sitter-language-pack のバージョン衝突で溶かしています。タイマー勝負としては負けですが、内訳が分かれば次からは 30 分に収まります。

この記事は「AIコードレビュー トークン削減」を Tree-sitter と MCP で 8〜49 倍まで持っていったときの、その手前の構築編です。運用編 (レビューが本当に軽くなる内訳) は AIレビューがトークン80%焼く問題、Tree-sitter+MCPで8-49倍削減 にすでに書いたので、混同されないように書き分けます。読者は運用編から入っても構いません。私は構築編から書いた方が納得感が出ました。

7ステップの全体像

先に地図を出します。全体は 3 つの区間に分かれます。構造抽出 (Step 1-3)永続化と検索 (Step 4-5)公開と意味付け (Step 6-7) です。

7ステップの全体像 - Pass 1 構造抽出とPass 2 意味付けの分岐

各ステップの詳細に入る前に、KG そのものの前提を掴んでおきたい方は、姉妹本 Knowledge Graph実践ガイド の第4章 (7ステップでKGを作る) が土台として合います。本記事はコード向けに具体化した実装ノートという位置づけです。

Step 1: 対象言語を1つに絞る

19 言語対応の Tree-sitter grammar が並んでいると、つい全部やりたくなります。やめておいた方がいいです。最初は Python か TypeScript のどちらか、自分のメイン言語 1 つに絞ります。理由は 2 つあります。

言語ごとにノード名 (function_definition / function_declaration / function_item) が違うので、多言語対応は「抽象化レイヤの設計」という別の仕事が発生します。それに、そもそも 1 言語で自分のプロジェクトを回して手応えを掴む前に汎用化しても、その汎用化が正しいか分かりません。

Step 2: Tree-sitter で AST 抽出

pip install tree-sitter tree-sitter-python から入ります。1 ファイルから関数定義と呼び出しを引き抜くコードは、写経でも 10 分あれば書けます。

import tree_sitter_python as tspython
from tree_sitter import Language, Parser

PY = Language(tspython.language())
parser = Parser(PY)

def extract(filepath):
    tree = parser.parse(open(filepath, "rb").read())
    funcs, calls = [], []
    def walk(node):
        if node.type == "function_definition":
            name = node.child_by_field_name("name").text.decode()
            funcs.append((name, node.start_point[0] + 1))
        elif node.type == "call":
            fn = node.child_by_field_name("function")
            if fn and fn.type == "identifier":
                calls.append((node.start_point[0] + 1, fn.text.decode()))
        for c in node.children:
            walk(c)
    walk(tree.root_node)
    return funcs, calls

Tree-sitter は 2026 年時点でも 19+ の公式バインディングと 100+ のコミュニティ grammar を持っていて、GLR ベースのインクリメンタルパーサーとして「差分だけ再パース」できるのが実務で効きます。ch13 の差分ビルドはこの特性の上に立ちます。

Step 3: ノード/エッジのスキーマを決める

ここで手を止めて設計します。ノード種別は最初は 4 つだけで十分です。Function / Class / Module / File。エッジ種別も最初は 3 つで足ります。CALLS / IMPORTS / DEFINED_IN

このミニマルセットで実務のレビュー質問の 8 割に答えられます。増やすのは、実際に「この質問はこの 4 種類ノードだけでは答えられない」と 3 回続けて感じてからで遅くありません。私は最初「せっかくだから」と 12 種類ノード + 8 種類エッジで始めて、翌週半分削りました。あの週末は返してほしいです。

Step 4: SQLite に永続化

グラフ DB は最初は要りません。SQLite で 10 万ノードまで捌けます。Neo4j や Kuzu の話は、SQLite が本当に詰まってから読めば十分です。

重要なのは 1 行のインデックスです。

CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst, type);

blast radius は「呼び出し先から呼び出し元を逆引きする」BFS が中心なので、dst 側のインデックスがないと 1 万ノードでも数秒待たされます。ここは Pass 1 が Pass 2 に速度で勝つ生命線です。

Step 5: blast radius を BFS で計算

Step 4 のスキーマ上で、指定関数から N ホップ遡って呼び出し元集合を返す BFS を書きます。閾値は confidence >= 0.8 を入れておくと、後で足す Pass 2 の低信頼エッジを自動で切れます。

def blast_radius(conn, name, max_hops=3):
    cur = conn.cursor()
    cur.execute("SELECT id FROM nodes WHERE type='Function' AND name=?", (name,))
    row = cur.fetchone()
    if not row: return {}
    visited, frontier, result = {row[0]}, {row[0]}, {0: {row[0]}}
    for hop in range(1, max_hops + 1):
        ph = ",".join("?" * len(frontier))
        cur.execute(
            f"SELECT src FROM edges WHERE type='CALLS' "
            f"AND dst IN ({ph}) AND confidence >= 0.8",
            tuple(frontier),
        )
        callers = {r[0] for r in cur.fetchall()} - visited
        if not callers: break
        result[hop] = callers
        visited |= callers
        frontier = callers
    return result

3 ホップまでで大体のレビューは足ります。4 ホップ以上を要求される案件は、たぶん設計の問題であって KG の問題ではありません。

Step 6: MCP サーバとして公開

MCP (Model Context Protocol) はここで登場します。Claude Code や Cursor が「自然言語で KG に質問する」ための橋渡しです。低レベル SDK の Server@server.list_tools()@server.call_tool() を書けば動きます。CodeGraph のような先行 OSS は 22 種類のツールを露出していますが、最初は 5 つで十分です。

  • get_function(name)
  • get_callers(name, max_hops=1)
  • blast_radius(name, max_hops=3, min_confidence=0.8)
  • search_symbols(query, type)
  • get_file_summary(path)

これで「UserService.create を変えたら何が壊れる?」「AuthMiddleware を呼んでいる箇所を全部見せて」の 8 割に答えられます。命名は動詞+目的語で、副詞や形容詞は引数に落とすこと。find_functionget_function を両方作ると AI が混乱します (これも失敗経験)。

Step 7: Pass 2 (LLM 意味抽出) はホットスポットだけ

Step 6 まででコードKGは動きます。ただし Pass 1 (機械抽出) だけでは、動的呼び出し、ダックタイピング、依存性注入で渡される関数が取れません。ここで Pass 2 (LLM に周辺コンテキストを渡して意味を抽出する) が要ります。

全ファイルに Pass 2 を回してはいけません。30 万行のリポジトリに全行 Pass 2 を通すと、Claude Sonnet で数百ドル飛びます。ホット/コールド戦略で「最新の PR で触られたファイル」だけに Pass 2 を当てます。入力は対象関数本体 + 周辺 docstring + 呼び出し元/先 1-2 個のシグネチャで、200-800 トークンに収まります。

信頼度は 3 層で管理します。EXTRACTED (Pass 1 由来、1.0) / INFERRED (Pass 2 由来、0.0-1.0) / AMBIGUOUS (人間レビュー待ち)。この分類は Graphify のスキーマを踏襲したものです。

実測: 何が 8〜49 倍削減の内訳か

構築編としては工程まで、というのが本記事の範囲ですが、成果指標だけ 1 行貼っておきます。

  • 全ファイル読み: 入力トークン 100 (基準)
  • Pass 1 + blast radius だけ: 入力トークン 12 (8.3 倍削減)
  • Pass 1 + Pass 2 ホット + blast radius: 入力トークン 2 (49 倍削減)

削減の実装内訳は運用編に譲ります。ここでは「7 ステップで作ったグラフを、レビュー時にどう食わせるか」までは本記事の外です。

本記事は構築編、運用編は別途

冒頭でも書きましたが、本記事は「KG をゼロから作る」構築編です。「作った KG をレビューでどう食わせて何倍削減できたか」の運用編は AIレビューがトークン80%焼く問題 を読んでください。同じネタを 2 度書いていない理由はここにあります。

同じ「7 ステップ」でも、そもそも「エージェントが今どこを見ていないのか」を可視化する前段 (=構築を始める前) の話は、別に Knowledge Graphを7ステップで作る前に、AIエージェントが「見えていない」3層を可視化する にまとめました。3 部作の中央が本記事です。

まとめ

  • 対象言語を 1 つに絞る、スキーマは最小 4 ノード + 3 エッジで始める
  • SQLite で十分、idx_edges_dst の 1 行が生命線
  • BFS は 3 ホップまで、閾値 confidence >= 0.8
  • MCP は 5 ツールで開始、命名は動詞+目的語
  • Pass 2 はホットスポットだけ、200-800 トークンで意味を足す

30 分で動くのは本当ですが、私が最初にやったときは実質 3 週間かかっています。多言語対応、ノード種別過多、Pass 2 の全ファイル爆撃を、順番に踏んで学んだからです。この記事が、あなたの実装をせめて 30 分側に寄せる助けになれば嬉しいです。

より深く 7 ステップ全体を追いたい方は、姉妹本 Knowledge Graph実践ガイド の第4章がそのままの地図になります。コード用の具体化は本記事、Knowledge Graph 一般論の土台は本を、という使い分けが読みやすいはずです。