はじめに
実務の現場では、「RAG導入後、マニュアルの図解やグラフの質問に答えられない」という課題が頻出します。テキストベースのRAGは有効ですが、製造業の図面や金融レポートのグラフなど、ビジネスデータの多くは視覚情報を含んでいるためです。
これを解決する手段がマルチモーダルRAGですが、単にChatGPT等のLLMを導入するだけでは不十分です。実運用においては、コスト、レイテンシ、検索精度のトレードオフを考慮したシステム設計が鍵となります。
本記事では、プロダクション環境で安定稼働させるためのアーキテクチャ選定と、LangChainを用いた実装コードを解説します。特に、検索精度とコンテキストウィンドウのバランスに優れた「Multi-Vector Retriever」のパターンについて深掘りします。
マルチモーダルRAGのアーキテクチャ選定基準
マルチモーダルRAGの実装は、データの埋め込み(Embedding)と検索(Retrieval)のアプローチによって大きく3つのパターンに分類されます。プロジェクトの要件(精度、速度、コスト)に応じて、最適なものを選択することが重要です。
パターンA:画像の要約(Image Summary)埋め込み方式
画像をマルチモーダル対応LLM(ChatGPTの画像認識モデル等)でテキスト要約し、そのテキストをベクトル化して検索する方式です。
- 仕組み: 画像 → テキスト要約(LLM) → Embedding → VectorStore
- メリット: 既存のテキスト検索インフラを流用可能です。画像内の文字や文脈を言語化するため、意味的な検索に強みを発揮します。
- デメリット: 画像ごとの要約生成に前処理コスト(時間およびAPI利用料)がかかります。
- 推奨ユースケース: 複雑な図表、手書きメモ、UIのスクリーンショットなど、深い意味解釈が求められる場面に適しています。
パターンB:生画像埋め込み(Raw Image Embedding)方式
CLIPやOpenCLIP等のマルチモーダルEmbeddingモデルを使用し、画像とテキストを共通のベクトル空間に直接埋め込む方式です。
- 仕組み: 画像/テキスト → マルチモーダルEmbedding → VectorStore
- メリット: LLMによる要約が不要なため、前処理が高速です。色合い、形状、構図といった視覚的類似度に基づく検索が可能です。
- デメリット: グラフの数値や微細な注釈など、詳細な意味理解に基づく検索精度はテキスト化方式に劣ります。
- 推奨ユースケース: 商品カタログ、素材サイト、アパレルなど、視覚的な特徴が重視される検索に適しています。
パターンC:ハイブリッド検索方式(Multi-Vector Retriever活用)
パターンAを発展させた構成で、検索用データ(要約)と生成用データ(生画像)を分離して管理します。LangChainのMulti-Vector Retriever等がこの方式に対応しています。
- 仕組み: 画像の要約テキストで検索を行い、回答生成時のLLMには元の画像(Base64エンコード等)を直接渡します。
- 技術的トレードオフ: ストレージ容量は増加しますが、検索精度(テキスト)と回答品質(画像)のバランスが最も優れています。
- 最新トレンド: ハイブリッド検索にリランキング(再順位付け)を組み合わせ、さらに精度を向上させるアプローチが増加傾向にあります。
| 評価軸 | パターンA (要約) | パターンB (生画像) | パターンC (ハイブリッド/推奨) |
|---|---|---|---|
| 検索精度(意味) | 高 | 中 | 最高 |
| 検索精度(視覚) | 低 | 高 | 高 |
| 実装難易度 | 低 | 中 | 高 |
| 運用コスト | 中(要約生成費) | 低 | 中 |
実際の業務システムやナレッジベース構築においては、パターンC(ハイブリッド検索方式)が最も実用的です。検索時は言葉で探し、回答時は画像を見るという、人間の自然なワークフローをシステム上で模倣できるためです。今回は、このパターンCの実装フローを具体的に解説します。
データパイプライン実装リファレンス:UnstructuredによるPDF解析
PDF等のドキュメントから画像とテキストを分離・抽出する前処理パイプラインを構築します。ここでは、強力なパース機能を持つ unstructured ライブラリを使用します。
必要なライブラリの準備とセキュリティ対策
LangChainのパッケージ構成変更に伴い、コア機能とコミュニティ機能が分離されています。また、シリアライゼーション関連の脆弱性(CVE-2025-68664等)への対策として、必ず修正済みの最新バージョンを使用するようにしてください。
# LangChainの最新構成とUnstructuredのインストール
pip install langchain langchain-community langchain-core unstructured[all-docs] pydantic lxml
# システム側で poppler-utils や tesseract-ocr のインストールも必須です
# (例: Ubuntuの場合)
# sudo apt-get install poppler-utils tesseract-ocr
partition_pdf関数のパラメータ設定
PDFから画像、テキスト、表データを抽出するコード例です。マルチモーダルRAGにおいては、extract_images_in_pdf=True の設定が重要なポイントとなります。
from typing import Any
from pydantic import BaseModel
from unstructured.partition.pdf import partition_pdf
# 出力パスの設定
output_path = "./figures"
# PDFの解析と要素抽出
# strategy="hi_res" は処理時間がかかりますが、レイアウト解析を行い画像や表を高精度に認識します
raw_pdf_elements = partition_pdf(
filename="./sample_document.pdf",
extract_images_in_pdf=True, # 画像抽出を有効化
infer_table_structure=True, # 表構造の推論を有効化
chunking_strategy="by_title", # タイトルベースでチャンク分割
max_characters=4000, # チャンクの最大文字数
new_after_n_chars=3800, # 新しいチャンクを作成する目安
combine_text_under_n_chars=2000, # 小さなチャンクを結合する閾値
image_output_dir_path=output_path,
)
画像・テキスト・表データの抽出戦略
抽出された要素(Element)は、タイプ別に分類して処理します。画像と表は後続の処理で要約を生成するため、テキストとは分けて管理します。Unstructuredのアップデートによるクラス構造の変更に備え、型判定は慎重に行うことを推奨します。
# 要素の分類用リスト
tables = []
texts = []
for element in raw_pdf_elements:
element_type = str(type(element))
# 表データの抽出
if "unstructured.documents.elements.Table" in element_type:
tables.append(str(element))
# テキストデータの抽出(CompositeElementなど)
elif "unstructured.documents.elements.CompositeElement" in element_type:
texts.append(str(element))
# 画像は指定ディレクトリに出力されているため、ファイルパスを取得して処理
import os
# 画像処理ライブラリ等が別途必要な場合があります
images = []
if os.path.exists(output_path):
for image_file in os.listdir(output_path):
if image_file.endswith(('.png', '.jpg', '.jpeg')):
image_path = os.path.join(output_path, image_file)
# 後でLLMに渡すためにBase64エンコードする処理が必要です
# ここではパスの保持のみ例示します
images.append(image_path)
この段階で、テキスト、表、画像(ファイルパス)のリストが準備できました。実運用では、各データをLLMに適した形式(Base64等)へ変換し、Vector Storeへ格納するプロセスに進みます。
VectorStoreとRetrieverの実装仕様:Multi-Vector Retrieverの活用
ここからが実装の核心部となります。Multi-Vector Retrieverを使用し、検索は要約テキストで行い、LLMには生画像データを渡す仕組みを構築します。
要約生成チェーンの準備
画像や表をテキスト要約に変換し、テキストベースのEmbeddingモデルで検索できるようにします。ChatGPT-preview 等のプレビュー版モデルは廃止されるケースが多いため、本番環境では必ず最新のマルチモーダルモデルを明示的に指定してください。
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# 画像要約用のチェーン
def generate_image_summaries(images):
# 最新のマルチモーダルモデルを指定(例: ChatGPT)
# ※モデル名は利用可能な最新バージョンを確認して設定してください
model = ChatOpenAI(model="ChatGPT", max_tokens=1024)
summary_batch = []
for img_base64 in images:
msg = model.invoke(
[
HumanMessage(
content=[
{"type": "text", "text": "以下の画像を詳細に説明してください。特に図表の数値や傾向に着目してください。"},
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{img_base64}"},
},
]
)
]
)
summary_batch.append(msg.content)
return summary_batch
# テキスト・表の要約も同様に生成(コード省略)
image_summaries = generate_image_summaries(images)
MultiVectorRetrieverの構成
MultiVectorRetriever は、ベクトルストア(検索用)とドキュメントストア(データ保持用)を連携させる役割を担います。ベクトルストアに要約を、ドキュメントストアに元の画像データを保存し、両者を doc_id で紐付けます。
import uuid
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain.storage import InMemoryStore
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
# 1. ベクトルストア(検索用インデックス)
vectorstore = Chroma(
collection_name="mm_rag_collection",
embedding_function=OpenAIEmbeddings()
)
# 2. ドキュメントストア(実データの保管場所)
# 本番環境ではRedisやDBを使用推奨ですが、ここではメモリ内ストアを使用
store = InMemoryStore()
# 3. doc_idの生成
id_key = "doc_id"
# 画像ごとのIDを生成
img_ids = [str(uuid.uuid4()) for _ in images]
# 4. 要約をDocumentオブジェクト化し、メタデータにIDを付与
summary_docs = [
Document(page_content=s, metadata={id_key: img_ids[i]})
for i, s in enumerate(image_summaries)
]
# 5. Retrieverの初期化
retriever = MultiVectorRetriever(
vectorstore=vectorstore,
docstore=store,
id_key=id_key,
)
# 6. データの登録
# ベクトルストアには「要約」を追加
retriever.vectorstore.add_documents(summary_docs)
# ドキュメントストアには「元の画像(Base64)」をIDと紐付けて保存
retriever.docstore.mset(list(zip(img_ids, images)))
この設計により、「〇〇の売上推移グラフを見せて」といった質問に対して、グラフの要約テキストを検索してヒットさせ、最終的な結果としてグラフの画像データをLLMに渡すことが可能になります。
生成チェーン構築リファレンス:LCELによるマルチモーダルプロンプト
Retrieverが取得した画像データを、回答生成用のLLMに渡すチェーンを構築します。ここでは、LangChain Expression Language (LCEL) を用いて実装します。
画像データの処理ロジック
Retrieverから返却されるBase64エンコードされた画像のリスト(またはテキスト)を、適切にプロンプトへ埋め込みます。
from langchain_core.runnables import RunnableLambda, RunnablePassthrough
def split_image_text_types(docs):
'''検索結果を画像とテキストに分離する'''
b64_images = []
texts = []
for doc in docs:
# ドキュメントストアから取り出した生データがBase64文字列か判定
# 実際の実装では型チェックやメタデータでの判断を推奨
if isinstance(doc, str) and is_base64(doc):
b64_images.append(doc)
else:
texts.append(doc.page_content)
return {"images": b64_images, "texts": texts}
def img_prompt_func(data_dict):
'''画像とテキストを組み合わせてプロンプトメッセージを作成'''
messages = []
# テキストコンテキストの追加
text_message = {
"type": "text",
"text": (
"以下のコンテキスト(テキストおよび画像)を使用して質問に答えてください。\n"
f"テキスト情報: {data_dict['texts']}\n"
f"質問: {data_dict['question']}"
),
}
messages.append(text_message)
# 画像コンテキストの追加
for image in data_dict['images']:
image_message = {
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image}"},
}
messages.append(image_message)
return [HumanMessage(content=messages)]
# RAGチェーンの構築
chain = (
{
"context": retriever | RunnableLambda(split_image_text_types),
"question": RunnablePassthrough(),
}
| RunnableLambda(img_prompt_func)
| ChatOpenAI(model="ChatGPT-preview", max_tokens=1024)
| StrOutputParser()
)
実行例
response = chain.invoke("第3四半期の売上グラフから読み取れる傾向は?")
print(response)
これにより、検索されたグラフ画像をLLMが視覚的に認識し、その内容に基づいて回答を生成するパイプラインが完成します。
実装トラブルシューティングと最適化
プロダクション環境での運用において、一般的に直面しやすい課題とその対策について解説します。
1. レートリミット(TPM)とコスト管理
ChatGPT等のVisionモデルは、トークン消費量が大きくなる傾向があります。特に高解像度画像は詳細モード(high detail)で処理されるため、多くのトークンを消費します。
- 対策: 画像のリサイズ処理をデータパイプラインに組み込みます。長辺を1024px以下に抑えるだけでも、大幅なトークン節約につながります。
- 再試行ロジック: LangChainの
.with_retryメソッドを活用し、API制限に達した際のエクスポネンシャルバックオフ(指数関数的待機)を設定して安定性を高めます。
model_with_retry = ChatOpenAI(model="ChatGPT-preview").with_retry(
stop_after_attempt=3,
wait_exponential_jitter=True
)
2. レイテンシの改善
インデックス作成時の画像要約生成プロセスは、非常に時間がかかる処理です。
- 対策: バッチ処理化と非同期処理(
ainvoke)を積極的に活用します。インデックス作成は夜間バッチで実行するなどの運用設計が不可欠です。また、検索時のレスポンス速度を向上させるため、画像そのものではなく要約テキストのみをコンテキストとしてLLMに渡す「軽量モード」を用意することも有効なアプローチです。
3. メモリ不足エラー
大量のBase64画像をメモリ上で扱うと、PythonプロセスがOOM(Out Of Memory)を引き起こしてクラッシュするリスクがあります。
- 対策:
InMemoryStoreはあくまでPoC(概念実証)用です。本番環境ではRedisやPostgreSQL等の外部データストアを使用し、必要なデータだけをオンデマンドでフェッチするようにByteStoreを構成してください。
まとめ
マルチモーダルRAGの実装は、テキストのみのRAGと比較してアーキテクチャが複雑になりますが、ユーザー体験の向上と解決可能なビジネス課題の幅は大きく広がります。
- アーキテクチャ: Multi-Vector Retrieverを用いた「要約で検索し、画像で回答を生成する」構成が、現時点における最適解の一つと言えます。
- データ処理: Unstructuredを用いた適切な前処理と、画像・テキストの分離管理が精度の鍵を握ります。
- 運用: コストと処理速度のトレードオフを論理的に評価し、画像リサイズや外部ストアの活用によってシステム全体を最適化します。
本記事で紹介したコードは基本形となります。実際のプロジェクトに適用する際は、ビジネス上のROI(投資対効果)を考慮しつつ、対象となるドキュメントの特性に合わせた細やかなチューニングを行ってください。
コメント