😇
# Streamlit記事生成エンジンを永続安定化するための完全実践マニュアル ─ `KeyError`連鎖とタイムアウト地獄から抜け出す最
はじめに
PythonだけでWebアプリが構築できるStreamlitは、データ可視化からAIデモまで爆速で作れる便利さと引き換えに、ウィジェットとsession_state
の扱いを誤ると想像以上に深刻なトラブルを呼び込みます。本稿では、実際に発生した「記事生成ステップがKeyError
連発で停止→リトライ暴走→タイムアウト」という典型的な障害を題材に、原因の深層構造を解体しながら再発しないコード設計・デバッグ戦略・運用フローを余すところなく解説します。
1. 障害の全体像を正確に把握する
1.1 表面症状
- 記事生成フロー中に
KeyError: 'h1'
が発生し、修正後もKeyError: 'h2'
が続発 - アプリ側で定義したリトライ処理が無制限に稼働し、実質的に無限ループ
- 外部API(Claudeなど)へのリクエストが雪だるま式に増加し、タイムアウトまたは最大リトライ上限に達して強制終了
- ユーザー視点では生成が完了せず画面が固まったように見える
1.2 影響範囲と潜在的リスク
- セッションに壊れたアウトラインデータが残存し、次回生成にも悪影響
- 外部API従量課金のコストが爆発的に増大
- タイムアウト後の例外がUIレスポンスを阻害し、UXを大幅に劣化
- 最悪の場合、サーバー側プロセスがメモリリークでクラッシュ
2. 原因の深層を掘り下げる
2.1 データ契約の崩壊
- 記事生成サービスは
{"h1": str, "h2": List[str]}
というごくシンプルな辞書を期待 - 実際には
{"title": "見出し", "subsections": [{"title": "小見出しA"}]}
のような多段ネストが未処理で送信 - 途中で
h1
キーやh2
キーが空・欠落・辞書になり、解析時点でKeyError
session_state
競合
2.2 Streamlit特有の罠 ─ - ウィジェット描画後に同一キーを直接書き換えると
StreamlitAPIException
- ユーザーがキーワードを再入力→即
st.session_state.keyword = new_val
実行→裏でレンダリング済みウィジェットと衝突
2.3 密結合ロジックと低可観測性
- アウトライン整形とAPI呼び出しが同関数内で行われ、責務が不明瞭
- ログ粒度が粗く、どの段階で壊れたか特定に時間を要した
3. 適用した二大修正策
3.1 ペンディング方式で安全にキーワード更新
3.1.1 実装手順
-
「追加キーワードで再検索」ボタン押下→
st.session_state.pending_keyword_update
に新キーワードを退避 -
main()
関数冒頭でペンディング変数を検査 -
存在すれば以下を実行
-
st.session_state.keyword
へ安全に代入 -
has_searched
やoutline_data
など関連ステートを初期化 - 処理後
pending_keyword_update
を削除してst.rerun()
不要化
-
3.1.2 得られた効果
- ウィジェット競合による例外を完全排除
- 再検索もワンクリックでシームレスに遷移
3.2 アウトライン正規化ロジックの強化
3.2.1 正規化仕様
section_data = {
"h1": str, # 大見出し文字列
"h2": List[str] # 小見出し文字列リスト(空でもリストで保持)
}
3.2.2 変換アルゴリズム
-
title
キーがあればh1
へ昇格、無ければ既存h1
を使用 -
subsections
やh2
が辞書リストなら各title
を抽出しリスト化 - 小見出しが皆無でも必ず
[]
をセットしNull参照を回避 - 大見出し欠損時はフォールバックで「タイトル不明」を挿入し
KeyError
防止
4. 安定稼働のための設計指針
4.1 データ契約を境界で保証
- 受け取った瞬間に「正規形」へ変換し、下流では一切形状が変わらない保証を作る
- 変換関数にユニットテストを付与し、CIで常時バリデーション
session_state
をイベント駆動で扱う
4.2 -
ウィジェット生成後は同名キーを安易に書き替えない
-
値を変える必要がある場合は
- ペンディング変数+初期化判定
- on_changeコールバックで更新
- 入力フォームを別画面として分離
4.3 観測可能性を高めるログ設計
- 外部API呼び出し単位で
trace_id
を生成し、全レイヤで共通ログ - リトライ回数・実行時間をメトリクスとして集計し、しきい値超過で警告
5. 実戦的デバッグフロー
5.1 再現性の高い最小ケースを作成
- 壊れたアウトラインと同じJSONをハードコードし、生成関数だけ実行
- UI・APIを切り離してロジック層を単体テスト
5.2 データ形状を逐次ダンプ
- 各ステージで
json.dumps(data, ensure_ascii=False, indent=2)
をログ出力 - 欠損キーや型違いを即座に視覚確認
5.3 バイナリサーチ的ロールバック
- コミット単位で過去に戻し、正常動作する境界を確定
- 境界コミット間の差分を精査しバグ挿入箇所を特定
5.4 自動回帰テストの導入
- pytest+Streamlit Testingを用い、代表的な入力パターンで生成成功をCI検証
- 将来仕様追加で壊れてもプルリク時点で検知
6. 運用フェーズでの再発防止策
6.1 リトライポリシーの見直し
- 外部API失敗時は指数バックオフ+最大試行数を小さく設定
- 同一アウトラインで連続エラーなら自動的に「失敗セクションをスキップ」モードへ移行
6.2 タイムアウトとメモリリーク監視
- 生成ロジック単体の最大実行時間を設定し、超過時は優雅にキャンセル
- メモリ使用量をプロセス単位で定期送信し、異常値で自動リスタート
6.3 障害時のUX維持
- 生成失敗時に「部分生成記事+エラー詳細」をダウンロード可能にし、ユーザーに作業エビデンスを残す
- 再試行ボタンは非同期で実行し、UIをブロックしない設計へ変更
7. まとめ ─ 再度同じ罠に落ちないために
- データ契約は「境界で正規化」「ユニットテストで常時保証」
-
session_state
はイベント駆動+ペンディング戦略で競合排除 - リトライ・タイムアウト・メモリを数値で監視し、自動復旧も視野に入れる
これらを組み込むことで、記事生成エンジンは高負荷環境でも安定動作し、無駄なAPIコストやユーザー離脱を最小限に抑えられます。初期フェーズから「例外を例外視しない」堅牢設計を心掛け、Streamlitが持つ爆速開発の恩恵を最大化しましょう。
Discussion