
【CCA Foundations対策 / Claude API編 #3】レスポンス制御と構造化出力——ストリーミング・プリフィル・JSON生成
Anthropic Academyの「Claude APIを使用した構築」コースをもとに解説しています。
APIから返ってくるレスポンスを「どう受け取るか」を工夫するだけで、アプリの使い勝手は大きく変わる。この記事ではストリーミングと構造化出力という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)
flush=True を忘れないようにする。これがないと出力がバッファに溜まり、まとめて表示されてしまいストリーミングの意味がなくなる。
ターミナルで実行すると、こんなふうに文字が流れてくる:
Pythonの良いところを3つ挙げます。
1. **読みやすい構文**
Pythonはインデントで構造を表現するため...
全部終わるのを待たず、生成されるそばから文字が現れる。
完全なメッセージを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\n入力トークン: {final_message.usage.input_tokens}")
print(f"出力トークン: {final_message.usage.output_tokens}")
get_final_message() は with ブロックの中、ループが終わったあとに呼ぶのがポイント。生成完了後に呼ばないと完全なメッセージが得られない。
プリフィルとストップシーケンス——構造化データ生成の前提知識
次のセクションで扱う構造化データの生成は、この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)
実行すると、プリフィルの続きが返ってくる:
コーヒーには豊かな香りと深みのある味わいがあり、
朝の目覚めにも仕事中の集中にも最適だからです。
プリフィルなしで「コーヒーとお茶どちらが好きですか?」と聞いたとすると、どちらが好きかは不定で、お茶派の回答が返ることもある。プリフィルで「コーヒーの方が好きです。なぜなら」を先置きすることで、必ずコーヒー推しの理由を語る回答になる。
ストップシーケンス
特定の文字列が生成された瞬間に出力を止めるパラメータ。止まった文字列自体はレスポンスに含まれない。
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をご確認ください:
```json
{
"title": "吾輩は猫である",
"author": "夏目漱石",
"year": 1905,
"genre": "小説"
}
上記のJSONを使用してください。
JSONは正しいが、マークダウンのコードブロックと説明文が混ざっていて、そのまま `json.loads()` に渡すとエラーになる。
### プリフィル + ストップシーケンスで解決する
2つのテクニックを組み合わせると、純粋なデータだけを取り出せる:
```python
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)
# 出力(マークダウンも説明文もない、クリーンなdict)
{'title': '吾輩は猫である', 'author': '夏目漱石', 'year': 1905, 'genre': '小説'}
json.loads() が通る状態のデータが直接返ってくる。改行ありの整形出力が必要な場合は json.dumps() を使う:
print(json.dumps(result, indent=2, ensure_ascii=False))
# {
# "title": "吾輩は猫である",
# "author": "夏目漱石",
# "year": 1905,
# "genre": "小説"
# }
JSONに限らず使える
このパターンはJSON以外でも応用できる:
| 欲しいもの | プリフィル | ストップシーケンス |
|---|---|---|
| Pythonコード | ```python |
``` |
| 箇条書きリスト | - |
\n\n |
| CSVデータ | name,value\n |
\n\n |
「Claudeが自然に閉じようとする区切り文字」をストップシーケンスに設定するのがコツ。
まとめ
- ストリーミングで生成中のテキストをリアルタイム表示できる。
client.messages.stream()+text_streamが最もシンプルな実装 flush=Trueがないとバッファに溜まって即時表示にならない- 完全なレスポンスが必要なら
withブロック内でget_final_message()を呼ぶ - プリフィルでアシスタントの発言の続きをClaudeに生成させ、出力の方向を制御できる
- ストップシーケンスで任意の文字列が出た時点で生成を止められる(その文字列自体はレスポンスに含まれない)
- 2つを組み合わせるとJSONやコードをクリーンに取り出せる
次回はプロンプトエンジニアリングとEval——プロンプトの品質を数値で測って改善する方法を扱う。