2편까지 정리하고 나니 인덱싱 쪽은 어느 정도 자신이 붙었다. 그런데 정작 데모 날짜가 다가오면서 진짜 부담스러웠던 건 따로 있었다. 현장 엔지니어가 설비 사진을 찍어서 바로 질문하는 시나리오였다. 사용자가 이미지를 올리는 순간부터 답변이 나오기까지, 텍스트 전용 RAG에서는 신경 쓸 필요 없던 문제들이 줄줄이 튀어나왔다. 이미지를 받는 엔드포인트를 어떻게 설계할지, 이미지에서 뽑은 정보와 벡터 검색 결과를 어떻게 하나의 답변으로 합칠지, 그리고 이 모든 걸 실제 서비스로 내놓을 때 뭘 점검해야 하는지까지. 이번 글에서는 실시간 이미지 질의 아키텍처를 어떻게 설계했고, 프로덕션에 올리면서 무엇을 놓쳤었는지를 정리한다. 결과적으로는 잘 돌아가는 시스템을 만들었지만, 그 과정에서 새벽에 알림을 받고 일어난 날도 적지 않았다.
이미지와 텍스트를 동시에 받는 엔드포인트 설계
텍스트만 받던 질의 API는 요청 바디에 문자열 하나만 있으면 끝났다. 이미지가 섞이는 순간 이 구조를 통째로 바꿔야 한다. 나는 FastAPI에서 멀티파트 폼 데이터로 이미지 파일과 텍스트 질의를 함께 받는 엔드포인트를 만들었다. 여기서 첫 번째로 마주친 문제는 이미지 검증이었다. 클라이언트가 어떤 확장자와 크기의 파일을 보낼지 예측할 수 없기 때문에, 파일 시그니처를 확인해서 실제 이미지 포맷인지 검증하고, 최대 크기를 넘는 요청은 조기에 거절하는 로직이 반드시 있어야 한다. 이 검증을 건너뛰고 바로 VLM 호출로 넘겼다가, 손상된 파일 하나 때문에 워커 프로세스가 죽는 경험을 한 뒤로는 입력 검증 단계를 절대 생략하지 않는다. 파일 확장자만 보고 판단하면 안 되고, 실제 바이너리 헤더를 읽어서 진짜 이미지 파일인지 확인하는 로직까지 넣어야 안전하다는 것도 이때 배웠다. 사용자가 실수로 PDF를 이미지 업로드 칸에 올리는 경우도 종종 있어서, PDF가 들어오면 자동으로 페이지를 이미지로 변환해서 처리하는 예외 분기도 추가했다.
두 번째는 비동기 처리 구조다. 이미지 인코딩과 VLM 호출은 텍스트 임베딩보다 훨씬 오래 걸리는 작업이라서, 동기 방식으로 짜면 요청이 몰릴 때 서버 전체가 느려진다. FastAPI의 비동기 엔드포인트 안에서 이미지 전처리는 별도 스레드풀로 넘기고, VLM 호출과 벡터 검색은 병렬로 실행되도록 짰다. 이미지를 이해하는 작업과 기존 문서에서 관련 청크를 찾는 작업은 서로 의존성이 없기 때문에, 굳이 순차적으로 기다릴 이유가 없다는 걸 뒤늦게 깨달았다. 처음에는 VLM이 이미지를 다 이해한 다음에야 검색어를 만들 수 있다고 생각해서 순차 구조로 짰는데, 실제로는 사용자가 입력한 텍스트 질의만으로도 1차 검색을 먼저 시작할 수 있는 경우가 많았다.
세 번째로 신경 쓴 부분은 이미지 리사이징 시점이다. 원본 이미지를 그대로 VLM에 넘기면 처리 시간도 길어지고 메모리 부담도 커진다. 그렇다고 무조건 작게 리사이즈하면 세밀한 정보가 손실될 수 있다. 나는 엔드포인트 레벨에서 이미지 종류를 먼저 대략적으로 판별해서, 사진처럼 일반적인 이미지는 적당히 압축하고, 표나 도면처럼 텍스트 정보가 많은 이미지는 원본 해상도를 최대한 유지하는 방식으로 분기 처리했다. 이 분기 로직 하나로 평균 응답 시간을 눈에 띄게 줄일 수 있었다.
이미지 이해 결과와 벡터 검색 결과를 하나로 합치는 법
이 부분이 실시간 멀티모달 질의에서 가장 까다로운 지점이다. 사용자가 사진을 올리고 “이 부품 이름이 뭐고 관련 매뉴얼 내용 찾아줘”라고 물으면, 시스템은 먼저 이미지를 VLM에 전달해서 부품 식별이나 상세 설명을 얻어낸다. 이 결과를 그대로 검색 쿼리로 쓰면 안 되는 경우가 많다. VLM이 생성한 설명은 문장이 길고 부수적인 정보가 섞여 있어서, 벡터 검색에 그대로 넣으면 노이즈가 커진다. 나는 VLM 응답에서 핵심 키워드만 다시 추출하는 짧은 후처리 단계를 하나 더 넣었다. 이미지 설명 전체가 아니라, 부품명이나 모델 번호처럼 검색에 실제로 도움이 되는 용어만 뽑아서 사용자의 텍스트 질의와 결합한 다음 벡터 검색을 돌리는 구조다.
검색이 끝나면 마지막으로 답변을 합성하는 단계가 남는다. 이미지에서 얻은 이해 결과, 벡터 검색으로 찾은 문서 청크, 그리고 사용자의 원래 질문을 모두 LLM에 컨텍스트로 넘겨서 최종 답변을 생성한다. 여기서 흔히 놓치는 부분이 출처 표시다. 답변이 이미지 자체에서 나온 정보인지, 검색된 문서에서 나온 정보인지 사용자가 구분할 수 있어야 신뢰도가 올라간다. 나는 답변을 생성할 때 이미지 기반 설명과 문서 기반 설명을 구조적으로 분리해서, 프론트엔드에서 각각 다른 섹션으로 보여주도록 프롬프트를 설계했다. 이렇게 하니 현장 엔지니어들이 답변을 훨씬 신뢰하기 시작했다는 피드백을 받았다.
검색 단계에서 하나 더 배운 게 있다. 이미지 기반 키워드와 텍스트 질의를 단순히 이어붙여서 하나의 검색어로 만들면, 경우에 따라 서로 다른 문서를 가리키는 키워드가 섞여서 검색 정확도가 오히려 떨어지는 상황이 있었다. 그래서 나는 이미지 기반 검색과 텍스트 기반 검색을 각각 별도로 돌린 다음, 두 결과를 리랭킹 단계에서 합치는 방식으로 바꿨다. 두 경로에서 겹치는 문서가 나오면 그 문서의 신뢰도를 가중치로 올려주는 로직도 함께 넣었는데, 이 방식으로 바꾸고 나서 관련 없는 문서가 답변에 섞여 들어가는 빈도가 확실히 줄었다.
이미지가 섞인 대화의 컨텍스트 관리
멀티턴 대화에서 이미지가 한 번 등장하면 그 다음 턴에서도 계속 참조될 수 있다는 점도 처음에는 놓쳤던 부분이다. 사용자가 사진을 올리고 질문한 다음, 다음 턴에서 “그럼 이거 교체 부품 어디서 구해”라고 물으면 이 질문의 이거는 앞서 올린 이미지를 가리킨다. 텍스트만 다루던 대화 히스토리 관리 로직으로는 이 참조를 처리할 수 없다. 나는 대화 세션마다 업로드된 이미지와 그 이미지에 대한 VLM 분석 결과를 별도로 캐싱해두고, 이후 턴에서 이미지를 다시 참조하는 표현이 감지되면 캐싱된 분석 결과를 컨텍스트에 다시 포함시키는 방식으로 처리했다. 매 턴마다 이미지를 다시 VLM에 태우는 건 비용도 크고 지연도 늘어나기 때문에, 한 번 분석한 결과를 세션 동안 재사용하는 구조가 훨씬 효율적이다.
이미지 참조가 여러 장으로 늘어나는 경우도 고려해야 했다. 사용자가 같은 대화 안에서 이미지를 두세 장 연달아 올리고 서로 비교해달라고 요청하는 패턴이 실제로 자주 나왔다. 이때는 세션 캐시에 이미지별로 고유 식별자를 부여하고, 사용자의 다음 질문이 어떤 이미지를 가리키는지 애매할 때는 가장 최근에 올라온 이미지를 기본값으로 삼되, 이전 이미지도 함께 참조할 수 있도록 컨텍스트에 남겨두는 방식으로 처리했다. 완벽한 해결책은 아니었지만, 사용자가 명시적으로 첫 번째 사진이라거나 아까 그 사진이라고 지칭할 때 그나마 자연스럽게 대응할 수 있었다.
프로덕션 배포 전에 반드시 점검해야 했던 것들
기능이 완성됐다고 바로 배포할 수 있는 건 아니었다. 가장 먼저 부딪힌 건 비용 통제였다. 이미지가 섞인 요청은 텍스트만 있는 요청보다 처리 비용이 훨씬 크다. 사용자별로 이미지 업로드 요청 수에 제한을 걸지 않으면, 악의적이지 않더라도 예상보다 훨씬 큰 비용이 나갈 수 있다. 나는 사용자 단위, 그리고 세션 단위로 이미지 업로드 횟수에 레이트 리밋을 걸었다. 처음에는 이 제한이 사용성을 해치지 않을까 걱정했는데, 실제 사용 패턴을 보니 정상적인 사용자는 한 세션에서 이미지를 대여섯 장 이상 올리는 경우가 거의 없었고, 제한을 넘긴 요청은 대부분 테스트성 트래픽이거나 잘못된 클라이언트 재시도 로직 때문이었다.
모니터링 관점에서도 텍스트 전용 시스템과는 다른 지표가 필요했다. 이미지 인코딩 단계의 지연 시간, VLM 호출 실패율, 그리고 이미지 크기 분포를 별도로 추적하는 대시보드를 새로 만들었다. 배포 초기에 이미지 인코딩 지연이 전체 응답 시간의 상당 부분을 차지한다는 걸 이 대시보드를 통해서야 확인할 수 있었고, 이 데이터를 근거로 이미지 전처리 단계에서 리사이징 기준을 다시 조정했다. 장애 알림 기준도 텍스트 전용 시스템 때와는 다르게 잡아야 했다. 이미지 처리 실패율은 텍스트 처리 실패율보다 자연스럽게 높게 나오는 경향이 있어서, 같은 임계값으로 알림을 걸어두면 불필요한 알림이 계속 울렸다. 몇 주간 실제 데이터를 쌓아보고 나서야 적절한 기준을 잡을 수 있었다.
보안 측면에서도 신경 쓸 게 늘었다. 사용자가 업로드하는 이미지에 개인정보나 민감한 사내 정보가 담길 가능성이 텍스트 질의보다 훨씬 크다. 얼굴이나 문서 스캔본이 실수로 올라오는 경우를 대비해서, 업로드된 이미지는 일정 시간이 지나면 자동으로 삭제되도록 보관 정책을 정했고, VLM 호출 로그에도 이미지 원본은 남기지 않고 분석 결과 텍스트만 저장하도록 바꿨다. 이 부분은 처음 설계 단계에서는 전혀 고려하지 못했다가, 보안팀 리뷰를 거치면서 뒤늦게 추가한 요구사항이었다.
비용 최적화를 위해 캐싱 전략도 다시 짰다. 같은 설비 사진을 여러 사용자가 반복해서 올리는 경우가 생각보다 많았다. 예를 들어 특정 고장 유형이 자주 발생하는 설비라면, 여러 현장에서 비슷한 각도로 찍은 사진이 반복적으로 들어온다. 이럴 때마다 VLM을 새로 호출하는 건 명백한 낭비다. 나는 이미지의 해시값을 기준으로 이전에 분석한 적 있는 이미지인지 확인하고, 일치하는 경우 캐싱된 분석 결과를 재사용하도록 만들었다. 완전히 동일한 이미지가 아니더라도 유사도가 높은 이미지는 별도로 추적해서, 캐시 적중률을 어떻게 더 끌어올릴지는 지금도 계속 실험 중이다.
결론, 멀티모달 RAG는 인프라와 제품 설계가 함께 가야 한다
세 편에 걸쳐 정리하고 나니 확실해진 게 하나 있다. 멀티모달 RAG는 모델 하나 바꿔 끼운다고 되는 일이 아니라는 것이다. 인덱싱 단계에서 이미지를 캡셔닝할지 임베딩할지 결정하는 것부터, VLM을 어떤 기준으로 고르고 vLLM으로 어떻게 서빙할지, 그리고 마지막으로 실시간 이미지 질의를 어떻게 설계하고 운영할지까지, 세 층위가 서로 맞물려서 하나의 시스템을 이룬다. 어느 한 층위만 잘 만들어도 나머지가 부실하면 전체 경험이 무너진다. 이 시리즈를 준비하면서 가장 많이 되새긴 원칙은, 텍스트 RAG에서 익숙했던 가정을 하나도 그대로 가져오지 말고 매 단계마다 이미지라는 모달리티가 무엇을 바꾸는지 다시 물어야 한다는 것이었다. 다음에는 세 편을 하나로 묶어서, 멀티모달 RAG를 처음부터 설계하는 사람이 참고할 수 있는 전체 그림을 정리해볼 생각이다.
지금 시스템을 돌이켜보면, 처음 기획 단계에서 예상했던 작업량의 몇 배가 실제로 들어갔다. 텍스트 RAG를 다뤄본 경험이 있으니 이미지 처리 정도는 금방 붙일 수 있을 거라 생각했던 게 가장 큰 오판이었다. 엔드포인트 설계, 검색 결과 합성, 대화 컨텍스트 관리, 비용과 보안까지, 각 단계마다 텍스트 전용 시스템에서는 아예 존재하지 않던 문제들이 새로 생겨났다. 그럼에도 불구하고 실제로 현장에서 사진 한 장으로 원하는 답을 빠르게 찾아내는 모습을 보면, 이 모든 과정을 거칠 만한 가치가 있었다는 확신이 든다.