비전-언어 모델 프로덕션: VQA와 멀티모달 AI 서비스 구축

AI 기술

비전 언어 모델VQA멀티모달 AIClaude Vision이미지 분석

이 글은 누구를 위한 것인가

  • 이미지에서 자동으로 정보를 추출해야 하는 팀
  • Claude Vision API를 프로덕션에 연동하려는 개발자
  • 차트, 문서, 스크린샷을 AI로 분석하고 싶은 엔지니어

들어가며

제품 이미지에서 결함을 감지하고, 차트에서 수치를 추출하고, 스크린샷에서 UI 버그를 찾는다. 비전-언어 모델이 이 모든 것을 텍스트 한 줄 지시로 처리한다.

이 글은 bluefoxdev.kr의 멀티모달 AI 활용 을 참고하여 작성했습니다.


1. VQA 시스템 아키텍처

[비전 AI 처리 흐름]

이미지 입력:
  URL (HTTP 다운로드)
  base64 인코딩 (직접 전송)
  파일 경로 (로컬)

전처리:
  크기 최적화: 최대 1568x1568 (Claude 제한)
  포맷 변환: PNG/JPEG/WebP/GIF
  용량 최적화: quality 85%로 압축
  비용: 이미지 크기에 비례 (토큰 계산)

분석 유형:
  VQA: 이미지에 대한 질의응답
  OCR: 텍스트 추출
  차트 분석: 수치 데이터 추출
  결함 감지: 품질 검사
  UI 분석: 스크린샷 접근성 체크

[비용 최적화]
  이미지 토큰 비용:
    1024x1024: ~1600 토큰
    512x512: ~400 토큰
    작게 리사이징하면 비용 75% 절감
  
  배치 처리:
    여러 이미지를 한 요청에
    또는 비동기 병렬 처리

2. Vision API 프로덕션 구현

import anthropic
import base64
import io
import json
from dataclasses import dataclass
from pathlib import Path
from PIL import Image

client = anthropic.Anthropic()

@dataclass
class ImageAnalysisResult:
    description: str
    extracted_data: dict
    confidence: float
    image_quality: str  # 'high', 'medium', 'low'

def preprocess_image(
    image_path: str,
    max_size: int = 1024,
    quality: int = 85,
) -> tuple[str, str]:
    """이미지 전처리 및 base64 인코딩"""
    
    with Image.open(image_path) as img:
        # RGBA → RGB 변환
        if img.mode in ("RGBA", "LA"):
            background = Image.new("RGB", img.size, (255, 255, 255))
            background.paste(img, mask=img.split()[-1])
            img = background
        elif img.mode != "RGB":
            img = img.convert("RGB")
        
        # 크기 최적화
        if max(img.width, img.height) > max_size:
            img.thumbnail((max_size, max_size), Image.LANCZOS)
        
        # 압축
        buf = io.BytesIO()
        img.save(buf, format="JPEG", quality=quality, optimize=True)
        
        encoded = base64.standard_b64encode(buf.getvalue()).decode()
        return encoded, "image/jpeg"

def make_image_content(image_path: str) -> dict:
    """이미지 Content 블록 생성"""
    encoded, media_type = preprocess_image(image_path)
    return {
        "type": "image",
        "source": {
            "type": "base64",
            "media_type": media_type,
            "data": encoded,
        },
    }

async def analyze_product_defects(
    image_path: str,
    product_type: str,
) -> dict:
    """제품 결함 감지"""
    
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=800,
        messages=[{
            "role": "user",
            "content": [
                make_image_content(image_path),
                {
                    "type": "text",
                    "text": f"""이 {product_type} 이미지에서 품질 결함을 분석하세요.

JSON으로 반환:
{{
  "defects_found": true/false,
  "defect_count": 숫자,
  "defects": [
    {{
      "type": "결함 유형 (스크래치/균열/오염/변형 등)",
      "location": "위치 설명",
      "severity": "minor/moderate/severe",
      "description": "상세 설명"
    }}
  ],
  "overall_quality": "pass/fail/review",
  "confidence": 0.0-1.0,
  "recommendation": "처리 권고사항"
}}"""
                }
            ]
        }]
    )
    
    return json.loads(response.content[0].text)

async def extract_chart_data(image_path: str) -> dict:
    """차트/그래프에서 데이터 추출"""
    
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1000,
        messages=[{
            "role": "user",
            "content": [
                make_image_content(image_path),
                {
                    "type": "text",
                    "text": """이 차트에서 데이터를 추출하세요.

JSON으로 반환:
{
  "chart_type": "bar/line/pie/scatter/table",
  "title": "차트 제목",
  "x_axis_label": "X축 레이블",
  "y_axis_label": "Y축 레이블",
  "data_points": [{"label": "항목", "value": 수치, "unit": "단위"}],
  "key_insights": ["인사이트1", "인사이트2"],
  "confidence": 0.0-1.0
}"""
                }
            ]
        }]
    )
    
    return json.loads(response.content[0].text)

async def analyze_ui_screenshot(
    screenshot_path: str,
    check_type: str = "accessibility",
) -> dict:
    """UI 스크린샷 분석"""
    
    prompts = {
        "accessibility": "접근성 문제를 찾으세요: 색상 대비, 텍스트 크기, 버튼 크기, ARIA 레이블 누락",
        "ux": "UX 개선 포인트를 찾으세요: 혼란스러운 레이아웃, 불명확한 CTA, 정보 과부하",
        "bugs": "UI 버그를 찾으세요: 깨진 레이아웃, 잘린 텍스트, 겹치는 요소",
    }
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=800,
        messages=[{
            "role": "user",
            "content": [
                make_image_content(screenshot_path),
                {
                    "type": "text",
                    "text": f"""{prompts.get(check_type, "이 UI를 분석하세요")}

JSON으로 반환:
{{
  "issues": [
    {{"type": "문제 유형", "element": "어떤 요소", "description": "설명", "priority": "high/medium/low"}}
  ],
  "score": 1-10,
  "summary": "전체 요약"
}}"""
                }
            ]
        }]
    )
    
    return json.loads(response.content[0].text)

async def batch_analyze_images(
    image_paths: list[str],
    analysis_fn,
    max_concurrent: int = 5,
) -> list[dict]:
    """이미지 배치 병렬 처리"""
    import asyncio
    
    semaphore = asyncio.Semaphore(max_concurrent)
    
    async def bounded_analyze(path: str) -> dict:
        async with semaphore:
            try:
                return {"path": path, "result": await analysis_fn(path), "success": True}
            except Exception as e:
                return {"path": path, "error": str(e), "success": False}
    
    return await asyncio.gather(*[bounded_analyze(p) for p in image_paths])

마무리

Vision API 비용의 핵심은 이미지 크기다. 1024px로 리사이징하면 4배 저렴해진다. 프로덕션에서는 이미지를 S3에 저장하고 URL로 전달하는 방식이 base64보다 네트워크 효율적이다. 결함 감지, 차트 분석, UI 검토를 조합하면 QA 인력을 대폭 절감할 수 있다.