0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

推論モデルの<think>の中のJSONを「答え」として拾ってしまう事故と、標準ライブラリだけの対策

0
Posted at

この記事で分かること

  • 推論モデルの出力からJSONを取り出すとき、エラーにならないのに、間違った値が返ってくることがある
  • その原因と、手元で10秒で再現できるコード
  • 標準ライブラリだけで書ける対策(そのままコピーして使えます)

はじめに

LLMの出力からJSONを取り出すとき、json.loads がエラーを出してくれるなら、まだ話は簡単です。困るのは、エラーにならずに、間違ったJSONがすっと返ってくるときです。

推論モデル(考える過程を <think>...</think> のようなタグで出力するモデル)を使い始めてから、この落とし穴を踏みやすくなりました。この記事では、実際に動かして確かめた結果を見ながら、原因と対策を整理します。

私は専門のプログラマーではなく、AI(Claude)に手伝ってもらいながら、壊れたJSONを直すAPIを作っています。その改良の途中で、この問題が見つかりました。

何が起きるのか

推論モデルの中には、答えの前に「考えた内容」を出力するものがあります。その考えの中に、JSONの下書きが混ざることがあります。

<think>
ユーザーは {"name": "...", "age": ...} の形式を求めている。例: {"x": 1}
</think>
{"name": "Ann", "age": 30}

(モデルや配信の方法によっては、考えが別のフィールドで返り、本文には混ざらないこともあります。ここでは、本文に混ざって届く場合の話をします。)

まず、よくある取り出し方を試す

「最初の { から最後の } まで」を切り出す方法です。

text[text.find("{"): text.rfind("}") + 1]

結果は JSONDecodeError でした。考えの中の {...} と、答えの {...} が、ひとつながりで切り出されてしまうためです。この場合は、少なくともエラーで気づけます。

次に、修復ライブラリを試す

JSONの壊れを直してくれるライブラリ(json-repair 0.63.5)に渡すと、今度はエラーになりません。

from json_repair import repair_json
repair_json(text, return_objects=True)
[{'name': '...', 'age': '...'}, {'x': 1}, {'name': 'Ann', 'age': 30}]

3つのJSONが、リストになって全部返ってきます。 エラーが出ないので、気づかないまま先へ進んでしまいます。スキーマで検証していれば「オブジェクトのはずが、リストになっている」と気づけますが、検証していなければ素通りです。

さらに危ないのは、考えの途中で切れたとき

トークン上限などで、考えている途中で出力が止まることがあります。

<think>うーん、{"name": "Ann"} かな

同じライブラリに渡すと、こうなります。

{'name': 'Ann'}

まだ答えを出していない段階の下書きが、完成した答えとして返ってきます。 値がたまたま合っていることも多いので、気づくのが遅れます。

手元で試すなら、次の2行で再現できます。

pip install json-repair
python -c "from json_repair import repair_json; print(repair_json('<think>うーん、{\"name\": \"Ann\"} かな', return_objects=True))"

対策は「考えを先に捨てる」

方針は2つだけです。

  1. JSONを探す前に、<think> ブロックを丸ごと捨てる(閉じていないものも含めて)。
  2. 残った文章から、最後に完全な形で読めたJSONを採用する。

標準ライブラリだけで書くと、次のようになります。

import json
import re

THINK_BLOCK = re.compile(r"<(think|thinking|reasoning)\b[^>]*>.*?</\1\s*>", re.S | re.I)
THINK_OPEN = re.compile(r"<(think|thinking|reasoning)\b[^>]*>.*\Z", re.S | re.I)


def extract_json(text: str):
    """推論モデルの出力から、最後に見つかった「完全なJSON」を返す。"""
    text = THINK_BLOCK.sub("", text)   # 閉じた思考ブロックを捨てる
    text = THINK_OPEN.sub("", text)    # 閉じ忘れ(途中で切れた思考)も捨てる
    dec = json.JSONDecoder()
    found, i = None, 0
    while i < len(text):
        if text[i] in "{[":
            try:
                found, end = dec.raw_decode(text, i)
                i = end                # 読めた範囲は飛ばす(入れ子の中身を拾わない)
                continue
            except json.JSONDecodeError:
                pass
        i += 1
    return found

ポイントは raw_decode です。「ここからJSONとして読めるか」を試して、読めた長さまで教えてくれます。そのため、前置きや後書きの文章が混ざっていても、JSONの部分だけを取り出せます。

動作確認

入力 結果
思考に下書きのJSONがある 答えのJSONだけを返す
前置き「こちらです:」と後書きがある JSONだけを返す
Markdownのコードブロック(json)で囲まれている JSONだけを返す
入れ子のJSON 外側のJSON全体を返す
途中で切れたJSON None
思考が閉じていない None

(実際に実行した結果です。)

大事なのは、最後の2行です。答えが出ていないときは、推測で埋めずに None を返します。 呼び出し側で「失敗」として扱えるので、静かな誤りを防げます。

この関数だけでは直せないもの

次のようなものは、この関数では直せません。

  • 末尾カンマ {"a": 1,}
  • シングルクォート {'a': 1}
  • 途中で切れたJSON {"a": 1

こうした構文の崩れは、先ほどの json-repair のようなライブラリの出番です。ただし、前に見たとおり、そのまま渡すと考えの中のJSONを拾ってしまうので、順番が大切です。

text = THINK_BLOCK.sub("", text)       # まず思考を捨てる
text = THINK_OPEN.sub("", text)
data = repair_json(text, return_objects=True)   # そのあと構文を直す

「考えを捨てる → 構文を直す → スキーマで検証する」の順にすると、安全です。

設計で気をつけたいこと

  • 直せなかったことを、成功として返さない:None や例外で返して、呼び出し側が判断できるようにします。
  • 何を捨てたかを記録する:ログに「思考ブロックを1つ除去」と残しておくと、あとで原因を追いやすくなります。
  • スキーマで検証する:「リストで返ってきた」「必須項目がない」といった問題を、最後の砦として見つけられます。

なお、私自身はこの分野の専門家ではありません。この記事のコードと実行結果は、AIと一緒に実際に動かして確かめたものです。モデルや配信の方法によって出力の形は変わるので、お使いの環境では結果が違うかもしれません。

おわりに

推論モデルは便利ですが、出力が「答えだけ」ではなくなる点には注意が必要でした。考えを先に捨てる、という小さな前処理だけで、静かな誤りの多くは防げます。

ほかの壊れ方や、もっと良い対策をご存じでしたら、コメントで教えていただけるとうれしいです。

この記事はAI(Claude)と一緒に書きました。コードはAIが実際に実行し、結果を確認しています。


この記事は、Zennに投稿した内容をQiita向けに掲載したものです。
元記事:https://zenn.dev/tonbifun/articles/ed67a9f4526c7e

補足:壊れたJSON・SQL・Mermaidを自動で直すAPI(FixMyシリーズ)も、RapidAPIで公開しています。思考ブロックの除去は、2026年10月のv0.2で取り込みました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?