ブログに戻る

APIとSDKガイド

LLM APIから有効なJSONを取得する:response_formatと再試行

パース可能なJSONを返すコピー可能なPythonリクエスト、空または途中で切れた回答を処理するjson.loadsラッパー、JSONが壊れる理由のチェックリスト。

投稿者: CheapTokens Team公開日 読了時間 7 分

response_format json_objectを含むチャット完了リクエストを示すターミナル
このページの内容
  1. 作業内容:メールを入力し、行を出力する
  2. フラグよりも例が重要な理由
  3. 安全にパースする:空、途中で切れた、または単に壊れている
  4. 構文だけでなく形を確認する
  5. メールのバッチにかかるコスト
  6. クイックチェック

チャットモデルからJSONを引き出すには、3つのことを行います。response_format={"type": "json_object"}を渡し、プロンプトに「json」という単語と構造の例を入れ、オブジェクト全体が収まるようにmax_tokensを十分に大きく設定します。そして、答えを盲信してはいけません。空でないことを確認し、finish_reasonを確認し、それから初めてjson.loadsを呼び出します。

これが短い説明です。このガイドの残りは、非常によくある作業、つまりメールからフィールドを抽出するための完全なスクリプトです。例ではベースURL https://api.cheaptokens.dev/v1 とモデルID deepseek-ai/DeepSeek-V4-Flash-0731 を使用しています(ドキュメント)。

作業内容:メールを入力し、行を出力する#

スクリプトがサポートメールを読み取り、それぞれをレコードに変換する必要があるとします。送信者、日付、金額、送信者の要望です。次のメールを見てください。

こんにちは、Lumen Ltd.のAnna Petrovaです。10月14日に請求書#3381に1,250 EURを支払いましたが、領収書はまだ未払いと表示されています。金曜日までに修正してください。

次のものを、それ以外は何も返してほしくありません。

json
{"sender": "Anna Petrova", "date": "2026-10-14", "amount": 1250, "currency": "EUR", "action": "fix receipt status"}

単純な「フィールドを抽出して」というプロンプトでは、親しみやすい文章、コードフェンス、またはその両方が返ってくる可能性があります。あなたのjson.loadsは最初のバッククォートで失敗します。これがresponse_formatと適切なプロンプトが一緒に解決する問題です。

Pythonとcurlでの同じリクエスト

Python

python
import json
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.cheaptokens.dev/v1",
    api_key=os.environ["CHEAPTOKENS_API_KEY"],
)
MODEL = "deepseek-ai/DeepSeek-V4-Flash-0731"

SYSTEM = """Extract fields from the email and answer with json only.
Example of the json format:
{"sender": "Jane Roe", "date": "2026-01-31", "amount": 99.5, "currency": "USD", "action": "short phrase"}
Use null for a field that is not in the email."""

resp = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": email_text},
    ],
    response_format={"type": "json_object"},
    max_tokens=1000,
    temperature=0,
)
print(resp.choices[0].message.content)

curl

bash
curl https://api.cheaptokens.dev/v1/chat/completions \
  -H "Authorization: Bearer $CHEAPTOKENS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-ai/DeepSeek-V4-Flash-0731",
    "messages": [
      {"role": "system", "content": "Extract fields from the email and answer with json only. Example: {\"sender\": \"Jane Roe\", \"amount\": 99.5}"},
      {"role": "user", "content": "Hi, this is Anna Petrova from Lumen Ltd. I paid invoice #3381 for 1,250 EUR..."}
    ],
    "response_format": {"type": "json_object"},
    "max_tokens": 1000
  }'

JSON Outputに関するDeepSeek APIリファレンス(2026年10月27日確認、ページに公開日なし)には、このリクエストが従うルールが記載されています。response_formatをjson_objectに設定し、システムまたはユーザープロンプトのどこかに「json」という単語を入れ、対象構造の例を含めます。例がないと、モデルはフィールド名を推測する必要があり、次のメールでは異なる推測をします。

フラグよりも例が重要な理由#

response_formatはAPIにJSONオブジェクトを返すよう要求します。どのオブジェクトかをモデルに伝えるわけではありません。同僚に「スプレッドシートを送って」と言い、どの列かを言わないようなものです。スプレッドシートは得られますが、あなたのものではありません。

プロンプト内の例は列ヘッダー行です。2つの習慣がよりうまく機能させます。

  • 型を含む実際の形を示す:1250は数値、"2026-10-14"は固定日付形式の文字列です。
  • フィールドが欠けている場合の対処を指定する(上記のnull)。そうしないと、モデルは隙間を埋めるために値をでっち上げます。

response_formatは強いヒントとして扱い、保証とは考えないでください。モデルは時々空文字列を返したり、オブジェクトを文章で包んだりします。だから次の部分が存在します。

安全にパースする:空、途中で切れた、または単に壊れている#

