MacBook Pro にローカル RAG を構築する

前稿では、MacBook Pro でローカル LLM を実用化するには、モデルが起動するだけでなく、用途に対して十分な応答速度、記憶容量、モデル規模、運用方法が成立していなければならないことを整理した[1]。ローカル RAG の構築では、この条件に文書処理と検索の経路が加わる。Ollama、Open WebUI、Qdrant を起動し、画面上でモデルと対話できても、登録文書を根拠とする回答が生成されていなければ、完成したとはいえない。

RAG の処理は、文書から文字を取り出し、検索可能な単位へ分割し、各断片を埋め込みモデルで数値ベクトルへ変換し、ベクトルデータベースへ保存する登録経路から始まる。質問時には、質問文から同じ埋め込みモデルでベクトルを生成し、意味の近い文書断片を検索し、その断片を質問とともに生成用 LLM へ渡す。登録経路と検索経路が同じベクトル空間で接続され、取得された断片が生成入力へ組み込まれて初めて、外部文書を参照する回答が成立する。

ローカル RAG の実用性は、モデルを選択して推論を実行するだけでは成立しない。データ、権限、既存システム、評価工程へ接続する実装によって、単発の対話機能から継続的な処理基盤へ変わる[2]。RAG は、生成モデルが学習時に内部化した知識と、運用者が外部で管理する検索可能な知識を組み合わせる構成である[3]。この構成では、流暢な回答が返ることよりも、適切な文書断片が取得され、その断片から逸脱せずに回答が生成されたかを分けて評価する必要がある[4]

本稿では、Apple Silicon 搭載 MacBook Pro 上で Ollama を直接実行し、Open WebUI と Qdrant を Docker Compose で管理する。生成用モデルには qwen3.5:9b-q4_K_M、埋め込み用モデルには embeddinggemma を使用する。Ollama を macOS 側へ置き、画面とベクトル保存層をコンテナーへ分離することで、モデル推論とサービス管理を別々に更新できる構成とする。

構築後には、一般的な学習データから推測できない架空の固有情報を文書へ登録する。Knowledge を接続しない通常対話と、登録文書を接続した対話を同じ質問で比較し、後者だけが文書内の値を回答できるかを確認する。この対照試験により、モデルが事前知識や推測から回答した場合と、検索された文書断片を根拠に回答した場合を区別する。

1
2
3
4
5
6
7
8
9
10
11
MacBook Pro

├─ macOS
│    └─ Ollama
│         ├─ qwen3.5:9b-q4_K_M
│         └─ embeddinggemma

└─ Docker Desktop
     └─ Docker Compose
          ├─ Open WebUI 0.10.2
          └─ Qdrant 1.18.2

1. ローカル RAG の完成条件を固定する

1.1 起動成功と RAG 成功は異なる

Open WebUI の画面が表示され、Ollama の生成用モデルと対話できても、文書検索が動作しているとは限らない。通常対話は、Open WebUI から Ollama へ質問を送り、生成結果を受け取るだけで成立する。この経路では、Qdrant への接続、文書の埋め込み生成、類似検索のいずれも使われないため、RAG 側に障害があっても自然な回答が返る場合がある。

文書登録の画面表示も、処理完了の十分条件にはならない。Open WebUI にファイル名や Knowledge が表示されていても、文書形式に対応する抽出処理が失敗し、本文が空になっている可能性がある。本文を取得できても、分割結果が極端に短い、表の行と見出しが分離している、文字コードの問題で内容が崩れている場合には、質問に必要な意味単位を埋め込みへ変換できない。

Qdrant にコレクションと点が作成されていることは、登録経路が少なくとも保存段階まで進んだことを示す。ただし、保存されたベクトルが質問と同じ埋め込みモデルで作られているか、各点の付随情報に元文書の断片が含まれているか、検索結果の上位に必要な断片が入るかは別の確認事項である。保存件数だけを見ても、検索に使える内容が格納されたとは判断できない。

最終回答の正しさにも、同じ判定上の問題が残る。生成用 LLM が質問対象を学習データ内で知っている場合、文書検索を一度も行わなくても正答できる。反対に、検索自体は成功していても、取得断片に複数の値が含まれていたり、生成時の指示が曖昧だったりすれば、文書とは異なる回答を返す。回答の外形だけでは、検索と生成のどちらが成功または失敗したのかを切り分けられない。

確認段階 観察する事実 残る未確認事項
サービス起動 Ollama、Open WebUI、Qdrant の各プロセスが応答する。 サービス間の接続、文書登録、検索経路の成立は確認できない。
モデル対話 Open WebUI から生成用 LLM を選択し、回答を取得できる。 Qdrant と埋め込みモデルを使用したかは分からない。
文書登録 Open WebUI にファイルと Knowledge が表示される。 文字抽出、分割、埋め込み生成、保存の成否は画面表示だけでは確定しない。
ベクトル保存 Qdrant にコレクションと点が作成される。 各点に適切な断片が保存され、質問時に取得されるかは分からない。
類似検索 質問に対して関連する文書断片が上位候補として返る。 取得断片が生成用 LLM の入力へ実際に組み込まれたかは別に確認する。
回答生成 生成用 LLM が質問に対する自然な文章を返す。 登録文書を根拠にした回答か、事前知識や推測による回答かは外形だけでは判別できない。

この段階分けにより、障害の位置をサービス、文書処理、埋め込み、保存、検索、生成のいずれかへ絞り込める。たとえば、Qdrant に点が存在しない場合は登録経路を調べ、点は存在するが目的の断片が検索されない場合は分割方法と埋め込み設定を調べる。適切な断片を取得できているのに回答が異なる場合は、生成用 LLM へ渡された文脈と指示を確認する。全体を一つの「RAG の不具合」として扱わず、処理境界ごとに観測可能な事実を置くことが、再現可能な検証の前提になる。

1.2 完成条件を処理経路で定義する

ローカル RAG の完成条件は、画面上の機能数ではなく、文書登録から回答生成までの処理経路によって定義する。登録時には、原文を検索可能なベクトルと、回答時に再利用できる文字列へ変換する。質問時には、質問と文書断片を同じ基準で比較し、関連する断片を生成入力へ戻す。二つの経路は Qdrant の類似検索で接続される。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
文書登録経路

文書
  ↓
文字抽出
  ↓
チャンク分割
  ↓
embeddinggemma で埋め込み生成
  ↓
ベクトルと文書断片を Qdrant へ保存


質問検索経路

質問
  ↓
embeddinggemma で埋め込み生成
  ↓
Qdrant で類似検索
  ↓
関連チャンクを取得


回答生成経路

質問
  +
取得した関連チャンク
  ↓
qwen3.5 へ入力
  ↓
文書に基づく回答

登録経路では、文書断片と埋め込みベクトルの対応関係を失ってはならない。ベクトルだけを保存しても、検索後に生成用 LLM へ渡す原文を復元できない。反対に、原文だけを保存して埋め込みベクトルを作らなければ、質問との意味的な近さを検索できない。Qdrant の各点には、類似度計算に使うベクトルと、検索後に参照する文書断片および出典情報が対応して保存される必要がある。

質問検索経路では、登録時と質問時に同じ埋め込みモデルを使う。異なるモデルは、同じ文章から異なる次元数や配置規則を持つベクトルを生成するため、登録文書と質問を同じ意味空間で比較できなくなる。モデルを途中で変更した場合は、質問側だけを切り替えるのではなく、既存文書を新しいモデルで再度埋め込み、Qdrant の保存内容を作り直す必要がある。

チャンク分割は、検索単位と生成時の根拠単位を同時に決める。チャンクが大きすぎると、一つのベクトルに複数の話題が混在し、質問に対応する箇所と無関係な箇所が同時に取得される。小さすぎると、主語、条件、数値、例外規則が別々の断片へ分かれ、単独では意味を確定できなくなる。重複部分は境界で失われる文脈を補うが、重複を増やしすぎると類似した断片が検索上位を占有し、異なる根拠を取得できる件数が減る。

生成経路では、取得成功と回答忠実性を分けて扱う。検索結果に正しい断片が含まれていても、生成用 LLM がその断片より事前知識を優先すれば、登録文書と異なる回答になる。生成時の指示では、与えられた文書を根拠とし、文書に記載がない事項を推測しないという制約を明示する必要がある。検索精度の調整だけでは、根拠から逸脱する生成を防げない。

検証用文書には、一般知識や名称から答えを推測できない情報を入れる。たとえば、架空の Argolet システムについて、「本番ポートは 47821」「緊急停止では Lapis-7 手順を使う」と記載する。名称、数値、手順名の間に一般的な関連がないため、モデルがこれらを正確に回答した場合、登録文書または会話中の入力から取得した可能性が高い。

試験条件 質問 期待する結果 判定できること
Knowledge を接続しない Argolet の本番ポートを尋ねる。 情報不足として回答できない。 生成用 LLM が事前知識として正答を保持していないことを確認できる。
Knowledge を接続する 同じ質問を同じ表現で尋ねる。 47821 と回答する。 登録文書の検索結果が回答へ反映されたことを確認できる。
Knowledge を接続する Argolet の緊急停止方法を表現を変えて尋ねる。 Lapis-7 手順を使うと回答する。 単純な文字列一致ではなく、質問と文書の意味的な対応によって検索できることを確認できる。
Knowledge を接続する 文書に記載していない復旧時間を尋ねる。 文書からは判断できないと回答する。 取得文書にない情報を生成用 LLM が補完していないことを確認できる。

未接続時に誤答し、接続時に正答するだけでは、検証としてはまだ弱い。未記載事項に対して回答を保留できるか、質問の言い換えでも同じ根拠を取得できるか、類似した別の数値を混在させても対象を取り違えないかを加える必要がある。正答試験、言い換え試験、情報不足試験を組み合わせることで、単なる偶然の一致ではなく、検索と生成の両方が期待どおりに機能しているかを判定できる。

1.3 本稿の構成値

構築時の値は、最適値としてではなく、再現可能な初期条件として固定する。モデル、サービス、チャンク設定を同時に変更すると、回答が変化しても原因を特定できない。最初に一つの構成で登録経路と検索経路を成立させ、その後に変更対象を一項目ずつ分離して比較する。

項目 採用値 選定理由 変更時の確認事項
生成用 LLM qwen3.5:9b-q4_K_M を使用する。 日本語を含む一般的な対話に使用でき、9B 級の 4 ビット量子化モデルとして MacBook Pro 上で検証しやすい。 回答速度、記憶使用量、文書に対する忠実性を同じ質問セットで比較する。
埋め込みモデル embeddinggemma を使用する。 検索と類似度比較に用途を限定した軽量な多言語埋め込みモデルであり、生成用 LLM と役割を分離できる。 変更後は既存文書をすべて再埋め込みし、検索順位を再評価する。
Open WebUI 0.10.2 を使用する。 構築時点の版を固定し、設定項目、文書処理、Knowledge の挙動が更新によって変わった場合に差分を追跡できるようにする。 更新後は文書登録、検索結果、環境変数の既定値を再確認する。
Qdrant 1.18.2 を使用する。 ベクトル保存層の版を固定し、保存形式や検索挙動の変更をアプリケーション側の変更と分離する。 更新前に保存データを退避し、コレクション、点数、検索結果が維持されるか確認する。
チャンクサイズ 1000 文字を初期値とする。 Open WebUI の既定値を出発点とし、一つの断片へ含まれる話題数と、質問に必要な条件のまとまりを確認しながら調整する。 検索結果に無関係な段落が混在する場合は縮小し、条件や数値が分断される場合は拡大する。
チャンク重複 100 文字を初期値とする。 段落や文の境界付近にある主語、条件、数値が完全に分離されることを抑える。 同じ内容の断片が検索上位を占有する場合は重複量を減らす。
取得件数 3 件を初期値とする。 短い検証文書では根拠を限定した方が、どの断片が回答へ使われたかを追跡しやすい。 複数箇所を統合しなければ回答できない質問では増やし、無関係な断片が生成へ混入する場合は減らす。

生成用 LLM と埋め込みモデルは、同じ「モデル」であっても交換条件が異なる。生成用 LLM を変更した場合、既存のベクトルを維持したまま、同じ検索結果に対する回答品質を比較できる。埋め込みモデルを変更した場合は、文書と質問の配置されるベクトル空間そのものが変わるため、保存済み文書を再利用してはならない。この違いを無視すると、生成能力の差と検索順位の差が同時に発生し、改善または劣化の原因を特定できなくなる。

チャンクサイズ、重複量、取得件数にも、文書を問わず通用する唯一の最適値はない。手順書では、操作名、前提条件、コマンド、警告を同じ断片へ残す必要がある。仕様一覧では、一項目が短いため、大きなチャンクにすると無関係な複数項目が混在する。初期値を固定する目的は調整を省くことではなく、検索に失敗した質問と取得断片を記録し、文書構造に応じて変更するための比較基準を作ることにある。

この構成で確認するのは、MacBook Pro 上で各サービスが起動したという事実ではない。架空情報を含む文書が分割され、embeddinggemma のベクトルとして Qdrant へ保存され、質問時に必要な断片が取得され、qwen3.5:9b-q4_K_M がその断片の範囲内で回答するところまでを一続きの処理として確かめる。ローカル RAG の完成は、構成要素の存在ではなく、文書を根拠へ変換する経路を観測可能な形で再現できることによって決まる。


2. Ollama をホスト、Open WebUI と Qdrant をコンテナーへ配置する

2.1 Apple Silicon と Docker の境界

本構成では、推論処理を担う Ollama を macOS 上で直接動かし、対話画面を提供する Open WebUI と、ベクトルを保存する Qdrant を Docker Desktop 上へ配置する。すべてをコンテナーへ収容するのではなく、計算資源を使う処理と、更新や保存を管理したいサービスを分離する。

Apple Silicon では、CPU と GPU が同じユニファイドメモリを共有する[5]。Ollama を macOS 上で直接実行すると、生成用 LLM と埋め込みモデルの推論処理から、この計算資源をホスト側の実装を通じて利用できる。Ollama の macOS 版は Apple M シリーズへ対応し、取得したモデルを利用者のローカル領域へ保存する[6]

生成用 LLM と埋め込みモデルを同じ Ollama 上へ置くのは、両者を同一モデルとして扱うためではない。生成用 LLM は質問と取得文書から回答を組み立て、埋め込みモデルは文書と質問を検索可能なベクトルへ変換する。役割は異なるが、モデルの取得、起動、API 接続、保存場所を一つのローカル推論基盤へ集約できるため、コンテナー内へ別の推論環境を構築する必要がなくなる。

Open WebUI と Qdrant は Docker Desktop 上へ配置する。Docker Desktop には Apple Silicon 用の正式な配布物があり、Arm64 環境に対応したコンテナーを実行できる[7]。Docker Compose を使えば、使用するイメージ、サービス間ネットワーク、環境変数、ポート、保存領域を一つの構成ファイルへ固定できる[8]

コンテナーは削除して再作成できることを前提とするため、Open WebUI の利用者情報や Knowledge の管理情報、Qdrant のベクトルデータをコンテナー内部だけへ保存してはならない。保存領域をボリュームとして分離すると、イメージを更新してコンテナーを作り直しても、運用状態を引き継げる[9]。アプリケーションの実行環境と永続データを分けることで、更新失敗時にも旧版へ戻しやすくなる。

Open WebUI はコンテナー内から、macOS 上で動作する Ollama の API へ接続する。コンテナー内の localhost は MacBook Pro ではなく、そのコンテナー自身を指す。Ollama の接続先へ http://localhost:11434 を指定すると、Open WebUI は自分自身の 11434 番ポートへ接続しようとして失敗する。

Docker Desktop では、コンテナーからホストへ接続するための名前として host.docker.internal が用意されている[10]。Open WebUI には Ollama の接続先として http://host.docker.internal:11434 を指定する。これにより、画面と文書管理はコンテナー内へ分離したまま、生成と埋め込みの要求だけを macOS 上の Ollama へ送れる。

処理 実行場所 配置理由 障害時の確認対象
生成推論 macOS 上の Ollama で実行する。 Apple Silicon の計算資源をホスト側の推論処理から利用する。 Ollama の起動状態、モデルの取得状態、11434 番ポートの応答を確認する。
埋め込み生成 macOS 上の Ollama で実行する。 生成用 LLM と同じローカル推論基盤でモデルの取得と API 接続を管理する。 埋め込みモデルの存在、Open WebUI のモデル指定、Ollama API の応答を確認する。
対話画面と文書管理 Docker 上の Open WebUI で実行する。 版、環境変数、ポート、永続領域を Docker Compose で固定する。 コンテナーのログ、Ollama 接続先、Knowledge の登録状態を確認する。
ベクトル保存と検索 Docker 上の Qdrant で実行する。 検索用データを Open WebUI 本体から分離し、コレクションと点を独立して観察する。 Qdrant の稼働状態、コレクション、点数、Open WebUI からの接続を確認する。

この配置では、Ollama の障害とコンテナー側の障害を分けて判定できる。Ollama API がホスト上で応答するのに Open WebUI からモデルが見えない場合は、Docker からホストへの接続を調べる。Open WebUI から通常対話はできるが文書登録に失敗する場合は、埋め込みモデルまたは Qdrant との接続を調べる。処理の境界を実行場所と一致させることで、RAG 全体を一つの不可視な処理として扱わずに済む。

2.2 MacBook Pro の状態を確認する

導入前に、CPU アーキテクチャー、macOS の版、ユニファイドメモリ容量、SSD の空き容量を記録する。同じモデル名を使用しても、搭載メモリや同時に動作するアプリケーションが異なれば、推論速度と安定性は変わる。後からモデルや Docker の設定を変更した際に比較できるよう、構築時点の条件を残しておく。

1
2
3
4
uname -m
sw_vers
system_profiler SPHardwareDataType
df -h /

uname -m の結果が arm64 であれば、Apple Silicon 用の実行環境であることを確認できる。sw_vers では macOS の版とビルド番号を記録する。Ollama や Docker Desktop の更新後に動作が変わった場合、OS 側の更新を含めて条件を比較できる。

system_profiler SPHardwareDataType では、Chip と Memory を確認する。ユニファイドメモリは Ollama だけが専有するものではなく、macOS、通常のアプリケーション、Docker Desktop の仮想環境と共有される。モデルファイルが SSD に収まることと、推論時にモデルを安定して実行できることは別の条件である。

qwen3.5:9b-q4_K_M は、モデルデータだけでおよそ 6.6 GB を使用する。これに Ollama の実行時領域、会話文脈、埋め込みモデル、Docker Desktop、Open WebUI、Qdrant、登録文書、ベクトル索引の使用量が加わる。SSD にはモデルファイルと Docker イメージを保存できるだけでなく、更新時に新旧データが一時的に併存できる余裕を確保する。

確認対象 確認する値 不足した場合に起きること 初期対応
CPU アーキテクチャー arm64 であることを確認する。 異なるアーキテクチャー向けの配布物やイメージを選ぶと、起動不能または変換実行になる。 Apple Silicon 用の Ollama と Docker Desktop を使用する。
ユニファイドメモリ 搭載容量と通常時の使用状況を確認する。 メモリ圧迫によりスワップが増え、生成速度の低下やアプリケーションの停止が起きる。 生成用 LLM の規模、文脈長、Docker への割り当てを下げる。
SSD 空き容量 df の Available を確認する。 モデル取得、イメージ更新、文書登録、索引作成が途中で失敗する。 不要なモデルと Docker データを整理し、更新用の空き容量を確保する。
macOS の版 製品版とビルド番号を記録する。 更新前後の性能差や接続障害を、構成変更と区別できなくなる。 構築記録へ版とビルド番号を残す。

16 GB 機では、9B 級モデルの起動だけで直ちに使用不能になるとは限らないが、Docker Desktop や他のアプリケーションとの同時使用によって余裕が小さくなる。メモリ圧迫やスワップの増加が続く場合は、生成用 LLM を 4B 級へ下げる選択肢を残す。24 GB 以上では 9B 級を初期構成として試し、応答速度とメモリ圧力を観察したうえで維持するか判断する。

これらの容量別判断は初期設定の目安であり、機種ごとの実測性能を示すものではない。

モデル規模の判断は、読み込めたかどうかだけでは決めない。短い質問には応答できても、Knowledge から取得した複数のチャンクと長い会話履歴を同時に入力すると、必要な記憶容量が増える。RAG の実用条件は、モデル単体の起動試験ではなく、文書検索を含む実際の入力長で確認する必要がある。

2.3 Ollama と Docker Desktop を導入する

Ollama は公式の macOS 用アプリケーションをインストールし、一度起動する。初回起動によって、バックグラウンドで API を受け付ける状態を作り、ターミナルから ollama コマンドを利用できるようにする。アプリケーションを配置しただけで起動していない場合、コマンドが存在しても 11434 番ポートの API は応答しない。

Docker Desktop も公式の Apple Silicon 用インストーラーから導入し、アプリケーションを起動する。docker コマンドが存在していても、Docker Desktop の仮想環境が停止していれば、コンテナーの作成や状態確認は実行できない。コマンドの導入と実行基盤の起動を分けて確認する。

1
2
3
ollama --version
docker version
docker compose version

ollama –version では、利用する Ollama の版を記録する。docker version では、クライアントだけでなくサーバー側の情報が表示されることを確認する。クライアント情報だけが表示され、デーモンへの接続エラーが返る場合は、Docker Desktop が起動していないか、初期化が完了していない。

docker compose version では、Docker Compose を独立した旧コマンドではなく、Docker CLI のサブコマンドとして利用できることを確認する。本稿の操作は docker-compose ではなく docker compose を使用し、構成の起動、停止、更新手順を統一する。

Docker Desktop の Resources では、Open WebUI と Qdrant が動作できるメモリを確保する。ただし、Docker へ割り当てたメモリは、ホスト上の Ollama と無関係に増えるわけではない。両者は最終的に同じユニファイドメモリを消費するため、Docker 側の上限を過剰に広げると、文書登録や検索は安定しても、生成用 LLM の推論時にメモリ圧迫が発生する。

24 GB 機では、Docker 側を 4 GB 前後から開始し、Open WebUI の起動、Qdrant への保存、文書登録時の使用量を観察して調整する。大量の文書を一度に登録すると、文字抽出、チャンク分割、埋め込み要求、Qdrant への保存が連続するため、通常対話時より負荷が高くなる。初期値を大きく設定するのではなく、実際の文書量で不足が確認された場合にだけ増やす。

確認状態 観察結果 判定
Ollama の版が表示される ollama –version が正常終了する。 コマンドは利用できるが、API の起動状態は別途確認する。
Docker のクライアントとサーバーが表示される docker version に両方の情報が出る。 Docker Desktop の実行基盤まで起動している。
Compose の版が表示される docker compose version が正常終了する。 後続の構成ファイルを Docker CLI から操作できる。

2.4 使用ポートを確認する

サービスを起動する前に、使用するポートと公開範囲を固定する。ポート番号が重複するとコンテナーは起動できず、公開先を省略すると、意図せずローカルネットワーク上の別端末から接続可能になる場合がある。ローカル RAG として一台の MacBook Pro 内で利用する限り、Open WebUI と Qdrant を外部インターフェースへ公開する必要はない。

ポート 用途 接続元 公開範囲
11434 Ollama のローカル API に使用する。 macOS 上のコマンドと、Open WebUI コンテナーから接続する。 macOS 上で利用し、コンテナーからは host.docker.internal を経由する。
3000 Open WebUI のブラウザー画面に使用する。 MacBook Pro 上のブラウザーから接続する。 127.0.0.1 にだけ割り当てる。
6333 Qdrant の REST API と管理画面に使用する。 Open WebUI コンテナーと、MacBook Pro 上の確認操作から接続する。 127.0.0.1 にだけ割り当てる。
6334 Qdrant の gRPC API に使用する。 必要な場合は Docker 内部のサービスから接続する。 本構成ではホストへ公開しない。

