RAG, 그게 뭔데?
- #AI
- #RAG
- #Embedding
- #Vector Search
- #LLM
얼마 전 PI Lab Sprint2를 마쳤다. Sprint2에서는 RAG를 집중적으로 학습하고 직접 실습도 해 봤는데, 학습하는 동안 더 알아보고 싶은 부분이 적지 않게 생겼다. 이번 글은 스프린트에서 학습한 내용과, 그 뒤에 혼자 더 찾아보고 공부한 내용을 함께 정리한 것이다.
범용 LLM은 자기가 학습한 범위의 지식만 안다. 내가 가진 회사 문서나 최신 자료는 모델 안에 없다. 부족한 지식을 채우는 가장 간단한 방법은 프롬프트에 직접 붙여 넣는 것이지만, 매번 관련 자료를 손수 찾아야 하고 넣을 수 있는 양도 컨텍스트 한계에 묶인다.
RAG(Retrieval-augmented generation)는 질문이 들어오면 그때그때 관련 문서를 검색해 프롬프트에 끼워 넣고 답하게 한다. 지식을 갱신하려면 문서만 갈아끼우면 되고, 어떤 자료를 보고 답했는지 출처도 제시할 수 있다. 그래서 자주 바뀌는 지식이나 내부 문서를 다룰 때는 파인튜닝이나 프롬프팅보다 유리하다. 이번 글에서는 RAG가 무엇이고 어떻게 동작하는지 정리하고, 간단한 실습도 하나 해 본다.
RAG란?
왜 RAG가 필요했나
LLM의 지식은 학습이 끝나는 순간 가중치 안에 고정된다. 그래서 세 가지 문제가 생긴다.
- 지식을 고치거나 늘리기 어렵다. 학습 이후에 나온 정보나 내가 따로 가진 문서는 모델이 모른다. 알게 하려면 모델을 다시 학습시켜야 하는데, 비용이 크다.
- 근거를 댈 수 없다. 답이 어떤 자료에서 나왔는지 모델 안에서는 추적되지 않는다.
- 환각(hallucination)을 일으킨다. 모르는 것도 멈추지 않고 그럴듯하게 지어낸다.
RAG는 지식을 가중치에 새겨 넣는 대신 외부 문서에서 가져와 쓴다. 덕분에 세 문제가 자연스럽게 해결된다.
RAG의 정의
RAG(Retrieval-augmented generation)는 이름 그대로 검색(retrieval)으로 보강한(augmented) 생성(generation)이다. 질문이 들어오면 관련 문서를 먼저 검색하고, 찾아온 문서를 질문과 함께 모델에 넣어 답을 생성한다. 모델 가중치에 든 지식만 쓰지 않고, 외부 문서에서 그때그때 찾아온 지식을 함께 쓰는 셈이다.
핵심 구조 — 검색기와 생성기
흐름은 단순하다.
RAG를 구성하는 핵심 부품은 검색기(Retriever)와 생성기(Generator)다.
- 검색기: 질문을 받아 외부 문서 저장소에서 관련 있는 문서를 찾아온다.
- 생성기: 질문과 찾아온 문서를 함께 받아 답을 쓴다. 우리가 흔히 쓰는 LLM이 맡는 역할이다.
단, 검색기가 검색할 외부 문서 저장소는 미리 채워 둬야 한다. 질문을 받기 전에 문서를 검색할 수 있는 형태로 가공해 저장해두는 준비 과정이 먼저 필요한데, 이 과정과 검색 방식은 다음 장에서 자세히 다룬다.
검색한 문서를 모델에 어떻게 넣을까
RAG를 처음 제안한 논문은 검색한 문서를 답에 쓰는 방식을 RAG-Sequence와 RAG-Token 두 가지로 나눴다.
- RAG-Sequence: 문서 하나를 골라 답변 전체를 그 문서로 쓴다.
- RAG-Token: 답을 만드는 동안 토큰마다 다른 문서를 참고해, 한 답에 여러 문서를 섞을 수 있다.
이렇게 나눈 이유가 있다. 당시 생성기였던 BART는 한 번에 넣을 수 있는 컨텍스트 크기가 작아, 문서 여러 개를 프롬프트에 통째로 넣을 수 없었다. 그래서 문서를 하나씩 따로 넣어 결과를 합치는 우회법이 필요했다.
이후 "여러 문서를 한 번에 함께 읽게 하자"는 방향(FiD, Fusion-in-Decoder)으로 옮겨갔고, LLM이 커지고 컨텍스트 창이 넓어지면서 검색한 문서를 프롬프트에 다 붙여 넣고 모델이 한 번에 읽는 지금의 방식에 도달했다. 모델이 좋아질수록 이런 우회법이 필요하지 않게 되었다.
그렇다고 항상 문서를 그대로 넣는 건 아니다. 관련 없는 부분까지 넣으면 비용이 늘고 답도 흐려져서, 질문과 관련된 부분만 추리거나 요약해 넣기도 한다. 리랭킹(rerank), 프롬프트 압축(LLMLingua), 질문 중심 요약 같은 기법이 그렇다.
흥미로운 건, 최근 새로 나오는 모델들의 컨텍스트 창이 수십만 토큰까지 커지면서 "다 넣으면 되지 검색이 필요한가"라는 논쟁도 활발하다는 점이다. 다만 다 넣을수록 비용이 커지고 엉뚱한 내용에 휩쓸려서, 필요한 것만 골라 주는 RAG가 여전히 쓸모 있다는 반론도 만만치 않다.
RAG는 어떻게 동작하는가
RAG의 파이프라인은 크게 두 단계로 나뉜다. 질문이 들어오기 전에 문서를 검색 가능한 형태로 저장해두는 인덱싱 단계와, 질문이 들어오면 그 저장소를 검색해 답하는 답변 생성 단계다.
인덱싱 — 문서를 미리 저장
질문이 들어오기 전에, 답변 생성 단계에서 검색할 수 있도록 사용할 문서들을 미리 벡터 DB에 저장해 두는 준비 단계다.
1. 문서에서 텍스트 추출
문서 종류는 PDF·워드·HTML·웹페이지 등 다양하다. 문서는 그대로는 검색에 쓸 수 없으니, 먼저 순수 텍스트만 뽑아낸다.
2. 청킹
문서를 통째로 다루면 한 덩어리에 여러 내용이 섞여 검색이 흐려진다. 작게 쪼개야 질문에 딱 맞는 조각만 골라낼 수 있다.
3. 청크 임베딩
각 청크를 임베딩 모델에 넣어 의미를 담은 숫자 벡터로 바꾼다. 그래야 비슷한 의미인지를 단어가 겹치는지가 아니라 벡터가 가까운지로 따질 수 있다.
4. 벡터 DB에 저장
벡터와 원본 텍스트를 쌍으로 저장한다. 앞에서 '외부 문서 저장소'라고 부른 곳이 바로 이 벡터 DB다. 벡터 간 거리(유사도)를 빠르게 계산해 가까운 것부터 찾아주도록 특화된 데이터베이스라, 질문과 가까운 벡터를 효율적으로 검색하고 짝지어진 원문을 꺼내 모델에 넘길 수 있다.
답변 생성 — 질문이 오면 검색해서 답하기
저장해둔 벡터 DB를 활용해, 질문이 들어올 때마다 반복되는 과정이다.
1. 질문 임베딩
질문도 벡터로 바꾼다. 이때 인덱싱 단계에서 문서를 임베딩한 것과 반드시 같은 모델을 써야 한다. 모델이 다르면 벡터 공간이 달라져 질문과 문서의 거리를 비교할 수 없기 때문이다.
2. 검색
저장된 청크 중 질문과 관련 있는 것을 찾는다. 검색 방식은 여러 가지지만, RAG에서는 보통 유사도 검색을 쓴다. 질문 벡터와 저장된 청크 벡터들의 거리를 재서 가장 가까운 몇 개를 찾는 방식이다. 거리는 보통 코사인 유사도로 재는데, 두 벡터가 이루는 각도로 방향이 얼마나 비슷한지를 본다. 방향이 비슷할수록 의미가 비슷하니, 가장 가까운 청크가 곧 질문과 가장 관련 있는 정보다.
3. 프롬프트 구성
검색한 정보를 컨텍스트로 삼아 질문과 함께 모델에 전달한다. 실제로는 답하는 규칙(지시)은 시스템 프롬프트에, 컨텍스트와 질문은 사용자 메시지에 나눠 넣는 경우가 많다.
4. LLM 생성
모델이 그 근거를 바탕으로 최종 답변을 쓴다.
실습 — PDF 문서 기반 RAG
전체 코드는 GitHub 레포에서 볼 수 있다.
앞에서 정리한 내용을 코드를 통해 더 깊게 알아보자. 예시로 회사 취업규칙 PDF를 두고, 직원이 규정을 직접 찾지 않아도 궁금한 걸 물으면 답해주는 사내 Q&A 상황을 만들어 본다. 사용하는 라이브러리는 다음과 같다.
pypdf: PDF에서 텍스트 추출openai: 임베딩과 답변 생성chromadb: 벡터 DB
LangChain 같은 프레임워크를 쓰면 더 간단하지만, 텍스트가 벡터가 되고 검색되는 과정을 직접 보기 위해 여기서는 쓰지 않는다. 생성 모델은 모듈처럼 갈아끼울 수 있어 Claude나 로컬 모델로 바꿔도 된다.
pip install openai chromadb pypdffrom openai import OpenAI
client = OpenAI() # OPENAI_API_KEY 환경변수를 읽는다인덱싱 — 질문이 오기 전에 문서를 벡터 DB에 저장하는 단계다.
1. 문서에서 텍스트 추출
from pypdf import PdfReader
reader = PdfReader("취업규칙.pdf")
text = "\n".join(page.extract_text() or "" for page in reader.pages)PDF에서 페이지별 텍스트를 뽑아 하나로 잇는다. 표나 이미지가 많은 PDF는 추출 품질이 떨어질 수 있어, 이 단계의 결과를 한 번 눈으로 확인해 두는 게 좋다.
표·이미지는 텍스트가 아니라서, 텍스트 임베딩 모델에 넣으려면 표는 Markdown 표로 변환하고 이미지는 OCR이나 비전 모델로 설명을 뽑아 먼저 텍스트로 바꿔야 한다.
2. 텍스트 청킹
def chunk_text(text, chunk_size=500, overlap=50):
chunks, start = [], 0
while start < len(text):
chunks.append(text[start:start + chunk_size])
start += chunk_size - overlap
return chunks
chunks = chunk_text(text, chunk_size=500, overlap=50)chunk_size(조각 하나의 길이): 너무 크면 한 조각에 여러 내용이 섞여 검색이 흐려지고, 너무 작으면 문맥이 끊긴다.overlap(조각 사이 겹침): 조각 경계에서 문장이 잘려 문맥이 끊기는 걸 막으려고, 앞 조각의 끝부분을 다음 조각에 조금 겹쳐 넣는다.
여기서는 글자 수 기준으로 단순하게 잘랐지만, 문장·문단 경계나 의미 단위로 자르는 등 청킹 전략은 다양하다. 어떻게 자르느냐에 따라 검색 품질이 꽤 달라진다.
3. 임베딩과 벡터 DB 저장
import chromadb
# 청크들을 한 번에 임베딩
resp = client.embeddings.create(model="text-embedding-3-small", input=chunks)
embeddings = [d.embedding for d in resp.data]
# 벡터 DB에 벡터와 원문을 함께 저장
db = chromadb.PersistentClient(path="./chroma_db")
collection = db.get_or_create_collection("docs")
collection.add(
ids=[f"chunk-{i}" for i in range(len(chunks))],
embeddings=embeddings,
documents=chunks,
)model="text-embedding-3-small"(임베딩 모델): 각 청크를 1536차원 벡터로 바꾼다. OpenAI 임베딩 모델 중small은large보다 저렴하고 빠르면서 한국어 품질도 충분해 실습에 적합하다. 질의 때도 반드시 같은 모델을 써야 질문과 문서가 같은 공간에 놓인다.documents에 원문도 함께 저장한다. 검색으로 찾은 벡터의 원문을 꺼내 모델에 넘겨야 하기 때문이다.
답변 생성 — 질문이 들어올 때마다 반복되는 단계다.
4. 질문 임베딩과 유사도 검색
question = "연차 휴가는 며칠까지 쓸 수 있어?"
q_emb = client.embeddings.create(
model="text-embedding-3-small", input=question
).data[0].embedding
results = collection.query(query_embeddings=[q_emb], n_results=3)
retrieved = results["documents"][0]질문도 같은 임베딩 모델로 벡터로 바꾼 뒤, 가장 가까운 청크를 찾는다.
n_results(top-k, 가져올 청크 수): 너무 적으면 답에 필요한 근거가 빠지고, 너무 많으면 관련 없는 내용까지 섞여 답이 흐려지고 비용도 는다.
5. 프롬프트 구성과 답변 생성
context = "\n\n".join(retrieved)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "다음 문서를 근거로 질문에 답하세요. 문서에 없는 내용은 모른다고 답하세요."},
{"role": "user", "content": f"[문서]\n{context}\n\n[질문]\n{question}"},
],
temperature=0,
)
print(resp.choices[0].message.content)- 메시지 분리: 답하는 규칙(지시)은
system에, 검색한 컨텍스트와 질문은user에 나눠 넣는다. 하나의 문자열로 합치지 않고 역할별로 나누는 게 일반적이다. - 시스템 프롬프트의 "문서에 없으면 모른다고" 한 줄이 환각을 억제한다. 검색이 엉뚱한 걸 가져왔을 때 지어내지 않고 모른다고 답하게 만든다. 프롬프트 설계는 개선 여지가 큰 부분으로, 출처 인용을 강제하거나 few-shot 예시를 주는 등 다양한 전략이 있다.
temperature=0: 무작위성을 없애 문서에 충실한 답을 내게 한다. 보통 0에서 2 사이 값을 두는데, RAG처럼 사실을 묻는 작업은 0에서 0.3 정도로 낮게, 창의적인 글쓰기는 0.7 이상으로 높인다.
결과
같은 질문을 RAG 없이 LLM에 그냥 물으면, 모델이 PDF 내용을 모르니 일반론을 답하거나 사실과 다른 내용을 지어낸다. 위 파이프라인을 거치면 문서에서 찾은 근거로 정확히 답한다. 저장할 때 metadatas에 페이지 번호를 함께 넣어 두면, 답과 함께 "출처: p.12" 같은 근거까지 보여줄 수 있다.
이 실험은 가장 기본적인 RAG라 결과가 만족스럽지 않을 수 있다. 위에서 다룬 시스템 프롬프트, 청킹 방식(chunk_size, overlap), n_results 같은 값을 바꿔가며 답이 어떻게 달라지는지 직접 실험해 봤으면 좋겠다.
프롬프트 엔지니어링 vs RAG vs 파인튜닝
지난 글에서 파인튜닝을, 이번 글에서 RAG를 다뤘다. 여기에 프롬프트 엔지니어링까지, 세 방법은 서로 다른 문제를 푸는 도구라 하나만 골라야 하는 구조가 아니다.
- 프롬프트 엔지니어링은 모델도 데이터도 건드리지 않고, 지시와 예시를 잘 써서 원하는 답을 끌어낸다.
- RAG는 모델이 쓸 지식을 외부에서 가져다 준다. 가중치는 그대로 두고, 검색한 문서를 그때그때 참고하게 한다.
- 파인튜닝은 모델의 행동을 바꾼다. 가중치를 학습시켜 말투·출력 형식·특정 작업 수행을 강화하는 기법이다.
셋은 다루는 영역 자체가 다르다.
| 프롬프트 엔지니어링 | RAG | 파인튜닝 | |
|---|---|---|---|
| 하는 일 | 지시·예시로 답 유도 | 외부 지식 주입 | 행동·스타일 학습 |
| 지식 갱신 | 매번 직접 | 문서 교체 | 재학습 |
| 비용·데이터 | 거의 없음 | 낮음·문서 | 높음·수백에서 수천 예시 |
| 출처 제시 | 어려움 | 가능 | 불가 |
| 약점 | 컨텍스트 한계 | 검색 품질에 좌우 | 구체적 사실은 지어냄 |
그래서 한쪽이 다른 쪽을 대신하지 못한다. 파인튜닝으로 새로운 지식을 외우게 하려 하면 모델이 없는 내용을 그럴듯하게 지어내는 환각이 생기고, RAG로는 말투나 형식을 바꿀 수 없다. 지식이 필요한 일은 RAG가, 모델의 행동을 바꾸는 일은 파인튜닝이 맡는다.
그래서 실제 서비스에서는 이 방법들을 섞어 쓴다. RAG로 최신 지식을 제공하고, 파인튜닝으로 말투·형식을 고정하는 식이다. 보통은 프롬프트 엔지니어링으로 시작해, 부족하면 RAG, 그래도 부족하면 파인튜닝을 더하는 순서로 간다. 간단하고 비용이 적은 방법부터 써 보고, 모자랄 때만 더 무겁고 비싼 방법으로 올라가는 게 일반적이기 때문이다.
마무리
이번 글에서는 RAG가 무엇이고 왜 필요한지, 인덱싱과 답변 생성 두 단계로 어떻게 동작하는지, PDF 문서로 직접 RAG를 만들어 보는 실습까지 다뤘다. 가중치를 건드리지 않고 외부 문서를 검색해 답에 쓰는 RAG는 자주 바뀌는 지식이나 내 문서를 다뤄야 할 때 현실적인 선택지다. 여기서 만든 건 기본 형태일 뿐, 청킹·하이브리드 검색·리랭킹 같은 고도화 여지가 많다. 실제로도 사내 문서 챗봇, 제품 매뉴얼 기반 고객 지원, 검색형 AI 어시스턴트처럼 '모델이 모르는 내 데이터'를 다뤄야 하는 곳에 널리 쓰인다.
PI Lab은 모든 과정을 페어 프로그래밍으로 진행한다. 이번 Sprint2의 RAG 학습도 셋이 함께 하면서, 궁금한 게 생길 때마다 같이 찾아보고 이해가 부족한 부분은 서로 설명하고 조사해주며 개념을 다졌는데, 혼자였다면 놓쳤을 부분까지 메울 수 있어 정말 좋았다.
이 글은 사실관계나 해석에 오류가 있을 수 있습니다. 잘못된 내용이나 질문이 있으면 댓글로 편하게 남겨 주세요.
Reference
- Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (NeurIPS 2020) — RAG가 처음 제안된 논문
- Izacard & Grave, Leveraging Passage Retrieval with Generative Models for Open Domain Question Answering (EACL 2021) — 여러 문서를 디코더에서 함께 읽는 FiD
- In Defense of RAG in the Era of Long-Context Language Models (2024) — 컨텍스트가 길어진 시대에도 RAG가 필요한지에 대한 논의
- RAG vs Fine-tuning: Pipelines, Tradeoffs, and a Case Study on Agriculture (2024) — 파인튜닝과 RAG를 직접 비교한 사례 연구
- IBM, RAG vs fine-tuning vs prompt engineering — 세 방법의 선택 기준 정리