T-CREATOR

Mistral が JSON 破綻する時の対処:出力拘束・再試行・検証リカバリ

Mistral が JSON 破綻する時の対処:出力拘束・再試行・検証リカバリ

Mistral AI を使ったアプリケーション開発では、API から JSON 形式でデータを受け取る場面が多くあります。しかし、LLM の出力は確率的であるため、期待通りの JSON が返ってこないことがあるんですね。

この記事では、Mistral が JSON を破綻させてしまうときの対処法を、出力拘束・再試行・検証リカバリの 3 つの軸で解説します。実際のエラーコードやサンプルコードを交えながら、堅牢な JSON 処理を実現する方法をお伝えしますね。

背景

Mistral AI における JSON 出力の重要性

Mistral AI は、テキスト生成だけでなく、構造化されたデータ(JSON)を返す用途でも広く使われています。

たとえば、以下のようなユースケースが考えられます。

#ユースケース期待する JSON 形式
1ユーザー情報の抽出{"name": "田中太郎", "age": 30}
2商品リストの生成{"products": [{"id": 1, "name": "商品A"}]}
3感情分析結果{"sentiment": "positive", "score": 0.85}

これらのデータは、フロントエンドやデータベースに渡すために JSON 形式である必要があります。

JSON 出力が必要になる背景

以下の図は、Mistral を含む典型的な Web アプリケーションのデータフローを示しています。

mermaidflowchart LR
  user["ユーザー"] -->|リクエスト| frontend["Next.js<br/>フロントエンド"]
  frontend -->|プロンプト送信| api["Backend API"]
  api -->|テキスト生成依頼| mistral["Mistral AI"]
  mistral -->|JSON レスポンス| api
  api -->|パース・検証| db[("データベース")]
  api -->|JSON| frontend
  frontend -->|表示| user

図で理解できる要点:

  • Mistral からの出力は API 層でパースされ、データベースやフロントエンドに渡される
  • JSON が破綻すると、パース処理でエラーが発生し、アプリケーション全体が停止する可能性がある
  • そのため、JSON の品質保証が極めて重要

このように、Mistral の出力が JSON として適切でないと、後続の処理すべてに影響が及びます。

課題

JSON 破綻の典型的なパターン

Mistral が返す JSON が破綻する原因は、大きく分けて以下の 3 つです。

#破綻パターン具体例
1不完全な JSON{"name": "田中" (閉じ括弧なし)
2余計なテキストの混入以下が結果です:{"name": "田中"}
3スキーマ不一致{"user_name": "田中"} (キー名が違う)

これらのパターンは、LLM の確率的な生成プロセスに起因します。

エラーコード例

JSON パースに失敗すると、以下のようなエラーが発生します。

エラーコード:SyntaxError: Unexpected end of JSON input

エラーメッセージ

javascriptSyntaxError: Unexpected end of JSON input
    at JSON.parse (<anonymous>)
    at parseMistralResponse (api.ts:42)

発生条件

  • Mistral が JSON の途中でトークン制限に達した
  • 閉じ括弧やクォートが不足している

解決方法

  1. max_tokens パラメータを増やす
  2. 再試行ロジックを実装する
  3. 出力拘束(JSON mode)を使用する

エラーコード:SyntaxError: Unexpected token 以 in JSON at position 0

エラーメッセージ

javascriptSyntaxError: Unexpected token 以 in JSON at position 0
    at JSON.parse (<anonymous>)
    at parseMistralResponse (api.ts:42)

発生条件

  • LLM が JSON の前後に説明文を追加した
  • プロンプトが曖昧で、JSON のみを返すという指示が伝わっていない

解決方法

  1. プロンプトに「JSON のみを返してください」と明示する
  2. 正規表現で JSON 部分のみを抽出する
  3. response_format パラメータで JSON mode を指定する

問題の構造を図解

以下の状態図は、JSON 破綻がどのように発生するかを示しています。

mermaidstateDiagram-v2
  [*] --> prompt_sent: プロンプト送信
  prompt_sent --> generating: 生成中
  generating --> json_complete: JSON 完成
  generating --> json_broken: JSON 破綻
  json_complete --> parse_success: パース成功
  json_broken --> parse_error: パースエラー
  parse_error --> retry: 再試行
  retry --> generating
  parse_success --> [*]

このように、JSON 破綻はランダムに発生するため、再試行やリカバリの仕組みが不可欠です。

解決策

1. 出力拘束(JSON Mode)の活用

Mistral API には、JSON 形式での出力を強制する response_format パラメータがあります。

これを使うことで、LLM が JSON 以外のテキストを返すリスクを大幅に減らせますよ。

response_format の基本設定

以下は、Mistral API で JSON mode を有効にする基本的な設定です。

typescriptimport MistralClient from '@mistralai/mistralai';

次に、クライアントを初期化します。

typescriptconst client = new MistralClient(
  process.env.MISTRAL_API_KEY || ''
);

そして、JSON mode を指定してリクエストを送ります。

typescriptconst response = await client.chat({
  model: 'mistral-small-latest',
  messages: [
    {
      role: 'user',
      content: 'ユーザー名と年齢を JSON で返してください',
    },
  ],
  response_format: { type: 'json_object' },
});

ポイント

  • response_format: { type: 'json_object' } を指定することで、JSON のみが返される
  • プロンプトには「JSON で返してください」という指示を含めるのがベストプラクティス

プロンプト設計のコツ

JSON mode を使う場合でも、プロンプトの設計は重要です。

typescriptconst prompt = `
以下の情報から、ユーザー情報を JSON 形式で抽出してください。

情報:田中太郎さんは30歳です。

出力形式:
{
  "name": "string",
  "age": number
}
`;

このように、期待する JSON の構造を例示することで、より正確な出力が得られます。

2. 再試行ロジックの実装

出力拘束を使っても、ネットワークエラーやトークン制限によって JSON が破綻することがあります。

そこで、再試行(Retry)ロジックを実装することが推奨されます。

基本的な再試行関数

まず、再試行の型定義を行います。

typescripttype RetryOptions = {
  maxRetries: number;
  delayMs: number;
};

次に、再試行ロジックを持つ関数を作ります。

typescriptasync function fetchWithRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = { maxRetries: 3, delayMs: 1000 }
): Promise<T> {
  let lastError: Error;

再試行ループを実装します。

typescript  for (let i = 0; i < options.maxRetries; i++) {
    try {
      return await fn(); // 成功したら即座に返す
    } catch (error) {
      lastError = error as Error;
      console.warn(`試行 ${i + 1} 失敗: ${lastError.message}`);

最後の試行でなければ、待機してから再試行します。

typescript      if (i < options.maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, options.delayMs));
      }
    }
  }
  throw lastError!; // 最終的に失敗したらエラーをスロー
}

