Ubuntu 24.04 + RTX 4060 TiでローカルAI開発環境を作り直す r004 ― Ollama / Open WebUI / Serena / Docling / Qdrant / FastAPI
はじめに
社内の過去資料をAIで検索したり、手元のコードをAIに読ませて修正を手伝ってもらったりする環境を、できるだけローカル側でまとめて動かしたいと考えていました。
建設コンサルタントの仕事では、AIに扱わせたいものが大きく2種類あります。
ひとつは、Python、TypeScript、Rust、Fortranなどで作っている解析・GIS・Webアプリのソースコードです。こちらは、単にファイル全文をLLMへ渡すより、シンボルや参照関係を追いながらコードを読めた方が扱いやすくなります。
もうひとつは、過去の業務報告書や技術資料です。PDFの中には、本文だけでなく表、図、数式、段組み、スキャン画像などが含まれます。こちらは「それらしい回答が返る」だけでは不十分で、どの資料の何ページを根拠にしたのかを後から確認できる必要があります。
そこで今回は、1台のUbuntuマシンに次の2系統をまとめます。
- コードを扱う開発支援: Ollama + Open WebUI + Serena
- PDFを扱うナレッジ検索: Docling + FastAPI + Celery + DuckDB + Qdrant
入口はOpen WebUIへ寄せますが、内部処理は分けます。
最初に作った構成でも一通り動かせる形にはなっていました。ただ、実装を見直すと、PDF解析をHTTPリクエストの中で最後まで実行していたり、Embeddingの次元数を固定していたり、コンテナ起動時に毎回Python環境を作ったりと、長く使うには気になる箇所がありました。
今回はそのあたりを一度整理し、再起動しやすいこと、処理途中で失敗しても追跡できること、更新時に壊れたことへ気付きやすいことを優先して作り直します。
この記事では、この版を r004 としています。
プロジェクト名は次のとおりです。
local_ai_platform_r004
参考のために、ZIPファイルを置いておきますので、ダウンロードして活用してください。
2026年8月18日時点の構成です。Ollama、Open WebUI、Serena、Qdrantなどは更新が速いため、実際に導入するときは各公式ドキュメントとリリースノートも確認してください。
全体の操作イメージです。
これまでの流れ
もともとは、Ubuntu上でOllamaとOpen WebUIを動かし、社内のPDFを外へ出さずに検索できるRAG環境を試すところから始めました。
RAG側では、QdrantへEmbeddingを保存し、FastAPIでPDF登録や検索APIを作り、Doclingで報告書をMarkdownやJSONへ変換する構成を考えていました。一方で、コード開発側ではSerenaを使い、LSPを通してプロジェクトのシンボルや参照関係を追える環境も試していました。
この2つは別々に動かしてもよいのですが、実際の作業では行き来することが多くなります。
例えば、過去の報告書から計算条件を探し、その条件を使う解析コードを確認する。あるいは、既存のPython処理をSerenaで追いながら、関連する設計根拠を過去資料から検索する、といった使い方です。
そこで今回は、Open WebUIを共通の入口にし、コードはSerena、文書はRAG APIへ渡す形へまとめ直しました。
ただし、1つの巨大なAIコンテナへ全部を詰め込む構成にはしていません。更新頻度も、障害の出方も、必要なデータも違うため、サービスは分けたままDocker Composeでまとめます。
「ローカルAI」の範囲
この記事でいうローカルAIは、推論処理、PDF原本、抽出結果、ベクトルDB、処理台帳をUbuntuマシンまたは社内LAN内へ置く構成を指しています。
初回から完全なオフライン環境を作るわけではありません。Docker image、Ollamaのモデル、Python package、Serenaのsourceなどを取得する段階では外部ネットワークを使います。完全閉域へ持ち込む場合は、image、モデル、Doclingが使うモデル資産などを別途持ち込めるように整理する必要があります。
まずはLAN内で一通り動作させ、外へ通信しなくても日常の検索と推論を行える部分を増やしていく方が、問題を切り分けやすいと考えています。
今回使うマシン
検証機は次の構成です。
OS : Ubuntu 24.04 Desktop
CPU : Intel Core i5-11400F
Memory : 64GB
Storage : 4TB
GPU : NVIDIA GeForce RTX 4060 Ti 16GB
Client : Windows 11
Windows 11側には重いAI環境を置かず、基本的にはブラウザからUbuntu上のOpen WebUIへ接続します。
GPUはまずOllamaへ使います。PDF解析側のDoclingは、最初はCPU実行として分けておきます。同じGPUへ何でも載せるより、まずどの処理がどの程度時間を使うかを確認してから調整した方が切り分けやすいためです。
なぜローカル側へ寄せるのか
クラウドのLLMや外部APIは便利ですが、業務報告書を扱う場合は「アップロードしてよい資料か」を先に整理する必要があります。
過去の報告書には、業務名、発注者、地形・地質条件、設計値、検討経緯など、外部へそのまま出したくない情報が含まれることがあります。そこで今回の検証では、PDF原本、抽出したMarkdownやJSON、Embedding、検索DBをUbuntuマシン側へ置きます。
ただし、ローカルで動いているから自動的に安全になるわけではありません。
LANへ開けるポート、利用者認証、APIキー、バックアップ、ログ、資料ごとの権限などは別に考える必要があります。今回も最初からすべてのサービスをLAN公開せず、Windowsから必要なOpen WebUIだけを公開する構成にしています。
本番運用まで一気に作るのではなく、まずは小さく動かして、PDFの読み取り、検索結果、引用ページが安定するかを確認します。
開発支援とRAGを1つにまとめる
今回やりたいことを図にすると、次のようになります。
ここで大事なのは、Open WebUIへ全部の責務を持たせないことです。
Open WebUIのKnowledgeは、少数の資料を登録してRAGをすぐ試すには便利です。一方、実務向けの検証では、次の情報も残したくなります。
- PDF原本のSHA-256
- 登録日時
- 処理状態
- 抽出したMarkdown / JSON
- 使用したEmbeddingモデル
- 文書ID
- ページ番号
- 章・節
- 年度
- 発注者
- 分野
- 機密区分
- 再処理したかどうか
このため、Open WebUI内蔵のKnowledgeとは別に、監査しやすい独自RAG APIを残します。
「すぐ試す経路」と「後から追跡できる経路」を分けた形です。
最初の案から見直したところ
元の構成から大きくサービスを入れ替えたわけではありません。主に、運用していると問題になりそうな境界を直しました。
| 項目 | 最初の案 | r004 |
|---|---|---|
| Serena MCP | SSE /sse
|
Streamable HTTP /mcp
|
| Micromamba | コンテナ起動時にinstall | Docker build時にinstall |
| Ollama Embedding |
/api/embeddings + prompt
|
/api/embed + input
|
| Embedding次元 |
1536固定 |
実際の応答から取得 |
| PDF解析 | FastAPIの/upload内で同期処理 |
Celery Workerへ分離 |
| チャンク | Markdownを空行で分割 | Doclingのchunkerを利用 |
| 引用情報 | chunk index中心 | 資料名・page・sectionを保持 |
| PDF保存 | アップロード名をそのまま利用 | SHA-256由来の文書ID |
| ポート公開 | 複数サービスを公開 | 原則Open WebUIだけ公開 |
| bind mount | コンテナ側UID任せ | Ubuntu側UID/GIDへ合わせる |
| 再処理 | 古いvectorが残る可能性 | 文書単位でQdrantから削除後に再登録 |
| 検索 | ベクトル検索のみ | 年度・分野・発注者・機密区分で絞り込み |
| バージョン |
latest中心 |
主要サービスをversion固定 |
特に影響が大きかったのは、PDF解析をHTTP処理から分離したことです。
PDF解析をHTTPリクエストの中で終わらせない
PDFが数ページなら、アップロードを受け取ったFastAPIがそのままDoclingを実行しても動きます。
しかし、実際の報告書は100ページを超えることがあります。さらにOCRや表解析が入ると、処理時間はPDFごとにかなり変わります。
HTTPリクエストの中で、
PDF保存
↓
Docling解析
↓
チャンク分割
↓
Embedding
↓
Qdrant登録
まで待つ構成にすると、途中でブラウザやProxyがタイムアウトしたときに扱いにくくなります。
そこでr004では、FastAPIは受付までにします。
これなら、利用者はdocument_idを使って処理状態を確認できます。
Redisが止まっていてキューへ登録できなかった場合も、DuckDBの状態をerrorへ変更します。いつまでもqueuedに見える状態にはしません。
DuckDBとQdrantを両方使う理由
Qdrantだけでも、ベクトルとpayloadは保存できます。
それでもDuckDBを別に残したのは、役割を分けたかったためです。
Qdrant
検索に使うデータを持ちます。
vector
text
document_id
filename
page_start
page_end
section
fiscal_year
client
category
confidentiality
DuckDB
文書処理の台帳として使います。
document_id
SHA-256
元ファイル名
保存先
ファイルサイズ
登録日時
処理状態
エラー内容
Embeddingモデル
chunk情報
「検索DB」と「処理履歴」を分けておくと、Embeddingモデルを変更したときや、Doclingの抽出方法を変えたときに追いやすくなります。
Doclingの構造をできるだけ残す
RAGでは文章を小さく分けてEmbeddingします。
最初の試作ではMarkdownを空行で区切っていましたが、これだと章や表の途中で分かれる場合があります。
r004ではDoclingのchunkerを使います。
from docling.chunking import HierarchicalChunker, HybridChunker
通常はHybridChunkerを使い、利用できない環境ではHierarchicalChunkerへフォールバックします。
各チャンクでは、本文だけでなく次も残します。
page_start
page_end
section
検索結果を人が確認するとき、本文だけ返ってきても原本を探せません。
最終的には、
○○業務報告書
p.42
4.3 計画高水流量の設定
まで戻れる状態を目標にしています。
OllamaのEmbedding次元は固定しない
最初の実装では、Qdrantのvector sizeをコードへ固定していました。
VECTOR_SIZE = 1536
この方式は、Embeddingモデルを変更したときに危険です。
r004ではOllamaの/api/embedへ実際に問い合わせ、返ってきたベクトルの長さを使ってQdrant collectionを作ります。
POST /api/embed
model : qwen3-embedding:0.6b
input : [...]
既存collectionと次元が合わない場合は、そのまま書き込まずエラーにします。
モデル変更時に「何となく検索できている」状態を避けるためです。
今回の初期モデルは次にしています。
Embedding : qwen3-embedding:0.6b
回答生成 : qwen3:8b
RTX 4060 Ti 16GBでまず全体を動かすための出発点です。検索精度や速度を見て、あとからモデルを比較します。
SerenaはStreamable HTTPで接続する
Serenaはコードをシンボル単位で検索・編集するために使います。
r004ではlegacy SSEではなく、Streamable HTTPを使います。
http://serena:9121/mcp
Compose側は次の形です。
serena:
image: local/serena:v1.7.0
build:
context: https://github.com/oraios/serena.git#v1.7.0
dockerfile: Dockerfile
target: production
command:
- serena
- start-mcp-server
- --transport
- streamable-http
- --port
- "9121"
- --host
- 0.0.0.0
- --context
- desktop-app
volumes:
- ./workspace/projects:/workspaces/projects
expose:
- "9121"
Open WebUIからは、Admin SettingsのExternal Toolsへ次を登録します。
Type: MCP (Streamable HTTP)
URL : http://serena:9121/mcp
ここで少し注意があります。
SerenaのHTTPサーバーはstatefulで、1つのサーバーで同時にactiveにできるprojectは1つです。
1人がprojectを切り替えながら使う分には扱いやすいですが、複数のcoding agentが別々のprojectを同時編集する場合は、共有Serenaを1個だけ置く構成をそのまま使わない方がよさそうです。
その場合は、agentごとのstdio起動か、Serena instance自体を分けます。
ディレクトリ構成
今回の実装は次のようにしています。
local_ai_platform_r004/
├── compose.yml
├── compose.lan-api.yml
├── environment.yml
├── .env.example
├── AGENTS.md
├── agent.md
├── README.md
├── serena-config/
│ └── serena_config.yml
├── services/
│ └── rag/
│ ├── Dockerfile
│ └── app/
│ ├── api.py
│ ├── clients.py
│ ├── core.py
│ ├── pipeline.py
│ ├── settings.py
│ ├── storage.py
│ ├── tasks.py
│ └── worker.py
├── scripts/
│ ├── init_env.sh
│ ├── check_env.sh
│ ├── preflight.sh
│ ├── host_test.sh
│ ├── validate_compose.py
│ ├── make_test_pdf.py
│ ├── smoke.sh
│ ├── integration_test.sh
│ └── docker_qa.sh
├── tests/
├── tests_docker/
├── docs/
│ ├── review_report.md
│ ├── testing.md
│ └── validation_result.md
├── workspace/
│ └── projects/
└── data/
├── documents/original/
├── extracted/markdown/
├── extracted/json/
└── metadata/
Serenaへ読ませたいGit projectは、workspace/projects/の下へ置きます。
PDF原本やRAGの処理結果はdata/側へ置き、コード用workspaceとは分けています。
Docker Compose
主要サービスは次の7つです。
ollama
open-webui
serena
qdrant
redis
rag-api
rag-worker
r004では主要イメージをlatestへせず、検証した版へ固定しています。
services:
ollama:
image: ollama/ollama:0.32.14
qdrant:
image: qdrant/qdrant:v1.19.0
redis:
image: redis:7.4-alpine
open-webui:
image: ghcr.io/open-webui/open-webui:v0.11.0
Serenaはv1.7.0のGit tagからbuildします。
バージョンを固定する理由は、最新版を避けたいからではありません。
更新したときに「どこから挙動が変わったか」を追えるようにするためです。更新は、Composeを書き換えたあとに後述のテストを通してから採用します。
Micromambaは起動時ではなくbuild時に使う
最初の構成では、コンテナを起動するたびにmicromamba installしていました。
これは試作では便利ですが、再起動のたびに依存解決が走ります。外部リポジトリの状態にも影響されます。
r004ではDockerfileへ移しました。
FROM mambaorg/micromamba:2.8.1 AS runtime
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libgl1 libglib2.0-0 libgomp1 \
&& rm -rf /var/lib/apt/lists/*
USER $MAMBA_USER
COPY --chown=$MAMBA_USER:$MAMBA_USER environment.yml /tmp/environment.yml
RUN micromamba install --yes --name base --file /tmp/environment.yml \
&& micromamba clean --all --yes
コンテナ起動時は、すでに作られた環境でFastAPIやCeleryを実行するだけにします。
Ubuntu側のUID/GIDを合わせる
rag-apiとrag-workerは、ホストの./dataをbind mountします。
volumes:
- ./data:/data
ここでコンテナ内ユーザーとUbuntu側の所有者がずれると、PDFやDuckDBへ書き込めずPermission deniedになることがあります。
そのため、初回に次を実行します。
./scripts/init_env.sh
このスクリプトで、
id -u -> LOCAL_UID
id -g -> LOCAL_GID
を.envへ保存します。
Composeでは、
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
として実行します。
同時にAPI_KEYとWEBUI_SECRET_KEYも生成し、.envのpermissionを0600へ変更します。
ネットワークに公開するサービスを絞る
通常のcompose.ymlでホストへ公開するのはOpen WebUIだけです。
ports:
- "${OPEN_WEBUI_BIND:-127.0.0.1}:${OPEN_WEBUI_PORT:-3000}:8080"
Ollama、Qdrant、Redis、Serena、RAG APIはexposeだけにし、Docker networkの中で通信します。
初期値も、
OPEN_WEBUI_BIND=127.0.0.1
にしています。
Windows 11から使うときだけUbuntu側のLAN IPを指定します。
hostname -I
例えばUbuntuが192.168.1.50なら、
OPEN_WEBUI_BIND=192.168.1.50
とします。
0.0.0.0でも動きますが、必要のないinterfaceまで待ち受ける必要はないので、利用するLAN IPへ固定しています。
FastAPIのSwagger UIをWindowsから直接確認したい場合だけ、overrideを重ねます。
docker compose \
-f compose.yml \
-f compose.lan-api.yml \
up -d
通常運用へ戻すときは、RAG APIを直接公開しない構成へ戻します。
初回起動
まず環境ファイルを作ります。
cd ~/local_ai_platform_r004
./scripts/init_env.sh
Windowsから利用する場合は.envのOPEN_WEBUI_BINDをUbuntuのLAN IPへ変更します。
続いて事前検査を行います。
./scripts/check_env.sh
./scripts/preflight.sh
問題がなければbuildします。
docker compose build
docker compose up -d
Embeddingモデルを取得します。
docker compose exec ollama \
ollama pull qwen3-embedding:0.6b
回答用モデルも用意します。
docker compose exec ollama \
ollama pull qwen3:8b
GPUへ載っているかは、
docker compose exec ollama ollama ps
で確認します。
単にOllamaが応答するだけでなく、GPUが使われていることまで確認しておきます。
Windows 11からOpen WebUIへ接続する
ブラウザから次へアクセスします。
http://<UbuntuのIP>:3000
Open WebUI側のOllamaとQdrantはComposeで設定してあります。
RAG用は次の値を入れています。
RAG_EMBEDDING_ENGINE: ollama
RAG_OLLAMA_BASE_URL: http://ollama:11434
RAG_EMBEDDING_MODEL: qwen3-embedding:0.6b
RAG_FILE_MAX_SIZE: 200
ENABLE_RAG_HYBRID_SEARCH: "true"
RAG_EMBEDDING_BATCH_SIZE: "16"
RAG_EMBEDDING_CONCURRENT_REQUESTS: "1"
Open WebUI内蔵Knowledgeでは、まずUIから少数PDFを登録して使い勝手を確認します。
ただし、表やスキャンPDFを含む実務資料の評価は、後述する独自RAG API側のDocling結果を基準にします。
独自RAG APIでPDFを登録する
RAG APIは通常LANへ直接公開しません。
Open WebUIからOpenAPI Toolとして使う場合は、Docker network上の次を参照できます。
http://rag-api:8000/openapi.json
APIにはX-API-Keyを付けます。
SwaggerをWindowsから確認する場合は、先ほどのcompose.lan-api.ymlを重ねます。
PDF登録のAPIは、処理完了まで待たず202 Acceptedを返します。
POST /api/v1/documents
受付時に確認する内容は次のとおりです。
拡張子
%PDF- signature
ファイルサイズ
SHA-256
重複登録
原本ファイルはアップロード名をそのまま使わず、SHA-256から作った文書IDで保存します。
利用者が登録した元ファイル名はメタデータとして残します。
処理状態を追う
PDF登録後は、DuckDB上で状態を持ちます。
queued
↓
processing
↓
completed
失敗時は、
error
になります。
Redis停止などでenqueueできなかった場合もerrorへ変更します。
復旧後はretry APIから再処理できます。
curl -X POST \
-H "X-API-Key: <API_KEY>" \
http://<UbuntuのIP>:8000/api/v1/documents/<document_id>/retry
再処理する前に、同じdocument_idの古いQdrant Pointを削除します。
chunkerを変更してチャンク数が減ったときに、古い末尾チャンクだけが検索DBへ残ることを防ぐためです。
年度や分野を指定して検索する
検索APIは次です。
GET /api/v1/search
例えば、
curl --get \
-H "X-API-Key: <API_KEY>" \
--data-urlencode "q=計画高水流量はいくつか" \
--data-urlencode "top_k=8" \
http://<UbuntuのIP>:8000/api/v1/search
のように検索します。
さらに、
fiscal_year
category
client
confidentiality
をfilterとして渡せます。
例えば2026年度の河川分野だけへ絞る場合は、
curl --get \
-H "X-API-Key: <API_KEY>" \
--data-urlencode "q=設計降雨量はいくつか" \
--data-urlencode "fiscal_year=2026" \
--data-urlencode "category=河川" \
http://<UbuntuのIP>:8000/api/v1/search
とします。
検索結果には、
document_id
filename
title
fiscal_year
client
category
confidentiality
page_start
page_end
section
text
score
を返します。
最終的な回答生成では、この情報から資料名、ページ、節を一緒に表示します。
AGENTS.mdを置く
今回、agent.mdのサンプルも欲しかったので、リポジトリにはagent.mdとAGENTS.mdの両方を置いています。
ただし、Codexへ自動的にプロジェクト指示を読ませる標準名はAGENTS.mdです。
ここには長い設計書を全部貼るのではなく、変更時に壊してほしくない境界と、必ず実行するテストをまとめます。
例えば今回のAGENTS.mdでは、次のような条件を書いています。
# AGENTS.md
## このリポジトリの目的
Ubuntu 24.04 + NVIDIA GPU 上で、社内LAN向けのローカルAI環境を構築する。
主な用途:
- Ollama + Open WebUI + Serena によるローカル開発支援
- Docling + DuckDB + Qdrant によるPDFナレッジ検索
## 変更してはいけない境界
- デフォルトComposeでホストへ公開するのはOpen WebUIだけにする。
- `.env`、APIキー、SecretをGitへ追加しない。
- PDF原本を削除・置換しない。
- アップロード元ファイル名を保存パスへ直接使わない。
- Doclingのpage / section情報を捨てない。
- Ollama Embedding APIは `/api/embed` + `input` を使う。
- Embedding次元を固定値にしない。
- 再処理時は古いQdrant Pointを削除してから登録する。
- PDF解析をFastAPIのHTTP処理へ戻さない。
- Serenaは `streamable-http` と `/mcp` を使う。
## 変更後に実行するテスト
```bash
./scripts/preflight.sh
./scripts/host_test.sh
```
Docker / GPU / Docling / Qdrant / Serenaを変更した場合:
```bash
./scripts/check_env.sh
./scripts/docker_qa.sh
```
## 完了時の報告
- 変更したファイル
- 変更理由
- 実行したテストと結果
- 実行できなかったテストと理由
- 運用手順への影響
このくらいの内容でも、コーディングエージェントがComposeのportを不用意に増やしたり、Embedding次元を固定値へ戻したりするのを防ぎやすくなります。
agent.mdは短い入口として、最初にAGENTS.mdを読むよう案内する形にしました。
テストは3段階に分ける
ローカルAI環境は、docker compose up -dでコンテナが起動しただけでは確認が足りません。
そこで、テストを3段階に分けました。
Gate 1: Docker不要のpreflight
./scripts/preflight.sh
ここでは第三者Python packageをできるだけ使わず、次を確認します。
Python syntax
Composeの基本契約
禁止しているport公開
Ollama /api/embedの契約
init_env.shの契約
AGENTS.mdの必須条件
shell syntax
Gate 2: ホスト側回帰テスト
./scripts/host_test.sh
作成時点では次の結果でした。
STATIC_COMPOSE_VALIDATION=PASS
39 passed
HOST_TEST=PASS
主な対象は次です。
path traversal対策
PDF signature
SHA-256文書ID
Docling page情報抽出
Ollama API契約
Embedding次元検査
Qdrant collection契約
Qdrant metadata filter
文書単位delete
Serena transport
backend port非公開
UID/GID mapping
Open WebUI RAG設定
Gate 3: Ubuntu + Docker + GPUのE2E
./scripts/docker_qa.sh
このテストは実際のUbuntu 24.04 + NVIDIA GPU環境で行います。
最後に、
DOCKER_QA=PASS
まで出ることを合格条件にしています。
特にollama psでGPU利用を確認し、CPU fallbackのままでも合格したことにしないようにしています。
実データは最初から全部入れない
自動テストが通った後、いきなり数百件の報告書を登録する予定はありません。
最初は性質の違う3~4件で試します。
1. テキストを選択できる普通の報告書
2. 表が多い報告書
3. スキャンPDF
4. 100ページを超える報告書
質問も適当に聞くのではなく、人が答えとページを確認できるものを用意します。
単純検索
対象流域の面積はいくつか。
採用した粗度係数はいくつか。
条件検索
2024年度の河川業務だけから、採用した解析手法を探す。
複数資料の比較
2022年度と2024年度で計画流量が変更されているか。
答えが資料にない質問
この業務の最終工事費はいくらか。
この種類も重要です。
資料に書かれていないのに、LLMがもっともらしい値を補ってしまわないかを確認します。
表や図を参照する質問
表4.2のケース2の最大水位はいくつか。
文章検索と表・図の検索は難しさが違うため、同じ評価に混ぜない方が原因を追いやすくなります。
まず見るのはLLMの文章ではなく検索結果
RAGを試すと、どうしても最終回答の文章に目が行きます。
ただ、回答がおかしい場合、LLMそのものではなく、前段で違うページを検索していることもあります。
そのため、最初は次の順で確認します。
PDFを正しく解析できたか
↓
ページ・見出しが残っているか
↓
正解ページが検索上位へ来たか
↓
その根拠だけを使って回答できたか
↓
資料にない内容を追加していないか
Embeddingモデルを大きくするのは、その後でよいと考えています。
今回はまだ入れていないもの
r004では、次の機能はあえて入れていません。
全社SSO
部署別ACL
Qdrant sparse vectorを使った独自hybrid retrieval
reranker専用モデル
表セル専用検索
Word / Excelへの自動出力
AIによる最終成果品の自動確定
先に機能を増やすより、3~4件のPDFと評価質問集で、どこまで正しく検索できるかを確認した方が次の判断がしやすいためです。
Open WebUI側ではhybrid searchを有効にしているため、UI側の検索結果と独自RAG API側のdense retrievalを比較することもできます。
今後やりたいこと
この構成が安定したら、次はPDF検索の精度をもう少し細かく評価します。
考えている順番は次です。
Step 1
テキストPDF、表中心PDF、スキャンPDFでDoclingの抽出結果を比較
Step 2
評価質問集を30~50問程度へ増やす
Step 3
vector検索だけでなくkeyword / hybrid / rerankerを比較
Step 4
資料名・ページ番号を必ず付けた回答生成へ接続
Step 5
過去業務の要約、類似案件検索、条件比較へ広げる
Step 6
利用者権限、バックアップ、監査ログを追加して社内運用を検討
コード側では、SerenaへPythonだけでなくRust、TypeScript、Fortranなどの実プロジェクトを読ませ、言語ごとのLSPがDocker環境で問題なく動くかも確認したいところです。
まとめ
最初は、Ollama、Open WebUI、Serena、Qdrant、FastAPI、Doclingを1つのDocker Composeへ置けば十分だと思っていました。
実際に実装を追ってみると、長く使うためにはサービス数よりも、処理の境界を決める方が重要でした。
今回特に変えたのは次の部分です。
PDF受付と重い解析を分離する
Embedding次元を固定しない
ページ・節を捨てない
原本と処理履歴を残す
再処理時に古いvectorを残さない
LANへ公開するサービスを絞る
AGENTS.mdで変更時のルールを残す
Docker/GPUまで含めたE2Eテストを用意する
ローカルAIを実務へ持ち込む場合、「LLMが動いた」で終わらせず、どの資料を読み、どのページを検索し、どの処理を通って回答したかを後から追えることがかなり重要だと感じています。
まずはこのr004をUbuntu 24.04 + RTX 4060 Tiの実機でdocker_qa.shまで通し、そのあとに実際の報告書を少数だけ登録してみます。
そこで検索結果と引用ページが安定するかを確認してから、報告書の要約やドラフト作成へ広げる予定です。
参考資料
- Ollama - Generate embeddings
- Ollama - qwen3-embedding:0.6b
- Open WebUI - Model Context Protocol (MCP)
- Open WebUI - Environment Variable Configuration
- Open WebUI - Knowledge
- Serena - Running Serena
- Docling - Chunking
- Qdrant - Delete points
- Qdrant - Local Quickstart
- OpenAI Codex - Custom instructions with AGENTS.md



