私物の MacBook Air 13inch (early 2020)が流石にIntelCPUなので最新OSも使えないというわけで、M5 に買い替えました。
買い替えたついでに巷で流行っているローカルLLMを導入してみました。
Docker Desktopは激重な気がするのでできるだけ軽量に動くようにPython仮想環境でやってしまえという感じで、Dockerフリー環境が出来上がりました。
その環境構築工程を記します。
デバイスの情報
- MacBook Air M5 13inch (メモリ32GB)
- macOS Tahoe 26.5.2
前提
- Homebrewがインストール済みであること
- ある程度ターミナルでコマンドを叩けること
- エラーが出たらログを見て自分で解決方法を調べられること
今回インストールするもの
- llama.cpp (Homebrew)
- llama-swap (Homebrew)
- OpenWebUI (uv)
- SearXNG (uv)
- Hugging Face CLI (uv)
事前準備
Python仮想環境はuvで構築するのでHomebrewで入れておきます。
uvの入れ方は色々ありますが、今回はHomebrewでまとめておいた感じです。
brew install uv
llama.cpp をインストール
Homebrewでインストールします。
llama-swapから使う前提のため、特に設定はいりません。
入れておくだけです。
brew install llama.cpp
llama-swap をインストール
Homebrewでインストールします。
後で使うモデルの準備ができたらconfig.yamlを書きます。
brew install llama-swap
Hugging Face CLI をインストール
モデルをHugging FaceからダウンロードするためHugging Face のCLIツールをインストールします。
モデルのダウンロード方法はお好みなので、wgetなり適当にブラウザでダウンロードでも構わないです。
以下の例はPython仮想環境をuvで作成し、そこにツールを導入する例です。
mkdir -p ~/llm/hf_models_env
cd ~/llm/hf_models_env
uv venv
source .venv/bin/activate
uv pip install -U huggingface_hub
Hugging Face にサインアップ
一部、Googleなど規約に同意が必要なモデルのダウンロードにはHugging Faceにアカウントを作る必要があります。
今回はlmstudio-communityのモデルをダウンロードしているので要らないですが、念のため、read onlyのtokenを作っておきたい場合は登録を。
tokenを作った後の認証は一つ前で入れたHugging Face のツールでできます。
hf auth login
ブラウザで認証するか、tokenを登録するか選択肢が出るので、先にtokenを作った場合はそれをコピーしてきて、tokenの登録で認証できます。
モデルのダウンロード
例えば、lmstudio-community/gemma-4-26B-A4B-it-QAT-GGUF は重みが1種類しかなく、もう一つはmmprojなので、リポジトリ丸ごとダウンロードしてきた方が有用性があります。
--local-dir オプションで置きたい場所を指定します。
hf download lmstudio-community/gemma-4-26B-A4B-it-QAT-GGUF --local-dir ./models/gemma-4-26B-A4B
個別にggufファイルを指定してダウンロードする場合は、重みのファイル名まですべて含めて指定します。
hf download hf://lmstudio-community/gemma-4-26B-A4B-it-QAT-GGUF/gemma-4-26B-A4B-it-QAT-Q4_0.gguf --local-dir ./models/gemma-4-26B-A4B
llama-swap のconfig.yaml を作成
モデルをいくつかダウンロードできたら、次にllama-swap のconfig.yamlを作成します。
llama.cppはHomebrewでインストールしているので、Apple Silliconの場合、/opt/homebrew/bin にあります。
通常はパスが通っているはずなので、フルパスは要らないですが、念のため。
ここで設定しているstartPortはllama-server起動時にllama-swapが自動でセットする ${PORT} の開始点です。
healthCheckTimeout: 60
logLevel: info
globalTTL: 60
startPort: 9300
macros:
# HOME from plist EnvironmentVariables
model_dir: "${env.HOME}/llm/hf_models_env/models"
llama_server: >
/opt/homebrew/bin/llama-server
--port ${PORT}
--host 127.0.0.1
--ctx-size 16384
--cache-type-k q8_0
--cache-type-v q8_0
--jinja
-ngl auto
models:
"gemma4:26B-A4B":
cmd: |
${llama_server}
--model ${model_dir}/gemma-4-26B-A4B/gemma-4-26B-A4B-it-QAT-Q4_0.gguf
--mmproj ${model_dir}/gemma-4-26B-A4B/mmproj-gemma-4-26B-A4B-it-QAT-BF16.gguf
--flash-attn on
"gemma4:E4B":
cmd: |
${llama_server}
--model ${model_dir}/gemma-4-E4B/gemma-4-E4B-it-QAT-Q4_0.gguf
--mmproj ${model_dir}/gemma-4-E4B/mmproj-gemma-4-E4B-it-QAT-BF16.gguf
--flash-attn on
"deepseek-coder-v2":
cmd: |
${llama_server}
--model ${model_dir}/DeepSeek-Coder-V2/DeepSeek-Coder-V2-Lite-Instruct-Q8_0.gguf
config.yamlを作成したら、内容が正しいかどうか、llama-swapを起動して確認してみることをおすすめします。
llama-swap --config ~/llm/llama-swap/config.yaml --listen 127.0.0.1:8888
起動したらブラウザで localhost:8888 を開いてみてllama-swapの画面が出れば起動できています。
Modelsにconfig.yamlに書いたモデルの一覧が表示されていれば認識していますが、その右の再生ボタンを押してllama-serverが起動できるところまで確認しておいた方が無難です。(オプションが間違っていたりすると起動しません)
llama-serverまでの起動確認ができたら、簡易的なチャットもできるので試しておいてもいいと思います。
ポート番号はあとでLaunchAgentsに登録する際に整理するので、確認が終わったら一旦 control+c で停止します。
OpenWebUI をインストール
フロントエンドとしてOpenWebUIを使用します。
最終的にエージェントが欲しくなったらHermesとかOpenClawに置き換えても良さそうです。
Pythonは3.11固定が良いようなので、指定して仮想環境を作成します。
mkdir -p ~/llm/owui_env
cd ~/llm/owui_env
uv venv --python 3.11
source .venv/bin/activate
uv pip install -U open-webui
起動前にデータ保管用のディレクトリを作成しておきます。
mkdir .open-webui-data
DATA_DIRを指定して起動します。
DATA_DIR=.open-webui-data open-webui serve
起動時に .webui_secret_key が自動作成されるので、起動パスが変わる場合はこのファイルの位置を指定する必要があります。
起動したら、管理者アカウントを作成します。
ブラウザからOpenWebUIを開きます。
特にportを指定せずに起動した場合は localhost:8080 でアクセスできるはずです。
portを変更したい場合は一度 control+c で停止し、起動コマンドのserve の後に --host 127.0.0.1 --port 8888 を追加します。
くれぐれも他のサービスとport番号が被らないように注意してください。(llama-swapは確認後に止めた前提)
もし、起動しているPC以外からアクセスさせたい、自分以外の人が使うなどの場合はadmin以外のアカウントを用意しておく方が無難です。
必要であればアカウントは適時作成してください。
ここまでで、Web検索以外の準備が整いました。
起動中のOpenWebUI (llama-swapが起動している場合はそれも)を control+c で停止しておきます。
次にバッググラウンド動作のための準備を行います。
LaunchAgents に追加する
llama-swapとOpenWebUIをバッググラウンドで起動しておくためのplistを作成します。
${HOME} などは使えないと思うので、パスを書くときはフルパスで書いた方が無難です。
パスは自分がインストールした場所を確認して埋めてください。 部分は自分のHOMEに合わせて変更してください。
llama-swapはport 9292で常駐する設定です。IPに127.0.0.1を指定するのは、同じPC上のOpenWebUIからしか参照しないためです。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.llm.llama-swap</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/llama-swap</string>
<string>--config</string>
<string>/Users/<Your Name>/llm/llama-swap/config.yaml</string>
<string>--listen</string>
<string>127.0.0.1:9292</string>
<string>--watch-config</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/<Your Name>/Library/Logs/local-llm/llama-swap.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/<Your Name>/Library/Logs/local-llm/llama-swap.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/<Your Name></string>
<!-- PATH so llama-swap can exec llama-server (Homebrew bin). -->
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
</dict>
</plist>
OpenWebUIは他のPCからのアクセスを想定してhostを0.0.0.0にしています。
待ち受けportは8888です。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.llm.open-webui.serve</string>
<key>ProgramArguments</key>
<array>
<string>/Users/<Your Name>/llm/owui_env/.venv/bin/open-webui</string>
<string>serve</string>
<string>--host</string>
<string>0.0.0.0</string>
<string>--port</string>
<string>8888</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/<Your Name>/Library/Logs/local-llm/open-webui.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/<Your Name>/Library/Logs/local-llm/open-webui.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>DATA_DIR</key>
<string>/Users/<Your Name>/llm/owui_env/.open-webui-data</string>
<key>WEBUI_SECRET_KEY</key>
<string>/Users/<Your Name>/llm/owui_env/.webui_secret_key</string>
</dict>
</dict>
</plist>
作ったplistを ~/Library/LaunchAgents に置きます。
置いただけでは起動していないので、サービスとして起動します。
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.llm.llama-swap.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.llm.open-webui.serve.plist
起動で問題が起こっている場合はエラーログが出力されているので、 ~/Library/Logs を確認してください。
OpenWebUI を llama-swap に接続する
無事にバックグラウンドで両方起動できている場合は、ブラウザで localhost:8888 を開き、OpenWebUIからllama-swapの接続を行います。
ログイン済みであれば、左下にプロフィールアイコン(画像がない場合はイニシャル)が表示されているはずです。
それをクリックして「設定」を選択します。
左メニューから「接続」を開いて、llama-swapを参照するように設定します。
llama-swapは OpenAI API互換でアクセスできるので、OpenAI API接続の管理で先ほどのplistに書いたllama-swapの接続先を入れます。
http://localhost:9292/v1
Ollama APIはOllamaを入れていなければオフにします。
右下の保存ボタン押して設定を保存し、今度は左メニューから「モデル」を開きます。
ここにllama-swapのconfig.yamlに書いたmodelsの一覧が表示されていれば接続できています。
左上の「戻る」をクリックしてチャット画面に戻り、適当なモデルを選択して何か話しかけて回答が来るか確認します。
Web検索はまだ入れてないので、リアルタイムな出来事は分からないと言われるかもしれません。
動作確認ができたら、次はWeb検索を使えるようにする工程です。
SearXNG をインストール
SearXNGもPython仮想環境で動作させるようにしますが、こちらはpipなどのライブラリに用意がありません。
gitからソースを取得してPython仮想環境で動くようにしてやる必要があります。
インストール方法は公式ページを参考にしました。
https://docs.searxng.org/admin/installation-searxng.html#install-searxng-dependencies
mkdir -p ~/llm/searxng_env
cd ~/llm/searxng_env
uv venv --python 3.11
source .venv/bin/activate
uv pip install -U pip setuptools wheel
uv pip install -U pyyaml msgspec typing-extensions pybind11
git clone https://github.com/searxng/searxng src
cd src
uv pip install --no-build-isolation -e .
uv で入れる場合は --use-pep517 オプションは不要なようです。
次にsettings.ymlの用意ですが、一つだけ注意点があり、git cloneしたソース以外の場所から起動する場合、use_default_settingsは使えないようです。
そのため、オリジナルのsettings.ymlをコピーして使用します。
mkdir data
cp src/searx/settings.yml ./data/
いくつか書き換え必須の場所があります。
serch.formats → - json を追加します
server.port → llama-swapとOpenWebUIに被らないportを指定します
server.secret_key → openssl rand -hex 16 などで作った文字列に置き換えます(最重要)
後は engines に検索サイトの設定が大量にありますが、必要なものだけ残して削除したり、必要なものを追加したりしてチューニングします。
SearXNG を LaunchAgents に追加する
同様にplistを用意します。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.local-llm.searxng</string>
<key>ProgramArguments</key>
<array>
<!-- 仮想環境内のPythonパス -->
<string>/Users/<Your Name>/llm/searxng_env/.venv/bin/python</string>
<!-- 起動スクリプトのパス -->
<string>/Users/<Your Name>/llm/searxng_env/src/searx/webapp.py</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<!-- 設定ファイルの場所を指定 -->
<key>SEARXNG_SETTINGS_PATH</key>
<string>/Users/<Your Name>/llm/searxng_env/data/settings.yml</string>
</dict>
<!-- ログ出力設定(トラブルシューティング用) -->
<key>StandardOutPath</key>
<string>/Users/<Your Name>/Library/Logs/local-llm/searxng.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/<Your Name>/Library/Logs/local-llm/searxng.err.log</string>
</dict>
</plist>
~/Library/LaunchAgents にplistを置いて起動します。
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.llm.searxng.plist
起動しているか単体確認する場合は、localhost:設定したport でSearXNGが開くか確認します。
OpenWebUI のWeb検索設定に SearXNG を連携する
OpenWebUIの設定を開き、「ウェブ検索」メニューを開きます。
ウェブ検索エンジンで searxng を選択します。
Searxng クエリ URL を設定します。
例えばportを8088として設定した場合のURLは下記になります。
http://localhost:8088/search?q=<query>
一番上のウェブ検索のスイッチをオンにします。
左上の「戻る」をクリックしてチャット画面に戻ります。
チャット入力欄のプラスボタンの右側にある「連携」ボタンを押してウェブ検索をオンにして、「現在の円ドル為替レートは?」などと聞いてみると検索した内容を答えてくれるかもしれません。
ローカルLLMにウェブ検索を連携してみた感想
結構、やり取りがシビアです。
「明日の東京の天気は?」と聞いても検索先のサイトによって内容が異なったり、時系列で分かれてるせいか、答えが曖昧になりがちです。
答えが画一的(現在の為替レートなど)なものである場合はそれなりに答えてくれますが、それ以外はなんとも...という感じになります。
これは今後のチューニング次第ですかね?それとも曖昧な検索にはエージェントの力が必要なのか?
一応、軽めの質問であれば、10秒程度で検索して返してくれます。
複雑なものはまだ試してないですが、難しいかもしれません。
コンテキストサイズ増やしてみようかな...。