v1.7.1 — Converse Stream API + ストリーミング UX 改善
マルチルート合成に Bedrock Converse Stream API を導入し、AgentCore Gateway の応答にタイピング効果を適用したリリースです。モニタリングクエリに時間範囲フィルターを追加し、pg pool 枯渇の問題も解決しました。
Converse Stream API · タイピング効果 (50char/15ms) · チャットバブル幅の安定化 · モニタリングクエリ時間フィルター 3 件
Converse Stream API によるリアルタイム合成
マルチルート応答の合成が、擬似チャンキングから本物のトークンストリーミングへ移行しました。
Before / After
| 項目 | Before (v1.7.0) | After (v1.7.1) |
|---|---|---|
| 合成 API | InvokeModelCommand | ConverseStreamCommand |
| 合成関数 | synthesizeResponses() | synthesizeResponsesStreaming() |
| ストリーミング方式 | 応答全体を生成 → simulateStreaming() | トークン生成と同時に chunk イベント送信 |
| 初回トークン遅延 | 応答全体の完了まで待機(5〜15 秒) | ~200ms(最初のトークンを即時送信) |
| ユーザー体感 | 長い待機の後に高速出力 | 即時開始、自然な流れ |
コード変更
Before — synthesizeResponses() + simulateStreaming():
// src/app/api/ai/route.ts (v1.7.0)
const synthesized = await synthesizeResponses(
lastMsg, successful, modelKey, clientLang
);
// 完成したテキストを 50 文字ずつ分割して擬似ストリーミング
await simulateStreaming(synthesized, send);
After — synthesizeResponsesStreaming() + ConverseStreamCommand:
// src/app/api/ai/route.ts (v1.7.1)
const synthesized = await synthesizeResponsesStreaming(
lastMsg, successful, send, modelKey, clientLang
);
// send() コールバックが contentBlockDelta ごとに即時 chunk を送信
synthesizeResponsesStreaming() の中核ロジック:
const response = await bedrockClient.send(new ConverseStreamCommand({
modelId,
system: [{ text: systemPrompt }],
messages: [{ role: 'user', content: [{ text: synthesisPrompt }] }],
inferenceConfig: { maxTokens: 4096 },
}));
for await (const event of response.stream) {
if (event.contentBlockDelta?.delta?.text) {
const text = event.contentBlockDelta.delta.text;
fullContent += text;
send('chunk', { delta: text }); // トークンを即時送信
}
}
タイピング効果 (simulateStreaming)
AgentCore Gateway は完成したテキストを一度に返すため、タイピング効果をシミュレートします。
// src/app/api/ai/route.ts
const CHUNK_SIZE = 50; // チャンクあたり 50 文字
const CHUNK_DELAY_MS = 15; // チャンク間 15ms のディレイ
async function simulateStreaming(
text: string,
send: (event: string, data: any) => void,
): Promise<void> {
for (let i = 0; i < text.length; i += CHUNK_SIZE) {
const chunk = text.slice(i, i + CHUNK_SIZE);
send('chunk', { delta: chunk });
if (i + CHUNK_SIZE < text.length) {
await new Promise(r => setTimeout(r, CHUNK_DELAY_MS));
}
}
}
適用対象(3 箇所):
- 単一 AgentCore Gateway 応答 — 完成したテキストにタイピング効果を適用
- マルチルート合成 (v1.7.0) —
synthesizeResponses()の結果に適用(v1.7.1 で Converse Stream に置き換え) - マルチルート単一成功 — 1 つの Gateway のみ成功した場合
Bedrock の直接呼び出しは InvokeModelWithResponseStreamCommand で本物のストリーミングが可能ですが、AgentCore Gateway は内部でツールを複数回呼び出した後に最終テキストのみを返します。ユーザーに一貫したストリーミング体験を提供するため、50 文字/15ms のタイピング効果を適用しています。
チャットバブル幅の安定化 (PR #8)
SSE ストリーミング中にチャットバブルの幅が突然変わる問題を修正しました。
| 項目 | Before | After |
|---|---|---|
| CSS | max-w-5xl | w-full max-w-5xl |
| 動作 | コンテンツ増加時に幅がジャンプ | 最初のチャンクから最大幅に固定 |
| ファイル | src/app/ai/page.tsx | 同一 |
Before — 幅がジャンプ:
<div class="max-w-5xl rounded-lg px-4 py-3 ...">
<!-- コンテンツが短いときは狭く、長くなると突然広がる -->
</div>
After — 安定した幅:
<div class="w-full max-w-5xl rounded-lg px-4 py-3 ...">
<!-- 最初から w-full で最大幅を確保し、max-w-5xl で上限を制限 -->
</div>
SSE ストリーミングのようにコンテンツが段階的に追加されるコンテナには w-full max-w-{size} の組み合わせを使用してください。
max-w-{size} のみを使用すると、コンテンツ量に応じて幅が変わります。
モニタリングクエリの時間範囲フィルター
時間範囲の制限がないモニタリングクエリが Steampipe API を通じて全履歴データを取得し、pg pool の 5 コネクションをすべて占有してしまう問題を修正しました。
| クエリ | ファイル | Before | After |
|---|---|---|---|
ec2CpuHourly | queries/metrics.ts | 時間制限なし | WHERE timestamp >= NOW() - INTERVAL '24 hours' |
ebsIopsHourly | queries/metrics.ts | WHERE r.timestamp IS NOT NULL | AND r.timestamp >= NOW() - INTERVAL '24 hours' |
rdsCpuDaily | queries/metrics.ts | 時間制限なし | WHERE timestamp >= NOW() - INTERVAL '30 days' |
46+ の EC2 インスタンス + 多数の EBS ボリュームがある環境では、時間制限のないクエリが数分間コネクションを占有します。
pg pool の max: 5 がすべて使用されると、ダッシュボードとすべてのページが statement_timeout: 120s まで待機した後に失敗します。
3 つのストリーミングモードまとめ
v1.7.1 では、AWSops の AI 応答は状況に応じて 3 つのストリーミングモードを使用します。
| モード | 使用タイミング | 実装 | 初回トークン遅延 |
|---|---|---|---|
| Real Streaming | Bedrock 直接呼び出し | InvokeModelWithResponseStreamCommand | ~200ms |
| Converse Stream | マルチルート合成 | ConverseStreamCommand + contentBlockDelta | ~200ms |
| Simulated Streaming | AgentCore Gateway 応答 | simulateStreaming()(50 文字/15ms) | 応答完了後すぐ |
3 つのモードすべてが同じ SSE chunk イベントを使用するため、クライアント(src/app/ai/page.tsx)はモードを区別せずに streamingContent 状態へ delta を蓄積し、ReactMarkdown でリアルタイムにレンダリングします。