CATHODE COASTaccess to tools
11 min read

ローカル RAG は「嘘」をどう直すか——Open WebUI + Qwen2.5 で試したハルシネーションと文書参照

Docker で Open WebUI を立て、ホスト側 Ollama の Qwen2.5 に社内文書を読ませるまでの作業ログ。小型モデルが平然とつく嘘が、RAG を一枚かませた途端に正解へ変わる様子を、途中のポート衝突やボリューム残存の罠も含めて記録する。

shareB!

前回、放置していた Ollama を入れ直して Qwen2.5 に日報を書かせるところまでやりました。その続きです。公式アプリ内蔵のチャットで日報は書けるようになったのですが、会話履歴が残らない・自分の文書を読ませられないという物足りなさがあり、今度は本格的な Web UI である Open WebUI を Docker で立ててみることにしました。

最終的なゴールは RAG です。手元の社内文書を読ませて、その内容に沿って答えさせる。ローカル LLM の「自分のデータを外に出さずに使える」という利点がいちばん活きる使い方です。

結論から言うと、Open WebUI の構築自体はいくつかの罠を踏みつつも到達できました。そして最後に試した RAG で、小型モデルが知らないことを平然と捏造し、文書を一枚読ませただけでそれが正解に変わるという、教科書のような対比が撮れました。この記事は、その過程で踏んだ罠と、RAG の効き目の記録です。

環境は前回と同じで、執筆時点(2026 年 7 月)は以下のとおりです。

  • macOS 15 系(Apple Silicon / arm64、メモリ 16GB)
  • Docker version 28.4.0
  • Ollama v0.11.4(Homebrew 版、ホスト側のポート 11434 で稼働)
  • Open WebUI v0.6.22

先にまとめ

急いでいる方向けに、結論と最終的なコマンドを先に置きます。

  • Open WebUI は Docker で立て、ホスト側の Ollama にはコンテナから host.docker.internal:11434 で繋ぐ。 これが Mac + Homebrew 版 Ollama の構成での肝です。
  • 踏んだ罠は 3 つ。 ①ポート 3000 が既存の開発サーバーと衝突 → 別ポートへ逃がす。②「まっさら」のつもりが前回導入時のアカウントが残っていた → コンテナ削除だけでは消えず、ボリュームごと削除が必要。③sqlite3 がコンテナに入っておらず、DB 確認は Python 経由で。
  • RAG の効果は劇的。 社内ルールを書いた文書について、読ませる前のモデルは「17 時」「業務管理システム」と嘘を答え、文書を添付した途端に「19 時 30 分」「HibiNote」と正解を返しました。

最終構成の起動コマンドはこれです。

Open WebUI 起動(Mac + ホスト側Ollama)
docker run -d \
  -p 3100:8080 \
  --add-host=host.docker.internal:host-gateway \
  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
  -v open-webui:/app/backend/data \
  --name open-webui \
  --restart always \
  ghcr.io/open-webui/open-webui:main

以下、実際にたどった順序で書いていきます。

Docker は動いているか、から始める

まず前提の確認です。Open WebUI は Docker で動かすので、Docker が生きているかを見ます。ここで注意したいのは、docker --version だけでは判定にならないことです。バージョン表示はデーモンが止まっていても返るからです。

バージョンだけでは判定できない
❯ docker --version
Docker version 28.4.0, build d8eb465

本当の判定は docker ps で、デーモンに繋がってコンテナ一覧が返るかどうかです。実行したところ、既存のコンテナがずらりと出てきました。別プロジェクトの開発環境(Web サーバー、DB、キャッシュなどの一式)と、ローカル用の DynamoDB が動いている状態です。

Docker は現役の開発環境だった
❯ docker ps
CONTAINER ID   IMAGE              ...   PORTS                          NAMES
...            (nginx)            ...   0.0.0.0:80->80, 443->443       (web)
...            (mysql)            ...   0.0.0.0:3306->3306             (db)
...            (redis)            ...   0.0.0.0:6379->6379             (cache)
...            dynamodb-local     ...   0.0.0.0:8000->8000             dynamodb-local
(ほか複数)

