간단한 RAG 파이프라인 만들기: 개념으로 따라가기
검색 증강 생성(RAG)을 단계별로 차근차근 쌓아 올립니다. 마법도, 특정 스택도 없이 파이프라인의 형태와 중요한 결정만 다룹니다.
검색 증강 생성(RAG)은 하나의 단일 기법처럼 들립니다. 하지만 사실은 파이프라인입니다. 작고 평범한 단계들의 연속이며, 이들이 함께 작동해 언어 모델이 학습 중에 외운 것만이 아니라 당신의 문서를 사용해 질문에 답하게 해줍니다. 각 단계는 그 자체로는 영리하지 않습니다. 기술은 이들을 연결해서, 모델이 답을 쓰는 바로 그 순간에 당신 텍스트의 알맞은 구절이 모델 앞에 놓이게 하는 데 있습니다. 이 글은 그 파이프라인을 한 단계씩 쌓아 올리고, 각 단계가 어디서 깨지기 쉬운지 짚어봅니다.
RAG가 애초에 왜 필요한가
언어 모델은 학습한 것을 압니다. 하지만 당신 회사의 사규, 지난주 사고 보고서, 1분 전에 업로드한 PDF의 내용은 모릅니다. 그 자료로 모델을 파인튜닝할 수도 있지만, 그건 비싸고 갱신이 느리며 망치기 쉽습니다. RAG는 더 저렴한 길을 택합니다. 모델은 그대로 두고, 질문마다 관련 구절을 찾아 프롬프트에 붙여 넣는 것이죠. 그러면 모델은 가지고 있을지 불확실한 기억이 아니라 눈으로 볼 수 있는 텍스트를 바탕으로 답합니다.
머릿속 그림은 책을 펼쳐 든 유능한 조수입니다. 조수는 똑똑하지만 당신의 구체적인 사정은 모릅니다. 책에는 구체적인 내용이 있지만 추론은 할 수 없습니다. RAG는 알맞은 때에 알맞은 페이지를 조수에게 건네줍니다. 아래의 모든 것은 "알맞은 때에 알맞은 페이지"를 위한 것입니다.
1단계: 문서 청킹하기
모델에게 도서관 전체를 건넬 수는 없으니, 문서를 청크(chunk)라는 작은 조각으로 나눕니다. 청크란 그저 한 구절입니다. 몇 개의 문단, 한 섹션, 한 페이지죠. 청킹은 보이는 것보다 더 중요합니다. 너무 큰 청크는 관련 문장을 주변 잡음으로 희석하고 프롬프트의 자리를 낭비합니다. 너무 작은 청크는 문장을 의미 있게 만드는 맥락을 잃습니다. "이건 지원되지 않습니다"라는 한 줄은 "이건"이 무엇인지 말하는 문단 없이는 쓸모가 없습니다.
합리적인 기본값은 글자 수를 무작정 세는 대신 문서의 자연스러운 구조, 즉 섹션, 제목, 문단을 따라 청킹하는 것입니다. 관련된 생각은 함께 묶으세요. 많은 파이프라인이 청크를 약간 겹치게 하기도 합니다. 경계 근처의 문장이 최소 한 청크에서는 온전히 나타나게 하기 위해서죠. 보편적으로 옳은 크기란 없습니다. 문서에 따라 다르며, 결과를 측정할 수 있게 되면 다시 들여다볼 가치가 있습니다.
2단계: 임베딩과 벡터 스토어
나중에 관련 청크를 찾으려면, 키워드 일치가 아니라 의미로 질문과 모든 청크를 비교할 방법이 필요합니다. 이것이 임베딩이 제공하는 것입니다. 임베딩 모델은 텍스트 조각을 숫자의 목록, 즉 벡터로 바꾸는데, 의미가 비슷한 텍스트가 그 숫자 공간에서 서로 가까이 놓이도록 배치합니다. "비밀번호를 어떻게 재설정하나요?"와 "계정 접근을 복구하는 단계"는 공유하는 단어가 거의 없지만 벡터로는 가까이 자리합니다.
모든 청크를 임베딩 모델에 한 번씩 통과시켜, 각 청크의 벡터를 원본 텍스트와 함께 저장합니다. 그 모음은 벡터 스토어에 담깁니다. "어떤 저장된 벡터가 이 벡터에 가장 가까운가?"라는 질문에 수백만 개에 걸쳐서도 빠르게 답하도록 만들어진 데이터베이스죠. 작은 프로젝트라면 벡터 스토어는 단순한 인메모리 구조일 수 있고, 규모가 커지면 전용 데이터베이스가 됩니다. 인터페이스는 어느 쪽이든 같습니다. 벡터를 넣고, 가장 가까운 이웃을 돌려받는 것이죠.
3단계: 쿼리 시점의 검색
이제 파이프라인이 실시간으로 돕니다. 사용자가 질문을 합니다. 청크에 사용했던 것과 같은 모델로 질문을 임베딩합니다. 이게 중요합니다. 서로 다른 모델에서 나온 벡터는 비교할 수 없기 때문입니다. 그 질문 벡터를 벡터 스토어에 건네고 가장 가까운 청크를 요청합니다. 스토어는 상위 몇 개, 즉 의미가 질문에 가장 가까운 구절들을 돌려줍니다.
"몇 개나"는 실질적인 결정입니다. 너무 적게 돌려받으면 답을 담은 구절을 놓칠 위험이 있습니다. 너무 많이 돌려받으면 미미하게만 관련된 텍스트로 프롬프트가 붐비는데, 이는 비용도 더 들고 모델의 주의도 흩뜨립니다. 적은 수, 즉 답을 담기에 충분하되 신호가 잡음에 묻힐 만큼 많지는 않은 정도가 보통의 출발점입니다. 검색은 또한 순수 의미 검색이 제품 코드나 이름 같은 정확한 용어에서 가끔 비틀거리는 지점이기도 합니다. 그래서 일부 파이프라인은 구식 키워드 검색과 혼합합니다. 단순하게 시작하세요. 실패가 보일 때만 추가하면 됩니다.
4단계: 프롬프트 조립하기
이제 사용자의 질문과 검색된 청크 몇 개가 있습니다. 생성 단계는 이들을 하나의 프롬프트로 조립합니다. 개념적으로는 이렇게 보입니다:
You are answering using only the context below.
If the answer is not in the context, say you don't know.
Context:
[chunk 1 text]
[chunk 2 text]
[chunk 3 text]
Question: [the user's question]
여기서 두 지시문이 조용하지만 무거운 일을 하고 있습니다. "아래 컨텍스트만 사용하여"는 모델에게 자신의 기억보다 제공된 구절을 우선하라고 말하는데, 이것이 RAG의 전부입니다. "답이 컨텍스트에 없으면 모른다고 말하라"는 모델에게 거절할 권한을 줍니다. 이것이 없으면 모델은 자신만만한 추측으로 빈자리를 채우는 경향이 있습니다. 그 실패 모드를 명명하는 것이 정직한 "찾을 수 없음"과 날조의 차이를 만듭니다.
5단계: 생성과 출처 인용
조립된 프롬프트가 언어 모델로 가고, 모델은 검색된 텍스트에 근거한 답을 씁니다. 각 청크의 원본 출처를 보관해 두었기에, 파인튜닝은 할 수 없는 일을 할 수 있습니다. 답이 어디서 왔는지 보여주는 것이죠. 각 청크에 식별자, 즉 문서 제목, 섹션, 페이지를 함께 지니고 가서 모델에게 그것을 인용하라고 요청하거나, 단순히 출처 구절을 답 아래에 표시하세요. 인용은 불투명한 응답을 사용자가 검증할 수 있는 응답으로 바꿉니다. 그리고 이 검증 가능성이야말로 RAG 시스템을 배포할 만큼 신뢰할 수 있게 만드는 경우가 많습니다.
이곳은 또한 파이프라인의 정직성이 시험받는 지점입니다. 검색이 잘못된 구절을 넘겼다면, 모델은 잘못된 구절을 바탕으로 유창하게 답할 것입니다. 자신만만한 답은 옳은 답의 증거가 아닙니다. 이는 곧 대부분의 첫 빌드가 건너뛰는 부분으로 이어집니다.
RAG 파이프라인이 실제로 깨지는 곳
실패는 모델에 있는 경우가 드뭅니다. 그보다 상류, 즉 검색에 있습니다. 관련 청크가 애초에 반환되지 않았다면 세상 최고의 모델도 그것을 쓸 수 없습니다. "쓰레기가 들어가면 유창한 쓰레기가 나온다"는 것이죠. 흔한 원흉은 이렇습니다. 답이 경계를 가로질러 쪼개져 어느 청크에도 온전히 나타나지 않은 청킹, 당신 도메인의 어휘를 포착하지 못하는 임베딩 모델, 혹은 단순히 너무 적은 청크를 요청한 경우. RAG 시스템이 틀린 답을 줄 때, 모델부터 탓하려는 충동을 참으세요. 그 질문에 대해 검색이 실제로 무엇을 반환했는지 보세요. 대부분의 경우 답은 검색된 집합 안에 아예 없었고, 해결책은 프롬프트가 아니라 청킹이나 검색에 있습니다.
이를 잡아내는 방법은 답을 이미 아는 실제 질문들로 파이프라인을 평가하고, 최종 텍스트만이 아니라 검색된 청크를 들여다보는 것입니다. 알맞은 구절을 검색한 뒤 잘 답하는 파이프라인은 작동하는 것입니다. 잘못된 구절을 검색했는데 운으로 잘 답하는 파이프라인은 더 어려운 질문을 기다리는 버그입니다.
정리
RAG는 하나의 잔재주가 아니라 짧은 조립 라인입니다. 문서를 분별 있게 청킹하고, 임베딩해서 벡터 스토어에 넣고, 질문마다 가장 가까운 청크를 검색하고, 근거 있는 프롬프트로 조립하고, 출처를 인용하는 답을 생성하는 것이죠. 모델은 쉬운 부분입니다. 시스템 전체의 품질은 청킹과 검색이, 즉 알맞은 순간에 알맞은 페이지를 모델 앞에 가져다 놓는 일이 결정합니다. 각 단계를 평이하게 만든 뒤 실제 질문으로 검색을 측정하면, 가장 인상적으로 보이는 곳이 아니라 실패가 실제로 사는 곳에 노력을 쏟게 됩니다.
