← ブログに戻る

GraphRAG コードレビュー実装7ステップ — MCPでClaude Codeに渡すまで

前に書いた「Knowledge Graph×MCPでAIコードレビューを8〜49倍圧縮」の記事、公開後にDMで一番来たのは「圧縮率は分かった、で、tree-sitterからMCPまでどう繋げるのか」でした。もっと深く読みたい人は書籍側のKnowledge Graph実践ガイド 第8章から第10章を通しで読むのが早いのですが、記事側では「同じパイプを最短で組むには」を7ステップで書きます。

書くのは実装の順番、詰まりやすいポイント、実運用で外した設計判断の3つです。パフォーマンス数字と設計論はさっきのリンク先に譲ります。あと Qiita に「GraphRAGが優秀なのは精度だけ

」っぽい記事がありますが、そちらはコスト比較の話、こちらは実装パイプの話です。1万件のドキュメントを流し込む話はしません。

そもそも何を作るのか

作るのは4つのコンポーネントを繋いだパイプです。

  1. tree-sitter (ローカル、決定論的なAST抽出)
  2. Neo4j か Kuzu (グラフDB、ノードとエッジを保存)
  3. MCP サーバー (Claude Code から叩ける口)
  4. Cypher クエリ (影響範囲や依存の質問を投げる)

Claude Code が「auth.pyを変更したら何に影響するか」を聞くと、MCP サーバー経由で Cypher が走り、影響ファイルのリストだけが戻ってきます。全ファイルを読ませない、変更に本当に関係する数ファイルだけを渡す。これで指摘の質が変わります。

なおMCP自体の primitive (tools、resources、prompts) の詳細解説はしません。他の記事で書いています。ここでは「MCPサーバーに Cypher を叩かせるまで」だけをやります。

tree-sitter→Neo4j→MCP→Cypher 4段パイプの俯瞰

Step 1: tree-sitter で AST を吐かせる

最初の穴はここです。tree-sitter は言語ごとに grammar が別で、Python 用と TypeScript 用は別パッケージです。Python プロジェクトなら tree-sitter + tree-sitter-python を入れれば動きます。

import tree_sitter_python as tspython
from tree_sitter import Language, Parser

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

with open("service.py", "rb") as f:
    tree = parser.parse(f.read())

for node in tree.root_node.children:
    if node.type == "function_definition":
        name = node.child_by_field_name("name").text.decode()
        print(f"func: {name}")

これで関数名とその位置は取れます。ここで甘くて後で困るのは「呼び出しグラフ」です。「A が B を呼んでる」の情報は AST の内側 (call_expression ノード) にあるので、再帰で降りて集める必要があります。深さと親のスコープを両方管理していないと、self.foo() の解決 (self が何のクラスか) を落とします。

私は最初これを甘く見て、後から「メソッド呼び出しが全部取れていない」に気づいて Step 2 のスキーマを引き直しました。

Step 2: Neo4j のスキーマは最初に固める

Neo4j はスキーマレスに近いですが、実運用ではラベルとプロパティを決め打ちしないと Cypher が地獄になります。私が最終的に落ち着いた設計はこれです。

ノード

  • File — path, language, sha
  • Class — name, file, line
  • Function — name, file, line, params (配列), complexity

エッジ

  • (File)-[:CONTAINS]->(Function)
  • (File)-[:CONTAINS]->(Class)
  • (Class)-[:HAS_METHOD]->(Function)
  • (Function)-[:CALLS]->(Function)
  • (File)-[:IMPORTS]->(File)

CALLS を「Function → Function」で持つのが大事で、「Function → 文字列 (呼び出し先名)」で持つと後で解決できません。tree-sitter で名前しか取れない呼び出しについては、Step 3 でグラフに入れる直前に名前解決を挟みます。

Step 3: 名前解決を挟んでからロード

user.save()userUser クラスのインスタンスか、モジュール user の関数呼び出しかは、AST だけでは決まりません。scope walk が要ります。

私の場合、tree-sitter で採れる情報から以下を優先順に解決しました。

  1. import 文で見えている名前
  2. 同ファイル内で定義された名前
  3. 型ヒント (Python なら -> User) から辿れる名前
  4. 解決できないものは Unknown ノードに逃がす

Unknown に逃がすのは負けているようで、実運用で大事です。「解決できなかったから CALLS を張らない」にすると影響範囲の網に穴が空きますが、Unknown を経由させれば後で潰せます。

ロードは Neo4j の bolt drivers で MERGE を叩くだけです。

from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "..."))

with driver.session() as session:
    for func in funcs:
        session.run(
            "MERGE (f:Function {name: $name, file: $file}) "
            "SET f.line = $line, f.complexity = $c",
            name=func.name, file=func.file, line=func.line, c=func.complexity
        )

MERGECREATE にすると再ビルドで重複が出ます。ここも一度やらかしました。

Step 4: MCP サーバーは薄く書く

MCP サーバー本体は薄い方が保守が楽です。Python SDK なら mcp パッケージで tool を1つ登録して、中で Cypher を叩くだけです。

from mcp.server import Server
from mcp.types import Tool, TextContent

