Claude Media
Claude APIストリーミングのエラー復旧がv4.6で変わった

Claude APIストリーミングのエラー復旧がv4.6で変わった

ストリーミング中断からの復旧手順が、Claude 4.6以降でassistantメッセージからuserメッセージ方式に変わりました。

Claude4.6以降でストリーミング復旧の実装が変わった

ストリーミング中にネットワーク障害やタイムアウトで応答が途切れたとき、続きから再開する手順が公式に定義されています。Claude4.5以前は、受け取った途中経過をassistantメッセージの続きとして次のリクエストに積みます。Claude4.6以降はこの積み方が変わり、続きを促すuserメッセージを新規に追加する形に切り替わりました。同じ「部分応答を捨てず再開する」という目的に対して、リクエストの組み立て方そのものが別物になっています。

移行を怠ると起きるのは、エラーではなく沈黙した劣化です。4.6以降のモデルに旧方式のassistantメッセージ継続を送っても、リクエスト自体は通ります。ただしモデルが「これは自分の発言の続きだ」と正しく認識しない場面が増え、文体が変わったり、同じ内容を繰り返し始めたりします。エラーログには何も残らないため、気づきにくい退行です(SDKの例外クラスとエラー形式の対応表を先に押さえておくと、こうした沈黙した劣化と実際のエラーとを切り分けやすくなります)。

復旧が発動するタイミング

ストリーミングのイベントには error イベントが混ざることがあります。高負荷時の overloaded_error(非ストリーミングでのHTTP529相当)が代表例で、次のような形で流れてきます。

event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

このほかネットワーク切断やクライアント側タイムアウトでもストリームは途中で止まります。公式ドキュメントは、一部のネットワークが一定時間アイドルな接続を切断する挙動を明記しており、max_tokens を大きく設定した非ストリーミングリクエストほどこの影響を受けやすいとしています。ストリーミングに切り替えるか、TCPのソケットキープアライブを設定することが緩和策として挙げられています。

復旧の起点は「エラー種別」ではなく「途中まで受け取れたコンテンツブロックがあるかどうか」です。text ブロックの content_block_delta を蓄積できていれば、そこから先を復旧対象にできます。エラーコード別の切り分けとリトライ設計はClaude APIのエラーハンドリング設計で詳しく扱っています。

HTTPステータス別に見る、そもそもリトライすべきエラーか

ストリーミング中の event: error は、非ストリーミングでのHTTPステータスに対応しています。復旧ロジックを組む前に、この3種は性質が違うことを押さえておく必要があります。

ステータスエラー種別性質
429エラー種別rate_limit_error性質組織のレート制限か月間の利用上限超過。スペンドキャップ超過の場合は retry-after ヘッダーが付かず、上限解除まで失敗し続ける
500エラー種別api_error性質Anthropic側の内部エラー。指数バックオフでリトライする対象
529エラー種別overloaded_error性質全ユーザーに影響する高負荷状態。リトライ対象だが、短時間の連続リトライは状況を悪化させやすい

公式SDKは接続エラー・レート制限・5xx系のサーバーエラーを、既定で2回まで指数バックオフ付きで自動リトライします。retry-after ヘッダーが付いていればそれを優先し、最大リトライ回数はクライアント初期化時のオプションで変更できます。この自動リトライは接続断そのものへの対処であり、本記事が扱う「途中経過をどう次のリクエストに積むか」という継続ロジックとは別の仕組みです。両方を組み合わせて初めて、ストリーミング障害への耐性が完成します。

共通の3ステップ(4.5系・4.6系どちらも同じ骨格)

  1. 途中経過の保存: エラー発生前に受信できた内容をすべて保持する
  2. 継続リクエストの構築: 保存した内容を使って新しいリクエストを組み立てる(ここだけモデル世代で分岐)
  3. ストリーミングの再開: 中断箇所から続きを受信する

骨格は共通でも、ステップ2の中身がまるごと入れ替わるため、モデル世代をまたぐ実装では分岐が必須になります。

Claude4.5以前 — assistantメッセージとして継続する

途中まで受け取った応答を、新しいリクエストの messages 配列にassistant役のメッセージとして追加します。モデルはそれを「自分が既に言った内容」として受け取り、続きを生成します。

messages = [
    {"role": "user", "content": "長い技術解説をお願いします"},
    {"role": "assistant", "content": partial_response_text},  # 途中経過をそのまま積む
]
 
response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    messages=messages,
)

Claude4.6以降 — userメッセージで続きを指示する

同じ目的でも、4.6以降は途中経過をassistantメッセージに混ぜません。代わりにuserメッセージとして「ここまでの内容の続きから書いて」と明示的に指示します。

continuation_prompt = (
    f"Your previous response was interrupted and ended with "
    f"[{partial_response_text}]. Continue from where you left off."
)
 
messages = [
    {"role": "user", "content": "長い技術解説をお願いします"},
    {"role": "user", "content": continuation_prompt},
]
 
response = client.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    messages=messages,
)

TypeScriptでも組み立て方は同じです。

const continuationPrompt =
  `Your previous response was interrupted and ended with ` +
  `[${partialResponseText}]. Continue from where you left off.`;
 
const response = await client.messages.create({
  model: "claude-opus-5",
  max_tokens: 1024,
  messages: [
    { role: "user", content: "長い技術解説をお願いします" },
    { role: "user", content: continuationPrompt },
  ],
});

なぜassistantメッセージ方式をやめたのか