実際には3つの失敗が現れ、それぞれ異なる痕跡を残します。

  • 空のコンテンツ。 DeepSeekのドキュメントによると、JSONモードは時々空のコンテンツを返すことがあり、プロンプトを言い換えることを勧めています。あなた側では、message.contentが""またはNoneであり、json.loads("")がExpecting value: line 1 column 1 (char 0)を発生させるように見えます。
  • 途中で切れた。 max_tokensがオブジェクトの途中で尽きました。finish_reasonはlengthで、テキストは文字列の途中で終わります。詳細はAIの回答が途中で止まる理由をご覧ください。
  • JSONではない。 オブジェクトの前の文章、またはそれを囲むフェンス。
空の回答に対するjson.loadsはJSONDecodeErrorを発生させる
空のJSONモード回答がPythonでどのように見えるか

1番目と3番目の修正は、より厳しいプロンプトでの再試行です。2番目については、同じ制限での再試行は何も変えないので、ラッパーは例外を発生させ、max_tokensを上げるよう伝えます。

python
def ask_json(email_text: str, retries: int = 1) -> dict:
    user = email_text
    for attempt in range(retries + 1):
        resp = client.chat.completions.create(
            model=MODEL,
            messages=[
                {"role": "system", "content": SYSTEM},
                {"role": "user", "content": user},
            ],
            response_format={"type": "json_object"},
            max_tokens=1000,
            temperature=0,
        )
        choice = resp.choices[0]
        if choice.finish_reason == "length":
            raise ValueError("JSON cut off by max_tokens: raise the limit")
        text = (choice.message.content or "").strip()
        if text:
            try:
                return json.loads(text)
            except json.JSONDecodeError:
                pass
        # empty or not JSON: ask again, more firmly
        user = email_text + "\n\nReply with one json object only, in the format of the example."
    raise ValueError("no valid JSON after retry")

スクリプトには1回の再試行で十分です。2回失敗したら、メールIDをログに記録して次に進みます。3回目の同一リクエストはほとんど役に立ちません。

ペアを合わせる

どの症状がどの原因を指すか?

  • finish_reasonが「length」max_tokensがオブジェクトに対して小さすぎる
  • contentが空文字列JSONモードが何も返さなかった。プロンプトを言い換えて再試行する
  • バッククォートである文字でのJSONDecodeErrorモデルがJSONをコードフェンスで包んだ
  • メール間でフィールド名が異なるプロンプトに構造の例がない

構文だけでなく形を確認する#

json.loadsはテキストが有効なJSONであることだけを証明します。要求したフィールドがあることは証明しません。パース直後に安価なチェックを追加します。

python
REQUIRED = {"sender", "date", "amount", "currency", "action"}

data = ask_json(email_text)
missing = REQUIRED - data.keys()
if missing:
    raise ValueError(f"missing fields: {sorted(missing)}")

ここからdictはデータベース行またはスプレッドシートに直接入ります。固定レコードを埋める代わりにモデルに独自の関数を呼び出させたい場合は、別のツールです。Pythonでの関数呼び出しをご覧ください。

メールからチェック済みレコードへの経路:例付きプロンプト、json_object、パース、フィールドチェック
5ステップの全体経路

メールのバッチにかかるコスト#

短い抽出は安価な種類のリクエストです。入力は数百トークン、出力は数十トークンです。1日1,000通のメールで、入力約400トークン(プロンプト、例、メール)と出力約80トークン(JSON)と仮定します。DeepSeek-V4-Flashでは1日あたり$0.014で、表は他のモデルでの同じ負荷を示しています。

モデル リクエストあたり 1,000 リクエスト OpenRouter、1,000 リクエスト
GLM-5.3-Flash $0.000011 $0.0112 $0.0226
DeepSeek-V4-Flash $0.000014 $0.014 $0.0315
MiniMax-M2.7 $0.000032 $0.032 $0.216

リクエストあたり 400 入力トークンと 80 出力トークン。100万トークンあたりの価格は現行のもので、当社の料金体系に合わせて変動します。OpenRouter 列は同じワークロードの参考価格を示しています。

合計で、入力と出力を合わせて数えると、DeepSeek-V4-Flashはこの負荷に対してOpenRouterの参照価格より安くなります。また、回答あたり最大32,768出力トークンを許可するので、長い明細リストが小さな制限のように途中で切れることはなく、価格はいつでも同じです。GLM-5.3-Flash(zai-org/GLM-5.3-Flash)は同じAPIアドレスでの別の選択肢です。最大8,192出力トークンで、メールごとに1レコードには十分です。現在の価格は価格ページにあり、DeepSeekモデルページにここで使用したモデルの詳細があります。

クイックチェック#

クイックチェック

JSONが文字列の途中で切れています。最初に何を見ますか?

答えを表示

答え: finish_reasonとmax_tokens

切れたオブジェクトはfinish_reasonがlengthに設定されて終わります。制限に達したので、max_tokensを上げてください。温度やアドレスを変えても役に立ちません。

まだリクエストを送っていない場合は、最初のAPIリクエストから始めて、戻って上記のコードに置き換えてください。