つまり私の Docker は普段使いの開発環境として現役でした。ここが後の判断に効いてきます。Open WebUI を足すのは構いませんが、既存のコンテナやポートを一切邪魔しないようにする必要があります。

接続先の Ollama も確認しておきます。ホスト側でちゃんと 11434 を掴んでいるかです。

ホスト側 Ollama は稼働中
❯ lsof -i :11434
ollama  ...  TCP localhost:11434 (LISTEN)
(ESTABLISHED の行も複数)

問題なし。Ollama はホスト(Mac)側で動いているので、あとはコンテナからここへどう繋ぐかです。

ポート衝突を、既存を止めずに回避する

Open WebUI の定番の起動例はポート 3000 を使います。ですが確認すると、3000 はすでに埋まっていました。

3000 は別プロセスが使用中
❯ lsof -i :3000
node    ...  TCP *:hbci (LISTEN)

node が LISTEN しています。おそらく別プロジェクトの開発サーバー(フロントエンドのローカルサーバーなど)でしょう。

ここで一瞬「この node を止めれば 3000 が空く」と考えましたが、やめました。その node が何なのかこの場で確定できませんし、止めれば別の作業が巻き添えになります。そして何より、Open WebUI 側のポートは自由に変えられるので、わざわざ既存の 3000 を奪う理由がありません。既存環境を一切触らずに済むよう、Open WebUI は 3100 に載せることにしました。空いていることだけ先に確認します。

3100 は空き
❯ lsof -i :3100
(出力なし=空き)

これで衝突の心配なく起動できます。「先にまとめ」に載せた docker run-p 3100:8080 がこれです。起動すると、ログに Open WebUI の起動バナーと初期化の進行が流れました。

起動ログ(抜粋)
INFO  [open_webui.env] VECTOR_DB: chroma
INFO  [open_webui.env] Embedding model set: sentence-transformers/all-MiniLM-L6-v2
...
v0.6.22 - building the best AI user interface.
INFO:     Started server process [1]
INFO:     Waiting for application startup.

注目したいのは VECTOR_DB: chroma と埋め込みモデルの行です。この時点で、RAG(文書をベクトル化して検索する仕組み)の基盤がすでにコンテナ内に組み込まれていることが分かります。あとで使う土台がもう入っている、ということです。

「まっさら」のはずが、前の自分が残っていた

コンテナは healthy になり、ブラウザで http://localhost:3100 を開きました。初回なら「アカウントを作成」画面が出るはずが——出てきたのはサインイン(ログイン)画面でした。試しに入力するとエラーになります。

ログインできない
The email or password provided is incorrect.
Please check for typos and try logging in again.

Open WebUI は、アカウントが 1 つもない初回は「サインアップ」を、既にある場合は「サインイン」を出します。つまりこの画面が出るということは、どこかで既にアカウントが作られていることを意味します。心当たりを辿ると、どうやら以前にも一度 Open WebUI を触っていたようでした。前回の Ollama といい、過去の自分の痕跡に何度も出くわします。

本当にアカウントがあるのか、DB を直接見て確認しようとしました。ところが最初のコマンドは空振りします。

sqlite3 はコンテナに入っていない
❯ docker exec -it open-webui sqlite3 /app/backend/data/webui.db "SELECT email, role FROM user;"
OCI runtime exec failed: exec: "sqlite3": executable file not found in $PATH: unknown

コンテナは軽量化のため sqlite3 コマンドを同梱していません。ここは、コンテナに必ず入っている Python から SQLite を叩くことで回避できます。

Python 経由で DB を確認
docker exec -it open-webui python -c "import sqlite3; print(sqlite3.connect('/app/backend/data/webui.db').execute('SELECT email, role FROM user').fetchall())"

これで既存アカウントの存在が確認できました。記事のために「初回セットアップから」を再現したいので、この残存データごと消してやり直すことにします。

ここが今回の重要な学びです。Docker で docker rm してもコンテナが消えるだけで、-v open-webui で作った名前付きボリューム(アカウントや会話履歴の実体)は残ります。前回、Ollama を brew uninstall してもモデルが ~/.ollama に残ったのと、まったく同じ構図です。まっさらにするにはボリュームまで消す必要があります。