server = Server("code-kg")

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="blast_radius",
            description="ファイルまたは関数を変更したときの影響ファイル一覧を返す",
            inputSchema={
                "type": "object",
                "properties": {
                    "target": {"type": "string"},
                    "hops": {"type": "integer", "default": 2}
                },
                "required": ["target"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name, arguments):
    if name == "blast_radius":
        result = run_blast_radius(arguments["target"], arguments.get("hops", 2))
        return [TextContent(type="text", text=result)]

肝は description です。「影響ファイル一覧を返す」だけだと Claude Code はこれをいつ呼ぶべきか分からず、変更が絡む質問でも別の tool を探しに行きます。description に「PR レビュー時にまずこれを呼ぶこと」まで書くとちゃんと呼びます。

Step 5: Cypher クエリを 4 本用意する

7ステップの中で一番磨いた場所です。全部を紹介しきれないので、実運用で稼働率が高い4本だけ書きます。

Blast radius (影響範囲)

MATCH path = (start:Function {name: $target})<-[:CALLS*1..3]-(caller)
RETURN DISTINCT caller.file AS file, length(path) AS hop
ORDER BY hop

*1..3 の上限は 3 が現実的です。4 以上にするとテストファイルまで含んで巨大化します。

Dead code (呼ばれていない public 関数)

MATCH (f:Function)
WHERE NOT (f)<-[:CALLS]-() AND NOT f.name STARTS WITH "_"
RETURN f.file, f.name

依存の逆引き (この File を import しているファイル)

MATCH (importer:File)-[:IMPORTS]->(target:File {path: $path})
RETURN importer.path

呼び出し深さの深い関数 (レビュー優先度が高い)

MATCH (f:Function)
OPTIONAL MATCH (f)-[:CALLS*]->(callee)
RETURN f.name, count(DISTINCT callee) AS reach
ORDER BY reach DESC LIMIT 20

この 4 本があれば「変更のレビュー」の 8 割は素直に流れます。残りの 2 割は semantic 検索とか循環検出とかで、そこは第10章の話です。

Cypher 4クエリと Claude Code がそれぞれをいつ呼ぶか

Step 6: Claude Code の CLAUDE.md にツールの使い所を書く

これを飛ばすと Claude Code はツールを持っているのに呼びません。tool description だけでは足りず、CLAUDE.md 側にも「いつ呼ぶか」を明示する必要があります。

## code-kg MCP の使い方

- PR や diff のレビューを頼まれた時、まず `blast_radius` を呼んで影響ファイルを取得する
- 影響ファイルが 10 件を超えたら `dead_code` を先に流して、レビュー範囲を絞る
- ファイルを import しているのが誰か知りたい時は `依存の逆引き` クエリを使う
- 巨大なリファクタは `呼び出し深さの深い関数` を先に見て、レビュー起点を決める

これを入れる前と入れた後で、tool 呼び出し率が私の観測では約 3 倍になりました。これは harness-ops 側の telemetry で数えた数字です。

Step 7: 実運用で気づいた 3 つの穴

きれいに動くように書きましたが、実運用で3つ落とし穴があります。

穴 1: グラフの再ビルドコスト。 ファイルが変わるたびにフルビルドを走らせると、中規模リポで数分持っていかれます。私は git の post-commit で差分ファイルだけ再パースして、そのファイルに関係するノード・エッジだけ MERGE で更新する構成にしています。

穴 2: Cypher の LIMIT を Claude 側に任せない。 LIMIT を書き忘れた blast_radius が数千行返した時、Claude Code のコンテキストが一発で埋まりました。全 Cypher に LIMIT 200 くらいのハードキャップをサーバー側で強制する方が安全です。

穴 3: Neo4j の bolt 接続がタイムアウトする。 MCP サーバーはプロセスが長寿命なので、Neo4j のアイドル切断に当たります。driver を毎回作り直すか、健全性チェックを挟んでからクエリを走らせる形にしました。

この3つは、書籍の第8章から第10章では「触れているが深追いしていない」領域です。実装するとまず刺さります。

実測データはどこにある

precision/recall の数字は前作のトークン削減記事側にまとめてあります。実装7ステップを終えた後で「何個の見落としが減ったか」を測りたい人はそちらへ。この記事は「まず動くパイプが手元にある」の状態まで持って行くのが目的です。

同クラスタで補足的に読める記事として、Cypher より上位の設計判断を書いた「GraphRAGはリレーションクエリの3型で古典RAGを上回る」も置いておきます。

まとめ

  • tree-sitter → Neo4j → MCP → Cypher の順で組む
  • スキーマは最初に固める、CALLS は Function → Function で持つ
  • 名前解決の穴は Unknown ノードに逃がしておく
  • MCP サーバーは薄く、tool description は「いつ呼ぶか」まで書く
  • Cypher は Blast radius / Dead code / 依存の逆引き / 呼び出し深さの 4 本で 8 割いける
  • Claude Code の CLAUDE.md にツールの使い所を書く
  • 実運用の穴は 再ビルドコスト / Cypher の LIMIT / bolt 接続の3つ

7ステップの実装詳細と、実運用で見つかった第4以降の穴、Cypher の応用パターンは書籍の第8章〜第10章にまとめてあります。実装しながら手元に置くと詰まる回数が減ります。

Knowledge Graph実践ガイド — コードをKG化してAIレビューに渡すまでの実装を、章立てで一気通貫に書いた書籍です。この記事の7ステップの各章に対応する詳細と、書ききれなかった第 8 のステップ (semantic 抽出の Pass 2) まで載せています。

ナレッジグラフ活用大全 関連書籍 ナレッジグラフ活用大全 ナレッジグラフ 活用大全 | GraphRAG・Neo4j・RDF・Property Graph・Emotion AI の実践書 書籍ページを見る →