「Open WebUIでは特定モデルの固定や社内ログ要件に対応できず、Pythonで自作したい」
そんな悩みを抱えるインフラ担当者や社内ツール開発者は多い。この記事では、PythonのWebアプリフレームワーク「Streamlit」とOllamaを組み合わせ、社内専用のローカルLLMチャットUIをゼロから構築する手順を段階的に解説する。最小構成から始めて、ストリーミング表示・会話履歴保持・動的モデル切り替えを実装し、systemdとNginxでチームに公開する本番運用の形まで仕上げる。コード量は最終的に70行前後になる。
この記事のポイント
・OllamaのPOST /api/chat とrequestsでStreamlitから直接接続できる
・st.write_streamでストリーミング表示、st.session_stateで会話履歴を保持する
・GET /api/tags でモデル一覧を取得し、サイドバーで動的に切り替える
・本番公開はsystemdサービス化+NginxのWebSocketプロキシ設定で安定運用する
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
StreamlitでOllamaのUIを自作するメリットとOpen WebUIとの違い
Open WebUIはChatGPTに近い操作感を提供する既製品UIだ。インストールするだけで動くため、チームへの素早い展開に向いている。一方で、細かいカスタマイズには制限がある。「全ユーザーに同じシステムプロンプトを強制したい」「社内の認証システムとシングルサインオン連携したい」「会話ログを自社DBに保存したい」といった要件は、Open WebUIだけでは対応が難しい。Streamlitは、PythonのコードだけでWebアプリを作れるオープンソースフレームワークだ。データサイエンスの分野で広く使われているが、LLMフロントエンドとしても実用的な選択肢になっている。OllamaはREST APIを提供しているため、requestsライブラリで直接呼び出せる。HTMLやJavaScriptの知識がなくても、Pythonだけでブラウザから使えるチャットUIが作れる。
Open WebUIとStreamlitの使い分け基準は明確だ。汎用チャット環境を素早く展開するならOpen WebUI、社内業務フローに合わせたカスタムロジックが必要ならStreamlitで自作する方が自由度が高い。特定の業務ドキュメントをアップロードしてローカルLLMで処理させたい場合、ログイン認証を社内SSO連携で行いたい場合は、Streamlit自作の方が現実的だ。
社内でChatGPTが使えない状況でローカルLLMを導入する意義や情シス担当者への説得材料については、社内でChatGPTが使えないときの代替手段も合わせて参照してほしい。
事前準備:Python仮想環境とStreamlitのインストール
Ubuntu Server 24.04 LTSとPython 3.11での動作確認済みだ。Ollamaが起動済みで、少なくとも1つのモデルがダウンロード済みであることを前提とする。Ollamaのセットアップがまだの場合は、Ubuntu ServerでローカルLLMを構築する方法を先に読んでほしい。まずPythonのバージョンを確認し、プロジェクト用のディレクトリと仮想環境を作成する。
# Pythonバージョン確認(3.10以上が必要) $ python3 --version Python 3.11.9 # プロジェクトディレクトリを作成 $ sudo mkdir -p /opt/streamlit-llm $ sudo chown ubuntu:ubuntu /opt/streamlit-llm $ cd /opt/streamlit-llm # 仮想環境を作成して有効化 $ python3 -m venv venv $ source venv/bin/activate # pip install でパッケージを追加する (venv) $ pip install streamlit requests ... Successfully installed streamlit-1.36.0 requests-2.32.3
# Streamlitバージョン確認 (venv) $ streamlit --version Streamlit, version 1.36.0 # Ollamaの疎通確認(モデル一覧を取得) $ curl -s http://localhost:11434/api/tags | python3 -m json.tool | head -12 { "models": [ { "name": "llama3.3:70b-instruct-q4_0", "modified_at": "2026-07-20T11:22:33Z", "size": 42523684864 } ] }
ステップ1:最小構成のチャットUIを実装する
まずは「質問を入力→送信→応答が表示される」だけの最小構成を動かす。会話履歴やストリーミングはこの後のステップで追加する。/opt/streamlit-llm/app.py を以下の内容で作成する。OllamaのAPIは /api/chat エンドポイントを使う。messages配列にrole(user/assistant)とcontentを入れてPOSTするだけで応答が返ってくる。
# /opt/streamlit-llm/app.py(最小構成) import streamlit as st import requests import json OLLAMA_URL = "http://localhost:11434" MODEL = "llama3.3:70b-instruct-q4_0" st.title("社内AIアシスタント") def call_ollama(messages): resp = requests.post( f"{OLLAMA_URL}/api/chat", json={"model": MODEL, "messages": messages}, timeout=120, ) resp.raise_for_status() return resp.json()["message"]["content"] prompt = st.text_area("質問を入力してください", height=100) if st.button("送信"): if prompt.strip(): with st.spinner("応答中..."): answer = call_ollama([{"role": "user", "content": prompt}]) st.write(answer)
# Streamlitアプリを起動(ポート8501) (venv) $ streamlit run app.py --server.port 8501 You can now view your Streamlit app in your browser. Local URL: http://localhost:8501 Network URL: http://192.168.1.10:8501
ステップ2:ストリーミング出力をリアルタイムで表示する
Ollamaの /api/chat は stream: true を指定すると、改行区切りのJSONオブジェクトをトークン単位で逐次送信する。Streamlitのst.write_streamはジェネレータ関数を受け取って出力を逐次表示する仕組みだ。この2つを組み合わせると、ChatGPTのように文字が流れるUXを実現できる。app.pyを以下の内容に書き替える。
# /opt/streamlit-llm/app.py(ストリーミング表示版) import streamlit as st import requests import json OLLAMA_URL = "http://localhost:11434" MODEL = "llama3.3:70b-instruct-q4_0" st.title("社内AIアシスタント") def stream_ollama(messages): with requests.post( f"{OLLAMA_URL}/api/chat", json={"model": MODEL, "messages": messages, "stream": True}, stream=True, timeout=300, ) as resp: resp.raise_for_status() for line in resp.iter_lines(): if line: data = json.loads(line) yield data["message"]["content"] if data.get("done"): break if prompt := st.chat_input("質問を入力してください"): st.chat_message("user").write(prompt) with st.chat_message("assistant"): st.write_stream(stream_ollama([{"role": "user", "content": prompt}]))
なお、この段階ではページをリロードすると会話がリセットされる。前の質問の文脈も引き継げない。次のステップで解決する。
ステップ3:会話履歴をセッションに保持させる
OllamaのAPIは /api/chat に過去のmessages配列をそのまま渡すことで文脈を引き継げる。Streamlitのst.session_stateはブラウザタブが開いている間、変数の状態を保持する機能だ。この2つを組み合わせると、連続した会話ができるUIになる。app.pyを以下の内容に書き替える。変更点は3箇所だ。「① 初回実行時にmessagesを初期化」「② 過去メッセージを画面に表示」「③ 新しいassistantの応答をmessagesに追記」のみで、stream_ollamaの実装は変わらない。
# /opt/streamlit-llm/app.py(会話履歴版) import streamlit as st import requests import json OLLAMA_URL = "http://localhost:11434" MODEL = "llama3.3:70b-instruct-q4_0" st.title("社内AIアシスタント") # ①初回実行時だけmessagesを初期化 if "messages" not in st.session_state: st.session_state.messages = [] def stream_ollama(messages): with requests.post( f"{OLLAMA_URL}/api/chat", json={"model": MODEL, "messages": messages, "stream": True}, stream=True, timeout=300, ) as resp: resp.raise_for_status() for line in resp.iter_lines(): if line: data = json.loads(line) yield data["message"]["content"] if data.get("done"): break # ②過去の会話を表示 for msg in st.session_state.messages: st.chat_message(msg["role"]).write(msg["content"]) # ③新規入力の処理 if prompt := st.chat_input("質問を入力してください"): st.session_state.messages.append({"role": "user", "content": prompt}) st.chat_message("user").write(prompt) with st.chat_message("assistant"): response = st.write_stream( stream_ollama(st.session_state.messages) ) st.session_state.messages.append({"role": "assistant", "content": response})
ステップ4:サイドバーでモデルを動的に切り替える
社内に複数のOllamaモデルが入っている場合、サイドバーのセレクトボックスから選べると便利だ。コンテキスト長が必要なドキュメント要約にはLlama3.3、軽量な用途にはMistralやPhi-4、画像解析が必要なタスクにはマルチモーダルモデル、という使い分けが社内で自然と生まれる。モデル選定の詳細な基準についてはローカルLLMのモデルを比較する方法を参照してほしい。以下が4ステップを組み込んだ完成形のapp.pyだ。
# /opt/streamlit-llm/app.py(完成形:モデル切り替え対応) import streamlit as st import requests import json OLLAMA_URL = "http://localhost:11434" st.title("社内AIアシスタント") # モデル一覧取得(60秒キャッシュ) @st.cache_data(ttl=60) def get_models(): try: resp = requests.get(f"{OLLAMA_URL}/api/tags", timeout=5) resp.raise_for_status() return [m["name"] for m in resp.json().get("models", [])] except Exception: return ["llama3.3:70b-instruct-q4_0"] def stream_ollama(model, messages): with requests.post( f"{OLLAMA_URL}/api/chat", json={"model": model, "messages": messages, "stream": True}, stream=True, timeout=300, ) as resp: resp.raise_for_status() for line in resp.iter_lines(): if line: data = json.loads(line) yield data["message"]["content"] if data.get("done"): break if "messages" not in st.session_state: st.session_state.messages = [] with st.sidebar: st.header("設定") models = get_models() selected_model = st.selectbox("モデルを選択", models) if st.button("会話をリセット"): st.session_state.messages = [] st.rerun() for msg in st.session_state.messages: st.chat_message(msg["role"]).write(msg["content"]) if prompt := st.chat_input("質問を入力してください"): st.session_state.messages.append({"role": "user", "content": prompt}) st.chat_message("user").write(prompt) with st.chat_message("assistant"): response = st.write_stream( stream_ollama(selected_model, st.session_state.messages) ) st.session_state.messages.append({"role": "assistant", "content": response})
systemdとNginxでチームに公開する
個人のターミナルで起動している状態では、ターミナルを閉じるとStreamlitが止まる。チームに安定して提供するには、systemdでサービス化してサーバー起動時に自動起動させ、NginxをリバースプロキシとしてHTTP経由でアクセスできるようにする。まずsystemdのサービスユニットを作成する。After=ollama.service を指定することで、Ollamaが起動した後にStreamlitが起動するよう依存関係を設定する。
# /etc/systemd/system/streamlit-llm.service を作成 $ sudo tee /etc/systemd/system/streamlit-llm.service << 'EOF' [Unit] Description=Streamlit Local LLM Chat UI After=network.target ollama.service [Service] Type=simple User=ubuntu WorkingDirectory=/opt/streamlit-llm ExecStart=/opt/streamlit-llm/venv/bin/streamlit run app.py \ --server.port 8501 \ --server.address 127.0.0.1 \ --server.headless true Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF $ sudo systemctl daemon-reload $ sudo systemctl enable streamlit-llm Created symlink /etc/systemd/system/multi-user.target.wants/streamlit-llm.service $ sudo systemctl start streamlit-llm $ sudo systemctl status streamlit-llm * streamlit-llm.service - Streamlit Local LLM Chat UI Loaded: loaded (/etc/systemd/system/streamlit-llm.service; enabled) Active: active (running) since Thu 2026-07-24 09:15:00 JST; 3s ago Main PID: 12345 (streamlit)
# /etc/nginx/sites-available/streamlit-llm を作成 $ sudo tee /etc/nginx/sites-available/streamlit-llm << 'EOF' server { listen 80; server_name chat.example.internal; location / { proxy_pass http://127.0.0.1:8501; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 300; proxy_send_timeout 300; } } EOF $ sudo ln -s /etc/nginx/sites-available/streamlit-llm /etc/nginx/sites-enabled/ $ sudo nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful $ sudo systemctl reload nginx
よくあるエラーと対処法
ConnectionError: HTTPConnectionPool ... Connection refusedOllamaが起動していないかポートが違う。systemctl status ollama でサービス状態を確認する。OllamaがデフォルトポートではないHost/Portで動いている場合は、app.py の OLLAMA_URL の値を合わせる(例: http://localhost:11435)。
AttributeError: module 'streamlit' has no attribute 'write_stream'
Streamlitのバージョンが古い。st.write_stream はStreamlit 1.28以降でサポートされている。pip install --upgrade streamlit で最新版にアップグレードする。
JSONDecodeError: Expecting value: line 1 column 1 (char 0)
ストリーミングレスポンスの空行をjson.loadsに渡した場合に起きる。iter_lines() は空行を返すことがある。stream_ollama関数内の if line: チェックで回避できる(上記コード例では対処済み)。
NginxプロキシでStreamlitのセッションが数分で切れる
proxy_read_timeout と proxy_send_timeout が短すぎる。LLMの応答生成に時間がかかるため、300秒以上を設定する。加えてNginxのkeepalive_timeoutも確認する。
ページをリロードすると会話履歴が消える
st.session_state はブラウザのページリロードでリセットされる設計だ。永続化が必要ならSQLiteに会話ログを書き出す仕組みを追加する。業務用途ではユーザーIDと紐づけてログに残す要件が多い。
まとめ
Streamlit+OllamaでローカルLLMのカスタムチャットUIを構築する手順を4ステップで解説した。Open WebUIの「既製品UI」とは異なり、Pythonコードで自由に拡張できる点がStreamlitの強みだ。ここで作ったベースに業務DBとの連携・ファイルアップロード処理・独自認証ロジックを追加することで、社内専用AIアシスタントへと育てられる。各ステップで実装したコマンドと主要設定の早見表を以下にまとめる。
| やりたいこと | コマンド・設定 |
|---|---|
| 仮想環境作成 | python3 -m venv venv && source venv/bin/activate |
| Streamlitインストール | pip install streamlit requests |
| アプリ起動(開発用) | streamlit run app.py --server.port 8501 |
| Ollamaモデル一覧確認 | curl -s http://localhost:11434/api/tags |
| ストリーミング呼び出し(Python) | requests.post("/api/chat", json={..., "stream": True}, stream=True) |
| Streamlitアップグレード | pip install --upgrade streamlit |
| systemdサービス有効化 | sudo systemctl enable streamlit-llm |
| systemdサービス起動 | sudo systemctl start streamlit-llm |
| Nginx設定テストと反映 | sudo nginx -t && sudo systemctl reload nginx |
StreamlitとOllamaを実機で動かすハンズオンを2日間で体験する
ローカルLLMのチャットUIを自分の手で組み立て、社内サーバーに組み込むまでを体験したい方向けに、「ローカルAIマスターセミナー」を開催しています。
少人数(最大8名)ZOOMハンズオン形式で実施しています。
・Ubuntu ServerでローカルLLMを構築する方法|Ollamaで機密データを外に出さず業務AIを動かす完全ガイド
・社内でChatGPTが使えないときの代替手段|機密データを守るローカルLLMという選択肢
・ローカルLLMのモデルを比較する方法|Llama3.3・Mistral・Gemma・Phi-4をUbuntuで使い分けるポイント
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 前のページへ:OllamaとAutoGenを連携させる方法|会話駆動型マルチエージェントをローカルLLMでLinuxサーバーに構築する手順
- この記事の属するカテゴリ:ローカルLLMへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます