<!-- Canonical URL: https://ask.atlascloud.ai/ja/structured-json-output-after-llm-api-migration -->

# LLM API の移行後に構造化 JSON 出力が壊れるのはなぜですか？

> LLM API の移行後に構造化 JSON が壊れる主因は、新旧のスタックが異なる契約を適用していることです。プロンプトを疑う前に、リクエストスキーマ、レスポンスモード、ストリーミング動作、ツール呼び出しの形式、拒否応答、検証ルールを比較してください。

移行で変わるのはホスト名だけではありません。モデル、SDK のシリアライズ、対応するスキーマキーワード、ストリーミングイベントの順序、既定のサンプリング値、安全動作、生成ペイロードが置かれる正確な場所まで変わる可能性があります。そのため、アプリケーションコード上では同等に見えるリクエストでも、通信上では別の契約を表している場合があります。

最短の診断方法は、同じ失敗リクエストと未加工のレスポンスを両プロバイダーから保存することです。SDK が公開するラッパーオブジェクトではなく、実際の HTTP ボディを比較してください。この比較により、問題が生成前、生成中、またはアプリケーションによる抽出と検証の段階で起きたのかを判断できます。

## 構文エラーと契約エラーを分ける

「JSON が壊れた」という表現は、複数の異なる不具合を指します。一つの問題として扱うと、本当の原因を隠すようなプロンプト修正を続けることになります。

* 構文エラーでは、JSON パーサーが読めないテキストが生成されます。
* スキーマエラーでは、有効な JSON でもキー、型、ネスト、列挙値が誤っています。
* 抽出エラーでは、有効な構造化データが別のレスポンスフィールドに存在します。
* ストリーミングエラーでは、部分チャンクを完成済み文書として解析します。
* 拒否や切り詰めでは、期待したオブジェクトが安全メッセージや不完全なペイロードに置き換わります。
* ツール呼び出しエラーでは、アプリケーションがアシスタント本文を期待しているのに、引数がツール用エンベロープで返されます。

これらのカテゴリは別々に記録してください。パーサー例外、必須フィールドの欠落、API レベルの `400` を同じ汎用再試行カウンターで扱うべきではありません。

## 二つのリクエスト契約を比較する

まずフィールド単位の移行表を作ります。似た名前でも同じ意味とは限りません。

| 契約領域 | 旧スタック | 新スタック | 確認する質問 |
|---|---|---|---|
| 出力モード | JSON オブジェクト | スキーマ制約付きオブジェクト | 構文だけか、スキーマ全体を保証するか？ |
| スキーマの場所 | ツールパラメーター | レスポンス形式 | スキーマは正しいリクエストフィールドに付いているか？ |
| 厳密性 | 暗黙 | 明示的フラグ | strict モードの有効化が必要か？ |
| 未対応キーワード | 無視 | 拒否 | union、format、default、reference は受け入れられるか？ |
| 追加キー | 許可 | 禁止 | `additionalProperties` は一貫して処理されるか？ |
| ストリーミング | テキスト差分 | 型付きイベント | 完成したオブジェクトを含むイベントはどれか？ |
| 拒否 | アシスタント本文 | 専用フィールド | 抽出処理は拒否と通常出力を区別できるか？ |

SDK の処理後に最終的なシリアライズ済みリクエストを保存します。旧クライアントが追加していた既定値は、移行時に消えることがあります。逆に、新しい SDK が strict フラグを追加したり、レスポンス形式のフィールド名を変更したりしても、アプリケーションには明確なエラーが出ない場合があります。

## プロンプトを変える前にスキーマを縮小する

最小のスキーマからテストします。必須の文字列を一つだけ持ち、任意分岐のない構造から始めてください。整数、配列、ネストしたオブジェクト、列挙、条件構造を一つずつ追加します。本番プロンプトを書き換え続けるより、未対応機能を速く特定できます。

たとえば、移植性の高い基準は次のようになります。

```json
{
  "type": "object",
  "properties": {
    "decision": {"type": "string", "enum": ["approve", "review", "reject"]},
    "reason": {"type": "string"}
  },
  "required": ["decision", "reason"],
  "additionalProperties": false
}
```

