Streamlit ChatGPT アプリ開発完全ガイド

はじめに

StreamlitとChatGPT APIを組み合わせたWebアプリケーション開発は、AI技術を活用したプロダクト開発において重要なスキルとなっています。本記事では、基本的な実装から本格運用まで、実践的な開発手法を体系的に解説します。

1. Streamlitの技術的基盤

1.1 アーキテクチャの理解

Streamlitは、Pythonスクリプトを逐次実行してWebアプリケーションを構築するリアクティブプログラミングフレームワークです。ユーザーがインターフェース要素を操作する度に、Pythonスクリプト全体が上から下へ再実行される仕組みを採用しています。

主要コンポーネント:

  • Frontend: React.js + TypeScript
  • Backend: Tornado WebServer
  • Communication: WebSocket
  • State Management: In-memory Storage

1.2 ChatGPT API統合の考慮事項

OpenAI ChatGPT APIとの統合において重要な技術的要件:

  • レート制限対応: API呼び出し頻度の制御
  • トークン管理: 入力トークン数の計算と上限制御
  • エラーハンドリング: ネットワーク障害とAPI障害の処理
  • セキュリティ: APIキー保護と入力検証

2. 基本実装

2.1 開発環境セットアップ

必要なライブラリ:

streamlit==1.29.0
openai==1.3.5
python-dotenv==1.0.0
pandas==2.1.4
numpy==1.24.3

環境構築:

python -m venv streamlit_env
source streamlit_env/bin/activate
pip install -r requirements.txt

2.2 基本アプリケーション実装

import streamlit as st
import openai
from dotenv import load_dotenv
import os

load_dotenv()

class ChatGPTClient:
    def __init__(self):
        self.client = openai.OpenAI(
            api_key=os.getenv("OPENAI_API_KEY")
        )
        self.model = "gpt-3.5-turbo"
    
    def generate_response(self, messages):
        try:
            response = self.client.chat.completions.create(
                model=self.model,
                messages=messages,
                max_tokens=1000,
                temperature=0.7
            )
            return response.choices[0].message.content
        except Exception as e:
            return f"エラーが発生しました: {str(e)}"

def main():
    st.set_page_config(
        page_title="ChatGPT Assistant",
        page_icon="🤖"
    )
    
    st.title("🤖 ChatGPT Assistant")
    
    if "messages" not in st.session_state:
        st.session_state.messages = []
    if "chatgpt_client" not in st.session_state:
        st.session_state.chatgpt_client = ChatGPTClient()
    
    # 会話履歴表示
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.markdown(message["content"])
    
    # ユーザー入力処理
    if prompt := st.chat_input("メッセージを入力してください"):
        st.session_state.messages.append({"role": "user", "content": prompt})
        
        with st.chat_message("user"):
            st.markdown(prompt)
        
        with st.chat_message("assistant"):
            with st.spinner("回答生成中..."):
                response = st.session_state.chatgpt_client.generate_response(
                    st.session_state.messages
                )
            st.markdown(response)
        
        st.session_state.messages.append({"role": "assistant", "content": response})

if __name__ == "__main__":
    main()

2.3 環境変数設定

# .env
OPENAI_API_KEY=your_api_key_here

実行方法:

streamlit run app.py

3. 高度な機能実装

3.1 ストリーミングレスポンス

リアルタイムでChatGPTのレスポンスを表示する機能:

def generate_streaming_response(self, messages):
    try:
        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            stream=True
        )
        
        full_response = ""
        placeholder = st.empty()
        
        for chunk in response:
            if chunk.choices[0].delta.content:
                full_response += chunk.choices[0].delta.content
                placeholder.markdown(full_response + "▌")
        
        placeholder.markdown(full_response)
        return full_response
        
    except Exception as e:
        return f"エラー: {str(e)}"

3.2 セッション管理最適化

import hashlib

def optimize_session_state():
    # メッセージ履歴の制限(直近100件)
    if len(st.session_state.messages) > 100:
        st.session_state.messages = st.session_state.messages[-100:]

def create_message_hash(messages):
    message_string = "".join([f"{msg['role']}:{msg['content']}" for msg in messages])
    return hashlib.md5(message_string.encode()).hexdigest()

@st.cache_data(ttl=3600)
def get_cached_response(message_hash, model, temperature):
    # キャッシュ機能の実装
    return None

3.3 エラーハンドリング強化

import time
import random
from functools import wraps

def retry_with_backoff(max_retries=3):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except openai.RateLimitError as e:
                    if attempt == max_retries:
                        raise e
                    delay = (2 ** attempt) + random.uniform(0, 1)
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