公式ドキュメントは変更の理由そのものを明言していませんが、隣接する仕様がヒントになります。Claude Fable 5.1では、extended thinkingブロックが会話の前提(システムプロンプト・tools定義・それ以前のメッセージ)にシグネチャで紐づけられ、前提が変わった状態で再送すると400エラーで拒否される仕組みが導入されています。ただし prefix_mismatch_behavior の設定次第では、拒否せずに該当ブロックを黙って落として処理を続ける挙動にもできます。会話履歴の途中を書き換えて再送する行為そのものに、モデル側が敏感になっている方向性がうかがえます。

assistantメッセージとして途中経過を積む方式は、本来モデルが生成していない内容を「自分の過去の発言」として履歴に混ぜる行為です。会話の一貫性チェックが厳しくなった世代ほど、この混ぜ方が不安定要因になりやすいと見ることもできます。userメッセージで明示的に「続きを書いて」と指示する形は、モデルに解釈を委ねず要求をそのまま伝える分、この種の不整合を起こしにくい構造です。ただしこれは公式に明記された因果ではなく、隣接仕様からの推測である点は留保が必要です。

使い分け早見表

利用形態影響度対応
SDKの標準機能だけを使う実装影響度ほぼ影響なし対応SDKが世代分岐を内部で吸収する前提だが、更新履歴の確認は必要
自前でストリーミング復旧を組んでいる実装影響度明確な影響あり対応モデルIDで分岐するロジックの追加が必須
複数世代のモデルを同時運用するマルチモデル構成影響度条件次第で影響大対応ルーティング層で復旧ロジックをモデル世代別に切り替える必要がある
復旧を実装せず単純リトライで済ませている構成影響度影響なし対応元々この継続方式に依存していないため関係しない

自前実装で復旧ロジックを持つチームが最も影響を受けます。SDKの新しいバージョンに更新するだけで済む場合もありますが、独自にリトライ層を書いているアーキテクチャでは、モデルIDを見て分岐する処理を足さないと4.6以降で沈黙した劣化が起きます。

thinkingとtool_useブロックは部分復旧できない

復旧できるのは text コンテンツブロックだけです。公式のベストプラクティスも明記している制約で、tool_use ブロックやextended thinkingのブロックは部分的な状態から再開できません。ツール呼び出しの途中でストリームが切れた場合、その tool_use ブロックごと破棄して、直前の完了済み text ブロックから再開するのが現実的な設計になります。

途中まで組み立てられた input_json_delta を無理に組み立てて再送しても、モデル側はそれを正しい入力として扱いません。ツール呼び出しの再開を前提にした複雑な差分マージロジックを書く前に、この制約を先に確認してください。stop_reasonrefusal のように、そもそも継続ではなくリセットが必要なケースはClaude APIのrefusal stop_reasonを検出してリセットする方法を参照してください。

継続リクエストのコストは「もう一度全部」ではない

復旧のたびに気になるのが、途中経過を含めた継続リクエストが二重にトークンを消費するのではないかという点です。継続リクエストの messages には、元の指示・途中経過・継続指示文がすべて含まれるため、ゼロから送り直すよりは軽いものの、完全に無料ではありません。message_delta イベントの usage フィールドは常に累積値である点も踏まえ、複数回の継続をまたいだ合計コストは自前で積算する必要があります。長い応答を扱う場合は、ストリーミング中断の頻度そのものを下げる設計(前述のTCPキープアライブ設定や、Message Batches APIへの切り替え)のほうが、継続ロジックを頑丈にするより効果的な場面もあります。

復旧ロジックを本番前にテストする方法

継続ロジックは、本番のネットワーク障害を待って初めて動作確認するものではありません。ストリーミング接続を途中で強制切断するテストダブルを用意し、意図的に text ブロックの途中でストリームを止めて、次のリクエストが正しく組み立てられるかを検証できます。確認すべきポイントは3つです。①モデルIDで4.5系/4.6系の分岐が正しく発火しているか ②tool_use ブロックの途中で切断した場合に、そのブロックを破棄して直前の text ブロックから正しく再開できているか ③継続後の応答が、途中経過の文体・語尾と不自然に変わっていないか。3つ目は自動テストにしにくいため、継続リクエスト発生時のログにモデルID・切断位置・継続後の冒頭数十文字を残しておき、目視レビューできる形にしておくのが現実的です。

SDKを使わず生のSSEを直接パースしている実装では、content_block_stop を受け取る前に接続が切れるケースの扱いにも注意が必要です。content_block_delta の蓄積が content_block_stop に到達していない場合、そのブロックは未完成として扱い、完成済みの直前のブロックまでを復旧対象にします。中途半端な text デルタを無理に完成したブロックとして扱うと、文の途中で不自然に文字が欠けたまま次のリクエストに積まれることになります。

実装のチェックリスト

  • モデルID(claude-opus-5 / claude-sonnet-4-5 等)で継続リクエストの組み立て方を分岐しているか
  • 復旧対象を text ブロックに限定し、tool_use / thinkingブロックは復旧せず破棄しているか
  • SDKを使っている場合、SDKのバージョンがモデル世代分岐に対応しているか(自前実装が残っていないか。Agent SDK側のストリーミング設定はAgent SDKのストリーミング出力を有効にするを参照)
  • 継続プロンプトの文言(Continue from where you left off. 相当)を固定テンプレートとして持っているか

まとめ

ストリーミング復旧は「途中経過を次のリクエストにどう積むか」という一点で、Claude4.6を境に別方式へ切り替わりました。4.5以前はassistantメッセージへの継続、4.6以降はuserメッセージでの明示的な継続指示です。自前で復旧ロジックを実装しているプロジェクトは、モデルIDによる分岐を入れないまま4.6以降のモデルへ切り替えると、エラーは出ないまま応答品質だけが落ちます。SDKの標準機能に乗っている場合も、使っているSDKのバージョンがこの分岐に対応しているかは一度確認しておく価値があります。

この記事を共有:XはてブLinkedIn