コンテナとボリュームの両方を消す
# コンテナを停止・削除
docker rm -f open-webui
 
# データボリュームを削除(★これが「前の自分」を消す肝)
docker volume rm open-webui

消えたことを確認します。

ボリュームが消えたことを確認
❯ docker volume ls | grep open-webui
(出力なし=削除成功)

この状態で改めて同じ docker run(3100 の版)を実行すると、ボリュームが空なので中身も空になり、今度こそ http://localhost:3100初回のアカウント作成画面から始まりました。ここで作った最初のアカウントが管理者になります。認証情報の入力は各自の手で行う部分なので、詳細は割愛します。

なお、既にタブを開いていると古い画面がキャッシュに残ることがあるので、作り直したあとはスーパーリロード(Cmd+Shift+R)しておくと確実でした。

ホストの Ollama に繋ぐ

アカウントを作ってログインしたら、本命の Ollama 接続を確認します。左サイドバー上部のモデル選択ドロップダウンを開き、モデルが出てくるかを見ます。

ここが今回いちばんの関門でした。Open WebUI はコンテナの中で動いていて、Ollama はホスト(Mac)側で動いています。コンテナから「ホストの Ollama」を指すために、起動時に OLLAMA_BASE_URL=http://host.docker.internal:11434 を渡してあります。host.docker.internal は、Docker on Mac でコンテナからホストを指すための特別なホスト名です。これが効いていれば、ホストでダウンロード済みのモデルがそのままリストに出ます。

結果、ドロップダウンには前回作った日報専用モデル nippouqwen2.5:7b が並びました。接続成功です。nippou を選んでキーワードを打つと、Docker コンテナの UI から、ホスト側で作ったモデルの日報がそのまま返ってきました。

ブラウザから nippou を呼べた
【本日の業務】
- 新規顧客A社との打ち合せ:需要増加への対応策について協議した。
- 見積書作成:A社の要望に合わせて製品ラインナップと価格設定を検討し、見積書を作成した。
- 社内MTG:来期方針を共有:市場動向や目標設定について全員で議論を行った。
- 資料レビュー2件:営業資料と新製品説明資料の内容確認を行い、必要な修正点を指摘した。
(以下 所感・課題/明日の予定と続く)

前回 CLI と公式アプリで仕上げた nippou が、ブラウザの ChatGPT 風 UI からそのまま動く。ここまでで構築は完了です。

本題の RAG:まず「嘘」を撮る

いよいよ RAG です。効果をはっきり見せるために、モデルが絶対に知らない情報を書いた文書を用意しました。架空の社内ルール集で、たとえば次のような固有の情報を仕込んであります。

  • 日報の提出期限は 19 時 30 分
  • 提出先ポータルの名前は「HibiNote」
  • 今期の重点施策は「プロジェクト・アオゾラ」

これらは学習データに存在しない架空の固有名詞なので、モデルが正しく答えられるとすれば「文書を読んだから」以外にありません。逆に言えば、読ませる前は答えられないはずです。まずそれを確認します。文書を添付せずに、素の状態で聞いてみました。

RAG なし:堂々とした嘘
Q: 日報の提出期限は何時までですか?提出先のポータル名は?
 
A: 日報の提出期限は17時までです。
   提出先のポータル名は「業務管理システム」です。

見事な嘘です。「17 時」も「業務管理システム」も、こちらは一言も与えていません。モデルは知らないことを「知りません」と言わず、それらしい答えをでっち上げます。これがハルシネーションで、特に 7B クラスの小型モデルでは強く出ます。この「読む前の状態」が、あとの対比の起点になります。

文書を一枚読ませると、正解に変わる

次に、同じ質問を文書付きで投げます。Open WebUI では、チャット入力欄のファイル添付から文書をアップロードすると、裏で自動的にベクトル化されます(起動ログにあった chroma と埋め込みモデルがここで働きます)。用意した社内ルール集を添付して、同じことを聞きました。

RAG あり:文書の内容を正しく参照
Q: 添付したガイドラインによると、日報の提出期限は何時まで?
   提出先のポータル名と、今期の重点施策の名前も教えて。
 
