Open WebUIとMarkItDownでドキュメント解析

昨今、OllamaやPodmanを用いたローカルLLM環境の構築は非常に容易になりました。しかし、PDFやExcelといったリッチドキュメントをAIに読み取らせる際、依然として高いハードルが存在します。多くのLLMはテキストデータの扱いに長けている反面、バイナリ形式のファイルを直接解釈する精度には限界があるためです。

この問題を解決する手段として、Microsoftが公開した「MarkItDown」が注目されています。これはPDFやOfficeファイルを、AIが最も理解しやすいMarkdown形式に変換するツールです。これをチャットインターフェースであるOpen WebUIとシームレスに連携させることで、手元の資料を即座に解析できる環境の構築を目指しました。

目次

変換プロセスの完全自動化と精度の追求

今回の構築目的は、Open WebUIのチャット画面にPDFをドラッグ&ドロップするだけで、MarkItDownによる高精度なMarkdown変換を経由し、その内容をLLMが即座に要約・分析できるパイプラインを完成させることです。

定量的な目標として、以下の2点を設定しました。

  1. 手動操作の削減: 外部ツールでのテキスト抽出やコピー&ペースト作業を100%撤廃し、チャットUI内での完結を目指します。
  2. 解析精度の向上: Open WebUI内蔵の標準パーサーによるテキスト抽出と比較し、図表や構造化データの保持能力に優れたMarkItDownを介すことで、複雑なデータシート等の読解精度を大幅に引き上げます。

開発を阻んだ「UUIDリネーム」と「キャッシュ」の壁

実装を進める中で、コンテナ環境特有の2つの技術的課題に直面しました。

  1. ファイルの「保存パスと名前の喪失」です。Open WebUIのチャット画面にファイルをドロップすると、サーバー上(コンテナ内)にはランダムなUUIDが付与されたファイル名(例:da99462c…_datasheet.pdf)で保存されます。LLMはこのランダムな文字列を認識できないため、MarkItDownに正確なファイルパスを渡せず、解析エラーを引き起こします。
  2. Podmanの「ビルドキャッシュによる依存関係の欠落」です。MarkItDownでPDFを解析するにはmarkitdown[all]という拡張ライブラリのインストールが必要ですが、後からrequirements.txtを修正しても、キャッシュ機能が働き、実際には重いPDF解析パッケージがインストールされない現象が発生しました。

構築方法:MCPOコンテナの追加とワイルドカード検索による突破

これらの課題に対し、インフラ構成とPythonスクリプトの両面から解決を図りました。

ファイル構造

作業ディレクトリ/
 ├── docker-compose.yml
 ├── webui-data/             # ※自動生成されます(Open WebUIのデータ保存先)
 └── mcpo-markitdown/        # 以下の3つのファイルを配置するフォルダ
      ├── requirements.txt
      ├── Dockerfile
      └── server.py

ボリュームマウントの共有と強制再ビルド

Open WebUIがファイルを保存するディレクトリと、MCPOコンテナが読みに行くディレクトリを同一にするため、docker-compose.ymlに以下の設定を追加しました。また、キャッシュの壁を越えるため、podman-compose build –no-cacheを用いて強制的にコンテナを再ビルドし、PDF解析ライブラリを確実に導入しました。

services:
  ollama:
    image: docker.io/ollama/ollama:0.20.6
    container_name: ollama
    ports:
      - "11434:11434"
    environment:
      - "OLLAMA_KEEP_ALIVE=-1"  # -1でVRAMから降ろさない
      - "OLLAMA_MAX_LOADED_MODELS=2"    # 同時に2つのモデルをロード許可
    volumes:
      - ollama_data:/root/.ollama
    devices:
      - nvidia.com/gpu=all
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    ports:
      - "3000:8080"
    volumes:
      # ホスト側の ./webui-data をコンテナの /app/backend/data にマウント
      - ./webui-data:/app/backend/data
    restart: always

  mcpo-markitdown:
    build: ./mcpo-markitdown
    container_name: mcpo-markitdown
    ports:
      - "8000:8000"
    volumes:
      # Open WebUIと全く同じホストディレクトリを「読み取り専用(ro)」でマウント
      - ./webui-data:/app/backend/data:ro
    restart: always

python依存パッケージ

pythonの依存パッケージです。

mcpo
markitdown[all]
mcp

Dockerfile

