CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-03-09中級

ストリーミングレスポンスの実装 — Claude API でリアルタイム応答を実現する

Claude API のストリーミング応答を Python / TypeScript の動くコード例で実装します。SSE の仕組み、イベント処理、切断時の復旧設計まで、実装で確かめた順にまとめました。

streaming16api38real-time2SSE4

ストリーミングを初めて自分のアプリに組み込んだときの驚きは、いまでもよく覚えています。それまでは応答が完成するまで画面が数十秒沈黙し、試してくれた方から「固まったのかと思って閉じました」と言われたこともありました。トークンが1つずつ流れ始めた途端、処理時間そのものは変わらないのに、体感はまるで別物になります。個人開発では応答待ちの数十秒が離脱に直結するだけに、この差は無視できません。ここでは Python / TypeScript でストリーミングを組み込む手順と、動かして初めて見える注意点を、実装で確かめた順に並べました。

ストリーミングとは

ストリーミングを使用すると、完全な応答を待つ代わりに、Claude の応答をトークンごとにリアルタイムで受信できます。これはユーザーに即座のフィードバックを提供し、反応の良いアプリケーションを構築するために不可欠です。

ℹ️
ストリーミングは特に以下の場合に価値があります: - チャット アプリケーション (応答が生成されているのを表示) - 長編コンテンツ (ニュース記事、レポート) - リアルタイム ダッシュボード - 帯域幅が限られたモバイル アプリケーション - 認識される応答時間を改善

Server-Sent Events (SSE) を理解する

ストリーミング レスポンスは Server-Sent Events (SSE) プロトコルを使用します。このプロトコルはサーバーからクライアントへイベント ストリームとしてデータを送信します。

SSE フォーマット

data: {"type":"content_block_start","index":0,"content_block":{"type":"text"}}

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}

data: {"type":"message_stop"}

各イベントは、イベントの種類を示す type フィールドを持つ JSON オブジェクトです。

Python ストリーミング実装

基本的なストリーミング例

import anthropic
 
client = anthropic.Anthropic()
 
messages = [
    {
        "role": "user",
        "content": "Write a short story about a robot learning to paint."
    }
]
 
# stream=True を使用してストリーミングを有効にする
with client.messages.stream(
    model="claude-sonnet-5",
    max_tokens=1024,
    messages=messages
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    print()  # 完全な応答後の改行

モデル名には 2026年7月時点の既定である claude-sonnet-5 を指定しています。2026年6月30日に公開された Sonnet 5 は導入価格(100万トークンあたり入力 $2・出力 $10、2026年8月31日まで)で利用でき、Opus 4.8 と比べて入出力とも4割ほど安く、ストリーミングを多用するチャット用途でコストを抑えやすい選択です。長時間の複雑なエージェント処理には claude-opus-4-8 を検討してください。

詳細なイベント処理

より細かく制御するために、個々のイベントを処理できます:

import anthropic
 
client = anthropic.Anthropic()
 
messages = [
    {
        "role": "user",
        "content": "Explain quantum computing in simple terms."
    }
]
 
# イベントを個別に処理
with client.messages.stream(
    model="claude-sonnet-5",
    max_tokens=1024,
    messages=messages
) as stream:
    for event in stream:
        if event.type == "content_block_start":
            print(f"Starting content block {event.index}")
 
        elif event.type == "content_block_delta":
            if event.delta.type == "text_delta":
                print(event.delta.text, end="", flush=True)
 
        elif event.type == "content_block_stop":
            print(f"\nFinished content block {event.index}")
 
        elif event.type == "message_stop":
            print("\nStream finished")

ストリーミング中の完全な応答の収集

import anthropic
 
client = anthropic.Anthropic()
 
def stream_message(user_input: str) -> str:
    """Stream a message and return the full response"""
    full_response = ""
 
    with client.messages.stream(
        model="claude-sonnet-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": user_input}]
    ) as stream:
        for text in stream.text_stream:
            full_response += text
            print(text, end="", flush=True)
 
    print()  # 最終改行
    return full_response
 
# 関数を使用
result = stream_message("What are the benefits of renewable energy?")
print(f"\nFull response ({len(result)} characters)")

TypeScript/JavaScript ストリーミング

公式 SDK を使用

import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic();
 
async function streamResponse() {
  const stream = client.messages.stream({
    model: "claude-sonnet-5",
    max_tokens: 1024,
    messages: [
      {
        role: "user",
        content: "Write a haiku about the moon."
      }
    ]
  });
 
  // テキスト イベントを処理
  stream.on("text", (text: string) => {
    process.stdout.write(text);
  });
 
  // 最終メッセージを取得
  const finalMessage = await stream.finalMessage();
  console.log("\n\nFinal message:", finalMessage);
}
 
streamResponse();

手動イベント処理

import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic();
 
