
レスポンス制御と構造化出力——ストリーミング・プリフィル・JSON生成【CCA Foundations対策】
CCA Foundations対策 · Claude API編 第3回(全17回)
Anthropic Academyの「Claude APIを使用した構築」コースをもとに解説しています。
APIから返ってくるレスポンスを「どう受け取るか」を工夫するだけで、アプリの使い勝手と堅牢性は大きく変わる。この記事ではストリーミングと構造化出力という2つのテクニックを扱う。
構造化出力については「プリフィル+ストップシーケンス」と「tool_use+JSON schema」という2つのアプローチを比較し、それぞれの使いどころと限界を整理する。
この記事でわかること:
- ストリーミングの仕組みと実装、生成完了後のデータ取得方法
- プリフィルとストップシーケンスでClaudeの出力を制御する方法
- プリフィル+ストップシーケンスでJSONをクリーンに取り出す技法
- より確実な構造化出力のために tool_use と JSON schema を使う考え方
- 2つのアプローチの使い分けと限界
ストリーミング——生成しながらリアルタイムで表示する
Claude APIのデフォルト動作は、生成が完全に終わってからレスポンスを返す。長い文章だと10〜30秒かかることもあり、その間ユーザーはただ待つことになる。
ストリーミングを使うと、生成されたテキストをチャンクごとに受け取り、即時で表示できる。ChatGPTやClaude.aiで文字が1つずつ流れてくる、あの動作と同じ仕組み。
【ストリーミングなし】
実行 → (10〜30秒の無音)→ テキスト全体がどっと表示される
【ストリーミングあり】
実行 → 「Python」「は」「読み」「やすい」「構文」「が」... と文字が流れ始める
イベントの種類
ストリーミング時のレスポンスはイベントの連続として届く:
message_start → 生成開始の通知(テキストはまだない)
content_block_start → テキストブロックの開始
content_block_delta → 実際のテキストチャンク(ここが本体)
content_block_stop → テキストブロックの終了
message_stop → 生成完了
実装では content_block_delta のチャンクを拾えばいい。
実装:client.messages.stream()
client.messages.stream() を使うと、text_stream プロパティでテキストチャンクだけを直接取り出せる:
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic()
model = "claude-sonnet-4-6"
messages = [{"role": "user", "content": "Pythonの良いところを3つ挙げてください"}]
with client.messages.stream(
model=model,
max_tokens=1000,
messages=messages,
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
ターミナルで実行すると、こんなふうに文字が流れてくる:
Pythonの良いところを3つ挙げます。
1. **読みやすい構文**
Pythonはインデントで構造を表現するため...
全部終わるのを待たず、生成されるそばから文字が現れる。
flush=True を忘れないようにする。 これがないと出力がバッファに溜まり、まとめて表示されてしまいストリーミングの意味がなくなる。
完全なメッセージをDBに保存する
ストリーミング中に得られるのは断片的なチャンクだけ。そのままではDBに1レコードとして保存できない。
get_final_message() を使うと、生成が完了した時点で完全なレスポンスをまとめて取得できる。「画面にはリアルタイム表示しながら、生成完了後にまとめてDBへ保存する」という両立が可能になる:
# get_final_message() なし
# → チャンクは流れるが、完全な文字列が手元に残らない
with client.messages.stream(...) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
# ← ここでstreamが閉じる。full_textを取り出す手段がない
# get_final_message() あり
# → リアルタイム表示しつつ、完全なテキストも取得できる
with client.messages.stream(
model=model,
max_tokens=1000,
messages=messages,
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True) # リアルタイム表示
final_message = stream.get_final_message()
full_text = final_message.content[0].text # 完全なテキスト → DBに保存できる
print(f"\n入力トークン: {final_message.usage.input_tokens}")
print(f"出力トークン: {final_message.usage.output_tokens}")
get_final_message() は with ブロックの中、ループが終わったあとに呼ぶのがポイント。
プリフィルとストップシーケンス——出力を制御する2つの道具
次のセクションで扱う構造化データの生成は、この2つのテクニックの組み合わせで成り立っている。先に単体で理解しておく。
プリフィル(アシスタントメッセージの先行入力)
messages リストの末尾に role: "assistant" のメッセージを追加すると、Claudeはそれを「自分がすでに書き始めた内容」と認識し、その続きから生成する。
messages = [
{"role": "user", "content": "コーヒーとお茶、どちらが好きですか?"},
{"role": "assistant", "content": "コーヒーの方が好きです。なぜなら"}, # プリフィル
]
response = client.messages.create(
model=model,
max_tokens=200,
messages=messages,
)
print(response.content[0].text)
コーヒーには豊かな香りと深みのある味わいがあり、
朝の目覚めにも仕事中の集中にも最適だからです。
プリフィルなしで「コーヒーとお茶どちらが好きですか?」と聞いたとすると、お茶派の回答が返ることもある。プリフィルで「コーヒーの方が好きです。なぜなら」を先置きすることで、必ずコーヒー推しの理由を語る回答になる。
なぜこれが動くのか: Claudeは messages リストを「会話のログ」として受け取る。アシスタントのメッセージが末尾にあれば「自分はここまで書いた」と解釈し、続きを生成しようとする。Claudeがテキストを確率的に生成する仕組みを利用した制御方法。
ストップシーケンス
特定の文字列が生成された瞬間に出力を止めるパラメータ。止まった文字列自体はレスポンスに含まれない。
response = client.messages.create(
model=model,
max_tokens=200,
messages=[{"role": "user", "content": "1から10まで数えてください"}],
stop_sequences=["五"], # 「五」が出た時点で停止
)
print(response.content[0].text)
一、二、三、四、
「五」が現れようとした瞬間に生成が止まる。「五」自体はレスポンスに含まれない。ストップシーケンスなしで同じ質問をすると「一〜十」と最後まで出力される。
構造化データの生成——クリーンなJSONを取り出す
アプリでJSONをClaudeに生成させると、デフォルトでは余計なものがついてくる:
以下のJSONをご確認ください:
```json
{
"title": "吾輩は猫である",
"author": "夏目漱石",
"year": 1905,
"genre": "小説"
}
```
上記のJSONを使用してください。
JSONは正しいが、マークダウンのコードブロックと説明文が混ざっていて、そのまま json.loads() に渡すとエラーになる。
プリフィル + ストップシーケンスで解決する
2つのテクニックを組み合わせると、純粋なデータだけを取り出せる:
import json
def generate_json(prompt: str) -> dict:
messages = []
messages.append({"role": "user", "content": prompt})
messages.append({"role": "assistant", "content": "```json"}) # プリフィル
response = client.messages.create(
model=model,
max_tokens=1000,
messages=messages,
stop_sequences=["```"], # 閉じコードブロックで停止
)
raw = response.content[0].text.strip()
return json.loads(raw)
仕組みをステップで追うと:
1. ユーザーメッセージ: 「本の情報をJSONで返して」
2. アシスタントプリフィル: "```json" を先置き
→ Claudeは「自分がコードブロックを書き始めた」と認識
3. Claudeが生成するのはJSONの中身だけ
4. 閉じ ``` が生成されようとした瞬間にストップシーケンスが発動
→ ``` より前で生成が止まる
result = generate_json(
"本の情報をJSONで生成してください。"
"フィールド: title(文字列), author(文字列), year(数値), genre(文字列)"
)
print(result)
{'title': '吾輩は猫である', 'author': '夏目漱石', 'year': 1905, 'genre': '小説'}
json.loads() が通る状態のデータが直接返ってくる。
JSONに限らず使える
このパターンはJSON以外でも応用できる:
| 欲しいもの | プリフィル | ストップシーケンス |
|---|---|---|
| Pythonコード | ```python |
``` |
| 箇条書きリスト | - |
\n\n |
| CSVデータ | name,value\n |
\n\n |
「Claudeが自然に閉じようとする区切り文字」をストップシーケンスに設定するのがコツ。
このアプローチの限界と、より確実な方法
プリフィル+ストップシーケンスは手軽だが、JSONの構文エラーを防ぐ保証がないという限界がある。
# このようなレスポンスが返ってくることがある
{
"title": "吾輩は猫である",
"author": "夏目漱石",
"year": 1905,
"genre": "小説"
# ← カンマが抜けていたり、フィールドが欠けていたりすることがある
Claudeが確率的にテキストを生成する以上、まれに不正なJSONが出ることがある。json.loads() がエラーになり、アプリが落ちる。
tool_use + JSON schema で構文エラーをなくす
より確実な方法は、tool_useとJSON schemaを組み合わせるアプローチ。
import json
tools = [
{
"name": "extract_book_info",
"description": "本の情報を構造化データとして抽出する",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "本のタイトル"},
"author": {"type": "string", "description": "著者名"},
"year": {"type": "integer", "description": "出版年"},
"genre": {"type": "string", "description": "ジャンル"},
"summary": {
"type": ["string", "null"], # nullable: 情報がない場合はnull
"description": "あらすじ(不明な場合はnull)"
}
},
"required": ["title", "author", "year", "genre"]
}
}
]
response = client.messages.create(
model=model,
max_tokens=1000,
tools=tools,
tool_choice={"type": "tool", "name": "extract_book_info"}, # 強制呼び出し
messages=[{"role": "user", "content": "『吾輩は猫である』の情報を抽出してください"}]
)
# tool_useブロックからデータを取得
tool_use_block = next(b for b in response.content if b.type == "tool_use")
result = tool_use_block.input
print(result)
{
'title': '吾輩は猫である',
'author': '夏目漱石',
'year': 1905,
'genre': '小説',
'summary': '人間社会を猫の視点から風刺した漱石の代表作...'
}
tool_choice の3つのモード
tool_choice には3種類の設定がある:
| 設定 | 動作 |
|---|---|
{"type": "auto"} |
Claudeが判断。ツールを使わずテキストを返すこともある |
{"type": "any"} |
必ずいずれかのツールを呼び出す。どれを使うかはClaude |
{"type": "tool", "name": "..."} |
特定のツールを強制呼び出し |
構造化データの抽出が目的なら "auto" は避ける。Claudeが「ツールを使わずに答えた方がよい」と判断してしまうと、テキストで返ってきて json.loads() がエラーになる。スキーマ準拠の出力を保証したいなら "any" か特定ツールの強制呼び出しを使う。
📋 試験ガイドより
公式試験ガイドのDomain 4 Task 4.3では、tool_choiceの3モード(auto / any / forced)の使い分けが構造化出力の設計ポイントとして取り上げられている。
nullable フィールドの設計
スキーマ設計で重要なのが、情報がない場合の扱い。
required に含めたフィールドに情報がない場合、Claudeは値を作り上げる(ハルシネーション)ことがある。存在しないかもしれない情報は nullable にしておくことで、モデルが null を返せるようにする:
# 悪い例:summaryが必須だと、情報がなくても作り上げる
"required": ["title", "author", "year", "genre", "summary"]
# 良い例:summaryはあってもなくてもいい
"properties": {
"summary": {
"type": ["string", "null"], # nullを許容
"description": "あらすじ(不明な場合はnull)"
}
},
"required": ["title", "author", "year", "genre"] # summaryは必須にしない
構文エラーは消えるが、意味的なエラーは残る
tool_use + JSON schemaで構文エラー(不正なJSON)はなくなる。しかし意味的なエラーは防げない:
yearフィールドに西暦ではなく元号(令和3年)が入るauthorフィールドに著者でなく出版社名が入る- 複数の数値が合計と一致しない
構文の正しさとデータの正しさは別の話。意味的な検証は別途コードで行う必要がある。
📋 試験ガイドより
同じくDomain 4 Task 4.3では、構文エラー(不正なJSON)と意味的エラーの区別が設計判断のポイントとして取り上げられている。tool_useは前者を防ぐが、後者は別途検証が必要という整理。
2つのアプローチの使い分け
| プリフィル+ストップ | tool_use+JSON schema | |
|---|---|---|
| 実装の手軽さ | 簡単 | やや複雑 |
| 構文エラーの防止 | 保証なし | 保証あり |
| 意味的エラーの防止 | 保証なし | 保証なし |
| 向いているケース | プロトタイプ・シンプルな抽出 | 本番・複雑なスキーマ |
よくある誤解まとめ
| 誤解 | 実際 |
|---|---|
flush=True はなくてもいい |
ないとバッファに溜まり、ストリーミングが機能しない |
| ストップシーケンスの文字列はレスポンスに含まれる | 含まれない。直前で止まる |
| プリフィル+ストップで必ずクリーンなJSONが返る | 確率的生成のため、まれに構文エラーが混じる |
tool_choice: "auto" で構造化出力を保証できる |
Claudeがツールを使わずテキストで返すことがある |
| tool_use+JSON schemaは意味的エラーも防ぐ | 構文エラーのみ防ぐ。意味的な正しさは別途検証が必要 |
まとめ
- ストリーミングで生成中のテキストをリアルタイム表示できる。
client.messages.stream()+text_streamが最もシンプルな実装 flush=Trueがないとバッファに溜まって即時表示にならない- 完全なレスポンスが必要なら
withブロック内でget_final_message()を呼ぶ - プリフィルでアシスタントの発言の続きをClaudeに生成させ、出力の方向を制御できる
- ストップシーケンスで任意の文字列が出た時点で生成を止められる(その文字列自体はレスポンスに含まれない)
- プリフィル+ストップは手軽だが構文エラーを保証しない
- tool_use+JSON schemaは構文エラーをなくせるが、意味的エラーは防げない
- 構造化出力を保証したいなら
tool_choiceは"any"か特定ツールの強制呼び出しを使う - 存在しない可能性のあるフィールドは nullable にしてハルシネーションを防ぐ
次回はプロンプトエンジニアリングとEval——プロンプトの品質を数値で測って改善する方法を扱う。