0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GraphCypherQAChainの中身を自作する:LLMとNeo4jを繋ぐアダプター実装

0
Posted at

はじめに

前回は GraphCypherQAChain を使って自然言語からNeo4jに問い合わせる仕組みを作りました。

今回はその中身を自分で実装します。「アダプター」とは何かを理解しながら、LLMとNeo4jを繋ぐ処理を一から書いていきます。


アダプターって何?

前回使った GraphCypherQAChain は内部でこういうことをしていた:

スクリーンショット 2026-07-12 004815.png

自然言語
  ↓ LLMに「Cypherに変換して」と頼む(スキーマも渡す)
Cypherクエリ
  ↓ Neo4jで実行
クエリ結果(JSON)
  ↓ LLMに「自然言語にして」と頼む
回答

この「LLMとNeo4jの間で変換処理を担う層」がアダプターです。

LangChainが隠してくれていた部分を自分で書くことで:

  • どんなプロンプトでCypherを生成しているか
  • エラーハンドリングをどこでやるか
  • 変換ロジックをどうカスタマイズするか

が自分でコントロールできるようになります。


対象者

  • 前回の記事でNeo4j + LLMの基本を理解した人
  • LangChainの抽象化の中身が気になる人
  • アダプターパターンを実装レベルで理解したい人

今回作るもの

NaturalLanguageToNeo4jAdapter
├── get_schema()         # Neo4jのスキーマを取得
├── generate_cypher()    # 自然言語 → Cypher(LLM)
├── execute_cypher()     # Cypherを実行
├── generate_answer()    # 結果 → 自然言語(LLM)
└── query()              # 上記を一括で実行するメイン関数

セットアップ

前回のプロジェクトをそのまま使う。

cd neo4j-llm-tutorial
uv add ollama

Ollamaが起動していてモデルが取得済みであることを確認:

ollama list
# llama3.1:8b が表示されればOK

1. スキーマ取得

アダプターの最初の仕事は、Neo4jのスキーマをLLMに渡せる形で取得することです。スキーマがないとLLMは正しいCypherを生成できません。

adapter.py を作る:

from neo4j import GraphDatabase
import ollama
import json

NEO4J_URI = "bolt://localhost:7687"
NEO4J_AUTH = ("neo4j", "password")
OLLAMA_MODEL = "llama3.1:8b"

class NaturalLanguageToNeo4jAdapter:
    def __init__(self):
        self.driver = GraphDatabase.driver(NEO4J_URI, auth=NEO4J_AUTH)

    def get_schema(self) -> str:
        """Neo4jのノードとリレーションシップ情報を取得する"""
        with self.driver.session() as session:
            labels = session.run("CALL db.labels()").value()
            rel_types = session.run("CALL db.relationshipTypes()").value()
            property_keys = session.run("CALL db.propertyKeys()").value()

        schema = f"""
ノードラベル: {', '.join(labels)}
リレーションシップ: {', '.join(rel_types)}
プロパティキー: {', '.join(property_keys)}
        """.strip()

        return schema

    def close(self):
        self.driver.close()

スキーマ取得の確認:

if __name__ == "__main__":
    adapter = NaturalLanguageToNeo4jAdapter()
    print(adapter.get_schema())
    adapter.close()
uv run adapter.py
ノードラベル: Person
リレーションシップ: FRIENDS_WITH
プロパティキー: name, age

2. 自然言語 → Cypherの変換

次にLLMにCypherを生成させる部分です。ここがアダプターの核心。

def generate_cypher(self, question: str) -> str:
    """自然言語をCypherクエリに変換する"""
    schema = self.get_schema()

    prompt = f"""あなたはNeo4jのCypherクエリの専門家です。
以下のグラフDBのスキーマを参照して、質問に答えるCypherクエリを生成してください。

スキーマ:
{schema}

ルール:
- Cypherクエリのみを返す(説明文は不要)
- マークダウンのコードブロックは使わない
- 存在しないノードやリレーションは使わない

質問: {question}
"""

    response = ollama.chat(
        model=OLLAMA_MODEL,
        messages=[{"role": "user", "content": prompt}],
        options={"temperature": 0},
    )

    cypher = response["message"]["content"].strip()
    # LLMがコードブロック付きで返すことがあるので除去
    cypher = cypher.replace("```cypher", "").replace("```", "").strip()
    return cypher

temperature=0 にしているのは、クエリ生成のブレを抑えるため。創造性より正確さが重要な場面。


