LLMと90分でオントロジーを作る ―修理報告書3通からナレッジグラフまで
連載「AIエージェント時代のオントロジー」第3回
(手を動かして検証するハンズオン完全版・付録つき)
「LLMにオントロジー(業務の言葉の定義集)を作らせたい」
しごくもっともな要望です。
正直、手作業でやりたい仕事ではないからです。LLMならオントロジーを構築するために自分の頭と手でコツコツと積み上げる作業を一気に解決してくれそうです。
ところがそこに罠が潜んでいます。最も重要なことはLLMはときどき間違える。
実在しない語彙を平気で使い、機種と個体を混ぜ、聞いてもいないことを補って書く。
「こんなの使い物にならん」
この何回も現場で吐かれまくった言葉が聞こえます。
では信用できないから使えないのか?
いいえ、違います。この問題を解決するために、間違いを機械で受け止める仕組みごと回せばいいのです。
今日はそのプロセスを実際に手を動かしながら、具体例として「修理報告書」たった3通を題材にしてこの仕組みを一周してみます。
前回のコラムで、AIを「博識だが自社を知らない新人」にたとえました。何年経っても社内の"例のヤツ"を覚えない、けれど一般知識は驚くほど豊富な新人です。今日はその新人に「会社の言葉」を渡す作業をします。配役を先に決めておきましょう。
LLM=大量の下書きを高速に出す博識な新人
人間=検収基準を持つレビュアー
検証ツール=機械的にルール違反をはじく門番
全工程はこの三者による「生成→検証→差し戻し」のループです(図)。

