SSEストリーミング順序・消失問題の修正
問題の概要
OpenWebUIでチャット回答のストリーミング表示に以下の問題が発生:
- 参考文献セクションの途中まで出力された後、セクション全体が消える
- 表示順序が入れ替わる(途中で戻る)
- FW(ファイアウォール/プロキシ)経由の場合のみ発生(ローカルでは正常)
- 別のチャットを開いて戻るとDBから正しく表示される
構成
agentic-ai-srv (OpenAI互換API) → FW/Proxy → OpenWebUI (OpenAI API設定)
根本原因
FWがSSEストリームをバッファリング・分割し、OpenWebUIがチャンクを正しく再構築できない
参考: OpenWebUI Discussion #13477
SSE(Server-Sent Events)はHTTP上の長時間接続を使用するため、FW/プロキシがこれを正しく処理しない場合、以下の問題が発生する:
- チャンクのバッファリングと一括送信
- 順序の入れ替わり
- タイムアウトによるチャンクの破棄
- 接続の早期切断
解決策
サーバー側(agentic-ai-srv)で以下の対策を実装:
1. レスポンスヘッダーの強化
return StreamingResponse(
generate_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # Nginx
"X-Content-Type-Options": "nosniff",
"Pragma": "no-cache",
"Expires": "0",
}
)
2. チャンクサイズの制御
大きなテキストを50文字ごとに分割して送信:
MAX_CHUNK_SIZE = 50
for i in range(0, len(delta), MAX_CHUNK_SIZE):
sub_delta = delta[i:i + MAX_CHUNK_SIZE]
chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": int(time.time()),
"choices": [{"index": 0, "delta": {"content": sub_delta}, "finish_reason": None}]
}
yield f"id: {chunk_id}\ndata: {json.dumps(chunk)}\n\n"
chunk_id += 1
await asyncio.sleep(0.005) # 5ms遅延
3. Keep-alive間隔の短縮
# 変更前: 5秒
KEEPALIVE_INTERVAL = 5
# 変更後: 1秒
KEEPALIVE_INTERVAL = 1
4. SSEイベントIDの追加
各チャンクにイベントIDを付与して順序を保証:
chunk_id = 0
yield f"id: {chunk_id}\ndata: {json.dumps(chunk)}\n\n"
chunk_id += 1
修正ファイル
| ファイル | 変更内容 |
|---|---|
| server/app.py | Keep-alive間隔を5秒→1秒に短縮 |
| server/app.py | 大きなテキストを50文字ごとに分割送信 |
| server/app.py | 各チャンク送信後に5ms遅延を追加 |
| server/app.py | レスポンスヘッダーにPragma, Expires追加 |
| server/app.py | SSEイベントIDを追加 |
ブランチ
feature/fix-streaming-chunk-order
検証方法
- 修正後、FW経由でOpenWebUIからテスト
- 参考文献を含む長い回答が正しく表示されることを確認
- テキストの順序が入れ替わらないことを確認
- ブラウザDevToolsのNetworkタブでSSEイベントを監視
代替案: FW設定の変更
FW管理者に確認すべき設定:
- SSE/長時間HTTP接続のバッファリング無効化
- アイドルタイムアウトを60秒以上に設定
text/event-streamを即時転送する設定- SSEストリームをWAF検査対象から除外