再試行の使用例

この関数を Mistral API 呼び出しに適用します。

typescriptconst result = await fetchWithRetry(async () => {
  const response = await client.chat({
    model: 'mistral-small-latest',
    messages: [
      { role: 'user', content: 'JSON で返してください' },
    ],
    response_format: { type: 'json_object' },
  });
  return JSON.parse(response.choices[0].message.content);
});

再試行のメリット

  • 一時的なネットワークエラーに対応できる
  • トークン制限による途中終了をリトライで回避できる
  • ユーザー体験の向上(エラー画面を見せずに済む)

3. 検証とリカバリの仕組み

JSON がパースできても、スキーマが期待通りでない場合があります。

そこで、Zod などのスキーマ検証ライブラリを使って、データの整合性を保証します。

Zod によるスキーマ定義

まず、Zod をインストールします。

bashyarn add zod

次に、期待する JSON のスキーマを定義します。

typescriptimport { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  age: z.number().int().positive(),
});

型を生成します。

typescripttype User = z.infer<typeof UserSchema>;

パース&検証関数

以下は、JSON パースと Zod 検証を組み合わせた関数です。

typescriptfunction parseAndValidate(jsonString: string): User {
  // まず JSON としてパース
  const raw = JSON.parse(jsonString);

Zod でスキーマ検証を行います。

typescript// Zod でスキーマ検証
const result = UserSchema.safeParse(raw);

検証に失敗した場合は、エラーをスローします。

typescript  if (!result.success) {
    throw new Error(`検証エラー: ${result.error.message}`);
  }
  return result.data;
}

リカバリ処理の例

検証に失敗した場合、デフォルト値を返すリカバリ処理も考えられます。

typescriptfunction parseWithFallback(jsonString: string): User {
  try {
    return parseAndValidate(jsonString);
  } catch (error) {
    console.error(
      'パース失敗、デフォルト値を返します',
      error
    );
    return { name: '不明', age: 0 }; // フォールバック
  }
}

このように、検証とリカバリを組み合わせることで、堅牢な処理が実現できますね。

解決策の全体フロー

以下の図は、出力拘束・再試行・検証の 3 つを組み合わせたフローを示しています。

mermaidflowchart TD
  start["開始"] --> send["Mistral にリクエスト<br/>(JSON mode)"]
  send --> receive["レスポンス受信"]
  receive --> parse["JSON パース"]
  parse -->|成功| validate["Zod 検証"]
  parse -->|失敗| retry_check1["再試行回数<br/>チェック"]
  validate -->|成功| done["完了"]
  validate -->|失敗| retry_check2["再試行回数<br/>チェック"]
  retry_check1 -->|上限未満| send
  retry_check1 -->|上限到達| fallback["フォールバック値返却"]
  retry_check2 -->|上限未満| send
  retry_check2 -->|上限到達| fallback
  fallback --> task_end["終了"]
  done --> task_end

図で理解できる要点:

  • JSON mode でリクエストを送信し、パースと検証を順次実行
  • 失敗時は再試行回数をチェックし、上限に達していなければ再送
  • 最終的に失敗した場合は、フォールバック値を返してアプリケーションの継続性を保つ

具体例

ユースケース:ユーザー情報抽出 API

ここでは、Mistral を使ってテキストからユーザー情報を抽出し、JSON で返す API を作ります。

