1일차에서 LLM의 네 가지 한계 중 두 가지로 지식 컷오프와 할루시네이션을 다뤘습니다. 모델은 학습 시점 이후의 정보를 알 수 없고, 모르는 내용을 그럴듯하게 지어내는 문제가 있습니다. RAG는 이 두 가지 한계를 정면으로 겨냥한 아키텍처입니다. 모델의 가중치를 건드리지 않고, 외부 문서를 검색해서 컨텍스트로 주입하는 방식으로 최신 정보와 검증된 정보를 답변에 반영합니다. 2020년 Lewis et al. 논문에서 처음 제안된 이후 Naive RAG에서 Advanced RAG, Modular RAG, Agentic RAG로 빠르게 진화하고 있는 이 구조를 전체적으로 정리합니다. 🤖
1. RAG의 개념
1.1 RAG의 정의
RAG(Retrieval-Augmented Generation)는 LLM의 텍스트 생성 과정에 외부 정보 검색을 결합한 아키텍처 패턴입니다. 이름 그대로 검색(Retrieval)으로 정보를 가져오고, 그 정보로 입력을 증강(Augmented)한 뒤, 생성(Generation)하는 세 단계로 구성됩니다.
LLM은 학습 데이터에 없는 정보를 알 수 없고, 모른다고 솔직하게 답하는 대신 그럴듯한 답을 지어내는 경향이 있습니다. RAG는 모델에게 "이 문서를 참고해서 답해"라는 방식으로 외부 메모리를 제공합니다. 모델의 가중치를 변경하는 파인튜닝과 달리, RAG는 모델을 그대로 두고 입력을 보강하는 방식이라 운영 비용이 낮고 정보 갱신이 유연합니다. 사내 문서 기반 QA 시스템을 만든다면 팀이 새 문서를 추가할 때마다 모델을 다시 학습시킬 필요 없이 벡터DB만 갱신하면 됩니다.
RAG는 모델이 "무엇을 아는가"를 바꾸는 게 아니라, "무엇을 보고 답하는가"를 바꾸는 방식입니다.

1.2 파인튜닝과의 차이
RAG와 파인튜닝은 LLM의 한계를 보완하는 두 가지 접근 방식입니다. 언제 어떤 방식을 선택할지는 "정보를 어떻게 모델에 주입하는가"의 방향이 다릅니다.

| 구분 | RAG | 파인튜닝 |
|---|---|---|
| 원리 | 외부 문서를 검색해서 컨텍스트로 주입 | 추가 데이터로 모델 가중치를 재학습 |
| 정보 갱신 | 문서 DB만 업데이트하면 됨 | 재학습 필요 |
| 비용 | 상대적으로 낮음 | GPU 비용 큼 |
| 할루시네이션 억제 | 출처 문서 기반이라 억제 효과 큼 | 여전히 발생 가능 |
| 적합한 용도 | 최신 정보, 사내 문서 기반 답변 | 특정 말투·도메인 스타일 학습 |
실무에서는 두 방식을 함께 쓰는 경우도 있습니다. 도메인 특화 어투나 응답 형식은 파인튜닝으로 학습시키고, 최신 정보나 사내 문서는 RAG로 주입하는 방식입니다. 리소스 제약이 있는 상황에서는 정보 정확도가 더 중요한 경우 RAG가 ROI가 높은 출발점입니다.
2. RAG 파이프라인 구조
2.1 전체 흐름
RAG 파이프라인은 크게 인덱싱(Indexing)과 검색-생성(Retrieval-Generation) 두 단계로 나뉩니다. 인덱싱은 사전 작업이고, 검색-생성은 사용자 질문이 들어올 때마다 실시간으로 일어납니다.


