← 기사 목록
日本語https://zenn.dev/topics/llm/feed

LLMにトリプル抽出させたら壊れたKG ─ 構築自動化3パターンと落とし穴

추출된 키워드

34
トリプル抽出·5ナレッジグラフ·5KG·5LLM·5Microsoft GraphRAG·4Self-correction·4マルチパス·4スキーマ駆動抽出·4Few-shot Prompt·4LLMGraphTransformer·4LangChain·4OntoGPT·4Cypher·4Neo4j·4RDF·4プロパティグラフ·4JSON Schema·3矛盾の取り扱い·3関係の方向性·3エンティティ表記揺れ·3Microsoft·3LazyGraphRAG·3SPIRES·3LinkML·3Function calling·3GPT-5-mini·3Pydantic·3SPARQL·3SNOMED·2Gene Ontology·2SentenceTransformer·2intfloat/multilingual-e5-large·2Neo4j LLM Knowledge Graph Builder·2コサイン類似度·2

원문

10,679
LLMにトリプル抽出させたら壊れたKG ─ 構築自動化3パターンと落とし穴

LLMにトリプル抽出させたら壊れたKG ─ 構築自動化3パターンと落とし穴

5,000ドキュメントを人手で組む地獄

社内のRFP・契約書・議事録を全部ナレッジグラフ(KG)に載せたい、という相談を受けたことがあります。

ファイル数を数えたら5,200本ありました。1本あたり平均15分で読み、エンティティと関係を3組ずつ抽出する。電卓を叩くと1,300時間でした。私一人だと半年仕事です。レビューを足したら8ヶ月、年度内に間に合いません。

「人手抽出は無理。LLMに任せます」と即答したものの、3週間後に出来上がったKGはノードだけで12万、エッジは40万。重複と矛盾だらけで、Cypherクエリを投げるたびに違う答えが返ってきました。「Microsoft」のノードを数えたら7つあって、それぞれ違うエッジを持っていたときは天井を見上げました。

その失敗を踏まえて整理した「構築自動化の3パターン」と、どのパターンでも必ず踏む落とし穴をまとめます。私と同じ轍を踏まないでください。

LLMでKGを組む3パターン比較

2026年の前提: どこを使えば外さないか

3パターンに入る前に、2026年5月時点の主要ツールの現在地を押さえます。古い情報のまま「OntoGPTってまだあるんですか?」と聞かれる場面が増えたので。

Property Graph vs RDFはどちらを狙うか

抽出ツール選定の前に、ターゲットがプロパティグラフかRDFかで分岐します。私の結論はこうです。

業務アプリのバックエンドに組み込むならプロパティグラフ(Neo4jなど)。Cypher が書きやすく、エッジに属性を持てるので「出典URL」「信頼度スコア」を後付けできます。一方、医療・化学・公共データのように既存オントロジー(SNOMED、Gene Ontology等)とつなぐ前提ならRDF。OntoGPTはRDF寄りです。

迷ったらプロパティグラフでスタートし、外部公開フェーズでRDFエクスポートを足す、という順序を私は取ります。最初からRDFで組むと、SPARQLの学習コストでチームメンバーが脱落しやすいというのが、3案件回した上での実感です。

パターン1: Few-shot Prompt + 後段Validator

最もシンプルな構成です。プロンプトに3〜5例のトリプル抽出例を見せて、JSON配列で返してもらう。後段でJSON Schemaバリデータに通す、それだけ。

PROMPT = """以下のテキストからトリプルを抽出してください。

例1: テキスト「Microsoftは2024年にGraphRAGを公開した」
出力: [{"subject":"Microsoft","predicate":"published","object":"GraphRAG","year":2024}]

テキスト: {document}
出力:"""

抽出後はPydanticで型検証し、

predicate
が定義済みの12種類のいずれかでなければ捨てる。これだけで「LLMが勝手に発明した動詞」を弾けます。
from pydantic import BaseModel, field_validator

ALLOWED_PREDICATES = {"published", "acquired", "develops", "works_at", ...}

class Triple(BaseModel):
    subject: str
    predicate: str
    object: str

    @field_validator("predicate")
    def check_predicate(cls, v):
        if v not in ALLOWED_PREDICATES:
            raise ValueError(f"unknown predicate: {v}")
        return v

最初の案件では、validator なしで動かしたら

acquires
purchased
bought
が全部別の述語として登録されてしまい、後でCypherクエリが書けなくなりました。バリデータは初日から入れるべきです。

エラーを raise するか、ログに書いて捨てるかは好みです。私は最初の3日間は raise して目で確認し、述語の揺れが落ち着いたら warning ログに変えて捨てる、という運用にしています。raise のままだと夜間バッチが止まって朝の対応に追われます。

強み: 半日で組める。GPT-5-miniなら1,000ドキュメントで$3程度。
弱み: スキーマ進化に弱い。新しい述語が必要になるたびにプロンプトを書き直す。