Open WebUI の 3000 番ポートを 127.0.0.1 へ限定すると、同じ MacBook Pro 上のブラウザーからは接続できるが、同一ネットワーク上の別端末からは直接接続できない。Qdrant の 6333 番ポートも同様に限定する。Qdrant には登録文書の断片やベクトルが保存されるため、認証や外部公開の設計を行っていない段階でネットワークへ公開しない。

6334 番ポートは Qdrant の gRPC に使用されるが、本構成ではホスト上から直接利用しない。Open WebUI と Qdrant は同じ Docker Compose ネットワーク内でサービス名を使って通信できるため、コンテナー間通信に必要なポートをすべて macOS 側へ公開する必要はない。ホストへの公開は、ブラウザーや確認用コマンドから直接接続するものに限定する。

起動前に、予定しているポートを別のプロセスが待ち受けていないか確認する。

1
2
3
lsof -nP -iTCP:11434 -sTCP:LISTEN
lsof -nP -iTCP:3000 -sTCP:LISTEN
lsof -nP -iTCP:6333 -sTCP:LISTEN

3000 番と 6333 番で出力が空であれば、現時点でそのポートを待ち受けているプロセスはない。出力がある場合は、プロセス名と PID を確認し、停止するか、Docker Compose 側のホストポートを変更する。既存サービスを確認せずに終了させてはならない。

11434 番は、Ollama を起動した後であれば待ち受けが確認できる。Ollama を起動しているにもかかわらず出力がない場合、Open WebUI の設定を進める前に Ollama 側の状態を調べる。ホスト上で API が待ち受けていなければ、host.docker.internal の指定が正しくてもコンテナーから接続できない。

ポート設計によって、利用経路と公開範囲を一致させる。生成と埋め込みは Open WebUI からホスト上の Ollama へ接続し、Open WebUI と Qdrant の確認画面は MacBook Pro 自身からだけ開く。RAG をローカルで動かすという条件には、モデルと文書を端末内へ置くだけでなく、不要なネットワーク公開を行わないことも含まれる。


3. Ollama の生成用 LLM と埋め込みモデルを個別に確認する

3.1 Ollama API の起動を確認する

Open WebUI と接続する前に、macOS 上の Ollama が単独で応答することを確認する。Open WebUI は、Ollama の HTTP API を通じて、取得済みモデルの一覧、文章生成、埋め込み生成を呼び出す[11]。この段階で API が応答しなければ、後から Open WebUI や Qdrant を起動しても RAG の処理経路は成立しない。

/api/tags は、Ollama のローカル領域へ取得済みのモデルを返す。これに対して ollama ps は、現在メモリへ読み込まれているモデルを表示する。保存済みモデルと実行中モデルは異なるため、二つの結果を同じ意味で扱わない。

1
2
3
curl -fsS http://localhost:11434/api/tags | python3 -m json.tool
ollama list
ollama ps

curl が正常終了して JSON を返せば、11434 番ポートの Ollama API へ接続できている。モデルをまだ取得していない場合、models は空の配列になる。これは API の障害ではなく、ローカルに利用可能なモデルが存在しない状態を示す。

ollama list にモデルが表示されても、ollama ps が空になることはある。取得済みモデルは SSD に保存されるが、推論要求を受けるまではメモリへ読み込まれないためである。生成または埋め込みを一度実行した後に ollama ps を確認すると、読み込まれたモデル、使用量、処理装置、割り当てられたコンテキスト長を確認できる。

確認方法 確認対象 空の場合の意味 エラーの場合の確認先
/api/tags API の応答と取得済みモデルを確認する。 API は動作しているが、モデルをまだ取得していない。 Ollama アプリケーションの起動状態と 11434 番ポートを確認する。
ollama list ローカルへ保存されたモデルを確認する。 生成用モデルと埋め込みモデルをまだ取得していない。 Ollama の導入状態と利用者のモデル保存領域を確認する。
ollama ps 現在メモリへ読み込まれているモデルを確認する。 実行中または待機中のモデルがない。 生成要求または埋め込み要求が正常終了したかを確認する。

接続拒否になる場合は、Ollama アプリケーションが起動しているかを確認する。JSON ではなく HTML や別サービスの応答が返る場合は、11434 番ポートを Ollama 以外のプロセスが使用していないかを調べる。Open WebUI 側の接続設定を変更するのは、ホスト上の API が正常に応答することを確認した後である。

3.2 生成用モデルを取得する

生成用モデルには qwen3.5:9b-q4_K_M を使用する。9B はモデルのおおよその規模を示し、q4_K_M は重みを 4 ビットへ量子化した形式を示す。量子化によって保存容量と実行時の記憶使用量を抑えられるため、MacBook Pro の限られたユニファイドメモリでモデルを実行しやすくなる。

量子化は、元の重みをより少ないビット数で表現する処理である。保存容量が減るだけでなく、推論時に読み込むデータ量も小さくなる一方、元の精度を完全には保持しない。q4_K_M は、最大精度を求める構成ではなく、応答品質、記憶使用量、生成速度の均衡を取るための選択である。

Ollama のモデルライブラリーには、qwen3.5:9b-q4_K_M のタグ、量子化形式、容量、対応する最大コンテキスト長が掲載されている[12]。本稿の構築時点では、モデルファイルは約 6.6 GB である。ただし、この値は SSD 上のモデルファイル容量であり、実行時の総記憶使用量を表すものではない。

1
2
3
ollama pull qwen3.5:9b-q4_K_M
ollama list
ollama show qwen3.5:9b-q4_K_M

ollama pull が完了した後、ollama list でモデル名と保存容量を確認する。ollama show では、モデル系列、量子化形式、対応機能、モデルに設定されたパラメーターを確認できる。記事内ではタグを省略せず、検証した量子化形式まで含めて記録する。同じ qwen3.5:9b 系列でも、異なる量子化タグを使用すれば、容量と実行特性が変わるためである。

16 GB 機で継続的なメモリ圧迫、スワップの増加、著しい生成速度低下が起きる場合は、同じ系列の 4B 級モデルへ変更する。モデル規模を下げる際も、どの量子化形式を使用したかを固定する。単に qwen3.5:4b と記録すると、後に既定タグの内容が変わった場合、同じ構成を再現できなくなる。

生成用モデルの変更は、既存文書の再登録を必要としない。文書と質問をベクトルへ変換する埋め込みモデルを維持する限り、Qdrant 内の索引はそのまま利用できる。生成用モデルを 9B 級から 4B 級へ変更した場合に比較するのは、同じ検索結果に対する回答の忠実性、文章品質、速度、記憶使用量である。

3.3 生成 API を単体で試験する

Open WebUI を起動する前に、Ollama の生成 API へ直接要求を送り、日本語入力から回答を生成できることを確認する。ここで失敗する場合、原因は Open WebUI や Qdrant ではなく、Ollama の起動、モデル取得、モデル読み込み、または生成処理にある。

/api/generate は、モデル名と入力文を受け取り、生成結果を返す。既定では応答が逐次返されるため、検証では stream を false にして、一つの JSON として結果と計測値を取得する。思考出力に対応するモデルでは、検証対象を最終回答へ限定するため、think も false に固定する。

1
2
3
4
5
6
7
8
curl -fsS http://localhost:11434/api/generate \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "qwen3.5:9b-q4_K_M",
    "prompt": "ローカル RAG における生成用 LLM と埋め込みモデルの役割の違いを三文で説明してください。",
    "stream": false,
    "think": false
  }' | tee /tmp/ollama-generate.json | python3 -m json.tool

response に日本語の回答が入り、done が true になれば、生成要求は最後まで完了している。質問内容に対する正確さは別途評価する必要があるが、少なくとも Open WebUI を介さない生成経路が成立したことを確認できる。

Ollama の非逐次応答には、入力処理時間、生成時間、入出力トークン数などの計測値が含まれる[13]。各時間はナノ秒で返されるため、秒へ換算して記録する。出力速度は、eval_count を eval_duration の秒数で割ることで求められる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python3 - <<'PY'
import json

with open("/tmp/ollama-generate.json", encoding="utf-8") as file:
    data = json.load(file)

total_seconds = data["total_duration"] / 1_000_000_000
load_seconds = data["load_duration"] / 1_000_000_000
prompt_seconds = data["prompt_eval_duration"] / 1_000_000_000
eval_seconds = data["eval_duration"] / 1_000_000_000
tokens_per_second = data["eval_count"] / eval_seconds if eval_seconds else 0.0

print(f"total_seconds={total_seconds:.3f}")
print(f"load_seconds={load_seconds:.3f}")
print(f"prompt_tokens={data['prompt_eval_count']}")
print(f"prompt_seconds={prompt_seconds:.3f}")
print(f"output_tokens={data['eval_count']}")
print(f"generation_seconds={eval_seconds:.3f}")
print(f"tokens_per_second={tokens_per_second:.2f}")
PY
項目 意味 判定への使い方
total_duration 要求開始から完了までの総時間を示す。 利用者が待つ総時間の比較に使う。
load_duration モデルをメモリへ読み込むために使った時間を示す。 初回実行と連続実行の差を分離する。
prompt_eval_count 処理した入力トークン数を示す。 質問や取得文書の長さが増えた場合の負荷を比較する。
prompt_eval_duration 入力トークンを処理した時間を示す。 RAG で複数チャンクを追加したときの入力処理負荷を確認する。
eval_count 生成した出力トークン数を示す。 回答量が異なる試験を時間だけで比較しないために使う。
eval_duration 出力トークンを生成した時間を示す。 一秒当たりの生成トークン数を算出する。

初回要求では、モデルを SSD からユニファイドメモリへ読み込むため、load_duration が大きくなる。モデルが待機状態に残っている間に同じ要求を再実行すると、読み込み時間が短くなる場合がある。初回値と連続実行値を混在させず、少なくとも二回実行して区別する。

ollama ps では、生成後にモデルがどの処理装置へ読み込まれたかを確認する。CPU への退避が増えている場合、モデル自体は動作しても生成速度が大きく低下する可能性がある。回答文だけでなく、PROCESSOR、CONTEXT、生成速度を併記することで、MacBook Pro 上で継続利用できる構成かを判断できる。

3.4 埋め込みモデルを取得する

埋め込みは、文章を固定長の数値ベクトルへ変換する処理である。生成用 LLM が文章を出力するのに対し、埋め込みモデルは文章同士の意味的な近さを計算できる表現を出力する。得られたベクトルは、意味検索、類似文書検索、分類、ベクトルデータベース、RAG に使用される[14]

本構成では embeddinggemma を使用する。生成用モデルと埋め込みモデルを分離することで、回答生成に必要な規模と、文書検索に必要な処理を個別に選択できる。埋め込み処理に 9B 級の生成モデルを流用せず、検索用に設計された小型モデルを使うため、文書登録と質問検索の負荷を抑えられる。

1
2
3
ollama pull embeddinggemma
ollama list
ollama show embeddinggemma

ollama list では、生成用の qwen3.5:9b-q4_K_M と埋め込み用の embeddinggemma が別々のモデルとして表示される。ollama show embeddinggemma では、モデルの詳細と埋め込み機能への対応を確認する。

Ollama の埋め込み API は、入力された文章ごとに数値配列を返す[15]。一つの文章を入力した場合も、複数の文章をまとめて入力した場合も、応答の embeddings は配列の配列となる。外側の要素数は入力文章数に対応し、内側の要素数は埋め込みベクトルの次元数に対応する。

埋め込みモデルを変更する場合、Qdrant に保存済みのベクトルをそのまま使ってはならない。モデルが変わると、ベクトルの次元数が同じであっても、各数値が表す配置規則は変わる。文書側だけが旧モデル、質問側だけが新モデルという状態では、距離計算を実行できても、意味的な類似度として解釈できない。

3.5 埋め込み API を単体で試験する

埋め込み API の単体試験では、入力数、ベクトル数、次元数を確認した後、意味の近い文章が相対的に近い値を示すかを確認する。配列が返ったという事実だけでは、選択したモデルが日本語の検索に利用できるかまでは判断できない。

次の試験では、ローカル LLM に関する二文と、話題の異なる一文を同時に埋め込む。文章埋め込みは、表面的な文字列の一致だけではなく、意味的に近い文章を近いベクトルとして比較するために設計される[16]

1
2
3
4
5
6
7
8
9
10
curl -fsS http://localhost:11434/api/embed \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "embeddinggemma",
    "input": [
      "MacBook Pro でローカル LLM を動かす",
      "Apple Silicon 上で大規模言語モデルを実行する",
      "冷蔵庫で野菜を保存する"
    ]
  }' | tee /tmp/ollama-embeddings.json | python3 -c 'import json, sys; data=json.load(sys.stdin); print("vectors=", len(data["embeddings"])); print("dimensions=", [len(vector) for vector in data["embeddings"]])'

vectors が 3 であり、三つの dimensions が同じ正の値であれば、各入力が同じ次元のベクトルへ変換されたことを確認できる。ただし、これは API の出力形状が正しいことを示すだけであり、意味検索の品質を示すものではない。

意味的な配置は、ベクトル間のコサイン類似度を計算して確認する。次の処理では、一文目と二文目、一文目と三文目の類似度を比較する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python3 - <<'PY'
import json
import math

with open("/tmp/ollama-embeddings.json", encoding="utf-8") as file:
    vectors = json.load(file)["embeddings"]

def cosine(left, right):
    dot = sum(a * b for a, b in zip(left, right))
    left_norm = math.sqrt(sum(value * value for value in left))
    right_norm = math.sqrt(sum(value * value for value in right))
    return dot / (left_norm * right_norm)

local_llm_similarity = cosine(vectors[0], vectors[1])
unrelated_similarity = cosine(vectors[0], vectors[2])

print(f"local_llm_similarity={local_llm_similarity:.6f}")
print(f"unrelated_similarity={unrelated_similarity:.6f}")
print(f"expected_order={local_llm_similarity > unrelated_similarity}")
PY

local_llm_similarity が unrelated_similarity より高ければ、この三文に対しては、ローカル LLM に関する二文が、冷蔵庫に関する文より近く配置されたことになる。この一回の結果だけで検索品質全体を保証することはできないが、日本語入力、ベクトル生成、類似度計算までの最小経路を確認できる。

確認段階 成功条件 確認できないこと
API 応答 embeddings が返る。 入力数と出力数が一致するかは別に確認する。
出力数 入力した三文に対して三つのベクトルが返る。 各ベクトルの次元が一致するかは別に確認する。
次元数 すべてのベクトルが同じ正の次元数を持つ。 意味的に妥当な配置かは次元数だけでは分からない。
類似度 同じ話題の文章が、無関係な文章より高い類似度を示す。 実際の文書群で必要なチャンクが検索上位へ入るかは分からない。

RAG では、文書登録時と質問時に同じ埋め込みモデルを使用するだけでなく、モデルの版とタグも固定する。埋め込みモデルを更新または変更した場合は、既存の Qdrant コレクションを新しいモデルで作成したベクトルへ置き換える。新旧ベクトルを同じコレクションへ混在させると、検索結果が不安定になっても原因を追跡できない。

3.6 コンテキスト長を必要以上に広げない

コンテキスト長は、生成用 LLM が一回の要求で参照できる入力と出力の上限である。RAG では、利用者の質問、会話履歴、システム指示、検索された文書断片が同じコンテキストを消費する。取得件数またはチャンクサイズを増やすと、モデルへ渡す根拠は増えるが、その分だけ記憶使用量と入力処理時間も増える。

モデルが対応する最大コンテキスト長と、実行時に確保されるコンテキスト長は異なる。qwen3.5:9b-q4_K_M のモデル情報に 256K と表示されていても、MacBook Pro 上で常に 256K が割り当てられるわけではない。Ollama は利用可能な計算資源と設定に応じて実行時の長さを決め、長いコンテキストほど多くの記憶領域を使用する[17]

初期検証では、Open WebUI のモデル設定を 8192 トークンから開始する。三件の短いチャンクと質問を入力する構成で不足が確認された場合は、16384 トークンへ増やす。上限値を先に最大化するのではなく、実際に取得するチャンク数、文書断片の長さ、会話履歴に対して必要な範囲だけを割り当てる。

設定または条件 増やした場合の効果 増やした場合の負担 調整判断
コンテキスト長 より長い質問、会話履歴、検索結果を一度に参照できる。 記憶使用量が増え、CPU への退避や処理速度低下が起きやすくなる。 必要な入力が切り捨てられる場合にだけ増やす。
チャンクサイズ 一つの検索結果へ長い文脈を残せる。 無関係な記述が混入し、一件当たりのトークン数が増える。 条件、数値、例外が分断される場合に調整する。
取得件数 複数箇所の根拠を同時に参照できる。 類似した断片や無関係な断片が入力へ入りやすくなる。 必要な根拠が検索上位から漏れる場合に増やす。
会話履歴 過去の質問と回答を踏まえて応答できる。 現在の質問に使える根拠領域が減る。 独立した検証では新しい会話を作成する。

設定後は、生成要求を一度実行してから ollama ps を確認する。

1
ollama ps

CONTEXT には実行中モデルへ割り当てられたコンテキスト長が表示される。PROCESSOR では、モデルが GPU 側へどの程度配置され、CPU 側へ退避しているかを確認する。設定したコンテキスト長を増やした後に CPU 側への退避が増え、生成速度が大きく低下した場合、その設定はモデルの上限内であっても、その MacBook Pro における実用値ではない。

RAG の目的は、文書全体を最大コンテキストへ詰め込むことではない。質問に必要な断片を埋め込み検索で選び、生成用 LLM へ渡す入力を限定することにある。チャンクサイズ 1000 文字、重複 100 文字、取得件数 3 件という初期条件で正しい根拠を取得できるなら、256K の最大値を確保する理由はない。

この章の完了条件は、生成用モデルと埋め込みモデルの名前が ollama list に表示されることではない。生成 API が日本語回答と計測値を返し、埋め込み API が入力数と一致するベクトルを返し、意味の近い文章が相対的に近く配置され、実行時のコンテキスト長と処理装置を ollama ps で確認できるところまでである。この単体試験を先に終えることで、後続の Open WebUI 接続や Qdrant 登録で障害が起きた際に、Ollama 側を切り分け済みの前提として扱える。


4. Docker Compose で Open WebUI と Qdrant を構築する

4.1 作業ディレクトリを作る

構成ファイル、永続データ、検証文書、バックアップを一つの作業ディレクトリへまとめる。Open WebUI と Qdrant の保存領域は分離し、どのサービスがどの状態を保持しているかをディレクトリ単位で判別できるようにする。

Open WebUI の保存領域には、利用者情報、会話、Knowledge の管理情報、アプリケーション設定が含まれる。Qdrant の保存領域には、埋め込みベクトル、文書断片に対応する付随情報、コレクションの索引が置かれる。両者を同じディレクトリへ混在させると、バックアップ、復元、障害調査の対象を分離できない。

1
2
3
4
5
6
mkdir -p "$HOME/local-rag/data/open-webui"
mkdir -p "$HOME/local-rag/data/qdrant"
mkdir -p "$HOME/local-rag/data/qdrant-snapshots"
mkdir -p "$HOME/local-rag/documents"
mkdir -p "$HOME/local-rag/backup"
cd "$HOME/local-rag"
パス 保存対象 消失した場合の影響
data/open-webui 利用者、会話、Knowledge の管理情報、Open WebUI の設定を保存する。 利用者情報と会話に加え、登録済み文書とベクトルの対応関係を失う。
data/qdrant Qdrant のコレクション、点、ベクトル、付随情報、索引を保存する。 文書が Open WebUI 側に残っていても、検索用ベクトルを再構築する必要が生じる。
data/qdrant-snapshots Qdrant が生成するスナップショットを保存する。 コレクション単位の復元に使える退避データを失う。
documents 登録前の原文と検証用文書を保存する。 索引を作り直す際の原本を失い、登録内容を再現できなくなる。
backup Open WebUI の保存領域や構成ファイルの退避先として使用する。 更新失敗や設定破損が起きた場合に、更新前の状態へ戻せなくなる。

この分離により、回答生成、文書管理、ベクトル検索のどこで状態が失われたかを判断できる。たとえば、Open WebUI の画面に文書が残っているのに検索できない場合は Qdrant 側を調べ、Qdrant にコレクションが残っているのに Knowledge が表示されない場合は Open WebUI 側の管理情報を調べる。

4.2 秘密値と版を環境変数ファイルへ記録する

Qdrant の API 鍵、Open WebUI の秘密鍵、使用するイメージ版、モデル名を .env へ記録する。Qdrant は QDRANT__SERVICE__API_KEY による API 鍵認証を設定できる[18]。本構成では 6333 番ポートを 127.0.0.1 に限定したうえで、Open WebUI からの接続にも同じ API 鍵を要求する。

WEBUI_SECRET_KEY は、Open WebUI がセッションや暗号化対象の情報を継続して扱うための秘密値である。コンテナーの再作成ごとに値が変わると、既存のセッションや暗号化済み情報を利用できなくなる可能性があるため、初回構築時に生成した値を更新後も維持する。

.env を書き込む前に権限を 600 にする。書き込み後に権限を変更するだけでは、生成から chmod までの短時間、通常の umask に基づく権限でファイルが存在する可能性がある。空ファイルを先に作成して権限を制限し、その後で秘密値を書き込む。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QDRANT_API_KEY=$(openssl rand -hex 32)
WEBUI_SECRET_KEY=$(openssl rand -hex 32)

: > .env
chmod 600 .env

cat > .env <<EOF
OPEN_WEBUI_IMAGE=ghcr.io/open-webui/open-webui:v0.10.2
QDRANT_IMAGE=qdrant/qdrant:v1.18.2
GENERATION_MODEL=qwen3.5:9b-q4_K_M
EMBEDDING_MODEL=embeddinggemma
ENABLE_SIGNUP=True
QDRANT_API_KEY=${QDRANT_API_KEY}
WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
EOF

unset QDRANT_API_KEY WEBUI_SECRET_KEY

イメージ名には latest や main を使用せず、Open WebUI 0.10.2 と Qdrant 1.18.2 を固定する。固定しない場合、同じ docker compose pull を実行しても取得される実装が変わり、環境変数、保存形式、検索処理のどこで挙動が変化したかを追跡できなくなる。

GENERATION_MODEL は、この章の Compose 構成から Open WebUI へ直接渡す値ではない。検証に使用するモデル名を構成情報と同じ場所へ記録し、後続の設定確認や試験用コマンドで再利用するために置く。これに対して EMBEDDING_MODEL は、Open WebUI の埋め込みモデル設定へ渡される。

ENABLE_SIGNUP は、初回の管理者登録を行うため、構築時には True とする。最初の利用者を登録した後、この値を False へ変更してコンテナーを再作成する。Open WebUI が初回登録後に新規登録を停止しても、環境変数を構成の正とする場合、再起動時に True が再適用されないよう明示的に固定する必要がある。

.env には認証情報が含まれるため、Git へ登録しない。Open WebUI と Qdrant の実データも、容量が大きく、実行時に更新され続けるため、通常のソースコード管理から除外する。

1
2
3
4
5
cat > .gitignore <<'EOF'
.env
data/
backup/
EOF

documents は除外していない。架空情報を含む検証文書を版管理すれば、どの原文からどの検索結果を得たかを後から再現できる。ただし、実際の業務文書や個人情報を登録する場合は、文書の機密区分に応じて documents も除外する。

4.3 compose.yaml を作成する

Open WebUI は Docker を主要な導入方法として案内し、/app/backend/data へ永続領域を割り当てる構成を提供している[19]。Open WebUI の環境変数では、Ollama の接続先、埋め込みエンジン、ベクトルデータベース、Qdrant の接続情報、検索件数、文書分割条件を指定できる[20]

