生成AIのJSONモードは絶対なのか?
最近の生成AIには「JSONモード」という機能がある。私は初め、稚拙にも、これを使えば、LLMの出力が「絶対」有効なJSONになるのだと勘違いしていた。しかし、このモードを使用するうちに、しばしばLLMが壊れたJSON構造を返してくることがあった。ここで、私の中に疑問が生まれる。そもそも、本質的には確率的言語モデルであるLLMにどうやって「絶対」を強いるのか?私が経験したように、特定の条件では「JSONモード」といえども壊れたJSONを返したりするのではないだろうか?そう思いついた私はどのような条件で「JSONモード」が壊れるのか軽い実験してみることにした。
本記事では、Gemini 2.0 FlashのJSONモードを使って、本当にJSONモードは絶対なのかを実験的に検証した。モデルは別になんでも良かったのだが、GeminiのAPIキーが手元にあったのでとりあえずGeminiで実験することにした。
結論から言うと、特定の条件下でJSONモードは壊れる。
JSONモードとは
Gemini APIでは、response_mime_type="application/json" を指定することで、出力をJSON形式にする「JSONモード」にできる。
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_API_KEY")
response = client.models.generate_content(
model="gemini-2.0-flash",
contents="3つの都市を挙げてください",
config=types.GenerateContentConfig(
response_mime_type="application/json", # JSONモード
),
)
通常はこれで有効なJSONが返ってくる。では、どんな条件で壊れるのだろうか?
意外にも堅牢なJSONモード
私はとりあえず、JSONモードの壊れる条件を探してみようと、下に列挙したような様々な「意地悪」なプロンプトを試してみた。しかし結果はなんと全部JSONモードの成功。つまり、壊れた構造のJSONデータは一つも返ってこなかったのである。
| プロンプト | 結果 |
|---|---|
| JSON形式の指示なしで質問 | 成功 |
| Markdown形式で出力を要求 | 成功 |
| コードブロックで出力を要求 | 成功 |
| XML形式で出力を要求 | 成功 |
| 50階層のネストJSONを生成 | 成功 |
| 複雑なJSONの内容変換 | 成功 |
| 複雑なJSONに複数の変換ルールを同時に適用 | 成功 |
| 複雑なJSONの変更を指示し変換内容の説明も同時に要求 | 成功 |
驚くほど堅牢だ。 XML形式を要求してもJSONで返してくるし、25,000文字を超えるような複雑な出力でも問題なく動作した。さらには50階層のネスト構造のJSONも問題なく生成できる。これはお手上げである。JSONモードではJSONの構造は壊れないように見える。しかし、だとすると、私が時折遭遇した壊れたJSONはどのようにして、どのような条件で生成されたのだろうか?
実験1:ネスト構造を持つJSONの変更を指示
私は自らの壊れたJSONとの遭遇経験を分析し直し、JSONモードが壊れるのは「生成指示」ではなく、「変換指示」を行った時なのではないかと仮説を立てた。さらには、「変換」を行う対象であるJSON構造の「ネストの深さ」も関係しているのではないかと考えた。そこで、以下のような深いネスト構造を持つJSONの変換を試みた。
プロンプト:
以下のJSONの全ての"value"を"REPLACED"に置換して返してください:
変更対象のJSON:
{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":{"j":{"k":{"l":{"m":{"n":{"o":{"value":"original1"}},"value":"original2"},"value":"original3"},"value":"original4"},"value":"original5"},"value":"original6"},"value":"original7"},"value":"original8"},"value":"original9"},"value":"original10"},"value":"original11"},"value":"original12"},"value":"original13"},"value":"original14"},"value":"original15"}
このJSON構造の特徴としては、15階層に深くネストしていることもそうだが、各階層で閉じ括弧 }の直前に "value"キーがあるという複雑な構造である点も挙げられる。
結果: 5回中4回失敗
Run 1: FAILED - Extra data
Run 2: FAILED - Extra data
Run 3: FAILED - Extra data
Run 4: SUCCESS ← インデント付きで出力された
Run 5: FAILED - Extra data
これはJSONモードの弱点を見つけたのではないか?
実験2:ネストの深さは関係あるのか?
上記の実験結果から、「やはり15階層もあるから失敗したのでは?」という疑問が浮かぶ。そこで、上記の実験の失敗に「ネストの深さ」が大きく寄与したのではないかという仮説を立て、同じキーの繰り返しがない純粋な深いネストのみのJSONの変換を試した。
{"a":{"b":{"c":{"d":{"e":{"f":{ ... {"z":{"value":"original"}}}}}}}}
各階層のキー名が異なる(a, b, c...)構造で、5階層から50階層まで変換タスクを実行した。
結果: 50階層まで全て成功
Depth 5: ✓ OK
Depth 10: ✓ OK
Depth 20: ✓ OK
Depth 30: ✓ OK
Depth 40: ✓ OK
Depth 50: ✓ OK
結果から見るに私の仮説は間違っていたようだ。どうやらネストの深さだけでは壊れない。 実験1で失敗したのは、15階層という深さではなく、「各階層に同じ "value"キーが存在する」といった複雑な構造が原因だったのではないか?
実験3:繰り返し文言は関係あるのか?
上記の実験により、「ネストの深さ」は単体としては、JSONが壊れる要因としては弱いことがわかった。そうなるともう一つの要因である「繰り返し文言」がJSON崩壊のキーとなる可能性が出てきた。そこで、実験1のJSONのネスト5階層版を用意して、変更指示を行った。しかし、結果は成功であった。JSONモードはちゃんとJSON構造を出力した。
実験4:ネストの深さと繰り返し文言の組み合わせ
これまでの実験の結果を合わせて考えると、「ネストの深さ」単体ではなく、「繰り返し文言」単体でもなく、その二つの組み合わせ、つまり「ネストの深さ」X「繰り返し文言」の組み合わせがJSONの崩壊を招いたのではと考える。そこで「繰り返し文言」の構造を保ちつつ、ネスト数を増やしながら、どの段階でJSONの崩壊が起きるか実験した。
結果: 9階層で不安定になる
Depth 5: ✓ OK
Depth 6: ✓ OK
Depth 7: ✓ OK
Depth 8: ✓ OK
Depth 9: ✓ FAILED
Depth 10: ✓ FAILED
やはりこの組み合わせがJSONモードの挙動を不安定にするらしい。
実験5: Webクロール結果の変換
実験3の結果を見て私は納得した。なぜなら、私が崩壊したJSONに遭遇したのは大体が、webクロールしたデータをJSON構造に落とし込んで、そのJSONに対してLLMに色々な操作を行わせていた時だったからである。Webクロール結果のJSONの構造は、まさに上記の実験で明らかとなったJSONの崩壊を招く要因である「深いネスト」と「繰り返す文言」の組み合わせを多く含む。ここで実際に、「JSON構造に落とし込んだWebクロール結果」に見立てたJSONを作成し、実験を行ってみた。
JSON:
{"html":{"tag":"html","attrs":{"lang":"ja"},"children":[{"tag":"head","attrs":{},"children":[{"tag":"meta","attrs":{"charset":"UTF-8"},"children":[],"text":""},{"tag":"title","attrs":{},"children":[{"text":"複雑なページ"}],"text":""}],"text":""},{"tag":"body","attrs":{"class":"page"},"children":[{"tag":"header","attrs":{},"children":[{"tag":"nav","attrs":{},"children":[{"tag":"div","attrs":{"class":"nav-wrapper"},"children":[{"tag":"ul","attrs":{},"children":[{"tag":"li","attrs":{},"children":[{"tag":"a","attrs":{"href":"/"},"children":[{"tag":"span","attrs":{},"children":[{"text":"ホーム"}],"text":""}],"text":""}],"text":""},{"tag":"li","attrs":{},"children":[{"tag":"a","attrs":{"href":"/about"},"children":[{"tag":"span","attrs":{},"children":[{"text":"概要"}],"text":""}],"text":""}],"text":""},{"tag":"li","attrs":{},"children":[{"tag":"div","attrs":{"class":"dropdown"},"children":[{"tag":"a","attrs":{"href":"/services"},"children":[{"tag":"span","attrs":{},"children":[{"text":"サービス"}],"text":""}],"text":""},{"tag":"ul","attrs":{"class":"dropdown-menu"},"children":[{"tag":"li","attrs":{},"children":[{"tag":"a","attrs":{"href":"/services/web"},"children":[{"tag":"span","attrs":{},"children":[{"text":"Web開発"}],"text":""}],"text":""}],"text":""},{"tag":"li","attrs":{},"children":[{"tag":"a","attrs":{"href":"/services/app"},"children":[{"tag":"span","attrs":{},"children":[{"text":"アプリ開発"}],"text":""}],"text":""}],"text":""},{"tag":"li","attrs":{},"children":[{"tag":"a","attrs":{"href":"/services/ai"},"children":[{"tag":"span","attrs":{},"children":[{"text":"AI導入"}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"main","attrs":{},"children":[{"tag":"section","attrs":{"class":"hero"},"children":[{"tag":"div","attrs":{"class":"container"},"children":[{"tag":"div","attrs":{"class":"row"},"children":[{"tag":"div","attrs":{"class":"col"},"children":[{"tag":"h1","attrs":{},"children":[{"text":"タイトル"}],"text":""},{"tag":"p","attrs":{},"children":[{"text":"説明文"}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"section","attrs":{"class":"features"},"children":[{"tag":"div","attrs":{"class":"container"},"children":[{"tag":"div","attrs":{"class":"row"},"children":[{"tag":"div","attrs":{"class":"col"},"children":[{"tag":"div","attrs":{"class":"card"},"children":[{"tag":"div","attrs":{"class":"card-header"},"children":[{"tag":"h3","attrs":{},"children":[{"text":"機能1"}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"card-body"},"children":[{"tag":"p","attrs":{},"children":[{"text":"内容1"}],"text":""},{"tag":"ul","attrs":{},"children":[{"tag":"li","attrs":{},"children":[{"text":"項目A"}],"text":""},{"tag":"li","attrs":{},"children":[{"text":"項目B"}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"col"},"children":[{"tag":"div","attrs":{"class":"card"},"children":[{"tag":"div","attrs":{"class":"card-header"},"children":[{"tag":"h3","attrs":{},"children":[{"text":"機能2"}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"card-body"},"children":[{"tag":"p","attrs":{},"children":[{"text":"内容2"}],"text":""},{"tag":"ul","attrs":{},"children":[{"tag":"li","attrs":{},"children":[{"text":"項目C"}],"text":""},{"tag":"li","attrs":{},"children":[{"text":"項目D"}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"col"},"children":[{"tag":"div","attrs":{"class":"card"},"children":[{"tag":"div","attrs":{"class":"card-header"},"children":[{"tag":"h3","attrs":{},"children":[{"text":"機能3"}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"card-body"},"children":[{"tag":"p","attrs":{},"children":[{"text":"内容3"}],"text":""},{"tag":"ul","attrs":{},"children":[{"tag":"li","attrs":{},"children":[{"text":"項目E"}],"text":""},{"tag":"li","attrs":{},"children":[{"text":"項目F"}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"section","attrs":{"class":"testimonials"},"children":[{"tag":"div","attrs":{"class":"container"},"children":[{"tag":"div","attrs":{"class":"slider"},"children":[{"tag":"div","attrs":{"class":"slide"},"children":[{"tag":"div","attrs":{"class":"testimonial"},"children":[{"tag":"div","attrs":{"class":"quote"},"children":[{"tag":"blockquote","attrs":{},"children":[{"tag":"p","attrs":{},"children":[{"text":"素晴らしいサービス"}],"text":""}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"author"},"children":[{"tag":"span","attrs":{"class":"name"},"children":[{"text":"田中"}],"text":""},{"tag":"span","attrs":{"class":"company"},"children":[{"text":"A社"}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"slide"},"children":[{"tag":"div","attrs":{"class":"testimonial"},"children":[{"tag":"div","attrs":{"class":"quote"},"children":[{"tag":"blockquote","attrs":{},"children":[{"tag":"p","attrs":{},"children":[{"text":"期待以上の品質"}],"text":""}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"author"},"children":[{"tag":"span","attrs":{"class":"name"},"children":[{"text":"佐藤"}],"text":""},{"tag":"span","attrs":{"class":"company"},"children":[{"text":"B社"}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"slide"},"children":[{"tag":"div","attrs":{"class":"testimonial"},"children":[{"tag":"div","attrs":{"class":"quote"},"children":[{"tag":"blockquote","attrs":{},"children":[{"tag":"p","attrs":{},"children":[{"text":"迅速な対応"}],"text":""}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"author"},"children":[{"tag":"span","attrs":{"class":"name"},"children":[{"text":"鈴木"}],"text":""},{"tag":"span","attrs":{"class":"company"},"children":[{"text":"C社"}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"footer","attrs":{},"children":[{"tag":"div","attrs":{"class":"container"},"children":[{"tag":"div","attrs":{"class":"row"},"children":[{"tag":"div","attrs":{"class":"col"},"children":[{"tag":"div","attrs":{"class":"footer-section"},"children":[{"tag":"h4","attrs":{},"children":[{"text":"リンク"}],"text":""},{"tag":"ul","attrs":{},"children":[{"tag":"li","attrs":{},"children":[{"tag":"a","attrs":{"href":"/privacy"},"children":[{"text":"プライバシー"}],"text":""}],"text":""},{"tag":"li","attrs":{},"children":[{"tag":"a","attrs":{"href":"/terms"},"children":[{"text":"利用規約"}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"col"},"children":[{"tag":"div","attrs":{"class":"footer-section"},"children":[{"tag":"h4","attrs":{},"children":[{"text":"連絡先"}],"text":""},{"tag":"p","attrs":{},"children":[{"text":"info@example.com"}],"text":""}],"text":""}],"text":""}],"text":""},{"tag":"div","attrs":{"class":"copyright"},"children":[{"tag":"p","attrs":{},"children":[{"text":"© 2024"}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}}
プロンプト:
以下はWebページをクロールしてJSON化したデータです。全ての"text"の値を英語に翻訳してください。構造は完全に維持してください。
(上記のJSON)
結果は予想通りで、5回試して4回JSONが崩壊した。
なぜこの条件でJSONが壊れやすいのか
そもそもJSONモードとは何か
まず誤解してはいけないのは、JSONモードは「必ず正しいJSONを出力させる強制装置」ではないという点である。実体としては、“JSONらしいトークン({ } " : , など)を優先的に出すようバイアスを掛けた確率的生成モード” にすぎないのだ。
LLMは内部に構文木を持っているわけではなく、常に「次のトークンの確率」だけで文章を構築する確率モデルであると見ていい。JSONモードというのはこの確率分布に補正をかけて「JSONの形になりやすく」してはいるが、絶対ルールではなく誘導でしかない。そのため、文脈によってはこのバイアスがあっさり上回られて、構造が崩れることがある。
深いネストが JSON モードを破る理由(構造整合性より内容語の確率が勝つ)
深いネスト( { "a": { "b": { ... }}} のような構造)が続くと、モデルは「開いた括弧をいつ閉じるか」を純粋な確率だけで管理しなければならない。しかし LLMは構造を正確に追跡する仕組みを持っておらず、深くなるほど “} を閉じる” という行為の確率は相対的に低くなる。
その理由はシンプルであり、深い文脈に入るほど、
- 説明文や内容語の方がトークン確率で優勢になる
- その結果、JSONモードによる “閉じ括弧を出したがるバイアス” を上回る確率差が発生する
という性質がLLMにあるからだ。
結果として、閉じ括弧が欠落したり、意図しない文章が混入することとなる。構造の複雑化がそのまま「バイアス突破」に繋がる典型例であると言える。
繰り返し文言が JSON モードを破る理由(確率の自己強化による“暴走”)
LLMには 「最近繰り返したトークンほど、次もそれを生成しやすくなる」 という性質が存在する。これは確率モデル特有の自己強化であり、同じフレーズが何度も出現すると、それ自体の尤度が急上昇するのだ。すると、
- JSON に必要な
,や} - JSON モードが優遇している構文的記号
よりも、「文章を続ける方が確率的に自然」 とモデルが判断し始める。この時点で、JSONモードの構文バイアスは完全に押し負けてしまい、JSONの外へ文章が“漏れ出す”現象が起きるのだ。
繰り返し文言は言わば 確率分布を歪める“増幅器” で、JSON モードの誘導を突破する主要因のひとつであると言える。
余談:minified要求
LLMの仕組みとJSONモードの正体がわかってくると、他にどんな要因がJSONモードの弱点となりえるかが見えてくる。要はJSONモードの確率バイアスを超えるような要因があればいいわけである。それで私が思いついたものに「minified形式を要求する」というものがある。以下に試しに行った実験を示す。
実験6
JSON:
{"html":{"tag":"div","children":[{"tag":"div","children":[{"tag":"div","children":[{"tag":"div","children":[{"tag":"div","children":[{"tag":"div","children":[{"tag":"div","children":[{"tag":"div","children":[{"text":"こんにちは"}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}],"text":""}}
プロンプト1:
以下のJSONの"text"を英語に翻訳してください:
(上記のJSON)
比較的短く、単純なJSONであることもあり、このタスクの成功率は100%である。しかし、プロンプトに少し手を加えると、結果は変化する。
プロンプト2:
以下のJSONの"text"を英語に翻訳し、結果をminified形式(改行・スペースなし、1行)で返してください:
(上記のJSON)
結果: 0/5 成功
Run 1: FAILED - Expecting ',' delimiter
Run 2: FAILED - Expecting ',' delimiter
Run 3: FAILED - Expecting ',' delimiter
比較的簡単なJSONではあったが「minified形式」を指定しただけで失敗した。では、これはなぜだろうか?これもLLMとJSONモードの仕組みと性質からある程度説明できる。
通常の整形JSONでは改行やスペースが「構文の目印」として機能し、LLMはその余白を頼りに閉じ括弧やカンマを挿入しやすくなっている。しかしminified形式ではすべてが連続しているため、余白に頼らず、トークン確率だけで整合性を維持する必要があり、JSONモードの補正バイアスが追いつかなくなってしまう。そして、次に出すべきトークン(例えば } や ,)の確率は翻訳文のトークン確率と競合し、改行やインデントによる「構造のヒント」がないことで、自然言語を続ける確率が勝ち、結果として構文エラーが発生しやすくなるというわけである。
まとめ
本記事の実験から、JSONモードは通常の生成タスクでは非常に堅牢であり、深いネスト構造や複雑な指示にも高い精度で応答できることがわかった。一方で、深いネスト × 同一文言の反復 × 変換タスクといった特定条件が重なると、階層の取り違えや構造崩壊が発生するケースが確認され、「壊れる状況」は確かに存在することも明らかになった。
しかし忘れてはならないのが、これはJSONモード固有の欠陥というより、LLMが本質的には確率的生成モデルであることによる自然な帰結であると言える点である。LLMにタスクを任せる際は、LLMの性質を念頭に置き、タスクの分割・スキーマ固定・前処理などの工夫を行うことで成功率を高めることができる。LLMの強力さに頼り切るのではなく、その確率モデルとしての性質を理解し、適切な設計を行うことで初めて、安定した操作が実現できるのだと考える。
Discussion