A(抜粋):
- 提出期限:19時30分まで
- 提出先ポータル名:HibiNote
- 今期の重点施策「プロジェクト・アオゾラ」に関連する業務を意識する

さっきは「17 時」「業務管理システム」と嘘をついたモデルが、文書を一枚読ませただけで「19 時 30 分」「HibiNote」「プロジェクト・アオゾラ」と、文書にしか書いていない固有名詞を正確に返しました。これが RAG です。モデルの重み(学習済みの知識)は一切変えず、回答の前に関連文書を検索して渡すだけで、知らなかったことに正しく答えられるようになります。

一点補足すると、この出力は日報の 3 セクション形式に整形されています。質問に答えつつ日報フォーマットに寄せようとした結果で、これは nippou モデルの指示(システムプロンプト)が効いているためです。純粋に RAG の Q&A だけを綺麗に見たいなら、素の qwen2.5:7b で聞くほうが対比がくっきりします。日報モデルと RAG を重ねるとこうなる、という記録としてはこれはこれで面白い挙動でした。

ハマりどころまとめ

今回引っかかった点の一覧です。

症状原因対処
docker --version は通るのに状態が分からないバージョン表示はデーモン停止中でも返るdocker ps でデーモン稼働とコンテナ一覧を確認
ポート 3000 が使えない既存の開発サーバー(node)が使用中既存を止めず -p 3100:8080 で別ポートへ逃がす
初回のはずがログイン画面が出てログイン不可過去に導入したアカウントがボリュームに残存docker rm に加え docker volume rm でボリュームごと削除
sqlite3: not found で DB を確認できないコンテナに sqlite3 コマンドが未同梱コンテナ内の python から sqlite3 を呼ぶ
モデルが知らないことを堂々と捏造する小型モデルのハルシネーションRAG で関連文書を渡し、根拠に基づかせる
作り直したのに古い画面が出るブラウザのキャッシュスーパーリロード(Cmd+Shift+R

やってみての所感

構築で踏んだ罠は、どれも「消したはずのものが残っている」「確認しようとした道具が無い」といった、ドキュメントの手順どおりに進めるだけでは見えないタイプのものでした。特にボリューム残存は、前回の Ollama のモデル残存とまったく同じ構造で、Docker でも Homebrew でも「本体を消してもデータは別に残る」という原則を、身をもって二度学んだ形です。

そして RAG の対比は、想像以上にくっきり出ました。「17 時」という嘘が「19 時 30 分」という正解に変わる瞬間は、RAG が何をしているのかを一発で理解させてくれます。

ここで一つ注意しておきたいのは、「小型モデルだから嘘をつく、大きいモデルなら安心」と単純化しないことです。ハルシネーションが起きにくくなる傾向は確かにありますが、大規模モデルでも、社内固有のルールのような学習データに無い情報を聞かれれば、自信ありげに間違えることはあります。つまり教訓はモデルの大小ではなく、「根拠を持たせたい情報は、モデルの記憶に頼らず RAG で外から与える」——ここに尽きます。手元のデータを外に出さずにそれができるのが、ローカルで組む RAG の価値だと感じました。

向いているのは、社内規程・製品仕様・過去の議事録のように「一般常識には無いが、正確さが必要な情報」を扱いたい人です。逆に、一般的な知識を聞くだけなら RAG の手間は要りません。今回のように、モデルが知らないと分かっている情報でこそ、RAG は効きます。

まとめ

Open WebUI を Docker で立て、ホスト側の Ollama にある Qwen2.5 に社内文書を読ませるところまでを一周しました。途中、ポート衝突・ボリューム残存・コンテナに道具が無い、という現実的な罠を踏みましたが、いずれも「本体とデータは別」「無ければ別の道具で代替」というローカル運用の基本に帰着します。

そして最後の RAG で、小型モデルの嘘が文書一枚で正解に変わりました。モデルを賢く鍛え直すのではなく、答えの前に根拠を差し出す。派手さはありませんが、手元の机の上で、自分のデータを外に出さずに嘘を正す仕組みが動く——ローカル LLM をわざわざ Docker で囲ってまで使う意味は、このあたりにあるのだと思います。