Qdrant は 6333 番ポートを HTTP API と管理画面に使用し、Docker イメージでは /qdrant/storage へ永続データを保存する[21]。スナップショットは通常の保存領域と分離し、コンテナーの再作成後も残るようにする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
cat > compose.yaml <<'EOF'
name: local-rag

services:
  qdrant:
    image: ${QDRANT_IMAGE}
    container_name: local-rag-qdrant
    restart: unless-stopped
    ports:
      - "127.0.0.1:6333:6333"
    environment:
      QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY}
      QDRANT__STORAGE__SNAPSHOTS_PATH: /qdrant/snapshots
    volumes:
      - ./data/qdrant:/qdrant/storage
      - ./data/qdrant-snapshots:/qdrant/snapshots

  open-webui:
    image: ${OPEN_WEBUI_IMAGE}
    container_name: local-rag-open-webui
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:8080"
    environment:
      WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY}
      ENABLE_PERSISTENT_CONFIG: "False"
      ENABLE_SIGNUP: "${ENABLE_SIGNUP}"
      ENABLE_OLLAMA_API: "True"
      OLLAMA_BASE_URL: "http://host.docker.internal:11434"
      ENABLE_OPENAI_API: "False"
      ENABLE_WEB_SEARCH: "False"
      ENABLE_KB_EXEC: "False"
      VECTOR_DB: "qdrant"
      QDRANT_URI: "http://qdrant:6333"
      QDRANT_API_KEY: ${QDRANT_API_KEY}
      QDRANT_PREFER_GRPC: "False"
      ENABLE_QDRANT_MULTITENANCY_MODE: "True"
      RAG_EMBEDDING_ENGINE: "ollama"
      RAG_EMBEDDING_MODEL: ${EMBEDDING_MODEL}
      RAG_TOP_K: "3"
      ENABLE_RAG_HYBRID_SEARCH: "False"
      BYPASS_EMBEDDING_AND_RETRIEVAL: "False"
      RAG_FULL_CONTEXT: "False"
      CHUNK_SIZE: "1000"
      CHUNK_OVERLAP: "100"
      RAG_TEXT_SPLITTER: "character"
      ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER: "True"
      CONTENT_EXTRACTION_ENGINE: ""
      PDF_EXTRACT_IMAGES: "False"
    volumes:
      - ./data/open-webui:/app/backend/data
    depends_on:
      - qdrant
EOF

docker compose config --quiet

docker compose config –quiet が出力なしで正常終了すれば、YAML の構文、必須変数の展開、サービス定義を読み取れる状態である。通常の docker compose config は展開後の構成を表示するため、QDRANT_API_KEY と WEBUI_SECRET_KEY も画面へ出力される。単純な構文確認では –quiet を使い、秘密値を端末のログや共有画面へ残さない。

設定群 指定内容 固定する理由
構成の優先順位 ENABLE_PERSISTENT_CONFIG=False を指定する。 管理画面に保存された旧設定ではなく、Compose の環境変数を再起動後も適用する。
生成 API Ollama を有効にし、OpenAI API を無効にする。 生成要求を macOS 上の Ollama へ限定し、意図しない外部生成 API の使用を防ぐ。
検索先 VECTOR_DB=qdrant と Qdrant の接続情報を指定する。 既定の保存先ではなく、独立した Qdrant コンテナーへベクトルを保存する。
埋め込み Ollama と embeddinggemma を指定する。 文書登録と質問検索を、前章で単体試験した同じ埋め込みモデルへ統一する。
検索方式 上位 3 件のベクトル検索とし、混合検索を無効にする。 初期試験では検索経路を単純化し、どの断片が取得されたかを追跡しやすくする。
文書分割 1000 文字、重複 100 文字、文字単位の分割を使用する。 第 1 章で固定した初期条件と実際の登録処理を一致させる。
文書抽出 既定の抽出処理を使用し、PDF 画像の OCR を無効にする。 初期試験を文字層のある文書に限定し、OCR の成否を検索試験へ混入させない。

ENABLE_PERSISTENT_CONFIG=False を指定すると、対象となる設定では環境変数が管理画面の保存値より優先される。管理画面で値を変更すると実行中の状態へ反映される場合があるが、コンテナーを再起動すると Compose に記述した値へ戻る。検索件数やチャンク条件を変更する場合は、管理画面だけで変更せず、.env または compose.yaml を更新してコンテナーを再作成する。

ENABLE_PLUGINS=False は指定していない。この設定は追加機能だけでなく、組み込みのツール呼び出し経路にも影響するため、Knowledge の取得方式まで同時に変化させる可能性がある。本稿では、外部生成 API、ウェブ検索、実験的な Knowledge 操作を個別に無効化し、標準的な文書登録とベクトル検索の経路を残す。

Docker Desktop では host.docker.internal がホスト接続用の名前として提供されるため、MacBook Pro 専用の本構成では extra_hosts を追加しない。Open WebUI から Ollama へ接続できない場合は、名前を追加する前に、ホスト上の 11434 番ポートとコンテナー内での名前解決を個別に確認する。

Open WebUI の Qdrant 統合は、Open WebUI 中心開発チームの中核保守対象ではない。Open WebUI の内部的なコレクション命名や付随情報の扱いが変わると、既存の Qdrant データとの対応が崩れる可能性がある[20]。Open WebUI と Qdrant のイメージ版を同時に無計画に更新せず、更新前に両方の保存領域と Qdrant のスナップショットを退避する。

Qdrant の API 鍵は接続者を制限するが、HTTP 通信そのものを暗号化しない。本構成は Open WebUI と Qdrant を同じ Docker Desktop 内で動かし、ホスト側の 6333 番ポートを 127.0.0.1 に限定することを前提とする。別端末や外部ネットワークから接続する構成へ変更する場合は、API 鍵だけに依存せず、TLS と接続元制限を追加する必要がある[18]

4.4 Qdrant を先に起動する

Compose の depends_on は、依存関係に基づくコンテナーの起動順を制御する。ただし、短い形式の depends_on は、Qdrant のプロセスが API 要求を受け付けられる状態になるまで Open WebUI の起動を待つものではない[22]。初回構築では Qdrant だけを先に起動し、認証付き API の応答を確認してから Open WebUI を起動する。

1
2
3
4
5
docker compose pull
docker compose up -d qdrant
docker compose ps qdrant

docker compose logs --tail=100 qdrant

docker compose pull では、.env に固定した二つのイメージを取得する。Qdrant のログでは、設定の読み込み失敗、保存領域の権限エラー、既存データの読み込みエラー、6333 番ポートの待ち受け開始を確認する。コンテナーが running でも、起動直後の初期化に失敗して再起動を繰り返している場合があるため、状態とログの両方を見る。

次に、.env を一時的なサブシェルへ読み込み、API 鍵付きでコレクション一覧を取得する。サブシェルを使うことで、秘密値を現在のシェル環境へ残さない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(
  set -a
  . ./.env
  set +a

  status=$(
    curl -sS \
      -o /tmp/qdrant-collections.json \
      -w '%{http_code}' \
      -H "api-key: ${QDRANT_API_KEY}" \
      http://127.0.0.1:6333/collections
  )

  if [ "$status" != "200" ]; then
    cat /tmp/qdrant-collections.json >&2
    rm -f /tmp/qdrant-collections.json
    exit 1
  fi

  python3 -m json.tool < /tmp/qdrant-collections.json
  rm -f /tmp/qdrant-collections.json
)

初回は collections が空でよい。HTTP 200 の JSON が返れば、Qdrant のプロセス、6333 番ポートの割り当て、API 鍵認証、REST API までが成立している。まだ Open WebUI を起動していないため、コレクションが存在しないことは異常ではない。

観察結果 考えられる状態 確認先
接続を拒否される Qdrant が起動していないか、6333 番ポートを待ち受けていない。 docker compose ps と Qdrant のログを確認する。
認証エラーになる Qdrant と確認用要求で異なる API 鍵を使用している。 展開元の .env とコンテナーの再作成状態を確認する。
保存領域のエラーで再起動する バインド先の作成状態、権限、既存データに問題がある。 data/qdrant とログ内の具体的なパスを確認する。
HTTP 200 で空の一覧が返る Qdrant は正常に起動しているが、文書はまだ登録されていない。 Open WebUI の起動工程へ進む。

4.5 Open WebUI を起動する

Qdrant の API 応答を確認した後、Open WebUI を起動する。起動時には、Ollama の接続先、埋め込みモデル、Qdrant の接続情報、文書分割条件が読み込まれる。環境変数名が誤っていてもコンテナー自体は起動する場合があるため、画面が表示されることだけで設定完了とは判断しない。

1
2
3
4
docker compose up -d open-webui
docker compose ps

docker compose logs --tail=200 open-webui

Open WebUI のログでは、データベースの初期化、Ollama 接続、Qdrant 接続、起動時の例外を確認する。起動直後に Ollama への接続警告が出た場合は、macOS 上で http://localhost:11434/api/tags が応答することと、Open WebUI コンテナーから host.docker.internal を解決できることを分けて調べる。

ブラウザーで http://localhost:3000/ を開く。新規の保存領域では、最初に登録した利用者が管理者となり、その登録処理によって新規登録は停止される。ただし、本構成では環境変数を再起動時の正としているため、.env の ENABLE_SIGNUP=True を残したままコンテナーを再作成すると、新規登録が再び有効になる可能性がある。

最初の管理者としてログインできることを確認した後、.env の値を False へ変更し、Open WebUI コンテナーを再作成する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python3 - <<'PY'
from pathlib import Path

path = Path(".env")
text = path.read_text(encoding="utf-8")
old = "ENABLE_SIGNUP=True"
new = "ENABLE_SIGNUP=False"

if old not in text:
    raise SystemExit("ENABLE_SIGNUP=True was not found in .env")

path.write_text(text.replace(old, new, 1), encoding="utf-8")
PY

chmod 600 .env
docker compose up -d --force-recreate open-webui
docker compose exec open-webui printenv ENABLE_SIGNUP

最後のコマンドが False を表示すれば、再作成後も新規登録を無効にする環境変数がコンテナーへ渡されている。ログアウト後に新規登録画面が表示されないことも確認する。管理者アカウントの作成前に False へ変更すると、最初の利用者を登録できなくなるため、順序を逆にしてはならない。

Open WebUI の画面が応答することは、次のコマンドでも確認できる。

1
2
3
4
5
6
7
8
9
10
11
status=$(
  curl -sS \
    -o /dev/null \
    -w '%{http_code}' \
    http://127.0.0.1:3000/
)

[ "$status" = "200" ] || {
  printf 'Open WebUI returned HTTP %s\n' "$status" >&2
  exit 1
}

HTTP 200 が返れば、3000 番ポートの割り当てと Open WebUI の HTTP 応答は成立している。ただし、この確認では、Ollama のモデル一覧を取得できるか、Qdrant へ接続できるか、文書から埋め込みを生成できるかまでは分からない。それらは管理画面の設定値、コンテナーのログ、後続の文書登録試験で確認する。

確認段階 成功条件 残る未確認事項
HTTP 応答 3000 番ポートから HTTP 200 が返る。 利用者登録、Ollama 接続、Qdrant 接続は確認できない。
管理者登録 最初の利用者としてログインし、管理画面を開ける。 再作成後も新規登録が閉じるかは環境変数を確認する必要がある。
登録停止 ENABLE_SIGNUP=False で再作成され、新規登録画面が閉じる。 生成モデルと埋め込みモデルが利用可能かは別に確認する。
サービス連携 Open WebUI で Ollama のモデル一覧を取得できる。 文書登録と Qdrant へのベクトル保存はまだ確認できない。

ENABLE_OPENAI_API=False と ENABLE_WEB_SEARCH=False は、Open WebUI から外部生成 API とウェブ検索を利用しないための設定である。ただし、この二つだけでコンテナーからの全外向き通信が存在しないことを証明するものではない。厳密な閉域運用が必要な場合は、Docker の通信、名前解決、更新確認をネットワーク観測またはファイアウォールで検証する。

この章の完了条件は、二つのコンテナーが running と表示されることではない。版と秘密値を固定した構成を検証でき、Qdrant が API 鍵付きの要求へ HTTP 200 を返し、Open WebUI の管理者を作成し、再作成後も新規登録を閉じ、Open WebUI と Qdrant の状態をそれぞれの永続領域へ保存できるところまでである。ここまでを独立して確認することで、後続の文書登録で問題が起きた場合に、コンテナー構築と認証を切り分け済みの前提として扱える。


5. Ollama、Open WebUI、Qdrant の接続を一方向ずつ確認する

5.1 Open WebUI コンテナーから Ollama API への接続を確認する

最初に、macOS 上の Ollama が取得済みモデルを返すことを再確認する。次に、同じ API を Open WebUI コンテナー内から呼び出す。ホスト上の確認とコンテナー内の確認を分けることで、Ollama 自体の障害と、Docker Desktop をまたぐ通信の障害を区別できる。

1
2
3
curl -fsS \
  http://127.0.0.1:11434/api/tags |
  python3 -m json.tool

この要求で qwen3.5:9b-q4_K_M と embeddinggemma が表示されれば、Ollama API とモデル保存状態は正常である。ただし、これは macOS 内の localhost から接続できることを示すだけであり、Open WebUI コンテナーから到達できることまでは証明しない。

Open WebUI コンテナーからは、host.docker.internal を使用して同じ API を呼び出す。

1
2
3
4
5
6
7
8
9
10
11
12
docker compose exec -T open-webui python3 - <<'PY'
import json
import urllib.request

url = "http://host.docker.internal:11434/api/tags"

with urllib.request.urlopen(url, timeout=10) as response:
    data = json.load(response)
    print(f"status={response.status}")
    for model in data.get("models", []):
        print(model["name"])
PY

status=200 と二つのモデル名が表示されれば、Open WebUI コンテナーから macOS 上の Ollama までの通信経路は成立している。この確認は Open WebUI の画面や内部設定を介さず、Docker Desktop の名前解決、ホストへの転送、Ollama API の応答だけを検証する。

macOS からの要求 コンテナーからの要求 判定
成功する 成功する。 Ollama API と Docker からホストへの通信が成立している。
成功する 失敗する。 host.docker.internal の名前解決、接続先、Docker Desktop の通信を調べる。
失敗する 失敗する。 Open WebUI ではなく、Ollama の起動状態と 11434 番ポートを調べる。
成功するがモデルがない 成功するがモデルがない。 API は正常であるため、ollama pull とモデル名を確認する。

通信試験に成功した後、Open WebUI のモデル選択欄に qwen3.5:9b-q4_K_M が表示されることを確認する。コンテナーから API を直接呼び出せるのに画面へモデルが表示されない場合は、ネットワークではなく、Open WebUI が実際に読み込んだ接続設定を調べる。

1
2
3
4
docker compose exec open-webui printenv OLLAMA_BASE_URL
docker compose exec open-webui printenv ENABLE_PERSISTENT_CONFIG
docker compose logs --tail=200 open-webui |
  grep -i -E 'ollama|connection|11434|error'

OLLAMA_BASE_URL が http://host.docker.internal:11434、ENABLE_PERSISTENT_CONFIG が False であることを確認する。本構成では永続データベース内の旧設定ではなく、Compose から渡した環境変数を設定の正とするため、管理画面に保存された接続先との優先関係を個別に調整する必要はない[20]

.env または compose.yaml を変更しただけでは、既に動作しているコンテナーの環境変数は変わらない。設定値が異なる場合は、Open WebUI コンテナーを再作成する。

1
2
3
4
5
docker compose config --quiet
docker compose up -d --force-recreate open-webui

docker compose exec open-webui printenv OLLAMA_BASE_URL
docker compose exec open-webui printenv ENABLE_PERSISTENT_CONFIG

環境変数、コンテナーからの直接接続、Ollama API のモデル一覧がすべて正常であるにもかかわらず、画面にモデルが表示されない場合は、Open WebUI の管理画面にある Ollama 接続設定とログを確認する。設定を無計画に追加するのではなく、Compose で指定した接続先と同じ値になっているかを比較する。

5.2 Open WebUI から Ollama の生成 API への経路を確認する

モデル一覧の取得は、Open WebUI が Ollama の読み取り API へ接続できることを示す。文章生成まで成立するかを確認するには、Open WebUI から実際に生成要求を送る必要がある。新しい会話を作成し、Knowledge、添付ファイル、ウェブ検索、追加ツールを接続しない状態で qwen3.5:9b-q4_K_M を選択する。

生成用 LLM と埋め込みモデルの役割の違いを三文で説明してください。

日本語の回答が返れば、ブラウザー、Open WebUI、Docker からホストへの通信、Ollama の生成 API、生成用モデルという経路が一続きに成立している。ただし、この試験では文書を登録しておらず、Knowledge も接続していないため、埋め込みモデルと Qdrant は使用されない。

観察結果 成立した経路 まだ確認していない経路
モデル一覧に生成用モデルが表示される Open WebUI から Ollama のモデル一覧を取得できる。 生成要求、埋め込み生成、Qdrant への保存は確認できない。
通常対話で回答が返る Open WebUI から Ollama の生成 API を呼び出せる。 Knowledge の検索と取得文書の挿入は確認できない。
回答内容が自然である 生成用モデルが日本語の回答を出力できる。 登録文書を根拠に回答したことは確認できない。

この通常対話は、後続の RAG 試験に対する対照条件となる。通常対話で回答できる一般知識だけを RAG の試験質問に使うと、Knowledge を接続した効果を判定できない。そのため、RAG の本試験では、通常対話では知り得ない架空の固有情報を使用する。

通常対話が失敗した場合は、Qdrant や文書分割の設定を変更しない。Knowledge を接続していない対話には、それらの設定が関与しないためである。Open WebUI のログと Ollama の状態を確認し、生成経路を復旧してから文書登録へ進む。

1
2
docker compose logs --tail=200 open-webui
ollama ps

Open WebUI のログに Ollama API の接続エラーがなく、ollama ps に qwen3.5:9b-q4_K_M が表示されれば、要求は生成用モデルまで到達している。モデルが表示されない場合は、画面上で選択したモデル名、Ollama に保存されたタグ、Open WebUI が送信した要求を照合する。

5.3 Open WebUI コンテナーから Qdrant への接続を確認する

Qdrant は http://127.0.0.1:6333/dashboard で管理画面を提供する。管理画面では、コレクションと点の確認、REST API の実行、スナップショットの操作ができる[23]。API 鍵を求められた場合は、.env に保存した QDRANT_API_KEY を入力する。

管理画面を開けることは、MacBook Pro のブラウザーから Qdrant へ接続できることを示す。ただし、Open WebUI コンテナーはホスト側の 127.0.0.1:6333 ではなく、Docker Compose ネットワーク内の qdrant:6333 へ接続する。ブラウザーからの接続とコンテナー間通信は別々に確認する必要がある。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker compose exec -T open-webui python3 - <<'PY'
import json
import os
import urllib.request

request = urllib.request.Request(
    "http://qdrant:6333/collections",
    headers={"api-key": os.environ["QDRANT_API_KEY"]},
)

with urllib.request.urlopen(request, timeout=10) as response:
    data = json.load(response)
    print(f"status={response.status}")
    print(json.dumps(data, ensure_ascii=False, indent=2))
PY

status=200 とコレクション一覧が返れば、Open WebUI コンテナーから Qdrant までの名前解決、HTTP 接続、API 鍵認証が成立している。文書登録前には、一覧が空であっても異常ではない。接続経路の確認では、コレクション数ではなく、認証付き要求が正常終了することを判定条件とする。

観察結果 考えられる状態 確認対象
名前解決に失敗する Open WebUI と Qdrant が同じ Compose ネットワークへ接続されていない。 サービス名、Compose プロジェクト、コンテナーのネットワークを確認する。
接続を拒否される Qdrant が停止しているか、6333 番ポートを待ち受けていない。 Qdrant の状態とログを確認する。
認証エラーになる Open WebUI と Qdrant に異なる API 鍵が渡されている。 両コンテナーの再作成状態と環境変数を確認する。
HTTP 200 で空の一覧が返る 接続と認証は正常だが、文書はまだ登録されていない。 文書登録前の基準値として記録する。
HTTP 200 でコレクションが返る 過去の文書登録データが保存領域に残っている。 新規試験と既存データを混同しないよう、名称と点数を記録する。

文書登録前の状態を API から保存する。後続の登録後に同じ要求を実行し、コレクションの増加と点数の変化を比較する。

1
2
3
4
5
6
7
8
9
10
11
(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -H "api-key: ${QDRANT_API_KEY}" \
    http://127.0.0.1:6333/collections
) |
  tee /tmp/qdrant-collections-before.json |
  python3 -m json.tool

この基準値を残すことで、文書登録後に作成されたコレクションを既存データと区別できる。Open WebUI の版や設定によってコレクション名が変わる可能性があるため、特定の名称が作成されることを前提にせず、登録前後の差分で判定する。

5.4 ローカル処理経路として設定されていることを確認する

Open WebUI は、Ollama 以外の生成 API、外部の埋め込みサービス、ウェブ検索、複数のベクトルデータベースにも接続できる。本構成がローカル RAG になるのは、Open WebUI や Ollama という製品を使用しているためではない。生成、埋め込み、検索、文書抽出の各処理先を、MacBook Pro と Docker Desktop の内部へ明示的に配置しているためである。

実行中の Open WebUI コンテナーへ、Compose で指定した接続設定が渡されていることを確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
docker compose exec -T open-webui python3 - <<'PY'
import os

names = (
    "ENABLE_PERSISTENT_CONFIG",
    "ENABLE_OLLAMA_API",
    "OLLAMA_BASE_URL",
    "ENABLE_OPENAI_API",
    "ENABLE_WEB_SEARCH",
    "VECTOR_DB",
    "QDRANT_URI",
    "QDRANT_PREFER_GRPC",
    "RAG_EMBEDDING_ENGINE",
    "RAG_EMBEDDING_MODEL",
    "CONTENT_EXTRACTION_ENGINE",
)

for name in names:
    print(f"{name}={os.environ.get(name, '')}")
PY
処理 設定した接続先 実行経路の確認方法 成功しても証明できないこと
生成 host.docker.internal:11434 の Ollama へ接続する。 コンテナー内からモデル一覧を取得し、Open WebUI で通常対話を行う。 Knowledge の検索結果が回答へ使われたことは証明できない。
埋め込み 同じ Ollama 上の embeddinggemma を使用する。 文書登録時の Ollama と Open WebUI のログ、Qdrant の点数を確認する。 取得された断片が質問に適切であることは点数だけでは分からない。
ベクトル保存 Docker ネットワーク内の qdrant:6333 へ接続する。 Open WebUI コンテナーから認証付き API を呼び出す。 保存された各点に適切な文書断片が含まれるかは別に確認する。
文書抽出 Open WebUI コンテナー内の標準抽出機構を使用する。 文字層を持つ Markdown、HTML、PDF を登録し、固有文字列を検索する。 画像だけの PDF や複雑な表を正確に抽出できることは証明できない。

ENABLE_OPENAI_API=False と ENABLE_WEB_SEARCH=False は、Open WebUI から外部生成 API とウェブ検索を利用しないための設定である。RAG_EMBEDDING_ENGINE=ollama と VECTOR_DB=qdrant により、埋め込み生成とベクトル保存もローカルの接続先へ固定する。

ただし、これらの設定だけで、Open WebUI コンテナーと Ollama の全外向き通信が遮断されたことにはならない。Docker Desktop の通常のネットワークには外部への通信経路があり、モデル取得、イメージ取得、更新確認など、RAG の回答処理とは異なる通信が発生する可能性がある。ここで確認できるのは、質問から回答までの生成、埋め込み、検索、文書抽出の接続先がローカルへ設定されていることである。

