대부분의 RAG 튜토리얼은 노트북 단계에서 끝납니다. 몇 개의 잘 정돈된 PDF를 불러오고, 텍스트를 1,000자마다 자르고, 그 조각들을 벡터 데이터베이스에 집어넣은 뒤 이를 아키텍처라고 부릅니다. 금요일 오후라면 그런 데모는 완벽하게 돌아갈 것입니다. 하지만 실제 운영 환경에서 동일한 파이프라인은 조용히 리스크로 변합니다.
검색 시스템의 진짜 병목 현상은 모델이나 프롬프트가 아닙니다. 바로 인제스션(Ingestion)입니다. RAG 파이프라인은 입력된 데이터만을 검색할 수 있으며, 만약 입력 데이터에 노이즈가 많거나, 오래되었거나, 불완전하다면 모델은 자신 있게 헛소리를 내뱉을 것입니다. 사용자가 봇이 환각(hallucination)을 일으킨다고 불평할 때, 그 잘못은 종종 아무도 면밀히 모니터링하지 않는 데이터 파이프라인의 훨씬 상류 단계에 있습니다.
화이트보드의 함정
아키텍처 다이어그램은 인제스션을 "Documents → Vector DB"라고 표시된 단일 화살표처럼 보이게 만듭니다. 하지만 현실은 훨씬 복잡합니다. 소스 시스템은 예고 없이 변경됩니다. HTML 레이아웃은 재설계됩니다. URL은 일반적인 랜딩 페이지로 리다이렉트됩니다. 자바스크립트 프레임워크는 초기 HTTP 응답 이후에 콘텐츠를 교체해 버리기도 합니다. 인제스션을 일회성 설정 작업으로 취급하는 것이 첫 번째 실수입니다. 이는 모든 ETL 파이프라인과 마찬가지로 엄격한 관리가 필요한 지속적인 데이터 엔지니어링 문제입니다.
RAG 실패가 주로 데이터 공급 실패인 이유
이런 상황을 가정해 봅시다. 사용자가 내부 어시스턴트에게 현재 환불 정책에 대해 묻습니다. 모델은 벡터 저장소에서 가장 유사한 청크를 가져와 환불 기간이 30일이라고 답변합니다. 하지만 실제 정책은 지난 분기에 60일로 변경되었습니다. LLM이 틀린 답을 지어낸 것이 아닙니다. 잘못된 입력을 신뢰했을 뿐입니다. 검색 레이어가 오래된 페이지를 제공했고, 임베딩이 의미론적으로 충분히 유사해 보였기 때문에 모델은 이를 정답(ground truth)으로 취급한 것입니다.
이러한 패턴은 끊임없이 반복됩니다. 팀들은 코퍼스(corpus)가 내비게이션 푸터, 중복된 보도 자료, 그리고 표를 반으로 쪼개버린 청크들로 가득 차 있음에도 불구하고, 정작 생성(generation) 단계의 temperature나 top-k를 조정하는 데 수 시간을 허비합니다. 생성 과정을 최적화하기 전에, 시스템이 무엇을 알 수 있도록 허용되었는지부터 감사하십시오.
인제스션을 망치는 7가지 함정
1. 첫 번째 실행 결과는 거짓입니다
초기 크롤링에서 초록색 체크 표시가 떴다고 해서 안심해서는 안 됩니다. 운영 데이터는 살아 움직입니다. 문서 페이지는 리팩토링되고, 블로그 퍼머링크는 깨지며, 사이트맵에서 섹션이 조용히 사라지기도 합니다. 파이프라인이 에러 없이 완료되었는지만 확인한다면, 당신은 눈을 가리고 비행하는 것과 같습니다. 출력값을 검증해야 합니다. 예상한 문서들이 존재하는지, 구조가 여전히 제대로 파싱되는지, 그리고 소스 측에서 페이지네이션 방식을 변경하여 전체 텍스트 양이 급감하지 않았는지 확인해야 합니다.
2. 크롤링은 인제스션이 아닙니다
HTML을 가져오는 것은 쉬운 부분입니다. 가공되지 않은 크롤링 데이터는 쿠키 배너, "관련 기사" 사이드바, 광고 블록, 푸터의 저작권 고지까지 모든 것을 캡처합니다. 만약 이런 가공되지 않은 HTML을 무지성으로 청킹한다면, 모든 텍스트 조각에는 내비게이션 메뉴의 파편들이 따라붙게 됩니다. 사용자가 API 속도 제한에 대해 물었을 때, 검색기가 사이드바 링크가 40%나 섞인 청크를 내놓을 수도 있습니다. 깨끗한 추출(Clean extraction)이 중요합니다. 메인 콘텐츠 영역을 식별하고, 보일러플레이트(boilerplate)를 제거하며, 모든 페이지에서 반복되는 요소들을 삭제해야 합니다. 그렇지 않으면 당신은 지식 베이스를 구축하는 것이 아니라, 웹사이트의 UI 요소(chrome)를 위한 검색 엔진을 만들고 있는 셈입니다.
3. 청킹은 의미를 파괴합니다
고정 크기 청킹(Fixed-size chunking)은 거의 모든 퀵스타트 가이드의 기본 설정이지만, 이는 위험합니다. 단순히 글자 수로만 문서를 나누면 표를 중간에서 잘라버리고, 번호가 매겨진 절차에서 4단계와 5단계를 분리하며, 불렛 포인트들을 헤딩(heading)으로부터 고립시킵니다. 가격표의 하단 절반만 포함된 청크는 의미론적으로 아무런 쓸모가 없습니다. 구조 인식형 청킹(Structure-aware chunking)은 원래의 형식을 존중해야 합니다. 헤딩 계층 구조를 파싱하십시오. 가능한 한 표를 온전하게 유지하십시오. 동일한 H2 또는 H3 아래에 있는 문단 경계에서 나누십시오. 불렛 포인트가 충분히 짧다면 하나의 청크 안에 보존하십시오. 목표는 균일한 크기의 블록을 만드는 것이 아닙니다. 목표는 일관된 의미 단위(coherent units of meaning)를 만드는 것입니다.
4. 신선도 문제
내부 위키의 정적 스냅샷을 가져오는 것은 간단합니다. 하지만 라이브 웹에서 지속적으로 데이터를 수집(ingesting)하는 것은 어렵습니다. 페이지가 마지막으로 수집된 시점, 그 이후 변경 여부, 그리고 해당 정보가 얼마나 오랫동안 유효한지를 알아야 합니다. 오래된 데이터(Stale data)가 반드시 눈에 보이는 오래된 날짜를 의미하는 것은 아닙니다. 때로는 페이지의 텍스트는 업데이트되었지만 URL은 그대로 유지되는 경우가 있어, 콘텐츠 해싱(content hashing) 없이는 시스템이 이를 감지하지 못할 수 있습니다. 소스의 변동성에 따라 명확한 갱신 규칙을 구축하십시오. 금융 데이터 피드는 시간 단위 확인이 필요할 수 있고, 기업 소개 페이지는 분기별 확인이면 충분할 수 있습니다. 타임스탬프를 기록하고 TTL(time-to-live) 범위를 설정하십시오. 특히 오래된 사실이 실제 피해를 줄 수 있는 규제 대상이나 안전이 중요한 분야라면 더욱 그렇습니다.
5. 중복 오염 (Duplicate Pollution)
웹사이트는 반복되는 내용으로 가득 차 있습니다. 동일한 제품 설명이 카테고리 페이지, 제품 페이지, 프로모션 랜딩 페이지에 모두 나타납니다. 동일한 보도 자료가 /news/, /press/, /blog/ 경로에 각각 존재하기도 합니다. 벡터 검색은 자동으로 중복을 제거하지 않습니다. 데이터베이스에 거의 동일한 청크(chunk)가 10개나 들어 있다면, top-k 검색 결과에서 다양하고 관련성 높은 결과들을 밀어낼 수 있습니다. 임베딩(embedding) 전에 정식(canonical) 추적이나 콘텐츠 중복 제거가 필요합니다. 두 청크가 같은 내용을 담고 있다면, 권위 있는 소스만 남기고 복사본은 버리십시오. 리트리버(retriever)의 슬롯은 제한되어 있습니다. 이를 낭비하지 마십시오.
6. 메타데이터 누락 (Missing Metadata)
메타데이터가 없는 벡터 데이터베이스는 문맥(context)에 대한 기억이 없는 밀집 텍스트 검색 엔진에 불과합니다. 스마트한 검색은 원시 임베딩(raw embeddings)이 제공할 수 없는 필터링 및 랭킹 신호에 의존합니다. 소스 URL, 수집 날짜, 문서 카테고리, 버전 번호를 저장하십시오. API 문서를 수집한다면 버전 관리는 필수적입니다. 버전 관리가 없으면 쿼리가 v1과 v2의 사양을 섞어서 하나의 답변으로 내놓을 수 있습니다. 인사 정책을 수집하는 경우, 지역이나 부서별로 태깅을 해두면 모델에 전달되기 전에 결과를 필터링할 수 있습니다. 메타데이터는 단순한 텍스트 더미를 정제된 지식 시스템으로 바꿔줍니다.
7. JavaScript 격차 (JavaScript Gaps)
현대적인 사이트들은 첫 번째 HTML 페이로드에 콘텐츠를 모두 담아 보내지 않습니다. 대신 스켈레톤(skeleton) 구조를 보낸 뒤 JavaScript 호출을 통해 데이터를 채워 넣습니다(hydrate). 기본적인 HTTP 요청만으로는 로딩 스피너와 레이아웃 껍데기 외에는 아무것도 볼 수 없을 수도 있습니다. 파이프라인이 JavaScript를 실행할 수 없다면, 빈 페이지나 불완전한 파편만을 수집하게 되고 무엇이 잘못되었는지조차 인지하지 못할 것입니다. 헤드리스 브라우저(headless browser)를 사용하면 렌더링 문제는 해결되지만, 더 많은 메모리 사용량, 느린 처리량(throughput), 봇 탐지 차단벽과 같은 새로운 문제들이 발생합니다. 트레이드오프(trade-offs)를 신중하게 선택하되, 모든 소스에 대해 단순한 curl 수준으로 충분하다고 착각하지 마십시오.
실무적인 수집 체크리스트
RAG 피드를 구축하거나 검토 중이라면, 다음 사항부터 시작하십시오:
- 소스 커버리지와 페이지네이션(pagination)을 검증하십시오. 사이트맵에는 카테고리의 처음 10개 기사만 나열되어 있을 수 있습니다. 깊이 크롤링하여 페이지가 나뉘어 있거나 동적으로 로드되는 콘텐츠가 실제로 수집되는지 확인하십시오.
- 청킹(chunking) 전에 불필요한 요소(boilerplate)를 제거하십시오. 네비게이션, 광고, 푸터, 반복되는 법적 고지 사항을 제거하십시오. 모든 페이지에 나타나는 문구는 노이즈입니다.
- 구조를 인식하는 청킹(structure-aware chunking)을 사용하십시오. 제목, 글머리 기호 목록, 표를 존중하십시오. 글자 수가 아닌 의미적 경계(semantic boundaries)를 기준으로 나누십시오.
- 풍부한 메타데이터를 첨부하십시오. URL, 수집 날짜, 콘텐츠 카테고리, 버전을 포함하십시오. 검색 쿼리에서 이러한 필드들을 필터링할 수 있도록 만드십시오.
- 데이터 변동성에 따라 갱신 빈도를 설정하십시오. 변경이 잦은 소스는 빈번한 재크롤링이 필요하지만, 정적 아카이브는 그렇지 않습니다.
- 작업 상태뿐만 아니라 코퍼스(corpus) 자체를 모니터링하십시오. 파이프라인이 쓰레기 데이터를 생성하면서도 종료 코드는 0(성공)으로 반환할 수 있습니다. 저장된 청크 샘플을 정기적으로 감사하여 데이터 드리프트(drift)와 품질을 확인하십시오.
- 버전 관리 및 삭제 규칙을 정의하십시오. 소스 페이지가 삭제되면 해당 청크도 삭제하십시오. 업데이트되면 덮어쓰거나 버전을 관리하십시오. 고립된 데이터(Orphaned data)는 소리 없는 살인마와 같습니다.
임베딩에 관한 냉혹한 진실
아무리 진보된 임베딩 모델이라도 누락된 문서를 복구할 수는 없습니다. 피드에 여전히 작년 데이터가 남아 있다면, 모델이 페이지가 지난주에 업데이트되었다는 사실을 추측할 수는 없습니다. 잘못된 청크 경계로 인해 헤더와 분리된 표 행의 문맥을 추론할 수도 없습니다. 임베딩은 의미를 압축하지만, 수집 계층(ingestion layer)이 의미를 보존하는 데 실패했을 때 새로운 의미를 만들어내지는 못합니다.
검색 품질은 수집 계층에서 시작됩니다. 이 계층이 여러분의 RAG 시스템을 유용한 도구로 만들지, 아니면 벡터 데이터베이스를 등에 업은 '자신감 넘치는 거짓말쟁이'로 만들지를 결정합니다.
핵심 요약
파이프라인 대시보드만으로 데이터 수집(ingestion) 상태를 판단하지 마십시오. 작업이 성공적으로 완료되고 로그가 깨끗하다고 해서 코퍼스가 깨끗하다는 보장은 없습니다. 데이터베이스를 직접 열어 사용자가 실제로 가져가게 될 청크(chunk)를 확인하십시오. 만약 텍스트가 저작권 고지, 깨진 테이블, 오래된 정책 페이지로 가득 차 있다면, 문제는 LLM이 아닙니다. 데이터 피드부터 바로잡으십시오. 그 외의 모든 작업은 결국 쓰레기 위에 튜닝을 하는 것에 불과합니다.
