텍스트, 이미지, 표, 차트 등 다양한 모달리티를 통합하는 멀티모달 RAG 시스템의 설계와 구현을 다룹니다. ColPali, 비전 기반 검색, 문서 파싱 전략을 배웁니다.
7장에서 멀티모달 임베딩과 크로스모달 검색을 배웠습니다. 이 장에서는 이를 기반으로 멀티모달 RAG(Retrieval-Augmented Generation) 시스템을 설계합니다. 전통적 텍스트 RAG를 넘어, 이미지, 표, 차트, 다이어그램이 포함된 문서에서 정보를 검색하고 LLM에 전달하는 파이프라인을 구축합니다.
텍스트 전용 RAG는 문서의 텍스트만 추출하여 검색합니다. 하지만 실무 문서에는 텍스트 외에 다양한 시각 정보가 포함되어 있습니다.
기술 문서: 아키텍처 다이어그램, 시퀀스 다이어그램
재무 보고서: 표, 차트, 그래프
의학 논문: X-Ray 이미지, 병리 사진
제품 매뉴얼: 스크린샷, 분해도
프레젠테이션: 슬라이드 (텍스트 + 시각 레이아웃)
텍스트 RAG로 "2023년 매출 추이 차트"를 검색하면 차트 자체는 검색되지 않고, 차트를 언급한 텍스트만 반환됩니다. 멀티모달 RAG는 이 한계를 해결합니다.
| 접근 방식 | 설명 | 장점 | 단점 |
|---|---|---|---|
| 텍스트 추출 | 문서에서 텍스트만 추출 | 단순, 빠름 | 시각 정보 손실 |
| 캡셔닝 | 이미지→텍스트 변환 후 텍스트 RAG | 기존 파이프라인 호환 | 캡션 품질에 의존 |
| 비전 임베딩 | 페이지 이미지를 직접 임베딩 | 시각 정보 보존 | 임베딩 품질 제한 |
| ColPali | 비전 모델로 문서 페이지를 직접 임베딩+검색 | 레이아웃 이해 | 모델 크기 |
| 하이브리드 | 텍스트 + 비전 결합 | 최고 정확도 | 복잡성 |
문서 입력
├─ [텍스트 추출] → 텍스트 청크 → [텍스트 임베딩] → 텍스트 벡터 DB
├─ [이미지 추출] → 이미지들 → [VLM 캡셔닝] → 캡션 + 이미지
│ └─ [CLIP 임베딩] → 이미지 벡터 DB
└─ [페이지 이미지] → [ColPali] → 페이지 벡터 DB
쿼리
→ [텍스트 임베딩] → 텍스트 검색 ─┐
→ [CLIP 임베딩] → 이미지 검색 ──┼─ [리랭킹] → [VLM] → 응답
→ [ColPali] → 페이지 검색 ──┘
ColPali는 2024년에 발표된 비전 기반 문서 검색 모델로, 문서 페이지 이미지를 OCR/파싱 없이 직접 임베딩합니다.
문서 페이지 이미지 → [Vision Encoder (PaliGemma)] → 패치별 임베딩
↓
쿼리 텍스트 → [Text Encoder] → 토큰별 임베딩 ──── Late Interaction ──→ 유사도 점수
ColPali는 Late Interaction 매커니즘을 사용합니다. 문서 이미지의 각 패치 임베딩과 쿼리의 각 토큰 임베딩 간의 최대 유사도를 합산하여 문서-쿼리 관련성을 측정합니다.
# ColPali 모델 로드 (개념적 코드)
from colpali import ColPali
model = ColPali.from_pretrained("vidore/colpali-v1.3")
# 문서 페이지 인덱싱
page_embeddings = model.encode_documents([
"page1.png", "page2.png", "page3.png"
])
# 쿼리 검색
query = "2023년 4분기 매출 차트"
query_embedding = model.encode_query(query)
# Late Interaction으로 유사도 계산
scores = model.score(query_embedding, page_embeddings)
# 가장 관련 높은 페이지 반환ColPali는 특히 표가 많은 문서(재무 보고서, 기술 사양서)에서 텍스트 RAG보다 월등한 성능을 보입니다. 표의 행/열 구조와 셀 값의 관계를 시각적으로 이해하기 때문입니다. 반면, 긴 텍스트 위주의 문서에서는 전통적 텍스트 RAG와 성능이 비슷하거나 약간 낮을 수 있습니다.
from dataclasses import dataclass
from typing import Literal
@dataclass
class DocumentChunk:
chunk_id: str
page_number: int
chunk_type: Literal["text", "table", "image", "chart"]
content: str # 텍스트 또는 캡션
image_data: bytes | None # 원본 이미지 (있는 경우)
metadata: dict
def parse_multimodal_document(pdf_path: str, vlm_client) -> list[DocumentChunk]:
"""PDF를 멀티모달 청크로 파싱"""
chunks = []
for page_num, page_image in enumerate(render_pdf_pages(pdf_path)):
# 1. 페이지 분석 — 영역 유형 식별
regions = analyze_page_regions(vlm_client, page_image)
for region in regions:
if region.type == "text":
chunks.append(DocumentChunk(
chunk_id=f"p{page_num}_text_{region.index}",
page_number=page_num,
chunk_type="text",
content=region.text,
image_data=None,
metadata={"position": region.bbox},
))
elif region.type in ("table", "chart", "image"):
# VLM으로 시각 영역 설명 생성
caption = generate_region_caption(vlm_client, region.image)
chunks.append(DocumentChunk(
chunk_id=f"p{page_num}_{region.type}_{region.index}",
page_number=page_num,
chunk_type=region.type,
content=caption,
image_data=region.image,
metadata={"position": region.bbox},
))
return chunksdef process_table(table_image: bytes, vlm_client) -> dict:
"""표를 마크다운과 캡션으로 변환"""
# VLM으로 표를 마크다운 변환
markdown = vlm_client.analyze(table_image, prompt="""
이 표를 마크다운 형식으로 정확하게 변환해주세요.
모든 행과 열의 데이터를 포함하세요.""")
# VLM으로 표 요약 생성
summary = vlm_client.analyze(table_image, prompt="""
이 표의 내용을 2-3문장으로 요약해주세요.
핵심 수치와 트렌드를 포함하세요.""")
return {
"markdown": markdown,
"summary": summary,
"image": table_image,
}class MultimodalRAGRetriever:
def __init__(self, text_store, image_store, page_store):
self.text_store = text_store
self.image_store = image_store
self.page_store = page_store
def retrieve(self, query: str, top_k: int = 5) -> list[DocumentChunk]:
"""멀티모달 검색"""
# 텍스트 검색
text_results = self.text_store.search(
encode_text(query), top_k=top_k
)
# 이미지/차트 검색 (CLIP)
image_results = self.image_store.search(
encode_clip(query), top_k=top_k
)
# 페이지 검색 (ColPali)
page_results = self.page_store.search(
encode_colpali(query), top_k=top_k
)
# 결과 통합 및 중복 제거
all_results = merge_and_deduplicate(
text_results, image_results, page_results
)
# 리랭킹
return rerank(query, all_results, top_k=top_k)def generate_answer(
query: str,
retrieved_chunks: list[DocumentChunk],
vlm_client,
) -> str:
"""검색된 멀티모달 청크로 답변 생성"""
content = []
for chunk in retrieved_chunks:
if chunk.image_data:
# 이미지가 있는 청크는 이미지와 캡션 모두 전달
img_b64 = base64.standard_b64encode(chunk.image_data).decode()
content.append({
"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": img_b64},
})
content.append({
"type": "text",
"text": f"[{chunk.chunk_type} — 페이지 {chunk.page_number}]\n{chunk.content}",
})
else:
content.append({
"type": "text",
"text": f"[텍스트 — 페이지 {chunk.page_number}]\n{chunk.content}",
})
content.append({
"type": "text",
"text": f"""위 자료를 기반으로 다음 질문에 답변해주세요.
질문: {query}
답변 시 출처(페이지 번호)를 명시하고,
시각 자료(표, 차트)의 데이터를 활용하여 구체적으로 답변해주세요.""",
})
response = vlm_client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{"role": "user", "content": content}],
)
return response.content[0].text멀티모달 RAG에서 이미지를 LLM에 전달할 때 토큰 비용이 크게 증가합니다. 고해상도 이미지 하나가 수천 토큰을 소비할 수 있으므로, 검색된 이미지의 수와 해상도를 관리하는 것이 중요합니다. 핵심 시각 자료만 선별하고, 불필요한 이미지는 텍스트 캡션으로 대체하는 전략이 비용 효율적입니다.
| 지표 | 측정 대상 | 방법 |
|---|---|---|
| Retrieval Recall | 관련 문서의 검색 비율 | 정답 문서 중 검색된 비율 |
| Visual Grounding | 시각 자료 활용도 | 답변이 차트/표의 데이터를 정확히 인용했는지 |
| Answer Faithfulness | 답변의 충실성 | 검색된 자료에 근거한 답변인지 |
| Cross-Modal Accuracy | 크로스모달 정확도 | 텍스트 질문으로 올바른 이미지를 검색했는지 |
멀티모달 RAG는 텍스트, 이미지, 표, 차트 등 문서의 모든 정보를 활용하는 검색-생성 파이프라인입니다. ColPali의 비전 기반 검색, VLM 캡셔닝, 하이브리드 검색을 결합하면 전통적 텍스트 RAG의 한계를 넘어설 수 있습니다. 비용과 정확도의 균형을 위해 문서 유형에 맞는 파싱 전략과 검색 방식을 선택하는 것이 중요합니다.
다음 장에서는 멀티모달 능력을 갖춘 AI 에이전트를 구축합니다. 화면을 보고 상호작용하는 에이전트, 다중 도구를 활용하는 멀티모달 에이전트의 설계 패턴을 배웁니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
시각적 이해 능력을 갖춘 AI 에이전트의 설계와 구현 — 화면 상호작용 에이전트, 멀티모달 도구 호출, Computer Use, 그리고 실전 에이전트 패턴을 다룹니다.
CLIP 기반 멀티모달 임베딩의 원리, 텍스트-이미지 크로스모달 검색, 통합 벡터 스토어 설계, 그리고 실전 멀티모달 검색 시스템 구축을 다룹니다.
멀티모달 AI 시스템의 프로덕션 배포 전략 — 서빙 인프라, 비용 관리, 지연 시간 최적화, 캐싱, 모니터링, 그리고 확장성 설계를 다룹니다.