運用条件 成立に必要な確認
回答処理をローカルで完結させる 生成、埋め込み、検索、文書抽出の接続先がすべてローカルであることを確認する。
外部生成サービスへ文書を送信しない 外部生成 API と外部埋め込み API を無効にし、使用モデルを Ollama へ限定する。
端末からの外向き通信を全面的に禁止する アプリケーション設定だけに依存せず、ファイアウォールまたはネットワーク制御で遮断する。
ネットワーク切断後も利用する 必要なモデル、イメージ、文書抽出用資源が事前にローカルへ取得されていることを確認する。

この区別を設けないと、「ローカルモデルを選択した」「ウェブ検索を無効にした」という画面上の設定だけで、外部通信が一切ないと誤認する。ローカル RAG の構成確認では、回答を作るデータ経路と、端末全体のネットワーク隔離を別の要件として扱う必要がある。

この章の完了条件は、Open WebUI の画面に生成用モデルが表示されることだけではない。macOS から Ollama API を呼び出せること、Open WebUI コンテナーから同じ API へ到達できること、Knowledge を接続しない通常対話が成功すること、Open WebUI コンテナーから認証付きで Qdrant へ接続できること、文書登録前のコレクション状態を記録できることまでを個別に確認する。これにより、次章の文書登録で障害が起きた場合は、生成経路とデータベース接続を切り分け済みの前提として、文字抽出、チャンク分割、埋め込み生成、ベクトル保存を調べられる。


6. 文書を検索可能なチャンクへ変換する

6.1 文脈を外部化して検索対象にする

利用者固有の判断基準、運用手順、例外条件は、生成用 LLM の内部知識には含まれていない。会話のたびに説明しなければ参照できない情報を継続的に利用するには、暗黙知を文書として外部化し、検索可能な情報構造へ変換する必要がある[24]

外部化とは、単に文章として保存することではない。対象、条件、値、例外、判断主体の対応関係を、文書を分割した後にも読み取れる形で記述する。たとえば「47821」という数値だけを記録しても、それが本番ポートなのか、障害番号なのか、時刻なのかを検索結果から確定できない。「Argolet の本番保守ポートは 47821 である」と記述することで、名称、環境、用途、値が一つの意味単位になる。

Open WebUI の Knowledge は、複数の会話から再利用する文書集合を管理する単位である。Focused Retrieval を使用すると、登録文書を質問のたびに全文投入するのではなく、質問と意味的に近いチャンクを検索して生成用 LLM へ渡す[25]。本構成では RAG_FULL_CONTEXT=False としているため、Knowledge 全文ではなく、Qdrant から取得した上位 3 件のチャンクが回答生成の候補になる。

情報の状態 生成用 LLM からの利用方法 制約
モデルの学習時に内部化された一般知識 外部文書を接続せずに回答へ使用する。 学習後に追加された情報、組織固有の規則、架空情報は原則として参照できない。
現在の会話へ直接記載した情報 会話のコンテキストから参照する。 別の会話では再入力が必要になり、会話履歴が長くなるほどコンテキストを消費する。
Knowledge へ登録した文書 質問に関連するチャンクを検索して回答へ使用する。 文字抽出、分割、埋め込み、検索のいずれかに失敗すると参照できない。
文書化されていない暗黙知 直接は参照できない。 生成用 LLM が一般知識から補完すると、実際の運用規則と異なる回答になる。

Knowledge へ登録する情報は、回答文そのものではなく、回答を決める根拠として記述する。運用担当者が「本番ポートは何か」と質問する場合、文書にはポート番号だけでなく、本番環境と開発環境の区別を残す。緊急停止方法を尋ねる場合は、手順名だけでなく、操作順序と適用条件を残す。検索された一つのチャンクだけを読んでも判断できる記述にすることで、生成用 LLM が欠落部分を推測する余地を減らせる。

6.2 対照試験用の Markdown を作る

RAG の成立を検証するには、生成用 LLM が事前知識から正答できない文書を用意する。実在する製品、一般的なポート番号、既知の運用用語を使うと、Knowledge を参照しなくても回答できる可能性が残る。そこで、架空のシステム名、一般的な規則から導けない数値、造語による手順名を組み合わせる。

検証文書には、正答対象だけでなく、取り違えや推測を検出するための対照情報も入れる。本番ポートと開発ポートを近い値にし、質問対象の環境を識別しなければ正答できない形にする。文書に記載していない開発責任者名も明示し、情報不足の質問に対して架空の人物名を生成しないかを確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
cat > documents/argolet.md <<'EOF'
# Argolet 運用ガイド

## 本番接続

Argolet の本番保守ポートは 47821 である。
開発環境の保守ポートは 47822 である。

本番環境へ接続する場合は、接続先が production であることを確認してから 47821 番ポートを使用する。
開発環境の 47822 番ポートを本番保守に使用してはならない。

## 緊急停止

緊急停止時には Lapis-7 手順を使用する。
Lapis-7 手順では、最初に接続キューを停止し、その後に書き込み処理を停止する。

書き込み処理を先に停止すると、接続キューに残った要求の状態を確定できなくなるため、操作順序を逆にしてはならない。

## 復旧条件

復旧を開始できるのは、監視指標 Rho-Blue が 15 分間連続して 0.20 未満になった場合だけである。

一時的に 0.20 未満になっても、15 分間の連続条件を満たしていない場合は復旧を開始しない。

## 管理情報

本書には Argolet の開発責任者名を記載していない。
開発責任者名を尋ねられた場合は、本書からは判断できないと回答する。
EOF

cat documents/argolet.md

短い文書であっても、名称、値、条件、例外を別々の見出しへ配置する。Markdown の見出しは文書構造を示すだけでなく、本構成ではチャンク分割の前処理にも使用される。見出し単位で話題を分離することで、本番接続の質問に対して緊急停止や復旧条件の記述が同じ検索単位へ混入しにくくなる。

作成後は、登録する原文を記録する。後から文書を書き換えて再登録した場合、旧ベクトルと新しい原文を混同すると、検索結果の差が設定変更によるものか文書変更によるものかを判定できない。

1
2
3
4
5
file documents/argolet.md
wc -c documents/argolet.md
shasum -a 256 documents/argolet.md
grep -n -E '47821|47822|Lapis-7|Rho-Blue|開発責任者' \
  documents/argolet.md
検証情報 文書に入れた理由 検出する失敗
本番ポート 47821 Knowledge 接続時に取得すべき正答とする。 文書が検索されない、数値が欠落する、別の値を生成する失敗を検出する。
開発ポート 47822 本番ポートに近い対照値として置く。 環境条件を無視し、類似する数値を取り違える失敗を検出する。
Lapis-7 手順 事前知識から推測できない固有の手順名とする。 意味検索が必要な文書断片を取得できない失敗を検出する。
接続キュー、書き込み処理の順序 名称だけでなく、複数段階の手順を保持できるか確認する。 取得チャンクが短すぎて操作順序が分断される失敗を検出する。
Rho-Blue の 15 分連続条件 指標名、閾値、継続時間を一体として検索させる。 閾値だけを取得し、時間条件を欠落させる失敗を検出する。
記載のない開発責任者名 情報不足時に回答を保留できるか確認する。 文書にない固有名詞を生成する失敗を検出する。

この文書では、単純な正答率だけでなく、条件識別、手順保持、情報不足時の抑制を試験できる。RAG が機能していても、取得チャンクに必要な条件が含まれなければ、生成用 LLM は部分的に正しい回答を返す。複数の失敗形式を一つの短い文書へ埋め込むことで、検索と生成のどちらに調整が必要かを判別しやすくする。

6.3 Open WebUI に Knowledge を作る

Open WebUI で Knowledge を作成し、検証文書を登録する。

  1. Open WebUI へ管理者または Knowledge の編集権限を持つ利用者としてログインする。
  2. Workspace を開く。
  3. Knowledge を開き、新しい Knowledge を作成する。
  4. 名称を Argolet Operations とする。
  5. 検索方式として Focused Retrieval を使用する。
  6. documents/argolet.md をアップロードする。
  7. 処理中の表示が終了し、ファイルが利用可能な状態になることを確認する。

ファイルを会話へ直接添付する方法でも、その会話内で文書を参照できる。Knowledge は、同じ文書集合を複数の会話やモデル設定から再利用する場合に使う。運用手順、仕様書、規程のように継続して参照する文書は Knowledge とし、一回限りの確認資料とは管理単位を分ける。

アップロード後、Open WebUI は文書の内容を抽出し、Markdown の見出し構造を前処理し、各部分を設定した大きさへ分割する。続いて各チャンクを embeddinggemma でベクトル化し、原文断片や出典情報とともに Qdrant へ保存する。画面上でファイル名が表示された段階と、検索用データの保存が完了した段階は同じとは限らない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
documents/argolet.md
  ↓
Open WebUI へアップロード
  ↓
Markdown から文字を抽出
  ↓
見出し単位で前処理
  ↓
文字数に基づいてチャンク分割
  ↓
各チャンクを embeddinggemma へ送信
  ↓
埋め込みベクトルを生成
  ↓
ベクトル、原文断片、付随情報を Qdrant へ保存
  ↓
Argolet Operations から検索可能になる

処理中に失敗した場合は、同じファイルを繰り返しアップロードする前にログを確認する。再試行のたびに中途半端な登録情報や別の点が作成されると、後から点数とチャンク数の関係を追跡しにくくなる。

1
2
3
4
docker compose logs --since=10m open-webui |
  grep -i -E 'document|chunk|embed|qdrant|error|exception'

ollama ps

登録処理中または登録直後に ollama ps へ embeddinggemma が表示されれば、Open WebUI から埋め込み要求が送られたことを確認できる。ただし、モデルが読み込まれたことだけでは、すべてのチャンクが Qdrant へ保存されたとは判断できない。次章では登録前後のコレクションと点数を比較し、保存段階まで確認する。

観察した事実 成立した可能性が高い段階 まだ確認できない段階
ファイル名が Knowledge に表示される アップロード要求と管理情報の作成 文字抽出、分割、埋め込み、Qdrant への保存
処理中の表示が終了する Open WebUI が登録処理を完了扱いにした状態 抽出された本文と各チャンクの妥当性
embeddinggemma が読み込まれる 埋め込み API の呼び出し 全チャンクの変換と保存
Qdrant の点数が増える 一つ以上のベクトル保存 必要な文書断片が検索上位へ入ること
質問に正答する 検索結果が生成へ反映された可能性 偶然や会話履歴による正答ではないこと

6.4 チャンク分割の意味

Open WebUI は文書をチャンクへ分割し、各チャンクを検索と埋め込みの単位にする。現在の構成では、ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER=True により、Markdown の見出しが最初の分割境界として使われる。その後、各見出し内の文章が RAG_TEXT_SPLITTER=character、CHUNK_SIZE=1000、CHUNK_OVERLAP=100 の条件で追加分割される[26]

見出し前処理と文字数分割は、どちらか一方だけが使われるのではない。最初に「本番接続」「緊急停止」「復旧条件」という文書構造を分離し、各部分が 1000 文字を超える場合に文字分割を適用する。この順序により、異なる見出しの話題を一つの大きなチャンクへまとめにくくしながら、長い節も検索可能な大きさへ制限できる。

今回の argolet.md は短いため、各見出しの本文が 1000 文字未満に収まる可能性が高い。その場合、チャンク数は単純に文書全体の文字数を 1000 で割った値にはならない。見出し前処理、付随情報、分割器の境界判断によって、見出しごとに複数のチャンクが生成される場合がある。

設定 小さくした場合 大きくした場合 今回の判定対象
チャンクサイズ 質問対象を細かく絞りやすいが、名称、値、条件、例外が別のチャンクへ分かれやすい。 前後関係を残しやすいが、無関係な規則が混入し、検索ベクトルの焦点がぼやける。 復旧条件の指標、閾値、15 分間という条件が同じチャンクに残るかを確認する。
重複量 保存量と重複検索は減るが、境界付近の主語や条件が失われやすい。 境界をまたぐ文脈は残るが、ほぼ同じチャンクが検索上位を占めやすい。 長い節へ拡張した場合に、手順の前後関係が境界で切れないかを確認する。
取得件数 生成用 LLM へ渡す根拠を限定できるが、複数箇所を統合する質問で不足しやすい。 根拠候補は増えるが、別環境の値や無関係な条件を混入させやすい。 本番ポートの質問で、開発ポートを含むチャンクが回答を誤らせないかを確認する。
見出し分割 無効にすると、隣接する異なる節が文字数だけで同じチャンクへ入る可能性がある。 有効にすると、文書構造を検索単位へ反映しやすい。 本番接続、緊急停止、復旧条件が別の意味単位として扱われるかを確認する。

チャンクサイズを小さくすれば検索精度が自動的に上がるわけではない。たとえば「復旧を開始できるのは」という文と、「15 分間連続して 0.20 未満」という条件が別々のチャンクへ分かれると、検索結果には閾値だけが残り、継続時間を欠いた回答が生成される。検索対象を細かくするほど、各断片が単独で判断根拠として成立しているかを確認しなければならない。

大きいチャンクにも別の失敗がある。本番接続、開発接続、緊急停止、復旧条件を一つのチャンクへまとめると、「本番ポート」という質問に対して 47821 と 47822 の両方が生成入力へ入る。埋め込み検索には成功していても、生成用 LLM が二つの値を取り違える可能性が高くなる。

重複量は、境界で失われる文脈を補うために使う。重複した 100 文字が、前チャンクの条件と次チャンクの操作を接続する場合には有効である。一方、短い文書で重複量を増やすと、ほぼ同じ内容のチャンクが複数作られ、取得件数 3 件のすべてを類似断片が占める場合がある。重複量は保存容量だけでなく、検索結果の多様性にも影響する。

チャンク条件を変更した場合、Qdrant に保存済みの点へ自動的に新しい境界が適用されるわけではない。登録済み文書を削除または再処理し、新しい設定で文字抽出、分割、埋め込み、保存をやり直す必要がある。設定だけを変更して同じ索引を検索すると、画面上の設定値と実際のチャンク構造が一致しない。

6.5 長いコンテキストと検索精度を区別する

生成用 LLM が長い入力を受け付けられることと、その入力内の必要情報を安定して利用できることは同義ではない。複数文書を含む長いコンテキストでは、関連情報が入力の中央付近に配置された場合、先頭または末尾にある場合より性能が低下する傾向が報告されている[27]

この性質があるため、モデルの最大コンテキスト長を広げ、登録文書をすべて入力すれば RAG が不要になるとは限らない。全文投入では、質問と無関係な記述も入力処理の対象になり、複数の類似値や例外規則が生成用 LLM の判断を競合させる。入力可能であることは、必要な根拠を選択できることを保証しない。

RAG は、コンテキスト長の上限を回避するだけの仕組みではない。質問ごとに候補文書を絞り、回答へ使用する根拠を観察可能な単位に限定する仕組みでもある。Argolet の本番ポートを尋ねた場合は本番接続のチャンクを取得し、復旧条件を尋ねた場合は Rho-Blue の条件を含むチャンクを取得する。検索結果を限定することで、生成用 LLM が判断すべき範囲を狭められる。

方式 生成用 LLM へ渡す情報 利点 失敗条件
全文投入 登録文書の全文 検索による取りこぼしがなく、短い文書では構造をそのまま参照できる。 文書が長くなると無関係な情報が増え、入力処理時間と記憶使用量も増える。
チャンク検索 質問に近い上位チャンク 根拠を限定し、長い文書集合から必要部分だけを入力できる。 分割境界、埋め込み、取得件数が不適切だと必要な条件を取りこぼす。
大きいチャンク 広い前後関係を含む断片 条件や例外を同じ断片へ残しやすい。 異なる対象や複数の値が混在し、回答対象を特定しにくくなる。
小さいチャンク 狭い話題に限定した断片 質問との類似性を明確にしやすい。 主語、値、適用条件、例外が分離すると、単独では根拠にならない。

初期値の 1000 文字、重複 100 文字、取得件数 3 件は、最適値ではなく比較の出発点である。質問ごとに、正しいチャンクが取得されたか、必要条件が一つのチャンクに残っているか、無関係なチャンクが生成へ混入したかを記録し、その結果に基づいて一項目ずつ変更する。

6.6 文字抽出の失敗を先に疑う

文書検索で期待する回答が得られない場合、生成用 LLM やベクトル検索の性能を調整する前に、原文から必要な文字が抽出されたかを確認する。抽出されなかった文字列はチャンクに含まれず、埋め込みベクトルにも反映されない。検索設定を変更しても、存在しない文字情報を取得することはできない。

Markdown のようなプレーンテキストでは、原文と抽出結果の差が小さいため、RAG の最小経路を確認しやすい。PDF では、画面上に文字が見えていても、内部に文字層がなく、各ページが画像として保存されている場合がある。複雑な段組み、脚注、表、縦書き、図中の文字も、読み順の崩れや欠落を起こしやすい。

Open WebUI の RAG 障害確認でも、文書取り込み、Knowledge の接続、埋め込み、検索条件、モデルへ渡されたコンテキストを分けて調べる必要がある[28]。回答が誤っているという最終結果だけでは、抽出、分割、検索、生成のどこで情報が失われたかを判定できない。

文書形式 主な失敗 最初の確認方法
Markdown 文字コード、見出し構造、極端に長い段落による分割の偏り 原文を cat と grep で確認し、固有文字列が存在することを確定する。
HTML 本文以外のナビゲーション混入、動的生成部分の欠落、表構造の平文化 抽出対象となる静的 HTML 内に本文が存在するかを確認する。
文字層のある PDF 段組みの読み順、改行、ヘッダー、フッター、表の列関係の崩れ PDF から文字を選択できるか、抽出後の順序が原文と一致するかを確認する。
画像だけの PDF 文字情報が抽出されず、空または不完全な文書になる。 OCR を使用しない構成では検索対象外として扱う。
図表中心の文書 位置関係、矢印、凡例、行列構造が文章へ変換されない。 重要な関係を別途文章化し、検索可能な説明として登録する。

本構成では PDF_EXTRACT_IMAGES=False とし、画像 OCR を処理経路へ含めない。OCR を有効にすると、画像解析、言語判定、文字認識、読み順復元という別の処理が加わり、検索失敗の原因が増える。最初に Markdown で文字抽出から回答生成までを成立させ、その後に文字層のある HTML や PDF へ対象を広げる。

画像だけの PDF や図表中心の資料を扱う場合は、単に OCR を追加するだけでは不十分である。数値を認識できても、表のどの列に属するか、図中の矢印が何を結ぶか、注記がどの値を制約するかを保持できなければ、検索可能な根拠にはならない。位置関係に依存する内容は、判断に必要な関係を文章として明示した補助文書を用意する方が再現性を確保しやすい。

6.7 文書登録工程の完了条件を固定する

文書登録の完了は、Knowledge にファイル名が表示されたことでは判定しない。原文が再現可能な状態で保存され、固有情報が文字として存在し、Open WebUI が処理を完了し、埋め込みモデルが呼び出され、Qdrant の保存内容が登録前から変化したところまでを確認する。

確認段階 確認する事実 失敗時に戻る工程
原文作成 検証対象の名称、数値、条件、未記載事項が Markdown に存在する。 文書設計を修正する。
原文固定 ファイル容量とハッシュ値を記録する。 登録した版と現在のファイルを照合する。
Knowledge 登録 対象ファイルが利用可能な状態になる。 Open WebUI のファイル処理ログを確認する。
埋め込み生成 登録時に embeddinggemma が使用される。 Ollama 接続と埋め込みモデル設定を確認する。
ベクトル保存 Qdrant のコレクションまたは点数が登録前から変化する。 Qdrant 接続、API 鍵、保存処理を確認する。
検索試験 質問に対応するチャンクが取得される。 文字抽出、チャンク境界、埋め込み、取得件数を確認する。

この章で確立するのは、文書をアップロードする操作ではない。利用者固有の規則を検索可能な意味単位として記述し、見出しと文字数に基づいて分割し、各チャンクを embeddinggemma のベクトルへ変換できる入力を作ることである。RAG の回答品質は生成用 LLM だけでは決まらず、質問に必要な条件を一つの根拠として取り出せるよう、登録前の文書を設計した段階で大きく制約される。


7. 埋め込みと Qdrant の保存状態を確認する

7.1 文書登録前後のコレクションを比較する

Qdrant のコレクションは、検索対象となる点をまとめる名前付きの集合である。各点はベクトルと付随情報から構成され、同じコレクションの同じベクトル項目では、次元数と距離尺度をそろえる必要がある[29]。文書登録後には、Open WebUI が作成または再利用したコレクションへ、文書チャンクに対応する点が保存される。

第 5 章では、文書登録前のコレクション一覧を /tmp/qdrant-collections-before.json へ保存した。Knowledge への登録後に同じ API を呼び出し、登録後の一覧を別ファイルへ保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -H "api-key: ${QDRANT_API_KEY}" \
    http://127.0.0.1:6333/collections \
    > /tmp/qdrant-collections-after.json
)

python3 -m json.tool \
  < /tmp/qdrant-collections-after.json

登録前後の一覧から、追加されたコレクション名と削除されたコレクション名を比較する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
python3 - <<'PY'
import json
from pathlib import Path

before_path = Path("/tmp/qdrant-collections-before.json")
after_path = Path("/tmp/qdrant-collections-after.json")

if not before_path.exists():
    raise SystemExit(
        "The pre-registration collection snapshot was not found: "
        "/tmp/qdrant-collections-before.json"
    )

def load_names(path):
    data = json.loads(path.read_text(encoding="utf-8"))
    collections = data.get("result", {}).get("collections", [])
    return {item["name"] for item in collections}

before = load_names(before_path)
after = load_names(after_path)

print("before:")
for name in sorted(before):
    print(f"  {name}")

print("after:")
for name in sorted(after):
    print(f"  {name}")

print("added:")
for name in sorted(after - before):
    print(f"  {name}")

print("removed:")
for name in sorted(before - after):
    print(f"  {name}")
PY

新しいコレクションが表示された場合、文書登録によって Qdrant の構成が変化したことを確認できる。ただし、新しい名前が増えなかったことだけで登録失敗とは判断しない。本構成では ENABLE_QDRANT_MULTITENANCY_MODE=True を指定しており、Open WebUI は同種のベクトルデータを共有コレクションへ集約できる[20]。既存コレクションへ点が追加された場合、コレクション名の一覧は登録前後で変化しない。

Open WebUI が使用するコレクションには既定の接頭辞があるが、名称規則は Open WebUI の版、データ種別、複数利用者モード、設定値によって変化し得る。特定のコレクション名を記事の完成条件として固定せず、実際の一覧、点数、付随情報を組み合わせて対象を特定する。

登録前後の変化 考えられる状態 次に確認する項目
新しいコレクションが増えた 文書登録用の保存先が新しく作成された可能性が高い。 新しいコレクションの点数、ベクトル設定、付随情報を確認する。
コレクション名は変わらない 複数利用者モードの共有コレクションが再利用された可能性がある。 既存コレクションの点数と付随情報を確認する。
コレクションが削除され、別の名前が増えた 再索引、設定変更、保存方式の移行が実行された可能性がある。 Open WebUI のログと再索引操作の有無を確認する。
一覧が空のままである 埋め込み生成または Qdrant への保存に到達していない可能性がある。 Open WebUI と Qdrant のログ、埋め込みモデルの起動状態を確認する。

コレクション一覧は、保存処理がどこかまで進んだことを確認する入口にすぎない。共有コレクションへの追加、既存点の更新、登録失敗を一覧だけで区別することはできないため、各コレクションの点数と設定を続けて確認する。

7.2 コレクションの点数とベクトル設定を確認する

Qdrant の点は、識別子、ベクトル、任意の JSON 形式の付随情報から構成される[30]。Open WebUI は、文書チャンクから生成した埋め込みベクトルに、元文書や Knowledge、利用者、チャンク本文を識別する情報を付けて保存する。