PoCや「とりあえずKGを見てみたい」というフェーズなら、迷わずこれです。私の5,200ドキュメント案件もパターン1から始めるべきでした。最初から本気でパターン3を組んで、スキーマが固まる前にコストだけ垂れ流したのが当時の反省点です。

パターン2: スキーマ駆動抽出(OntoGPT / LangChain型)

スキーマを先に定義し、LLMにはそれを埋めるタスクだけ与える方式です。

OntoGPTならLinkML形式のスキーマYAML、LangChain LLMGraphTransformerなら

allowed_nodes
allowed_relationships
を渡します。
from langchain_experimental.graph_transformers import LLMGraphTransformer

transformer = LLMGraphTransformer(
    llm=llm,
    allowed_nodes=["Company", "Product", "Person"],
    allowed_relationships=["ACQUIRED", "DEVELOPS", "WORKS_AT"],
    node_properties=["name", "founded_year"],
    relationship_properties=["date", "amount"],
)
graph_documents = transformer.convert_to_graph_documents(docs)

LLMがFunction callingで構造化出力するため、JSONパースの失敗がほぼなくなります。OntoGPTのSPIRESはさらに洗練されていて、ネストしたオブジェクトを根からたどって再帰抽出します(SPIRES論文)。

OntoGPT 側のスキーマはLinkMLのYAMLで書きます。クラス・スロット・enumを階層的に定義するだけで、LLMへのプロンプトも自動生成されます。

classes:
  Acquisition:
    attributes:
      acquirer:
        range: Company
      acquired:
        range: Company
      date:
        range: date
      amount_usd:
        range: integer

このスキーマだけで「主語と目的語の取り違え」がほぼ起きなくなります(後述の落とし穴2)。なぜなら

acquirer
acquired
という名前で意味が制約されるからです。

強み: スキーマで型が固定されるため、表記揺れ以外のノイズはかなり減る。
弱み: スキーマ設計が前提作業として重い。第4章で書いたNeo4j 7ステップのStep 3そのものです。

5,200ドキュメント案件は、結局ここに行き着きました。最初の2週間でオントロジーを固め、残り1週間で抽出を回す、という配分でした。スキーマ確定までは「とにかくPMと業務担当者と会議室にこもる」が一番効きます。エンジニアだけで設計すると、現場で実際に使う関係が漏れます。

パターン3: マルチパス + Self-correction

抽出 → 検証 → 修正の3回LLMを走らせる方式です。

  • Pass 1: パターン2と同じ方式でトリプル抽出
  • Pass 2: 抽出結果を原文と並べてLLMに「事実関係が一致しているか」を問う
  • Pass 3: Pass 2で不一致だったトリプルだけ、修正案をLLMに出させる

Microsoft GraphRAGの内部処理に近い構造です(GraphRAG GitHub)。最近はLazyGraphRAGがこのうちPass 2/3を検索時に遅延実行する方式を提案していて、構築コストが下がっています。

# Pass 2: 検証
verify_prompt = f"""
原文: {source_text}
抽出されたトリプル: {triples_json}

各トリプルについて、原文と矛盾しないか judge してください。
出力: [{{"triple_id":1,"verdict":"valid|invalid|partial"}}]
"""

Pass 3 はこんな具合です。

# Pass 3: 修正
fix_prompt = f"""
原文: {source_text}
不正確と判定されたトリプル: {invalid_triples}
原文に基づいて、各トリプルを修正してください。
修正不能(=原文に該当する事実がない)なら null で返してください。
"""

Pass 2 で

invalid
判定が出たトリプルだけPass 3に回せば、コストを抑えられます。私の案件ではPass 2の
invalid
率は8%程度でした。

強み: 精度が体感で2割以上上がる。本番グレードのKGを組むならこれ一択。
弱み: トークンコストが3倍。GPT-5系で本気で回すと、1,000ドキュメントで$30〜$50。月次バッチで5万ドキュメント回すと、ざっくり$2,000のラインに乗ります。

3パターンの選び方

私の使い分けはこうです。

フェーズパターン理由
PoC・1週間で見せたいパターン1半日で組める
本番投入・スキーマが固まったパターン2コストと精度のバランス
規制業界・監査対象パターン3Self-correctionで後から説明できる

順番に昇格していくのが安全です。最初からパターン3を組むと、スキーマがブレた瞬間に全部のパスを書き直すことになります。

チーム規模で見ると、1人なら迷わずパターン1。2〜3人ならパターン2。5人以上で本番運用するチームならパターン3。それぞれ「人がスキーマレビューに割ける時間」「コストの説明責任の重さ」が判断材料です。私が見てきた失敗の多くは、人数とパターンのミスマッチでした。

評価と更新の現実については、過去記事『KG×LLMを本番に入れて気づいた評価・更新の現実』も参照してください(検索すれば出てきます)。本番運用ではトリプル抽出の精度より、更新パイプラインの設計のほうが事故率を左右します。新しい文書が来たときに「再構築するのか」「差分マージするのか」を決めずに作ると、半年後に誰もKGを信用しなくなります。