인덱싱 단계는 세 단계로 나뉩니다. 외부 문서를 일정 크기의 청크로 나누고(청킹), 각 청크를 임베딩 모델을 통해 숫자 벡터로 변환합니다. 이 벡터를 벡터DB에 저장해두면 인덱싱이 완료됩니다. 검색-생성 단계는 사용자 쿼리가 들어오면 동일한 임베딩 모델로 벡터화하고, 벡터DB에서 코사인 유사도로 가장 관련 있는 청크 k개를 찾아 LLM 프롬프트에 컨텍스트로 주입한 뒤 답변을 생성합니다.
2.2 임베딩과 벡터 유사도
RAG에서 "관련 문서를 찾는다"는 게 구체적으로 어떻게 동작하는지 이해하려면 임베딩(embedding)을 알아야 합니다. 임베딩은 텍스트를 고차원의 숫자 벡터로 변환하는 과정입니다. 의미가 비슷한 문장은 벡터 공간에서 가까운 위치에 놓이고, 의미가 다른 문장은 멀리 떨어지는 구조입니다.

"사과 가격이 얼마예요?"와 "과일 시장의 Apple 단가는?"은 표현은 다르지만 의미가 비슷하기 때문에 임베딩 공간에서 가까운 위치에 놓입니다. RAG는 사용자 쿼리를 임베딩으로 변환한 뒤, 벡터DB에 저장된 문서 청크들의 임베딩과 코사인 유사도를 계산해서 가장 가까운 k개의 청크를 가져옵니다. 키워드가 일치하지 않아도 의미적으로 관련 있는 문서를 찾을 수 있다는 게 임베딩 기반 검색의 핵심입니다.
임베딩 모델은 RAG 파이프라인에서 검색 품질을 결정하는 가장 중요한 요소 중 하나입니다. LLM 모델과 임베딩 모델은 별개이기 때문에, 생성에는 GPT-4o를 쓰더라도 임베딩에는 OpenAI의 text-embedding-3-small이나 한국어에 강한 별도 임베딩 모델을 따로 사용합니다. 한국어 문서 기반 RAG를 구축한다면 한국어 임베딩 품질을 반드시 별도로 평가해야 합니다.
3. 청킹 전략
3.1 청킹의 원리
청킹(chunking)은 긴 문서를 모델이 처리할 수 있는 작은 조각으로 나누는 작업입니다. 2일차에서 다룬 컨텍스트 윈도우 한계 때문에 문서 전체를 그대로 넣을 수 없고, 관련 있는 부분만 골라서 넣어야 합니다. 이 "부분"이 청크(chunk)입니다.

청크 크기는 검색 품질에 직접 영향을 줍니다. 청크가 너무 크면 관련 없는 내용이 같이 들어가서 모델이 핵심을 잡기 어렵고, 너무 작으면 문맥이 잘려서 의미를 파악하기 어렵습니다. 일반적으로 chunk_size를 500~1000 토큰, chunk_overlap을 100~200 토큰으로 설정하는 것이 시작점으로 많이 쓰입니다. chunk_overlap은 청크가 잘리는 경계에서 문맥이 유실되는 걸 방지하기 위해 앞 청크의 끝부분을 다음 청크의 시작 부분과 겹치게 하는 설정입니다.
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ".", " "]
)
chunks = text_splitter.split_documents(documents)
print(f"총 청크 수: {len(chunks)}")
separators는 어떤 기준으로 문서를 분할할지 우선순위를 정합니다. 단락(\n\n) → 줄바꿈(\n) → 마침표(.) → 공백 순으로 시도하면서, 청크 크기를 넘지 않는 가장 자연스러운 분할 지점을 찾습니다.
3.2 청킹 전략 비교
청킹 방식은 문서의 성격에 따라 다르게 선택해야 합니다.
| 전략 | 특징 | 적합한 문서 |
|---|---|---|
| 고정 크기 | N 토큰마다 자름 | 구조가 없는 긴 텍스트 |
| 재귀 분할 | 단락→문장→단어 순으로 자름 | 일반 문서, 보고서 |
| 마크다운/HTML 기반 | 헤더·태그 구조를 기준으로 자름 | 기술 문서, 위키 |
| 시맨틱 청킹 | 의미 변화 지점을 감지해서 자름 | 다양한 주제가 섞인 긴 문서 |
청킹 전략은 "한 번 정하면 끝"이 아닙니다. 같은 문서라도 chunk_size를 500으로 했을 때와 1000으로 했을 때 검색 품질이 달라질 수 있습니다. 프로덕션에 넣기 전에 다양한 쿼리로 검색 결과를 직접 확인하고, 청크 크기와 overlap을 조정하는 실험 과정이 필요합니다.
4. RAG의 진화: Naive → Advanced → Modular
4.1 Naive RAG의 구조와 한계
2020년 처음 제안된 RAG는 지금 기준으로 Naive RAG라고 부릅니다. 문서를 청킹하고 임베딩해서 벡터DB에 저장한 뒤, 쿼리가 들어오면 코사인 유사도로 청크를 검색해서 LLM에 넣는 구조입니다. 빠르게 구축할 수 있지만 몇 가지 명확한 한계가 있습니다.
검색 정확도가 낮을 수 있습니다. 사용자가 "Q3 매출이 왜 떨어졌어?"라고 물으면, 시맨틱 검색만으로는 "3분기 수익 감소 원인"이라는 표현의 문서를 놓칠 수 있습니다. 검색된 청크가 관련은 있지만 핵심 정보가 아닌 경우도 많고, 같은 정보가 여러 청크에 중복으로 들어가면 컨텍스트 윈도우를 낭비하는 문제도 생깁니다.

