5
5

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のJSON Schemaを厳しくしても業務検証を消せない理由

5
Posted at

LLMのJSON Schemaを厳しくしても業務検証を消せない理由

Structured OutputsでJSON Schemaに一致する出力を得られるなら、アプリ側のvalidationも消せそうに見えます。消せるのは、主に「JSONとして壊れている」「必須キーがない」「enum外の値が来る」といった形式上の失敗です。

start_dateよりdue_dateが前、入力に存在しない依頼者名が補完される、といった誤りはSchemaを通過できます。Schemaを厳しくするほど楽になるのは構造の検証であって、業務上の正しさの検証ではありません。

2026年9月30日時点のOpenAI公式ドキュメントでも、Structured Outputsは指定したJSON Schemaへの準拠を保証する仕組みとして説明される一方、生成内容そのものには誤りが残り得ると明記されています。

JSON modeで残る失敗を分離する

まず、問題を3種類に分けます。

たとえば、作業依頼を次の形へ変換するとします。

{
  "title": "在庫表を確認する",
  "priority": "high",
  "start_date": "2026-10-05",
  "due_date": "2026-10-08"
}

ここで起きる失敗は同じではありません。

  1. JSONが途中で切れるなど、そもそもparseできない
  2. priorityが想定外、キーが欠けるなど、構造が違う
  3. due_dateがstart_dateより前など、構造は正しいが意味がおかしい

JSON modeが主に扱うのは1です。

OpenAIのStructured model outputsでは、JSON modeはvalid JSONを保証する一方、特定のSchemaへの準拠は保証しないとされています。

つまり、こんなJSONは普通にparseできます。

import json

raw = """
{
  "title": "在庫表を確認する",
  "priority": "urgent",
  "start_date": "2026-10-05"
}
"""

data = json.loads(raw)
print(data)

しかしアプリが期待しているpriorityがlow | medium | highで、due_dateが必須なら、このデータは使えません。

ここでStructured Outputsを入れる意味があります。

strictなSchemaが消してくれるもの

Responses APIではStructured Outputsをtext.formatで指定できます。公式ドキュメントでは、Structured Outputsが利用できる場合はJSON modeよりこちらを使うことが推奨されています。

2026年9月30日時点の公式ドキュメントに合わせ、Pythonで最小構成を書くと次のようになります。

依存はopenaiです。APIの仕様変更を追えるよう、実際のプロジェクトでは動作確認したバージョンを固定してください。

openai
import json
from openai import OpenAI

client = OpenAI()

schema = {
    "type": "object",
    "properties": {
        "title": {"type": "string"},
        "priority": {
            "type": "string",
            "enum": ["low", "medium", "high"],
        },
        "start_date": {
            "type": "string",
            "format": "date",
        },
        "due_date": {
            "type": "string",
            "format": "date",
        },
    },
    "required": [
        "title",
        "priority",
        "start_date",
        "due_date",
    ],
    "additionalProperties": False,
}

response = client.responses.create(
    model="gpt-6-astra",
    instructions="入力文から作業依頼を抽出してください。",
    input=(
        "10月5日に在庫表の確認を開始し、"
        "10月8日までに終える。優先度は高。"
    ),
    text={
        "format": {
            "type": "json_schema",
            "name": "work_request",
            "strict": True,
            "schema": schema,
        }
    },
)

data = json.loads(response.output_text)
print(data)

ここではSchemaが責任を持つ範囲がかなり明確です。

priorityは3種類に限定されます。4つのプロパティは必須です。余計なプロパティも許しません。日付文字列にはformat: "date"を指定しています。

なおStructured Outputsのstrict modeでは、すべてのフィールドをrequiredにし、objectにはadditionalProperties: falseを設定する必要があります。optional相当の値が必要なら、公式ドキュメントではnullとのunionで表現する方法が案内されています。

これなら、LLMが返したJSONを受け取るたびに、

if "due_date" not in data:
    ...

のような防御コードをあちこちへ足す必要はありません。

Schemaは「アプリが受け取れる形」をLLMとの境界で固定するために使う、と考えると扱いやすいです。

ただし、ここでvalidationを全部削除すると別の穴が残ります。

Schemaを通っても壊れているデータ

LLMのJSON Schemaを厳しくしても業務検証を消せない理由

次の値を見てください。

{
  "title": "在庫表を確認する",
  "priority": "high",
  "start_date": "2026-10-08",
  "due_date": "2026-10-05"
}