次の処理では、すべてのコレクションについて、点数、索引済みベクトル数、区画数、ベクトル設定を一覧表示する。API 鍵は出力せず、各コレクションの状態だけを表示する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
(
  set -a
  . ./.env
  set +a

  python3 - <<'PY'
import json
import os
import urllib.parse
import urllib.request

base_url = "http://127.0.0.1:6333"
headers = {"api-key": os.environ["QDRANT_API_KEY"]}

def get_json(path):
    request = urllib.request.Request(
        base_url + path,
        headers=headers,
    )
    with urllib.request.urlopen(request, timeout=10) as response:
        return json.load(response)

collection_list = get_json("/collections")
collections = collection_list.get("result", {}).get("collections", [])

for item in sorted(collections, key=lambda value: value["name"]):
    name = item["name"]
    encoded_name = urllib.parse.quote(name, safe="")
    result = get_json(f"/collections/{encoded_name}").get("result", {})

    summary = {
        "name": name,
        "status": result.get("status"),
        "optimizer_status": result.get("optimizer_status"),
        "points_count": result.get("points_count"),
        "indexed_vectors_count": result.get("indexed_vectors_count"),
        "segments_count": result.get("segments_count"),
        "vectors": (
            result.get("config", {})
            .get("params", {})
            .get("vectors")
        ),
    }

    print(json.dumps(summary, ensure_ascii=False, indent=2))
PY
)

points_count が 0 より大きければ、そのコレクションに一つ以上の点が保存されている。短い Markdown 文書であっても、見出し分割、チャンク分割、付随情報の管理方法によって複数の点が作成される場合がある。原文の段落数と点数が一致することを前提にしてはならない。

indexed_vectors_count は、検索用索引へ組み込まれたベクトル数を示す。小規模なコレクションでは、点が存在していても索引化の閾値に達せず、全走査によって検索される場合がある[31]。そのため、indexed_vectors_count が points_count より少ないことや、検証用の小さなコレクションで 0 になることだけを保存失敗とは判定しない。

項目 示す状態 判定時の注意
status コレクション全体の動作状態を示す。 点の内容や検索結果の妥当性までは確認できない。
optimizer_status 保存領域や索引の最適化処理の状態を示す。 処理中であっても点の書き込み自体は完了している場合がある。
points_count コレクションに保存された点数を示す。 一点が一段落または一ファイルに対応するとは限らない。
indexed_vectors_count 検索用索引へ組み込まれたベクトル数を示す。 小規模なコレクションでは点数より少なくても直ちに異常ではない。
segments_count コレクション内部の保存区画数を示す。 文書数やチャンク数と直接対応する値ではない。
vectors ベクトルの次元数と距離尺度を示す。 埋め込みモデルの実際の出力次元と一致する必要がある。

対象コレクションを特定したら、名前を COLLECTION へ設定し、詳細を個別に保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
COLLECTION='実際のコレクション名'

(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -H "api-key: ${QDRANT_API_KEY}" \
    "http://127.0.0.1:6333/collections/${COLLECTION}" \
    > /tmp/qdrant-collection-info.json
)

python3 -m json.tool \
  < /tmp/qdrant-collection-info.json

コレクション名に空白や URL 上で特別な意味を持つ文字が含まれる場合は、管理画面から確認するか、Python の urllib.parse.quote で符号化して要求する。通常の Open WebUI 由来の名称では問題になりにくいが、API 操作では名称を無加工で安全と仮定しない。

7.3 埋め込みモデルの出力次元とコレクション設定を照合する

Qdrant に点が保存されていても、使用した埋め込みモデルとコレクションのベクトル次元が一致していなければ、同じ空間で質問を検索できない。第 3 章では embeddinggemma の埋め込み API が返す配列の次元を確認した。ここでは、現在の Ollama の出力と Qdrant の設定を同じ処理内で比較する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
COLLECTION='実際のコレクション名'

(
  set -a
  . ./.env
  set +a
  export COLLECTION

  python3 - <<'PY'
import json
import os
import urllib.parse
import urllib.request

collection = os.environ["COLLECTION"]
qdrant_key = os.environ["QDRANT_API_KEY"]
embedding_model = os.environ["EMBEDDING_MODEL"]

embed_request = urllib.request.Request(
    "http://127.0.0.1:11434/api/embed",
    data=json.dumps({
        "model": embedding_model,
        "input": "Argolet の本番保守ポートを確認する",
    }).encode("utf-8"),
    headers={"Content-Type": "application/json"},
    method="POST",
)

with urllib.request.urlopen(embed_request, timeout=60) as response:
    embed_data = json.load(response)

embeddings = embed_data.get("embeddings", [])
if len(embeddings) != 1:
    raise SystemExit(
        f"Unexpected number of embedding vectors: {len(embeddings)}"
    )

embedding_size = len(embeddings[0])

encoded_collection = urllib.parse.quote(collection, safe="")
collection_request = urllib.request.Request(
    f"http://127.0.0.1:6333/collections/{encoded_collection}",
    headers={"api-key": qdrant_key},
)

with urllib.request.urlopen(collection_request, timeout=10) as response:
    collection_data = json.load(response)

vectors = (
    collection_data.get("result", {})
    .get("config", {})
    .get("params", {})
    .get("vectors")
)

if not isinstance(vectors, dict):
    raise SystemExit("The Qdrant vector configuration was not found")

if "size" in vectors:
    qdrant_sizes = {"default": vectors["size"]}
else:
    qdrant_sizes = {
        name: settings.get("size")
        for name, settings in vectors.items()
        if isinstance(settings, dict)
    }

print(f"embedding_model={embedding_model}")
print(f"embedding_size={embedding_size}")
print(f"qdrant_vector_sizes={qdrant_sizes}")

if embedding_size not in qdrant_sizes.values():
    raise SystemExit(
        "The embedding output size does not match the Qdrant collection"
    )

print("dimension_match=True")
PY
)

dimension_match=True になれば、現在の embeddinggemma が返す次元数と、対象コレクションの少なくとも一つのベクトル設定が一致している。ただし、次元数の一致は必要条件であり、同じ埋め込みモデルを使用したことの十分条件ではない。

異なる埋め込みモデルが偶然同じ次元数を返す場合でも、文章を配置する意味空間は一致しない。Qdrant は配列の長さを検証できるが、その数値がどのモデルから生成されたかまでは判定しない。モデル名、版、登録日時を構成記録として保持し、変更後には既存文書を再索引する必要がある。

確認結果 判定
次元数が一致し、登録時から埋め込みモデルを変更していない 文書ベクトルと質問ベクトルを同じ空間で比較できる可能性が高い。
次元数が一致するが、埋め込みモデルを変更した 配列形状は一致しても意味空間は互換ではないため、再索引が必要である。
次元数が一致しない 対象コレクションの誤選択、旧モデルの残存、再索引未実施を疑う。
埋め込み API が失敗する Qdrant を調べる前に、Ollama と埋め込みモデルの状態を確認する。

7.4 点の付随情報から文書本文を確認する

点数と次元数が正しくても、原文が空であったり、必要な文字列が抽出されていなかったりすれば、検索可能な根拠にはならない。対象コレクションから点を取得し、ベクトル本体を除外したうえで、JSON の付随情報を確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
COLLECTION='実際のコレクション名'

(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -X POST \
    -H "api-key: ${QDRANT_API_KEY}" \
    -H 'Content-Type: application/json' \
    -d '{
      "limit": 100,
      "with_payload": true,
      "with_vector": false
    }' \
    "http://127.0.0.1:6333/collections/${COLLECTION}/points/scroll" \
    > /tmp/qdrant-points.json
)

python3 -m json.tool \
  < /tmp/qdrant-points.json

with_vector を false にすることで、長い数値配列を表示せず、点の識別子と付随情報だけを確認できる。検証用文書は短いため 100 件を上限としているが、既存文書を多数含む共有コレクションでは、一回の取得だけで対象点が含まれない場合がある。その場合は Qdrant の管理画面、付随情報による絞り込み、または next_page_offset を使った続きの取得が必要になる。

取得した JSON 全体から、検証文書に含めた固有文字列を再帰的に検索する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
python3 - <<'PY'
import json
from pathlib import Path

path = Path("/tmp/qdrant-points.json")
data = json.loads(path.read_text(encoding="utf-8"))

targets = (
    "47821",
    "47822",
    "Lapis-7",
    "Rho-Blue",
    "開発責任者",
)

matches = {target: [] for target in targets}

def walk(value, location="$"):
    if isinstance(value, dict):
        for key, child in value.items():
            walk(child, f"{location}.{key}")
    elif isinstance(value, list):
        for index, child in enumerate(value):
            walk(child, f"{location}[{index}]")
    elif isinstance(value, str):
        for target in targets:
            if target in value:
                matches[target].append(location)

walk(data)

failed = False

for target in targets:
    locations = matches[target]
    print(f"{target}:")
    if locations:
        for location in locations:
            print(f"  {location}")
    else:
        print("  not found")
        failed = True

if failed:
    raise SystemExit(
        "One or more verification strings were not found in the retrieved payloads"
    )
PY

すべての固有文字列が見つかれば、少なくとも取得した点の付随情報に、検証文書の主要部分が文字列として保存されている。これにより、ファイル名だけが登録され、本文が空になっている状態を除外できる。

ただし、文字列が存在することは、質問時にその点が検索上位へ入ることを保証しない。付随情報の保存は登録経路の確認であり、質問ベクトルとの類似検索は次の段階である。本文が保存されているのに回答できない場合は、文字抽出ではなく、チャンク境界、検索順位、Knowledge の接続、生成用 LLM への投入を調べる。

確認した状態 成立したこと まだ成立を確認していないこと
点数が 0 より大きい 一つ以上の点が Qdrant へ保存された。 点に必要な本文が含まれているかは分からない。
付随情報に 47821 がある 本番ポートの文字列が保存された。 本番ポートの質問でその点が取得されるかは分からない。
47821 と 47822 が同じ点にある 本番と開発の対照情報が同じチャンクに含まれる。 生成用 LLM が二つの値を正しく区別できるかは分からない。
Rho-Blue と 15 分間の条件が同じ点にある 復旧条件が一つの根拠単位として保存された。 言い換えた質問で検索できるかは分からない。
固有文字列が見つからない 取得範囲外、別コレクション、抽出失敗、保存失敗のいずれかである。 回答生成を試す前に保存対象を特定する必要がある。

7.5 Open WebUI と Qdrant のログを両側から確認する

通常対話が成功していても、文書登録時だけ障害が発生する場合がある。通常対話は生成用 LLM だけで成立するが、文書登録では文字抽出、チャンク分割、埋め込み生成、Qdrant 認証、点の保存が追加されるためである。

登録操作を行った時刻に近いログを、Open WebUI と Qdrant の両方から確認する。

1
2
3
4
5
6
7
docker compose logs --since=15m open-webui |
  grep -Ei 'document|chunk|embedding|qdrant|retriev|error|exception|traceback' |
  tail -n 200 || true

docker compose logs --since=15m qdrant |
  grep -Ei 'collection|point|request|error|warning|panic' |
  tail -n 200 || true

grep の検索語は、版によって変化するログ表現を広く拾うためのものであり、特定の文字列が必ず出力されることを前提にしていない。該当行が見つからない場合は、絞り込みを外して同じ時間範囲のログ全体を見る。

1
2
3
4
5
docker compose logs --since=15m --timestamps open-webui |
  tail -n 300

docker compose logs --since=15m --timestamps qdrant |
  tail -n 300
症状 疑う処理 確認対象
埋め込みモデルが見つからない Open WebUI から Ollama への埋め込み要求 RAG_EMBEDDING_MODEL、ollama list、Ollama API を確認する。
Ollama への接続を拒否される コンテナーからホストへの通信 OLLAMA_BASE_URL、host.docker.internal、11434 番ポートを確認する。
Qdrant の認証に失敗する Open WebUI と Qdrant の API 鍵 両コンテナーへ同じ QDRANT_API_KEY が渡されているかを確認する。
ベクトル次元の不一致が出る 旧コレクションと現在の埋め込みモデルの組み合わせ 埋め込みモデルの変更履歴、コレクション設定、再索引の実施状態を確認する。
文書登録処理が例外で終了する 文字抽出、分割、埋め込み、保存のいずれか 最初に発生した例外と、その直前の処理名を確認する。
Open WebUI は成功表示だが点数が増えない 保存処理または確認対象コレクションの選択 複数利用者モード、コレクション一覧、Qdrant 側の要求ログを確認する。

障害調査では、最後に出力された一般的なエラー行だけでなく、最初の例外を確認する。埋め込み生成の失敗が原因で Qdrant への保存処理が呼ばれず、後続処理が別の空値エラーを出す場合がある。末尾の症状だけを修正すると、最初の原因が残る。

Open WebUI の RAG 障害は、文書処理、埋め込み、ベクトルデータベース、検索、生成への投入を分けて調べる必要がある[28]。チャット画面に表示された最終回答だけを見て、Qdrant または生成用 LLM のどちらが原因かを決めてはならない。

7.6 埋め込みモデルまたはチャンク条件を変更したら再索引する

埋め込みモデルを変更すると、次元数が同じであっても、文章が配置される意味空間が変わる。旧モデルで生成した文書ベクトルと、新しいモデルで生成した質問ベクトルは互換ではない。Open WebUI でも、埋め込みモデルを変更した場合は、Knowledge に属するすべての文書を再索引する必要がある[32]

本構成では ENABLE_PERSISTENT_CONFIG=False としているため、埋め込みモデルの変更は管理画面だけで行わず、構成の正である .env を更新する。新しいモデルを Ollama へ取得した後、Open WebUI コンテナーを再作成し、実行中の環境変数を確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
NEW_EMBEDDING_MODEL='model-name'

ollama pull "${NEW_EMBEDDING_MODEL}"

export NEW_EMBEDDING_MODEL

python3 - <<'PY'
import os
from pathlib import Path

path = Path(".env")
text = path.read_text(encoding="utf-8").splitlines()
new_model = os.environ["NEW_EMBEDDING_MODEL"]

updated = []
found = False

for line in text:
    if line.startswith("EMBEDDING_MODEL="):
        updated.append(f"EMBEDDING_MODEL={new_model}")
        found = True
    else:
        updated.append(line)

if not found:
    raise SystemExit("EMBEDDING_MODEL was not found in .env")

path.write_text("\n".join(updated) + "\n", encoding="utf-8")
PY

unset NEW_EMBEDDING_MODEL
chmod 600 .env

docker compose config --quiet
docker compose up -d --force-recreate open-webui

docker compose exec open-webui \
  printenv RAG_EMBEDDING_MODEL

model-name は実際に使用する Ollama のモデルタグへ置き換える。最後のコマンドで新しいモデル名が表示されても、既存の Qdrant の点は旧モデルで生成されたままである。環境変数の変更と Knowledge の再索引は別の工程として実行する。

Open WebUI の管理画面で Admin PanelSettingsDocuments を開き、Knowledge の再索引を実行する。再索引では、既存の Knowledge 用ベクトルを削除し、現在のチャンクサイズ、重複量、文字分割方式で文書を再分割し、現在の埋め込みモデルでベクトルを作り直す[32]

1
2
3
4
5
6
7
8
9
10
11
12
13
Knowledge の再索引

既存の Knowledge 用ベクトルを削除
  ↓
保存済みの原文を読み直す
  ↓
現在のチャンク条件で再分割
  ↓
現在の埋め込みモデルで再変換
  ↓
新しいベクトルを Qdrant へ保存
  ↓
点数、次元数、付随情報を再確認

再索引は既存の検索状態を書き換えるため、実行前に Open WebUI の保存領域と Qdrant の保存領域またはスナップショットを退避する。処理中に失敗すると、一時的に Knowledge から検索できない状態になる可能性がある。更新前の状態へ戻せることを確認してから実施する。

変更内容 既存 Knowledge への影響 必要な操作
生成用 LLM だけを変更する 文書ベクトルの空間は変わらない。 通常は Knowledge の再索引を必要としない。
埋め込みモデルを変更する 旧ベクトルと新しい質問ベクトルの意味空間が互換でなくなる。 すべての Knowledge 文書を再索引する。
チャンクサイズまたは重複量を変更する 既存文書は以前の境界を維持し、新規文書だけが新設定を使用する。 検索条件を統一する場合は再索引する。
文字分割方式を変更する 既存文書のチャンク構造は変化しない。 新しい分割方式を適用するには再索引する。
会話へ直接添付したファイルがある Knowledge の再索引対象には含まれない。 新しい埋め込みモデルを適用するにはファイルを再度添付する。

Open WebUI の Knowledge 再索引は、Knowledge に所属する文書を対象とする。会話へ直接添付したファイルのベクトルは同じ操作では更新されないため、埋め込みモデル変更後に再利用する場合は、各ファイルを再度アップロードする必要がある[32]

再索引後には、コレクション一覧、点数、ベクトル次元、付随情報を再度確認する。画面上で再索引が完了したと表示されても、旧次元のコレクションが残っていたり、付随情報から本文が消えていたりする場合には、検索試験へ進まない。

7.7 保存工程の完了条件を固定する

Qdrant の保存確認では、コレクションが存在するという一つの事実だけを完成条件にしない。登録前後の変化、点数、ベクトル次元、付随情報、ログを組み合わせ、文字抽出からベクトル保存までの各段階を判定する。

確認段階 成功条件 失敗時に戻る工程
コレクション接続 認証付きで一覧と詳細を取得できる。 Qdrant の起動、ポート、API 鍵を確認する。
保存状態 対象コレクションの points_count が 0 より大きい。 Open WebUI の登録処理と Qdrant への書き込みを確認する。
ベクトル設定 現在の埋め込みモデルの出力次元と Qdrant の設定が一致する。 対象コレクション、モデル変更履歴、再索引状態を確認する。
本文保存 付随情報に 47821、Lapis-7、Rho-Blue などの固有文字列が含まれる。 文字抽出、チャンク分割、確認対象コレクションを確認する。
登録時ログ 未解決の埋め込み、認証、次元不一致、保存例外がない。 最初に発生した例外の処理段階へ戻る。
変更後の整合性 埋め込みモデルまたはチャンク条件の変更後に再索引されている。 旧ベクトルを削除し、現在の設定で文書を再処理する。

ここまでの確認によって、Argolet の原文がチャンクへ分割され、embeddinggemma のベクトルとして Qdrant に保存され、回答時に再利用できる文字列が付随情報へ残っていることを確認できる。ただし、保存された点が質問に対して検索されるか、取得した断片が生成用 LLM へ渡されるか、文書に忠実な回答になるかはまだ確定していない。次の対照試験では、Knowledge を接続しない状態と接続した状態を比較し、保存されたベクトルが回答経路へ実際に使われることを確認する。


8. 対照試験によって RAG の成立を確認する

8.1 試験条件を固定する

RAG の成立は、Knowledge を接続した会話で正答が一度返っただけでは確認できない。生成用 LLM が偶然正しい値を生成した可能性、直前の会話履歴から情報を保持した可能性、質問文に含まれる語から推測した可能性が残るためである。Knowledge を接続しない対照条件と、接続した試験条件を同じモデル、同じ質問、同じ生成設定で比較する。

各試験は新しい会話で実行する。一つの会話で本番ポート、緊急停止、復旧条件を順番に尋ねると、前の回答や出典が後の質問へ残り、Qdrant から新たに取得した情報と会話履歴から再利用した情報を区別できなくなる。厳密な確認では、一問ごとに会話を作り直し、Knowledge の接続状態だけを変更する。

固定対象 固定する内容 固定する理由
生成用モデル qwen3.5:9b-q4_K_M を使用する。 モデル差による回答変化を検索効果と混同しない。
埋め込みモデル embeddinggemma を使用する。 登録時と質問時のベクトル空間を一致させる。
Knowledge Argolet Operations の未接続と接続だけを切り替える。 回答差を Knowledge の有無へ帰属できるようにする。
会話履歴 各試験を新しい会話で実行する。 以前の質問、回答、出典が後続試験へ混入することを防ぐ。
追加機能 ウェブ検索、外部ツール、別の添付ファイルを使用しない。 回答根拠を Argolet 文書と生成用 LLM の内部知識に限定する。
質問文 未接続時と接続時に同じ文字列を使用する。 表現差による検索順位や生成結果の変化を除外する。

試験前には、Open WebUI のモデル選択欄に正しい生成用モデルが表示され、Argolet Operations に argolet.md が登録されていることを確認する。Qdrant の対象コレクションに点が存在し、付随情報に 47821、Lapis-7、Rho-Blue が含まれていることも前章の手順で確認しておく。

8.2 Knowledge を接続しない状態を対照条件とする

新しい会話を作成し、qwen3.5:9b-q4_K_M を選択する。Knowledge、ファイル、ウェブ検索、追加ツールを接続せず、次の質問をそのまま入力する。

Argolet の本番保守ポートは何番ですか。

Argolet は検証用に作成した架空のシステムであり、その本番保守ポートを生成用 LLM が正当に知る経路はない。期待する応答は、「情報がない」「確認できない」「文書を提示してほしい」といった回答保留である。別の数値を返した場合は、事前知識ではなく根拠のない補完として記録する。

未接続状態で偶然 47821 と答える可能性も理論上は残る。その場合、正答したという事実だけで RAG の成立を判定できない。出典がなく、登録文書も接続していないため、その回答は文書に根拠を持たない偶然の一致として扱う。

未接続時の回答 判定
情報不足として回答を保留する 対照条件として適切である。接続後の正答との差を確認できる。
47821 以外の数値を回答する 根拠のない補完が発生している。接続後に文書へ忠実になるかを確認する。
47821 と回答するが出典がない 偶然の一致であり、RAG の成功とは扱わない。
Argolet を別の実在対象として説明する 固有名詞の誤同定が発生している。Knowledge 接続後に文書内容へ置き換わるかを確認する。

回答全文、使用モデル、Knowledge が未接続であること、試験日時を記録する。この結果は、Knowledge 接続後の回答と比較するための基準であり、未接続時の回答品質そのものを評価する試験ではない。

8.3 Knowledge を接続して同一質問を行う

別の新しい会話を作成し、同じ qwen3.5:9b-q4_K_M を選択する。Argolet Operations だけを接続し、未接続時と同じ質問を入力する。

Argolet の本番保守ポートは何番ですか。

期待する回答は 47821 である。回答に出典が表示される場合は、単に argolet.md の名前が表示されるだけでなく、「Argolet の本番保守ポートは 47821 である」という対応箇所が検索候補として取得されていることを確認する。

同じチャンクに開発環境の 47822 も含まれている場合、生成用 LLM は二つの値から本番環境に対応する値を選ばなければならない。47821 を返しただけでなく、本番用と開発用を取り違えていないことが、この質問の判定対象になる。

確認対象 成功条件 失敗例
検索 本番保守ポートと 47821 を含むチャンクが取得される。 緊急停止や復旧条件だけが取得される。
条件識別 本番用の 47821 を回答する。 開発用の 47822 を回答する。
根拠忠実性 文書に記載された値だけを回答する。 別のポート番号や未記載の接続方式を追加する。
出典 argolet.md の本番接続部分を確認できる。 出典がないか、回答と無関係な箇所だけが表示される。

未接続時には回答を保留し、接続時には正しいチャンクを出典として 47821 を回答した場合、Knowledge の有無によって回答が変化したことを確認できる。ただし、この一問だけでは、単純な名称と数値の検索が成功したことしか分からない。手順、複合条件、未記載情報についても試験する。

8.4 手順と複合条件を別々に試験する

手順と復旧条件は、それぞれ新しい会話で試験する。両方の質問を本番ポートの会話に続けて入力すると、既に取得された Argolet 文書が会話履歴へ残り、質問ごとの検索結果を判定しにくくなる。