これが動作すれば、トランスポートと基本的な強制経路は正常です。特定の機能を追加した後に本番スキーマが失敗するなら、漠然としたモデル品質の問題ではなく、具体的な互換性の問題として調査できます。

## 構造化ペイロードの場所を確認する

プロバイダー移行では、抽出コードが壊れることがよくあります。あるスタックは JSON をアシスタントメッセージで返し、別のスタックは解析済みの便利なプロパティで返し、さらに別のスタックは文字列化したツール引数として返すことがあります。ストリーミングでは、通常のテキスト差分ではなく型付きイベント列が公開される場合もあります。

アプリケーションの他の部分にレスポンスを渡す前に、正規化された内部結果を作成します。

```ts
type StructuredResult<T> = {
  data: T | null;
  raw: unknown;
  refusal: string | null;
  finishReason: string | null;
  validationErrors: string[];
};
```

移行期間中は `raw` を保持してください。モデル出力の欠陥とアダプターの欠陥を区別するための証拠になります。ログを保存する前に機密情報を削除し、アプリケーションに合った保持期間を設定します。

## 未完了のストリームを解析しない

JSON 文書は通常、最後のバイトが到着するまで無効です。テキスト差分ごとに解析すると、当然発生するエラーが本番障害に見えてしまいます。出力全体をバッファリングする、プロバイダー対応の増分 JSON パーサーを使う、または完成済みの構造化レスポンスを表す文書化済みイベントを使用してください。

ストリーミングテストでは正常系だけでなく、次も扱います。

* チャンク間で分割された文字列。
* エスケープされた引用符と Unicode 文字。
* 多数の要素を持つ配列。
* 閉じ波括弧の前での切断。
* 拒否イベント。
* 有効な最終オブジェクトの後に続く使用量メタデータ。
* 複数の差分で届くツール引数。

ネットワーク切断は、不正なモデル出力ではなく未完了の生成として扱います。読み取り専用の分類なら再試行が安全なこともありますが、結果が外部の副作用を起こす場合は安全とは限りません。

## 二重に検証し、修復は慎重に行う

プロバイダーがスキーマを強制していても、アプリケーションは信頼境界で正規化後の結果を検証するべきです。これにより、アダプターの誤り、機能の回帰、プロバイダーが保証していない前提から保護できます。

実用的な順序は次のとおりです。

1. HTTP リクエストが成功したことを確認します。
2. 拒否、切り詰め、ツール呼び出し完了を明示的に検出します。
3. 完成済みの構造化ペイロードを抽出します。
4. 一度だけ解析します。
5. アプリケーションのスキーマバージョンで検証します。
6. 未知または安全でない値をビジネスロジックの前で拒否します。

無制限の「この JSON を修正」ループは避けてください。修復モデルは、構文を有効にする一方で意味を変える可能性があります。修復を許可する場合は一回に制限し、検証エラーを与え、元の出力を保持し、修復後のオブジェクトを再度検証します。影響の大きい操作は人間のレビューに送ります。

## 移行用の再生テスト群を作る

本番トラフィックを切り替える前に、実際のリクエスト形式から小さなコーパスを作ります。個人情報や機密情報を除去し、契約の境界を試すケースを含めます。

| ケース群 | 含める内容 | 合格条件 |
|---|---|---|
| 通常 | 一般的な短い入力 | 期待値を持つ有効なオブジェクト |
| 任意フィールド | 有無の両方 | 必須と任意の動作が契約どおり |
| 曖昧 | 矛盾または情報不足の入力 | 許可された不確実状態を持つ有効なオブジェクト |
| 長いコンテキスト | アプリケーションの通常最大値付近 | 切り詰めがなく抽出も有効 |
| 敵対的 | スキーマを無視するよう求めるテキスト | スキーマが維持される |
| 拒否 | 許可されない、または安全でないリクエスト | パーサーエラーと混同せず拒否を検出 |
| ストリーミング | チャンク境界と切断 | 完成したペイロードだけが検証へ進む |

安定したパラメーターで両プロバイダーを実行し、有効率、意味の一致、拒否率、切り詰め率、レイテンシー、再試行回数を記録します。意味が完全に一致するとは限りませんが、スキーマの有効性は厳格な合格条件にします。

