第2部 プロンプト · 第4回 / 全20回 · 15分くらい
出力の形を決めて、後ろの処理との約束にする
人が読む答えなら、多少形がずれても困りません。でも、その答えを次の処理が読むなら話は別です。この回では、出力の形がなぜ崩れるのかを仕組みから見て、AIの機能で防げる範囲と、自分のコードで確かめるしかない範囲の境目を引きます。
履修 0 / 20 回
この回を読み終えると、できるようになること
- 構造化出力で防げること・防げないことを、理由とともに3つずつ言える
- 分類や抽出のスキーマを、選択肢・空の値・項目の順番まで含めて設計できる
- 受け取る側の検査を「止まった理由 → 形 → 中身」の3段で書ける
専門用語は、初めて出たところで括弧に短い説明を付けています。説明が長くなるものには ※番号を付け、ページの終わりの用語メモにまとめました。
1なぜ自由な文章の出力は、後ろの処理で壊れるのか
言語モデルは、それまでの文字列を見て、次に来るトークン※1を1つずつ確率で選んでいきます。「JSON※2で返して」と頼んでも、JSON らしいトークンが選ばれやすくなるだけで、JSON 以外のトークンが選べなくなるわけではありません。だから、ふだんは守られても、ときどき次のように崩れます。
| 崩れ方 | 例 | 後ろで起きること |
|---|---|---|
| 前後に余計な文 | 以下がJSONです:{…} | そのまま読むと構文エラー(決まった書き方に合わず、読み込めないこと) |
| コードの囲み | ```json {…} ``` | 同上 |
| 項目名のゆれ | category と Category、カテゴリ | キー(JSON の項目名)が見つからず、空のまま黙って進む |
| 値のゆれ | bug・Bug・バグ・不具合 | 集計で別々に数えられる |
| 途中で切れる | 長さの上限に達して {"items": [{"a":1},{"a | 構文エラー。頼み直しても同じ長さで切れる |
いちばん困るのは3つ目と4つ目です。構文エラーなら処理が止まって気づけますが、項目名や値のゆれはエラーにならずに空振りするので、気づかないうちに壊れます。
2形をそろえる3つの方法と、それぞれの効き方
| 方法 | やり方 | 形が崩れないか |
|---|---|---|
| 指示で頼む | 「次の形の JSON だけを返して」と書く | 保証はない(選ばれやすくなるだけ) |
| 例を見せる | 入力と出力の組を2〜3個見せる | 保証はない(さらに選ばれやすくなる) |
| 構造化出力※3 | API(プログラムからサービスを呼び出す窓口)にスキーマ※4を渡す。Claude なら設定項目 output_config.format に json_schema を、Ollama(手元の Mac で言語モデルを動かすソフト)なら format を使う | 構文・型(文字列・数・真偽値など、値の種類)・必須項目は崩れない |
スキーマを文法に変えて、生成の1歩ごとに「この文法では次に来られないトークン」を候補から外してから選ぶからです。これを制約付きデコーディング※5と呼びます。お願いするのではなく、選べる候補そのものを減らしているので、構文エラーは起きようがありません。
後ろに処理がつながるなら、使える環境では構造化出力を使うのが第一の選択です。ただし、次の節で見るとおり、形が正しいことと中身が正しいことは別の話です。
3構造化出力で防げること、防げないこと
| 防げること | 防げないこと |
|---|---|
| JSON として読める(構文エラーが出ない) | 中身の誤り。bug に分けるべきものを question にしても、形は正しい |
| 必須の項目がそろう | 拒否したとき。止まった理由※6が refusal の応答は、スキーマに合わないことがある |
| 型が合う | 上限で切れたとき。止まった理由が max_tokens なら、途中までしかない |
| 選択肢(enum(選べる値を並べて、それ以外を許さない指定))の外の値が出ない | API が対応していない条件。Claude では、数の範囲(minimum など)や文字数(minLength など)は対応外で、SDK(API を呼ぶための公式のライブラリ)は説明文に移したうえで、受け取ったあとに確かめる |
右の列は、すべて自分のコードで確かめることになります。とくに、止まった理由を見ずにパース(文字列を読み取って、プログラムで扱えるデータに変えること)すると、拒否や打ち切りを普通の答えとして扱ってしまいます。
4スキーマを設計するときの5つの判断
判断1 選択肢は閉じて、逃げ道を1つ用意する。分類の値を自由な文字列にすると、似た名前が毎回増えます。enum で閉じたうえで、other(どれにも当てはまらない)を必ず入れます。逃げ道がないと、AIは当てはまらない入力も、どれかに無理に押し込みます。押し込まれた誤りは形が正しいので、あとから見つけにくくなります。
判断2 わからないときは null にする。値が見つからないときに "" や "不明" を入れさせると、「本当に空」と「見つからなかった」が区別できません。型を ["string", "null"] にして、見つからなければ null(値が無いことを表す特別な値)と決めておきます。
判断3 根拠を先に、判定を後に置く。AIは前から順に書くので、後ろの項目は前の項目を見てから書かれます。根拠の引用を先に書かせれば、判定はその根拠に引っぱられます。逆に判定を先に書かせると、根拠はあとから判定に合わせて選ばれやすくなる、と自分は考えています。
Claude の構造化出力では、出力の項目は「必須の項目が先、任意の項目が後」に並び、それぞれの中ではスキーマに書いた順になります。根拠を先に出したいなら、根拠と判定の両方を必須にして、スキーマで根拠を先に書きます。
判断4 「考えた過程」を書かせる項目は作らない。実践編の最初の版では「考える場所と答える場所を分ける」と書きましたが、これは訂正します。Claude のドキュメントには、順を追った考えを書かせる項目は拒否を招くことがあるので、短い説明を求めるように、と書かれています。考えさせたいときは拡張思考※7に任せ、スキーマには「判定の理由を1文で」くらいの短い項目だけを置きます。
判断5 説明文は、その項目への指示になる。各項目の description(スキーマの項目に添える説明文)は、AIにとってはその欄の書き方の指示です。「原文からそのまま引用する。言い換えない」のように、あとで検査したい条件をここに書いておきます。
{
"type": "object",
"properties": {
"evidence": {
"type": "string",
"description": "判定の根拠になった原文の一部。原文からそのまま引用し、言い換えない"
},
"category": {
"type": "string",
"enum": ["bug", "feature", "question", "other"]
},
"product": {
"type": ["string", "null"],
"description": "書かれている製品名。書かれていなければ null"
}
},
"required": ["evidence", "category", "product"],
"additionalProperties": false
} 読み方:properties は項目の一覧、required は必須の項目、additionalProperties: false は「一覧にない項目を足さない」という指定です(※4)。
5受け取る側は「止まった理由 → 形 → 中身」の3段で確かめる
形はAIの側でそろえられても、防げないことは残ります。受け取る側では、手間の少ないものから順に、3段で確かめます。
import json
CATEGORIES = {"bug", "feature", "question", "other"}
def accept(resp, source_text):
"""(結果, 理由) を返す。結果が None なら使わない(fail-closed)。"""
# 1段目:止まった理由。拒否や打ち切りのときは形が崩れていることがある
if resp.stop_reason != "end_turn":
return None, f"途中で止まった: {resp.stop_reason}"
# 2段目:形。構造化出力でも、対応外の条件はここで確かめる
data = json.loads(resp.content[0].text)
if data["category"] not in CATEGORIES:
return None, "選択肢の外"
# 3段目:中身。根拠の引用が原文に本当にあるか(言い換えや作り話を落とす)
if data["evidence"] not in source_text:
return None, "根拠の引用が原文に見つからない"
return data, "" 読み方:stop_reason は止まった理由(※6)で、end_turn は「最後まで書き終えた」という意味です。json.loads は JSON の文字列をデータに変える関数、A not in B は「A が B の中に文字どおりには含まれていない」という判定です。
63段目がいちばん効く
引用が原文に文字どおりあるかどうかは、コードで確実に判定できます。見つからなければ、その判定は根拠のない推測だった可能性が高いと考えて扱えます。
だめだったときの扱いは、先に決めておきます。理由をつけて1回だけ頼み直し、それでもだめなら使わない(other にする、または人に回す)。このように、迷ったら「使わない」ほうに倒す作り方を fail-closed※8 と呼びます。頼み直しの回数に上限を付けるのは、上限で切れる種類の失敗は、何度頼み直しても同じだからです。
7AIが自分で言う「確信度」は、あてにしない
出力に "confidence": 0.9 のような項目を作り、0.8以上なら採用する、という作りをよく見かけます。ただ、言語モデルに確信度を言葉や数字で答えさせると、実際に当たる割合より高めに出やすいという報告があります(Xiong ほか、2023)。較正※9されていない数字は、判断材料のように見えるぶん、ないより悪いこともあります。
代わりに使えるのは、数えられる手がかりです。
- 逃げ道を選んだか:
otherやnullが選ばれたら人に回す - 根拠が原文にあるか:前の節の3段目
- 何回か聞いて答えがそろうか:同じ入力で3回分類し、割れたものだけ人が見る(費用は3倍になる)
誤解よくある思い違い
構造化出力にすれば、出力は正しくなる
正しくなるのは形だけです。中身の誤りは、形が整っているぶん、かえって目立たなくなります。
温度を0にすれば、毎回同じ答えになる
温度※10を下げればばらつきは減りますが、同じになる保証はありません。後ろの処理は、答えがゆれる前提で作ります。
JSON にしておけば、モデルを替えても後ろはそのまま動く
形が同じでも、分け方のくせはモデルごとに違います。モデルを替えたら、評価セット(第10回)で中身を確かめ直します。
演習手を動かして確かめる
演習1
このスキーマの問題を3つ挙げてください。
{
"type": "object",
"properties": {
"category": { "type": "string" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"reasoning": { "type": "string",
"description": "判定に至るまでの考えを、順を追って詳しく書く" }
}
} 答えを見る
categoryが自由な文字列。enumで閉じ、otherを入れる(判断1)confidenceはAIの自己申告で較正されていない。そのうえminimum・maximumは Claude の構造化出力では対応外で、受け取ったあとの確認が必要。項目ごと外して、数えられる手がかりに替える(7節)reasoningが「順を追って詳しく」。拒否を招くことがあるので、短い理由の項目にするか、拡張思考に任せる(判断4)
ほかにも、required が無い(何も必須になっていない)、additionalProperties: false が無い、根拠の項目が無い、が挙げられていれば十分です。
演習2
この判定を採用しますか。
原文:アプリが落ちることがあります。特に写真を開いたときです。
出力:{"evidence": "アプリが落ちます", "category": "bug", "product": null}
答えを見る
採用しません。判定(bug)そのものはたぶん正しいのですが、evidence が原文の言い換えで、文字どおりの引用になっていないので、3段目の検査で落ちます。
「たぶん正しいから通す」を許すと、検査の意味がなくなります。「原文からそのまま引用して」と理由をつけて1回だけ頼み直し、だめなら使いません。全角と半角、空白の違いだけで落ちるのが困るなら、比べる前に両方を正規化※11しておきます。
演習3
スマホから送ったメモを分類する仕組みに、別のシステムからの障害の知らせ {"content":"⚠️ 定期処理が失敗: Network connection lost."} が混ざって届きます。これをAIに分類させるべきでしょうか。
答えを見る
分類させないほうがよいと考えます。「{"content": …} の形なら中身を取り出す」「⚠️ で始まるならシステムの知らせ」は、決まった規則で判定できます。判断がいらないものは、AIに回す前にコードで振り分けます(第2回の「歯止めの場所」)。
自分の環境では、この知らせが JSON の文字列のままメモとして分類され、TODO に入って1か月以上、毎日リマインドされていました。形が決まっている入力ほど、AIではなくコードの仕事です。
自分は、意味の判断が要るところ(分類・振り分け・抽出)は形を決めた出力に、決まった規則で決まるところは普通のコードに、と分けるようにしています。AIに数えさせたり比べさせたりするのは、コードで済むならやめておくほうが安全でした。
用語メモ本文の ※番号の説明
- ※1トークン AIが文章を扱うときの最小単位。単語や文字の断片で、日本語ではおおむね1〜2文字が1トークンになる。AIは「次の1トークン」を確率で選ぶことをくり返して文章を作る。 本文へ戻る ↩
- ※2JSON データを文字で書き表すときの決まった書き方。
{"名前": "値"}のように、項目名と値を組にして書く。人にもプログラムにも読めるので、プログラム同士の受け渡しによく使われる。 本文へ戻る ↩ - ※3構造化出力(structured outputs) AIの出力を、渡したスキーマどおりの JSON に必ずそろえる API の機能。Claude・OpenAI・Ollama などにある。名前や細かい対応範囲はサービスごとに違う。 本文へ戻る ↩
- ※4スキーマ(JSON Schema) 「この JSON にはどんな項目があり、それぞれどんな型で、どれが必須か」を書いた設計図。設計図そのものも JSON で書く。
properties(項目の一覧)、required(必須の項目)、enum(選べる値の一覧)、additionalProperties: false(一覧にない項目を許さない)などの決まった語を使う。 本文へ戻る ↩ - ※5制約付きデコーディング(constrained decoding) デコーディングは、AIが次のトークンを選んで文章を作っていく処理のこと。その1歩ごとに、決めた文法(ここではスキーマ)に合わないトークンを候補から外してから選ぶやり方を、制約付きデコーディングと呼ぶ。頼むのとは違い、合わない出力がそもそも出てこない。 本文へ戻る ↩
- ※6止まった理由(stop_reason) API の応答に付く「なぜ書くのをやめたか」の値。Claude では
end_turn(書き終えた)、max_tokens(指定した長さの上限で切れた)、refusal(安全上の理由で応じなかった)、tool_use(道具を使うために止まった)などがある。 本文へ戻る ↩ - ※7拡張思考(extended thinking) 答えを書く前に、AIが考える過程を別の欄(thinking ブロック)に書けるようにする Claude の機能。考える場所が答えとは別に用意されるので、答えの JSON に考えを混ぜずにすむ。 本文へ戻る ↩
- ※8fail-closed 判断に迷ったときや、検査に通らなかったときに、「使わない・通さない」安全なほうに倒す作り方。反対の fail-open は「迷ったら通す」。後ろの処理に渡す場面では、間違ったものが黙って流れるより、止まって気づけるほうが被害が小さい。 本文へ戻る ↩
- ※9較正(calibration) 「確信度90%」と言ったものが、実際にも90%くらい当たっている状態のこと。較正されていない確信度は、数字の大きさと当たる割合が対応していないので、「0.8以上なら採用」のような線引きに使えない。 本文へ戻る ↩
- ※10温度(temperature) 次のトークンを選ぶときの、ばらつきの大きさを決める設定。低いほど確率の高いトークンばかりを選び、高いほど意外なトークンも選ぶ。0にしても、計算の細かなゆれなどで出力が変わることはある。 本文へ戻る ↩
- ※11正規化(Unicode 正規化・NFKC) 見た目は同じでも中身の文字コードが違う文字を、1つの形にそろえること。NFKC はその方式の1つで、全角の英数字を半角に、半角カナを全角に、などをまとめて行う。Python なら
unicodedata.normalize("NFKC", s)。 本文へ戻る ↩
出典確かめた資料(2026年10月時点)
- Anthropic「Structured outputs」 制約付きデコーディング、防げる範囲、対応外の条件、項目の順番、拒否と打ち切りの扱い。
- Ollama「Structured outputs」
formatに JSON スキーマを渡す方法。 - JSON Schema「Understanding JSON Schema」
enum・required・additionalPropertiesの意味。 - Xiong ほか「Can LLMs Express Their Uncertainty?」(2023) AIに言葉で述べさせた確信度が、高めに出やすいという報告。