
RAG가 실제 운영 환경에서 작동하는 방식 (모델은 쉬운 부분입니다)
요약
RAG 시스템의 핵심이 언어 모델 자체보다 검색(Retrieval) 품질에 있음을 강조하며, 실제 운영 환경에서의 작동 원리를 상세히 설명합니다. 데이터 수집 파이프라인부터 쿼리 처리, 평가 및 유지보수까지의 전 과정을 다룹니다.
핵심 포인트
- RAG의 성패는 모델보다 검색 품질에 의해 결정됨
- 데이터 수집(청킹, 임베딩, 인덱싱) 파이프라인의 중요성
- 검색, 재순위화, 프롬프트 조립으로 이어지는 쿼리 흐름
- 실패 모드 분석 및 검색/생성 단계의 분리 평가 필요성
대부분의 사람들에게 검색 증강 생성 (RAG, Retrieval-Augmented Generation) 시스템이 어떻게 작동하는지 물어보면, 대개 언어 모델 (Language Model)에 관한 답변을 듣게 됩니다. 문서를 어딘가에 넣어두면 모델이 그것을 읽고 질문에 답한다는 식이죠. 모델은 모두가 이야기하는 부분인데, 왜냐하면 모델이야말로 마법처럼 느껴지는 부분이기 때문입니다. 하지만 만약 당신이 이러한 시스템 중 하나를 실제로 출시해 보았고, 모델이 확신에 찬 태도로 잘못된 질문에 답하거나 데이터 안에 바로 놓여 있는 답변을 놓치는 것을 본 적이 있다면, 당신은 이미 불편한 진실을 알고 있을 것입니다. 모델은 쉬운 부분입니다. RAG는 언어 모델의 옷을 입고 있는 검색 (Retrieval) 문제이며, 모델에게 차례가 오기 훨씬 전부터 검색 품질에 따라 생사가 결정됩니다.
이 글은 긴 버전의 설명입니다. 우리는 왜 가공되지 않은 모델 (Raw Model)만으로는 충분하지 않은지, 그리고 RAG가 실제로 무엇을 해결하는지부터 시작하여, 오프라인에서 실행되는 데이터 수집 파이프라인 (Ingestion Pipeline)인 청킹 (Chunking), 임베딩 (Embedding), 인덱싱 (Indexing) 과정을 살펴볼 것입니다. 그다음 단일 쿼리 (Query)가 검색 (Retrieval), 재순위화 (Reranking), 프롬프트 조립 (Prompt Assembly)을 거쳐 모델이 우리가 찾아낸 정보에 근거하여 어떻게 답변을 생성하는지 따라가 보겠습니다. 그 후에는 이 시스템이 실제 사용자와 접촉했을 때 살아남을 수 있을지를 결정하는 부분들, 즉 실패 모드 (Failure Modes), 두 부분(검색과 생성)이 서로 다르게 실패하는 시스템을 어떻게 평가하는지, 그리고 인덱스 (Index)가 오래되지 않게 유지하는 방법을 다룹니다. 마지막으로 언어 모델과는 전혀 상관없는 시스템에도 적용할 수 있는 패턴들을 뽑아낼 것입니다. 저는 특정 업체의 내부 수치를 인용하지 않을 것입니다. 청크 크기 (Chunk Sizes), 검색할 구절의 수, 임베딩 차원 (Embedding Dimensions)과 같이 수치가 중요한 부분에 대해서는 업계의 전형적인 범위를 제시하고 그렇게 명시하겠습니다. 왜냐하면 전달되어야 하는 것은 정확한 수치가 아니라 그 논리이기 때문입니다.
왜 가공되지 않은 모델만으로는 충분하지 않은가
언어 모델 (Language model)은 자신이 학습한 내용만을 알고 있으며, 그 외의 것은 알지 못합니다. 모델의 지식은 학습 중단 시점 (training cutoff)에 고정되어 있으므로, 그 날짜 이후에 발생한 일은 전혀 본 적이 없습니다. 모델은 귀사의 내부 위키, 고객 지원 티켓, 제품의 현재 가격, 또는 사용자가 30초 전에 업로드한 계약서 등을 본 적이 없습니다. 그 중 어느 것도 학습 데이터에 포함되어 있지 않으며, 해당 데이터가 비공개이거나 최신 정보이거나 혹은 둘 다이기 때문에 포함될 수도 없습니다.

