開発者は、LLM(大規模言語モデル)が生成する不正なJSONの修正コストを、リトライ回数を2回に制限することで抑えることができます。簡単なPythonループがそれを示しています。「生成、パース、検証、リトライ、そして受理または失敗」というパターンにより、「おそらく有効なJSON」を測定可能な成功率へと変え、トークン使用量の増大を防ぐことができます。

なぜリペアループが重要なのか

LLMには「JSONを返して」と指示されることが多いですが、出力には構文エラー、キーの欠落、あるいはビジネスルールに反する値が含まれることがよくあります。厳格なペイロードを期待するダウンストリームシステムは、そのような出力が渡されるとクラッシュしたり、誤った結果を出力したりします。これらの問題を検出し修正する体系的な方法がなければ、チームは高い失敗率を受け入れるか、正しくなるまでモデルに繰り返しプロンプトを送り続け、コストを浪費することになります。

3つの検証レイヤー

信頼性の高いループでは、検証を以下の3つに分類します。

  • 構文 (Syntax) – 文字列がJSONとしてパースできるか?
  • 構造 (Shape) – トップレベルのオブジェクトに、期待通りのキーが正確に含まれているか?
  • 意味論 (Semantic) – 値がドメインの制約に従っているか(例:「category」が定義済みのセットに含まれているか)?

最初に失敗したレイヤーで停止することで、モデルに正確なエラーメッセージを伝えることができ、次回の試行で正しく修正される確率を劇的に向上させることができます。

最小限のPython例

以下のコードは標準ライブラリのみを使用しています。小さなスキーマ(categorysummary というキーを持つ辞書)と、許可されたカテゴリの固定リストを定義しています。

import json

CATEGORIES = {"bug", "feature", "question"}

def validate(raw: str):
    try:
        value = json.loads(raw)
    except json.JSONDecodeError as exc:
        return None, f"syntax: {exc.msg}"

    if not isinstance(value, dict):
        return None, "shape: root must be an object"
    if set(value) != {"category", "summary"}:
        return None, "shape: expected category and summary"
    if value["category"] not in CATEGORIES:
        return None, "semantic: invalid category"
    if not isinstance(value["summary"], str) or not value["summary"].strip():
        return None, "semantic: summary must be text"

    return value, None

リペアループ自体に、試行回数の上限を設けています。

MAX_ATTEMPTS = 2
raw = model_response   # initial LLM output

for attempt in range(MAX_ATTEMPTS + 1):
    value, error = validate(raw)
    if error is None:
        break
    if attempt == MAX_ATTEMPTS:
        raise RuntimeError(f"failed after {MAX_ATTEMPTS} retries: {error}")

    # Send the error and schema back to the model for a fix
    raw = ask_model_to_repair(raw, error)

JSONが最初のパスで検証を通過すれば、ループは即座に終了します。そうでなければ、元の出力、正確なエラー文字列、および期待されるスキーマという簡潔なペイロードをモデルに送り、リトライします。2回の試行に失敗すると、プロセスは明確な例外とともに中断されます。

コスト効率の高いリペアのための2つのルール

  1. リペア用のプロンプトを簡潔に保つ – 生のJSON、エラーメッセージ、スキーマのみを含めます。長いチャット履歴を追加するとトークン使用量が増大し、モデルを混乱させる可能性があるため、精度を向上させずにリトライあたりのコストを押し上げてしまいます。

  2. 検証とファクトチェックを分離する – パーサーは構造的な問題を検出できますが、「summary(要約)」の内容が真実であるかどうかを検証することはできません。ファクトチェックは後の段階で行うべきです。誤った主張を「修正」しようとしても、単に嘘を綺麗に見せるだけになってしまいます。

成功の測定

各試行のエラータイプ(構文、構造、意味論)とループが成功したかどうかをログに記録することで、具体的な指標が得られます。それは、制限されたリトライ回数内で利用可能になったLLMレスポンスの割合です。チームはこれを継続的に追跡したり、プロンプトのスタイルを比較したり、異なるスキーマ定義を試したりすることができます。

潜在的なデメリット

ループに上限を設けるということは、上限に達した後に一部の不正な出力が破棄されることを意味し、モデル自体の品質が低い場合は全体の失敗率が上がる可能性があります。すべてのレスポンスが重要な環境では、開発者は追加のトークンを犠牲にしてでも、より高いリトライ上限を好むかもしれません。トレードオフは明確です。カバー率を高めるためにコストをかけるか、予測可能な上限を設けて予算を抑えるかです。

次に注目すべき点

  • リペアのためのプロンプトエンジニアリング – エラーメッセージの形式を洗練させることで、必要なリトライ回数を減らせる可能性があります。
  • 代替のパーサー – 一部のライブラリは、軽微な問題を自動修正できる寛容なパース機能を提供しています。それらのコストと、上限付きループのコストを比較する価値はあります。
  • モデルのアップデート – 新しいバージョンのLLMは、最初からより綺麗なJSONを生成する可能性があるため、リトライ制限をさらに下げられる可能性があります。

上限付きのJSONリペアループは、コストを急増させることなく、曖昧なLLMの出力を信頼できるデータに変えるための実用的なツールを開発者に提供します。検証を重要なステップとして扱い、リトライ回数を制限することで、チームは予算とダウンストリームシステムの両方を健全に保つことができます。