この記事で分かること
LLM に JSON を出させるとき、JSON schema の後ろの方のフィールドだけが、なぜか空で返ってくることがあります。私はこれを「モデルが慎重すぎる」と解釈して遠回りしました。実際の原因は schema 側にあり、全フィールドを required にするだけで直ります。同じ症状で悩んでいる方向けに、原因と 10 行の修正を書きます。
前提知識
-
構造化出力: Ollama や各社 API には、JSON schema を渡すと、その schema に合う JSON しか生成しないモード があります (Ollama では
formatパラメータに schema を渡す)。内部では schema を「文法」に変換し、文法に合わないトークンを生成時に禁止しています。これを文法制約デコードと呼びます。 -
pydantic: Python でデータモデルを書くライブラリ。
model_json_schema()で JSON schema を出せるので、構造化出力の schema をこれで作るのが定番です。 - LLM は JSON を先頭から順に 1 トークンずつ書きます。schema にフィールドが
A, B, Cの順で並んでいれば、出力もその順になります。
環境と用途
- Ollama、ローカルの 26B 級モデル、温度 0.2〜0.3 (低め)
- 用途: ニュース記事を読ませて、「事実」「記事間の相違点」「注意点」「未解明の点」などをそれぞれリストにした JSON を出させる個人プロジェクト
症状
- 14 フィールドの JSON を出させるプロンプトで、schema の一番後ろのフィールドだけが 245 件中 144 件 (59%) 空でした。前の方のフィールドは埋まります。
- 別のプロンプトでは、指示文に 2 行足しただけで、後ろの方のリストが 4 件中 4 件で欠落しました。schema は一切触っていません。
- これらを「モデルが慎重で書き渋る」「思考モード (thinking) を切ると書き漏らす」と解釈し、指示文の書き換えや思考モードの比較に時間を使ってしまいました。
原因
pydantic は、既定値を持つフィールドを JSON schema の required に入れません。
class Out(BaseModel):
headline: str # required に入る
facts: list[str] = Field(default_factory=list) # 既定値あり → required に入らない
caveats: list[str] = Field(default_factory=list) # 同上
Ollama はこの schema をそのまま文法にします。required でないフィールドは省略可能なので、モデルが facts を書き終えた時点で } (JSON を閉じる) が文法的に合法になります。モデルは低温度では確率の高い方へ流れるので、後ろにある省略可能なフィールドほど「書かずに閉じる」側に落ちやすいのです。
つまり「慎重なモデル」ではなく「省略を許している文法」が原因でした。
対処
Python 側では既定値を残したまま (使いやすさのため)、LLM に渡す schema だけ全フィールドを必須にします。pydantic の __get_pydantic_json_schema__ を上書きすると、model_json_schema() の結果だけを書き換えられます。
from pydantic import BaseModel, Field, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema
def require_all_properties(schema: JsonSchemaValue) -> JsonSchemaValue:
"""LLM に渡す schema の全プロパティを required にする。"""
props = schema.get("properties")
if isinstance(props, dict):
schema["required"] = list(props)
return schema
class Out(BaseModel):
headline: str
facts: list[str] = Field(default_factory=list)
caveats: list[str] = Field(default_factory=list)
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
return require_all_properties(dict(handler(core_schema)))
- リストの要素がオブジェクトの場合、その要素のモデルにも同じ上書きを付けます。
- 「該当なし」は空リスト
[]として必ず書かせる形になるので、「書き忘れ」と「本当に無い」を区別できるようになります。
効果
- 空欄 59% は消えました。
- 「慎重すぎる」と見えていた判定 (記事の主題を決める判定で、グレーな例を避ける傾向) も同時に消え、用意したグレーな例 20 件を全問正解しました。主題フィールドが schema の 11 番目にあり、10 番目で閉じられると主題まで空になっていたのが正体でした。
- 「思考モードを切ると書き漏らす」という以前の観測も再現しなくなりました (思考あり・なし各 10 件で差なし)。思考モードの優位に見えていたのは、途中で閉じる頻度の差だった可能性が高いです。
教訓
- 構造化出力で欄の欠落を見たら、モデルの性格を疑う前に schema の
requiredを確認する。 - フィールドの並び順は結果に効きます。重要な欄を最後に置くと落ちやすくなります。
- 文法制約デコードで品質が落ちること自体は研究があります ("Let Me Speak Freely?" arXiv:2408.02442、"The Format Tax" arXiv:2604.03616)。ただ「省略可能なプロパティが原因で途中で閉じる」という機構は、私が探した範囲では見つかりませんでした。同じ現象を見た方がいれば教えてください。