膨大なPDF資料の海から、必要な「数値」を探し出すのに苦労した経験はありませんか? 私も以前、証券会社のレポートを分析するシステムを構築していた際、OCR(光学文字認識)だけではどうにもならない壁にぶつかりました。テキスト情報は抽出できても、肝心の「売上推移を示す折れ線グラフ」や「市場シェアを比較した円グラフ」が、単なる画像として無視されてしまうのです。
従来の RAG(検索拡張生成) は、テキストデータをベクトル化して検索する手法でした。しかし、現実世界のビジネスドキュメントには、テキスト以外にも多くの情報が含まれています。私たちは長い間、これら非構造化データの宝の山を、ただのピクセルの集合体として扱い、検索不可能なまま放置してきました。
ここで必要になるのが、視覚情報も文脈として理解できる Multimodal RAG です。文字だけでなく、画像や図表、レイアウト情報を統合的に扱うこの技術は、AIエージェントがより高度なタスクをこなすための 画期的な転換点 となります。本記事では、エンジニア向けにMultimodal RAGの内部構造を紐解き、実際に動作するPythonコードを通じてその実装方法を解説します。
テキストだけのRAGが見落としてきた「空白」
なぜ今、Multimodal RAGが必要なのでしょうか。その理由は、従来の手法が持つ構造的な限界にあります。
既存のテキストベースのRAGシステムは、PDFなどのドキュメントを解析する際、基本的にテキスト抽出処理を行います。しかし、このプロセスには2つの大きな問題があります。
1つ目は、レイアウト情報の喪失です。例えば、「左側の画像に対応する説明文」という関係性が、テキスト化することで断ち切られてしまうことがあります。AIは画像の説明文を読むことはできても、それがどのグラフを指しているのかを特定する手がかりを失ってしまうのです。
2つ目は、画像内情報の完全な欠落です。ビジネスレポートにおける最も重要なインサイトは、しばしば図表に凝縮されています。「前年比20%増」というテキストが書かれていない場合でも、棒グラフの高さを見れば増加を読み取れます。しかし、テキストのみのRAGはこのグラフを「空白」あるいは「ノイズ」として処理してしまいます。
これを解決するために、私たちはドキュメントを「読む」だけでなく「見る」機能をAIに与える必要があります。それがMultimodal RAGです。
Multimodal RAGの仕組みとアーキテクチャ
Multimodal RAGを実現するためには、大きく分けて2つのアプローチがあります。
- 画像要約アプローチ:ドキュメント内の画像を抽出し、Vision LLM(例:GPT-4o)を用いて詳細なテキスト説明(キャプション)を生成させ、その説明文をテキストRAGと一緒にベクトル化する方法。
- マルチモーダル埋め込みアプローチ:CLIPやOpenAIの最新モデルのように、テキストと画像を同じ潜在空間(ベクトル空間)にマッピングするモデルを使用し、画像ベクトルとテキストベクトルの類似度を直接計算する方法。
2026年現在の実務的な観点から言うと、精度と制御のしやすさを考慮すると、1の「画像要約アプローチ」をベースとしつつ、必要に応じて2の埋め込みを併用するハイブリッド構成が最も堅実です。画像を一度テキストに変換してしまうことで、既存の強力なテキスト検索エンジンのエコシステムをそのまま活用できるからです。
以下に、典型的なMultimodal RAGのデータフローを図示しました。
このフローにおける重要なポイントは、画像をただ保存するのではなく、Vision LLM を通じて「意味情報」に変換し、それを検索可能なテキストとして再投入するループです。これにより、ユーザーが「売上の減少トレンドを示すグラフを探して」と質問したとき、画像の要約文に「減少」「トレンド」などの単語が含まれていれば、正確にヒットするようになります。
Pythonによる実装:LlamaIndexとOpenAIを活用する
それでは、具体的な実装に移りましょう。ここでは、Pythonと LlamaIndex、そして OpenAI のAPIを利用して、PDF内の画像を含めて検索可能なシステムを構築します。
このコードは、単なる動作確認ではなく、エラーハンドリングやロギングを考慮した実用的な構成となっています。
事前準備
必要なライブラリをインストールします。
pip install llama-index-core llama-index-readers-file llama-index-llms-openai llama-index-multi-modal-llms-openai llama-index-embeddings-openai python-dotenv実装コード
以下のコードは、指定されたディレクトリ内のPDFを読み込み、画像を抽出して要約を作成し、インデックスを作成するスクリプトです。
import logging
import os
import sys
from typing import List, Optional
from dotenv import load_dotenv
from llama_index.core import (
Settings,
SimpleDirectoryReader,
VectorStoreIndex,
StorageContext,
)
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.schema import BaseNode, ImageNode
from llama_index.multi_modal_llms.openai import OpenAIMultiModal
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# ロギングの設定
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
# 環境変数の読み込み
load_dotenv()
class MultimodalRAGPipeline:
def __init__(
self,
input_dir: str,
model_name: str = "gpt-4o",
embed_model_name: str = "text-embedding-3-small",
persist_dir: str = "./storage"
):
"""
Multimodal RAGパイプラインの初期化
Args:
input_dir (str): PDFが格納されているディレクトリパス
model_name (str): 使用するLLMモデル名
embed_model_name (str): 使用する埋め込みモデル名
persist_dir (str): インデックスを保存するディレクトリ
"""
self.input_dir = input_dir
self.persist_dir = persist_dir
# APIキーのチェック
if not os.getenv("OPENAI_API_KEY"):
logger.error("OPENAI_API_KEYが設定されていません。")
raise ValueError("Missing OpenAI API Key")
# LLMとEmbeddingモデルの設定
try:
self.llm = OpenAI(model=model_name)
self.embed_model = OpenAIEmbedding(model=embed_model_name)
# マルチモーダルLLM(画像理解用)の設定
self.multi_modal_llm = OpenAIMultiModal(model=model_name)
Settings.llm = self.llm
Settings.embed_model = self.embed_model
logger.info(f"モデルの初期化完了: LLM={model_name}, Embedding={embed_model_name}")
except Exception as e:
logger.error(f"モデル初期化中にエラーが発生しました: {e}")
raise
def load_documents(self) -> List[BaseNode]:
"""
ドキュメントを読み込み、画像とテキストを抽出する
Returns:
List[BaseNode]: 抽出されたノードのリスト
"""
logger.info(f"ディレクトリ '{self.input_dir}' からドキュメントを読み込み中...")
try:
# 画像を自動抽出するリーダー
reader = SimpleDirectoryReader(
self.input_dir,
required_exts=[".pdf", ".jpg", ".png"],
recursive=True,
# 画像を抽出してImageNodeとして扱う設定
file_metadata=lambda x: {"file_name": x}
)
documents = reader.load_data()
logger.info(f"ドキュメント読み込み成功: {len(documents)} 件のドキュメント")
# ノードパーサーの設定(テキスト用)
text_parser = SentenceSplitter(
chunk_size=1024,
chunk_overlap=20
)
text_nodes = []
image_nodes = []
for doc in documents:
if doc.image_embeds is not None or isinstance(doc, ImageNode):
image_nodes.append(doc)
else:
# テキストノードの分割処理
text_nodes.extend(text_parser.get_nodes_from_documents([doc]))
logger.info(f"ノード分割完了: テキスト={len(text_nodes)}, 画像={len(image_nodes)}")
return text_nodes + image_nodes
except FileNotFoundError:
logger.error(f"ディレクトリが見つかりません: {self.input_dir}")
raise
except Exception as e:
logger.error(f"ドキュメント読み込み中に予期せぬエラー: {e}")
raise
def create_image_summaries(self, image_nodes: List[ImageNode]) -> List[BaseNode]:
"""
画像ノードに対して、Vision LLMを用いて要約テキストを生成する
Args:
image_nodes (List[ImageNode]): 画像ノードのリスト
Returns:
List[BaseNode]: 要約テキストを含むノードリスト
"""
if not image_nodes:
logger.info("要約対象の画像ノードはありません。")
return []
logger.info(f"{len(image_nodes)} 枚の画像に対して要約生成を開始します...")
processed_nodes = []
for img_node in image_nodes:
try:
# 画像パスの取得
image_path = img_node.metadata.get("file_path")
if not image_path or not os.path.exists(image_path):
logger.warning(f"画像ファイルが見つかりません: {image_path}, スキップします。")
continue
# プロンプトの作成
prompt = """
この画像を詳細に説明してください。特に、グラフの場合は数値の推移や傾向、
表の場合はキーとなるデータポイントを抽出してテキスト化してください。
説明は日本語で行い、検索しやすいような具体的なキーワードを含めてください。
"""
# Vision LLMによる画像の理解と要約生成
response = self.multi_modal_llm.complete(
prompt=prompt,
image_documents=[img_node]
)
summary_text = response.text
logger.info(f"画像要約生成成功 ({os.path.basename(image_path)}): {summary_text[:50]}...")
# 要約テキストを新しいノードとして作成し、元の画像への参照を保持
summary_node = BaseNode(
text=summary_text,
metadata={
"type": "image_summary",
"source_image_path": image_path,
"file_name": img_node.metadata.get("file_name", "unknown")
}
)
processed_nodes.append(summary_node)
except Exception as e:
logger.error(f"画像要約生成中にエラーが発生しました ({img_node.metadata.get('file_name')}): {e}")
# エラーが発生しても処理を止めずに次へ進む
continue
return processed_nodes
def build_index(self, nodes: List[BaseNode]):
"""
ベクトルインデックスを構築し、保存する
Args:
nodes (List[BaseNode]): インデックス化するノードリスト
"""
logger.info("ベクトルインデックスの構築を開始します...")
try:
# ストレージコンテキストの作成
storage_context = StorageContext.from_defaults()
# インデックスの作成
index = VectorStoreIndex(nodes, storage_context=storage_context)
# インデックスの永続化
index.storage_context.persist(persist_dir=self.persist_dir)
logger.info(f"インデックスの保存完了: {self.persist_dir}")
return index
except Exception as e:
logger.error(f"インデックス構築中にエラーが発生しました: {e}")
raise
def run_pipeline(self):
"""パイプライン全体を実行する"""
try:
# 1. ドキュメント読み込み
nodes = self.load_documents()
# 画像ノードとテキストノードを分離
image_nodes = [n for n in nodes if isinstance(n, ImageNode)]
text_nodes = [n for n in nodes if not isinstance(n, ImageNode)]
# 2. 画像の要約生成
summary_nodes = self.create_image_summaries(image_nodes)
# 3. ノードの統合(元のテキスト + 画像要約)
all_nodes = text_nodes + summary_nodes
# 4. インデックス構築
index = self.build_index(all_nodes)
logger.info("パイプライン完了。クエリエンジンの準備ができました。")
return index.as_query_engine(similarity_top_k=5)
except Exception as e:
logger.critical(f"パイプライン実行中に致命的なエラーが発生しました: {e}")
sys.exit(1)
if __name__ == "__main__":
# 使用例
INPUT_DIR = "./data" # PDFを含むディレクトリ
STORAGE_DIR = "./storage_multimodal"
try:
pipeline = MultimodalRAGPipeline(
input_dir=INPUT_DIR,
persist_dir=STORAGE_DIR
)
query_engine = pipeline.run_pipeline()
# テストクエリ
response = query_engine.query("財務諸表において、売上高の変動が最も激しい四半期はいつですか?その根拠となるグラフも教えてください。")
print("\n=== 回答 ===")
print(response)
except Exception as e:
logger.error(f"実行に失敗しました: {e}")コードの解説
この実装における重要なポイントは、create_image_summaries メソッドです。ここでは、抽出された ImageNode を対象に OpenAIMultiModal モデルを呼び出しています。
画像を直接ベクトル化するのではなく、**「画像の内容を説明するテキスト」**をLLMに生成させ、そのテキストをベクトルDBに格納しています。これにより、「売上の減少」というテキストクエリに対して、画像内のピクセルパターンではなく、「このグラフは売上が前年比で減少していることを示している」という 意味的な説明文 がマッチするようになります。
また、エラーハンドリングとして、画像の読み取りに失敗した場合やAPI呼び出しでエラーが発生した場合に、ログを出力して処理を継続するように設計しています。大量のドキュメントを処理する際、1つの画像のエラーで全体が停止するのは避けるべきだからです。
ビジネスユースケース:証券アナリスト業務の効率化
具体的なビジネスシーンを想定してみましょう。証券会社のアナリストは、毎日数百社に及ぶ企業の決算説明資料(IR資料)や適時開示資料を確認する必要があります。
従来であれば、資料を開き、目次をめくり、該当するページのグラフを目視で確認し、Excelに数値を転記するという手作業が必要でした。これは単純作業ですが、膨大な時間を要するため、アナリストが本来注力すべき「企業の成長戦略の分析」や「市場動向の予測」といったコア業務の時間を圧迫していました。
Multimodal RAGを導入することで、以下のような業務変革が可能になります。
- インサイトの横断検索: 「ROEが20%を超えているが、営業利益率が低下している企業を探して」といった複雑なクエリを実行できます。システムは、各企業の資料内にある「ROEの推移グラフ」と「利益率の表」を画像認識で解析し、条件に合致する企業と該当ページを特定します。
- 自動レポート作成: 特定のセクター(例えば、EV関連企業)の決算短信を収集し、重要な図表(売上予想、設備投資計画図)を自動抽出して、要約レポートをドラフト作成できます。
このように、Multimodal RAGは単なる検索ツールではなく、知的生産性を飛躍的に高める基盤となります。特に、非構造化データが意思決定の鍵を握る金融、法務、製造、医療などの分野において、その価値は計り知れません。
よくある質問
Q: Multimodal RAGの導入コストはどの程度ですか? A: コストは主に使用するLLMのAPI利用料とベクトルデータベースの維持費です。GPT-4oのような高性能モデルを画像理解に用いる場合、テキストのみのRAGに比べてトークン数が増加する傾向にあるため、プロンプトの最適化やキャッシュ戦略が重要になります。
Q: 図表の精度が低い場合、どう改善すればよいですか? A: 画像の解像度を確保することはもちろんですが、複雑な図表に対しては、画像全体を一度に処理するのではなく、オブジェクト検知モデルなどを用いて「グラフ部分」「凡例部分」「タイトル部分」に分割してからLLMに入力するアプローチが有効です。
Q: セキュリティが重要な業界でも利用可能ですか? A: はい。OpenAIやAnthropicのAPIを用いるクラウド版ではなく、Llama 3.2 VisionやQwen2-VLなどのオープンソースモデルをオンプレミス環境でホストすることで、データを社内ネットワークに閉じたまま運用可能です。
まとめ
- 従来の RAG はテキスト情報しか扱えず、ドキュメントに含まれる図表や画像情報という重要なリソースを活用できていなかった。
- Multimodal RAG は、Vision LLMを用いて画像をテキスト説明に変換したり、マルチモーダル埋め込みを用いることで、視覚情報を検索可能にする技術である。
- 実装においては、LlamaIndexなどのフレームワークを活用し、画像抽出→要約生成→インデックス化というパイプラインを構築する。
- 証券アナリスト業務のような、非構造化データの解析が中心となるビジネスシーンにおいて、業務効率と分析精度を 大きく変える 可能性を秘めている。
推奨リソース
- 書籍: 『Building Multimodal AI Applications』(2025年刊行予定)
- Multimodal AIの基礎から応用まで、最新のアーキテクチャを網羅した技術書です。
- ツール: LlamaIndex
- データローダーからインデックス作成まで、Multimodal RAG構築に必要な機能が一通り揃っているPythonライブラリです。非常に柔軟性が高く、プロダクション開発にも適しています。
- SaaS: Azure AI Vision
- OCRに加えて、画像内のテキスト読み取り(Layout Analysis)やキャプション生成機能をAPIで提供しており、自前のVision LLMをホストする前の前処理基盤として優秀です。
AI導入支援・開発のご相談
貴社の業務には、どのような非構造化データの活用機会がありますか? 「画像も含めて検索したい」「図表から数値を読み取って自動集計したい」など、具体的な課題があればお気軽にご相談ください。
参考リンク
[1] OpenAI API Documentation - Vision https://platform.openai.com/docs/guides/vision [2] LlamaIndex - Multi-modal https://docs.llamaindex.ai/en/stable/examples/multi_modal/ [3] CLIP: Connecting Text and Images https://openai.com/research/clip