新人が下書きし、レビュアーが基準に照らして見て、門番が機械的にはじく。はじかれたら差し戻して、また生成する。この一周を自分の手で回し切ることが今日のゴールです。所要時間はおよそ90分。週末にでも試してみてください。
準備(5分)
Python環境を用意します。uv(高速なPythonパッケージ管理ツール)を使うと3行で終わります。
uv init hk-handson
cd hk-handson
uv add rdflibrdflib(PythonでRDF/Turtleを読み書きし、SPARQLという問い合わせ言語を実行できるライブラリ)だけあれば、今日の全工程が動きます。
題材:修理報告書3通
農機具メーカーの修理報告書3通を題材にします。どこの会社にもある、ごく普通の業務文書です。
報告書1 :6月3日、田中農園様のトラクターHK-200(製造番号SN-88613)のエンジン始動不良で訪問。バッテリー端子の腐食を確認し、端子交換で復旧。担当:佐藤。
報告書2 :6月10日、みどり営農組合様のコンバインRX-50(SN-90211)、刈取部の異音。ベアリング摩耗のため部品交換。次回定期点検は9月を提案。担当:鈴木。
報告書3 :6月18日、田中農園様より再訪依頼。SN-88613、今度は油圧系の応答遅れ。作動油の劣化と判断し交換。保守契約(C-2025-0113)の範囲内で対応。担当:佐藤。
今回はこのデータを使いますが、この仕組みのサイクルがわかったら、自社の実務データで試すのもよいと思います。
自社の文書に差し替えて試す場合は、機密情報・個人情報の扱いを社内のAI利用規程で確認するのが第0ステップです。ここは省略しないでください。
Step 1:まず『問い』を作らせる(10分)
最初にやるのはスキーマ作りではありません。
意外だと思いますが、実は問いを決めることです。
オントロジー設計の世界では、完成したナレッジグラフが答えるべき問いをCQ(コンピテンシー・クエスチョン=そのデータが答えられるべき業務上の質問)と呼びます。答えるべき問いが決まっていなければ、何を作っても検収のしようがない。
そこで、まず問いを生成する手伝いをLLMに依頼します。
このプロンプトをLLMに渡します。
あなたはオントロジー設計の支援者です。以下の修理報告書を読み、
この業務データに対して現場が問いたくなる「コンピテンシー・クエスチョン」
の候補を10個提案してください。
条件:
- 複数の概念を関係でまたぐ問いにする(単一の値を聞くだけの問いは不可)
- 「誰が・いつ・どの機体に・何をしたか」の観点を必ず含める
- 各CQに、答えるために必要な概念を添える
[報告書1〜3を貼り付け]10個の候補が返ってきます。ここからが人間の仕事です。私は5つに絞りました。
ちなみに、候補は毎回異なるCQを出すこともあるでしょう。
でもその中で、コツは実際の現場でやり取りされる「問い」を選べばよいのです。
CQ1:ある顧客が所有する全機体と、それぞれの機種は?
CQ2:ある機体に対して行われた全修理の、日付・担当・症状・処置は?
CQ3:特定の担当者が対応した修理の一覧は?
CQ4:保守契約の範囲内で対応された修理はどれか?
CQ5:ある機種の個体を所有する顧客は?(機種→個体→所有→顧客の多段)
落としたものの例も挙げておきます。
「HK-200の馬力は?」は単一の値を聞くだけで狭すぎる。
「各修理のときのその場の天気は?」はそもそも報告書にデータがない。
10個を5個に絞る――この取捨選択は業務を知る人間にしかできない仕事で、LLMには任せられません。
太字にしたCQ4は覚えておいてください。あとで小さな事件が起きます。
なお、この「問いを先に決めてから作る」進め方は、ソフトウェア開発のテスト駆動開発(TDD)とまったく同じ構造です。TDDを経験しているかたはたぶんこの時点で腑に落ちると思いますが、TDDってなに?というかたはある違和感を感じるかもしれません。
これについては後章の設計方法論の回で本格的に扱います。
Step 2:スキーマの下書き(15分)
CQが決まったら、それに答えるためのスキーマ(データの設計図)をLLMに下書きさせます。
ここで重要なのは、プロンプトに設計規約、つまり禁止事項を明文化して渡すことです。
新人に仕事を頼むとき、やってほしいことだけでなく「やってはいけないこと」を先に伝えるのと同じです。
合意済みCQ(以下)に答えるためのオントロジーをTurtleで下書きしてください。
[絞った5個のCQ]
設計規約(厳守):
1. 名前空間: 語彙は hko: <https://example.com/hk/ontology#>、
クラスはUpperCamelCase、プロパティはlowerCamelCase、英語名+日本語rdfs:label
2. 機種(ProductModel)と個体(MachineUnit)は別クラスにし、
owl:disjointWith を宣言。個体→機種は hko:model で結ぶ
3. 「顧客」「担当者」を Person のサブクラスにしない(役割は関係で表す)
4. 修理は出来事としてクラス化(RepairEvent)し、対象個体・日付・
担当・症状・処置をプロパティでぶら下げる
5. 全クラス・全プロパティに rdfs:label(日本語)と rdfs:comment を付ける
6. プロパティには rdfs:domain / rdfs:range を必ず宣言する
7. CQに不要な概念は作らない(過剰設計の禁止)Turtle(RDFを人間が読み書きしやすく書くためのテキスト形式)でスキーマ全体が返ってきます。その核になるのがこの断片です。
これで、基本的なスキーマとしてのオントロジーの一部が構築されたことになります。生成結果をファイルに保存してください。拡張子は.ttlが一般的です。
hko:ProductModel a owl:Class ;
rdfs:label "機種"@ja ;
owl:disjointWith hko:MachineUnit . # 機種と個体は決して交わらない
hko:MachineUnit a owl:Class ;
rdfs:label "機体(個体)"@ja .中身をみてみましょう。これがTurtleのデータです。今はよくわからなくてもかまいません。
A a Bという並びがめちゃたくさんあるなあ、程度の解像度でいいのです。
たとえば、owl:disjointWithの一行は「HK-200というカタログ上の機種と、工場から出荷された1台1台の個体は、絶対に同じものではない」という宣言です。
当たり前に聞こえますか?
しかし規約2を書かずに生成させると、LLMはかなりの確率でHK-200と製造番号SN-88613を同じ階層に並べてきます。
これは人間の初学者が必ず落ちる穴と同じ穴です。つまりLLMは初学者と同じ穴に、初学者より速く落ちる。だから穴の場所をあらかじめ規約として渡しておくのです。なぜこの区別に一行を割く価値があるのかは、次回の基礎編でじっくり扱います。
(スキーマの全体は末尾の付録に載せています。ここでは核だけを示しました。)
Step 3:人間レビュー(10分)
出てきたスキーマを、レビュアーとしてチェックします。観点は3つに絞れば十分です。
「すべての○○は△△である」と音読して反例が出るis-a関係(上位・下位の分類関係)はないか
HK-200やSN-88613のような固有名詞が、クラスとして紛れ込んでいないか(頻出)
CQに不要な概念(「天気」「圃場」など)を勝手に作っていないか
見つけた問題は、その場で手で直さないでください。指摘として差し戻し、LLMに直させるのが原則です。手で直すとプロンプト側の規約が育たず、次の生成でまた同じ間違いが出ます。差し戻せば、指摘そのものが規約に昇格し、資産になります。
なお、このステップは最初戸惑うかもしれませんが、厳密に100%を目指して修正する必要はありません。人間だって、LLMだって抜けや間違いは発生するというやんわりな気持ちでよいです。
Step 4:オントロジーを『型』にして抽出(15分)
検収済みのスキーマを今度は「型」として使い、報告書3通からデータ(トリプル=主語・述語・目的語の3点セット)を抽出させます。
以下のオントロジーの語彙【のみ】を使い、報告書1〜3の内容を
Turtleのトリプルとして抽出してください。
規約:
- インスタンスのURIは hk: 配下。個体は製造番号、修理は hk:repair-001 形式の連番
- オントロジーに存在しないクラス・プロパティを発明しない。
表現できない情報は末尾に「## 未表現」としてコメントで列挙する
- 不確かな推測でトリプルを作らない
[検収済みオントロジー]
[報告書1〜3]仕掛けは2番目の規約です。語彙の発明を禁止したうえで、こぼれた情報を「未表現」として報告させる。実際に回すと、抽出結果の末尾にこう上がってきました。
## 未表現
# 報告書3「保守契約(C-2025-0113)の範囲内で対応」
# → 修理と保守契約を結ぶプロパティも、契約クラスもスキーマに無いこれは見過ごせない報告です。LLMが、自分に渡されたスキーマの穴を報告してきたのですから。そして思い出してください――CQ4は「保守契約の範囲内で対応された修理はどれか?」でした。契約を表す語彙がスキーマにない以上、このままではCQ4に答えられないはずです。この予感を胸に、次のステップへ進みます。
(抽出されたデータの全体は、末尾の付録の `data.ttl` にあります。ちなみに付録のデータは完全版なのでテストに使う場合は、適宜修正してお使いください)
Step 5:機械の門番と受け入れテスト(25分)
ここが今日の山場です。人間の目視レビューには限界があります。だから機械の門番を立てる。以下が検証スクリプトの全文です。約40行、これで完結します。
"""門番(A構文/B語彙)+ 受け入れテスト(5CQ)。
使い方: uv run validate_min.py <ontology.ttl> <data.ttl>
"""
import sys
from rdflib import Graph, RDF, RDFS, OWL
PREFIX = """
PREFIX hk: <https://example.com/hk/resource/>
PREFIX hko: <https://example.com/hk/ontology#>
"""
CQ = {
"CQ1 顧客が所有する機体と機種":
"SELECT ?c ?u ?m WHERE { ?x hko:owns ?u ; hko:name ?c . ?u hko:model ?y . ?y hko:name ?m . }",
"CQ2 SN-88613 の全修理(日付・症状)":
"SELECT ?d ?s WHERE { ?r hko:repairTarget hk:SN-88613 ; hko:repairedOn ?d ; hko:symptom ?s . }",
"CQ3 佐藤が担当した修理":
"SELECT ?d WHERE { ?t hko:name '佐藤' . ?r hko:handledBy ?t ; hko:repairedOn ?d . }",
"CQ4 保守契約の範囲内で対応された修理":
"SELECT ?d ?no WHERE { ?r hko:coveredByContract ?k ; hko:repairedOn ?d . ?k hko:contractNo ?no . }",
"CQ5 HK-200 を所有する顧客":
"SELECT ?c WHERE { ?x hko:owns ?u ; hko:name ?c . ?u hko:model hk:HK-200 . }",
}
# (A) 構文チェック: パースできなければここで例外
onto = Graph().parse(sys.argv[1], format="turtle")
data = Graph().parse(sys.argv[2], format="turtle")
print(f"(A) 構文OK スキーマ {len(onto)} / データ {len(data)} トリプル")
# (B) 語彙チェック: データの述語・クラスがスキーマに宣言済みか
BUILTIN = (str(RDF), str(RDFS), str(OWL), "http://www.w3.org/2001/XMLSchema#")
props = set(onto.subjects(RDF.type, OWL.ObjectProperty)) | set(onto.subjects(RDF.type, OWL.DatatypeProperty))
classes = set(onto.subjects(RDF.type, OWL.Class))
bad = [f"述語 {p}" for p in set(data.predicates())
if p != RDF.type and not str(p).startswith(BUILTIN) and p not in props]
bad += [f"クラス {c}" for c in set(data.objects(None, RDF.type))
if not str(c).startswith(BUILTIN) and c not in classes]
print("(B) 語彙OK" if not bad else "(B) 語彙NG " + " / ".join(bad))
# (C) 受け入れテスト: 5つのCQをSPARQLで問う
g = onto + data
for label, q in CQ.items():
n = len(list(g.query(PREFIX + q)))
print(f" [{'OK' if n else '空 '}] {label}: {n}件")門番の仕事は2つです。
(A)構文チェック
そもそもTurtleとしてパース(構文解析)できるか。LLMはたまに閉じ忘れやセミコロンの欠落をやらかしますが、ここで例外が出て即座に止まります。
(B)語彙チェック
データに出てくる述語とクラスが、すべてスキーマに宣言済みか。つまり「その言葉は台帳に登録済みか?」を機械的に照合します。
お気づきでしょうか。この(B)は、Step 4でLLMに課した「スキーマにない語彙は使うな、こぼれは未表現として報告せよ」とまったく同じ問いです。未表現は抽出側(生成する側)からの照合、語彙チェックは検証側(受け取る側)からの照合。同じ一つの問いを、ループの両側から掛けているのです。この背骨は最後にもう一度回収します。
そして
(C)受け入れテスト
5つのCQをRDFデータの問い合わせ機能をもったSPARQLで実際に問い、答えが返るかを数えます。CQを最初に決めたのは、まさにこの瞬間のためでした。実行します。
uv run validate_min.py ontology.ttl data.ttl結果はこうなりました(コンソールで日本語が化ける場合は、実行前に環境変数 PYTHONUTF8=1 を設定してください)。
(A) 構文OK スキーマ 60 / データ 37 トリプル
(B) 語彙OK
[OK] CQ1 顧客が所有する機体と機種: 2件
[OK] CQ2 SN-88613 の全修理(日付・症状): 2件
[OK] CQ3 佐藤が担当した修理: 2件
[空 ] CQ4 保守契約の範囲内で対応された修理: 0件
[OK] CQ5 HK-200 を所有する顧客: 1件構文は通り、語彙も通り、CQ1・2・3・5は答えが返る。しかしCQ4だけが0件で落ちました。Step 1で予告した事件です。原因はもう分かっています。
Step 4の「未表現」が教えてくれたとおり、保守契約を表す語彙がスキーマに存在しないのです。目視では「なんとなく良さそう」で通ってしまいそうな設計の穴を、受け入れテストが数字で突きつけてくれました。
差し戻し ―設計を直す
事件の処理は差し戻しです。データを小手先でいじるのではなく、スキーマ側に保守契約クラスと、修理と契約を結ぶプロパティを追加します。
hko:MaintenanceContract a owl:Class ;
rdfs:label "保守契約"@ja ;
rdfs:comment "顧客と結ぶ保守契約。契約番号で識別する。"@ja .
hko:coveredByContract a owl:ObjectProperty ;
rdfs:label "適用保守契約"@ja ;
rdfs:domain hko:RepairEvent ;
rdfs:range hko:MaintenanceContract .
hko:contractNo a owl:DatatypeProperty ;
rdfs:label "契約番号"@ja ;
rdfs:domain hko:MaintenanceContract ;
rdfs:range xsd:string .データ側には、契約のインスタンスと、報告書3の修理を契約に結ぶ一行を足します。
hk:contract-C-2025-0113 a hko:MaintenanceContract ;
hko:contractNo "C-2025-0113" .
# repair-003 に一行足す:
hk:repair-003 hko:coveredByContract hk:contract-C-2025-0113 .再実行します。
(A) 構文OK スキーマ 73 / データ 40 トリプル
(B) 語彙OK
[OK] CQ1 顧客が所有する機体と機種: 2件
[OK] CQ2 SN-88613 の全修理(日付・症状): 2件
[OK] CQ3 佐藤が担当した修理: 2件
[OK] CQ4 保守契約の範囲内で対応された修理: 1件
[OK] CQ5 HK-200 を所有する顧客: 1件CQ4が1件――報告書3の油圧系修理――で通り、全チェック通過です。生成→検証→差し戻し→再生成のループが、これで一周を閉じました。
このハンズオン90分の中で一番大事なのは、きれいな成果物ではなく、この「一周した」という体感です。
これが人間とLLMと共同してオントロジーを回す感覚なのです。
おまけ:門番は語彙のごまかしも見抜く
もう一つだけ試しておきます。
LLMがスキーマにない語彙を発明した場合は考えてみましょう。
たとえば修理費用をhko:repairCost、天気をhko:Weatherと勝手に書いてきた場合を想定した壊れデータを、門番に食わせてみます。
(A) 構文OK スキーマ 73 / データ 5 トリプル
(B) 語彙NG 述語 https://example.com/hk/ontology#repairCost / クラス https://example.com/hk/ontology#Weather構文としては完全に正しいのに、語彙チェック(B)が未宣言の述語とクラスを2件はじきました。
ここが恐ろしいところで、この種のミスはTurtleとして正しく、名前もそれらしいため、目視レビューではまず見逃します。
人間のレビュアーは設計の筋の良し悪しを見る係、機械の門番は台帳との照合を漏れなくやる係。この役割分担がはっきりしているからこそ、両方が活きるのです。
なお、より本格的な制約検証にはSHACLという別の標準もありますが、今日の規模ならこの40行で十分です。
持ち帰ってほしいこと
3つだけ持ち帰ってください。
1つ目。資産は規約であり、対話は消耗品です。 今日LLMと交わしたチャットのやり取りは、明日には使い捨てて構いません。しかしStep 2の設計規約7カ条とStep 4の抽出規約、そしてvalidate_min.pyは、報告書が3通から1万通に増えてもそのまま再利用できます。差し戻しのたびに規約へ一行足していけば、新人はミスの傾向ごと組織に「覚えさせられて」いきます。
2つ目。今日の全工程を貫いていたのは、たった一つの問いです。「その言葉は、スキーマという台帳に登録済みか?」――Step 4の「未表現」は、この問いを抽出側から掛けたものでした。Step 5の語彙チェックは、同じ問いを検証側から掛けたものでした。CQ4の事件は、台帳に「保守契約」という言葉が載っていなかったことが、受け入れテストの数字になって現れたものでした。オントロジーを作るとは、結局のところ、組織で使う言葉を一つの台帳に保ち続ける営みなのです。
3つ目。主題に戻ります。LLMは間違えます。 それは今日も何度も見たとおりです。しかし、門番が機械的にはじき、テストが数字で答え、差し戻しが規約を育てる――この一周のループがある限り、その間違いは怖くありません。むしろ間違える前提が仕組みに織り込まれているからこそ、安心してLLMに大量の下書きをさせられる。「信用できないから使わない」ではなく「信用しなくても回る仕組みで使う」。これが、AIエージェント時代にオントロジーを作る側の基本姿勢です。
付録:完成した2ファイル
本文のプロンプトで生成すると、細部は各自のLLMごとに変わります。ここでは、私が実際に検証に使った最終形を置いておきます。この2つを `ontology.ttl` / `data.ttl` として保存し、`uv run validate_min.py ontology.ttl data.ttl` を実行すると、本文と同じ「スキーマ73 / データ40・全チェック通過」がそのまま再現できます。
冒頭のCQ4が落ちる状態を再現したい人は、保守契約まわり――スキーマの `MaintenanceContract`・`coveredByContract`・`contractNo` の3ブロックと、データの契約2行――を消せば、出発点の状態(スキーマ60 / データ37)に戻ります。
ontology.ttl(スキーマ)
@prefix hko: <https://example.com/hk/ontology#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
## クラス
hko:ProductModel a owl:Class ;
rdfs:label "機種"@ja ;
rdfs:comment "カタログ上の製品モデル。個体ではなく型そのもの(例:HK-200)。"@ja ;
owl:disjointWith hko:MachineUnit .
hko:MachineUnit a owl:Class ;
rdfs:label "機体(個体)"@ja ;
rdfs:comment "工場から出荷された1台の個体。製造番号で識別する(例:SN-88613)。"@ja .
hko:Customer a owl:Class ;
rdfs:label "顧客"@ja ;
rdfs:comment "当社と取引のある組織または個人。役割でありPersonのサブクラスにはしない。"@ja .
hko:Technician a owl:Class ;
rdfs:label "担当者"@ja ;
rdfs:comment "修理に対応する当社の技術者。役割でありPersonのサブクラスにはしない。"@ja .
hko:RepairEvent a owl:Class ;
rdfs:label "修理"@ja ;
rdfs:comment "特定の機体に対して行われた1回の修理という出来事。"@ja .
hko:MaintenanceContract a owl:Class ;
rdfs:label "保守契約"@ja ;
rdfs:comment "顧客と結ぶ保守契約。契約番号で識別する(例:C-2025-0113)。"@ja .
## プロパティ(関係)
hko:model a owl:ObjectProperty ;
rdfs:label "機種"@ja ; rdfs:comment "この個体が属する機種。"@ja ;
rdfs:domain hko:MachineUnit ; rdfs:range hko:ProductModel .
hko:successor a owl:ObjectProperty ;
rdfs:label "後継機種"@ja ; rdfs:comment "この機種の直接の後継機種。"@ja ;
rdfs:domain hko:ProductModel ; rdfs:range hko:ProductModel .
hko:owns a owl:ObjectProperty ;
rdfs:label "所有する"@ja ; rdfs:comment "顧客がこの機体を所有している。"@ja ;
rdfs:domain hko:Customer ; rdfs:range hko:MachineUnit .
hko:repairTarget a owl:ObjectProperty ;
rdfs:label "対象機体"@ja ; rdfs:comment "この修理の対象となった機体。"@ja ;
rdfs:domain hko:RepairEvent ; rdfs:range hko:MachineUnit .
hko:handledBy a owl:ObjectProperty ;
rdfs:label "担当者"@ja ; rdfs:comment "この修理を担当した技術者。"@ja ;
rdfs:domain hko:RepairEvent ; rdfs:range hko:Technician .
hko:coveredByContract a owl:ObjectProperty ;
rdfs:label "適用保守契約"@ja ; rdfs:comment "この修理が範囲内で対応された保守契約。"@ja ;
rdfs:domain hko:RepairEvent ; rdfs:range hko:MaintenanceContract .
## プロパティ(属性)
hko:name a owl:DatatypeProperty ;
rdfs:label "名称"@ja ; rdfs:comment "表示用の名称。"@ja ; rdfs:range xsd:string .
hko:contractNo a owl:DatatypeProperty ;
rdfs:label "契約番号"@ja ; rdfs:comment "保守契約の識別番号(例:C-2025-0113)。"@ja ;
rdfs:domain hko:MaintenanceContract ; rdfs:range xsd:string .
hko:repairedOn a owl:DatatypeProperty ;
rdfs:label "修理日"@ja ; rdfs:comment "この修理を行った日付。"@ja ;
rdfs:domain hko:RepairEvent ; rdfs:range xsd:date .
hko:symptom a owl:DatatypeProperty ;
rdfs:label "症状"@ja ; rdfs:comment "報告された不具合の症状。"@ja ;
rdfs:domain hko:RepairEvent ; rdfs:range xsd:string .
hko:treatment a owl:DatatypeProperty ;
rdfs:label "処置"@ja ; rdfs:comment "実施した処置の内容。"@ja ;
rdfs:domain hko:RepairEvent ; rdfs:range xsd:string .data.ttl(データ)
@prefix hk: <https://example.com/hk/resource/> .
@prefix hko: <https://example.com/hk/ontology#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
## 機種
hk:HK-200 a hko:ProductModel ;
hko:name "トラクター HK-200" ;
hko:successor hk:HK-300 .
hk:RX-50 a hko:ProductModel ;
hko:name "コンバイン RX-50" .
## 機体(個体)
hk:SN-88613 a hko:MachineUnit ; hko:model hk:HK-200 .
hk:SN-90211 a hko:MachineUnit ; hko:model hk:RX-50 .
## 顧客と所有
hk:customer-tanaka a hko:Customer ;
hko:name "田中農園" ; hko:owns hk:SN-88613 .
hk:customer-midori a hko:Customer ;
hko:name "みどり営農組合" ; hko:owns hk:SN-90211 .
## 担当者
hk:tech-sato a hko:Technician ; hko:name "佐藤" .
hk:tech-suzuki a hko:Technician ; hko:name "鈴木" .
## 保守契約(差し戻しで追加)
hk:contract-C-2025-0113 a hko:MaintenanceContract ;
hko:contractNo "C-2025-0113" .
## 修理
hk:repair-001 a hko:RepairEvent ;
hko:repairTarget hk:SN-88613 ;
hko:repairedOn "2026-06-03"^^xsd:date ;
hko:handledBy hk:tech-sato ;
hko:symptom "エンジン始動不良、バッテリー端子の腐食" ;
hko:treatment "バッテリー端子の交換" .
hk:repair-002 a hko:RepairEvent ;
hko:repairTarget hk:SN-90211 ;
hko:repairedOn "2026-06-10"^^xsd:date ;
hko:handledBy hk:tech-suzuki ;
hko:symptom "刈取部の異音、ベアリング摩耗" ;
hko:treatment "ベアリングを含む部品交換" .
hk:repair-003 a hko:RepairEvent ;
hko:repairTarget hk:SN-88613 ;
hko:repairedOn "2026-06-18"^^xsd:date ;
hko:handledBy hk:tech-sato ;
hko:symptom "油圧系の応答遅れ、作動油の劣化" ;
hko:treatment "作動油の交換" ;
hko:coveredByContract hk:contract-C-2025-0113 .次回予告
次回・第4回は「賢いのに、信用できないAIへ ――なぜ今、オントロジーか」。ここからが基礎編です。今日は規約として天下り的に渡した「なぜ機種と個体を分けるのか」「なぜ役割をサブクラスにしないのか」といった設計の"なぜ"を、一つずつ腑に落としていきます。
前回:[RAGはどこで力尽きるか ――GraphRAGが解いた3つの弱点]
試した結果の共有は #AI時代のオントロジー でぜひ。マガジンをフォローすると毎週火曜の更新が届きます。
Q&Aコーナー
全チェックが「0件」になってしまう
公開後さっそく手を動かしてくださった読者の方から、こんな報告をいただきました。この連載でいちばん大事にしている「自分で試す」を実践してくださった質問なので、章末で取り上げます。
自分の題材でオントロジーとデータのttlを作って検証にかけたら、こうなりました。原因は何でしょうか?
(A) 構文OK スキーマ 93 / データ 41 トリプル
(B) 語彙OK
[空 ] CQ1 顧客が所有する機体と機種: 0件
[空 ] CQ2 SN-88613 の全修理: 0件
[空 ] CQ3 佐藤が担当した修理: 0件
[空 ] CQ4 保守契約の範囲内で対応された修理: 0件
[空 ] CQ5 HK-200 を所有する顧客: 0件
まず、これは壊れているのではありません。むしろ門番が正しく仕事をした結果です。順に読み解きましょう。
(A)が通っているので、ttlは構文的に正しく書けています。(B)も通っているので、データで使った語はすべて自分のスキーマに宣言済みです。ここまでは満点。ところが (C)受け入れテストだけが全部0件 。この「(A)(B)は通るのに(C)だけ0件」という組み合わせが、原因をピンポイントで指しています。
鍵は、(B)と(C)が別のものを見ていることです。(B)の語彙チェックは「データがスキーマの中に収まっているか」という、ファイル内部だけで完結する照合でした。あなたのttlが独自の名前で首尾一貫していれば、その名前が何であろうと(B)は通ります。いっぽう(C)のCQは、`validate_min.py` の中に `hko:owns`・`hko:name`・`hk:SN-88613` といった具体的な名前がベタ書きされています。つまり(C)は、あなたのttlではなく「記事の付録と同じ名前」を探しに行くのです。
そして、ここが今日の主題そのものなのですが――LLMに作らせると、名前は毎回変わります。 名前空間が `https://example.com/hk/ontology#` ではなく各自の `http://example.org/…` になっていたり、「所有する」が `owns` ではなく `hasOwner`、顧客の識別子が `customer-tanaka` ではなく `tanaka` になっていたり。どれか一つでもCQのベタ書きとズレれば、パターンは一致せず、静かに0件が返ります。スキーマが93トリプル(付録は73)と多めなのも、LLMが独自に、少しリッチに、そして独自の名前で書いた証拠です。
原因を目で確かめるには、数行で足ります。
from rdflib import Graph
g = Graph().parse("あなたのontology.ttl", format="turtle")
g.parse("あなたのdata.ttl", format="turtle")
for p in sorted(set(map(str, g.predicates()))):
print(p) # データが実際に使っている述語の一覧ここに出てくる述語が `…example.com/hk/ontology#owns` なら一致、`…example.org/…#hasOwner` のように違っていれば、それがズレの正体です。CQが探している名前(`owns`・`name`・`model`・`repairTarget`・`SN-88613`・`HK-200` …)と、この一覧を突き合わせてください。
直し方は二つあります。
まず仕組みを動かして確かめたい人は、記事末尾の付録の `ontology.ttl` / `data.ttl` をそのまま保存して検証にかけてください。付録はCQと名前が一致するよう検証済みなので、必ず `スキーマ73 / データ40・全チェック通過` が出ます。環境とスクリプトが正しいことを、ここで一度確定させるのがおすすめです。
自作のttlを検証したい人は、CQ側をあなたの名前に合わせます。validate_min.py 冒頭の PREFIX hk: … /`PREFIX hko: … を自分のttlの @prefix と同じIRIに書き換え、CQ = { … } の中の owns・name・SN-88613 などを、先ほどの一覧に出てきた実際の語へ置き換える。これだけで、あなたのオントロジーがあなたの問いに答えられるようになります。
最後に一つ。この「0件」は落胆する結果ではなく、この回の主張がそのまま起きた瞬間でもあります。LLMの出力は揺れる。だからこそ、答えを機械が数字で突きつける受け入れテストを挟む。0件という赤信号は、「あなたのグラフと、あなたの問いが、まだ同じ言葉でつながっていない」ことを確実に教えてくれました。ズレを一つ直すたびに、緑が一つ増えていく――その手応えこそ、今日いちばん持ち帰ってほしいものだったのです。
