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段構えの防衛線
- 正規表現による抽出: Markdownコードブロックの除去
- 文字列サニタイズ: 制御文字のエスケープと禁止文字削除
- 構造修復: スタックマシンによる括弧の補完
実装コード解説
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開発者の皆さんはぜひ自前の修復関数を育ててみてください。