async function processStreamEvents() {
  const stream = await client.messages.create({
    model: "claude-sonnet-5",
    max_tokens: 1024,
    stream: true,
    messages: [
      {
        role: "user",
        content: "Describe the water cycle."
      }
    ]
  });
 
  for await (const event of stream) {
    switch (event.type) {
      case "content_block_start":
        console.log(`Starting block ${event.index}`);
        break;
 
      case "content_block_delta":
        if (event.delta.type === "text_delta") {
          process.stdout.write(event.delta.text);
        }
        break;
 
      case "content_block_stop":
        console.log(`\nFinished block ${event.index}`);
        break;
 
      case "message_stop":
        console.log("Stream complete");
        break;
    }
  }
}
 
processStreamEvents();

ウェブベースのストリーミング (フロントエンド)

Fetch API を使用したストリーミング

async function streamFromAPI(userMessage) {
  const response = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.ANTHROPIC_API_KEY,
      "anthropic-version": "2023-06-01"
    },
    body: JSON.stringify({
      model: "claude-sonnet-5",
      max_tokens: 1024,
      stream: true,
      messages: [
        { role: "user", content: userMessage }
      ]
    })
  });
 
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let fullResponse = "";
 
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
 
    const chunk = decoder.decode(value);
    const lines = chunk.split("\n");
 
    for (const line of lines) {
      if (line.startsWith("data: ")) {
        const eventData = line.slice(6);
        try {
          const event = JSON.parse(eventData);
 
          if (event.type === "content_block_delta") {
            if (event.delta.type === "text_delta") {
              const text = event.delta.text;
              fullResponse += text;
              // リアルタイムで UI を更新
              document.getElementById("response").textContent = fullResponse;
            }
          }
        } catch (e) {
          // 無効な JSON をスキップ
        }
      }
    }
  }
 
  return fullResponse;
}

React ストリーミング コンポーネント

import { useState } from "react";
 
export function StreamingChat() {
  const [input, setInput] = useState("");
  const [response, setResponse] = useState("");
  const [loading, setLoading] = useState(false);
 
  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setResponse("");
 
    try {
      const fetchResponse = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: input })
      });
 
      const reader = fetchResponse.body.getReader();
      const decoder = new TextDecoder();
      let fullText = "";
 
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
 
        const chunk = decoder.decode(value);
        const lines = chunk.split("\n");
 
        for (const line of lines) {
          if (line.startsWith("data: ")) {
            try {
              const event = JSON.parse(line.slice(6));
              if (event.type === "content_block_delta" &&
                  event.delta.type === "text_delta") {
                fullText += event.delta.text;
                setResponse(fullText);
              }
            } catch (e) {
              // 無効な JSON をスキップ
            }
          }
        }
      }
    } finally {
      setLoading(false);
    }
  };
 
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Enter your message..."
          disabled={loading}
        />
        <button type="submit" disabled={loading}>
          {loading ? "Streaming..." : "Send"}
        </button>
      </form>
      <div id="response">{response}</div>
    </div>
  );
}

ストリーム イベントの種類と拾い方

サポートされているイベント

# メッセージ開始イベント
{
  "type": "message_start",
  "message": {
    "id": "msg_123",
    "type": "message",
    "role": "assistant",
    "stop_reason": null
  }
}
 
# コンテンツ ブロック開始
{
  "type": "content_block_start",
  "index": 0,
  "content_block": {
    "type": "text"
  }
}
 
# テキスト デルタ (トークン受信)
{
  "type": "content_block_delta",
  "index": 0,
  "delta": {
    "type": "text_delta",
    "text": "Hello"
  }
}
 
# コンテンツ ブロック停止
{
  "type": "content_block_stop",
  "index": 0
}
 
# メッセージ デルタ (最終統計)
{
  "type": "message_delta",
  "delta": {
    "stop_reason": "end_turn"
  },
  "usage": {
    "output_tokens": 42
  }
}
 
# メッセージ停止
{
  "type": "message_stop"
}

ストリームでのエラー処理

ストリーム エラー処理

import anthropic
 
client = anthropic.Anthropic()
 
try:
    with client.messages.stream(
        model="claude-sonnet-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hello"}]
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)
 
except anthropic.APIError as e:
    print(f"API Error: {e}")
    # レート制限、認証エラー などを処理
 
except anthropic.APIConnectionError as e:
    print(f"Connection Error: {e}")
    # ネットワーク エラーを処理
 
except anthropic.RateLimitError as e:
    print(f"Rate Limited: {e}")
    # 指数バックオフを実装

タイムアウト処理

async function streamWithTimeout(
  userMessage: string,
  timeoutMs: number = 30000
) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
 
  try {
    const stream = client.messages.stream({
      model: "claude-sonnet-5",
      max_tokens: 1024,
      messages: [{ role: "user", content: userMessage }],
      signal: controller.signal
    });
 
    for await (const event of stream) {
      if (event.type === "content_block_delta" &&
          event.delta.type === "text_delta") {
        console.log(event.delta.text);
      }
    }
  } catch (error) {
    if (error.name === "AbortError") {
      console.error("Stream timeout");
    } else {
      throw error;
    }
  } finally {
    clearTimeout(timeoutId);
  }
}

UI/UX ベストプラクティス

段階的な表示

応答がそのまま表示されるようにします:

<div id="chat">
  <div class="message user">Your question</div>
  <div class="message assistant" id="response">
    <!-- レスポンスがここに表示され、トークンごと -->
  </div>
