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 の途中でトークン制限に達した
- 閉じ括弧やクォートが不足している
解決方法
max_tokensパラメータを増やす- 再試行ロジックを実装する
- 出力拘束(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 のみを返すという指示が伝わっていない
解決方法
- プロンプトに「JSON のみを返してください」と明示する
- 正規表現で JSON 部分のみを抽出する
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
エラーハンドリングのベストプラクティス
以下の表は、エラーの種類ごとに推奨される対処法をまとめたものです。
| # | エラーの種類 | 推奨対処法 | 優先度 |
|---|---|---|---|
| 1 | JSON パースエラー | 再試行 + 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 つの観点から解説しました。
重要なポイントをまとめますね。
| # | 対処法 | 効果 | 実装の難易度 |
|---|---|---|---|
| 1 | JSON mode の使用 | JSON 以外のテキスト混入を防ぐ | ★☆☆ |
| 2 | 再試行ロジック | 一時的なエラーに対応 | ★★☆ |
| 3 | Zod によるスキーマ検証 | データの整合性を保証 | ★★☆ |
| 4 | フォールバック処理 | アプリケーションの継続性を確保 | ★☆☆ |
これらの手法を組み合わせることで、LLM の出力が不安定でも、堅牢なアプリケーションを構築できます。
特に、本番環境では再試行とスキーマ検証を必ず実装することをおすすめしますね。JSON の破綻は予測不可能なため、事前の対策が成功の鍵となります。
Mistral を使った開発がより安定し、ユーザーに価値を届けられることを願っています。
関連リンク
articleMistral が JSON 破綻する時の対処:出力拘束・再試行・検証リカバリ
articleMistral vs GPT/Claude 比較:和文要約・指示遵守・コストの総合評価
articleMistral 使い方入門:要約・説明・翻訳・書き換えの基礎プロンプト 20 連発
articleMistral で作る RAG 設計 7 パターン:分割・埋め込み・再ランク・圧縮
articleMistral の始め方:API キー発行から最初のテキスト生成まで 5 分クイックスタート
articleMistral とは? 軽量・高速・高品質を両立する次世代 LLM の全体像
articleNode.js で GraphQL サーバー構築:Yoga/Apollo を最小構成で立ち上げる
articleMistral が JSON 破綻する時の対処:出力拘束・再試行・検証リカバリ
articleNext.js の キャッシュ無効化設計:タグ・パス・スケジュールの 3 軸でコントロール
articleJavaScript Web Crypto 実務入門:署名・ハッシュ・暗号化の使い分け
articlehtmx で実現するハイパーメディア設計:リンクとフォームを API として扱う
articleHomebrew Cask 早見表:install/upgrade/uninstall/zap の使い分け一発理解
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来