class ResilientChatGPTClient(ChatGPTClient):
    @retry_with_backoff(max_retries=3)
    def generate_response_with_retry(self, messages):
        return super().generate_response(messages)

4. ユーザーインターフェース改良

4.1 カスタムCSS適用

def apply_custom_css():
    st.markdown("""
    <style>
    .main .block-container {
        padding-top: 2rem;
        max-width: 1200px;
    }
    
    .stChatMessage {
        background-color: #f8f9fa;
        border-radius: 10px;
        padding: 15px;
        margin: 10px 0;
    }
    
    .stButton > button {
        border-radius: 20px;
        background: linear-gradient(45deg, #007bff, #0056b3);
        color: white;
    }
    </style>
    """, unsafe_allow_html=True)

4.2 サイドバー設定機能

def create_sidebar():
    with st.sidebar:
        st.header("設定")
        
        model_option = st.selectbox(
            "モデル選択",
            ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"]
        )
        
        temperature = st.slider(
            "創造性(Temperature)",
            min_value=0.0,
            max_value=2.0,
            value=0.7,
            step=0.1
        )
        
        if st.button("会話履歴をクリア"):
            st.session_state.messages = []
            st.experimental_rerun()
        
        return model_option, temperature

4.3 分析機能

def create_analytics():
    st.subheader("📊 会話分析")
    
    if not st.session_state.messages:
        st.info("会話を開始してください")
        return
    
    user_messages = [msg for msg in st.session_state.messages if msg["role"] == "user"]
    assistant_messages = [msg for msg in st.session_state.messages if msg["role"] == "assistant"]
    
    col1, col2, col3 = st.columns(3)
    
    with col1:
        st.metric("ユーザーメッセージ数", len(user_messages))
    
    with col2:
        st.metric("アシスタントメッセージ数", len(assistant_messages))
    
    with col3:
        total_chars = sum(len(msg["content"]) for msg in st.session_state.messages)
        st.metric("総文字数", f"{total_chars:,}")

5. データ永続化

5.1 SQLiteデータベース統合

import sqlite3
import json
from datetime import datetime