</div>

ローディング インジケータ

function showLoadingIndicator() {
  const response = document.getElementById("response");
  response.innerHTML = '<span class="loading">Thinking...</span>';
}
 
function updateResponse(token) {
  const response = document.getElementById("response");
  // 最初のトークンで読み込みインジケータを削除
  if (response.textContent.includes("Thinking")) {
    response.textContent = token;
  } else {
    response.textContent += token;
  }
}

ネットワークの問題への対応

import anthropic
import time
 
def stream_with_retry(prompt, max_retries=3):
    client = anthropic.Anthropic()
 
    for attempt in range(max_retries):
        try:
            with client.messages.stream(
                model="claude-sonnet-5",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}]
            ) as stream:
                for text in stream.text_stream:
                    yield text
            return  # 成功
 
        except anthropic.APIConnectionError as e:
            if attempt == max_retries - 1:
                raise  # 最後の試み
            wait_time = 2 ** attempt  # 指数バックオフ
            print(f"Retry in {wait_time}s...")
            time.sleep(wait_time)

パフォーマンスを引き出すヒント

  1. 長い応答には常にストリーミングを使用 — より良い UX と認識される遅延の短縮
  2. デバウンスを実装 — 高速なストリームで各トークンで UI を更新しない
  3. 接続プーリングを使用 — 複数のストリームで HTTP 接続を再利用
  4. ストリーム ヘルスを監視 — エラーと再試行失敗を追跡
  5. 合理的なタイムアウトを設定 — ハングしている接続を防止
⚠️
ユーザーに表示する前に完全な応答をバッファリングしないでください。最良のユーザー エクスペリエンスのためにトークンが到着したらすぐにストリーミングしてください。

実装例: ストリーミング チャット アプリケーション

import anthropic
import json
 
def chat_with_streaming(conversation_history: list[dict]):
    """メモリ付きチャット会話をストリーム"""
    client = anthropic.Anthropic()
 
    # システム コンテキストを追加
    messages = [
        {
            "role": "user",
            "content": "You are a helpful assistant."
        }
    ] + conversation_history
 
    full_response = ""
 
    print("Assistant: ", end="", flush=True)
 
    with client.messages.stream(
        model="claude-sonnet-5",
        max_tokens=1024,
        messages=messages
    ) as stream:
        for text in stream.text_stream:
            full_response += text
            print(text, end="", flush=True)
 
    print()  # 新しい行
    return full_response
 
# 使用例
history = []
while True:
    user_input = input("You: ")
    if user_input.lower() == "quit":
        break
 
    history.append({"role": "user", "content": user_input})
    assistant_response = chat_with_streaming(history)
    history.append({"role": "assistant", "content": assistant_response})

次のステップ

  • チャット アプリケーションにストリーミングを実装
  • ストリーミングを使用してリアルタイム ダッシュボードを構築
  • ストリーミング実装にエラー復旧を追加
  • 高速ストリーム用の UI 更新を最適化

実装を次に進めるなら、App Router での組み込み手順をまとめた Claude API のSSEストリーミングをNext.js App Routerに実装する が続きとして読みやすいはずです。エラーも出さずに止まるストリームの検知と途中再開については、Claude API のストリーミングが「エラーも出さずに」止まるとき に運用の詳細をまとめています。

ストリーミングは体験が華やかな反面、個人開発で私自身が実際に組んでみると、つまずくのはたいてい「途中で切れた接続の後始末」でした。Dolice Labs のアプリでも、UI を凝る前にまず再接続とエラー時のフォールバックを固めるようにしています。見栄えは後から足せますが、壊れ方の設計は最初に決めておくほうが楽です。実装では、最後に受け取ったイベントのインデックスを保持しておき、切断時はそこから再開できるようにすると、ユーザーには一瞬の引っかかり程度で復帰でき、体験の毀損を最小限に抑えられます。

検証時に確認すべきこと

  • 本番相当の負荷をかけた状態で再現できるか
  • ログだけでなくメトリクスにエラー率を出しているか
  • 失敗時の人間への通知が遅延なく届くか
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-06-27
Claude API のストリーミングが「エラーも出さずに」止まるとき — 沈黙する停止を検知して途中から続ける運用メモ
Claude API のストリーミングが例外も出さずに無言で止まる「沈黙停止」を、トークン間隔のウォッチドッグで検知し、受信済みテキストを引き継いで途中から再開する実装と、長時間の自動運用で崩れないためのタイムアウト予算の設計をまとめます。
API & SDK2026-05-06
Claude API × Python 実践:ツール呼び出しとストリーミングを組み合わせてAIアシスタントを作る
Claude API の Tool Use とStreamingを同時に使うPython実装を解説。ツールを定義してリアルタイム応答するAIアシスタントの完成コードと、組み合わせ時に詰まりやすいポイントを丁寧に解説します。
API & SDK2026-05-04
Claude API の stop_sequences が効かないときに確認すべき5つのポイント
Claude API の stop_sequences パラメータが期待通りに止まらない問題の原因を、トークン境界・空白・Tool Use・ストリーミングの観点から実例コード付きで切り分ける手順をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →