ストリーミングを実装したのに、Claudeの返答がある行で突然止まる。エラーが返ってくれば手がかりになりますが、無音で停止するケースの方が厄介です。「接続は生きているのに、テキストが来ない」という状態になったことはないでしょうか。
この止まり方にはいくつかのタイプがあり、タイプを特定しないまま「とりあえずリトライ」で対応しても、根本解決にはなりません。ここでは症状から原因を絞り込む診断フローと、コードレベルの修正パターンをまとめます。
まず「エラーか無音か」でタイプを分ける
最初の判断ポイントは、例外が発生したかどうかです。
エラーが発生した場合 — エラーコードを確認する
429 Too Many Requests → レート制限(原因2)
529 Overloaded → サーバー過負荷(原因2と同じ対処)
400 Request too large → コンテキストウィンドウ超過(原因3)
ReadTimeout / TimeoutError → ネットワークタイムアウト(原因1)
ECONNRESET → 接続リセット(原因1)
エラーなしで止まった場合 — stop_reason を確認する
stop_reason: "max_tokens" → max_tokens設定の問題(原因4)
stop_reason: "end_turn" → 正常終了(期待どおり)
イベント自体が来なくなった → クライアント実装の問題(原因5)
stop_reasonはストリームの最後のイベントに含まれています。確認していなかった場合は、まずここをロギングするだけで原因が分かることがほとんどです。
原因1: ネットワークタイムアウト(最も多い)
長い応答を生成中に、HTTP接続がタイムアウトで切れるケースです。requestsやhttpxのデフォルトタイムアウトは30〜60秒に設定されていることが多く、Claudeが長文を生成する最中に静かに切れてしまいます。
import anthropic
import httpx
# デフォルト設定では長い応答が途切れることがある
client_default = anthropic.Anthropic()
# ストリーミング用にタイムアウトを調整する
client = anthropic.Anthropic(
timeout=httpx.Timeout(
connect=10.0, # 接続確立
read=300.0, # データ受信(長い生成に対応)
write=10.0, # 送信
pool=10.0, # コネクションプール
)
)
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=4096,
messages=[{"role": "user", "content": "詳細なレポートを作成してください..."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)read=300.0(5分)は多く見えるかもしれませんが、claude-sonnet-4-6が複雑なタスクを処理する場合、2〜3分かかることがあります。私の本番環境ではread=300を標準設定にしていますが、それ以来タイムアウト起因の切断はほぼなくなりました。
原因2: レート制限と過負荷(429 / 529)
429 RateLimitErrorはリクエスト数またはトークン数の上限超過、529 OverloadedErrorはサーバー側の一時的な過負荷です。どちらも時間を置けば回復するため、指数バックオフ付きのリトライで対応します。
import anthropic
import time
def stream_with_retry(client, max_retries=3, **kwargs):
"""指数バックオフ付きストリーミング"""
for attempt in range(max_retries):
try:
with client.messages.stream(**kwargs) as stream:
collected = ""
for text in stream.text_stream:
print(text, end="", flush=True)
collected += text
return collected
except anthropic.RateLimitError:
if attempt == max_retries - 1:
raise
wait = (2 ** attempt) * 30 # 30秒 → 60秒 → 120秒
print(f"\n⚠️ レート制限 — {wait}秒待機 ({attempt+1}/{max_retries})")
time.sleep(wait)
except anthropic.APIStatusError as e:
if e.status_code == 529 and attempt < max_retries - 1:
wait = (2 ** attempt) * 10
print(f"\n⚠️ 過負荷 — {wait}秒待機 ({attempt+1}/{max_retries})")
time.sleep(wait)
else:
raise
client = anthropic.Anthropic()
stream_with_retry(
client,
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[{"role": "user", "content": "こんにちは"}],
)レート制限が頻繁に発生する場合は、Anthropicのコンソールでティアを確認してください。ティア1では1分あたりのトークン数制限が比較的低く設定されています。詳細はClaude API 429エラーの対処法をご覧ください。
原因3: コンテキストウィンドウ超過(400エラー)
入力トークン数がモデルの上限を超えると、400 Bad Requestが返されます。これはストリーミング開始前に発生するため、「ストリームが始まらない」という症状になります。
max_tokensが大きすぎる(入力トークン数 + max_tokens がウィンドウ上限を超える)場合も同様です。
import anthropic
client = anthropic.Anthropic()
def get_safe_max_tokens(messages, model="claude-sonnet-4-6", buffer=100):
"""入力に合わせて安全なmax_tokensを計算する"""
count_response = client.messages.count_tokens(
model=model,
messages=messages,
)
input_tokens = count_response.input_tokens
# claude-sonnet-4-6のコンテキストウィンドウは200,000トークン
# 出力は最大64,000トークン
context_limit = 200000
output_limit = 64000
max_available = context_limit - input_tokens - buffer
safe_max = min(max_available, output_limit)
print(f"入力: {input_tokens}トークン / 利用可能出力: {safe_max}トークン")
return max(safe_max, 1)
messages = [{"role": "user", "content": "非常に長い文書..."}]
safe_tokens = get_safe_max_tokens(messages)
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=safe_tokens,
messages=messages,
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)長い会話履歴を全て渡す設計の場合、10往復を超えた辺りでこの問題が起きやすくなります。古いメッセージを要約して圧縮するか、重要な部分だけを抽出する設計を検討してください。
原因4: max_tokensに達した(正常終了との区別)
これは「エラー」ではなく「正常終了」ですが、生成途中で止まったように見えるため混乱しやすいです。stop_reasonを確認することで判別できます。
import anthropic
client = anthropic.Anthropic()
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=256, # 短すぎると途中で止まる
messages=[{"role": "user", "content": "Pythonの非同期処理について詳しく説明してください"}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
# ストリーム終了後にstop_reasonを確認
final_message = stream.get_final_message()
print(f"\n\n--- stop_reason: {final_message.stop_reason} ---")
if final_message.stop_reason == "max_tokens":
print("⚠️ max_tokensに達しました。max_tokensを増やしてください。")
elif final_message.stop_reason == "end_turn":
print("✅ 正常に完了しました。")max_tokens=256のような低い値のままで詳細な説明を求めると、必ず途中で切れます。用途に応じて適切な値に設定してください。claude-sonnet-4-6は最大64,000トークンの出力をサポートしています。
原因5: クライアント実装のバグ
エラーもなく、stop_reasonもend_turnなのにテキストが途切れているように見える場合は、クライアント側の実装に問題がある可能性があります。よくあるパターンを確認します。
非同期コードでのawait漏れ
import asyncio
import anthropic
# 同期クライアントを非同期コードで使う(間違い)
async def wrong_approach():
client = anthropic.Anthropic() # 非同期には AsyncAnthropic を使う
# ...
# 正しい実装
async def correct_approach():
client = anthropic.AsyncAnthropic()
async with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "こんにちは"}],
) as stream:
async for text in stream.text_stream:
print(text, end="", flush=True)
print() # 改行
asyncio.run(correct_approach())ストリームを途中でクローズしてしまう
# 問題のある実装
with client.messages.stream(...) as stream:
for text in stream.text_stream:
if some_condition:
break # ここでストリームが切断される
process(text)
# 全イベントを受け取ってから処理する場合はcollect_streamを使う
message = client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "こんにちは"}],
).get_final_message()
print(message.content[0].text)本番で使えるストリーミング実装
上記5つの原因に対応した、本番向けの実装例です。
import anthropic
import httpx
import time
from collections.abc import Iterator
def create_robust_client() -> anthropic.Anthropic:
"""本番用クライアントの初期化"""
return anthropic.Anthropic(
timeout=httpx.Timeout(connect=10.0, read=300.0, write=10.0, pool=10.0)
)
def stream_with_diagnostics(
client: anthropic.Anthropic,
messages: list,
model: str = "claude-sonnet-4-6",
max_tokens: int = 4096,
max_retries: int = 3,
) -> Iterator[str]:
"""
診断機能付きストリーミング。
タイムアウト・レート制限・過負荷を自動リトライ。
stop_reasonをロギングして問題を可視化する。
"""
for attempt in range(max_retries):
try:
with client.messages.stream(
model=model,
max_tokens=max_tokens,
messages=messages,
) as stream:
for text in stream.text_stream:
yield text
final = stream.get_final_message()
if final.stop_reason == "max_tokens":
print(f"\n⚠️ max_tokens({max_tokens})に到達。必要なら増やしてください。")
# stop_reason: "end_turn" = 正常完了
return
except anthropic.RateLimitError:
wait = 2 ** attempt * 30
if attempt < max_retries - 1:
print(f"\n⚠️ レート制限 — {wait}s待機 ({attempt+1}/{max_retries})")
time.sleep(wait)
else:
raise
except anthropic.APIStatusError as e:
if e.status_code == 529 and attempt < max_retries - 1:
wait = 2 ** attempt * 10
print(f"\n⚠️ 過負荷 — {wait}s待機 ({attempt+1}/{max_retries})")
time.sleep(wait)
else:
raise
except (httpx.ReadTimeout, httpx.ConnectError) as e:
if attempt < max_retries - 1:
print(f"\n⚠️ ネットワークエラー: {e} — 再試行 ({attempt+1}/{max_retries})")
time.sleep(5)
else:
raise
# 使用例
client = create_robust_client()
for text in stream_with_diagnostics(
client,
messages=[{"role": "user", "content": "Pythonの非同期処理について詳しく説明してください"}],
):
print(text, end="", flush=True)stop_reasonのロギングを追加するだけでも、「なぜ止まったのか」が分かるケースがかなり多いです。エラーハンドリングよりまず先に、stop_reasonの可視化から始めることをおすすめします。
APIエラー全般についてはClaude APIエラー完全ガイドもあわせてご参照ください。
ストリーミングが止まる原因を絞り込む手順は、エラーコード確認 → stop_reason確認 → クライアントログ確認の順です。次のステップとして、既存のストリーミングコードにfinal_message.stop_reasonのログ出力を1行追加してみてください。原因不明だった切断のうち、半数以上はmax_tokens到達だったと分かることがよくあります。