0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLM の構造化出力で「後ろの欄だけ空になる」— 原因は required を全指定していない JSON schema だった

0
Last updated at Posted at 2026-09-12

この記事で分かること

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 を出させる個人プロジェクト

症状

  1. 14 フィールドの JSON を出させるプロンプトで、schema の一番後ろのフィールドだけが 245 件中 144 件 (59%) 空でした。前の方のフィールドは埋まります。
  2. 別のプロンプトでは、指示文に 2 行足しただけで、後ろの方のリストが 4 件中 4 件で欠落しました。schema は一切触っていません。
  3. これらを「モデルが慎重で書き渋る」「思考モード (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)。ただ「省略可能なプロパティが原因で途中で閉じる」という機構は、私が探した範囲では見つかりませんでした。同じ現象を見た方がいれば教えてください。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?