3. Cypherの実行

生成したCypherをNeo4jで実行する部分です。

def execute_cypher(self, cypher: str) -> list[dict]:
    """Cypherクエリを実行して結果を返す"""
    with self.driver.session() as session:
        result = session.run(cypher)
        return [dict(record) for record in result]

4. 結果 → 自然言語の変換

クエリ結果をそのままユーザーに返しても読みにくい。LLMで自然な回答に変換します。

def generate_answer(self, question: str, cypher: str, results: list[dict]) -> str:
    """クエリ結果を自然言語の回答に変換する"""
    prompt = f"""以下の情報をもとに、質問に対する自然な日本語の回答を生成してください。

質問: {question}
実行したCypherクエリ: {cypher}
クエリの結果: {json.dumps(results, ensure_ascii=False)}

回答は簡潔に、1〜2文でまとめてください。
"""

    response = ollama.chat(
        model=OLLAMA_MODEL,
        messages=[{"role": "user", "content": prompt}],
        options={"temperature": 0.3},
    )

    return response["message"]["content"].strip()

5. メイン関数でまとめる

ここまでのステップを1つの query() にまとめます。

def query(self, question: str) -> dict:
    """自然言語の質問からNeo4jに問い合わせて回答を返す"""
    print(f"\n質問: {question}")

    # Step1: Cypher生成
    cypher = self.generate_cypher(question)
    print(f"生成されたCypher: {cypher}")

    # Step2: 実行
    results = self.execute_cypher(cypher)
    print(f"クエリ結果: {results}")

    # Step3: 自然言語に変換
    answer = self.generate_answer(question, cypher, results)
    print(f"回答: {answer}")

    return {
        "question": question,
        "cypher": cypher,
        "results": results,
        "answer": answer,
    }

6. 動作確認

if __name__ == "__main__":
    adapter = NaturalLanguageToNeo4jAdapter()

    questions = [
        "Aliceの友達は誰ですか?",
        "Aliceの友達の友達は誰ですか?",
        "25歳の人は誰ですか?",
    ]

    for q in questions:
        adapter.query(q)
        print("-" * 40)

    adapter.close()
uv run adapter.py

実行するとこんな感じ:

質問: Aliceの友達の友達は誰ですか?
生成されたCypher: MATCH (:Person {name: 'Alice'})-[:FRIENDS_WITH*2]->(fof) RETURN fof.name AS name
クエリ結果: [{'name': 'Carol'}]
回答: Aliceの友達の友達はCarolです。

7. エラーハンドリングを加える

LLMが不正なCypherを生成することがある。そのままNeo4jに渡すとエラーになるので対処する。

def execute_cypher(self, cypher: str) -> list[dict]:
    """Cypherクエリを実行して結果を返す(エラーハンドリング付き)"""
    try:
        with self.driver.session() as session:
            result = session.run(cypher)
            return [dict(record) for record in result]
    except Exception as e:
        print(f"Cypher実行エラー: {e}")
        return []

def query(self, question: str) -> dict:
    cypher = self.generate_cypher(question)
    print(f"生成されたCypher: {cypher}")

    results = self.execute_cypher(cypher)

    if not results:
        return {
            "question": question,
            "cypher": cypher,
            "results": [],
            "answer": "該当するデータが見つかりませんでした。",
        }

    answer = self.generate_answer(question, cypher, results)
    print(f"回答: {answer}")

    return {
        "question": question,
        "cypher": cypher,
        "results": results,
        "answer": answer,
    }

GraphCypherQAChainと何が違うか

スクリーンショット 2026-07-12 004822.png

GraphCypherQAChain 自作アダプター
プロンプト 固定(カスタム困難) 自由に変更できる
エラーハンドリング ライブラリ任せ 自分でコントロール
ログ verbose=Trueのみ 好きなだけ出せる
拡張性 低い 高い

実運用するなら自作の方が柔軟に対応できます。


まとめ

今回作ったアダプターの流れ:

自然言語
  ↓ generate_cypher()  ← スキーマを渡してLLMに変換させる
Cypherクエリ
  ↓ execute_cypher()   ← Neo4jで実行
クエリ結果
  ↓ generate_answer()  ← LLMで自然言語に戻す
回答

GraphCypherQAChain がやっていたことを自分で実装することで、各ステップでのカスタマイズや拡張がしやすくなります。プロンプトを変えたい、リトライ処理を入れたい、ログを細かく残したいといった場合に自作アダプターが活きます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?