5살 아이에게 설명하듯 쉽게 이해하는 RAG (직접 실행 가능한 실제 코드 포함)
요약
RAG(검색 증강 생성)의 개념을 실생활 예시를 통해 초보자도 이해하기 쉽게 설명합니다. LLM의 폐쇄형 시험 방식과 RAG의 오픈북 시험 방식을 비교하며, 검색과 생성의 원리를 다룹니다.
핵심 포인트
- RAG는 모델이 외부 문서를 참조하여 답변하는 '오픈북 시험' 방식임
- 검색(Retrieval)과 생성(Generation)의 결합으로 정확도 향상
- LLM의 기억력 한계와 환각 현상을 보완하는 핵심 기술
- 실제 실행 가능한 코드를 통해 이론을 실습할 수 있음
5살 아이에게 설명하듯 쉽게 이해하는 RAG (직접 실행 가능한 실제 코드 포함)
좋습니다. 지난번에 RAG에 대해 글을 썼을 때, 많은 분이 "멋지긴 한데, 실제로 내부에서 어떤 일이 일어나는지는 여전히 모르겠어요"라고 메시지를 보내주셨습니다. 일리 있는 지적입니다. 제가 임베딩 (embeddings)이나 벡터 검색 (vector search) 같은 단어들을 제대로 설명하며 천천히 풀어나가지 않고 그냥 던지듯 사용했으니까요. 그래서 이번에는 다르게 접근해 보려 합니다.
전문 용어를 쏟아붓지 않겠습니다. 먼저 실생활 예시부터 들어갑니다. 그다음, AI를 한 번도 다뤄본 적이 없는 분이라도 오늘 당장 자신의 노트북에서 실행해 볼 수 있는, 실제로 작동하고 복사해서 붙여넣을 수 있는 코드 예시를 보여드리겠습니다. 이 글을 다 읽고 나면, 단순히 취업 면접에서 "RAG"라는 단어를 반복할 수 있는 수준을 넘어, 실제로 어떤 일이 일어나고 있는지 이해하게 될 것입니다.
커피 한 잔 준비하세요. 내용이 좀 깁니다.
먼저, 컴퓨터와는 전혀 상관없는 실생활 예시부터 시작해 봅시다
여러분이 시험을 치르고 있다고 상상해 보세요. 두 가지 버전의 시험이 있습니다.
첫 번째 버전은 '폐쇄형 시험 (closed book)'입니다. 여러분은 순수하게 기억력만으로 모든 질문에 답해야 합니다. 만약 3개월 전에 공부했던 내용을 잊어버렸다면, 운이 나쁜 겁니다. 이제는 추측해야 하죠. 그리고 추측할 때 여러분은 "잘 모르겠습니다"라고 말하지 않습니다. 일반적인 시험에서는 확신에 찬 것처럼 써야 점수를 받을 수 있기 때문에, 그냥 그럴듯하게 들리는 내용을 적습니다.
두 번째 버전은 '오픈북 시험 (open book)'입니다. 여러분은 노트를 가져가는 것이 허용됩니다. 질문이 나오면, 적절한 페이지를 펼쳐서 관련 단락을 읽고, 거기에 실제로 적혀 있는 내용을 바탕으로 답을 작성합니다.
여러분의 질문에 답하는 일반적인 ChatGPT와 같은 LLM (Large Language Model)은 기본적으로 폐쇄형 시험을 치르고 있는 것과 같습니다. 모델은 과거 어느 시점에 수행된 학습 (training) 과정에서 암기한 내용을 바탕으로 답변합니다. 여기에는 여러분의 개인 문서, 회사의 내부 위키 (wiki), 또는 그 어떤 비공개 정보도 포함되어 있지 않습니다.
RAG는 모델에게 대신 오픈북 시험을 주는 것과 같습니다. 모델이 답변하기 전에, 여러분이 직접 자신의 문서에서 적절한 페이지를 찾아 모델 앞에 놓아주는 것입니다. 그런 다음 질문을 던집니다. 모델은 기억에 의존해 추측하는 대신, 자기 앞에 놓인 내용을 읽고 그에 기반하여 답변합니다.
그것이 진정한 핵심 아이디어입니다. RAG는 Retrieval Augmented Generation (검색 증강 생성)의 약자로, 단순히 "정보를 찾아본 다음, 찾아낸 내용을 사용하여 답변을 생성한다"는 말을 멋지게 표현한 것입니다.
두 번째 실생활 예시: 이번에는 사서와 함께
거대한 도서관에 걸어 들어가 사서에게 "우리 회사의 반품 정책 중 개봉된 전자제품에 대해 뭐라고 되어 있나요?"와 같은 질문을 던지는 상황을 상상해 보세요.
아무것도 확인하지 않고 즉석에서 대답하는 사서는 대략적으로 맞을 수도 있지만, 다른 매장의 일반적인 반품 정책을 바탕으로 완전히 지어낼 수도 있습니다. 이것이 바로 일반적인 LLM (대규모 언어 모델)입니다.
반면 유능한 사서는 "확인해 보겠습니다"라고 말한 뒤, 올바른 선반으로 걸어가 특정 정책 문서를 꺼내고, 관련 페이지를 펼쳐 그 단락을 읽은 다음, 실제로 거기에 적힌 내용을 바탕으로 답변합니다. "선반으로 걸어가서 올바른 페이지를 찾는" 부분이 검색 (Retrieval)입니다. "그 내용을 읽고 쉬운 언어로 답변을 주는" 부분이 생성 (Generation)입니다. 이 두 가지를 합치면 검색 증강 생성 (Retrieval Augmented Generation)이 됩니다.
모델이 그냥 모든 것을 외우면 안 되나요?
좋은 질문이며, 많은 사람들이 혼란스러워하는 지점입니다. 몇 가지 이유가 있습니다.
모델은 과거의 어느 시점에 인터넷에 있는 거대한 공개 텍스트 더미를 바탕으로 학습되었습니다. 여러분의 개인적인 메모, 회사의 내부 문서, 코드베이스, 또는 학습이 완료된 이후에 생성된 모든 것과 같은 비공개 정보는 모델이 결코 본 적이 없습니다. 그런 정보가 존재한다는 사실을 알 방법이 없습니다.
설령 여러분의 데이터를 특정하여 모델을 학습시키려 노력하더라도 (이를 미세 조정 (Fine-tuning)이라고 합니다), 이 과정은 느리고 비용이 많이 들며, 문서가 변경될 때마다 매번 다시 수행하기에는 매우 번거롭습니다. 만약 내일 회사의 정책이 업데이트된다면, 모델 전체를 다시 학습시키고 싶지는 않을 것입니다. 그저 시스템이 새로운 문서를 바로 가져오기를 원할 뿐입니다.
RAG는 이 모든 과정을 건너뜁니다. 당신의 문서는 평소에 저장되어 있던 곳에 그대로 머물러 있습니다. 누군가 질문을 하면, 그 즉시 해당 문서들을 검색하여 관련 있는 부분을 가져온 뒤, 매번 모델에게 신선한 상태로 전달합니다. 문서를 업데이트하면, 바로 그다음 질문부터 자동으로 업데이트된 내용이 반영됩니다. 재학습 (Retraining)이 필요 없습니다.
좋습니다, 그런데 컴퓨터는 문서의 어느 부분이 "관련 있는지" 어떻게 알까요?
이 부분은 모두가 대충 넘어가곤 하는 지점입니다. 하지만 이것이 전체 시스템의 핵심이므로, 여기서 잠시 속도를 늦추고 제대로 살펴보겠습니다.
컴퓨터는 인간이 하는 방식처럼 의미를 이해하는 데 서툽니다. 만약 기본적인 검색을 사용하여 문서에서 "cancel my subscription"이라는 정확한 단어를 검색했는데, 실제 문서에는 "how to terminate your membership"이라고 적혀 있다면, 단순한 키워드 검색 (Keyword search)은 이를 완전히 놓치게 됩니다. 인간에게 이 두 문구는 분명히 거의 같은 의미임에도 말이죠.
이를 해결하기 위해 우리는 임베딩 모델 (Embedding model)이라고 불리는 것을 사용합니다. 이 모델의 유일한 임무는 텍스트 조각을 가져와서 [0.12, -0.87, 0.44, ...]와 같이 수백 또는 수천 개의 숫자로 이루어진 리스트로 변환하는 것입니다. 이 숫자 리스트를 벡터 (Vector)라고 부릅니다.
여기서 중요한 점이 있습니다. 임베딩 모델은 실제 단어가 완전히 다르더라도 의미가 유사한 텍스트는 결국 유사한 숫자 리스트를 갖게 되는 방식으로 학습됩니다. 따라서 "cancel my subscription"과 "terminate my membership"은 이 수치 공간 (Numerical space) 내에서 서로 가까운 벡터를 갖게 되는 반면, "recipe for banana bread"와 같이 전혀 관련 없는 내용은 멀리 떨어지게 됩니다.
문서와 사용자의 질문 모두가 이러한 숫자 리스트로 변환되고 나면, "관련 있는" 텍스트를 찾는 것은 수학 문제가 됩니다. 단지 어떤 문서 벡터가 질문의 벡터와 가장 가까이 위치하는지를 측정하는 것뿐입니다. 그게 전부입니다. 그것이 비밀입니다. 이것은 인간적인 의미에서의 이해가 아니라, 의미를 나타내는 숫자들에 대한 매우 영리한 수학적 계산일 뿐입니다.
코드를 다루기 전, 쉬운 영어로 설명하는 전체 파이프라인 단계별 과정
실제로 일이 일어나는 순서는 다음과 같습니다. 머릿속에 여전히 남아있는 사서(librarian) 예시를 활용하여 각 단계를 설명해 드리겠습니다.
1단계, 문서를 가져와서 더 작은 조각들로 자릅니다. 이것을 청킹 (chunking)이라고 부릅니다. 누군가에게 단 한 단락을 찾으라고 하면서 300페이지짜리 매뉴얼 전체를 건네주지는 않겠죠? 대신 특정 페이지를 건네줄 것입니다. 여기서도 같은 개념이 적용됩니다.
2단계, 모든 청크 (chunk)를 임베딩 모델 (embedding model)에 통과시켜 각 청크에 대한 숫자 리스트를 얻습니다. 이 모든 숫자 리스트를 어딘가에 저장하는데, 이 저장소를 보통 벡터 스토어 (vector store) 또는 벡터 데이터베이스 (vector database)라고 부릅니다.
3단계, 이 모든 과정은 누군가 질문을 던지기도 전에, 사전에 단 한 번 실행됩니다. 도서관 선반을 미리 정리해 두는 것이라고 생각하면 됩니다.
4단계, 누군가 실제로 질문을 하면, 그 질문을 동일한 임베딩 모델 (embedding model)에 통과시켜 질문의 숫자 리스트를 얻습니다.
5단계, 질문의 숫자들을 모든 청크의 숫자들과 비교하여 가장 유사한 것들을 뽑아냅니다. 이 비교에는 보통 코사인 유사도 (cosine similarity)라고 불리는 것을 사용하는데, 이는 두 숫자 리스트가 얼마나 유사한지를 측정하는 수학적 방법으로, 마치 두 화살표 사이의 각도를 측정하는 것과 비슷합니다.
6단계, 가장 유사하게 매칭된 청크들을 원래의 질문과 함께 프롬프트 (prompt)에 붙여넣은 다음, 그 전체 내용을 LLM에 보냅니다.
7단계, LLM은 당신이 제공한 청크들을 읽고, 기억에 의존해 추측하는 대신 그 정보를 사용하여 질문에 답변합니다.
이것이 전체 시스템입니다. 이제 실제로 하나를 만들어 봅시다.
진짜로 작동하는 아주 작은 버전을 만들어 봅시다
라이브러리 문서 속에서 길을 잃는 대신 실제로 무슨 일이 일어나고 있는지 따라올 수 있도록, 도구들은 의도적으로 지루할 정도로 단순한 것들을 사용할 것입니다. 우리는 순수 파이썬 (Python), 당신의 컴퓨터에서 로컬로 실행되는 작은 무료 임베딩 모델 (이 단계에서는 API 키가 필요 없습니다), 그리고 numpy를 이용한 기본적인 수학을 사용할 것입니다.
먼저, 필요한 것들을 설치합니다.
pip install sentence-transformers numpy
sentence-transformers 라이브러리는 우리가 무료로 사용할 수 있는 준비된 임베딩 (embedding) 모델을 제공하며, 처음 실행할 때 자동으로 다운로드합니다.
1단계, 우리의 아주 작은 "문서들"
실제 시스템에서는 이 문서들이 실제 파일, 데이터베이스 (database), 또는 웹사이트에서 가져오게 됩니다. 단순하고 따라하기 쉽게 만들기 위해, 저는 코드 안에 텍스트로 직접 작성하겠습니다.
documents = [
"저희의 반품 정책에 따라 고객은 미개봉 전자제품을 30일 이내에 반품하여 전액 환불을 받을 수 있습니다.",
"개봉된 전자제품은 제조 결함이 있는 경우에만 동일한 품목으로 교환이 가능합니다.",
...
2단계, 청킹 (chunking)
이 예시에서는 각 문장이 이미 적절하게 작은 청크 (chunk)이므로, 더 이상 나눌 필요가 없습니다. 실제 프로젝트에서는 보통 긴 문서를 각각 수백 단어 정도의 조각으로 나누며, 청크 경계에서 문장이 실수로 잘리는 것을 방지하기 위해 종종 청크 사이에 약간의 중첩 (overlap)을 둡니다. 우리의 아주 작은 예시에서는 위의 각 줄이 이미 하나의 깔끔한 청크입니다.
3단계, 문서를 숫자 리스트로 변환하기 (임베딩, embeddings)
from sentence_transformers import SentenceTransformer
# 이 코드는 작고 무료인 임베딩 모델을 당신의 컴퓨터에 로드합니다
...
만약 document_embeddings[0]을 출력하면, 말 그대로 긴 소수점 숫자 리스트를 보게 될 것입니다. 그것이 바로 첫 번째 문장의 "의미"를 단어 대신 수학적으로 표현한 것입니다.
4단계, 사용자의 질문도 숫자 리스트로 변환하기
question = "온라인으로 구매한 게임에 대해 돈을 돌려받을 수 있나요?"
question_embedding = model.encode([question])
질문에 "환불"이나 "소프트웨어"라는 단어를 직접 사용하지 않고, "돈을 돌려받다"와 "온라인으로 구매한 게임"이라고 말한다는 점에 주목하세요. 기본적인 키워드 검색 (keyword search) 방식이라면 아마도 올바른 문서를 완전히 놓쳤을 것입니다. 우리의 임베딩 기반 접근 방식이 더 나은 성능을 보이는지 확인해 봅시다.
5단계, 수학을 사용하여 가장 유사한 문서 찾기
import numpy as np
def cosine_similarity(a, b):
...
이 코드를 실행하면, 질문에 "software"나 "refundable"이라는 단어가 전혀 포함되지 않았음에도 불구하고, 소프트웨어 제품 및 디지털 다운로드는 환불이 불가능하다는 문장이 가장 상위 결과로 나옵니다. 이는 임베딩 모델 (embedding model)이 단순히 글자 그대로의 단어가 아니라, "온라인으로 구매한 게임"과 "환불"이라는 의미를 포착했기 때문입니다. RAG가 실제로 작동하는 것을 처음 보았을 때, 제 머릿속에서 RAG의 개념이 진정으로 이해된 순간이 바로 이때였습니다.
6단계, 검색된 청크 (chunks)로 프롬프트 (prompt) 구성하기
이제 가장 유사도가 높은 매칭 청크들을 가져와서, 명확한 지침과 함께 LLM (대규모 언어 모델)에 전달합니다.
retrieved_text = "\n".join([text for score, text in top_matches])
prompt = f"""
...
추측하지 말라고 지시하는 이 명령은 언뜻 보이는 것보다 훨씬 더 중요합니다. 이 지침이 없으면, 모델은 검색된 문맥 (context)에 자신의 일반적인 지식을 섞어버리는 경향이 있으며, 이로 인해 실제 문서에 근거하여 답변을 생성하는 (grounding) RAG의 핵심 목적을 잃게 됩니다.
7단계, 실제 LLM에 전송하기
마지막 단계는 어떤 AI 제공업체를 사용하는지에 따라 다르지만, 구조는 항상 동일합니다. 프롬프트를 보내고 텍스트를 돌려받는 것입니다. 일반적인 API 설정에서의 모습은 다음과 같습니다.
import anthropic
client = anthropic.Anthropic(api_key="your-api-key-here")
...
이제 끝입니다. 이것이 원문 문서부터 근거 있는 답변에 이르기까지의 완전하고 실제적인, 작동하는 RAG 파이프라인 (pipeline)입니다. 이 선 위에서 설명한 모든 내용이 전체 개념입니다. 실제 세상의 RAG 시스템이 그 위에 추가하는 것들은, 단지 이 7가지 단계를 더 큰 규모에서 더 빠르고, 저렴하며, 더 정확하게 만드는 것뿐입니다.
이 장난감 예제에서 실제 프로젝트로 넘어갈 때 변하는 점
핵야 핵심 아이디어는 변하지 않지만, 단 5개의 문장이 아니라 수천 개의 문서를 다루게 되면 몇 가지 사항이 훨씬 더 진지해집니다.
임베딩 (embeddings)을 일반적인 파이썬 리스트 (Python list)에 저장하는 대신, Chroma, Pinecone 또는 Weaviate와 같은 실제 벡터 데이터베이스 (vector database)를 사용하게 됩니다. 이러한 데이터베이스는 수백만 개의 숫자 리스트를 빠르게 검색할 수 있도록 구축되었습니다.
문서를 코드에 직접 작성하는 대신, PDF, 웹사이트 또는 데이터베이스에서 불러오게 됩니다. 이때 적절한 청킹 (chunking) 전략이 필요한데, 보통 청크(chunk)당 수백 단어 정도로 구성하며 청크 사이에 약간의 중첩 (overlap)을 둡니다.
또한, 관련 있는 내용을 전혀 찾지 못하는 경우도 처리해야 하며, 모델이 무언가를 지어내는 대신 실제로 "모릅니다"라고 말하도록 보장해야 합니다. 이것이 프롬프트 (prompt) 내의 지침이 매우 중요한 이유입니다.
그리고 소스 문서 (source documents)가 변경될 때마다 임베딩 (embedding) 단계를 주기적으로 다시 실행해야 할 것입니다. 그렇지 않으면 무언가 잘못되었다는 경고 없이 오래된 정보를 사용하여 질문에 답하게 됩니다.
하지만 이 중 그 어떤 것도 근본적인 아이디어를 바꾸지는 않습니다. 여전히 검색을 한 뒤, 찾아낸 내용을 사용하여 답변하는 것일 뿐입니다. 그 외의 모든 것은 정교화 (refinement) 과정입니다.
일상생활에서 실제로 이 기술을 접하게 되는 경우
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기