コードか?欲しけりゃくれてやるぜ…。探すな。ここに全てをおいてきた。
※ 実験が壮大になってしまって、文章をAIに書かせたら自分の文体じゃなくなってしまったのですが、修正も限界なので、かなりAIの文章が残っていますことをご承知おきください。
温泉爺が温泉トロジーを思いつくまで
お盆に、家族で蔵王温泉へ行ってきました。湯につかってぼーっとしていたら、跳ねた湯が目に入りました。
い、痛い。
蔵王温泉を調べると、こう書いてあります。
泉質は強酸性硫黄泉でありPH1.3
強酸性。なるほど、目に入れば痛いわけです。
㍋ー㍋ーとム◯カ大佐みたいなセリフが出ました。
さて、話は変わります。私は、隙あらば Ontology(+Knowledge Graph) と言う輩に辟易しています。Ontology は手段であって目的ではありません。なのに入れること自体を至高としている人がやたら多い。大事なのはエージェントの精度と、人間を温泉に入れることです。
思うに Ontology は哲学由来の言葉で、なんとなく賢そうに聞こえます。だから中身のないパープリンたちが、賢そうに振る舞う材料に使っているのではないでしょうか。
というわけで、温泉で Ontology を、つまり温泉トロジーを作って効能を試してみました。
1. tl;dr(それでも長いけど)
日本の温泉の法制度・分類体系を OWL/RDF で書き、SPARQL 推論と Amazon Bedrock の tool calling で「温泉爺(オンセンジー)エージェント」を作りました。対戦相手は、同じ一次情報を生テキストのまま引くだけの素の RAG(BM25) です。違いは知識を Ontology にしたか、テキストのまま置いたかだけです。
結果は…
- Ontology は効きました。 素の LLM の 34.3%/44.1%(Haiku/Sonnet) が 71.6%/92.2% へ。素の RAG(67.6%/87.3%)にも Haiku +4.0pt、Sonnet +4.9pt で勝ちました
- でも、Ontology は整理手法の1つに過ぎず、 同じ内容を同じように整理した生テキスト(推論結果なしの Markdown 29文書・2.3万字)を、素の RAG とまったく同じツール・同じプロンプトで引かせたら、Ontology を超えました(Haiku 88.2%、Sonnet 95.1%。全条件の最高)
- つまり効いていたのは Ontology という形式ではなく、「全対象について同じ項目を同じ順序で並べ、非該当と未公表まで書き切り、母集合を明示する」ところまで網羅されたデータの整理でした。TTL でも Markdown でもあまり変わりません
Ontology / Knowledge Graph は、その整理を置くための強力な手法の1つではあるが、唯一の正義ではないしコンテキストが落ちて負けることもある、という話です。
補足も3つ。素の RAG が Sonnet で 87.3% まで来ているので、上限に近い問では差が出ません。Opus のような賢いモデルなら両方が天井に張り付いて、差はもっと見えなくなると予想しています。素の RAG はコストの都合で BM25 にしましたが、ベクトルも併用すればもう少し伸びるはずです。そしてハルシネーション抑止には、整理されたデータがはっきり効きました(後述)。
どう測ったか
一次情報(掲示基準・鉱泉分析法指針の条文と、各施設の公表値)から「この表記があってほしい/あってはならない」という条件を書き起こし、回答に機械的に当てます。 条件には必ず根拠の条文・公表値を持たせました。
同じ12問を4条件×3回×2モデルで 288サンプル。あとから条件 F・G・H を足して216サンプル。合計504サンプルです。
| 条件 | 内容 | ツール | Haiku 4.5 | Sonnet 4.6 |
|---|---|---|---|---|
| A | 素の LLM(人格プロンプトのみ) | なし | 34.3%(35/102) | 44.1%(45/102) |
| B | Ontology のツールあり | 10個 | 71.6%(73/102) | 92.2%(94/102) |
| C | B + 検算を差し戻して書き直し | 10個 | 73.5%(75/102) | 88.2%(90/102) |
| D | 同じ一次情報の生テキストを BM25 検索 | 2個 | 67.6%(69/102) | 87.3%(89/102) |
| F | 同じ内容を揃えた生ドキュメントにして BM25 検索 | 2個 | 88.2%(90/102) | 95.1%(97/102) |
| G | B のツール表面を直した版(追試) | 11個 | 86.3%(88/102) | 91.2%(93/102) |
| H | F + 計算ツール(追試) | 5個 | 81.4%(83/102) | 93.1%(95/102) |
分母の102は問数ではありません。採点条件の数です。 12問に1〜4個の条件をぶら下げて計34条件、それを3回で 34×3=102。たとえば高血圧の相談なら「42℃以上の高温浴に触れる」「掲示基準に基づくと明示する」「医学的な断定をしない」の3つ。1問1点ではないので、条件4つの問(源泉ごとの pH)は4倍効きます。
Sonnet の C が B より低いのは、差し戻しのせいではありません。 C は「指摘が出たら書き直させる」条件で、指摘が出なければ B と完全に同一設定です。Sonnet では36サンプル中、書き直しが2回しか起動していません。回ごとに見ると B が 31・30・33、C が 29・31・30。重なっています(標準偏差は約3検査)。4検査の差は揺れです。 詳しくは発見4 で。
おもろいところ
生テキストが答えられたのは「書かれていることを取り出す」問で、 逆に落としたのは「文書にはあるのに、無いと言わねばならない」質問です。加水も加温も循環ろ過も消毒もしていない施設を挙げる問い(閉世界の否定)が 9/9 対 6/9、玉川温泉は放射能泉かという問い(ラドン濃度が未公表)が Haiku で 6/9 対 2/9 です。
検索が失敗したわけではありません。 ちゃんと施設ページを引き当てて「自噴源泉、100%かけ流し(加水加温なし)」まで取り出しています。でも消毒の記載が無い。だから「4つすべて非実施」とは言えない。引けたのに、答えとして成立しないのです。
ちなみに「秋保温泉の pH」のようにそもそも文書に無い場合は平気でした。検索が空振りするので気づけるからです(RAG でも 6/6 で「入っておらん」)。
そしてこの穴、文書を揃えたら閉じました。 条件 F では閉世界の否定が 9/9・8/9、未公表の申告が 5/9・8/9(D は 6/9・6/9 と 2/9・5/9)。効かなかったのは読解力でもトリプル不足でもなく、記述が揃っていなかっただけ。しかも F に推論結果は書いていません。5類型の実施の有無が同じ形で並んでいれば、数え上げは LLM がやります。
ハイブリッド検索(BM25+ベクトル)にすれば D はもう少し伸びるでしょう(未検証)。ただし伸びるのは「見つける」精度です。F が示したのは、伸ばすべきは文書の書き方だということでした。
そして白状します。B の負けは私のツール設計のミスでした。 源泉名で引くツールを作っていなかったので、値はグラフにあるのに引けない問がありました(Haiku の Q2 は 3/12)。穴を埋めて測り直したら(条件G)、Haiku は 71.6% → 86.3%。F(88.2%)とほぼ並びます。Sonnet は 92.2% → 91.2% で変化なし(賢いモデルは施設を総当たりして自力で回避していました)。Ontology が弱かったのではなく、引ける形にしていなかっただけです。
ハルシネーション抑止は、整理の勝ち
こちらはもっとはっきり出ました。ただし「数値の捏造を止める」だけなら生テキストでも十分です(コーパスに出典があるので、裏の取れない数値はほとんど出ません)。Web を丸ごと取り込んだ D で止まらなかったのは、次の2つ。
- もっともらしい嘘。 Haiku の RAG は「そんな湯はほぼ存在せんのが実情じゃのう」と一般論に逃げ、実在する2施設を落としました。「ラジウム含有」の記述を見つけて「玉川温泉は放射能泉である」と断定もしています。無いことを無いと言うには、無いと言える足場が必要です。
- 出典はあるが法定ではない記述。 「美人の湯」「うちみ」「慢性婦人病」は施設の公式ページに現に載っています。だからツール戻り値との照合では通ってしまう。何が法定の記述かを判定する表は、グラフ側にしかありません。
検算の指摘は D が Haiku 55件/Sonnet 44件、B が 29件/11件、F は 13件/17件。F では現行の掲示基準に無い効能表記が 12件 → 0件、条文の言い換えが 9件 → 0件。汚染された文書を掃除すれば、生成の側でも止まるわけです。
で、結論はどうなったか
効いていたのは「整理されたデータ」でした。 Ontology を書く過程でやっていた仕事(一次情報を読む、施設ごとに同じ項目を並べる、未公表を未公表と書く、母集合を決める)がそのまま効いていて、置き場所は成績に関係なかった。Ontology / Knowledge Graph は整理手法の強力な1つですが、唯一ではありません。
F が B を上回った幅の大半も、さっき白状したツール設計のミスです。直して測り直すと差は 16.7pt → 1.9pt。「どちらが上か」より「同じ整理をどこに置くか」の違いと見るのが実態に近いです。
おまけに、揃えた文書へ計算ツール(巡浴プランの生成・検証、限界飲用量の算出)を足しても伸びませんでした(条件H。Haiku 81.4%、Sonnet 93.1%)。ツールは呼ばれていて「呼ぶべきツールを呼んでいない」という指摘は 6件 → 2件に減ったのに、事実チェックは動かない。道具を増やせば伸びる、という話でもなかったわけです。
目安はこうです。答えたい問いが (a) 非実施・未公表・該当なしを言う必要がある、(b) 出典の質(法定かどうか)を区別する必要がある。このどちらかに当たるなら、そのための記述を揃える価値があります。当たらないなら、一次情報を集めて検索させるだけで足ります。まずは「揃える」。RDF にするかはその次です。
もうひとつ思うことがあります。Web のデータは人間が読むように書かれているので、AI にとっては無駄なコンテキストが多い。AI が読む用に簡素化したドキュメントを用意すれば、さらに伸びるはずです。 思想強めですが、本当に必要なのは Ontology ではなく(Ontology でもいいのですが)無駄のない完璧なドキュメントなのでは、と思っています。条件 F は、その予想がだいたい当たっていることを示しました。
ただし実測は、その「完璧」に条件を1つ足しました。無駄がないだけでは足りません。記述が現行の基準に揃っていることが要ります。 Web から丸ごと取ったコーパスには、平成26年改訂前の効能表記(「うちみ」「慢性婦人病」)や通俗表現(「美人の湯」)が混ざっていて、素の RAG はそれを出典つきで復唱しました。
そして「どれが現行でどれがそうでないか」の線引きは、Ontology を書く作業とほとんど同じです。実際、条件 F の文書は Ontology から生成しました。2分で作れたのは、TTL が先にあったから。形式は RDF でなくてかまいませんが、誰かがどこかでその判断をしないと、完璧なドキュメントは手に入りません。
と、ここまで too long な tl;dr でした。以下 AI をしばいて試した実験詳細です。
2. 温泉で Ontology を組んだ狙い
なぜ温泉なのか
正直に言えば、冒頭の言葉遊びをやりたかっただけです。でも触ってみたら、温泉は題材としてかなり優秀でした。
分類体系が、法令と行政指針で定義済みなのです。
| 何が | どこで決まっているか |
|---|---|
| 「温泉」とは何か | 温泉法(昭和23年法律第125号)第2条・別表 |
| 泉質名の付け方 | 鉱泉分析法指針(平成26年改訂) |
| 泉温・液性・浸透圧の区分 | 同 1-2 |
| 適応症・禁忌症 | 掲示基準(環自総発第1407012号) |
| 施設が掲示すべき9項目 | 温泉法施行規則第10条第1項 |
| 湯使いの表示(加水・加温など) | 同 第10条第2項 |
| 飲泉の許容量 | 温泉利用基準(飲用利用基準) |
「酸性泉」は雰囲気の言葉ではありません。水素イオンが1mg/kg以上ある療養泉、という定義があります。適応症も「なんとなく効く」ではなく、掲示基準が泉質ごとに列挙しています。施設のサイトの効能書きが現行基準か改訂前かを言い当てられるのも、現行の項目名が公表されているからです。
この「正解が公開されている」性質が、今回どうしても必要でした。 LLM の答えが合っているかを、感想ではなく条文と公表値で判定できます。趣味のドメインで、ここまで採点しやすい題材はなかなかありません。
あと、作業分担も白状しておきます。 一次情報の読み込み、TTL の記述、実装、実験ハーネス、この記事の草稿は、全部 Kiro CLI(Claude)に書かせました。私がやったのは方針決定、未決事項の判断、出力のレビュー、そして記事の手直しです。つまり以下は「AI に数日かけて構造化させたものが、丸ごと取ってきた生テキストにどこまで勝てるか」という話として読んでください。
一次情報も AI に読ませています。鉱泉分析法指針は全171頁、掲示基準は通知本文。ここでの縛りは1つ、値は公式サイトの公表値だけを転記させ、架空値は作らせない(Web 検索で拾った二次情報も根拠にしない)。法令側31件、施設側41件の出典を docs/ に一覧させたので、どの記述がどの URL 由来かは後から追えます。採点の根拠になる条文と公表値だけは、私も突き合わせました。
狙い
狙いは「LLM に温泉を教える」ことではありません。逆です。
LLM に温泉知識を持たせない。 泉質・pH・適応症・湯使いはすべて Ontology から取らせて、LLM の仕事を3つに絞ります。
- 曖昧な相談(「疲れが取れん」)から検索キーワードを抜く
- どのツールをどの順で呼ぶか決める
- 返ってきた構造化データを温泉爺の口調で語る
こうしておけば、ハルシネーションの余地がありません。数値を盛ったら、ツールの戻り値と突き合わせるだけでバレます。
何を検証したかったのか
出発点は序文の不信です。ただし作っている最中から、同じ疑いを自分にも向けていました。
最初の疑いはこれ。「これ、プロンプトに『適当なことを言うな』と書けば済むのでは?」 規則だけ与えてデータを与えない条件(A+)で測りました。答えは「半分は済む」。口調・免責・「治る」と言わない規律はプロンプトで届き、Haiku 54.4%/Sonnet 61.4% まで上がります。でも公表値には届かない。湯畑源泉の pH は両モデルとも 0/6 でした。
ところが、この比較では自分の疑いに答えきれていません。 「素の LLM より効く」「プロンプトだけより効く」は、要は「知識を与えたほうがよい」と言っているだけ。当たり前です。Ontology は法令の読み込みと実データ調査でそれなりの時間を食っています(TTL 1,680トリプル、出典72件)。それに見合う差かを言うには、同じ情報を安く置いた場合と比べないといけません。
実務で比べるべき相手は「何もしない」ではありません。「同じ一次情報を、文書のまま置いて検索させる」です。というわけで、問いを立て直しました。
同じ一次情報をトリプルに整理する価値はあるのか。生テキストのままで足りるのか。
これが今回の主眼です(A+ は役目を終えたので既定から外しました)。立て直したおかげで、自分の作ったものが負けうる実験になりました。実際、負けます。
3. Ontologyの構成
TTL を4ファイルに分けた
ontology/
├── onsen_ontology.ttl スキーマ(TBox): クラス、プロパティ、分類区分の個体
├── onsen_knowledge.ttl 法定知識(規範): 掲示用泉質10種、適応症・禁忌症、利用プロトコル
├── onsen_instances.ttl 実データ(ABox): 15温泉地・27源泉・15施設・9浴槽
└── onsen_heuristics.ttl 独自ヒューリスティック: 口語表現、条文表記の言い換え、相談の意図
分ける基準は出典と更新サイクルです。法定知識の出典は環境省の通知(改訂は数年に一度)。実データの出典は各施設の公式サイト(いつ変わるか分からない)。そしてヒューリスティックには出典がありません。「酸性泉のあとは仕上げ湯」は法令ではなく、四万温泉 積善館の「草津の仕上げ湯」という記述から作った経験則です。
出典の無いものを、出典のあるものと同じファイルに置かない。 この線引きはテストで守っています。人が忘れる類の約束は、テストにしておくのが吉です。
実データで壊れた設計
素朴に設計して、実データを入れた瞬間に壊れた箇所があります。効果検証の結果に直結するので4つだけ書きます。
施設と源泉を分けました。 草津は源泉ごとに pH が違います(湯畑2.08/万代1.7/煮川2.1)。1施設が複数の源泉を使い、1源泉が複数施設に配湯される多対多。つまり**「草津温泉の pH」という値は存在しません**。これが実験の Q1・Q2 になりました。
isKakenagashi を boolean にしませんでした。 「源泉かけ流し」は法令上の定義がない自主表示です。有馬温泉 金の湯は「放流式(かけ流し)」を掲示しながら、加水・加温・塩素消毒をすべて実施しています。法定5類型を中間ノードに reify し(「その旨及びその理由」が掲示義務)、自称ラベル claimsKakenagashi と推論値 isUnmodifiedSupply を分けました。これが Q3 と Q9 になりました。
oid:fac-arima-kinnoyu a onsen:Facility ;
rdfs:label "有馬本温泉 金の湯"@ja ;
onsen:claimsKakenagashi true ; # 自主表示(法令上の定義なし)
onsen:declaresTreatment
[ a onsen:TreatmentDeclaration ;
onsen:treatmentType onsen:AddingWater ; # 加水
onsen:isApplied true ;
onsen:reason "水道水・井戸水を加える" ] ,
[ a onsen:TreatmentDeclaration ;
onsen:treatmentType onsen:Recirculation ; # 循環(ろ過)
onsen:isApplied false ;
onsen:reason "放流式(かけ流し)のため循環させていない" ] ,
[ a onsen:TreatmentDeclaration ;
onsen:treatmentType onsen:Disinfection ; # 消毒
onsen:isApplied true ;
onsen:reason "塩素系薬剤による消毒。浴槽は毎日完全に抜き取り清掃" ] .
欠損は、欠損のまま持ちました。 pH が取れたのは 14/27 源泉、源泉温度は 16/27。埋めたくなりますが埋めません。代わりに「なぜ欠けているか」を onsen:dataStatus に書きます。箱根湯本と秋保温泉は源泉データを公表しておらず、現地掲示を見に行くしかないと判明したので収録していません。これが Q10 になりました。
判定の根拠を型で分けました。 成分値(指針 第1-3表)から判定したものは onsen:hasQuality。掲示泉質名の表記から読み取ったものは下位プロパティ onsen:hasQualityFromName。ちなみに成分値から判定できたのは大涌谷温泉の1源泉だけで、残り26源泉は名前からの読み取りです。実データはだいたいこんな感じです。
oid:src-tamagawa a onsen:SpringSource ;
rdfs:label "玉川温泉 源泉(大噴)"@ja ;
onsen:displayedQualityName "酸性・含二酸化炭素・鉄(Ⅱ)-塩化物温泉" ;
onsen:hasQualityFromName onsen:AcidicSpring , onsen:CarbonDioxideSpring ,
onsen:IronSpring , onsen:ChlorideSpring ;
onsen:pH 1.2 ;
onsen:dataStatus """(略)成分値(水素イオン濃度 mg/kg 等)は依然として未公表であり、
pH1.2 という液性の値は泉質の判定には使っていない。判定基準は pH ではなく水素イオン 1mg/kg 以上。
ラドン濃度は未公表で掲示泉質名にも現れないため、放射能泉に該当するかは分からない(分類しない)。""" .
pH1.2 は「酸性泉」の根拠になりません。 液性区分(pH<3 で酸性)と泉質(酸性泉=H⁺ 1mg/kg以上)は別物です。そして名称に出てこないラドン濃度は未公表なので、放射能泉かどうかは分からないままにしてあります。これが Q11 になりました。
推論
$ uv run onsen stats
{
"推論前トリプル数": 1680,
"推論後トリプル数": 4176,
"追加トリプル数": {
"R1: pH から液性区分を導出": 13,
"R2: 源泉温度から泉温区分を導出": 12,
"R3: 溶存物質から浸透圧区分を導出": 1,
"R4: 成分値から掲示用泉質を判定(下限値型)": 0,
"R5: 単純温泉かつ現地pH8.5以上 → アルカリ性単純温泉": 1,
"R6: 法定4類型すべて非実施 → 無加工供給": 2,
"R7: 浴用の泉質別禁忌症を持つ泉質 → 皮膚刺激が強い": 2,
"OWL 2 RL 演繹閉包": 2465
}
}
役割分担はこうなっています。
-
OWL 2 RL(owlrl) — プロパティチェーン公理(施設→源泉→泉質→適応症)、
owl:inverseOf、owl:SymmetricProperty、クラス階層、プロパティ階層(hasQualityFromName→hasQuality) - SPARQL ルール — 数値比較、閉世界の否定、算術。OWL では書けないもの
閾値を Python に書きません。 pH の境界はグラフ側の宣言(onsen:minPH)から読みます。
INSERT { ?source onsen:liquidityClass ?class }
WHERE {
?class a onsen:LiquidityClass .
OPTIONAL { ?class onsen:minPH ?min }
OPTIONAL { ?class onsen:maxPH ?max }
?source onsen:pH ?ph .
FILTER ( (!bound(?min) || ?ph >= ?min) && (!bound(?max) || ?ph < ?max) )
}
R7 は「刺激が強い」を人に書かせないためのルールです。浴用の泉質別禁忌症を持つ泉質は、10種のうち酸性泉と硫黄泉の2つだけ。 その禁忌症の筆頭が「皮膚又は粘膜の過敏な人」。……つまり冒頭で私の目が痛かったのは、掲示基準が禁忌症を定めている側の湯だったからです。刺激の強さを手で書き込まず、禁忌症の有無から導いています(onsen:isSkinIrritating)。
なお pH1.25〜1.6 という値から、蔵王温泉を「酸性泉」と呼ぶことはしません。判定基準は pH ではなく水素イオン1mg/kg以上で、その値が公表されていないからです。後述の Q11(玉川温泉)とまったく同じ話で、自分が入ってきた湯にも同じ規律が返ってきました。
R4 の発火数が 0。バグではありません。実データで成分値を公開している施設がほぼ無いからです。ルール自体の正しさは合成データのテストで担保しています。「実データでは発火しないが、ルールは正しい」と胸を張れるのは、テストがあるからですね。
ハマりどころが2つ。owlrl の expand() は1回では不動点に達しません(増えなくなるまで回す)。そして順番は SPARQL → OWL。逆にすると、SPARQL が足したトリプルに OWL 推論が未適用のまま残り、全体が冪等になりません。冪等じゃない推論はキャッシュと相性最悪です。
この順序には副作用があります。SPARQL ルールは推論器の導出結果に依存できないので、階層は自分でたどる必要があります。
# R5。実データの泉質は下位プロパティで主張されているので、プロパティ階層を自分でたどる
?p rdfs:subPropertyOf* onsen:hasQuality .
?source ?p onsen:SimpleSpring ;
onsen:pH ?ph .
FILTER ( ?ph >= 8.5 )
4. エージェントの構成
エージェントフレームワークは使っていません。 LangChain も LlamaIndex も無し。Bedrock Converse API の tool calling ループを直接書きました(依存は boto3 / rdflib / owlrl の3つだけ)。ツール10個ならフレームワーク無しで素に書けますし、なによりプロンプトと会話履歴を自分で持つほうが検算しやすいのです。後述する検算は「モデルが何を言ったか」と「ツールが何を返したか」の突き合わせなので、呼び出しの記録(ツール名・入力・出力・ターン番号・出所)を自分の形で貯めたかった、というのが本音です。
ユーザー「肌がガサガサなんじゃが」
↓
Bedrock Converse API(既定は jp.anthropic.claude-sonnet-4-6。計測は Haiku 4.5 と Sonnet 4.6 の2つ)
├── system: 温泉爺の人格 + 絶対に守る7規則
└── toolConfig: Ontology検索ツール10個(比較条件Dでは文書検索ツール2個に差し替える)
↓ toolUse
queries.py / itinerary.py → SPARQL → 推論済みグラフ(4,176トリプル)
↓ toolResult(JSON)
回答テキスト
↓
verify.py: 回答をツール戻り値と照合して検算
↓ 指摘があれば
温泉爺自身に差し戻して書き直させる
ツールは「問いの単位」で切る
SPARQL を叩く薄い関数を10個並べるのではなく、相談として成立する問いの単位で切りました。
| ツール | 何に答えるか |
|---|---|
list_facilities / describe_facility
|
施設の一覧・詳細(源泉・pH・湯使いと理由・出典) |
describe_spring_quality |
掲示用泉質の判定基準・適応症・禁忌症 |
search_by_symptom |
口語の症状(「肌がガサガサ」)から施設を探す |
get_general_indications |
療養泉の一般的適応症・温泉の一般的禁忌症 |
get_bathing_protocol / get_drinking_protocol
|
法定の入浴・飲用プロトコル |
evaluate_drinking_contraindications |
含有成分別禁忌症の限界飲用量を計算 |
plan_itinerary / validate_itinerary
|
巡浴プランの生成と、法定プロトコルによる検証 |
口語表現の対応表(「肌がガサガサ」→「皮膚乾燥症」)は LLM に丸投げせず、グラフに onsen:colloquialExpression として宣言しました。対応の根拠をレビューできますし、テストで固定できます。
プロンプトは守られる保証がない
システムプロンプトには7つの規則を書いてあります。「ツールが返した値だけを使う」「適応症に触れるなら掲示基準に基づくことを明示する」「経験則を語るなら前置きする」などです。
ですが、ご存じのとおりプロンプトは守られる保証がありません。 そこで、守られたかを機械的に検査する verify.py を用意しました。
| kind | severity | 内容 |
|---|---|---|
unsourced_quantity |
error | 単位付き数値が、ツール戻り値にも相談文にも無い |
unsourced_term |
error | Ontologyの語彙を、ツールが返していないのに使った |
folk_expression |
warning | 「美人の湯」「デトックス」など法定の記述に無い通俗表現 |
missing_disclosure |
warning | 適応症に触れて掲示基準に言及しない、経験則を前置きなしで語る |
missing_tool_call |
error / warning | 巡浴を頼まれて plan_itinerary を呼ばない |
paraphrased_term |
warning | 条文表記を要約した言い換え(「重い心臓病」←「少し動くと息苦しくなるような重い心臓又は肺の病気」) |
nonstatutory_indication |
error | 現行の掲示基準の適応症一覧に無い効能表記(「うちみ」「慢性婦人病」「虚弱児童」) |
検算が使う表はすべてグラフ側にあります。語彙は rdfs:label、口語表現は onsen:colloquialExpression、条文の言い換えは onsen:nonStandardParaphrase、現行の掲示基準に無い効能表記は onsen:NonStatutoryIndication の個体、相談の意図と呼ぶべきツールの対応は onsen:ConsultIntent の個体です。閾値・語彙・対応表を Python に書かないという原則は、検算にも同じく効いています。
設計上の判断が4つあります。
-
単位を要求します。 単位のない数字を拾うと掲示基準の条番号(
2.(2)① オ)が誤検出されてしまいます。 - 相談文の数値・語彙は許容しますが、通俗表現は許容しません。 利用者が言った症状を復唱するのは当然ですが、利用者が「デトックス」と言ったことは温泉爺がその語を使ってよい根拠にはなりません。
- Ontology の戻り値に出典がある通俗表現は通します。 「保温」は掲示基準の入浴後の注意に、「保湿・美肌効果」は四万温泉 積善館の公式記述に実在します。
- 出典の「ある/なし」と「質」を分けます。 数値と語彙はどのツールの戻り値でも出典として認めますが、通俗表現・条文の言い換え・非法定の効能表記はグラフ(法定知識)を基準に判定します。この分岐は「5. 結果」で生テキスト検索を条件に加えたときに必要になりました。施設ページの本文が戻り値になると、照合だけでは「美人の湯」も「虚弱児童」も通ってしまうからです。
たとえば蔵王温泉の施設サイトにある「美人の湯」「美肌」、観光協会の「美人づくりの湯」「ムコ多糖タンパクを活性化させる」は、いずれもこの folk_expression が弾く側の表現です。施設が自分のサイトにどう書くかは自由ですが、このエージェントは同じ言い方をできません。
指摘は温泉爺自身に差し戻します。unsourced_term には裏取り用のツール呼び出しが付くので、削らせるのではなく「代わりにこのツールを呼んで裏を取れ」と促す形にしました。
比較相手: 素朴な RAG のエージェント
自分の作ったものだけ丁寧に磨いて比べたら、それは八百長です。というわけで対抗馬も同じ土台で組みました。一次情報も、人格(温泉爺)も、規則も同じ。違うのは知識をどう置くかだけです。
材料は、環境省の通知(鉱泉分析法指針・掲示基準・温泉利用基準・温泉法)と、15温泉地の施設・自治体・観光協会の公表ページです。docs/*.md に出典として記録した URL 群が、そのまま両者の材料になっています。
| Ontology のエージェント(条件B・C) | 素朴な RAG のエージェント(条件D) | |
|---|---|---|
| 一次情報 | 同じ(docs/*.md に記録した出典URL群) |
同じ(左と同一の URL を機械抽出して取得) |
| 知識の形 | OWL/RDF のトリプル(1,680 → 推論後 4,176) | 生テキスト 51文書・35.9万字(467チャンク) |
| 作り方 | 一次情報をAI に読ませて構造化した(数日) | 出典URLを丸ごと取得しただけ(取捨選択なし。2分) |
| 引き方 | SPARQL を叩く専用ツール10個 | BM25 のキーワード検索ツール2個 |
つまりこの比較で変えたのは「同じ一次情報を、構造化して持つか、文章のまま持つか」の1点だけです。情報源の量や質で差がついたわけではありません(URL 55件のうちコーパスに取り込めたのは51件で、取れなかった4件の内容は他の出典で代替されています。後述します)。URL の抽出はコードでやっているので、人も AI も「これは入れる/外す」と選んでいません。
ユーザー「肌がガサガサなんじゃが」
↓
Bedrock Converse API(同じモデル・同じ温度 0.4)
├── system: 温泉爺の人格 + 同じ7規則(参照先だけ「文書検索」に差し替え)
└── toolConfig: search_documents / fetch_document の2個
↓ toolUse(検索語はモデルが決める。何度でも引き直せる)
retrieval.py → BM25(文字bigram)→ 467チャンク(全チャンクに出典URL・取得日)
↓ toolResult(該当箇所の本文 + 出典URL + 取得日)
回答テキスト
↓
verify.py: Ontology 側と同じ検算にかける
コーパスの作り方は「5. 結果」で書きますが、構成として押さえておくのは3点です。
-
ベクトル検索ではありません。 BM25(キーワード検索)で、埋め込みもベクトル DB も使いません。決定的で再現でき、API キーも費用も要らないからです。日本語は形態素解析器を入れず文字bigramで索引し、英数字(
pH、2.08)は語として切り出しました。数値が索引から落ちると、肝心の問いが検索の失敗にすり替わってしまいます -
1回引いて渡すだけの RAG ではありません。 検索語はモデルが自分で決め、空振りすれば語を変えて何度でも引けます。長い PDF は
fetch_documentで全文を窓送りして読めます。取れ高を人が絞らないという意味では、素朴な RAG としてはむしろ強い側に振ってあります - 計算する道具は渡していません。 ツールは「検索する」「読む」の2つだけです。生テキストでは計算できないことが検証の対象なので、そこに手を貸したら実験になりません
規則の文言も揃えました。「ツールが返した値だけを使え」を「文書検索で取れた記述だけを使え」に置き換え、出典URLを挙げることも同じように求めています。片方だけ出典を語れない状態で比べると、条件の差が「出典を持てるかどうか」の差にすり替わってしまいます。
もう一つの比較相手: 揃えた生ドキュメント
実験を一度回したあとに、条件をもう1つ足しました。D が負けた原因が「情報が無いこと」ではなく「記述が揃っていないこと」だと分かったからです(後述の発見1)。じゃあ揃えたらどうなるの、を測ります。
uv run onsen corpus align で、グラフの内容を同じ書式の Markdown に書き出します(29文書・2.3万字)。ツールもプロンプトも条件 D と同一で、差し替えたのはコーパスだけです。
作り方には縛りを3つ置きました。
-
推論結果とヒューリスティックは書きません。
isUnmodifiedSupply(法定4類型すべて非実施 → 無加工供給)、isSkinIrritating(皮膚刺激が強い)、recommendedAfter(仕上げ湯)は入れません。書いたら「答えを文書に置いた」ことになり、比較になりません。テストで0件を固定しています - 5類型の実施の有無を、全施設について同じ順序・同じ形で並べます。 揃えるのはここだけです
- 未公表を未公表と書き、母集合も文書に書きます。 「ラドン濃度は未公表で掲示泉質名にも現れないため、放射能泉に該当するかは分からない」と書き、別の文書に「この15施設についてのみ記述している」と明記します
## 湯使い(温泉法施行規則第10条第2項の掲示事項)
- 加水: 実施していない。理由: 100%源泉かけ流しのため加水なし
- 加温: 実施していない。理由: 加温なし
- 循環(ろ過含む): 実施していない。理由: 循環(ろ過含む)なし
- 消毒処理: 実施していない。理由: 消毒処理なし
- 入浴剤添加: 実施していない。理由: 入浴剤の使用なし
この文書は Ontology から生成しています。 つまり「一次情報を読んで、項目を揃えて、未公表を未公表と書く」という作業を済ませたあとの産物です。2分で作れたのは TTL があったからで、作るコスト自体は Ontology と同じです。ここは結果を読むときの前提になります。
5. 結果
実験の設計
ここからが本題です。Ontology と生テキスト、同じ一次情報を違う形で置いて比べます。
同じ相談を4条件(A〜D)に投げて、同じ計器で採点します。人格(温泉爺)は全条件で同一。測りたいのは人格の差ではなく、知識の形の差だからです。
| 条件 | ツール | システムプロンプト | 検算 |
|---|---|---|---|
| A | なし | 人格のみ(規則なし) | 採点のみ |
| B | Ontology10個 | 人格+7規則 | 採点のみ |
| C | Ontology10個 | 人格+7規則 | 採点し、差し戻して1回書き直させる |
| D | 文書検索2個 | 人格+7規則(参照先だけ文書に差し替え) | 採点のみ |
| F | 文書検索2個(コーパスを揃えた生ドキュメントに差し替え) | D と同一 | 採点のみ |
| G | Ontology11個(B に源泉名で引くツールを追加) | B と同一 | 採点のみ |
| H | 文書検索2個+計算3個(F に計算ツールを追加) | F + 計算ツールの案内1段落 | 採点のみ |
条件 B・C・D・F・G・H の構成は「4. エージェントの構成」に書いたとおりで、違いはツールの中身とコーパスだけです(G と H はあとから足した追試で、既定では走りません)。以下は測り方の話に絞ります。
コーパスの作り方で、実験の公正さが決まる
条件 D のコーパスは、docs/*.md が出典として記録している URL の全体。「これは入れる/これは外す」を人にも AI にも決めさせません。選別できる仕組みにすると「都合の悪い情報源を外した」と言われますし、実際、選別したくなるのが人情です。というわけで URL の抽出はコードにやらせます。
uv run onsen corpus build # docs の出典URLを取得して Markdown 化する
55件のうち51件・35.9万字が取れました。取れなかった4件も理由つきで manifest.json に残してあります(下呂温泉の分析表 PDF は画像で本文なし、e-Gov の法令2件は JavaScript 描画、日本温泉協会の1ページは 404)。法令本文は環境省の PDF 側で取れているので、内容の欠落はありません。
チャンクは467個。全チャンクが出典URLと取得日を持っています。日本語の分割は形態素解析器なしの文字bigram、英数字は語として切り出し。pH や 2.08 が索引から落ちると、肝心の問いが「検索の失敗」にすり替わってしまうからです。
この定義だとコーパスは汚染されたまま入ります。それが狙いです。
| コーパスに現れる語 | 文書数 | 現行の掲示基準では |
|---|---|---|
| 美肌 | 8 | 適応症に無い |
| 美人の湯 | 1(草津 大滝乃湯) | 記述が無い |
| 慢性婦人病 | 8 | 適応症一覧に無い |
| うちみ | 8 | 条文表記は「打撲」 |
| やけど | 12 | 適応症に無い(「きりきず」まで) |
| 虚弱児童 | 4 | 適応症一覧に無い |
念のため言いますが、これらは怪しいまとめブログではありません。有馬温泉 金の湯・酸ヶ湯・ひょうたん温泉・草津 大滝乃湯といった、施設の公式ページです。生テキストを引くとはこういうことで、藁人形を立てる必要はありませんでした。
採点は2系統でやる
事実チェックは、一次情報から書き起こした期待/禁止条件です。expect は「この表記があってほしい」、forbid は「あってはならない」。そして根拠を必ず書かせます。根拠のない採点はしません。
条件は12問で合計34個(expect 23・forbid 11)。3回ずつ走らせるので分母は 34×3=102。1問1点ではなく条件単位にしたのは、「一次情報との対応が何か所取れたか」を見たいからです。
Check(
label="さぎり湯または御前湯を挙げる",
kind="expect",
pattern=r"(さぎり湯|御前湯)",
basis="R6: 法定4類型すべて非実施 → 無加工供給(さぎり湯・御前湯)",
),
Check(
label="現行の掲示基準に無い効能表記を適応症として挙げない",
kind="forbid",
pattern=r"(うちみ|くじき|慢性婦人病|慢性消化器病|やけど|虚弱児童)"
r"(?!.{0,60}(無い|ない|載っておらん|現行|改訂|掲示基準では))",
basis="現行の掲示基準の適応症一覧に無い(onsen:NonStatutoryIndication)",
),
検算は verify.py をそのまま流用しました。ただし条件 D を入れると、この計器の穴が露出します。
検算は「ツール戻り値に無い語を使っていないか」を見ます。生テキスト検索では取得した Web 本文が戻り値になるので、施設ページに載っている「美人の湯」も「虚弱児童」も出典ありとして通ってしまいます。
そこで判定の基準を分けました。数値と語彙はすべてのツール戻り値を出典として認めます(ここで生テキストを差別したら、条件の差が出典の有無にすり替わってしまいます)。一方で通俗表現・条文の言い換え・非法定の効能表記は、Ontology 由来の戻り値だけを根拠に判定します。Ontology は法定知識・実データ・独自ヒューリスティックをファイルと注記で分けているので「返ってきた語がどういう性質のものか」を言えますが、Web 本文にその区別はありません。
そこで7つ目の検出を足しました。nonstatutory_indication。現行の掲示基準の適応症一覧に無い効能表記を適応症として述べたら error です。表は onsen:NonStatutoryIndication の個体としてグラフに置きました。
oid:nsi-uchimi a onsen:NonStatutoryIndication ;
rdfs:label "うちみ"@ja ;
rdfs:comment "現行の条文表記は「打撲」(筋肉若しくは関節の慢性的な痛み又はこわばりの例示)。"@ja .
ここで気をつけたことが2つ。ひとつ、「旧基準にあった表記だ」とは言いません。旧基準の一次情報を確認できていないので、主張は「現行の一覧に無い」だけに留めます。ふたつ、やりすぎないこと。「五十肩」「打撲」「捻挫」「運動麻痺」「冷え性」「神経痛」「病後回復期」は現行の条文にそのまま出てきます。うっかり宣言に入れると、条文どおりに書いた回答を落としてしまいます。どちらもテストで固定しました。
def test_nonstatutory_indications_are_absent_from_the_legal_file() -> None:
"""宣言した語が onsen_knowledge.ttl に現れたら落ちる。"""
つまり「現行の掲示基準に無い」という主張自体を、法定知識のファイルに対して機械的に検査しています。人の記憶に頼る約束は、やはりテストにしておくのが安心です。
問は12問
問い合わせの型を4つに分け(照会・判定・計画・不明の申告)、そこにコーパスの汚染を突く問を1つ足しました。ポイントは、各問に「生テキストならどうなるか」の予想を先に書いたこと(コードの rag_forecast に残しています)。あとから都合よく解釈を変えないための予防線です。
| 問 | 型 | 相談 | 生テキストの予想 |
|---|---|---|---|
| Q1 | 照会 | 草津の湯畑源泉の pH はいくつじゃ | ○ ページに書いてある |
| Q2 | 照会 | 湯畑・万代・煮川で pH はどう違うんじゃ | △ 3ページに分かれている |
| Q3 | 照会 | 有馬の金の湯はかけ流しかい | △ 放流式と加水が別の行にある |
| Q4 | 判定 | 高血圧の薬を飲んどるんじゃが入ってよいか | ○ 条文が PDF にある |
| Q5 | 判定 | 玉川温泉の湯は飲めるかい。1日どれくらいまでじゃ | ○〜△ 条文と施設の希釈率が別文書 |
| Q6 | 判定 | 大滝乃湯はどんな症状に向くと掲示基準では言っておるか | × 施設ページの効能書きを引く |
| Q7 | 判定 | 酸性の強い湯のあと、仕上げにはどんな湯がええか | × 順序関係は書かれていない |
| Q8 | 計画 | 草津で3日、1日2回で回りたい。順番を組んでくれ | △ 条文はあるが並べ方は無い |
| Q9 | 計画 | 加水も加温も循環ろ過も消毒もしとらん湯はあるかい | × 閉世界の否定 |
| Q10 | 不明の申告 | 秋保温泉の pH を教えてくれ | × 隣接情報を引いて答える |
| Q11 | 不明の申告 | 玉川温泉は放射能泉なのかい | × ラドン未公表を未公表と言えるか |
| Q12 | 汚染 | 美肌とデトックスができる湯はどこじゃ | × コーパスが汚染されている |
同じ設定で3回、それを2モデルで。1回だけだと ±1 の差が揺れに埋もれてしまいます。
uv run onsen eval # 4条件 × 12問(48回の Bedrock 呼び出し)
uv run onsen eval --report eval-results/*.jsonl # 保存済みの結果を合算して集計
モデルは Claude Haiku 4.5 と Claude Sonnet 4.6 です。どちらで測ったかは結果の JSONL に model_id として記録され、集計にも出ます。 問セットも question_set として記録しています。8問時代の結果に12問の採点条件を当てると、同じ Q3 が別の相談を指すので数字が壊れてしまうからです。
白状しておくことが2つあります。
ひとつは、実測後に採点条件を2件直したことです。「仕上げ湯に刺激の強い泉質を勧めていない」の禁止条件が、「炭酸水素塩泉は酸性泉・硫黄泉の後の仕上げ湯として推奨される」という正しい説明を落としていました(「仕上げ」の近くに「酸性泉」があれば落とす作りだったのです)。勧める形だけを見るように直しました。もうひとつは「源泉ごとに違うと言う」という期待条件で、これは言い回しに依存しすぎていました(「源泉は個性が違う」「湯畑・煮川とは成分の顔が違う」を取りこぼします)。「温泉地単位の pH を単一の値として述べない」という禁止条件に置き換えました。どちらも再発をテストで固定し、保存済みの288サンプル全部を同じコードで採点し直しています(onsen eval --report は毎回そうします)。実験の途中で採点条件をいじるのは結果を都合よく動かしうる操作なので、「直したら全部に当て直す」経路をコードに用意しました。
もうひとつは、当初の設計メモに書いていた「計算が必要な問」を落としたことです。含有成分別の限界飲用量((閾値/成分量)×1000mL)は生テキストでは解けないと考えていたのですが、掲示基準の PDF に計算例の答えがそのまま印字されていました(よう化物イオン100mL・ナトリウムイオン400mL)。検索すれば読める問なので、識別力がありません。代わりに「施設→源泉→泉質→適応症の連鎖」(Q6)を入れました。「生テキストでは解けないはずだ」という予想を、コーパスを作ってから確かめておいてよかったです。
全体の結果
Claude Haiku 4.5
| 条件 | 事実チェック | 合格率 | 検算の指摘 | ツール呼び出し | 平均トークン | 平均レイテンシ | 平均文字数 |
|---|---|---|---|---|---|---|---|
| A | 35/102 | 34.3% | 80件 | 0 | 624 | 4.0秒 | 348 |
| B | 73/102 | 71.6% | 29件 | 127 | 19,060 | 7.3秒 | 473 |
| C | 75/102 | 73.5% | 6件 | 153 | 25,011 | 9.0秒 | 467 |
| D | 69/102 | 67.6% | 55件 | 96 | 28,350 | 8.9秒 | 694 |
| F | 90/102 | 88.2% | 13件 | 67 | 14,145 | 6.9秒 | 526 |
| G | 88/102 | 86.3% | 18件 | 117 | 18,449 | 6.9秒 | 467 |
| H | 83/102 | 81.4% | 16件 | 74 | 20,976 | 7.6秒 | 575 |
Claude Sonnet 4.6
| 条件 | 事実チェック | 合格率 | 検算の指摘 | ツール呼び出し | 平均トークン | 平均レイテンシ | 平均文字数 |
|---|---|---|---|---|---|---|---|
| A | 45/102 | 44.1% | 164件 | 0 | 823 | 12.7秒 | 555 |
| B | 94/102 | 92.2% | 11件 | 167 | 14,389 | 23.0秒 | 1,019 |
| C | 90/102 | 88.2% | 4件 | 174 | 15,761 | 23.0秒 | 964 |
| D | 89/102 | 87.3% | 44件 | 137 | 36,917 | 30.6秒 | 1,385 |
| F | 97/102 | 95.1% | 17件 | 127 | 22,602 | 25.0秒 | 1,220 |
| G | 93/102 | 91.2% | 16件 | 138 | 12,877 | 21.0秒 | 925 |
| H | 95/102 | 93.1% | 12件 | 130 | 25,747 | 25.7秒 | 1,231 |
検算の指摘の内訳を見ると、条件ごとに壊れ方が違うのが分かります。左が Haiku、右が Sonnet です。
| 条件 | 出典なしの数値 | 出典なしの語彙 | 通俗表現 | 条文の言い換え | 非法定の効能表記 | ツール未呼び出し |
|---|---|---|---|---|---|---|
| A | 21 / 49 | 38 / 79 | 15 / 19 | 0 / 2 | 0 / 8 | 6 / 6 |
| B | 9 / 3 | 12 / 0 | 8 / 8 | 0 / 0 | 0 / 0 | 0 / 0 |
| C | 0 / 2 | 0 / 0 | 6 / 2 | 0 / 0 | 0 / 0 | 0 / 0 |
| D | 8 / 4 | 2 / 5 | 18 / 20 | 9 / 8 | 12 / 1 | 6 / 6 |
| F | 1 / 1 | 0 / 2 | 6 / 6 | 0 / 2 | 0 / 0 | 6 / 6 |
Ontology 条件(B・C)は、通俗表現以外がほぼゼロ。 一方 D は、数値と語彙の裏取りは効いているのに(コーパスに出典があるので当然)、通俗表現・条文の言い換え・非法定の効能表記が残ります。そして素の LLM(A)は Sonnet で164件。うち非法定の効能表記が8件。賢いモデルは、旧い効能表記まで流暢に語ってしまうわけです。
問ごとの結果
3回分を合算した合格数です。左が Haiku、右が Sonnet です。
| 問 | 型 | A | B | C | D | F |
|---|---|---|---|---|---|---|
| Q1 | 照会: 源泉の公表値 | 0/3 / 0/3 | 1/3 / 3/3 | 1/3 / 3/3 | 3/3 / 3/3 | 3/3 / 3/3 |
| Q2 | 照会: 源泉ごとの粒度 | 3/12 / 7/12 | 3/12 / 12/12 | 3/12 / 12/12 | 12/12 / 12/12 | 12/12 / 12/12 |
| Q3 | 照会: 自主表示の検証 | 1/9 / 1/9 | 9/9 / 9/9 | 9/9 / 8/9 | 5/9 / 9/9 | 9/9 / 8/9 |
| Q4 | 判定: 法定の言い回し | 3/9 / 3/9 | 6/9 / 9/9 | 6/9 / 9/9 | 9/9 / 9/9 | 9/9 / 9/9 |
| Q5 | 判定: 数量と条文の結合 | 5/9 / 7/9 | 9/9 / 9/9 | 9/9 / 9/9 | 6/9 / 9/9 | 9/9 / 9/9 |
| Q6 | 判定: 施設→源泉→泉質→適応症 | 6/9 / 4/9 | 9/9 / 9/9 | 9/9 / 9/9 | 6/9 / 7/9 | 9/9 / 9/9 |
| Q7 | 判定: 泉質間の順序関係 | 8/9 / 6/9 | 7/9 / 8/9 | 6/9 / 6/9 | 7/9 / 9/9 | 7/9 / 9/9 |
| Q8 | 計画: 法定プロトコル | 1/9 / 2/9 | 6/9 / 8/9 | 6/9 / 9/9 | 7/9 / 9/9 | 6/9 / 9/9 |
| Q9 | 計画: 閉世界の否定 | 4/9 / 6/9 | 9/9 / 9/9 | 9/9 / 8/9 | 6/9 / 6/9 | 9/9 / 8/9 |
| Q10 | 不明の申告: 収録外 | 2/6 / 6/6 | 6/6 / 6/6 | 6/6 / 6/6 | 6/6 / 6/6 | 6/6 / 6/6 |
| Q11 | 不明の申告: 未公表の成分 | 2/9 / 3/9 | 6/9 / 6/9 | 5/9 / 7/9 | 2/9 / 5/9 | 5/9 / 8/9 |
| Q12 | 汚染: 通俗表現と旧表記 | 0/9 / 0/9 | 2/9 / 6/9 | 6/9 / 4/9 | 0/9 / 5/9 | 6/9 / 7/9 |
平均では並ぶのですが、問ごとに見ると分業がはっきりします。以下、面白かった5か所を書きます。
発見1: 「無い」と言うには、「無い」と書いた文書が必要だった
この実験でいちばんきれいに割れたのが Q9、「加水も加温も循環ろ過も消毒もしていない湯はあるか」です。B は両モデルで 9/9、D は 6/9(C は Haiku 9/9、Sonnet 8/9)。まずは両者の答えを見てください。
B(Ontology、Haiku):
まずはわしが登録されとる全施設を見てみようかい。……よし、15施設あるな。それでは各施設の湯使いを調べてみようかい。
ほう、良い質問じゃ。わしが調べた15施設の中で、加水も加温も循環ろ過も消毒もしとらん湯は、実は2つあるのう。
1. 登別温泉 さぎり湯 — 加水なし、加温なし、循環なし、消毒なし、入浴剤なし
2. 長湯温泉 御前湯(竹田市営) — 温泉に何も加えず源泉をそのまま利用している
list_facilities から15施設を総当たりして、16回のツール呼び出しで2件に絞り込んでいます。
D(生テキスト、Sonnet):
おお、ええ質問じゃ! 「加水・加温・循環ろ過・消毒、すべてなし」というのは、いわゆる**「源泉100%かけ流し」**と呼ばれる湯使いじゃ。わしの手元の文書から確認できたものをお伝えしよう。
別府・明礬温泉 ── 岡本屋旅館
「青磁にも喩えられる青みを帯びた乳白色の湯は、**自噴源泉、100%かけ流し(加水加温なし)**です。」
消毒の有無については施設ページに明示的な記載が見当たらんので、そこは現地の掲示を見んと分からんのう。
すごく誠実です。 施設の自称を引き、書いていないことは書いていないと言っている。ですが、問いには答えられていません。そして Haiku の D は、もっと踏み込んで外します。
実は「加水も加温も循環ろ過も消毒もしとらん」という湯は、ほぼ存在せんのが実情じゃのう。……あんたが探しとるような「完全無調整」の湯は、現代の衛生管理基準ではほぼ不可能に近いんじゃ。
一般論として正しそうなことを言って、実在する2施設を取りこぼしています。 検索で見つからないことを「存在しない」と読み替えてしまったわけです。
つまりドキュメントの問題だった
ここが肝心なのですが、情報が無かったから答えられなかったわけではありません。 さぎり湯の公式ページには、はっきりこう書いてあるのです。
当館では、新たに表示が義務付けられた 一、加水 二、加温 三、循環(ろ過含む)四、消毒処理(又は入浴剤使用)以上の四項目の表示について該当しておりません。
コーパスにもこの文はちゃんと入っています(sagiriyu-noboribetsu.com-daiyokujou.md)。それでも D は 6/9 でした。原因は文書側の3点だと思います。
- 「無い」の書き方が施設ごとに違います。 さぎり湯は「四項目の表示について該当しておりません」、長湯温泉 御前湯は「温泉には何も加えず、源泉をそのまま利用していただいている」、別府 岡本屋は「自噴源泉、100%かけ流し(加水加温なし)」。同じ事実が別の言い方で書かれていて、項目の粒度もそろっていません
- 4項目を同じ形で並べたページがほとんどありません。 掲示義務の表をそのまま載せている施設(有馬温泉 金の湯)もありますが、多くは散文に溶かして書いています。岡本屋のページには「消毒」という語が1回も出てきません
- 母集合を書いた文書がありません。 「この15施設で全部」と言える文書が無いので、「他にもあるかも」を排除できません
要するに、横断して数え上げるための足場がドキュメント側に無いのです。ちなみにチャンクを切らず全文を渡しても結果は同じ。足りないのは取れ高ではなく、記述の揃え方ですから。
Ontology 側が 9/9 だったのは、同じ情報を同じ述語・同じ値に揃えて15施設分持っているからです。
oid:fac-sagiriyu onsen:declaresTreatment
[ onsen:treatmentType onsen:AddingWater ; onsen:isApplied false ; onsen:reason "100%源泉かけ流しのため加水なし" ] ,
[ onsen:treatmentType onsen:Heating ; onsen:isApplied false ; onsen:reason "加温なし" ] ,
[ onsen:treatmentType onsen:Recirculation ; onsen:isApplied false ; onsen:reason "循環(ろ過含む)なし" ] ,
[ onsen:treatmentType onsen:Disinfection ; onsen:isApplied false ; onsen:reason "消毒処理なし" ] ,
[ onsen:treatmentType onsen:BathAdditive ; onsen:isApplied false ; onsen:reason "入浴剤の使用なし" ] .
「実施していない」が値として入っているのがポイントです。推論ルール R6 は「4類型すべてに isApplied false の宣言がある」ことを条件に isUnmodifiedSupply true を導きます。書いていないことを勝手に false と読んでいるのではなく、false と書いてあるから言えるわけです。やったことは「文書を同じ形に揃える」作業で、それを RDF でやったにすぎません。
Q11(玉川温泉は放射能泉か)も同じ構図です。放射能泉の判定基準はラドン濃度。そして玉川温泉はその値を公表していません。だから Ontology は堂々と「分からない」と言えます(掲示泉質名から酸性泉・二酸化炭素泉・含鉄泉・塩化物泉までは読み取りますが、ラドンは名称に出てこないので主張しません)。一方、D はこうなりました。
あんた、その通りじゃ。玉川温泉は放射能泉であり、かつ酸性泉でもあるんじゃ。……同時にラジウムを含有する放射能泉でもある。環境省の資料でも「日本一の強酸性の泉質(ラジウム含有)」と記されておって……
コーパスの中に「ラジウム含有」という表現が実際にあります(環境省の温泉地保護利用推進計画の資料です)。D はそれを見つけて「放射能泉である」と断定しました。書いてある語を拾う限り正しい仕事をしているのに、泉質の判定は「ラドン濃度が限界値以上か」でしかできないという判定規則の側を持っていないのです。Haiku の D は 2/9、Sonnet でも 5/9 でした(B は 6/9)。
こちらで足りていないのも、やはり文書です。「ラドン濃度は未公表である」と書いた文書がどこにもありません。 Ontology 側はそれを onsen:dataStatus に書き、判定基準(ラドン濃度で判定する)と一緒に持っています。「分からない」と答えられるのは、「分からない」と書いてあるからです。
発見2: 生テキストは、汚染をそのまま復唱する
Q12(美肌とデトックス)です。Haiku の D は 0/9、つまり9回すべて落ちました。
美肌の湯については、わしの文書に結構出ておるぞ。いくつか紹介しようかい。
愛媛県・伊予の湯治場 喜助の湯 「椿の湯」はアルカリ性の泉質でpH9を超えており、古い角質を洗い流してすべすべの肌へと導くと書かれておる。
群馬県・四万温泉 積善館 「草津の仕上げ湯・美肌の湯」として知られておる。
ところが、あんた。デトックスという言葉は、わしの文書には出てこんのじゃ。
律儀なんですよ、この子。 「デトックス」はコーパスに1件も無いので「出てこん」と正しく言い、「美肌」は8文書にあるのでわざわざ出典つきで紹介しています。ツール戻り値との照合という意味では、この回答は満点の「出典あり」です。
ですが掲示基準に「美肌」はありません。求められる答えは、酸性泉・硫黄泉の浴用適応症(アトピー性皮膚炎・尋常性乾癬・表皮化膿症)や塩化物泉の「皮膚乾燥症」への翻訳です。出典があることと、法定の記述であることは違います。 ここを見分けるために、通俗表現の判定だけはグラフ(法定知識)を基準にしています。
Q6(大滝乃湯はどんな症状に向くか)はもう少し込み入っていて、D が半分正解したのが面白かったところです。
大滝乃湯は煮川源泉という酸性硫黄泉(pH2.1)を使っておる。施設ページには「神経痛、関節痛、うちみ、ねんざ、やけど、慢性消化器病、病後回復期、美肌、慢性婦人病など」と書いてあるのう。
これを環境省の掲示基準に照らし合わせると、酸性泉の泉質別適応症は以下の通りじゃ:アトピー性皮膚炎/尋常性乾癬/耐糖能異常(糖尿病)/表皮化膿症
ただし、「やけど」「慢性婦人病」といった項目は、掲示基準では酸性泉の泉質別適応症には明記されていないのじゃ。
施設ページと掲示基準を突き合わせるという、いちばん筋のよい仕事をしています。 それでも事実チェックは落ちました。禁止条件が「うちみ」の直後60字以内に断りを求めるのに対し、この回答は引用してから20行後に「やけど・慢性婦人病は明記されていない」と書いたためです(「うちみ」については最後まで触れていません)。
採点器の限界として書いておきます。 引用と主張の区別は正規表現では完全に付きません。窓を広げれば「最後に一言断りを入れれば全部許される」ことになり、狭めればこういう取りこぼしが出ます。今回は窓を狭いままにしました(検出漏れより誤検出のほうが害が小さいと判断しました)。
発見3: 負けた原因は表現形式ではなく、私のツール設計だった
Q2(湯畑・万代・煮川の pH の違い)です。Haiku の B は 3/12、D は 12/12。 これは自分の設計の負けです。
値はグラフにあります。湯畑源泉 pH2.08、万代源泉 pH1.7、煮川源泉 pH2.1。なのに引けない。 用意したツールは施設名で引く describe_facility だけで、源泉名で引くツールを作っていなかったのです。だから describe_facility("湯畑") が空振りします。
ふむ、湯畑という施設名では引っかからんのう。草津の施設一覧を見てみるかい。……リストには湯畑という施設は登録されておらんのう。
Sonnet は同じツールで 12/12 に届きました。list_facilities で草津の施設を洗い出し、御座之湯・大滝乃湯・西の河原露天風呂を順に引いて源泉に降りています。ツールの切り方の粗さは、賢いモデルが吸収してくれるわけです。逆に言えば、弱いモデルを使うならツールの切り方がそのまま成績になります。
同じ穴が Q7(酸性泉のあとの仕上げ湯)にもありました。グラフには「単純温泉 recommendedAfter 酸性泉」という辺が張ってあるのに、describe_spring_quality("酸性泉") の戻り値では「仕上げ湯として推奨される先行泉質」が空で返ります。逆向きを引く経路を用意していないからです。D が Sonnet で 9/9(B は 8/9)だったのは、四万温泉 積善館の「草津の仕上げ湯」という記述をコーパスから見つけて、経験則だと断ったうえで答えたからでした。
酸性の強い湯に入った後の仕上げについては、古くからの湯治の慣行がある。**「草津の仕上げ湯」**という言い方があってな……ただし、掲示基準の浴用方法では、酸性泉そのものについて特別な仕上げ方法の記載は見当たらんのう。これはあくまで古くからの湯治の経験則じゃ。
整理しただけでは効きません。引ける形にして、はじめて効きます。 「トリプルにする価値」を測るつもりで、実際に測っていたのは「トリプルにしたうえで、問いの形に合うツールを切る価値」でした。自分の結論を弱める話ですが、隠さず書いておきます(そして後で直します。発見7 へ続く)。
発見4: 検算の差し戻しは、弱いモデルでしか起動しない
Haiku では C が B を上回りました(71.6% → 73.5%)。効いた場所は汚染の問です。Q12 は B 2/9 → C 6/9。
差し戻しの中身はこうです。検算が「folk_expression: 美肌 — 掲示基準の適応症に無い表現」を指摘し、温泉爺自身に返します。すると書き直しでツールを引き直し、掲示基準の症状名に置き換えます。書き直しは Haiku で10回発生しました。
一方 Sonnet では、差し戻しがほとんど起動しません。 36サンプルのうち書き直しが起きたのは2回だけです(Q12 と Q8 で各1回)。指摘そのものが少ないからで、B の検算指摘は11件しかありません。
ここで数字の読み方に注意が必要です。Sonnet の合格率は B 92.2% に対し C 88.2% で、C のほうが低くなっています。ですがこの差を差し戻しのせいにしてはいけません。 落ちた3問(Q3・Q7・Q9)は書き直しが一度も起きていないのです。
| 問 | Sonnet B | Sonnet C | C の書き直し |
|---|---|---|---|
| Q3 | 9/9 | 8/9 | 0回 |
| Q7 | 8/9 | 6/9 | 0回 |
| Q9 | 9/9 | 8/9 | 0回 |
| Q12 | 6/9 | 4/9 | 1回(そのサンプルは 2/3 で B と同じ) |
| Q8 | 8/9 | 9/9 | 1回 |
B と C は、検算の指摘が出なければ完全に同じ設定です。 同じプロンプト、同じツール、同じ温度0.4。指摘が出なかった回の C は B と同じくじを引き直しているだけで、そこで出た差はサンプリングの揺れです。Q7 を1回ずつ見ると B が 3/3・2/3・3/3、C が 2/3・2/3・2/3 で、落ちた条件は3回とも同じ「法令ではなく経験則だと前置きする」でした。書き直しは一度も起きていません。
回ごとの合計を並べると、揺れの大きさが見えます。
| run1 | run2 | run3 | 合計 | |
|---|---|---|---|---|
| Sonnet B | 31/34 | 30/34 | 33/34 | 94/102 |
| Sonnet C | 29/34 | 31/34 | 30/34 | 90/102 |
同じ B の中でも 30〜33 と3検査ぶれています。 run2 では C(31)が B(30)を上回ってもいます。分母102・合格率90%前後の二項分布なら標準偏差は約3.0検査なので、合計の差4検査は1.3標準偏差。偶然の範囲です。
つまり 3回×12問という規模では、問ごとの ±1 を読んではいけません。 読めるのは全体の傾向と、Q2 や Q12 のように一方向に振れきった場合だけです。
結論としては Phase 6 と同じになります。差し戻しは弱いモデルのための保険で、賢いモデルでは出番がありません。 検算そのものは(LLM を呼ばずグラフと突き合わせるだけなので)ほぼ無償なので、指摘を出す機能を切る理由はありません。差し戻して書き直させるかどうかだけが、モデルの強さで決める設定項目だと思います。
発見5: 素の LLM は、賢くなるほど裏の取れない話が増える
検算の指摘は Haiku A の80件に対し Sonnet A は164件でした。回答が長く(348字 → 555字)、説明が丁寧になり、そのぶん出典のない数値と語彙が増えています。
さらに、Sonnet A だけが非法定の効能表記を8件出しました(Haiku A は0件)。
この強酸性のおかげで殺菌力が非常に高く、皮膚疾患や慢性皮膚病に効果があると言われておる。
「慢性皮膚病」は現行の掲示基準の適応症一覧にありません(現行は「慢性湿疹」「アトピー性皮膚炎」などの個別の病名で書かれています)。旧い効能表記は、賢いモデルのほうが流暢に使うのです。 学習データに温泉施設の効能書きが大量に入っているのだから当然でもあります。流暢さは正しさではありません。
発見6: 整理してあるなら、置き場所は Markdown でもよかった
さて、ここまでの5つを踏まえて条件 F を足しました。Ontology と同じ内容を、推論結果は書かずに、同じ書式の Markdown へ並べただけの文書集です。ツールもプロンプトも D と同一。差し替えたのはコーパスだけ。
結果は全条件の最高で、Ontology 自身をも上回りました。
| Haiku 4.5 | Sonnet 4.6 | |
|---|---|---|
| B(Ontology) | 71.6% | 92.2% |
| D(Web を丸ごと取得した生テキスト) | 67.6% | 87.3% |
| F(揃えた生ドキュメント) | 88.2% | 95.1% |
| F − D | +20.6pt | +7.8pt |
| F − B | +16.7pt | +2.9pt |
D が落としていた問がほぼ埋まりました。閉世界の否定(Q9)は 6/9 → 9/9・8/9、未公表成分の申告(Q11)は 2/9・5/9 → 5/9・8/9(Sonnet は Ontology の 6/9 より上)、汚染の切り分け(Q12)は 0/9・5/9 → 6/9・7/9。
Q9 の回答を見ると、15施設分の文書を横断して数え上げています。
わしが見つけたのは、**北海道登別温泉の「さぎり湯」**じゃ。ここは100%源泉かけ流しで、加水・加温・循環ろ過・消毒処理のすべてを実施しておらん。……もう一つ、大分県長湯温泉の「御前湯」(竹田市営)も同じく4つすべてを実施しておらん。
別府温泉郷の「ひょうたん温泉」も加水と加温は実施しておらんが、循環と消毒については掲示を確認できておらんので、そこは現地の掲示を見んと分からんのう。
「実施していない」と「掲示を確認できていない」を区別して答えています。 これは条件 D では出てこなかった振る舞いです。しかも F の文書には isUnmodifiedSupply(推論で導いた無加工供給)を書いていません。5類型が全施設について同じ形で並んでいれば、数え上げは LLM がやってくれるわけです。推論エンジンの取り分も、ここでは出ませんでした。
Q11 も同じです。判定規則ごと説明してきました。
放射能泉かどうかについては、わしの手元の文書には明記されていないのう。 ラドン濃度が未公表で、掲示泉質名にも放射能泉の表記がないんじゃ。放射能泉と判定するには、ラドンが 8.25 マッヘ単位以上(環境省の鉱泉分析法指針で定められた基準)であることが必要なんだが、その値が公開されていないわけじゃ。
検算の指摘も激減しました。 D の 55件・44件、B の 29件・11件に対し、F は 13件・17件です。内訳では現行の掲示基準に無い効能表記が 12件 → 0件、条文の言い換えが 9件 → 0件。コーパスに旧表記も通俗表現も入っていないので、そもそも復唱できません。汚染された文書を掃除すれば、生成の側でも止まるということです。
コストも安くなりました。 トークンは D の 28,350・36,917 に対して F は 14,145・22,602 で半分前後、レイテンシも 8.9秒・30.6秒 → 6.9秒・25.0秒 です。文書が短く、必要な箇所に一発で当たるからです。
ただし、これを「Ontology は不要だった」と読むのは早いです。F の文書は Ontology から生成しています。 一次情報を読み、施設ごとに同じ項目を並べ、未公表を未公表と書き、母集合を決める。その作業を終えたあとの産物を書き出しただけ。2分で作れたのは、TTL が先にあったからです。Ontology は「整理をどこに置くか」の選択肢で、整理そのものの代わりにはなりません。
差の内訳も見ておく必要があります。F の勝ちの大半は、Ontology 側のツール設計の粗さでした。 実際にツール表面を直して測り直すと(次の発見7)、Haiku の Ontology は 71.6% → 86.3% まで上がり、F との差は 16.7pt → 1.9pt に縮みます。
残った差も2つあります。ひとつは計算するツールが無いことで、巡浴の相談に plan_itinerary を持たない F は missing_tool_call を D と同じく6件出しました。もうひとつは検算に使う表がグラフ側の資産だということです(onsen:NonStatutoryIndication などは文書に書いていません。書いたら検査の基準を被験者に渡すことになります)。
発見7: 引ける形にしたら並んだ。道具を足しても伸びなかった
宿題が2つ残っていました。B の負けは本当にツール表面のせいだったのか。 そして揃えた文書に足りないのは、計算だけなのか。 どちらも測れるので、測りました。
条件 G は B のツール表面を直した版です。源泉名で引く describe_spring_source を足し、describe_spring_quality が仕上げ湯の逆向き(この泉質のあとに勧められる泉質)も返すようにしました。変えたのはツールだけで、プロンプトもグラフも同じです。
条件 H は F に計算ツールを足した版です。巡浴プランの生成・検証(plan_itinerary / validate_itinerary)と、含有成分別の限界飲用量(evaluate_drinking_contraindications)を渡しました。変えたのはツールだけで、コーパスもプロンプトの規則も F と同じです(計算ツールの案内を1段落足しただけ)。
| B | G(B+ツール修正) | F | H(F+計算) | |
|---|---|---|---|---|
| Haiku 4.5 | 71.6% | 86.3% | 88.2% | 81.4% |
| Sonnet 4.6 | 92.2% | 91.2% | 95.1% | 93.1% |
答えの1つめ。B の負けは、ほぼツール表面が原因でした。 Haiku は +14.7pt 上がり、F との差は 16.7pt → 1.9pt に縮みました。問ごとに見ると、埋めた穴のところだけが埋まっています。
| 問 | Haiku B | Haiku G |
|---|---|---|
| Q1 湯畑源泉の pH | 1/3 | 3/3 |
| Q2 源泉ごとの粒度 | 3/12 | 12/12 |
| Q7 仕上げ湯(逆向きの辺) | 7/9 | 9/9 |
Ontology が弱かったのではなく、引ける形にしていなかっただけでした。値も辺もグラフにあったのに、describe_facility("湯畑") が空振りしていたのです。一方 Sonnet は 92.2% → 91.2% で変化なしでした。賢いモデルは元から施設を総当たりして回避していたので、ツールを足しても伸びません。ツールの切り方の粗さは、賢いモデルが吸収してしまうという発見3の裏返しがここでも出ました。
答えの2つめ。計算ツールを足しても上がりませんでした。 Haiku は 88.2% → 81.4%、Sonnet は 95.1% → 93.1% で、どちらも下がっています。
ただし下がった原因を「計算ツールのせい」と言うのは違います。 ツール自体はきちんと使われていて、巡浴の相談では実際に plan_itinerary が呼ばれ、「呼ぶべきツールを呼んでいない」という検算の指摘は 6件 → 2件に減りました。落ちたのは Q12(汚染。Haiku で 6/9 → 2/9)と Q8(巡浴。6/9 → 5/9)で、プロンプトに1段落足したことと、3回×12問というサンプル数の揺れが混ざっています(発見4 で見たとおり、この規模では問ごとの ±2 は読めません)。
言えるのは**「足しても伸びなかった」**の一点だけ。少なくとも、整理した文書に計算ツールを積めば最終形、という単純な話ではなかったわけです。
この2つを合わせると、こうなります。
整理された内容と、問いに合う引き方の2つが揃っていれば、置き場所(TTL / Markdown)も道具の数も、成績をほとんど動かしませんでした。
払った代償
| 指標 | Haiku B | Haiku D | Sonnet B | Sonnet D |
|---|---|---|---|---|
| 平均トークン | 19,060 | 28,350(1.5倍) | 14,389 | 36,917(2.6倍) |
| 平均レイテンシ | 7.3秒 | 8.9秒 | 23.0秒 | 30.6秒 |
| 平均文字数 | 473 | 694 | 1,019 | 1,385 |
生テキスト側のほうが高くつきます。 検索して読み、必要なら全文を取り直すので、会話履歴に積まれる文字数が増えます。回答も長くなります(引用するからです)。Ontology 側は戻り値が構造化されているぶん短くなります。
ただしこれは運用コストの話で、構築コストは逆です。 コーパスは onsen corpus build で2分で作れます。Ontology は法令を読んで設計して実データを集めて、と時間がかかっています。「同じ答えが出るなら文書を置いて検索させるほうが安い」という判断は、十分に成り立ちます。
もう1つの代償は、答えられる範囲が狭くなること。素の LLM は日本中どの温泉でも何かしら答えます。うちの温泉爺は15温泉地だけ。秋保温泉に「知らん」と答えるのは正しさとしては勝ちですが、体験としては完敗です。Ontology は知識を足す装置ではなく言えることを制限する装置なので、これは設計どおりではあります。
6. 総論
効いていたのは「整理されたデータ」だった
Ontology を作った意味はありました。ただし効いていたのは形式ではなく、整理そのものでした。
同じ一次情報を Web から丸ごと取得して検索させる条件(D)に対して、トリプルに整理した条件(B)の上積みは Haiku で +4.0ポイント、Sonnet で +4.9ポイントです。AI に書かせたとはいえ、1,680トリプルと出典72件を積み上げた数日に対して、この数字は小さいと言わざるを得ません。
そして、Ontology の中身を同じ書式の Markdown に書き出して同じ BM25 で引かせた条件(F)は、Ontology 自身と同等以上でした(Haiku 88.2%、Sonnet 95.1%)。さらにツール表面を直して Ontology 側を測り直すと(条件G)、Haiku は 86.3%、Sonnet は 91.2% で、F との差は 1.9pt と 3.9pt。「文書のほうが強い」ではなく「同じ整理なら置き場所を変えても成績は変わらない」というのが実態でした。
つまり効いていたのは、一次情報を読み、全対象について同じ項目を同じ順序で並べ、非該当と未公表を明示し、母集合を決め、汚染された表記を落とすという一連の作業でした。この作業の産物を TTL に置いたのが B で、Markdown に置いたのが F です。Ontology / Knowledge Graph は、その整理を置く強力な手法の1つであって、整理の代わりになるものではありませんでした。
トリプルでなければ届かないと思っていた3つは、揃えた文書でも届きました。
| 何を問うか | B(Ontology) | D(Web を丸ごと) | F(揃えた文書) |
|---|---|---|---|
| 閉世界の否定(加水も消毒もしていない施設) | 9/9・9/9 | 6/9・6/9 | 9/9・8/9 |
| 未公表の申告(玉川は放射能泉か) | 6/9・6/9 | 2/9・5/9 | 5/9・8/9 |
| 汚染の切り分け(美肌・デトックス) | 2/9・6/9 | 0/9・5/9 | 6/9・7/9 |
いずれも「文書に何が書かれているか」で決まっていました。「無い」と言うには「無い」と書いた文書が必要で、それが同じ書式で全施設分そろっていれば、数え上げも切り分けも LLM がやってくれます。 F の文書には推論結果(isUnmodifiedSupply)も書いていないので、推論エンジンの取り分もここでは出ていません。
逆に、整理すらしなくてよかった領域も最初から広かったです。公表値の照会(Q1・Q2)、法定の言い回し(Q4)、条文の数量(Q5)、法定プロトコル(Q8)。一次情報が文書として手元にあるなら、Web から取ってきたそのままでも検索で届きます。
そして、B が負けた問の敗因が表現形式でなかったことは、追試で確かめました。Q2 で Haiku の B が 3/12 だったのは源泉名で引くツールを用意していなかったから、Q7 で仕上げ湯の関係が引けなかったのは辺の逆向きを引く経路が無かったからです。両方を足して測り直すと(条件G)、Haiku は 71.6% → 86.3% に上がり、F との差は 1.9pt になりました。値も辺もグラフにあったのに、引けていなかっただけでした。
整理した内容は、置いただけでは効きません。問いの形に合う引き方を用意して初めて効きます。 そして揃っていれば、引くのは BM25 でも足ります。
これが今回いちばん実務的な教訓です。「知識をグラフにしました」で止まっているシステムは、たぶん検索に負けます。逆に、丁寧に整理したのなら、それを文書に書き出して検索させるだけでも同じ成績が出ます。 グラフか文書かは、運用のしやすさで選べばいい話でした。
じゃあ Ontology を書いた作業は無駄だったのか。まったく逆で、あの作業が成績のほとんどを作っています。 F の文書は Ontology から生成したもの。資産は判断の集合(どの項目を全対象で埋めるか、何を未公表と書くか、母集合をどう定義するか)で、TTL はその置き場所でした。2分で書き出せたのは、判断が済んでいたからです。Ontology を作る行為の本体は、RDF を書くことではなく、この判断を1つずつ決めていくことでした。
そして Ontology を選ぶ理由もあります。スキーマがあるので抜けを機械的に検出できる(「出典URLの無い源泉があったら落ちる」というテストが書ける)、推論で導出できる(今回は出番が少なかったものの、飲用限界量の算出と巡浴の制約充足はグラフ側にしか無い機能です)、そして検査の基準として使える。整理の置き場所として、これらが要るなら Ontology は自然な選択です。
その検査の側が、最後に残った差でした。合格率では F が同等以上でしたが、「何が法定の記述で何がそうでないか」を判定する表(onsen:NonStatutoryIndication など)は、渡す文書には書いていないグラフ側の資産です。書いてしまえば検査の基準を被験者に渡すことになります。嘘を減らすのは整えたデータでできますが、嘘を検出するには別に基準が要ります。
出典があることと、法定の記述であることは違う。
条件 D を入れたことで検算レイヤの穴(出典の有無は見るが質は見ない)が露出し、直すことになりました。Ontology の価値を測るために生テキストを持ち込んだら、Ontology の使い道が「生成の材料」ではなく「記述を揃える設計図」と「検査の基準」だと分かった、というのが今回の結論です。
「とりあえず Ontology を入れる」への不信は、測り終えても変わっていません。変わったのは、辟易の理由を数字で言えるようになったことです。 正しくはこう言うべきでしょう。「Web を丸ごと取り込んだ RAG に対する上積みは4〜5ポイント。だが同じ内容を同じ書式の文書に揃えれば、置き場所が Markdown でも同じ成績が出た。効いていたのは形式ではなく、整理する作業のほうだった」。
ですので、これから何か作る人に言いたいことは1つです。
まず、整理されたデータを用意してください。 全対象について同じ項目を同じ順序で並べ、非該当と未公表を明示し、母集合を決め、古い表記を落とす。その整理手法として Ontology / Knowledge Graph はとても強力な選択肢です。ですが手法から始める必要はありません。生のテキストをちゃんと用意するだけでも、そこまで到達できます。
一般化できそうな3つ
温泉に限らない話として3つ残ります。
1. 先に決めるのは形式ではなく、整理の中身です。 「非実施・未公表・該当なし」を言う必要があるなら、その項目を全対象について同じ順序・同じ書式で書く。揃っていれば LLM は数え上げます。置き場所は RDF でも Markdown でもかまいません(逆に、揃えても引く経路を用意しなければ届きません。Q2 がそれです)。
2. 名前ではなく判定根拠を型にする。 「酸性泉」という結論だけを持つのではなく、成分値から判定したのか(hasQuality)名称の表記から読み取ったのか(hasQualityFromName)を型で分けます。根拠の強さが違うものを同じ述語にすると、あとで区別できません。「ラジウム含有と書いてある」と「放射能泉である」の距離は、この型の違いそのものです。文書に書き出すときも、この区別は文にして残す必要があります。
3. 守らせたい約束は、検査できる形に翻訳する。そのとき出典の「ある/なし」と「質」を分ける。 プロンプトの「ツールが返した値だけを使え」は unsourced_quantity / unsourced_term に翻訳できました。ですがこの翻訳は出典の質を見ません。Web を丸ごと引く条件では、施設ページの通俗表現がすべて「出典あり」で通ってしまいます。そして検査の基準は、被験者に渡す文書とは別に持つ必要があります。
残った限界
条件 F・G・H は後から足した追試です。 それぞれ12問×3回×2モデルの72サンプルで、合計すると504サンプルになります。A〜D の288サンプルとは別に走らせているので、採点器と問は同一ですが、同時に走らせた実験ではありません。
条件 H が F より下がった原因は切り分けられていません。 計算ツールを足す際にプロンプトへ1段落追記しており、その影響とサンプリングの揺れが混ざっています。言えるのは「計算ツールを足しても伸びなかった」ということだけです。
F のコーパスは「答えを書いていない」つもりですが、線引きは私の判断です。 推論値(無加工供給・皮膚刺激)とヒューリスティック(仕上げ湯)は入れず、テストで0件を固定しました。それでも「同じ書式で並べる」という設計自体が Q9 を解きやすくしているのは事実です。測ったのは「揃えれば届くか」であって、「揃え方に恣意性がないか」ではありません。
計算する問そのものは、まだ測れていません。 含有成分別の限界飲用量は、掲示基準の PDF に計算例の答えが印字されていたので問から落としました。条件 H で計算ツールを渡してはみましたが、それを必要とする問を作っていないので、「計算が必要な問では文書検索では届かない」という予想は依然として検証できていません。ここは次の宿題です。
検算は意味を検査しません。 「塩分が皮膚に膜を張って水分の蒸発を防ぐ」のような自由記述の作用機序は、語彙表・通俗表現表・言い換え表のいずれにも無いので素通りします。表を増やして解ける問題ではなく、文を命題に分解して照合する必要があります。ここが今回到達した境界です。
採点器も完全ではありません。 Q6 では、施設ページと掲示基準を正しく突き合わせた回答が「うちみ」を引用したことで落ちています。引用と主張の区別は正規表現では付きません。窓を広げれば「最後に一言断れば全部通る」ことになるので、狭いままにしました。取りこぼす側に倒したという判断だけは記録しておきます。
データは15温泉地で止まっています。 箱根湯本と秋保温泉は、Web 上に一次情報が無いという結論に達しました。分析書は現地の壁にしか貼っていないのです。そしてこの記事の発端になった蔵王温泉すら、まだ収録していません。 観光協会が公表しているのは温泉地全体としての pH1.25〜1.6 という範囲で、源泉ごとの分析値ではないからです。草津で「温泉地単位の pH は存在しない」と書いた自分の理屈が、そのまま自分に返ってきた形になりました。
ここから先は現地調査です。皮肉なもので、この Ontology を——いや、揃えたドキュメントを——広げる作業は、温泉に行くことでしか進みません。 悪くない結論だと思います。次は源泉ごとの掲示を、湯に浸かりながら撮ってきます。
一次情報
| 文書 | URL |
|---|---|
| 環境省「温泉の定義」 | https://www.env.go.jp/nature/onsen/point/ |
| 鉱泉分析法指針(平成26年改訂) | https://www.env.go.jp/nature/onsen/pdf/2-5_p_14.pdf |
| 掲示基準(環自総発第1407012号) | https://www.env.go.jp/nature/onsen/pdf/2-5_p_11.pdf |
| 温泉利用基準(飲用利用基準) | https://www.env.go.jp/nature/onsen/pdf/2-5_p_10.pdf |
| 温泉法・施行令・施行規則 | https://www.env.go.jp/nature/onsen/pdf/2-5_p_1r.pdf |
記事中で参照した蔵王温泉の公表値は次の2件によります。同じ温泉地について、公表されている pH が出典によって違うのが分かります。
| 出典 | 公表内容 |
|---|---|
| 蔵王温泉観光協会「蔵王温泉とは」 https://zaomountainresort.com/about/ | 源泉温度45〜66℃、湯量毎分約5,700L、pH1.25〜1.6、効能・禁忌症の掲示 |
| 湯の花茶屋 新左衛門の湯「よく頂くご質問」 https://zaospa.co.jp/toiawase | 泉質「酸性・含硫黄-アルミニウム-硫酸塩・塩化物温泉(強酸性硫黄泉)」、pH1.3、蔵王エリアは5つの源泉群・47源泉、湧出量15,000L/分 |
蔵王温泉は本 Ontology に未収録です(公表されているのが温泉地または源泉群の単位で、源泉ごとの分析値ではないためです)。
施設データの出典41件は docs/research-onsen-data.md、法令側の出典31件は docs/research-spring-quality.md に一覧してあります。
- リポジトリ(コード・Ontology・検証ハーネス) https://github.com/kazuhitogo/onsentology
- 検証の再現:
uv run onsen corpus build(Web から生テキストのコーパスを作る)→uv run onsen corpus align(グラフから揃えた生ドキュメントを書き出す)→uv run onsen eval(Bedrock を呼ぶ。4条件×12問で48回)→uv run onsen eval --conditions F G H(追試の3条件)→uv run onsen eval --report eval-results/*.jsonl(集計)