JSONとして正しい。型も正しい。必須項目もあります。enumにも違反していません。日付もそれぞれ単独では妥当です。

それでも「開始日より期限が前」という業務ルールを採用するシステムなら不正です。

Schemaで各フィールドを厳しくしても、複数フィールドの関係から決まる不変条件は別問題です。

さらに厄介なのが「根拠」の問題です。

入力が、

在庫表を来週確認しておいて。

だけだったとします。

出力が、

{
  "title": "在庫表を確認する",
  "priority": "high",
  "start_date": "2026-10-05",
  "due_date": "2026-10-08"
}

ならSchema上は問題ありません。しかし入力に優先度や具体的な期限がなければ、それらの値を確定してよいかはアプリ側の仕様次第です。

OpenAIのStructured Outputs公式ドキュメントにも、ユーザー入力がSchemaに適合する回答を作れない場合、モデルがSchemaへ合わせようとして値を生成する可能性があるため、その場合の扱いを指示するよう注意があります。

これはSchemaを複雑にすれば解決する話ではありません。

Schemaが見ているのは出力です。「その値が入力のどこに根拠を持つか」は、別の情報を設計しない限り判定できません。

domain validatorを別層に置く

そこで、構造検証の後ろに業務ルール専用のvalidatorを置きます。

API呼び出しなしでも確認できる最小コードにすると、こうなります。Python 3.11以降で動かせます。

from datetime import date
from typing import Literal, TypedDict


class WorkRequest(TypedDict):
    title: str
    priority: Literal["low", "medium", "high"]
    start_date: str
    due_date: str


def validate_domain(req: WorkRequest) -> None:
    start = date.fromisoformat(req["start_date"])
    due = date.fromisoformat(req["due_date"])

    if due < start:
        raise ValueError(
            "due_date must be on or after start_date"
        )


valid: WorkRequest = {
    "title": "在庫表を確認する",
    "priority": "high",
    "start_date": "2026-10-05",
    "due_date": "2026-10-08",
}

invalid: WorkRequest = {
    "title": "在庫表を確認する",
    "priority": "high",
    "start_date": "2026-10-08",
    "due_date": "2026-10-05",
}

validate_domain(valid)

try:
    validate_domain(invalid)
except ValueError as e:
    print(e)

出力は次です。

due_date must be on or after start_date

ポイントは、これを「LLMの出力をもう一度parseする処理」と考えないことです。

たとえばPydanticのモデルからJSON Schemaを生成し、そのモデルへ戻す構成自体は便利です。公式ドキュメントでもSDKのSchema helperを使い、プログラミング言語側の型とJSON Schemaが乖離しないようにする方法が推奨されています。

ただ、型へparseできたことと、業務上受理できることは同義ではありません。

私は業務システムやAIを使った自動化を考えるとき、AIの出力をそのまま確定値として扱わない設計を大切にしています。このケースも同じで、境界を次のように分けると整理しやすいです。

入力
  ↓
LLM
  ↓
Structured Outputs
  ↓
型・必須項目・enumなどの構造保証
  ↓
domain validator
  ↓
日付関係・入力根拠・業務上の不変条件
  ↓
後続処理

入力根拠まで検査したいなら、出力Schemaそのものを変える方法もあります。たとえば抽出値だけでなく、その値が明示されていたかを表すフィールドを持たせます。

{
  "priority": "high",
  "priority_source": "explicit"
}

ただしpriority_sourceがexplicitであること自体もモデルの出力です。高い信頼性が必要なら、原文の該当範囲を保持してアプリ側で照合する、未確定値をnullにして人が確定する、といった設計まで考える必要があります。

ここは「Schemaをどこまで厳しくできるか」ではなく、「何を機械的に保証でき、何を業務判断として残すか」の境界です。

Structured Outputsを導入した結果、validatorがゼロになることを目標にすると設計を誤りやすいです。構造のための再試行や防御コードを減らし、その代わりdomain validatorには業務上の不変条件だけを残す。この分離なら、Schemaを厳しくした効果がコードにもテストにも表れます。

適用できないのは、正しさ自体をコードで判定できない処理です。要約の品質や自由記述の妥当性のような問題は、日付の前後関係と同じvalidatorでは扱えません。その場合はSchemaの追加ではなく、評価方法、人による確認、根拠情報の保持を別途設計する必要があります。

参考

Structured model outputs | OpenAI API

Function calling | OpenAI API

5
5
1

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
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?