powered by TechFeed
表示モード
ハウツー

ローカルLLMで非構造化テキストをナレッジグラフに変換する — API費用ゼロで動くエンド・ツー・エンド実装

9月29日、Iván Palomares Carrascosaが「Automating Knowledge Graph Population: Extracting Entities and Triples from Unstructured Text with an LLM」と題した記事を公開した。OllamaのローカルLLMを使って非構造化テキストからエンティティとSPOCクワッドを自動抽出し、API費用ゼロでナレッジグラフを構築するエンド・ツー・エンドの実装手法を詳しく紹介している。

9月29日、Iván Palomares Carrascosaが「Automating Knowledge Graph Population: Extracting Entities and Triples from Unstructured Text with an LLM」と題した記事を公開した。OllamaのローカルLLMを使って非構造化テキストからエンティティとSPOCクワッドを自動抽出し、API費用ゼロでナレッジグラフを構築するエンド・ツー・エンドの実装手法を詳しく紹介している。


「ナレッジグラフをGraph-RAGに組み込みたい。だが、そのグラフの中身をどうやって作るか」という実装ギャップを埋めるのが本記事の目的だ。外部APIへの依存なし、費用ゼロで完結するローカル環境での構築手順を、動くコードとともに示している。

SPOCクワッドとは何か

まず基礎的な概念を整理する。従来のナレッジグラフは **RDFトリプル**(Subject・Predicate・Object の3要素)でファクトを表現する。例えば:

("LeBron James", "plays_for", "Lakers")

本記事で扱うSPOCクワッドはこれに4番目の要素「Context(文脈)」を加えたものだ:

("LeBron James", "plays_for", "Lakers", "NBA_2023_Roster")

Contextにはファクトの出典や有効期間を記録できる。これにより、Graph-RAGシステムにおける検索の確定性と事実精度が向上する。本記事は、筆者が以前公開した3階層Graph-RAGシステムの構築に関する記事の続編として位置づけられており、そのシステムに投入するグラフデータの自動生成を担う。

なお、後述するプロンプト例でLLMに返させるJSONはSubject・Predicate・Objectの3要素構成になっている。Contextフィールドはコード側でソース名を付与する設計のため、LLMへの指示はトリプル形式で行い、格納時にクワッドへ昇格させる実装となっている点に注意されたい。

パイプラインの全体像

使用するコンポーネントは以下の3つだ:

  • Ollama: ローカル環境でLLMを動かすランタイム
  • Llama 3.2: 軽量・無料のオープンモデル。JSON出力モードを厳格に強制できる
  • QuadStore: Pythonで実装された軽量なナレッジグラフDB(本記事ではモック実装を利用)

Google Colab上でも動作するよう設計されており、セットアップは以下のコマンドで完結する:

!apt-get update -qq && apt-get install -y -qq zstd
!curl -fsSL https://ollama.com/install.sh | sh
!pip install wikipedia requests

Ollamaサーバーの起動とモデルのプルは、Pythonのsubprocessでバックグラウンド実行する:

import subprocess
import time

process = subprocess.Popen(["ollama", "serve"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(3)
subprocess.run(["ollama", "pull", "llama3.2"])

抽出エンジンの核心:Few-shotプロンプト+JSON強制

パイプラインの肝はextract_spoc_quads_final関数だ。Llama 3.2に送るプロンプトにはfew-shot例を埋め込み、"facts"キーを持つJSONオブジェクトを必ず返すよう指示する。LLMへの出力指示はSubject・Predicate・Objectの3要素形式で行い、Contextはパイプライン側がソース名を付与してクワッドに昇格させる:

prompt = f"""
You are an expert data extraction algorithm. Extract atomic facts from the text.
You must output a valid JSON object containing a single key called "facts".
The value of "facts" must be an array of objects.

Example output format:
{{
  "facts": [
    {{"subject": "LeBron James", "predicate": "plays_for", "object": "Lakers"}},
    {{"subject": "Lakers", "predicate": "based_in", "object": "Los Angeles"}}
  ]
}}

Text to process:
{text}
"""

APIリクエストでは"format": "json"と**"temperature": 0.0**を指定することで、出力の一貫性を担保している。

LLMが異なるキー名を使った場合のフォールバック処理と、"Subject"と"subject"のような大文字小文字の揺れに対するキー正規化も実装されており、実用上の堅牢性が考慮されている点が実践的だ。"format": "json"の指定はOllamaのJSON強制モードを有効にし、文法的に正しいJSONだけを返すよう内部で制御する。これとtemperature 0の組み合わせにより、パース失敗のリスクを大幅に低減している。

実行結果

Wikipediaの「Alan Turing」ページから取得した1,244文字のテキスト(冒頭2段落)を入力した結果、以下のようなクワッドが生成された:

S: Alan Mathison Turing | P: was             | O: an English mathematician... | C: Wikipedia_Alan_Turing
S: Alan Mathison Turing | P: was born        | O: in London                   | C: Wikipedia_Alan_Turing
S: Alan Mathison Turing | P: was raised      | O: in southern England         | C: Wikipedia_Alan_Turing

LLMが返したトリプルにパイプライン側が"Wikipedia_Alan_Turing"をContextとして付与し、出典が追跡可能な形で記録される。ソース名はWikipediaのページタイトルから自動生成されるため、複数ページを処理する場合でもContextによるファクトの出典管理が一貫して機能する。

QuadStoreへの格納

抽出したクワッドは、以下のシンプルなQuadStoreクラスに格納する。本記事のコードはこれをモックとして実装しているが、構造は前作のGraph-RAGシステムと互換性がある:

class QuadStore:
    def __init__(self):
        self.quads = []

    def add(self, subject, predicate, obj, context):
        quad = (subject, predicate, obj, context)
        if quad not in self.quads:
            self.quads.append(quad)

    def query(self, subject=None, predicate=None, obj=None, context=None):
        results = []
        for q_sub, q_pred, q_obj, q_ctx in self.quads:
            if (subject is None or subject == q_sub) and \
               (predicate is None or predicate == q_pred) and \
               (obj is None or obj == q_obj) and \
               (context is None or context == q_ctx):
                results.append((q_sub, q_pred, q_obj, q_ctx))
        return results

%%writefile quadstore.pyマジックコマンドでノートブック内にファイルを生成し、通常のPythonモジュールと同様にインポートして使う構成も示されている。重複チェックはif quad not in self.quadsでインメモリ比較するシンプルな実装だ。本番環境ではRDFLibやNeo4jなどの永続化ストアへの差し替えを検討するとよい。

押さえておきたい実装上のポイント

  • Wikipedia取得時にauto_suggest=Falseを指定する。指定しないと「Turing」が「tuning」に補正されてクラッシュする
  • Llama 3.2はローカル動作のため、API費用ゼロで運用できる
  • Google Colab(T4 GPU)での抽出処理は「数秒」と記述されており、現実的な速度感だ

非構造化テキストからナレッジグラフを自動構築するパイプラインを、外部APIへの依存なしに完結させられる点が本実装の特徴である。ローカルLLMの制約を逆手に取り、JSON強制モードとtemperature 0の組み合わせで出力品質を制御するアプローチは、プロダクション用途への応用を考える上でも参考になる。

詳細はAutomating Knowledge Graph Population: Extracting Entities and Triples from Unstructured Text with an LLMを参照していただきたい。