가공되지 않은 모델 (raw model)은 고정된 학습 지식으로부터 답변하며, 그 범위를 벗어난 질문을 받으면 그럴듯한 세부 사항을 지어냅니다. 반면 RAG는 쿼리 시점에 검색된 문서에 답변의 근거를 둡니다.
이제 이러한 모델에게 한 번도 본 적 없는 데이터에 대해 질문했을 때 어떤 일이 발생하는지 살펴보십시오. 모델은 멈춰서 모른다고 말하지 않습니다. 그것은 이 모델들이 작동하는 방식이 아닙니다. 모델들은 유창하고 그럴듯한 텍스트의 연속을 생성하도록 학습되었으며, 자신 있게 틀린 답변을 내놓는 것이 무지를 인정하는 것보다 더 유창합니다. 따라서 모델은 그 공백을 그럴듯하게 들리는 무언가, 즉 지어낸 인용구, 존재하지 않는 정책, 혹은 형태는 맞지만 틀린 숫자로 채워버립니다. 우리는 이것을 환각 (hallucination)이라고 부르며, 이는 패치로 해결할 수 있는 버그가 아닙니다. 이는 모델이 답변할 근거가 없는 질문에 대해, 모델이 설계된 목적 그대로를 수행하고 있는 것입니다.
RAG는 이 문제를 직접적으로 해결합니다. 모델에게 기억(memory)에 의존해 답변하라고 요청하는 대신, 먼저 사용자의 데이터에서 관련 문서를 검색(retrieve)한 다음, 그 문서들을 질문과 함께 모델에게 전달하며 오직 제공된 정보만을 바탕으로 답변하도록 지시합니다. 지식은 더 이상 모델의 고정된 가중치(frozen weights) 안에 머물지 않습니다. 지식은 사용자가 제어할 수 있는 코퍼스(corpus)에 존재하며, 문서가 변경되는 즉시 업데이트할 수 있습니다. 모델의 역할은 바로 눈앞에 있는 텍스트를 읽고 요약하는 것으로 축소됩니다. 이는 훨씬 더 쉬운 작업이며, 훨씬 더 안전한 작업입니다. 다만, 이 글의 나머지 내용이자 핵심적인 문제는, 답변의 품질이 오직 검색된 문서의 품질에만 의존한다는 점입니다. 만약 검색(retrieval) 단계에서 모델에게 잘못된 구절을 전달한다면, 모델은 그 잘못된 구절을 충실하게 요약할 것이고, 결과적으로 여러분은 원래의 환각(hallucination)보다 잡아내기 더 어려운, 유창하고 출처가 명확해 보이지만 자신 있게 틀린 답변을 얻게 됩니다.
이 시점에서 '왜 지식을 모델에 미세 조정(fine-tune)하여 주입하지 않는가?'라는 합리적인 의문이 들 수 있습니다. 미세 조정(Fine-tuning)은 행동(behavior), 형식(format), 어조(tone), 그리고 작업(task)을 가르치기 위한 것이지만, 계속해서 변하는 사실(facts)을 주입하기에는 비효율적이고 비용이 많이 드는 방법이며, 출처를 인용할 수도 없습니다. RAG는 변화하는 지식을 담당하고, 미세 조정(fine-tuning)은 행동을 담당하며, 성숙한 시스템은 이 두 가지를 모두 사용합니다.
파이프라인 개요
각 부분을 하나씩 살펴보기 전에 전체적인 형태를 먼저 살펴보겠습니다. 특정 단계가 어디에 위치하는지 알지 못하면 단일 단계를 이해하기 어렵기 때문입니다.