試験 質問 期待する回答 確認する構造
緊急停止手順 緊急停止時に使用する手順名と、最初の二工程を説明してください。 Lapis-7 手順を使用し、接続キューを停止してから書き込み処理を停止する。 手順名だけでなく、二工程の内容と順序が同じチャンクまたは取得文脈に残っているか。
復旧条件 復旧を開始できる監視条件を説明してください。 Rho-Blue が 15 分間連続して 0.20 未満になった場合だけ開始できる。 指標名、閾値、継続時間、開始可否の四要素を欠落なく取得できるか。

緊急停止試験では、Lapis-7 という名称だけを回答しても完全な正答とはしない。「接続キューを停止する」「その後に書き込み処理を停止する」という二工程と順序が必要である。操作順序を逆にした場合は、関連チャンクを取得できていても、生成時に因果関係を保持できなかったと判定する。

復旧条件では、Rho-Blue と 0.20 未満だけでは不十分である。「15 分間連続して」という継続条件を欠くと、一時的な閾値低下でも復旧を開始できるという別の規則になる。回答の一部に正しい語が含まれているかではなく、運用判断を決める条件がすべて保存されているかを確認する。

回答状態 手順試験の判定 復旧条件試験の判定
全要素が正しく、余分な補完がない 合格 合格
主要な名称だけが正しい 手順内容不足 条件内容不足
必要な要素はあるが順序または継続条件が誤る 手順関係の保持に失敗 条件関係の保持に失敗
文書にない工程または条件を追加する 根拠外の補完 根拠外の補完
無関係なチャンクを出典とする 検索失敗 検索失敗

8.5 言い換えによって意味検索を確認する

元文書と同じ語を含む質問だけでは、意味検索ではなく、表面的な語の一致によって関連チャンクが取得された可能性が残る。別の新しい会話で、文書にない表現へ言い換えて質問する。

障害対応後、サービスを再開してよいと判断する基準は何ですか。

文書では「復旧を開始できる監視条件」と記述しており、質問では「サービスを再開してよいと判断する基準」と表現している。期待する回答は、Rho-Blue が 15 分間連続して 0.20 未満になることである。

この質問で復旧条件のチャンクを取得できれば、完全な文字列一致ではなく、質問と文書の意味的な近さを使って検索できたと判断できる。取得できない場合は、埋め込みモデルの日本語表現への適合、チャンク本文、取得件数を確認する。

言い換え試験でも、回答だけで合否を決めない。正答していても、出典に復旧条件のチャンクが含まれていなければ、生成用 LLM が会話文脈や一般的な障害対応の知識から推測した可能性が残る。

8.6 文書に存在しない情報を質問する

別の新しい会話で Argolet Operations を接続し、文書に記載していない情報を質問する。

Argolet の開発責任者は誰ですか。

argolet.md には開発責任者名を記載していない。期待する回答は、「文書には記載がない」「提供された情報からは確認できない」である。特定の人物名を生成した場合、検索結果が不足しているのではなく、生成用 LLM が根拠のない情報を補完している。

この試験では、文書内の「開発責任者名を記載していない」という記述が取得されることも確認する。該当チャンクが取得され、回答も保留した場合は、検索と生成の両方が期待どおりに動作している。該当チャンクを取得できたのに人物名を生成した場合は、検索ではなく回答忠実性の問題である。

検索結果 回答 判定
未記載であることを示すチャンクを取得する 確認できないと回答する。 合格。情報不足を根拠に回答を保留できている。
未記載であることを示すチャンクを取得する 人物名を生成する。 生成失敗。取得文書より事前知識または推測を優先している。
無関係なチャンクを取得する 確認できないと回答する。 回答は妥当だが検索は不十分である。
無関係なチャンクを取得する 人物名を生成する。 検索と生成の両方に失敗している。

RAG の実用性は、記載された値へ正答できることだけでは決まらない。文書にない情報を「ない」と判断できなければ、利用者は正しい回答と作り出された回答を区別できない。未記載情報への回答保留は、正答試験と同じ重さで評価する。

8.7 検索結果と生成回答を分けて判定する

Qdrant は、質問から生成されたベクトルと保存済みの点を距離尺度で比較し、近い点を検索結果として返す[33]。Open WebUI は取得した文書断片を生成用 LLM の入力へ組み込み、最終回答を生成する。検索と生成は連続しているが、失敗原因は異なる。

検索結果 回答 判定 主な確認対象
正しいチャンクを取得する 文書どおりに回答する。 検索から生成までの基本経路が成立している。 同じ試験を再実行し、再現性を確認する。
正しいチャンクを取得する 値、条件、順序を誤る。 検索は成功し、生成が失敗している。 生成用 LLM、RAG テンプレート、取得文脈量、競合する記述を確認する。
必要なチャンクを取得しない 誤答または回答不能になる。 検索経路が失敗している。 文字抽出、チャンク分割、埋め込み、Knowledge 接続、取得件数を確認する。
必要なチャンクを取得しない 偶然正答する。 根拠に基づく回答ではないため不合格とする。 事前知識、会話履歴、質問内の手掛かりを確認する。
正しいチャンクと無関係なチャンクを取得する 正答する。 基本回答は成功しているが、検索精度に改善余地がある。 チャンクサイズ、取得件数、類似断片の重複を確認する。
正しいチャンクを取得する 文書にない情報を追加する。 根拠忠実性に失敗している。 回答指示と生成用モデルの挙動を確認する。

出典表示は、文書が回答に使われた可能性を確認する手掛かりである。出典名だけでなく、表示された断片に質問への回答を決める値、条件、順序が含まれているかを見る。出典に必要な根拠が存在しないのに正答した場合は、RAG による正答とは扱わない。

検索結果が正しいのに回答が誤る場合、チャンクサイズや埋め込みモデルを先に変更してはならない。検索段階では必要な情報を取得できているため、生成用 LLM へ渡された文脈、複数チャンク間の競合、回答指示、モデル規模を確認する。反対に、必要なチャンクを取得できていない場合は、生成用 LLM を大きくしても根拠不足は解消しない。

8.8 試験結果を判定表として保存する

各試験について、回答文だけでなく、Knowledge の接続状態、取得された出典、合否、失敗段階を記録する。モデルや Open WebUI を更新した後に同じ質問を再実行し、検索結果と回答の変化を比較できるようにする。

試験 ID Knowledge 試験内容 期待結果 合格条件
T0 未接続 本番保守ポートを質問する。 情報不足として回答を保留する。 文書に基づく正答を装わない。
T1 接続 本番保守ポートを質問する。 47821 と回答する。 本番接続のチャンクを取得し、47822 と取り違えない。
T2 接続 緊急停止手順を質問する。 Lapis-7、接続キュー停止、書き込み処理停止を回答する。 手順名、二工程、順序がすべて正しい。
T3 接続 復旧条件を質問する。 Rho-Blue、0.20 未満、15 分間連続を回答する。 指標、閾値、継続時間を欠落しない。
T4 接続 復旧条件を言い換えて質問する。 T3 と同じ条件を回答する。 文書と異なる表現でも復旧条件のチャンクを取得する。
T5 接続 開発責任者名を質問する。 文書からは確認できないと回答する。 人物名を作らず、未記載であることを示す出典を取得する。

試験記録には、次の情報を残す。

記録項目 内容
実行日時 試験を行った日時を記録する。
Open WebUI 使用したイメージタグを記録する。
Qdrant 使用したイメージタグを記録する。
生成用モデル モデル名と量子化タグを記録する。
埋め込みモデル 登録時と試験時のモデル名を記録する。
文書 argolet.md の SHA-256 を記録する。
検索設定 チャンクサイズ、重複量、取得件数、混合検索の有無を記録する。
質問 入力した文字列を省略せず記録する。
検索結果 表示された出典と、回答に必要な断片が含まれたかを記録する。
回答 生成された回答全文を記録する。
判定 検索、忠実性、完全性、回答保留を個別に判定する。

一回の成功結果だけでは、構成変更後も同じ挙動が続くことを保証できない。少なくとも T1 から T5 を再実行し、正しい出典と回答が繰り返し得られることを確認する。結果が揺れる場合は、同じ質問でも検索順位または生成結果が変化しているため、成功率と失敗形式を記録する。

8.9 RAG の完成条件を確定する

本構成における RAG の完成条件は、Open WebUI の画面で Knowledge を選択できることでも、Qdrant に点が存在することでもない。Knowledge を接続しない状態では答えられない固有情報について、接続後には質問に対応するチャンクが取得され、そのチャンクに含まれる値、手順、条件だけを使って回答できることである。

完成条件 確認内容
対照差 Knowledge 未接続時と接続時で、根拠の有無に対応した回答差が現れる。
検索適合 質問に必要な値、手順、条件を含むチャンクが取得される。
条件保持 対象環境、工程順序、閾値、継続時間を欠落させない。
根拠忠実性 取得文書にない数値、人物、工程、条件を追加しない。
回答保留 文書に存在しない情報について、確認できないと回答する。
再現性 新しい会話で同じ試験を行っても、同じ根拠と結論を得られる。

未接続時には Argolet を知らず、接続時には 47821、Lapis-7、Rho-Blue の復旧条件を文書どおりに回答し、未記載の開発責任者名について回答を保留できれば、登録経路、検索経路、生成経路が一続きに成立している。ここで確認されたのは、モデルが文書を記憶したことではない。質問のたびに必要なチャンクを Qdrant から取得し、その範囲内で回答を構成できるローカル RAG の処理経路である。


9. 検索精度を文字抽出、分割、検索、生成に分けて調整する

9.1 基準構成を固定して一項目ずつ変更する

RAG の回答が不正確な場合、生成用 LLM、埋め込みモデル、チャンクサイズ、重複量、取得件数を同時に変更してはならない。回答が改善しても、どの変更が有効だったのかを特定できず、別の文書で同じ調整を再現できないためである。

調整前に、第 8 章の T1 から T5 を基準試験として実行し、質問、取得された出典、回答、合否を保存する。そのうえで一つの設定だけを変更し、Knowledge を再処理する必要があるかを判断してから、同じ試験を繰り返す。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
原文
  ↓
文字抽出
  ↓
見出し処理とチャンク分割
  ↓
埋め込みベクトル生成
  ↓
Qdrant へ保存
  ↓
質問に対する類似検索
  ↓
取得チャンクを生成入力へ追加
  ↓
生成用 LLM が回答
  ↓
出典と回答を個別に評価
変更対象 変化する処理 再索引 比較対象
文書本文 抽出対象、チャンク本文、埋め込みベクトルが変わる。 必要 変更前後の原文、点数、付随情報、検索結果を比較する。
文字抽出機構 抽出される本文と読み順が変わる。 必要 固有文字列、表の列関係、段落順序を比較する。
チャンクサイズ、重複量、分割方式 検索単位とベクトル数が変わる。 必要 点数、各チャンクの本文、検索順位を比較する。
埋め込みモデル 文書と質問を配置する意味空間が変わる。 必要 ベクトル次元、検索順位、言い換え試験の結果を比較する。
取得件数 生成用 LLM へ渡すチャンク数が変わる。 不要 必要な根拠の取得率、無関係な断片、入力文脈量を比較する。
RAG テンプレート 取得チャンクを生成用 LLM へ渡す指示が変わる。 不要 正しい根拠を取得した場合の回答忠実性を比較する。
生成用 LLM、温度 同じ取得文脈から生成される回答が変わる。 不要 根拠忠実性、回答保留、再現性、応答速度を比較する。

最初に変更するのは、最終回答から最も遠い生成設定ではなく、処理経路の上流にある文字抽出である。上流で失われた情報は、下流の埋め込みモデルや生成用 LLMを変更しても復元できない。抽出結果が正しいことを確認してから、分割、検索、生成の順に進む。

9.2 原文と抽出結果の一致を確認する

文書に答えが記載されているのに検索結果へ現れない場合、最初に必要な文字列が抽出されたかを確認する。画像だけの PDF、複雑な段組み、表、ヘッダーとフッターを持つ文書では、画面上に情報が見えていても、抽出結果から欠落したり、読み順が崩れたりする。

Open WebUI の文書表示または取得した Qdrant の付随情報から、質問への回答に必要な文字列を確認する。Argolet の検証では、少なくとも次の対応関係が文字列として残っている必要がある。

質問対象 抽出結果に必要な文字列 文字列だけでは不十分な条件
本番保守ポート Argolet、本番、47821 開発環境の 47822 と対象環境を区別できる必要がある。
緊急停止 Lapis-7、接続キュー、書き込み処理 二つの工程の順序が保持されている必要がある。
復旧条件 Rho-Blue、0.20 未満、15 分間 一時的な低下ではなく、連続条件である必要がある。
開発責任者 記載していない、判断できない 人物名が存在しないこと自体を根拠として取得する必要がある。

抽出結果に必要な文字列が存在しない場合は、チャンクサイズや取得件数を変更しない。同じ内容を Markdown へ移し、Markdown では検索できるかを確認する。Markdown で成功し、元の PDF で失敗する場合、RAG 全体ではなく文書抽出が原因である。

1
2
3
4
5
6
7
8
9
元の PDF では失敗
  ↓
同じ内容を Markdown として登録
  ↓
Markdown では成功
  ↓
埋め込み、Qdrant、検索、生成の基本経路は成立
  ↓
PDF の文字層、読み順、表構造、抽出機構を調整

文字列が存在していても、表の列関係や手順の順序が崩れている場合は、検索用文書として十分ではない。位置関係に依存する情報は、判断対象と値の対応を文章として明示する。抽出機構の高度化より、検索に必要な関係を補助 Markdown として作成する方が再現性を確保しやすい場合もある。

9.3 検索可能なチャンク本文になっているか確認する

文字抽出が正しくても、分割後のチャンクが単独で意味を持たなければ、安定した検索はできない。Open WebUI では、Markdown 見出し分割を有効にすると見出し単位で前処理した後、文字またはトークンによる通常の分割を行う。チャンクサイズは、RAG_TEXT_SPLITTER=character の場合は文字数、token の場合はトークン数として扱われる[34]

本構成では文字分割を使用しているため、CHUNK_SIZE=1000 は最大 1000 文字を意味する。Open WebUI の画面または Qdrant の付随情報から、取得対象となるチャンク本文を確認し、質問への回答に必要な要素が同じ意味単位へ残っているかを見る。

チャンク本文の状態 検索または生成への影響 調整方向
主語と値が別のチャンクへ分かれている 47821 を取得しても、何の値かを確定できない。 チャンクサイズを広げるか、原文の一文内へ主語と値をまとめる。
条件と例外が別のチャンクへ分かれている 閾値だけを取得し、15 分間の連続条件を欠落させる。 条件を一段落へまとめ、必要に応じてサイズまたは重複量を増やす。
複数の対象と数値が一つのチャンクへ混在している 本番の 47821 と開発の 47822 を取り違えやすくなる。 見出しまたは段落を分け、チャンクサイズを縮小する。
見出しだけの極端に短いチャンクがある 埋め込みに必要な意味情報が不足し、検索候補を消費する。 見出し直下に対象と要点を記述し、短い断片を隣接本文へ統合する。
同じ文章が複数チャンクへ大量に重複している 取得上位を類似断片が占め、別の根拠を取得できなくなる。 重複量を減らすか、原文内の重複記述を整理する。

検索に失敗した質問について、最初に「取得されるべき理想的なチャンク」を文章として定義する。たとえば復旧条件では、次の一文が一つの根拠単位として残ることが望ましい。

復旧を開始できるのは、監視指標 Rho-Blue が 15 分間連続して 0.20 未満になった場合だけである。

理想的なチャンク本文を決めずにサイズだけを変更すると、点数の増減は観察できても、検索品質が改善したかを判断できない。各試験質問について、必要な主語、値、条件、例外が同じチャンクに含まれることを先に確認する。

9.4 チャンクサイズと重複量を段階的に調整する

短い仕様書では、1000 文字のチャンクに複数の節が入る場合がある。Argolet の本番接続、緊急停止、復旧条件が同じチャンクへ混在する場合は、チャンクサイズを段階的に縮小する。ただし、サイズと重複量を同時に変更せず、どちらの変更が検索結果へ影響したかを分ける。

試験構成 チャンクサイズ 重複量 変更目的
C0 1000 100 現在の基準構成とする。
C1 800 100 異なる節の混在が減るか確認する。
C2 600 100 質問対象をさらに限定できるか確認する。
C3 600 50 同じ内容の重複取得を抑えられるか確認する。
C4 600 150 条件や手順が境界で切れる場合に、連続性を補えるか確認する。

C1 と C2 では重複量を 100 のまま維持し、チャンクサイズの効果だけを比較する。適切なサイズが決まった後、C3 または C4 で重複量を調整する。すべての値を試す必要はなく、前段階で失敗原因が解消した時点で変更を止める。

構成値を変更する場合は、.env や compose.yaml を構成の正として更新し、Open WebUI コンテナーを再作成する。現在の構成ではチャンク値を compose.yaml に直接記述しているため、対象値を一つだけ変更する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python3 - <<'PY'
from pathlib import Path

path = Path("compose.yaml")
text = path.read_text(encoding="utf-8")

old = '      CHUNK_SIZE: "1000"'
new = '      CHUNK_SIZE: "800"'

if old not in text:
    raise SystemExit("The expected CHUNK_SIZE setting was not found")

path.write_text(text.replace(old, new, 1), encoding="utf-8")
PY

docker compose config --quiet
docker compose up -d --force-recreate open-webui

docker compose exec open-webui printenv CHUNK_SIZE
docker compose exec open-webui printenv CHUNK_OVERLAP

チャンクサイズまたは重複量を変更しただけでは、Qdrant に保存済みの点は変化しない。Knowledge の再索引を実行し、現在の設定で文字抽出、分割、埋め込み、保存をやり直す[32]。再索引後には、点数と付随情報を確認してから T1 から T5 を再実行する。

1
2
3
4
5
6
7
8
9
10
11
12
13
構成値を一つ変更
  ↓
Open WebUI を再作成
  ↓
実行中の環境変数を確認
  ↓
Knowledge を再索引
  ↓
Qdrant の点数とチャンク本文を確認
  ↓
同じ試験質問を実行
  ↓
基準構成と比較

点数が増えたこと自体は改善を意味しない。チャンクを小さくすると点数は増えやすいが、必要な条件が分断される可能性も高まる。評価対象は、点数ではなく、質問に必要なチャンクが取得され、余分なチャンクが減り、回答に必要な関係が保持されたかである。

9.5 取得件数は検索漏れを確認してから増やす

RAG_TOP_K は、質問ごとに取得する候補チャンク数の基準値である。現在の構成では 3 件を取得する。必要なチャンクが検索順位の 4 位または 5 位にある場合は、5 件へ増やすことで検索漏れを減らせる。

取得件数を増やす前に、現在の上位 3 件を確認する。正しいチャンクが既に含まれている場合、5 件へ増やしても検索失敗は解消しない。追加される無関係なチャンクによって生成用 LLM の判断が不安定になる可能性がある。

現在の上位 3 件 取得件数を増やす判断
正しいチャンクが 1 位にある 増やさない。生成用 LLM または RAG テンプレートを確認する。
正しいチャンクが 2 位または 3 位にある 回答へ渡されているなら増やさない。競合する上位チャンクを確認する。
正しいチャンクが 4 位または 5 位にある 3 から 5 へ増やし、回答と入力文脈の変化を比較する。
正しいチャンクが上位候補にない 取得件数ではなく、抽出、チャンク本文、埋め込みを確認する。
同じ内容のチャンクが上位を占める 取得件数を増やす前に、重複量と重複文書を調整する。

Open WebUI の RAG では、チャンクサイズと取得件数の積に近い量の文書が生成入力へ加わる。1000 トークンのチャンクを 5 件取得する構成では、検索結果だけでおよそ 5000 トークンを使用し、これにシステム指示、会話履歴、質問、回答用の領域が加わる[28]。文字分割を使用する本構成では文字数とトークン数は一致しないが、取得件数を増やすほど文脈消費が増える関係は変わらない。

RAG_TOP_K だけを変更する場合、保存済みのチャンクとベクトルは変わらないため、Knowledge の再索引は不要である。Open WebUI コンテナーを再作成し、実行中の値を確認した後、同じ質問を再実行する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python3 - <<'PY'
from pathlib import Path

path = Path("compose.yaml")
text = path.read_text(encoding="utf-8")

old = '      RAG_TOP_K: "3"'
new = '      RAG_TOP_K: "5"'

if old not in text:
    raise SystemExit("The expected RAG_TOP_K setting was not found")

path.write_text(text.replace(old, new, 1), encoding="utf-8")
PY

docker compose config --quiet
docker compose up -d --force-recreate open-webui

docker compose exec open-webui printenv RAG_TOP_K

取得件数を 5 へ増やした結果、必要なチャンクが追加されて正答できるようになった場合は、検索漏れが原因だったと判断できる。正しいチャンクが増えた一方で回答が不安定になった場合は、取得件数を戻し、チャンク本文または埋め込み検索を改善する。

9.6 検索が正しい場合だけ生成設定を調整する

正しいチャンクが取得され、そのチャンクに必要な値、条件、順序が含まれているのに回答が誤る場合、検索経路は成立している。ここで初めて、RAG テンプレート、生成用 LLM、生成パラメーターを確認する。

Open WebUI の RAG テンプレートには、取得した文書断片を挿入する {{CONTEXT}} が必要である。質問文はテンプレート処理後に自動的に追加されるため、{{QUERY}} を重ねて記述すると、同じ質問が二回入力される[34]

回答規則は、取得文書の用途と情報不足時の動作を明示する。次のように、文書以外の推測を禁止し、根拠がない場合の応答を固定する。

以下の参考文脈だけに基づいて回答してください。参考文脈に回答を決める情報がない場合は、確認できないと回答してください。数値、人物名、手順、条件を推測して補わないでください。

この規則は、正しいチャンクが取得されていることを前提とする。検索結果が誤っている状態で推測を禁止すると、誤答が回答不能へ変わるだけであり、必要な根拠を取得できるようにはならない。

検索結果 生成結果 調整対象
必要な根拠を取得している 値または条件を誤る。 競合するチャンク、RAG テンプレート、生成用 LLM を確認する。
必要な根拠を取得している 文書にない情報を追加する。 根拠限定と回答保留の規則を強化する。
必要な根拠を取得している 実行ごとに回答が変わる。 温度、乱数種、モデル規模を確認する。
必要な根拠を取得していない 回答不能または誤答になる。 生成設定を変更せず、抽出、分割、検索へ戻る。

温度を下げると、同じ入力に対する語句選択の変動は小さくなる。Ollama では温度が高いほど出力の多様性が増え、低いほど一貫した出力になりやすい[35]。ただし、温度を下げても、取得文書にない情報を参照できるようになるわけではなく、誤った根拠への忠実性を改善するものでもない。

回帰試験では、温度だけでなく乱数種も固定できれば、同じ入力に対する生成差を減らせる。ただし、Open WebUI から Ollama へ実際に渡された設定を確認し、画面上の表示だけで固定されたと判断しない。生成用モデルを変更した場合も、同じ取得チャンクを使い、回答忠実性と速度だけを比較する。

より大きな生成用 LLM へ変更するのは、正しい根拠を取得できているにもかかわらず、条件の対応、手順順序、回答保留を安定して処理できない場合である。検索が誤っている状態でモデルだけを大型化すると、誤った根拠をより自然な文章へ変換する結果になり得る。

9.7 症状から最初に調べる層を決める

