前回の宿題を片付ける:Web検索Workerの常駐化と自動判定
前回の記事で、SearXNGを使ったWeb検索Workerを家庭内AI基盤に組み込んだ。ただしあの時点では2つ宿題が残っていた。
ひとつは、Web検索Workerが手動起動のままだったこと。ターミナルで python worker_web.py と叩いて動かしているだけで、サーバーを再起動したら自分で立ち上げ直さないといけない状態だった。
もうひとつは、Web検索を使うのに毎回 task_type: "web_search" を明示指定しなければならなかったこと。「今日の天気は?」と聞いても、指定を忘れれば普通のローカルLLMが「私には最新情報が分かりません」と答えてしまう。これでは実用にならない。
今回はこの2つを片付ける。前半はsystemd化(とそこで踏んだ地雷)、後半はWeb検索の自動判定。最後に、この作業を3つのAIにレビューしてもらって次の方針を決めた話まで書く。
今回やること
1. worker_web.py を systemd 化して常駐させる
2. Coordinator に「最新情報が必要な質問」の自動判定を入れる
ちなみに我が家のAI基盤の構成はこうなっている。
OpenWebUI(Windows Server)
↓
Coordinator API(AI-Core VM / FastAPI + Redis + PostgreSQL)
├─ tasks:gpu → rtx3070ti(GPUモデル)
├─ tasks:cpu → moon / rtx3070ti(CPUモデル)
└─ tasks:web → worker_web(SearXNG検索 + LLM要約) ← 今回の主役
タスクはRedisのキューに積まれ、各Workerが自分の担当キューだけを監視する。Web検索は tasks:web という専用キューを持っていて、それを処理するのが worker_web.py だ。
第1部:worker_web.py を systemd 化する
なぜ systemd 化するのか
手動起動の問題は「人間が生きていないと動かない」ことに尽きる。SSHを切ったらプロセスが死ぬし、VMを再起動したら誰も立ち上げてくれない。家庭内サーバーは停電やWindows Update、WSLの再起動で平気で落ちる。落ちるたびに手で起動し直すのは現実的じゃない。
systemdに登録しておけば、
- OS起動時に自動で立ち上がる
- プロセスが死んでも自動で再起動する(
Restart=always) - ログが
journalctlで一元的に追える
という運用上の安心が手に入る。
サービスファイルを書く
/etc/systemd/system/web-worker.service を作る。ポイントは、接続先をすべて環境変数で外出しにしたこと。
[Unit]
Description=Web Search Worker
After=network.target ollama.service
[Service]
User=sagawa
WorkingDirectory=/home/hogehoge/coordinator
ExecStart=/home/hogehoge/coordinator/venv/bin/python worker_web.py
Restart=always
RestartSec=5
# 接続先設定(移設時はこのブロックを書き換えるだけでよい)
Environment=REDIS_HOST=localhost
Environment=REDIS_PORT=6379
Environment="PG_DSN=host=localhost dbname=coordinator user=coordinator password=coordinator"
Environment=SEARXNG_URL=http://192.168.0.1:8888
Environment=OLLAMA_HOST=http://localhost:11434
Environment=OLLAMA_MODEL=hf.co/LiquidAI/LFM2.5-1.2B-JP-GGUF:Q4_K_M
Environment=COORDINATOR_URL=http://localhost:8000
Environment=WORKER_ID=web-worker-aicore
Environment=WORKER_HOST=192.168.0.40
[Install]
WantedBy=multi-user.target
環境変数を全部ここに並べたのには理由がある。このWeb検索Workerは今はAI-Core VMに暫定で置いているが、将来的にはRAG基盤と同じ別VMへ引っ越す予定だ。そのとき、接続先がコードにベタ書きされていると毎回コードを直すことになる。環境変数にしておけば、引っ越し先ではこのサービスファイルの数値を書き換えるだけで済む。コードは1行も触らなくていい。
RestartSec=5 を入れたのも地味に効く。Restart=always だけだと、Ollamaがまだ起動しきっていないタイミングで起動失敗→即再起動→また失敗、という高速ループに陥ることがある。5秒待たせることでこれを防ぐ。
ここでハマった:PG_DSN のクォート問題
最初、PG_DSN をこう書いていた。
Environment=PG_DSN=host=localhost dbname=coordinator user=coordinator password=coordinator
一見問題なさそうに見える。でもこれは動かない。
systemdの Environment= は、1行に VAR1=値1 VAR2=値2 とスペース区切りで複数の変数を書ける仕様になっている。つまり上の書き方だと、systemdは値の中のスペースで切ってしまう。
PG_DSN=host=localhostdbname=coordinator(別の変数扱い)user=coordinator(別の変数扱い)password=coordinator(別の変数扱い)
と解釈する。結果、PG_DSN は host=localhost だけになってDB接続に失敗する。
正解は、値全体をダブルクォートで囲むこと。
Environment="PG_DSN=host=localhost dbname=coordinator user=coordinator password=coordinator"
値にスペースを含む環境変数は囲む。これはsystemdユニットを書くときの定番の落とし穴なので、覚えておくと将来の自分が救われる。
登録して起動する
sudo cp web-worker.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable web-worker.service
sudo systemctl start web-worker.service
# 確認
sudo systemctl status web-worker.service
起動ログがこうなっていれば成功だ。
Active: active (running)
[worker_web] INFO Redis OK
[worker_web] INFO SearXNG OK
[worker_web] INFO Ollama OK
[worker_web] INFO [register] worker_id=web-worker-aicore supports=['web_search']
[worker_web] INFO Waiting on queues ['tasks:web'] ...
Redis・SearXNG・Ollamaの全部に疎通が取れ、Coordinatorへの登録も済み、tasks:web キューの監視が始まっている。メモリ消費は33MBほどで、Worker本体は検索結果をSearXNGとOllamaに投げているだけなので、プロセス自体はとても軽い。
enabled になっているので、これで再起動後も勝手に立ち上がる。宿題ひとつめ、完了。
第2部:Web検索の自動判定を入れる
何が問題だったか
これまでは、Web検索を使うのにこう書く必要があった。
curl -X POST http://localhost:8000/route \
-d '{"prompt": "今日の東京の天気は?", "task_type": "web_search"}'
task_type: "web_search" を付けないと、ただのローカルLLM推論に回る。OpenWebUIのチャット欄から普通に「今日の天気は?」と打っても、この指定は付かない。つまり実質的に使えていなかった。
やりたいのは、プロンプトの中身を見て「これは最新情報が要る質問だな」と分かったら、自動的にWeb検索に回すこと。
設計:ドメイン分類とは別の軸として作る
ここで設計判断がある。既存のドメイン分類(code / math / japanese / general)に「web」を足すこともできた。でもそれはやめた。
ドメイン分類は「この質問はどの分野か」を判定するもので、Web検索判定は「この質問は最新情報が要るか」を判定するもの。この2つは独立した軸だ。
プロンプト
↓
classify_web_search() ← まずここで「最新情報が要るか」を判定
├─ True → tasks:web(Web検索Worker)
└─ False → 通常のドメイン分類 → tasks:gpu / tasks:cpu
実装:キーワードマッチで判定する
WEB_SEARCH_KEYWORDS = [
"今日", "今週", "今月", "今年", "現在", "いま", "いまの",
"最新", "最近", "ニュース", "速報", "新着", "今朝", "今夜",
"現状", "今どう", "いま何", "何時", "何度", "天気",
"today", "latest", "current", "news", "weather", "now",
"this week", "this month", "recently",
]
def classify_web_search(prompt: str) -> bool:
prompt_lower = prompt.lower()
for kw in WEB_SEARCH_KEYWORDS:
if kw in prompt_lower:
return True
return False
LLMは使わない。判定のために毎回LLMを呼ぶとそれ自体が遅延とリソース消費になるからだ。
is_web_search = (
req.task_type == "web_search"
) or classify_web_search(req.prompt)
動作確認
# 「今日」を含む → true
curl -sG http://localhost:8000/route/preview \
--data-urlencode "prompt=今日の東京の天気は?"
# 「domain": "general", "auto_web_search": true
# 普通のコード質問 → false
curl -sG http://localhost:8000/route/preview \
--data-urlencode "prompt=Pythonでソートを実装して"
# 「domain": "code", "auto_web_search": false
両方とも狙い通り。「今日」「天気」がヒットして天気質問はtrue、コード質問はfalseになった。
curl -X POST http://localhost:8000/route \
-d '{"prompt": "今日の東京の天気は?"}'
# → {"worker_id":"web-worker","model":"web_search","queue":"tasks:web", ...}
結果をポーリングすると約31秒で返ってきた。
今日の東京の天気は晴れで、最高気温は31℃、降水確率は0%です。
半袖や薄手のシャツを着ると快適でしょう。
SearXNGで検索 → ローカルLLMで要約 → PostgreSQLに保存 → 返却、という全経路が通った。
宿題ふたつめ、完了。
今わかっている限界
このキーワードマッチには明確な穴がある。たとえば「今日学んだPythonの復習をしたい」という質問は、「今日」がヒットしてWeb検索に回ってしまう。本当はローカルLLMで答えるべき質問なのに。
文脈を見ない単純マッチの宿命だ。これをどう直すかは、後述のAIレビューで良いアイデアが出たので、次回以降の課題にする。
第3部:3つのAIにレビューしてもらって次を決める
ここからが今回いちばん面白かった部分かもしれない。作業が終わった後、更新した引継ぎ資料をClaude・ChatGPT・Geminiの3つに読ませて、それぞれにレビューしてもらった。
ChatGPT:優先順位を組み替えてきた
ChatGPTは全体の発展性と優先順位付けが得意だ。今回いちばん刺さったのは「Dead Worker Retry(死んだWorkerのタスク再投入)の優先度が低すぎる」という指摘だった。
Gemini:実装レベルまで踏み込んできた
- 品質評価は必ず夜間バッチにせよ。
- Web検索の誤爆はネガティブキーワードで防げ。
- エラーログとバックアップの方針が抜けている。
Claude:トレードオフの整理役
Claudeは、ChatGPTとGeminiの意見を踏まえて、優先順位や投資対効果の観点から整理する役割を果たした。
3つの視点を並べると
| 観点 | ChatGPT | Gemini | Claude |
|---|---|---|---|
| 得意領域 | 全体の発展性・優先順位 | コードレベルの実装・運用 | トレードオフの整理 |
| 今回の主な貢献 | Retryの優先度繰り上げ | 夜間バッチ化・ネガティブKW・運用設計 | 順序判断の両論併記 |
次回への展望
1. Dead Worker Retry(自己修復) ← 最優先・安定性
2. ここで分岐
A. RAG基盤(Qdrant)+ Web検索Worker移設 ← 体感価値を上げたいなら
B. 品質評価(夜間バッチ) ← 基盤を締めたいなら
3. 会話履歴(短期記憶 + 要約記憶)
4. Web検索判定の誤爆対策(ネガティブKW)
それとは別に、Geminiが指摘してくれた運用面(エラーログのDB集約、pg_dump バックアップ)も、地味だが3台体制になった今こそ効くので早めに入れたい。
次回最初の作業は、落ちたWorkerのタスクを別のWorkerが拾い直す「Dead Worker Retry」になる。Workerが3台に増えて、いよいよ「1台落ちても全体は止まらない」自己修復の仕組みが必要なフェーズに入ってきた。
家庭内のローカルLLM環境が、少しずつ分散システムらしくなってきた。