LLMの不安定なJSONレスポンスを絶対にねじ伏せる「堅牢パースロジック」の実装

はじめに

AI機能を組み込んだアプリ開発で最も頭を悩ませるのが、LLMからのレスポンス処理です。「必ずJSONで返して」とSystem Promptで懇願しても、AIは平気で裏切ります。

  • 末尾に余計なカンマをつける [1, 2, 3,]
  • 文字列中の改行コードをエスケープし忘れる "text": "Hello World"
  • トークン制限でJSONが途中で切れる {"key": "val

JSON.parse() はこれらに対して無慈悲にエラーを吐いて死にます。 本記事では、Google Gemini / Claude / GPT などのLLM APIを利用する際に必須となる、「壊れたJSONを意地でもパースする」 実装テクニックを紹介します。

戦略: 3段構えの防衛線

  1. 正規表現による抽出: Markdownコードブロックの除去
  2. 文字列サニタイズ: 制御文字のエスケープと禁止文字削除
  3. 構造修復: スタックマシンによる括弧の補完

実装コード解説

1. JSON部分の抽出

LLMは親切心で ```json ... ``` で囲んで返してくることが多いですが、JSON.parse にとっては邪魔です。

typescript
// prompt.ts
export function parseJSON<T>(text: string): T {
  let jsonStr = text;
  // Markdownコードブロック記法への対応
  const codeBlockMatch = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/);
  if (codeBlockMatch) {
    jsonStr = codeBlockMatch[1];
  }
  // 最初の { から最後の } までを貪欲に取得
  const jsonMatch = jsonStr.match(/\\{[\\s\\S]*\\}/);
  if (!jsonMatch) throw new Error('No JSON found');

  return JSON.parse(jsonMatch[0]) as T;
}

2. 配列内の改行コード問題(これにハマった)

特に厄介なのが、配列や長い文章の中に「生の改行コード」が含まれるケースです。

json
{
  "strengths": [
    "箇条書きが
    途中で改行されている"
  ]
}

これは仕様違反のJSONですが、AIは頻繁にこれをやります。正規表現での修正は限界があるため、状態を持つパーサー(ステートマシン)で修正します。

typescript
function escapeControlCharsInJsonString(jsonStr: string): string {
  let inString = false;
  let result = '';
  for (let i = 0; i < jsonStr.length; i++) {
    const char = jsonStr[i];
    // エスケープされていないダブルクォートを検出して状態反転
    if (char === '"' && (i === 0 || jsonStr[i - 1] !== '\\\\')) {
      inString = !inString;
      result += char;
    } else if (inString) {
      // 文字列内にある制御文字だけをエスケープする
      if (char === '\\n') result += '\\\\n';
      else if (char === '\\r') result += '\\\\r';
      else if (char === '\\t') result += '\\\\t';
      else result += char;
    } else {
      result += char;
    }
  }
  return result;
}

3. トークン切れ対策(括弧の自動補完)

max_tokens に引っかかってJSONが途中で終わってしまった場合も、そこまでのデータだけでも救出したい場合があります。

typescript
      // 1. 開いている文字列を閉じる
      if ((cleanJson.match(/"/g) || []).length % 2 !== 0) {
        cleanJson += '"';
      }

      // 2. 開いている括弧をスタックで管理して閉じる
      const stack = [];
      for (const char of cleanJson) {
        if (char === '{') stack.push('}');
        else if (char === '[') stack.push(']');
        else if (char === '}' || char === ']') {
          const expected = stack[stack.length - 1];
          if (char === expected) stack.pop();
        }
      }
      // スタックに残っている閉じ括弧を逆順に追加
      while (stack.length > 0) {
        cleanJson += stack.pop();
      }

まとめ

「プロンプトエンジニアリングでJSONを出力させる」だけでなく、「受け取る側のコードで泥臭くカバーする」ことで、アプリの安定性は劇的に向上します。 特に escapeControlCharsInJsonString のような処理を挟むだけで、パースエラーの8割は防げます。AI開発者の皆さんはぜひ自前の修復関数を育ててみてください。