症状 最初に確認する層 最初の操作 再索引
文書内の固有文字列が Qdrant の付随情報にない 文字抽出 抽出結果を確認し、同じ内容を Markdown で試験する。 文書または抽出設定を変更した場合は必要
文字列はあるが、主語と値が別の点に分かれている チャンク分割 原文構造またはチャンクサイズを調整する。 必要
本番と開発の値が一つの大きなチャンクへ混在する チャンク分割 見出しと段落を分け、サイズを段階的に縮小する。 必要
必要なチャンクが 4 位または 5 位にある 検索件数 RAG_TOP_K を 3 から 5 へ増やす。 不要
必要なチャンクが候補に現れない 埋め込みまたは文書表現 言い換え試験、チャンク本文、埋め込みモデルを確認する。 埋め込みモデルまたは本文を変更した場合は必要
同じ内容のチャンクが上位を占有する 重複量または文書重複 重複量を減らし、同内容の文書を整理する。 必要
正しいチャンクを取得しているが値を誤る 生成 競合文脈、RAG テンプレート、生成用モデルを確認する。 不要
文書にない人物名や数値を生成する 生成 根拠限定と情報不足時の回答保留を明示する。 不要
会話が長くなると以前は取得できた根拠を使えない 文脈量 新しい会話で再試験し、取得件数と履歴量を減らす。 不要

調整では、症状に近い設定を無作為に変更するのではなく、処理経路上で最初に期待値と異なった段階へ戻る。付随情報に本文がなければ抽出、本文はあるが必要な単位に分かれていなければ分割、正しいチャンクが候補に入らなければ検索、正しいチャンクを渡しても誤るなら生成が対象になる。

9.8 調整結果を比較可能な形で記録する

各構成について、設定値だけでなく、Qdrant の点数、取得されたチャンク、最終回答を一つの記録として残す。回答だけを保存すると、改善が検索によるものか生成によるものかを後から判定できない。

記録項目 記録内容
構成 ID C0、C1、K1 など、変更内容を識別できる名称を付ける。
文書ハッシュ argolet.md の SHA-256 を記録する。
分割設定 分割方式、見出し分割、チャンクサイズ、重複量を記録する。
埋め込み設定 埋め込みモデル名とベクトル次元を記録する。
Qdrant 対象コレクション、点数、主要チャンク本文を記録する。
検索設定 取得件数、混合検索の有無を記録する。
検索結果 質問ごとの取得チャンクと順位を記録する。
生成設定 生成用モデル、温度、コンテキスト長を記録する。
最終回答 回答全文と出典を記録する。
判定 検索適合、条件保持、根拠忠実性、回答保留を個別に採点する。

一つの設定変更で T1 だけが改善し、T2 や T3 が悪化する場合、その構成を全面的な改善とは扱わない。たとえばチャンクを小さくして本番ポートの検索順位が上がっても、緊急停止の二工程が分断されたなら、単一値の検索と複合条件の検索の間で別の調整が必要である。

9.9 調整の終了条件を決める

検索精度の調整には、すべての文書へ通用する唯一の最適値はない。短い仕様書、長い手順書、表中心の資料では、必要な検索単位が異なる。構成を無期限に変更するのではなく、対象文書と質問集合に対する終了条件を先に決める。

終了条件 判定内容
文字抽出 回答に必要な名称、値、条件、例外が付随情報へ残っている。
チャンク構造 各質問に必要な判断根拠が一つまたは少数のチャンクで完結する。
検索適合 T1 から T5 で必要なチャンクが取得件数内へ入る。
不要取得 回答を競合させる無関係な値や重複チャンクが上位を占有しない。
根拠忠実性 取得文書にない人物名、数値、条件、手順を追加しない。
条件保持 本番と開発、工程順序、閾値、継続時間を取り違えない。
回答保留 文書に存在しない情報について確認できないと回答する。
再現性 新しい会話で再試験しても同じ根拠と結論を得られる。

この調整工程で決めるのは、チャンクサイズや取得件数の一般的な正解ではない。対象文書から必要な判断根拠を失わずに取り出し、MacBook Pro の限られた文脈と記憶容量の範囲で、生成用 LLM へ過不足なく渡せる構成である。検索結果を確認せずにモデルを大型化するのではなく、情報が失われた最初の段階へ戻ることが、ローカル RAG を再現可能な処理基盤として調整する条件になる。


10. 再起動、保存、更新、復元まで確認する

10.1 再起動試験を処理層ごとに行う

初回の対照試験に成功した後、再起動によって利用者情報、Knowledge、文書ベクトル、構成値が失われないことを確認する。再起動試験は、コンテナーの再起動、Docker Desktop の再起動、MacBook Pro の再起動に分ける。三つを同時に行うと、状態消失や接続失敗がどの境界で起きたかを特定できない。

最初に Qdrant コンテナーだけを再起動する。再起動後は固定時間だけ待つのではなく、認証付きのヘルスチェックが成功するまで確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
docker compose restart qdrant

(
  set -a
  . ./.env
  set +a

  count=0

  until curl -fsS \
    -H "api-key: ${QDRANT_API_KEY}" \
    http://127.0.0.1:6333/healthz \
    > /dev/null
  do
    count=$((count + 1))

    if [ "$count" -ge 30 ]; then
      printf '%s\n' 'Qdrant did not become ready.' >&2
      exit 1
    fi

    sleep 1
  done
)

Qdrant が要求を受け付けられる状態になった後、Open WebUI を再起動する。Open WebUI は起動時に Qdrant と Ollama の接続設定を読み込むため、依存先の準備前に確認を始めると、一時的な接続エラーを永続的な障害と誤認する可能性がある。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
docker compose restart open-webui

count=0

until curl -fsS \
  -o /dev/null \
  http://127.0.0.1:3000/
do
  count=$((count + 1))

  if [ "$count" -ge 60 ]; then
    printf '%s\n' 'Open WebUI did not become ready.' >&2
    exit 1
  fi

  sleep 1
done

docker compose ps
docker compose logs --tail=100 qdrant
docker compose logs --tail=200 open-webui

再起動後には、ブラウザー画面が表示されることだけでなく、次の状態を確認する。

確認対象 再起動後の成功条件 失敗時の確認先
利用者情報 既存の利用者名とパスワードでログインできる。 data/open-webui の保存状態と Open WebUI のログを確認する。
新規登録 新規登録が無効のままである。 ENABLE_SIGNUP=False が実行中のコンテナーへ渡されているか確認する。
会話履歴 再起動前の会話を開ける。 Open WebUI のデータベースと保存領域を確認する。
Knowledge Argolet Operations と登録ファイルが残っている。 Open WebUI の管理情報とアップロード済み文書を確認する。
Qdrant 対象コレクションと点数が再起動前と一致する。 data/qdrant の保存状態と Qdrant のログを確認する。
RAG T1 から T5 の検索結果と回答が再現する。 Knowledge 接続、取得チャンク、生成入力を確認する。

続いて Docker Desktop を終了し、再度起動する。Docker Desktop の起動後に同じヘルスチェック、ログイン、コレクション確認、対照試験を行う。ここでは、Docker の仮想環境が再作成されても、ホスト上のバインド先から状態を読み直せることを確認する。

最後に MacBook Pro を再起動する。再起動後は、Ollama と Docker Desktop が起動していることをそれぞれ確認する。自動起動されない場合はアプリケーションを起動してから試験を続ける。

1
2
3
4
5
6
curl -fsS \
  http://127.0.0.1:11434/api/tags |
  python3 -m json.tool

docker version
docker compose ps

MacBook Pro の再起動後にも T1 から T5 が成立すれば、端末の稼働中だけ保持される一時状態ではなく、Ollama のモデル保存領域、Open WebUI のバインド先、Qdrant のバインド先から必要な状態を復元できている。

10.2 コンテナーの削除と再作成を試験する

docker compose restart は既存コンテナーのプロセスを再起動するだけであり、同じ構成ファイルから環境を再現できることまでは確認しない。コンテナーを削除して作り直し、実行環境と永続データが分離されていることを確認する。

docker compose down は、既定では Compose が作成したコンテナーとネットワークを削除する。明示したバインド先はホスト上のディレクトリであり、コンテナーとは別に残る[36]

1
2
3
4
5
6
7
8
docker compose down

test -d data/open-webui
test -d data/qdrant
test -f .env
test -f compose.yaml

docker compose up -d qdrant

Qdrant の起動完了をヘルスチェックで確認してから、Open WebUI を作成する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(
  set -a
  . ./.env
  set +a

  count=0

  until curl -fsS \
    -H "api-key: ${QDRANT_API_KEY}" \
    http://127.0.0.1:6333/healthz \
    > /dev/null
  do
    count=$((count + 1))

    if [ "$count" -ge 30 ]; then
      printf '%s\n' 'Qdrant did not become ready.' >&2
      exit 1
    fi

    sleep 1
  done
)

docker compose up -d open-webui

count=0

until curl -fsS \
  -o /dev/null \
  http://127.0.0.1:3000/
do
  count=$((count + 1))

  if [ "$count" -ge 60 ]; then
    printf '%s\n' 'Open WebUI did not become ready.' >&2
    exit 1
  fi

  sleep 1
done

docker compose ps

固定秒数の sleep だけに依存すると、起動が速い機種では不要な待機が発生し、起動が遅い場合には準備前に次の処理へ進む。API の成功を条件にすることで、端末負荷や更新後の初期化時間が変わっても同じ判定を使える。

再作成後には、実行中のコンテナーへ構成値が渡されていることを確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
docker compose exec -T open-webui python3 - <<'PY'
import os

names = (
    "ENABLE_PERSISTENT_CONFIG",
    "ENABLE_SIGNUP",
    "OLLAMA_BASE_URL",
    "VECTOR_DB",
    "QDRANT_URI",
    "RAG_EMBEDDING_MODEL",
    "CHUNK_SIZE",
    "CHUNK_OVERLAP",
    "RAG_TOP_K",
)

for name in names:
    print(f"{name}={os.environ.get(name, '')}")
PY

利用者、会話、Knowledge、Qdrant の点が残り、T1 から T5 が再現すれば、コンテナー自体には復元不能な状態を保持していない。反対に、画面は起動しても初回登録画面へ戻った場合は Open WebUI のバインド先、コレクションが消えた場合は Qdrant のバインド先を確認する。

本構成ではバインド先を使用しているため、docker compose down だけではデータディレクトリを削除しない。ただし、data/open-webui や data/qdrant を手動で削除すれば状態は失われる。将来、名前付きボリュームへ変更した場合の誤操作も避けるため、再作成試験では docker compose down -v を使用しない。

10.3 Open WebUI の永続領域を停止時に保存する

Open WebUI の永続領域には、利用者、会話、Knowledge の管理情報、アップロード済みファイル、設定、SQLite データベースが含まれる。公式文書も、/app/backend/data に対応する永続領域全体をファイル単位のバックアップ対象としている[37]

SQLite データベースを含むディレクトリを実行中に複製すると、データベース本体と関連ファイルの時点がそろわない可能性がある。本構成では Open WebUI を停止し、書き込みが止まった状態で永続領域、原文、構成ファイル、秘密値を一つのアーカイブへ保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
cd "$HOME/local-rag"

STAMP=$(date '+%Y%m%d-%H%M%S')
ARCHIVE="backup/open-webui-${STAMP}.tar.gz"

umask 077

docker compose stop open-webui

backup_status=0

tar -czf "$ARCHIVE" \
  data/open-webui \
  documents \
  compose.yaml \
  .env ||
  backup_status=$?

docker compose start open-webui

if [ "$backup_status" -ne 0 ]; then
  printf '%s\n' 'Open WebUI backup failed.' >&2
  exit "$backup_status"
fi

tar -tzf "$ARCHIVE" > /dev/null
shasum -a 256 "$ARCHIVE" > "${ARCHIVE}.sha256"

chmod 600 "$ARCHIVE" "${ARCHIVE}.sha256"

ls -lh "$ARCHIVE" "${ARCHIVE}.sha256"

アーカイブには .env の Qdrant API 鍵と Open WebUI の秘密鍵が含まれるため、通常の文書バックアップと同じ場所へ無条件に共有してはならない。権限を 600 にし、暗号化された保存先へ置く。

保存対象 復元する状態
data/open-webui 利用者、会話、Knowledge 管理情報、アップロード済みファイル、内部データベース
documents 再索引に使用する原文
compose.yaml サービス構成、ポート、バインド先、環境変数の割り当て
.env イメージ版、モデル名、API 鍵、秘密鍵
SHA-256 保存後または転送後にアーカイブが変化していないことの確認

このアーカイブには Qdrant のコレクションデータを含めない。Open WebUI の Knowledge 管理情報だけを復元しても、対応する Qdrant の点がなければ文書検索は成立しない。Qdrant は次節のスナップショットとして別に保存する。

10.4 Qdrant のスナップショットを作成して端末外のファイルへ保存する

Qdrant のコレクションスナップショットには、コレクション設定、点、ベクトル、付随情報、構築済み索引が含まれる。スナップショットは、再索引を行わずにコレクションを復元するために使用できる[38]

対象コレクション名を指定し、スナップショットを作成する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
cd "$HOME/local-rag"

COLLECTION='実際のコレクション名'
STAMP=$(date '+%Y%m%d-%H%M%S')
SNAPSHOT_META="backup/qdrant-snapshot-${STAMP}.json"

ENCODED_COLLECTION=$(
  python3 -c \
    'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' \
    "$COLLECTION"
)

(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -X POST \
    -H "api-key: ${QDRANT_API_KEY}" \
    "http://127.0.0.1:6333/collections/${ENCODED_COLLECTION}/snapshots" \
    > "$SNAPSHOT_META"
)

SNAPSHOT=$(
  python3 - "$SNAPSHOT_META" <<'PY'
import json
import sys

with open(sys.argv[1], encoding="utf-8") as file:
    data = json.load(file)

name = data.get("result", {}).get("name")

if not name:
    raise SystemExit("The snapshot name was not returned")

print(name)
PY
)

printf 'snapshot=%s\n' "$SNAPSHOT"

Qdrant コンテナー内のスナップショット保存先へ作成できただけでは、Qdrant の保存領域自体を失った場合に復元できない。REST API からスナップショットをダウンロードし、独立したバックアップファイルとして保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ENCODED_SNAPSHOT=$(
  python3 -c \
    'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' \
    "$SNAPSHOT"
)

SNAPSHOT_FILE="backup/${SNAPSHOT}"

(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -H "api-key: ${QDRANT_API_KEY}" \
    -o "$SNAPSHOT_FILE" \
    "http://127.0.0.1:6333/collections/${ENCODED_COLLECTION}/snapshots/${ENCODED_SNAPSHOT}"
)

test -s "$SNAPSHOT_FILE"

shasum -a 256 "$SNAPSHOT_FILE" \
  > "${SNAPSHOT_FILE}.sha256"

chmod 600 "$SNAPSHOT_FILE" "${SNAPSHOT_FILE}.sha256"

ls -lh "$SNAPSHOT_FILE" "${SNAPSHOT_FILE}.sha256"

Open WebUI が複数のコレクションを使用している場合は、必要なコレクションごとにスナップショットを作成する。コレクション単位のスナップショットにはコレクション別名が含まれないため、別名を使用している構成ではその定義も別途記録する[38]

元文書もスナップショットとは別に保持する。ベクトルを復元できても、原文がなければ埋め込みモデルやチャンク条件を変更した際に正しい再索引を行えない。

10.5 Qdrant のスナップショットを一時コレクションへ復元する

スナップショットファイルが存在することと、実際に復元できることは同義ではない。元のコレクションを削除せず、一時コレクションへアップロードして、点数と付随情報を比較する。

復元先には元のコレクションと異なる名前を使用する。復元試験はスナップショット作成時と同じ Qdrant イメージ版で行い、版の非互換による失敗をバックアップ破損と混同しない。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
RESTORE_COLLECTION="${COLLECTION}-restore-${STAMP}"

ENCODED_RESTORE_COLLECTION=$(
  python3 -c \
    'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' \
    "$RESTORE_COLLECTION"
)

(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -X POST \
    -H "api-key: ${QDRANT_API_KEY}" \
    -F "snapshot=@${SNAPSHOT_FILE}" \
    "http://127.0.0.1:6333/collections/${ENCODED_RESTORE_COLLECTION}/snapshots/upload?priority=snapshot" \
    | python3 -m json.tool
)

元コレクションと復元先コレクションの点数を取得し、同じであることを確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(
  set -a
  . ./.env
  set +a
  export COLLECTION RESTORE_COLLECTION

  python3 - <<'PY'
import json
import os
import urllib.parse
import urllib.request

base_url = "http://127.0.0.1:6333"
headers = {"api-key": os.environ["QDRANT_API_KEY"]}

def get_collection(name):
    encoded = urllib.parse.quote(name, safe="")
    request = urllib.request.Request(
        f"{base_url}/collections/{encoded}",
        headers=headers,
    )

    with urllib.request.urlopen(request, timeout=10) as response:
        return json.load(response)["result"]

source = get_collection(os.environ["COLLECTION"])
restored = get_collection(os.environ["RESTORE_COLLECTION"])

source_count = source.get("points_count")
restored_count = restored.get("points_count")

print(f"source_points={source_count}")
print(f"restored_points={restored_count}")

if source_count != restored_count:
    raise SystemExit("The restored point count does not match")
PY
)

点数だけでなく、復元先の付随情報に検証用の固有文字列が残っていることを確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -X POST \
    -H "api-key: ${QDRANT_API_KEY}" \
    -H 'Content-Type: application/json' \
    -d '{
      "limit": 100,
      "with_payload": true,
      "with_vector": false
    }' \
    "http://127.0.0.1:6333/collections/${ENCODED_RESTORE_COLLECTION}/points/scroll" \
    > /tmp/qdrant-restored-points.json
)

python3 - <<'PY'
import json
from pathlib import Path

data = json.loads(
    Path("/tmp/qdrant-restored-points.json").read_text(
        encoding="utf-8"
    )
)

targets = (
    "47821",
    "Lapis-7",
    "Rho-Blue",
)

serialized = json.dumps(data, ensure_ascii=False)

missing = [
    target
    for target in targets
    if target not in serialized
]

if missing:
    raise SystemExit(
        "Missing restored values: " + ", ".join(missing)
    )

print("restored_payload_check=True")
PY

点数と主要な付随情報が一致した後、一時コレクションを削除する。

1
2
3
4
5
6
7
8
9
10
11
12
13
(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -X DELETE \
    -H "api-key: ${QDRANT_API_KEY}" \
    "http://127.0.0.1:6333/collections/${ENCODED_RESTORE_COLLECTION}" \
    | python3 -m json.tool
)

rm -f /tmp/qdrant-restored-points.json

この試験により、スナップショットの作成、ダウンロード、アップロード、コレクション作成、点と付随情報の復元までが成立したことを確認できる。元コレクションを残したまま試験するため、通常の検索環境を破壊しない。

10.6 Open WebUI のバックアップを一時環境で読み込む

Open WebUI のアーカイブも、作成後に内容を検査する。最初に一時ディレクトリへ展開し、SQLite データベースの整合性を確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cd "$HOME/local-rag"

ARCHIVE='backup/open-webui-実際の日時.tar.gz'
RESTORE_DIR="$PWD/restore-test"

rm -rf "$RESTORE_DIR"
mkdir -p "$RESTORE_DIR"

tar -xzf "$ARCHIVE" \
  -C "$RESTORE_DIR"

python3 - "$RESTORE_DIR/data/open-webui/webui.db" <<'PY'
import sqlite3
import sys

connection = sqlite3.connect(sys.argv[1])

try:
    result = connection.execute(
        "PRAGMA integrity_check"
    ).fetchone()[0]
finally:
    connection.close()

print(f"integrity_check={result}")

if result != "ok":
    raise SystemExit("The restored database is not consistent")
PY

次に、展開したデータを本番環境とは異なる 3001 番ポートで一時的に起動する。バックアップ時に保存したイメージタグと WEBUI_SECRET_KEY を使用する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
(
  set -a
  . "$RESTORE_DIR/.env"
  set +a

  docker run -d \
    --rm \
    --name local-rag-open-webui-restore-test \
    -p 127.0.0.1:3001:8080 \
    -e "WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}" \
    -e "ENABLE_PERSISTENT_CONFIG=False" \
    -e "ENABLE_SIGNUP=False" \
    -e "ENABLE_OLLAMA_API=False" \
    -e "ENABLE_OPENAI_API=False" \
    -e "ENABLE_WEB_SEARCH=False" \
    -v "$RESTORE_DIR/data/open-webui:/app/backend/data" \
    "$OPEN_WEBUI_IMAGE"
)

count=0

until curl -fsS \
  -o /dev/null \
  http://127.0.0.1:3001/
do
  count=$((count + 1))

  if [ "$count" -ge 60 ]; then
    docker logs local-rag-open-webui-restore-test
    exit 1
  fi

  sleep 1
done

ブラウザーで http://127.0.0.1:3001/ を開き、バックアップ時の利用者でログインする。利用者情報、会話、Knowledge の名称、登録ファイルが表示されることを確認する。この一時環境では Ollama と Qdrant を無効にしているため、RAG の検索試験ではなく、Open WebUI の管理情報とファイルを復元できることが対象になる。

確認後、一時コンテナーと展開先を削除する。

1
2
docker stop local-rag-open-webui-restore-test
rm -rf "$RESTORE_DIR"

Open WebUI の一時復元試験と Qdrant の一時コレクション復元試験を組み合わせることで、管理情報と検索データをそれぞれ保存できていることを確認できる。障害時には両方を同じバックアップ時点へ戻さなければ、Knowledge の管理情報と Qdrant の点の対応がずれる可能性がある。

10.7 更新前の状態と版を記録する

Open WebUI の更新では、永続データを保存し、変更点を確認し、特定の版へ固定したうえでイメージを再作成することが推奨されている。データベース移行を伴う更新では、単に旧イメージへ戻しても旧版が更新後のデータベースを読めない可能性があるため、更新前のバックアップが必要になる[39]

更新前に、Ollama、モデル、コンテナーイメージ、展開後の Compose 構成を記録する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cd "$HOME/local-rag"

STAMP=$(date '+%Y%m%d-%H%M%S')
RECORD="backup/versions-before-${STAMP}.txt"
EXPANDED="backup/compose-expanded-before-${STAMP}.yaml"

umask 077

{
  printf '%s\n' '=== Ollama ==='
  ollama --version

  printf '\n%s\n' '=== Ollama models ==='
  ollama list

  printf '\n%s\n' '=== Docker Compose images ==='
  docker compose images
} > "$RECORD"

docker compose config \
  > "$EXPANDED"

chmod 600 "$RECORD" "$EXPANDED"

shasum -a 256 "$RECORD" "$EXPANDED"

展開後の Compose 構成には秘密値が含まれるため、共有資料として扱わない。更新前記録の目的は、使用したイメージタグ、環境変数、ポート、保存先を障害発生後に再現することである。

更新前には、少なくとも次の保存物がそろっていることを確認する。

保存物 復元対象
Open WebUI アーカイブ 利用者、会話、Knowledge 管理情報、登録ファイル、構成、秘密値
Qdrant スナップショット コレクション設定、ベクトル、付随情報、索引
原文 再索引に使用する入力文書
版記録 更新前の Ollama、モデル、Open WebUI、Qdrant の版
回帰試験結果 T0 から T5 の検索結果と回答

10.8 Open WebUI と Qdrant を別々に更新する

Open WebUI と Qdrant を同時に更新すると、文書登録や検索が失敗した場合に、どちらの変更が原因かを特定できない。最初に Open WebUI だけを更新し、回帰試験を完了してから Qdrant を更新する。

.env の OPEN_WEBUI_IMAGE を更新先の固定タグへ変更する。main や latest へ置き換えず、更新対象の版を明示する。

1
2
3
4
5
6
7
8
docker compose config --quiet
docker compose pull open-webui
docker compose up -d \
  --no-deps \
  open-webui

docker compose ps open-webui
docker compose logs --tail=200 open-webui