두 개의 파이프라인: 오프라인 인제스션(offline ingestion)은 코퍼스를 로드, 청크(chunk) 분할, 임베딩(embed), 그리고 인덱싱(index)합니다. 쿼리 시점(query time)에는 시스템이 쿼리를 임베딩하고, 검색(retrieve)하고, 재순위화(rerank)하며, 프롬프트(prompt)를 구성하고, 답변을 생성(generate)합니다.
여기에는 실제로 두 가지 시스템이 있으며, 이들은 서로 다른 시점에 작동합니다. 첫 번째는 인제스션 (ingestion, 데이터 주입)으로, 사용자가 나타나기 전 오프라인에서 실행됩니다. 소스 문서를 로드하고, 이를 구절 (passages) 단위로 분할하며, 임베딩 모델 (embedding model, 텍스트를 벡터로 매핑하는 네트워크)을 사용하여 각 구절을 벡터 (숫자 리스트)로 변환한 뒤, 빠른 유사도 검색 (similarity search)을 위해 구축된 인덱스 (index)에 해당 벡터들을 저장합니다. 이것은 배치 작업 (batch job)이거나 스트리밍 파이프라인 (streaming pipeline)이지만, 어느 쪽이든 사용자가 이를 기다리지는 않습니다. 이 시스템의 출력물은 검색 가능한 인덱스입니다.
두 번째 시스템은 쿼리 타임 (query time, 질의 시점)에 실행되며, 사용자가 기다리고 있는 동안 작동하므로 매 밀리초가 중요합니다. 사용자의 질문을 가져와 문서에 사용했던 것과 동일한 모델로 임베딩하고, 인덱스에서 질문과 가장 유사한 구절들을 검색하며, 선택적으로 정밀도를 높이기 위해 해당 후보들을 재순위화 (rerank)합니다. 그런 다음 가장 적합한 구절들을 모델의 컨텍스트 윈도우 (context window, 모델이 한 번에 읽을 수 있는 고정된 텍스트 양)에 맞는 프롬프트 (prompt)로 조립하고, 마지막으로 해당 프롬프트를 모델에 보내 답변을 생성 (generate)합니다. 검색 품질은 거의 전적으로 오프라인 파이프라인에서 결정되지만, 그 영향은 전적으로 쿼리 타임에 체감됩니다. 대부분의 팀은 눈에 보이는 부분인 쿼리 타임의 모델 호출에 주의를 집중하고, 눈에 보이지 않는 부분인 인제스션에는 투자를 소홀히 하는데, 이는 정확히 거꾸로 된 방식입니다. 인제스션을 잘못 설정하면 그 어떤 영리한 프롬프트로도 상황을 구제할 수 없습니다.
청킹 (Chunking): 조용히 성능의 한계를 결정짓는 결정
인제스션에서의 첫 번째 실제 결정 사항은 문서를 어떻게 조각낼 것인가입니다. 백 페이지 분량의 매뉴얼 전체를 하나의 벡터로 임베딩할 수는 없으며, 그렇게 해서도 안 됩니다. 따라서 문서를 몇 문장에서 몇 문단 정도의 크기인 청크 (chunks)로 분할하며, 각 청크는 검색의 단위가 됩니다. 이것은 마치 배관 작업처럼 들릴 수 있습니다. 하지만 실제로는 전체 시스템에서 가장 영향력이 큰 결정 중 하나이며, 많은 RAG 시스템이 첫날부터 소리 없이 망가지는 지점이 바로 이곳입니다.