ビルドキャッシュの罠を回避し、PDF解析用の重いライブラリを確実にインストールするためのDockerfileです。

FROM python:3.12-slim

WORKDIR /app

# まず要件ファイルをコピーして基本インストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# [all]オプション(PDFやOffice解析エンジン)を確実にインストールさせるための念押し
RUN pip install --no-cache-dir "markitdown[all]"

# スクリプト群をコピー
COPY . .

# mcpo経由で標準入出力をWeb API(OpenAPI)に変換して起動
CMD ["mcpo", "--host", "0.0.0.0", "--port", "8000", "--", "python", "server.py"]

ワイルドカード検索によるパス自動特定スクリプト

UUID問題を解決するため、MCPO側で動かすスクリプト(server.py)を改修しました。LLMには「元のファイル名」だけを渡させ、Pythonのglobモジュールを用いてアップロードディレクトリ内をワイルドカード検索する仕組みを実装しました。

import os
import glob
from mcp.server.fastmcp import FastMCP
from markitdown import MarkItDown

# MCPサーバーとMarkItDownの初期化
mcp = FastMCP("MarkItDown Converter")
md = MarkItDown()

@mcp.tool()
def convert_to_markdown(filename: str) -> str:
"""
チャットにアップロードされたファイルをMarkdownに変換します。
Args:
filename: アップロードされたファイルの名前、またはその一部(例: datasheet.pdf)
"""
# docker-composeで共有マウントしたディレクトリのアップロード先パス
upload_dir = "/app/backend/data/uploads"

# UUIDが付与されたファイル群の中から、ファイル名を含むものをワイルドカード検索
search_pattern = os.path.join(upload_dir, f"*{filename}*")
matched_files = glob.glob(search_pattern)

if not matched_files:
return f"Error: '{filename}' を含むファイルがアップロードフォルダに見つかりませんでした。"

# 同名ファイルが複数ある場合は、一番新しくアップロードされたものを対象とする
target_file = max(matched_files, key=os.path.getmtime)

try:
# 見つけ出したフルパスを使ってMarkItDownで変換実行
result = md.convert(target_file)
return result.text_content

except Exception as e:
return f"Error 変換中に問題が発生しました: {str(e)}"

if __name__ == "__main__":
# mcpoがラップして通信するため、内部的にはstdioで起動
mcp.run()

これにより、ユーザーは「この datasheet.pdf を読んで」と指示するだけで、システムが自動的にUUID付きのフルパスを特定し、解析を実行できるようになりました。

起動と連携の手順(総仕上げ)

ファイルの配置が終わったら、以下のコマンドでキャッシュを使わずにクリーンビルドして起動します。(※ podman-compose または docker-compose をお使いの環境に合わせて実行してください)

podman-compose down
podman-compose build --no-cache
podman-compose up -d

実行結果

ESP32-WROOM-32EのDatasheetを読み込ませ解析で来たか確認しました。

チャットの回答のソースにはアップロードしたファイルとtool_convert_to_markdown_postというソースがあり、後者がmarkitdownで変換されたmarkdownになっている

表紙の部分の情報を抜き出した例は以下

ESP32­WROOM­32E ESP32­WROOM­32UE Datasheet の表紙

ESP32­WROOM­32E
ESP32­WROOM­32UE
Datasheet

2.4 GHz
Wi­Fi + Bluetooth®
+ Bluetooth
LE module
Xtensa®
Built around ESP32 series of SoCs, dual­core 32­bit LX6 microprocessor
4/8/16
MB flash available
---------
------------------
--------------
-----------------
26 GPIOs,
rich set
of peripherals
On­board
PCB antenna
or external
antenna connector

実用的なパーソナルAIアシスタントへの進化

本構築により、ESP32等の複雑な英語データシートをチャットに投じるだけで、MarkItDownがパースした構造化データを元に、ローカルのQwen3やGemma3が的確に内容を要約する環境が整いました。

Open WebUIとMCPツールの連携は、単なるチャットボットを「自律的にツールを使いこなすエージェント」へと変貌させます。今回確立した「UUIDを意識させないファイル検索手法」は、MarkItDownに限らず、ローカルファイルを扱うあらゆるMCPサーバに応用可能です。広い空間はなくとも、こうした技術の積み重ねが、納戸という限られた場所から無限の可能性を引き出す鍵になると確信しています。