4.2 Advanced RAG — 쿼리·검색 최적화
Naive RAG의 한계를 보완하기 위해 검색 전후 단계를 강화한 방식이 Advanced RAG입니다. 크게 사전 검색(pre-retrieval) 최적화와 사후 검색(post-retrieval) 최적화로 나뉩니다.

사전 검색 최적화의 핵심은 쿼리를 더 좋게 만드는 것입니다. 대표적인 기법이 쿼리 확장(query expansion)인데, "Q3 매출 하락 원인"이라는 쿼리를 "3분기 수익 감소 이유", "Q3 revenue decline cause" 등 여러 버전으로 확장해서 각각 검색하고 결과를 합치는 방식입니다. 쿼리 리라이팅(query rewriting)은 LLM이 사용자의 질문을 검색에 더 적합한 표현으로 바꾸는 방식입니다.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
query_expansion_prompt = ChatPromptTemplate.from_template("""
다음 질문을 검색에 최적화된 3가지 다른 표현으로 바꿔줘.
줄바꿈으로 구분해서 출력해.
원본 질문: {question}
""")
llm = ChatOpenAI(model="gpt-4o", temperature=0)
query_expander = query_expansion_prompt | llm | StrOutputParser()
expanded = query_expander.invoke({"question": "Q3 매출이 왜 떨어졌어?"})
print(expanded)
# → "3분기 매출 감소 원인 분석"
# → "Q3 revenue decline root cause"
# → "3사분기 실적 하락 이유"
사후 검색 최적화의 핵심은 검색된 청크를 더 잘 선별하는 것입니다. 벡터 유사도로 검색된 상위 k개를 리랭킹(reranking) 모델로 한 번 더 정렬합니다. Cross-encoder 방식이 주로 쓰이는데, 쿼리와 청크를 함께 입력해서 관련도를 더 정밀하게 계산합니다. 검색 결과를 먼저 좁힌 다음 리랭킹을 적용하는 투 스테이지 구조로 씁니다.
2025~2026년 기준으로 하이브리드 검색(키워드 BM25 + 시맨틱 임베딩 결합)은 프로덕션 RAG의 사실상 표준으로 자리잡았습니다. 고유명사, 코드명, 약어처럼 임베딩이 잘 못 잡는 쿼리는 BM25가 보완해주기 때문입니다. LangChain의 EnsembleRetriever, LlamaIndex의 QueryFusionRetriever가 이 패턴을 기본 지원합니다.
4.3 Modular RAG와 Agentic RAG
가장 최근의 패러다임은 Modular RAG입니다. Naive RAG가 고정된 파이프라인이었다면, Modular RAG는 인덱싱·검색·생성 각 단계를 독립적인 모듈로 분리해서 필요에 따라 교체하거나 조합할 수 있는 구조입니다. 레고 블록처럼 조립 가능한 프레임워크라고 볼 수 있습니다.
여기서 더 나아간 것이 Agentic RAG입니다. 검색 필요 여부를 모델이 스스로 판단하고, 필요하면 검색을 반복적으로 수행하며, 여러 검색 결과를 종합해서 다단계 추론을 하는 방식입니다. "A 지표와 B 지표 중 어느 쪽이 더 중요한지 분석해줘"라는 복합 질문에 Naive RAG는 한 번의 검색으로 부족한 컨텍스트를 가져오지만, Agentic RAG는 A 문서 검색 → B 문서 검색 → 두 결과 비교 분석 순서로 자율적으로 처리합니다.
청킹 → 임베딩 → 유사도 검색 → 생성. 단순하고 빠르지만 검색 정확도에 한계
쿼리 최적화 + 하이브리드 검색 + 리랭킹. 프로덕션 표준으로 자리잡는 중
검색 필요 여부 자율 판단 + 반복 검색 + 다단계 추론. 복잡한 업무 자동화에 적합
5. RAG 파이프라인 구현
5.1 기본 구현 코드
LangChain을 사용한 Advanced RAG 수준의 파이프라인입니다. 하이브리드 검색(EnsembleRetriever)까지 포함한 구현입니다.
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# 1. 청킹
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200
)
chunks = text_splitter.split_documents(documents)
# 2. 시맨틱 검색 (임베딩 + 벡터DB)
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(chunks, embeddings)
semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# 3. 키워드 검색 (BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
# 4. 하이브리드 검색 (시맨틱 0.6 + 키워드 0.4)
ensemble_retriever = EnsembleRetriever(
retrievers=[semantic_retriever, bm25_retriever],
weights=[0.6, 0.4]
)
# 5. 프롬프트 + 체인
prompt = ChatPromptTemplate.from_template("""
다음 컨텍스트를 참고해서 질문에 답하세요.
컨텍스트에 없는 내용은 모른다고 답하세요.
컨텍스트:
{context}
질문: {question}
""")
llm = ChatOpenAI(model="gpt-4o", temperature=0)
rag_chain = (
{"context": ensemble_retriever, "question": RunnablePassthrough()}
| prompt | llm | StrOutputParser()
)
response = rag_chain.invoke("Q3 매출 하락의 주요 원인은 무엇인가요?")
print(response)
EnsembleRetriever의 weights 파라미터로 시맨틱 검색과 키워드 검색의 비중을 조정합니다. 문서가 정형화된 보고서 형식이라면 BM25 비중을 높이고, 자연어 질문이 많은 경우라면 시맨틱 비중을 높이는 식으로 튜닝합니다.
5.2 RAG 품질 평가
RAG를 프로덕션에 넣기 전에 품질을 측정하는 방법이 필요합니다. 대표적으로 RAGAS 프레임워크가 쓰입니다. RAGAS는 RAG 파이프라인을 네 가지 지표로 평가합니다.
| 지표 | 측정 대상 | 의미 |
|---|---|---|
| Faithfulness | 답변이 컨텍스트에 근거하는가 | 할루시네이션 억제 수준 |
| Answer Relevance | 답변이 질문에 얼마나 관련 있는가 | 답변 품질 |
| Context Precision | 검색된 청크가 얼마나 정확한가 | 검색 정밀도 |
| Context Recall | 필요한 정보가 빠짐없이 검색됐는가 | 검색 재현율 |
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
from datasets import Dataset
eval_data = {
"question": ["Q3 매출 하락의 원인은 무엇인가요?"],
"answer": [response],
"contexts": [retrieved_chunks],
"ground_truth": ["실제 정답"]
}
dataset = Dataset.from_dict(eval_data)
result = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall]
)
print(result)
RAGAS 점수를 기준으로 파이프라인을 개선합니다. Faithfulness가 낮으면 프롬프트에 "컨텍스트에 없는 내용은 답하지 말 것" 지시를 강화하고, Context Recall이 낮으면 청킹 전략이나 검색 k값을 조정합니다. 평가 → 개선 → 재평가 사이클을 반복하는 것이 RAG 최적화의 기본 흐름입니다.
6. RAG의 한계와 적용 기준
6.1 RAG가 해결하지 못하는 문제
RAG가 만능은 아닙니다. 몇 가지 구조적인 한계가 있습니다.
첫째, 검색 자체가 실패하면 답변도 실패합니다. 쿼리와 관련 있는 청크가 검색되지 않으면 모델은 컨텍스트 없이 답해야 하고, 이때 다시 할루시네이션 문제가 발생합니다. 청킹 전략이 잘못됐거나, 임베딩 품질이 낮거나, k값이 너무 작을 때 이런 상황이 생깁니다. 둘째, lost in the middle 문제가 있습니다. 컨텍스트가 길어질수록 모델이 중간 부분을 제대로 참조하지 못하는 경향이 있어서, k값을 무작정 키우는 게 항상 좋은 건 아닙니다. 셋째, 문서 자체가 틀렸거나 오래된 정보라면 RAG도 틀린 답을 냅니다. 외부 메모리의 품질이 곧 답변 품질의 상한선입니다.
6.2 RAG 도입 판단 기준
| 상황 | RAG 적합도 | 이유 |
|---|---|---|
| 사내 문서 기반 QA | ✅ 높음 | 모델이 알 수 없는 사내 정보를 주입 |
| 최신 뉴스 요약 | ✅ 높음 | 지식 컷오프 이후 정보 활용 가능 |
| 수치 기반 정밀 계산 | ⚠️ 주의 | 검색 오류 시 잘못된 수치 인용 위험 |
| 일반 상식 질문 | ❌ 낮음 | 모델이 이미 알고 있는 정보 |
| 창작·아이디어 생성 | ❌ 낮음 | 외부 정보보다 창의성이 중요 |
데이터 분석 업무에서 RAG가 유용한 시나리오는 명확합니다. 사내 데이터 사전(컬럼 정의, 지표 계산 방식)을 문서로 만들어 RAG로 연결하면, "이 지표는 어떻게 계산하는 거야?"라는 질문에 사내 기준에 맞는 정확한 답을 받을 수 있습니다. RAG의 진화 방향인 Agentic RAG까지 연결하면, 분석 에이전트가 필요한 데이터를 스스로 찾아오고 → 비교 분석하고 → 인사이트를 도출하는 자동화 파이프라인으로 확장됩니다.
참고 자료
- Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (2020): https://arxiv.org/abs/2005.11401
- Gao et al., RAG for Large Language Models: A Survey (2023): https://arxiv.org/abs/2312.10997
- LangChain, RAG Tutorial: https://python.langchain.com/docs/tutorials/rag/
- Zilliz, Naive vs Advanced vs Modular RAG: https://zilliz.com/blog/advancing-llms-native-advanced-modular-rag-approaches
- RAGAS, RAG Evaluation Framework: https://docs.ragas.io
'AI > LLM' 카테고리의 다른 글
| [LLM] 프롬프트 엔지니어링이란 (0) | 2026.06.19 |
|---|---|
| [LLM] 토큰, 컨텍스트, temperature (1) | 2026.06.16 |
| [LLM] Large Language Model이란 (1) | 2026.06.12 |
HELLO WORLD
안녕하세요. 데이터로 말하는 분석가 모모입니다.
데이터를 구조화하고 분석하는 과정과 실무에 활용되는 도구 중심의 내용을 기록합니다.