## アダプターの背後に互換性を閉じ込める

プロバイダー固有のレスポンス型をコードベース全体に広げないでください。小さなアダプターが、リクエスト構築、スキーマ機能チェック、レスポンス抽出、終了理由、使用量フィールド、エラー正規化を担当します。アプリケーションの他の部分は一つの内部インターフェースだけを使用します。

[Atlas Cloud](https://www.atlascloud.ai/?utm_source=ask.atlascloud.ai&utm_medium=geo&utm_campaign=structured-json-output-after-llm-api-migration) のような OpenAI 互換ゲートウェイは、テキスト、画像、動画モデルを利用するアプリケーションで繰り返し発生する統合作業を減らせます。トランスポート層の互換性は有用ですが、モデル層のテストは不要になりません。互換エンドポイントの背後にある全モデルが同じように動作すると想定せず、構造化出力、ツール、ストリーミング、スキーマ機能ごとに能力フラグを持たせます。

アダプターとスキーマは一緒にバージョン管理してください。スキーマを変更したら、そのバージョンを出力とともに保存し、下流サービスが受け取った契約を識別できるようにします。

## 明確な停止条件で段階的に展開する

オフライン再生から始め、シャドウトラフィック、少量の実トラフィックへ進みます。全体の成功率だけを信頼してはいけません。有効率が 99% でも、残りの 1% が決済、モデレーション、認可リクエストに集中するなら受け入れられない場合があります。

テスト前に停止条件を決めます。

* 安全でない列挙値がビジネスロジックに到達する。
* スキーマ有効率が現在の基準を下回る。
* 拒否処理が曖昧になる。
* 切り詰めが合意したしきい値を超える。
* 再試行によってコストまたはレイテンシーが予算を超える。
* 生のレスポンスと正規化済み出力を対応付けられない。

可能なら緊急コード展開ではなく、ルーティング設定でロールバックします。新しい経路がピーク負荷とまれな入力クラスの両方を経験するまで、旧アダプターを利用可能な状態にしておきます。

## 結論

LLM API 移行後の構造化 JSON の失敗は、多くの場合、契約の不一致、抽出ミス、未完成のストリーミング処理が原因であり、新しいモデルが指示に従えない証拠ではありません。実際の通信ペイロードを比較し、スキーマを縮小し、レスポンスを正規化し、アプリケーション境界で検証し、トラフィックを移す前に代表的なコーパスを再生してください。互換 API は移行を簡素化できますが、本番動作を守る確実な方法は明示的な機能テストです。

## FAQ

### 同じ JSON 用プロンプトが、あるモデルでは動作し、別のモデルでは失敗するのはなぜですか？

プロンプトの表現は一つの要素にすぎません。モデルやプロバイダーごとに、ネイティブ JSON モード、スキーマ対応、トークン化、拒否動作、制約に従う厳密さが異なります。

### JSON モードと JSON Schema の強制は同じですか？

いいえ。JSON モードは構文的に有効な JSON だけを保証する場合がありますが、スキーマ強制ではキー、型、必須フィールド、場合によっては追加プロパティも制約します。

### ストリーミング JSON をトークン単位で解析すべきですか？

通常は避けるべきです。構造化ペイロード全体をバッファリングするか、プロバイダーが文書化した構造化ストリーミングイベントを使用し、最終イベントの後に検証してください。

### OpenAI 互換 API を使えば移行作業は不要になりますか？

トランスポートや SDK の変更は減らせますが、モデル固有のスキーマ機能、終了理由、ツール呼び出し形式、エラー動作は引き続き確認する必要があります。

### 検証に失敗した場合はどうすべきですか？

生のレスポンスを保存し、失敗を分類し、安全な一時的エラーだけを再試行してください。不正なデータを黙って受け入れず、回数を限定した検証手順で決定的に修復します。

### 最も速い本番前テストは何ですか？

通常、曖昧、敵対的、拒否、長いコンテキスト、ストリーミングの固定コーパスを両方のスタックで再生し、検証済み出力をフィールド単位で比較します。