청크(Chunks)가 너무 크면 임베딩 (embedding)이 희석되고 컨텍스트 (context)를 낭비하게 됩니다. 너무 작으면 의미를 잃게 됩니다. 겹치는 경계(overlapping edges)를 가진 중간 크기는 각 구절의 경계에서 일관성을 유지해 줍니다.
이러한 긴장 관계는 설명하기는 쉽지만 해결하기는 어렵습니다. 청크를 너무 크게 만들면 두 가지 나쁜 일이 발생합니다. 큰 청크의 임베딩 (embedding)은 많은 아이디어의 평균값이 되므로, 특별히 어느 한 곳을 가리키지 못하고 쿼리 (query)와 약하게 매칭됩니다. 즉, 주변의 모든 정보에 의해 특정 사실에 대한 신호가 희석됩니다. 또한, 큰 청크는 쿼리 시점에 컨텍스트 (context) 예산을 잡아먹기 때문에, 더 적은 수의 청크를 포함할 수밖에 없습니다. 반대로 청크를 너무 작게 만들면 정반대의 문제가 발생합니다. 주변 맥락에서 떨어져 나온 단일 문장은 종종 그것을 유용하게 만들었던 의미를 잃어버립니다. 만약 청크가 "그것"이 무엇을 가리키는지 담고 있지 않다면, "그것은 최대 200개의 연결을 지원합니다"라는 문장은 쓸모가 없습니다. 쿼리의 단어와 일치하는 파편을 검색(retrieve)할 수는 있지만, 실제로 그 질문에 답할 수는 없게 되는 것입니다.
산문(prose)의 경우 실질적인 절충안은 수백 토큰(token) 정도의 청크(chunk) 크기이지만, 정확한 숫자보다는 경계 문제(boundary problem)를 직접적으로 해결하는 두 가지 기술이 더 중요합니다. 첫 번째는 오버랩(overlap)입니다. 연속된 청크들이 경계 부분에서 일부 텍스트를 공유하도록 하여, 그렇지 않으면 경계에서 고립될 문장이 적어도 하나의 청크에는 온전하게 나타나도록 하는 것입니다. 두 번째이자 더 중요한 것은 구조 인식 분할(structure-aware splitting)입니다. 매 N자마다 맹목적으로 자르는 대신, 제목(headings), 단락(paragraphs), 리스트 항목(list items), 코드 블록(code blocks), 표 행(table rows)과 같이 문서 자체의 구조를 기준으로 분할합니다. 섹션 경계를 존중하는 청크는 완전한 생각을 담고 있지만, 문장을 반으로 자르는 청크는 어느 쪽의 의미도 제대로 담지 못합니다. Markdown, HTML 또는 소스 코드와 같은 구조화된 콘텐츠의 경우, 원시 길이(raw length)가 아닌 구조를 기준으로 분할하는 것이 종종 얻을 수 있는 가장 큰 품질 향상 방법이며, 이는 더 나은 파서(parser)를 사용하는 것 외에는 비용이 들지 않습니다.
한 가지 기술을 더 사용하면 검색되는 단위와 전송되는 단위를 분리할 수 있습니다. 매칭을 위해 작고 정밀한 청크를 인덱싱(index)하지만, 검색 시점에는 각 검색 결과(hit)를 주변 윈도우(window)나 상위 섹션(parent section)으로 확장하여 모델이 파편을 단독으로 읽는 대신 문맥(context) 속에서 읽을 수 있도록 합니다. 이를 흔히 small-to-big 또는 부모 문서 검색(parent-document retrieval)이라고 부릅니다.
임베딩(Embeddings): 의미를 기하학으로 변환하기
청크를 생성하고 나면, 질문과 관련된 청크를 찾는 방법이 필요합니다. 키워드 매칭(Keyword matching)만으로는 충분하지 않은데, 예를 들어 "비밀번호를 어떻게 재설정하나요"라고 묻는 사용자는 단어가 거의 겹치지 않더라도 "계정 액세스 복구"라는 제목의 문서를 찾아야 하기 때문입니다. 철자가 아닌 의미(meaning)를 기준으로 매칭해야 하며, 이것이 바로 임베딩(embeddings)이 제공하는 기능입니다.

임베딩 모델 (embedding model)은 텍스트를 벡터 (vector)로 매핑하여 의미적 유사성 (semantic similarity)이 기하학적 근접성 (geometric closeness)이 되도록 합니다. 즉, 쿼리 (query)와 청크 (chunks)가 하나의 벡터 공간 (vector space)을 공유하게 됩니다.
임베딩 모델 (embedding model)은 텍스트 조각을 벡터 (vector), 즉 일반적으로 수백 개에서 수천 개의 숫자로 이루어진 리스트로 매핑하도록 훈련된 신경망 (neural network)입니다. 이 모델을 유용하게 만드는 것은 바로 훈련 목적 (training objective)입니다. 모델은 유사한 의미를 가진 텍스트들이 이 고차원 공간 (high-dimensional space)에서 서로 가까이 위치하고, 의미가 다른 텍스트들은 서로 멀리 떨어지도록 훈련됩니다. "비밀번호 재설정"과 "계정 액세스 복구"는 겉으로 드러나는 단어는 다르지만, 모델이 두 문장이 거의 같은 의미를 지닌다는 것을 학습했기 때문에 결국 근처의 지점으로 위치하게 됩니다. 인간의 모호한 개념인 의미적 유사성 (semantic similarity)이 계산 가능한 숫자인 기하학적 거리 (geometric distance)가 되는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기