Node.js 50行未満でストリーミング応答するClaudeチャットボットを作る方法
ストリーミングは、難しそうに聞こえるのに、ユーザー体験をまったく別物にしてしまう機能のひとつです。3〜5秒間スピナーを見つめる代わりに、ユーザーは応答が単語ごとに表示されるのを見ることになります――まるで誰かが入力しているのを眺めているように。
Node.jsでClaudeを使ってそれを行う方法を紹介します。全体は50行未満です。
完全なコード
const Anthropic = require('@anthropic-ai/sdk');
const readline = require('readline');
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const history = [];
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
async function chat(userMessage) {
history.push({ role: 'user', content: userMessage });
process.stdout.write('
Claude: ');
let fullResponse = '';
const stream = await client.messages.stream({
model: 'claude-opus-4-5',
max_tokens: 1024,
messages: history
});
for await (const chunk of stream) {
if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
process.stdout.write(chunk.delta.text);
fullResponse += chunk.delta.text;
}
}
console.log('
');
history.push({ role: 'assistant', content: fullResponse });
}
function prompt() {
rl.question('You: ', async (input) => {
if (input.toLowerCase() === 'quit') return rl.close();
await chat(input);
prompt();
});
}
prompt();
以上です。次のコマンドで実行してください:
npm install @anthropic-ai/sdk
ANTHROPIC_API_KEY=your_key node chatbot.js
ストリーミングが実際にどう動いているか
ClaudeのAPIはServer-Sent Events(SSE)を使用します。messages.stream() を呼び出すと、接続が開いたままになり、サーバーが生成に合わせてチャンクをプッシュしてきます。
各チャンクには type があります。注目すべきものは:
| Type | 意味 |
|---|---|
content_block_start |
Claudeがテキストブロックの開始を行おうとしています |
content_block_delta |
ここに次のテキストの断片があります |
content_block_stop |
そのブロックは完了です |
message_stop |
レスポンス全体が完了です |
実際の単語が入っているのは text_delta 付きの content_block_delta だけです。ここです。
Expressサーバーに追加する
テスト目的ならターミナル版で問題ありません。これをHTTPエンドポイントとして公開する方法は次のとおりです:
const express = require('express');
const Anthropic = require('@anthropic-ai/sdk');
const app = express();
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
app.use(express.json());
app.post('/chat', async (req, res) => {
const { messages } = req.body;
// SSEヘッダーを設定する
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = await client.messages.stream({
model: 'claude-opus-4-5',
max_tokens: 1024,
messages
});
for await (const chunk of stream) {
if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
res.write(`data: ${JSON.stringify({ text: chunk.delta.text })}
`);
}
}
res.write('data: [DONE]
');
res.end();
});
app.listen(3000);
フロントエンドでストリームを消費する
async function sendMessage(messages) {
const response = await fetch('/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ messages })
});
返却形式: {"translated": "翻訳されたHTML"}const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true });
const lines = buffer.split('
');
buffer = lines.pop(); // 不完全な行をバッファに保持する
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
const { text } = JSON.parse(data);
// テキストをUI要素に追加する
document.getElementById('response').textContent += text;
}
}
}
}
よくある間違い
1. バッファの扱いを正しくない
SSEのチャンクは、常にJSONの境界に揃っているとは限りません。上記の buffer のパターンは、チャンクがJSONの途中で分割された場合に対応します。
2. レスポンスの終了処理を忘れる
ストリームが終わった後に res.end() を呼ばないと、クライアントはそれ以上のデータを待ち続けてしまいます。
3. 正しいヘッダーを設定していない
Content-Type: text/event-stream がないと、ブラウザはそれをSSEとして扱わず、何も描画せずにレスポンス全体が返ってくるのを待ちます。
4. 分離せずに複数ユーザーへストリーミングする
それぞれのリクエストには、そのリクエスト専用のストリームインスタンスが必要です。リクエスト間でストリームの状態を共有しないでください。
コストの疑問
ストリーミングはトークン数を変えません。ストリームしてもしなくても同じトークン数を支払うことになります。変わるのは体感のレイテンシです。2秒の応答がストリーミングされると、まとめて一度に表示される2秒の応答よりも体感で3倍速く感じられます。
トークン単位のAPIを構築しているなら、このレイテンシ改善は無料です。もし定額制API(SimplyLouie のような月$2)を構築しているなら、コスト計算を気にする必要がまったくありません—常にすべてストリーミングしましょう。
次に作るべきもの
ストリーミングが動くようになったら、自然な次のステップは以下です:
- タイプ表示(インジケータ): 最初のチャンクが届く前に「Claudeが考えています...」を表示する
- 停止ボタン: 長い応答をストリームの途中でユーザーに中断させる
- トークンカウント: 応答がストリームされるのに合わせてライブのトークンカウンタを表示する
-
会話のエクスポート:
message_stopの後に、完全なストリーミング応答を履歴に保存する
上記の完全なコードはそのままで動きます。そのままコピーして、APIキーを設定して、実行してください。2分以内にストリーミング対応のClaudeチャットボットができあがります。
ClaudeのAPIを使って開発していますか? SimplyLouie は月$2の定額でAPIアクセスを提供しています—トークンカウントなし、請求のサプライズなし。