class ConversationDatabase:
    def __init__(self, db_path="conversations.db"):
        self.db_path = db_path
        self.init_database()
    
    def init_database(self):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS sessions (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    session_id TEXT UNIQUE NOT NULL,
                    title TEXT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """)
            
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS messages (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    session_id TEXT NOT NULL,
                    role TEXT NOT NULL,
                    content TEXT NOT NULL,
                    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    FOREIGN KEY (session_id) REFERENCES sessions (session_id)
                )
            """)
            
            conn.commit()
    
    def save_message(self, session_id, role, content):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO messages (session_id, role, content)
                VALUES (?, ?, ?)
            """, (session_id, role, content))
            conn.commit()
    
    def load_session_messages(self, session_id):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("""
                SELECT role, content FROM messages 
                WHERE session_id = ? 
                ORDER BY timestamp ASC
            """, (session_id,))
            
            return [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]

5.2 セッション管理

def initialize_session_with_db():
    if "session_id" not in st.session_state:
        st.session_state.session_id = f"session_{int(time.time())}"
        st.session_state.db = ConversationDatabase()
        st.session_state.db.create_session(st.session_state.session_id)
    
    if "messages" not in st.session_state:
        saved_messages = st.session_state.db.load_session_messages(
            st.session_state.session_id
        )
        st.session_state.messages = saved_messages

def save_message_to_db(role, content):
    st.session_state.db.save_message(
        st.session_state.session_id,
        role,
        content
    )

6. セキュリティ対策

6.1 入力検証

class InputValidator:
    @staticmethod
    def validate_user_input(text):
        if len(text) > 10000:
            return False, "入力が長すぎます"
        
        forbidden_patterns = ["DROP TABLE", "DELETE FROM", "<script>"]
        text_upper = text.upper()
        
        for pattern in forbidden_patterns:
            if pattern in text_upper:
                return False, f"禁止パターン: {pattern}"
        
        return True, "OK"
    
    @staticmethod
    def sanitize_output(text):
        import html
        return html.escape(text)

6.2 レート制限

def implement_rate_limiting(user_id="default"):
    current_time = time.time()
    
    if f"rate_limit_{user_id}" not in st.session_state:
        st.session_state[f"rate_limit_{user_id}"] = []
    
    user_requests = st.session_state[f"rate_limit_{user_id}"]
    
    # 過去1分間のリクエスト数チェック
    recent_requests = [
        req_time for req_time in user_requests
        if current_time - req_time < 60
    ]
    
    if len(recent_requests) >= 20:  # 1分間20リクエスト制限
        return False
    
    recent_requests.append(current_time)
    st.session_state[f"rate_limit_{user_id}"] = recent_requests
    
    return True

7. デプロイメント

7.1 Streamlit Cloudデプロイ

requirements.txt:

streamlit>=1.29.0
openai>=1.3.5
python-dotenv>=1.0.0

.streamlit/config.toml:

[server]
headless = true
port = 8501

[theme]

primaryColor = “#007bff” backgroundColor = “#ffffff”

secrets.toml:

[openai]
api_key = "your_api_key"

7.2 Docker化

Dockerfile:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

EXPOSE 8501

CMD ["streamlit", "run", "app.py"]

docker-compose.yml:

version: '3.8'

services:
  streamlit-app:
    build: .
    ports:
      - "8501:8501"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    volumes:
      - ./data:/app/data

8. パフォーマンス最適化

8.1 キャッシュ戦略

@st.cache_data(ttl=600)
def get_model_response(message_hash, model, temperature):
    # 実際のAPI呼び出し
    pass

@st.cache_resource
def initialize_chatgpt_client():
    return ChatGPTClient()

8.2 メモリ管理

def optimize_memory():
    import gc
    
    # セッション状態のクリーンアップ
    if len(st.session_state.messages) > 50:
        st.session_state.messages = st.session_state.messages[-50:]
    
    # ガベージコレクション実行
    gc.collect()

9. 監視とログ

9.1 基本ログ機能

import logging

def setup_logging():
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler('app.log'),
            logging.StreamHandler()
        ]
    )
    
    return logging.getLogger(__name__)

logger = setup_logging()

def log_user_interaction(action, details):
    logger.info(f"User action: {action}, Details: {details}")

9.2 エラー追跡

def handle_error_with_logging(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            logger.error(f"Error in {func.__name__}: {str(e)}")
            st.error("申し訳ございません。エラーが発生しました。")
            return None
    return wrapper

10. 技術的制約と限界

10.1 Streamlitの制約

パフォーマンス制約:

  • リアクティブプログラミングモデルによる再実行オーバーヘッド
  • 同時接続数の制限
  • WebSocket通信の遅延

スケーラビリティ制約:

  • 単一プロセスでの動作
  • メモリ使用量の増加
  • リアルタイム更新の困難

10.2 不適切なユースケース

避けるべき用途:

  • 高頻度リアルタイム処理
  • 大規模商用アプリケーション
  • ミッションクリティカルシステム
  • 高速APIサービス

10.3 代替アーキテクチャ

適切でない場合の代替案:

  • FastAPI + React: 高性能API + 柔軟UI
  • Flask + Vue.js: 軽量バックエンド + モダンフロントエンド
  • Next.js + Node.js: フルスタックJavaScript

11. 実践的推奨事項

11.1 開発フェーズ戦略

フェーズ1: プロトタイプ (1-2週間)

  • 基本的なChatGPT連携
  • シンプルなUI実装
  • 基本的なエラーハンドリング

フェーズ2: 機能拡張 (3-4週間)

  • データ永続化
  • ユーザー管理
  • 監視機能追加

フェーズ3: 本格運用 (4-6週間)

  • セキュリティ強化
  • パフォーマンス最適化
  • デプロイメント自動化

11.2 技術選択指針

用途推奨度理由
プロトタイピング★★★★★高速開発、低学習コスト
社内ツール★★★★☆限定ユーザー、運用コスト最小
MVP★★★☆☆迅速な市場投入、後の移行前提
エンタープライズ★★☆☆☆プロトタイプのみ推奨

11.3 成功のポイント

DO (推奨):

  • 適切な用途での活用
  • セキュリティ対策の実装
  • パフォーマンス最適化
  • 段階的な機能拡張

DON’T (非推奨):

  • 不適切な用途での強引な使用
  • セキュリティの軽視
  • スケーラビリティの無視
  • 技術的制約の軽視

まとめ

Streamlit ChatGPTアプリケーション開発は、適切な用途と技術的制約を理解して実装すれば、効果的な開発手法です。プロトタイピングから中規模運用まで対応可能ですが、エンタープライズレベルでは制約があります。

本記事で解説した実装パターン、最適化手法、セキュリティ対策を適用することで、高品質なAI Webアプリケーションを開発できます。技術的成熟度に応じた段階的アプローチを取り、将来的なスケーリング要件も考慮した設計を行うことが成功の鍵となります。

参考資料:

  • Streamlit公式ドキュメント
  • OpenAI API仕様書
  • PostgreSQL性能チューニングガイド
  • Docker セキュリティベストプラクティス