# 構造化出力で不具合報告を JSON にする：スキーマ設計・検証・再試行の実装

> 自由記述の不具合報告を、Claude API の構造化出力と Pydantic で JSON にする実装手順。スキーマの設計、マスタとの照合、拒否と途切れの扱い、再試行と人の確認への振り分けまで、Python のコードで示す。

- URL: https://weaveai.jp/journal/structured-output-defect-reports/
- カテゴリ: AI最新技術
- 公開: 2026-10-08 / 更新: 2026-10-08
- 執筆: weaveAI 編集部（株式会社サオス）

自由記述の不具合報告を JSON にするなら、言語モデルの構造化出力で「形式」を保証し、Pydantic とマスタ照合で「中身」を確かめ、通らないものだけを再試行か人の確認に回す、という 3 段に分けるのが実装しやすい形です。この記事では、Claude API の構造化出力（2026年10月時点で一般提供）と Pydantic v2 を使って、その 3 段を Python で組みます。考え方は [不具合報告と作業日報をデータに変える](https://weaveai.jp/journal/field-reports-to-data/) に書いたので、ここでは実装に絞ります。

## 全体の構成

```text
不具合報告（自由記述）
   │
   ▼  1. 構造化出力で取り出す（形式はAPIが保証）
DefectReport（Pydantic モデル）
   │
   ▼  2. マスタ照合・業務ルール（Pydantic の検証 + context）
   ├─ OK  → 品質記録へ登録
   └─ NG  → 3. 誤りを伝えて1回だけ再試行 → それでも NG なら人の確認へ
```

1 段目で保証されるのは、JSON として正しく、スキーマどおりの型であることまでです。設備番号が実在するか、工程と現象の組み合わせがありえるかは、2 段目で自分で確かめます。

## 準備

```bash
pip install anthropic pydantic
export ANTHROPIC_API_KEY=...
```

2026年10月時点の最新は、`anthropic` が 1.12.0、`pydantic` が 2.13.5 です。構造化出力は以前はベータ機能で `output_format` というパラメータでしたが、一般提供になって API の上では `output_config.format` に移り、ベータヘッダーも不要になりました。古い記事のコードをそのまま使うと 400 エラーになるので注意してください。

一方で、Python SDK の便利な関数 `client.messages.parse()` は、Pydantic のモデルを `output_format=` という名前の引数で受け取ります。SDK がモデルからスキーマを作って `output_config.format` として送り、返ってきた JSON を検証して `parsed_output` に入れてくれます。名前は同じでも、API の古いパラメータとは別物です。

## 1. スキーマを Pydantic で書く

```python
from typing import Literal
from pydantic import BaseModel, Field

Process = Literal["板金", "曲げ", "溶接", "塗装", "組立", "配線", "検査"]
Phenomenon = Literal["クラック", "キズ", "変形", "寸法不良", "異物", "動作不良", "その他"]

class DefectReport(BaseModel):
    occurred_on: str | None = Field(None, description="発生日。本文の表記のまま。書かれていなければ null")
    equipment_code: str | None = Field(None, description="設備番号（例: EQ-1001）。書かれていなければ null")
    process: Process | None = Field(None, description="工程。本文から判断できなければ null")
    phenomenon: Phenomenon = Field(description="不具合の現象")
    location: str | None = Field(None, description="不具合の部位")
    suspected_cause: str | None = Field(
        None, description="推定原因。本文に無ければ null。書き手の推測なら末尾に（推定）"
    )
    action: str | None = Field(None, description="取った処置。書かれていなければ null")
```

設計で気をつけた点は 3 つです。

- 集計に使う項目（工程・現象）は `Literal` で選択肢を固定する。自由な文字列にすると「曲げ」「ベンディング」「曲げ加工」が別々に数えられます。
- 書かれていないことは `null` にさせる。`description` で明示しておくと、モデルが推測で埋める余地が減ります。
- 「理由」や「考えた過程」を書かせる項目は作らない。Anthropic の文書は、モデルの思考や段階的な推論を求める項目が拒否（`reasoning_extraction`）の原因になりうるとし、短い説明を求めるよう勧めています。

スキーマの複雑さには上限があります。2026年10月時点の文書では、省略可能な項目は合計 24 個まで、`anyOf` や `["string", "null"]` のような型の組み合わせを使う項目は 16 個までです。上のモデルは省略可能な項目が 6 個なので余裕があります。

## 2. 取り出す

```python
import anthropic
from pydantic import ValidationError

client = anthropic.Anthropic()

SYSTEM = (
    "あなたは製造現場の不具合報告から項目を取り出す担当です。"
    "本文に書かれていないことは推測で埋めず null にしてください。"
)

class NeedsReview(Exception):
    """人の確認に回す"""

def extract(text: str) -> DefectReport:
    try:
        response = client.messages.parse(
            model="claude-sonnet-5",
            max_tokens=1024,
            system=SYSTEM,
            messages=[{"role": "user", "content": text}],
            output_format=DefectReport,
        )
    except ValidationError as e:
        # 途中で切れた出力などは、SDK の検証で弾かれる
        raise NeedsReview(f"形式の検証に失敗しました（{e.error_count()}件）") from e

    if response.stop_reason == "refusal":
        raise NeedsReview("モデルが応答を拒否しました")
    if response.stop_reason == "max_tokens":
        raise NeedsReview("出力が上限で途切れました")
    return response.parsed_output
```

構造化出力でも、スキーマどおりにならない場合が文書に明記されています。

| 状況 | 起きること | 扱い |
|---|---|---|
| 拒否 | `stop_reason` が `refusal`。HTTP は 200 で、生成した分は課金される | 人の確認へ |
| 出力の上限 | `stop_reason` が `max_tokens`。出力が不完全になりうる | `max_tokens` を上げて再試行するか、人の確認へ |
| 選択肢の大文字・小文字 | `enum` と `const` の文字列は大文字・小文字が保証されない。エラーにも特別な `stop_reason` にもならない | 比較を大文字・小文字を無視して行う |

3 つ目は見落としやすい点です。日本語の選択肢では起きにくいのですが、`EQ-1001` のような英字を含むコードを選択肢にすると、`eq-1001` で返ってきてもエラーになりません。

## 3. マスタと照合する

形式が正しくても、設備番号がマスタに無ければ使えません。Pydantic の検証関数に、`context` でマスタを渡して照合します。

```python
from pydantic import ValidationInfo, field_validator

class CheckedReport(DefectReport):
    @field_validator("equipment_code", mode="after")
    @classmethod
    def must_exist(cls, v: str | None, info: ValidationInfo) -> str | None:
        if v is None:
            return v
        code = v.strip().upper()  # 大文字・小文字の揺れをここで吸収する
        equipment = (info.context or {}).get("equipment", set())
        if code not in equipment:
            raise ValueError(f"設備マスタに無い設備番号です: {v}")
        return code

EQUIPMENT = {"EQ-1001", "EQ-1002", "EQ-1003"}  # 実際は設備マスタから読む

report = extract(text)
checked = CheckedReport.model_validate(report.model_dump(), context={"equipment": EQUIPMENT})
```

Pydantic の文書のとおり、`context` はモデルを直接作るときには渡せないので、`model_validate()` を通します。マスタに無い値は、ここで `ValidationError` になります。

## 4. 再試行と人の確認への振り分け

マスタに無い値が返ってきたときは、誤りを伝えて 1 回だけ取り出し直させます。それでも通らなければ人の確認に回します。何度も繰り返すと、費用がかさむうえに、もっともらしい値を作り出させる方向に働くからです。

```python
def process(text: str, equipment: set[str], retries: int = 1) -> CheckedReport:
    prompt = text
    for attempt in range(retries + 1):
        report = extract(prompt)
        try:
            return CheckedReport.model_validate(report.model_dump(), context={"equipment": equipment})
        except ValidationError as e:
            if attempt == retries:
                raise NeedsReview(str(e)) from e
            reason = e.errors()[0]["msg"]
            prompt = (
                f"{text}\n\n"
                f"前回の取り出し結果に誤りがありました（{reason}）。"
                "本文に設備番号が無い場合は null にしてください。"
            )
```

`NeedsReview` になった報告は、元の文章と理由を一覧にして人が見ます。現場で運用するときは、取り出した値・元の文章・使ったモデル名・日時を一緒に保存しておくと、あとで「なぜこの値になったか」を確かめられます。

## OpenAI の API で書く場合

同じことは OpenAI の Responses API でもできます。Pydantic モデルを `text_format` に渡し、結果を `output_parsed` で受け取ります。

```python
from openai import OpenAI

client = OpenAI()
response = client.responses.parse(
    model="gpt-6-astra",
    input=[
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": text},
    ],
    text_format=DefectReport,
)
report = response.output_parsed
```

OpenAI の厳密モードでは、すべての項目を必須にし、省略可能な項目は `null` との組み合わせで表すこと、すべてのオブジェクトに `additionalProperties: false` を付けることが求められます。どちらの API でも、使えない JSON Schema の機能を含めるとエラーになります。

## はまりどころ

- 最初のリクエストだけ遅い。Anthropic の構造化出力は、新しいスキーマを最初に使うときに文法を組み立てるため、待ち時間が増えます。組み立てた結果は最後に使ってから 24 時間キャッシュされます。スキーマを頻繁に変えると、そのたびに遅くなります。
- 数値の範囲や文字数の制約は、Anthropic では API 側でサポートされない。SDK は `minimum`・`maximum`・`minLength` などを項目の説明文に移し、受け取ったあとに手元で検証します。範囲の確認は 3 段目の検証でも重ねて行うのが確実です。
- Amazon Bedrock で使う場合は対応状況を確認する。2026年10月時点の Anthropic の文書では、構造化出力は Claude API や各クラウドの Claude Platform では一般提供ですが、Amazon Bedrock では旧来の連携（Opus 4.6 以前のモデル）での提供で、新しい「Claude in Amazon Bedrock」では使えないとされています。国内リージョンで処理させたい場合は、使うモデルと連携方式を先に確かめてください。
- モデル名は変わる。この記事のモデル名は 2026年10月時点の文書に載っているものです。使う時点で各社のモデル一覧を確認してください。

取り出した結果の受け皿には、決まった形の品質記録が要ります。weaveAI FactorySuite の品質の画面は、不良コード別の内訳と処置を製造実績にひもづけて記録するので、ここで取り出した `phenomenon` や `action` を流し込む先になります。

## 出典

- Anthropic「Structured outputs」（2026年10月8日確認） https://platform.claude.com/docs/en/build-with-claude/structured-outputs
- OpenAI「Structured Outputs」（2026年10月8日確認） https://developers.openai.com/api/docs/guides/structured-outputs
- OpenAI「openai-python helpers」 https://github.com/openai/openai-python/blob/main/helpers.md
- Pydantic「Models」 https://pydantic.dev/docs/validation/latest/concepts/models/
- Pydantic「Validators」 https://pydantic.dev/docs/validation/latest/concepts/validators/
- PyPI「anthropic」 https://pypi.org/project/anthropic/
- 参考（中国語）CSDN「让 AI 100% 返回合法 JSON：Schema 校验 + 自动重试，生产级可靠性」（2026年6月） https://blog.csdn.net/smile_tianya/article/details/161633795
- 参考（中国語）CSDN「手把手教你用大模型实现结构化信息提取：从 Pydantic 建模到少样本提示实战」（2025年4月） https://blog.csdn.net/The_Thieves/article/details/147293904
