ローカル・LLMスタック構築手順
ゲーム用のPCにLLM環境を構築する。
はじめはwindows上にtext-generation-webuiで構築しようとしたが、AutoAWQローダーの導入回りで躓き、WSL2上に構築した。モデルは、CodeLlama-70B-Instruct-AWQ,Qwen2.5-Coder-7B-Instructあたりも試したが、*Mistral-7B-Instruct-v0.2が軽くて良かった。
使い道はあまりない。
― RTX 3090/WSL2(Ubuntu)+ vLLM+Streamlit+Web検索 ―
既存環境の構成
- Windows 11(WSL2 Ubuntu 22.04)
- GPU: RTX 3090(24 GB VRAM)
- Python 3.10 – 3.12
- CUDA 12.8 以上+対応 PyTorch
- モデル: Mistral-7B-Instruct-v0.2(HF公開)
- 追加機能: ストリーミング対応チャット UI+DuckDuckGo 検索+履歴管理
1. WSL2 & GPU ドライバ
wsl --install
# Ubuntu を選択しユーザー作成
# NVIDIA ドライバ (Windows) は 551.xx 以降
# WSL2 CUDA ツールキット
wsl --update
Ubuntu 内:
wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-wsl-ubuntu.pin
sudo mv cuda-wsl-ubuntu.pin /etc/apt/preferences.d/cuda-repository-pin-600
wget https://developer.download.nvidia.com/compute/cuda/12.8.0/local_installers/cuda-repo-wsl-ubuntu-12-8-local_12.8.0-1_amd64.deb
sudo dpkg -i cuda-repo-wsl-ubuntu-12-8-local_12.8.0-1_amd64.deb
sudo cp /var/cuda-repo-wsl-ubuntu-12-8-local/cuda-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cuda-toolkit-12-8
nvidia-smi # 動作確認
2. Python 仮想環境 & vLLM
sudo apt install python3-venv python3-pip -y
python3 -m venv vllm_env
source vllm_env/bin/activate
pip install --upgrade pip
# CUDA12.8ビルドの PyTorch
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
# vLLM 本体 + Streamlit + 検索ライブラリ
pip install vllm sseclient-py duckduckgo-search streamlit
3. モデル取得(Hugging Face)
mkdir -p ~/models
cd ~/models
huggingface-cli download mistralai/Mistral-7B-Instruct-v0.2 \
--local-dir mistral7b --local-dir-use-symlinks False
トークン不要・公開モデル。
他モデルの場合はhuggingface-cli loginでトークン取得。
4. vLLM サーバー起動
vllm serve ~/models/mistral7b \
--trust-remote-code \
--dtype float16 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--host 0.0.0.0 \
--port 8000
-
VRAM不足時は
--max-model-lenを 4096 まで縮小 - ストリーミング出力は OpenAI 互換 (
stream=True) で利用可
5. Streamlit チャット UI (ui.py)
以下の機能を提供:
- ストリーミングチャット: vLLM からリアルタイム応答
-
Web検索:
!searchコマンドで DuckDuckGo 検索 - 履歴管理: チャット履歴の保存・読み込み・削除
- 新規チャット: サイドバーから新規チャット開始
ui.py
# ui.py
import streamlit as st
import requests
import sseclient
import json
import os
import datetime
from duckduckgo_search import DDGS
# 履歴保存ディレクトリ
HISTORY_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "chat_history")
if not os.path.exists(HISTORY_DIR):
os.makedirs(HISTORY_DIR)
st.write(f"履歴ディレクトリを作成しました: {HISTORY_DIR}")
# 検索機能
def web_search(query, max_results=3):
with DDGS() as ddgs:
results = ddgs.text(query)
summary = ""
for i, result in enumerate(results):
if i >= max_results:
break
summary += f"[{result['title']}]({result['href']}): {result['body']}\n\n"
return summary
# 履歴保存機能
def save_chat_history(messages, title=None):
if not title:
title = f"chat_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
# systemメッセージを除いた履歴を保存
chat_history = messages[1:] if messages else []
history_data = {
"title": title,
"timestamp": datetime.datetime.now().isoformat(),
"messages": chat_history
}
filename = f"{HISTORY_DIR}/{title}.json"
# デバッグ情報を表示
st.write(f"保存先: {filename}")
st.write(f"メッセージ数: {len(chat_history)}")
st.write(f"履歴ディレクトリ存在: {os.path.exists(HISTORY_DIR)}")
try:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(history_data, f, ensure_ascii=False, indent=2)
# 保存確認
if os.path.exists(filename):
st.write(f"ファイル保存成功: {filename}")
else:
st.error(f"ファイル保存失敗: {filename}")
return filename
except Exception as e:
st.error(f"保存エラー: {e}")
raise e
# 履歴読み込み機能
def load_chat_history(filename):
try:
with open(filename, 'r', encoding='utf-8') as f:
history_data = json.load(f)
return history_data
except Exception as e:
st.error(f"履歴の読み込みに失敗しました: {e}")
return None
# 履歴削除機能
def delete_chat_history(filename):
try:
os.remove(filename)
return True
except Exception as e:
st.error(f"履歴の削除に失敗しました: {e}")
return False
# 履歴ファイル一覧取得
def get_history_files():
if not os.path.exists(HISTORY_DIR):
return []
files = []
for filename in os.listdir(HISTORY_DIR):
if filename.endswith('.json'):
filepath = os.path.join(HISTORY_DIR, filename)
try:
with open(filepath, 'r', encoding='utf-8') as f:
history_data = json.load(f)
files.append({
'filename': filepath,
'title': history_data.get('title', filename),
'timestamp': history_data.get('timestamp', ''),
'message_count': len(history_data.get('messages', []))
})
except:
continue
# タイムスタンプでソート(新しい順)
files.sort(key=lambda x: x['timestamp'], reverse=True)
return files
st.set_page_config(page_title="Mistral-7B Chat", layout="wide")
st.title("🧠 Mistral-7B-Instruct-v0.2 Chat")
# チャット履歴の初期化(最初に実行)
if "messages" not in st.session_state:
st.session_state.messages = [
{"role": "system", "content": "あなたは親切で有能なAIアシスタントです。回答は日本語で記述してください。"}
]
# サイドバーに履歴管理UIを追加
with st.sidebar:
st.header("📚 チャット履歴")
# 新規チャット作成ボタン
if st.button("🆕 新規チャット", key="new_chat_button"):
st.session_state.messages = [
{"role": "system", "content": "あなたは親切で有能なAIアシスタントです。回答は日本語で記述してください。"}
]
st.success("新規チャットを開始しました")
st.rerun()
# 現在のチャット情報を表示
current_messages = len(st.session_state.messages) - 1 # systemメッセージを除く
if current_messages > 0:
st.info(f"現在のチャット: {current_messages}件のメッセージ")
else:
st.info("現在のチャット: 新規")
st.divider()
# 現在の履歴を保存
st.subheader("💾 履歴を保存")
# 保存する履歴があるかチェック
if len(st.session_state.messages) > 1: # systemメッセージ以外がある場合
# タイトル入力
title = st.text_input("履歴のタイトル",
value=f"chat_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}",
key="save_title")
# 保存ボタン
if st.button("保存", key="save_button"):
try:
filename = save_chat_history(st.session_state.messages, title)
st.success(f"履歴を保存しました: {filename}")
# 入力フィールドをクリア
st.session_state.save_title = f"chat_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
st.rerun()
except Exception as e:
st.error(f"保存に失敗しました: {e}")
else:
st.warning("保存する履歴がありません")
st.divider()
# 保存済み履歴の一覧
history_files = get_history_files()
if history_files:
st.subheader("保存済み履歴")
for file_info in history_files:
col1, col2 = st.columns([3, 1])
with col1:
if st.button(f"📖 {file_info['title']}", key=f"load_{file_info['filename']}"):
history_data = load_chat_history(file_info['filename'])
if history_data:
# systemメッセージを追加して履歴を復元
st.session_state.messages = [
{"role": "system", "content": "あなたは親切で有能なAIアシスタントです。回答は、Markdown形式で構造化し、日本語で記述してください。"}
] + history_data['messages']
st.success(f"履歴を読み込みました: {file_info['title']}")
st.rerun()
with col2:
if st.button("🗑️", key=f"delete_{file_info['filename']}"):
if delete_chat_history(file_info['filename']):
st.success("履歴を削除しました")
st.rerun()
st.caption(f"メッセージ数: {file_info['message_count']} | {file_info['timestamp'][:19]}")
else:
st.info("保存済みの履歴はありません")
# メインコンテンツ
# 履歴表示
for msg in st.session_state.messages[1:]: # systemを除く
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
# ユーザー入力
if prompt := st.chat_input("メッセージを入力してください(先頭に !search を付けると検索)"):
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
full_response = ""
placeholder = st.empty()
# 検索要求がある場合
if prompt.startswith("!search"):
query = prompt.replace("!search", "").strip()
if query: # 検索クエリが空でない場合
web_info = web_search(query)
st.markdown("🔎 検索結果を反映しています...")
# 検索結果を含むメッセージを追加
search_message = f"以下はインターネット検索結果の要約です:\n\n{web_info}\n\nユーザーの質問: {query}"
st.session_state.messages.append({"role": "user", "content": search_message})
else:
# 検索クエリが空の場合
st.session_state.messages.append({"role": "user", "content": prompt})
else:
# 通常のメッセージ
st.session_state.messages.append({"role": "user", "content": prompt})
# vLLM にストリームリクエスト送信
try:
res = requests.post(
"http://localhost:8000/v1/chat/completions",
json={
"model": "mistralai/Mistral-7B-Instruct-v0.2",
"messages": st.session_state.messages,
"temperature": 0.7,
"stream": True
},
stream=True,
headers={"Content-Type": "application/json"}
)
client = sseclient.SSEClient(res)
for event in client.events():
if event.data == "[DONE]":
break
data = json.loads(event.data)
token = data["choices"][0]["delta"].get("content", "")
full_response += token
placeholder.markdown(full_response)
except Exception as e:
placeholder.error(f"エラー: {e}")
full_response = "エラーが発生しました。"
st.session_state.messages.append({"role": "assistant", "content": full_response})
起動:
streamlit run ui.py
主要機能
- サイドバーで履歴管理(保存・読み込み・削除)
!search 検索語でWeb検索結果を反映- ストリーミング応答でリアルタイム表示
- Markdown形式での構造化回答
6. 逆引き・最適化メモ
| 症状 | 対策 |
|---|---|
| 遅い |
--max-model-len を 4096、temperature 下げ、stream 有効 |
| OOM |
gpu-memory-utilization を 0.85、dtype bfloat16 (Ampere以降) |
| Chat Template エラー |
role は system(最初のみ) + user/assistant のみ |
| 検索API制限 | Serper.dev や Bing API に置換可 |
| 履歴保存エラー |
chat_history ディレクトリの権限確認、JSON形式チェック |
7. デプロイ拡張案
- Docker: vLLM + Streamlit をマルチステージでまとめ、GPU Container Runtime を利用
- RAG: LangChain / LlamaIndex でベクターDB+Retriever を実装し、Streamlit 内で呼び出す
-
認証: Streamlit の
st.secretsでベーシック認証、あるいは Nginx でリバースプロキシ -
複数モデル: vLLM の
--model-pathで複数モデルを切り替え可能
8. トラブルシューティング
vLLM サーバー起動エラー
# VRAM不足の場合
vllm serve ~/models/mistral7b \
--dtype bfloat16 \
--gpu-memory-utilization 0.75 \
--max-model-len 4096
# モデルパス確認
ls -la ~/models/mistral7b/
Streamlit 接続エラー
# vLLM サーバーが起動しているか確認
curl http://localhost:8000/v1/models
# ポート確認
netstat -tlnp | grep 8000
まとめ
この手順で GPU1枚・完全ローカル でも
- 高速推論 (
vLLM) - ストリーミングチャット UI (
Streamlit) - Web検索連携 (
DuckDuckGo) - 履歴管理機能
が動く"セルフホスト AI アシスタント"環境を即構築できます。
カスタマイズ・拡張はご自由に。