Open WebUI の更新後には、ブラウザーの古い画面資源が残る場合があるため、必要に応じて強制再読み込みする[39]。そのうえで、次の順に確認する。

  1. 既存の利用者でログインできること。
  2. 新規登録が無効のままであること。
  3. 既存の会話と Knowledge が表示されること。
  4. Ollama の生成用モデルが表示され、通常対話が成功すること。
  5. 既存の Argolet 文書について T1 から T5 が成立すること。
  6. 新しい検証文書を登録し、Qdrant の点数が増えること。
  7. 登録した新しい情報を検索できること。

既存 Knowledge の検索だけでは、旧データを読めることしか確認できない。新しい文書登録を含めることで、文字抽出、埋め込み生成、Qdrant への新規保存まで、更新後の書き込み経路を確認する。

Open WebUI の回帰試験が完了した後に、.env の QDRANT_IMAGE を更新先の固定タグへ変更する。

1
2
3
4
5
6
7
8
docker compose config --quiet
docker compose pull qdrant
docker compose up -d \
  --no-deps \
  qdrant

docker compose ps qdrant
docker compose logs --tail=200 qdrant

Qdrant の更新後には、ヘルスチェック、コレクション一覧、点数、ベクトル次元、付随情報を確認する。続いて T1 から T5 と、新しい文書の登録試験を再実行する。

更新対象 更新後に必要な試験
Open WebUI ログイン、会話、Knowledge、通常対話、既存検索、新規文書登録、回答忠実性
Qdrant 起動、認証、コレクション、点数、付随情報、既存検索、新規点保存
埋め込みモデル 埋め込み API、次元数、Knowledge の再索引、言い換え検索
生成用 LLM 通常対話、条件保持、回答保留、生成速度

10.9 更新失敗時はイメージと保存状態を分けて戻す

更新後に障害が起きた場合、最初に変更したイメージタグを旧版へ戻す。ただし、更新時に Open WebUI のデータベース移行や Qdrant の保存形式変更が行われた場合、旧イメージへ戻すだけでは復旧しない。更新前の保存状態も同じ時点へ戻す必要がある。

Open WebUI を更新前へ戻す場合は、現在の障害状態を別名で残し、更新前アーカイブから永続領域と構成を復元する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cd "$HOME/local-rag"

ARCHIVE='backup/open-webui-更新前の日時.tar.gz'
FAILED_STAMP=$(date '+%Y%m%d-%H%M%S')

docker compose stop open-webui

mv data/open-webui \
  "data/open-webui.failed-${FAILED_STAMP}"

tar -xzf "$ARCHIVE" \
  data/open-webui \
  documents \
  compose.yaml \
  .env

chmod 600 .env

docker compose config --quiet
docker compose up -d open-webui

復元後には、利用者、会話、Knowledge が戻っていることを確認する。Open WebUI の管理情報を更新前へ戻した場合、Qdrant も対応する更新前スナップショットへ戻す必要があるかを判断する。更新後に文書の追加、削除、再索引を行っていた場合、両者の時点は一致しない。

Qdrant の元コレクションをスナップショットから戻す場合は、Open WebUI を停止し、現在のコレクション名を確認してから実行する。次の操作は既存コレクションを削除するため、復元試験済みのスナップショットを指定する。

次の手順は対象コレクションを削除する。コレクション名、スナップショット、更新前の Open WebUI アーカイブが対応することを確認してから実行する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
COLLECTION='復元する実際のコレクション名'
SNAPSHOT_FILE='backup/復元する実際のスナップショット.snapshot'

ENCODED_COLLECTION=$(
  python3 -c \
    'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' \
    "$COLLECTION"
)

docker compose stop open-webui

(
  set -a
  . ./.env
  set +a

  curl -fsS \
    -X DELETE \
    -H "api-key: ${QDRANT_API_KEY}" \
    "http://127.0.0.1:6333/collections/${ENCODED_COLLECTION}" \
    | python3 -m json.tool

  curl -fsS \
    -X POST \
    -H "api-key: ${QDRANT_API_KEY}" \
    -F "snapshot=@${SNAPSHOT_FILE}" \
    "http://127.0.0.1:6333/collections/${ENCODED_COLLECTION}/snapshots/upload?priority=snapshot" \
    | python3 -m json.tool
)

docker compose start open-webui

復元後には点数、ベクトル次元、付随情報を確認し、T1 から T5 を再実行する。画面が起動したことや点数が戻ったことだけでは、Open WebUI の Knowledge と Qdrant のコレクションが再び対応したとは判断できない。

10.10 構成値の保存場所を一つずつ固定する

Open WebUI の一部の環境変数は、既定では ConfigVar として内部データベースへ保存され、後の起動では外部環境変数より優先される。本構成では ENABLE_PERSISTENT_CONFIG=False としているため、Compose から渡した環境変数を毎回読み込み、管理画面で変更した ConfigVar は再起動後に保存されない[20]

元稿のように、初期構成を Compose で指定した後、管理画面の変更を別の設定源として併用すると、画面上の値と構成ファイルのどちらが正しいか判別できなくなる。本構成では、接続先、検索設定、文書分割条件を compose.yaml と .env へ集約する。

情報 正とする保存場所 更新方法
サービス構成、ポート、保存先 compose.yaml ファイルを変更し、構文確認後にコンテナーを再作成する。
イメージ版、モデル名、API 鍵、秘密鍵 .env 権限 600 のまま変更し、対象コンテナーを再作成する。
Ollama、Qdrant、検索、分割の設定 compose.yaml の環境変数 管理画面だけで変更せず、構成ファイルを変更する。
利用者、会話、Knowledge 管理情報 data/open-webui Open WebUI の通常操作によって更新する。
ベクトル、付随情報、索引 data/qdrant 文書登録、削除、再索引によって更新する。
再索引用の原文 documents 登録した版とハッシュ値を対応させて管理する。
復元点 backup のアーカイブとスナップショット 更新前および重要な文書変更前に新しく作成する。

管理画面で設定値を試験的に変更できる場合でも、再起動後に残るとは扱わない。採用する値が決まった時点で compose.yaml または .env へ反映し、コンテナーを再作成した後に実行中の環境変数を確認する。

10.11 バックアップに含まれない実行資源を確認する

Open WebUI のアーカイブと Qdrant のスナップショットだけでは、MacBook Pro を失った場合の完全なオフライン復元にはならない。Ollama のモデルデータと Docker イメージは、この章で作成した Open WebUI アーカイブに含まれていない。

資源 通常の復元方法 ネットワーク切断下で必要な保存
Open WebUI データ 停止時アーカイブから復元する。 アーカイブを暗号化して別媒体へ保存する。
Qdrant データ スナップショットから復元する。 各コレクションのスナップショットを別媒体へ保存する。
原文 documents から再登録する。 原文とハッシュ値を別媒体へ保存する。
Docker イメージ 固定タグを再取得する。 docker image save でイメージ本体を保存する。
Ollama モデル モデルタグを指定して再取得する。 Ollama のモデル保存領域を別途保全する。

Docker イメージも端末内へ保存する場合は、固定したイメージ名を一つのアーカイブへ出力する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(
  set -a
  . ./.env
  set +a

  STAMP=$(date '+%Y%m%d-%H%M%S')
  IMAGE_ARCHIVE="backup/local-rag-images-${STAMP}.tar.gz"

  umask 077

  docker image save \
    "$OPEN_WEBUI_IMAGE" \
    "$QDRANT_IMAGE" |
    gzip > "$IMAGE_ARCHIVE"

  shasum -a 256 "$IMAGE_ARCHIVE" \
    > "${IMAGE_ARCHIVE}.sha256"

  chmod 600 \
    "$IMAGE_ARCHIVE" \
    "${IMAGE_ARCHIVE}.sha256"
)

復元時には次のように読み込む。

1
2
3
gzip -dc \
  backup/local-rag-images-実際の日時.tar.gz |
  docker image load

モデルやイメージを再取得できる通常運用では、容量の大きい実行資源を毎回保存する必要はない。一方、ネットワーク切断後も復旧できることを要件とするなら、データだけでなく、実行に必要なモデルとイメージも復旧対象へ含める。

10.12 運用工程の完了条件を固定する

確認工程 完了条件
コンテナー再起動 利用者、会話、Knowledge、コレクション、検索結果が残る。
Docker Desktop 再起動 バインド先から状態を読み直し、同じ対照試験が成立する。
MacBook Pro 再起動 Ollama と Docker Desktop の起動後、同じ処理経路を再現できる。
コンテナー再作成 docker compose down 後の再作成でも状態が維持される。
Open WebUI 保存 停止時アーカイブを作成し、SQLite の整合性と一時起動を確認できる。
Qdrant 保存 スナップショットをダウンロードし、一時コレクションへ復元できる。
更新 Open WebUI と Qdrant を別々に更新し、各段階で回帰試験を通過する。
ロールバック 旧イメージだけでなく、対応する更新前データへ戻せる。
設定管理 Compose と .env を設定の正とし、再起動後の値を再現できる。

ローカル RAG の運用条件は、構築直後に一度だけ正答することではない。MacBook Pro、Docker Desktop、コンテナーを再起動しても状態が残り、コンテナーを削除しても構成から再作成でき、更新前の Open WebUI と Qdrant を独立した復元物として保存できる必要がある。

さらに、保存物は実際に一時環境へ復元し、利用者情報、Knowledge、点数、付随情報を読み出せなければならない。バックアップファイルが存在するだけでは、復元経路が成立したことにはならない。再起動、再作成、更新、復元を同じ対照試験で検証できる状態になって初めて、この構成は一時的な実験環境ではなく、継続して運用可能なローカル RAG となる。


11. ローカル RAG は観測可能な処理経路として完成する

11.1 三つのサービスが起動しただけでは完成しない

Ollama、Open WebUI、Qdrant を同じ MacBook Pro 上で起動し、ブラウザーから生成用 LLM と対話できても、ローカル RAG が成立したとはいえない。通常対話は Ollama だけで完結するため、文書抽出、埋め込み生成、Qdrant への保存、類似検索のいずれかが停止していても、自然な回答が返る場合がある。

本構成で確認したのは、三つの製品が同時に動くことではない。文書を検索可能なチャンクへ変換し、embeddinggemma で埋め込みベクトルを生成し、Qdrant へ原文断片とともに保存し、質問時に関連チャンクを取得し、その根拠を qwen3.5:9b-q4_K_M へ渡す処理経路である。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
文書登録

原文
  ↓
Open WebUI が文字を抽出
  ↓
見出しと文字数に基づいてチャンク分割
  ↓
embeddinggemma が文書ベクトルを生成
  ↓
Qdrant がベクトルと原文断片を保存


質問応答

質問
  ↓
Open WebUI が embeddinggemma を呼び出す
  ↓
Qdrant が質問に近いチャンクを返す
  ↓
Open WebUI が質問と取得根拠を組み立てる
  ↓
qwen3.5:9b-q4_K_M が回答を生成する
  ↓
出典と回答を個別に評価する

この経路が最後まで成立していれば、生成用 LLM が学習時に知らなかった Argolet の本番保守ポートを、Knowledge 接続時にだけ 47821 と回答できる。Lapis-7 手順では接続キューを停止してから書き込み処理を停止し、復旧条件では Rho-Blue が 15 分間連続して 0.20 未満であることを欠落なく説明できる。文書に記載していない開発責任者名については、人物名を補わず、確認できないと回答する。

これらの対照試験によって、単に正しい文字列が出力されたのではなく、外部文書が質問時の根拠として使われたことを確認できる。Knowledge 未接続時の回答、接続時に取得されたチャンク、出典、最終回答を分けて記録することで、偶然の正答や会話履歴からの再利用を RAG の成功から除外できる。

11.2 誤答を処理段階へ戻せることが実用条件になる

観測可能な構成では、最終回答が誤った場合に、処理経路を上流へたどって原因を限定できる。Qdrant の付随情報に必要な文字列がなければ、生成用 LLM を変更する前に文字抽出を調べる。文字列は存在するが主語と値が別のチャンクへ分かれていれば、チャンクサイズ、重複量、原文構造を調整する。

正しい本文が保存されていても検索上位へ入らない場合は、埋め込みモデル、質問表現、取得件数を確認する。必要なチャンクが取得されているのに値、条件、工程順序を誤る場合は、生成用 LLM、競合する取得文脈、RAG テンプレートを確認する。処理段階を分けることで、誤答を一括して「モデル性能が低い」と処理せずに済む。

観察した失敗 最初に確認する段階 確認内容
必要な固有文字列が保存されていない 文字抽出 原文の文字層、読み順、抽出後の本文を確認する。
名称、値、条件が別々の点に分かれている チャンク分割 原文構造、チャンクサイズ、重複量を確認する。
必要なチャンクが検索候補へ現れない 埋め込みと検索 埋め込みモデル、質問の言い換え、取得件数を確認する。
正しいチャンクを取得しているのに誤答する 生成 競合する文脈、回答規則、生成用 LLM を確認する。
文書にない人物名や数値を追加する 根拠忠実性 情報不足時の回答保留と根拠限定の指示を確認する。
再起動後に Knowledge または点が消える 永続化 Open WebUI と Qdrant のバインド先を確認する。
更新後に既存文書だけ検索できない 互換性と再索引 埋め込みモデル、チャンク条件、コレクション設定を確認する。

この切り分けができない構成では、回答品質を改善するたびに、生成用 LLM、埋め込みモデル、チャンク設定を無作為に変更することになる。改善しても原因を説明できず、別の文書や更新後の環境で再現できない。処理境界ごとに確認可能な事実を置くことが、ローカル RAG を調整可能な技術基盤へ変える。

11.3 Qdrant を分離する意味は中間状態を確認できることにある

Qdrant を独立したコンテナーとして配置した理由は、将来の文書数増加に備えるためだけではない。Open WebUI の画面の外側から、どのコレクションに何件の点が保存され、どのベクトル次元と距離尺度が使われ、各点の付随情報にどの文書断片が残っているかを確認できるためである。

生成用 LLM の回答だけを観察する構成では、検索された根拠が誤っていたのか、正しい根拠を生成時に取り違えたのかを判別できない。Qdrant のコレクション、点数、ベクトル設定、付随情報を確認できれば、文書登録から類似検索までの中間状態を生成処理から分離して評価できる。

この分離は、埋め込みモデルを変更した場合にも意味を持つ。新旧モデルの出力次元が同じであっても意味空間は互換ではないため、既存文書を再索引しなければならない。Qdrant の保存状態を観察できれば、旧モデルで作られた点が残っていないか、再索引後に点数と付随情報が変化したかを確認できる。

11.4 ローカル完結と運用可能性は別々に確認する

本構成では、生成と埋め込みを macOS 上の Ollama、文書管理を Open WebUI、ベクトル保存と検索を Qdrant が担当する。生成、埋め込み、検索、文書抽出の接続先を MacBook Pro と Docker Desktop 内へ限定することで、質問と登録文書を外部の生成 API や埋め込み APIへ送らずに回答処理を実行できる。

ただし、回答処理がローカルで完結することと、運用可能な状態が続くことは別の条件である。コンテナーの再起動、Docker Desktop の再起動、MacBook Pro の再起動、コンテナーの削除と再作成を行った後にも、利用者、Knowledge、会話、Qdrant の点、検索結果が残る必要がある。

バックアップも、ファイルを作成しただけでは完了しない。Open WebUI の停止時アーカイブは一時環境で起動し、SQLite の整合性、利用者情報、Knowledge を確認する。Qdrant のスナップショットは一時コレクションへ復元し、点数と Argolet の固有情報が戻ることを確認する。復元試験を通過して初めて、保存物を障害時の復旧手段として扱える。

成立条件 確認した内容
ローカル処理 生成、埋め込み、検索、文書抽出の接続先が MacBook Pro 内にある。
観測可能性 文字抽出、チャンク、ベクトル、取得根拠、最終回答を個別に確認できる。
対照可能性 Knowledge 未接続時と接続時の回答差を同じ質問で比較できる。
根拠忠実性 文書にある条件を保持し、文書にない情報を補わない。
永続性 再起動とコンテナー再作成後にも利用者、文書、ベクトルが残る。
復元可能性 Open WebUI と Qdrant の保存物を一時環境へ復元できる。
更新可能性 Open WebUI と Qdrant を別々に更新し、同じ回帰試験で確認できる。

11.5 完成したのはモデルではなく根拠を制御する経路である

この構成によって、生成用 LLM 自体が Argolet の運用規則を学習したわけではない。文書は Open WebUI によってチャンクへ分割され、embeddinggemma のベクトルとして Qdrant へ保存される。質問時には必要な断片だけが検索され、qwen3.5:9b-q4_K_M は、その場で渡された根拠から回答を構成する。

文書を更新した場合は再登録または再索引によって根拠を差し替えられ、埋め込みモデルを変更した場合は検索空間を作り直せる。生成用 LLM を変更しても、同じ検索結果を与えて回答忠実性と速度を比較できる。モデル、検索、文書を分離したことで、知識の更新をモデル再学習ではなく、管理可能な文書と索引の更新として扱える。

MacBook Pro 上のローカル RAG が実用段階へ達したと判断できるのは、回答が返った時点ではない。文書のどの断片が保存され、なぜその断片が検索され、生成用 LLM がどの根拠から回答し、障害や更新後に同じ状態をどう復元するかを説明できる時点である。完成したのは三つのサービスを並べた環境ではなく、利用者が根拠、失敗位置、設定、復旧手順を制御できる処理経路である。

次稿では、この推論・検索基盤を MCP、VS Code、Continue、Git の作業経路へ接続する。次の段階で問われるのは、ローカル RAG が単独で正答できるかではない。開発作業のどの時点で文書検索を実行し、どの操作をモデルへ許可し、どの変更を人間が確認し、どの状態からロールバックできるようにするかである。


参考文献

  1. id774, MacBook Pro でローカル LLM を実用化する条件(2026-07-17). https://blog.id774.net/entry/2026/07/17/5102/
  2. id774, 生成 AI の競争軸は、モデルから業務実装へ移る(2026-06-25). https://blog.id774.net/entry/2026/06/25/4922/
  3. Patrick Lewis, Ethan Perez, Aleksandra Piktus, Fabio Petroni, Vladimir Karpukhin, Naman Goyal, Heinrich Küttler, Mike Lewis, Wen-tau Yih, Tim Rocktäschel, Sebastian Riedel, Douwe Kiela, Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, Advances in Neural Information Processing Systems 33(2020). https://proceedings.neurips.cc/paper/2020/hash/6b493230205f780e1bc26945df7481e5-Abstract.html
  4. Shahul Es, Jithin James, Luis Espinosa Anke, Steven Schockaert, RAGAs: Automated Evaluation of Retrieval Augmented Generation, Proceedings of the 18th Conference of the European Chapter of the Association for Computational Linguistics: System Demonstrations(2024). https://aclanthology.org/2024.eacl-demo.16/
  5. Apple Developer, Explore the new system architecture of Apple silicon Macs, WWDC20(2020). https://developer.apple.com/videos/play/wwdc2020/10686/
  6. Ollama, macOS(n.d., 2026-07-16 参照). https://docs.ollama.com/macos
  7. Docker, Install Docker Desktop on Mac(n.d., 2026-07-16 参照). https://docs.docker.com/desktop/setup/install/mac-install/
  8. Docker, Compose file reference(n.d., 2026-07-16 参照). https://docs.docker.com/reference/compose-file/
  9. Docker, Volumes(n.d., 2026-07-16 参照). https://docs.docker.com/engine/storage/volumes/
  10. Docker, Explore networking how-tos on Docker Desktop(n.d., 2026-07-16 参照). https://docs.docker.com/desktop/features/networking/networking-how-tos/
  11. Ollama, Introduction, API Reference(n.d., 2026-07-16 参照). https://docs.ollama.com/api/introduction
  12. Ollama, qwen3.5:9b(n.d., 2026-07-16 参照). https://ollama.com/library/qwen3.5:9b
  13. Ollama, Usage, API Reference(n.d., 2026-07-16 参照). https://docs.ollama.com/api/usage
  14. Ollama, Embeddings(n.d., 2026-07-16 参照). https://docs.ollama.com/capabilities/embeddings
  15. Ollama, Generate embeddings, API Reference(n.d., 2026-07-16 参照). https://docs.ollama.com/api/embed
  16. Nils Reimers, Iryna Gurevych, Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks, Proceedings of EMNLP-IJCNLP(2019). https://aclanthology.org/D19-1410/
  17. Ollama, Context length(n.d., 2026-07-16 参照). https://docs.ollama.com/context-length
  18. Qdrant, Security(n.d., 2026-07-16 参照). https://qdrant.tech/documentation/security/
  19. Open WebUI, Quick Start(n.d., 2026-07-16 参照). https://docs.openwebui.com/getting-started/quick-start/
  20. Open WebUI, Environment Variable Configuration(n.d., 2026-07-16 参照). https://docs.openwebui.com/reference/env-configuration/
  21. Qdrant, Local Quickstart(n.d., 2026-07-16 参照). https://qdrant.tech/documentation/quickstart/
  22. Docker, Control startup and shutdown order in Compose(n.d., 2026-07-16 参照). https://docs.docker.com/compose/how-tos/startup-order/
  23. Qdrant, Qdrant Web UI(n.d., 2026-07-16 参照). https://qdrant.tech/documentation/web-ui/
  24. id774, AI に自身の文脈を持ち運ぶ(2026-05-04). https://blog.id774.net/entry/2026/05/04/4675/
  25. Open WebUI, Knowledge(n.d., 2026-07-16 参照). https://docs.openwebui.com/features/workspace/knowledge/
  26. Open WebUI, Retrieval Augmented Generation (RAG)(n.d., 2026-07-16 参照). https://docs.openwebui.com/features/chat-conversations/rag/
  27. Nelson F. Liu, Kevin Lin, John Hewitt, Ashwin Paranjape, Michele Bevilacqua, Fabio Petroni, Percy Liang, Lost in the Middle: How Language Models Use Long Contexts, Transactions of the Association for Computational Linguistics 12(2024). https://aclanthology.org/2024.tacl-1.9/
  28. Open WebUI, Troubleshooting RAG(n.d., 2026-07-16 参照). https://docs.openwebui.com/troubleshooting/rag/
  29. Qdrant, Collections(n.d., 2026-07-16 参照). https://qdrant.tech/documentation/manage-data/collections/
  30. Qdrant, Points(n.d., 2026-07-16 参照). https://qdrant.tech/documentation/manage-data/points/
  31. Qdrant, Indexing Vectors in HNSW, Collections(n.d., 2026-07-16 参照). https://qdrant.tech/documentation/manage-data/collections/#indexing-vectors-in-hnsw
  32. Open WebUI, Changing the Embedding Model, Retrieval Augmented Generation (RAG)(n.d., 2026-07-16 参照). https://docs.openwebui.com/features/chat-conversations/rag/#changing-the-embedding-model
  33. Qdrant, Search(n.d., 2026-07-16 参照). https://qdrant.tech/documentation/search/search/
  34. Open WebUI, RAG Template Customization and Chunking Configuration, Retrieval Augmented Generation (RAG)(n.d., 2026-07-16 参照). https://docs.openwebui.com/features/chat-conversations/rag/#rag-template-customization
  35. Ollama, Modelfile Reference(n.d., 2026-07-16 参照). https://docs.ollama.com/modelfile
  36. Docker, docker compose down(n.d., 2026-07-16 参照). https://docs.docker.com/reference/cli/docker/compose/down/
  37. Open WebUI, Backups(n.d., 2026-07-16 参照). https://docs.openwebui.com/tutorials/maintenance/backups/
  38. Qdrant, Snapshots(n.d., 2026-07-16 参照). https://qdrant.tech/documentation/snapshots/
  39. Open WebUI, Updating Open WebUI(n.d., 2026-07-16 参照). https://docs.openwebui.com/getting-started/updating/