プロジェクト構成

bashyarn init -y
yarn add @mistralai/mistralai zod
yarn add -D typescript @types/node tsx

型定義ファイル

まず、型とスキーマを定義します。

typescript// types.ts
import { z } from 'zod';

export const UserSchema = z.object({
  name: z.string().min(1),
  age: z.number().int().min(0).max(150),
});

export type User = z.infer<typeof UserSchema>;

再試行ユーティリティ

再試行関数を別ファイルに切り出します。

typescript// retry.ts
export type RetryOptions = {
  maxRetries: number;
  delayMs: number;
};

再試行ロジックを実装します。

typescriptexport async function fetchWithRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = { maxRetries: 3, delayMs: 1000 }
): Promise<T> {
  let lastError: Error;
  for (let i = 0; i < options.maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      if (i < options.maxRetries - 1) {
        await new Promise((r) =>
          setTimeout(r, options.delayMs)
        );
      }
    }
  }
  throw lastError!;
}

Mistral クライアント

Mistral API を呼び出すクライアントを作成します。

typescript// mistral.ts
import MistralClient from '@mistralai/mistralai';

export const client = new MistralClient(
  process.env.MISTRAL_API_KEY || ''
);

JSON mode でユーザー情報を抽出する関数を実装します。

typescriptexport async function extractUserInfo(text: string): Promise<string> {
  const response = await client.chat({
    model: 'mistral-small-latest',
    messages: [
      {
        role: 'user',
        content: `以下のテキストからユーザー情報を抽出し、JSON 形式で返してください。

テキスト:${text}

出力形式:
{
  "name": "string",
  "age": number
}
`
      }
    ],
    response_format: { type: 'json_object' }
  });

レスポンスから JSON 文字列を取り出します。

typescript  return response.choices[0].message.content;
}

メイン処理

すべてを統合したメイン処理です。

typescript// main.ts
import { UserSchema, User } from './types';
import { fetchWithRetry } from './retry';
import { extractUserInfo } from './mistral';

パースと検証を行う関数を定義します。

typescriptfunction parseAndValidate(jsonString: string): User {
  const raw = JSON.parse(jsonString);
  const result = UserSchema.safeParse(raw);
  if (!result.success) {
    throw new Error(
      `Validation error: ${result.error.message}`
    );
  }
  return result.data;
}

メイン関数を実装します。

typescriptasync function main() {
  const text = '山田花子さんは25歳です。';

再試行ロジックを使って API を呼び出します。

typescript  try {
    const user = await fetchWithRetry(async () => {
      const jsonString = await extractUserInfo(text);
      return parseAndValidate(jsonString);
    });

成功時の処理を記述します。

typescript    console.log('抽出成功:', user);
    // { name: '山田花子', age: 25 }
  } catch (error) {
    console.error('最終的に失敗しました:', error);
  }
}

main();

実行とデバッグ

実行には以下のコマンドを使います。

bashMISTRAL_API_KEY=your_api_key_here npx tsx main.ts

成功時の出力例

css抽出成功: { name: '山田花子', age: 25 }

失敗時の出力例(再試行ログ含む)

lua試行 1 失敗: Unexpected token 以 in JSON at position 0
試行 2 失敗: Unexpected end of JSON input
試行 3 失敗: Validation error: age must be positive
最終的に失敗しました: Error: Validation error: age must be positive

エラーハンドリングのベストプラクティス

以下の表は、エラーの種類ごとに推奨される対処法をまとめたものです。

#エラーの種類推奨対処法優先度
1JSON パースエラー再試行 + JSON mode★★★
2スキーマ検証エラーZod 検証 + フォールバック★★☆
3ネットワークエラー再試行 + タイムアウト設定★★★
4トークン制限超過max_tokens 増加 + 再試行★★☆

応用:複数フィールドの抽出

より複雑なスキーマにも対応できます。

typescriptconst OrderSchema = z.object({
  orderId: z.string().uuid(),
  items: z.array(
    z.object({
      productId: z.number(),
      quantity: z.number().positive(),
    })
  ),
  total: z.number().positive(),
});

このように、ネストした JSON でも Zod で厳密に検証できますよ。

まとめ

この記事では、Mistral が JSON 破綻する際の対処法を、出力拘束・再試行・検証リカバリの 3 つの観点から解説しました。

重要なポイントをまとめますね。

#対処法効果実装の難易度
1JSON mode の使用JSON 以外のテキスト混入を防ぐ★☆☆
2再試行ロジック一時的なエラーに対応★★☆
3Zod によるスキーマ検証データの整合性を保証★★☆
4フォールバック処理アプリケーションの継続性を確保★☆☆

これらの手法を組み合わせることで、LLM の出力が不安定でも、堅牢なアプリケーションを構築できます。

特に、本番環境では再試行とスキーマ検証を必ず実装することをおすすめしますね。JSON の破綻は予測不可能なため、事前の対策が成功の鍵となります。

Mistral を使った開発がより安定し、ユーザーに価値を届けられることを願っています。

関連リンク