どのパターンでも踏む3つの落とし穴

KG構築で必ず踏む3つの落とし穴

ここから先は、パターン選定とは別レイヤの話です。3つとも私が踏み抜きました。

落とし穴1: エンティティ表記揺れ

「Microsoft」「マイクロソフト」「MS」「Microsoft Corporation」が4つの別ノードになって、Cypherクエリで「Microsoftの製品一覧」を出すと半分しか取れない、というやつです。

対策は2段構えです。

# 段1: 正規化辞書(手で20社ほど作る)
ALIASES = {
  "マイクロソフト": "Microsoft",
  "MS": "Microsoft",
  "Microsoft Corporation": "Microsoft",
}

# 段2: 埋め込み類似度で残りをマージ
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("intfloat/multilingual-e5-large")
# コサイン類似度0.92以上を同一エンティティ候補にする

Neo4j LLM Knowledge Graph Builderには post-extraction clean-up が組み込まれていて、この処理を自動化してくれます。自前実装が面倒ならこちらを使うのも手です。

ただ、自動マージは「気付かないうちに別物が同一視される事故」も起こします。例えば「Apple(会社)」と「apple(果物)」がコサイン類似度0.93で1ノードに統合されたことがありました。マージ閾値を上げるか、ドメイン語(

Company
vs
Food
)で先にクラスタを切るのが防御策です。

最終的に有効だったのは、辞書を「育てる」という発想でした。最初の20社で立ち上げて、運用しながら週次でレビュー会を回し、半年で200社まで増やす。一気に作ろうとするとレビューが追いつきません。辞書は機械学習モデルと同じで、データが集まるほど精度が上がります。

落とし穴2: 関係の方向性

「AがBを買収した」が「BがAを買収した」になる事故です。日本語は語順が緩いため、LLMが主語と目的語をひっくり返すことが体感で10〜15%発生します。

対策はプロンプトで方向を明示すること。

述語ACQUIREDは「買収企業 -[ACQUIRED]-> 被買収企業」と読んでください。
例: Microsoft -[ACQUIRED]-> GitHub (Microsoftが買収企業)

それでも10%ぐらいは間違えます。パターン3のSelf-correctionで「主語と目的語を入れ替えたほうが原文と合うか」を聞き直すと、ほぼ救えます。

スキーマ駆動(パターン2)を使うと、述語名そのものが方向を担うので事故率が下がります。

ACQUIRED
ではなく
acquirer / acquired
のような attribute 名にした OntoGPT スキーマが強いのは、この点でも理にかなっています。

落とし穴3: 矛盾の取り扱い

文書Aには「CEOは山田」、文書Bには「CEOは佐藤」と書いてある。LLMは両方とも素直にトリプル化します。

私が最初にやった失敗は「新しい日付の文書を優先」というルールを足したことでした。結果、古い契約書を参照したい場面で答えが出なくなりました。

正解はこうです。

  • トリプルを 削除しない
  • エッジ属性に
    source_doc_id
    extracted_at
    を必ず付ける
  • 矛盾は「矛盾として可視化」する。クエリ時にユーザーに見せる

矛盾の存在自体が、ビジネス上の有用なシグナルだったりします。隠してはいけません。

実際の案件では、契約書のKGに「契約金額」が3パターン同居していたことがありました。覚書で増額、別紙で減額、本文で初期金額。これを1つに正規化するとビジネス側が「そんなはずはない」と怒り出します。3つを並べて見せると「あ、それは別紙が間違ってます」と即答が出ました。矛盾は意思決定の入り口です。

まとめ

  • KGの自動構築は3パターンに整理できる: Few-shot / スキーマ駆動 / マルチパス
  • フェーズに応じて1→2→3と昇格させるのが安全
  • 2026年はOntoGPT・LangChain LLMGraphTransformer・Neo4j LLM Graph Builder・Microsoft LazyGraphRAGが主要選択肢
  • パターン選定とは別レイヤで、表記揺れ・方向性・矛盾の3つは必ず踏む
  • 矛盾は隠さず可視化する。削除すると後で痛い目を見る

5,200ドキュメント案件はパターン2 + 落とし穴対策3点セットで、最終的に精度78%まで持ち上がりました。100点には届きませんが、人手レビューと組み合わせれば実用域です。半年運用したあとに測り直すと、辞書とスキーマが太っていって85%まで上がりました。KGは生き物です。

最後にコスト感の目安を置いておきます。1,000ドキュメント規模で、パターン1=$3、パターン2=$10前後、パターン3=$30〜$50。年間の運用ではこれに加えてレビュー工数とインフラ費が乗りますが、人手で組むよりはるかに安いのは間違いありません。1,300時間の人手作業と比べると、たとえパターン3で月次$2,000かかったとしても安いものです。

KGは作って終わるものではありません。運用しながら磨いていきます。最初から100点を狙わず、まずパターン1で動くものを見せましょう。動くものさえあれば、社内の協力者は集まります。

GitHubで編集を提案